[
  {
    "path": ".ccignore",
    "content": ".pre-commit-config.yaml\n.github/\ndocs/changelog.mdx\ndocs/python-sdk/\nexamples/\nsrc/fastmcp/contrib/\ntests/contrib/\n"
  },
  {
    "path": ".claude/hooks/session-init.sh",
    "content": "#!/bin/bash\nset -e\n\n# Only run in remote/cloud environments\nif [ \"$CLAUDE_CODE_REMOTE\" != \"true\" ]; then\n  exit 0\nfi\n\ncommand -v gh &> /dev/null && exit 0\n\nLOCAL_BIN=\"$HOME/.local/bin\"\nmkdir -p \"$LOCAL_BIN\"\n\nARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')\nVERSION=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | grep '\"tag_name\"' | cut -d'\"' -f4)\nTARBALL=\"gh_${VERSION#v}_linux_${ARCH}.tar.gz\"\n\necho \"Installing gh ${VERSION}...\"\nTEMP=$(mktemp -d)\ntrap 'rm -rf \"$TEMP\"' EXIT\ncurl -fsSL \"https://github.com/cli/cli/releases/download/${VERSION}/${TARBALL}\" | tar -xz -C \"$TEMP\"\ncp \"$TEMP\"/gh_*/bin/gh \"$LOCAL_BIN/gh\"\nchmod 755 \"$LOCAL_BIN/gh\"\n\n[ -n \"$CLAUDE_ENV_FILE\" ] && echo \"export PATH=\\\"$LOCAL_BIN:\\$PATH\\\"\" >> \"$CLAUDE_ENV_FILE\"\necho \"gh installed: $(\"$LOCAL_BIN/gh\" --version | head -1)\"\n"
  },
  {
    "path": ".claude/settings.json",
    "content": "{\n  \"hooks\": {\n    \"SessionStart\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"bash \\\"$CLAUDE_PROJECT_DIR\\\"/.claude/hooks/session-init.sh\",\n            \"timeout\": 120\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": ".claude/skills/code-review/SKILL.md",
    "content": "---\nname: reviewing-code\ndescription: Review code for quality, maintainability, and correctness. Use when reviewing pull requests, evaluating code changes, or providing feedback on implementations. Focuses on API design, patterns, and actionable feedback.\n---\n\n# Code Review\n\n## Philosophy\n\nCode review maintains a healthy codebase while helping contributors succeed. The burden of proof is on the PR to demonstrate it adds value. Your job is to help it get there through actionable feedback.\n\n**Critical**: A perfectly written PR that adds unwanted functionality must still be rejected. The code must advance the codebase in the intended direction. When rejecting, provide clear guidance on how to align with project goals.\n\nBe friendly and welcoming while maintaining high standards. Call out what works well. When code needs improvement, be specific about why and how to fix it.\n\n## What to Focus On\n\n### Does this advance the codebase correctly?\n\nEven perfect code for unwanted features should be rejected.\n\n### Dependency version compatibility\n\nWhen a PR adapts code to a new version of a dependency (e.g., removing a parameter that was dropped upstream, using a new API):\n- **The version pin in `pyproject.toml` must match.** If the change breaks compatibility with the previously-pinned minimum version, the minimum version must be bumped. Otherwise users on the old version get a regression.\n- **If backwards compatibility with the old version is desired**, the code must handle both versions (e.g., try/except, version check). Simply deleting the old API usage without bumping the pin is always wrong — it silently breaks users on the old version.\n- **Lock file (`uv.lock`) changes should be scoped to the PR's purpose.** A PR fixing a ty compatibility issue should not also include unrelated dependency version bumps (anthropic, google-auth, etc.) from running `uv sync --upgrade`. These create noise and make the diff harder to review.\n\n### API design and naming\n\nIdentify confusing patterns or non-idiomatic code:\n- Parameter values that contradict defaults\n- Mutable default arguments\n- Unclear naming that will confuse future readers\n- Inconsistent patterns with the rest of the codebase\n\n### Specific improvements\n\nProvide actionable feedback, not generic observations.\n\n### User ergonomics\n\nThink about the API from a user's perspective. Is it intuitive? What's the learning curve?\n\n## For Agent Reviewers\n\n1. **Read the full context**: Examine related files, tests, and documentation before reviewing\n2. **Check against established patterns**: Look for consistency with codebase conventions\n3. **Verify functionality claims**: Understand what the code actually does, not just what it claims\n4. **Consider edge cases**: Think through error conditions and boundary scenarios\n\n## What to Avoid\n\n- Generic feedback without specifics\n- Hypothetical problems unlikely to occur\n- Nitpicking organizational choices without strong reason\n- Summarizing what the PR already describes\n- Star ratings or excessive emojis\n- Bikeshedding style preferences when functionality is correct\n- Requesting changes without suggesting solutions\n- Focusing on personal coding style over project conventions\n\n## Tone\n\n- Acknowledge good decisions: \"This API design is clean\"\n- Be direct but respectful\n- Explain impact: \"This will confuse users because...\"\n- Remember: Someone else maintains this code forever\n\n## Decision Framework\n\nBefore approving, ask:\n\n1. Does this PR achieve its stated purpose?\n2. Is that purpose aligned with where the codebase should go?\n3. Would I be comfortable maintaining this code?\n4. Have I actually understood what it does, not just what it claims?\n5. Does this change introduce technical debt?\n\nIf something needs work, your review should help it get there through specific, actionable feedback. If it's solving the wrong problem, say so clearly.\n\n## Comment Examples\n\n**Good comments:**\n\n| Instead of | Write |\n|------------|-------|\n| \"Add more tests\" | \"The `handle_timeout` method needs tests for the edge case where timeout=0\" |\n| \"This API is confusing\" | \"The parameter name `data` is ambiguous - consider `message_content` to match the MCP specification\" |\n| \"This could be better\" | \"This approach works but creates a circular dependency. Consider moving the validation to `utils/validators.py`\" |\n\n## Checklist\n\nBefore approving, verify:\n\n- [ ] All required development workflow steps completed (uv sync, prek, pytest)\n- [ ] Changes align with repository patterns and conventions\n- [ ] API changes are documented and backwards-compatible where possible\n- [ ] Error handling follows project patterns (specific exception types)\n- [ ] Tests cover new functionality and edge cases\n- [ ] The change advances the codebase in the intended direction\n"
  },
  {
    "path": ".claude/skills/python-tests/SKILL.md",
    "content": "---\nname: testing-python\ndescription: Write and evaluate effective Python tests using pytest. Use when writing tests, reviewing test code, debugging test failures, or improving test coverage. Covers test design, fixtures, parameterization, mocking, and async testing.\n---\n\n# Writing Effective Python Tests\n\n## Core Principles\n\nEvery test should be **atomic**, **self-contained**, and test **single functionality**. A test that tests multiple things is harder to debug and maintain.\n\n## Test Structure\n\n### Atomic unit tests\n\nEach test should verify a single behavior. The test name should tell you what's broken when it fails. Multiple assertions are fine when they all verify the same behavior.\n\n```python\n# Good: Name tells you what's broken\ndef test_user_creation_sets_defaults():\n    user = User(name=\"Alice\")\n    assert user.role == \"member\"\n    assert user.id is not None\n    assert user.created_at is not None\n\n# Bad: If this fails, what behavior is broken?\ndef test_user():\n    user = User(name=\"Alice\")\n    assert user.role == \"member\"\n    user.promote()\n    assert user.role == \"admin\"\n    assert user.can_delete_others()\n```\n\n### Use parameterization for variations of the same concept\n\n```python\nimport pytest\n\n@pytest.mark.parametrize(\"input,expected\", [\n    (\"hello\", \"HELLO\"),\n    (\"World\", \"WORLD\"),\n    (\"\", \"\"),\n    (\"123\", \"123\"),\n])\ndef test_uppercase_conversion(input, expected):\n    assert input.upper() == expected\n```\n\n### Use separate tests for different functionality\n\nDon't parameterize unrelated behaviors. If the test logic differs, write separate tests.\n\n## Project-Specific Rules\n\n### No async markers needed\n\nThis project uses `asyncio_mode = \"auto\"` globally. Write async tests without decorators:\n\n```python\n# Correct\nasync def test_async_operation():\n    result = await some_async_function()\n    assert result == expected\n\n# Wrong - don't add this\n@pytest.mark.asyncio\nasync def test_async_operation():\n    ...\n```\n\n### Imports at module level\n\nPut ALL imports at the top of the file:\n\n```python\n# Correct\nimport pytest\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\nasync def test_something():\n    mcp = FastMCP(\"test\")\n    ...\n\n# Wrong - no local imports\nasync def test_something():\n    from fastmcp import FastMCP  # Don't do this\n    ...\n```\n\n### Use in-memory transport for testing\n\nPass FastMCP servers directly to clients:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\nmcp = FastMCP(\"TestServer\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nasync def test_greet_tool():\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        assert result[0].text == \"Hello, World!\"\n```\n\nOnly use HTTP transport when explicitly testing network features.\n\n### Inline snapshots for complex data\n\nUse `inline-snapshot` for testing JSON schemas and complex structures:\n\n```python\nfrom inline_snapshot import snapshot\n\ndef test_schema_generation():\n    schema = generate_schema(MyModel)\n    assert schema == snapshot()  # Will auto-populate on first run\n```\n\nCommands:\n- `pytest --inline-snapshot=create` - populate empty snapshots\n- `pytest --inline-snapshot=fix` - update after intentional changes\n\n## Fixtures\n\n### Prefer function-scoped fixtures\n\n```python\n@pytest.fixture\ndef client():\n    return Client()\n\nasync def test_with_client(client):\n    result = await client.ping()\n    assert result is not None\n```\n\n### Use `tmp_path` for file operations\n\n```python\ndef test_file_writing(tmp_path):\n    file = tmp_path / \"test.txt\"\n    file.write_text(\"content\")\n    assert file.read_text() == \"content\"\n```\n\n## Mocking\n\n### Mock at the boundary\n\n```python\nfrom unittest.mock import patch, AsyncMock\n\nasync def test_external_api_call():\n    with patch(\"mymodule.external_client.fetch\", new_callable=AsyncMock) as mock:\n        mock.return_value = {\"data\": \"test\"}\n        result = await my_function()\n        assert result == {\"data\": \"test\"}\n```\n\n### Don't mock what you own\n\nTest your code with real implementations when possible. Mock external services, not internal classes.\n\n## Test Naming\n\nUse descriptive names that explain the scenario:\n\n```python\n# Good\ndef test_login_fails_with_invalid_password():\ndef test_user_can_update_own_profile():\ndef test_admin_can_delete_any_user():\n\n# Bad\ndef test_login():\ndef test_update():\ndef test_delete():\n```\n\n## Error Testing\n\n```python\nimport pytest\n\ndef test_raises_on_invalid_input():\n    with pytest.raises(ValueError, match=\"must be positive\"):\n        calculate(-1)\n\nasync def test_async_raises():\n    with pytest.raises(ConnectionError):\n        await connect_to_invalid_host()\n```\n\n## Running Tests\n\n```bash\nuv run pytest -n auto              # Run all tests in parallel\nuv run pytest -n auto -x           # Stop on first failure\nuv run pytest path/to/test.py      # Run specific file\nuv run pytest -k \"test_name\"       # Run tests matching pattern\nuv run pytest -m \"not integration\" # Exclude integration tests\n```\n\n## Checklist\n\nBefore submitting tests:\n- [ ] Each test tests one thing\n- [ ] No `@pytest.mark.asyncio` decorators\n- [ ] Imports at module level\n- [ ] Descriptive test names\n- [ ] Using in-memory transport (not HTTP) unless testing networking\n- [ ] Parameterization for variations of same behavior\n- [ ] Separate tests for different behaviors\n"
  },
  {
    "path": ".claude/skills/review-pr/SKILL.md",
    "content": "---\nname: review-pr\ndescription: Monitor and respond to automated PR reviews (Codex bot). Use when pushing a PR, checking review status, or responding to bot feedback. Handles the full cycle of push -> wait for review -> evaluate comments -> fix -> re-push.\n---\n\n# PR Review Workflow\n\nThis repo has `chatgpt-codex-connector[bot]` configured as an automated reviewer. After every push to a PR branch, Codex reviews the diff and either:\n- Reacts with a thumbs-up on its review body (no suggestions — PR is clean)\n- Posts inline comments with suggestions (each tagged with a priority badge)\n\n## Checking review status\n\nAfter pushing, check whether Codex has reviewed the latest commit:\n\n```bash\n# Get the latest commit SHA on the branch\nLATEST=$(git rev-parse HEAD)\n\n# Check if Codex has reviewed that specific commit\ngh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/reviews \\\n  | jq \"[.[] | select(.user.login == \\\"chatgpt-codex-connector[bot]\\\" and .commit_id == \\\"$LATEST\\\")] | length\"\n```\n\nIf the count is 0, Codex hasn't reviewed the latest push yet. Wait and check again.\n\nIf the count is > 0, check for inline comments on the latest review:\n\n```bash\n# Get the review body to check for thumbs-up\ngh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/reviews \\\n  | jq '[.[] | select(.user.login == \"chatgpt-codex-connector[bot]\") | {state, body: .body[:300], commit_id: .commit_id}] | last'\n```\n\nA clean review from Codex looks like a review body that contains a thumbs-up reaction or says \"no suggestions.\" If the body contains \"Here are some automated review suggestions,\" there are inline comments to evaluate.\n\n## Evaluating Codex comments\n\nFetch all inline comments from Codex:\n\n```bash\ngh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/comments \\\n  | jq '[.[] | select(.user.login == \"chatgpt-codex-connector[bot]\") | {body, path, line, created_at}]'\n```\n\nCodex comments include priority badges:\n- `P0` (red) — Critical issue, likely a real bug\n- `P1` (orange) — Important, worth fixing\n- `P2` (yellow) — Moderate, evaluate on merit\n\n**How to evaluate Codex comments:**\n\n1. **Treat Codex as a competent but sometimes overzealous reviewer.** It catches real bugs (cache eviction ordering, silent data loss, missing validation) but also suggests scope expansions and hypothetical improvements.\n\n2. **Fix real bugs** — issues in code you actually changed where behavior is incorrect or data is silently lost.\n\n3. **Dismiss scope expansion** — if a comment points out a pre-existing limitation unrelated to your diff, note it as a potential follow-up but don't block the PR.\n\n4. **Dismiss speculative concerns** — if a comment describes a scenario that requires very specific conditions and the existing behavior is acceptable, dismiss it.\n\n5. **When fixing, be proactive** — if Codex found one instance of a pattern bug (e.g., missing role validation in one handler), check all similar code paths before pushing. Codex will find the next instance on the next review cycle, so get ahead of it.\n\n## Responding to every comment\n\n**Every Codex comment must get a visible response** — either a fix or a reply explaining why it was dismissed. The maintainer can't see your reasoning otherwise.\n\n- **If fixing**: The fix itself is the response. No reply needed unless the fix is non-obvious.\n- **If dismissing**: Reply to the comment thread with a brief explanation of why. Keep it to 1-2 sentences. Examples:\n  - \"This is pre-existing behavior unrelated to this diff — the scope lookup fallback existed before caching was added. Worth a follow-up issue but not blocking this PR.\"\n  - \"The AsyncExitStack handles cleanup when the session exits, so the subprocess isn't leaked — just kept alive slightly longer than necessary in this edge case.\"\n  - \"Gemini supports a much wider range of media types than OpenAI/Anthropic, so a restrictive allowlist would be inaccurate here.\"\n\nUse `gh api` to reply (note: use `in_reply_to`, not a `/replies` sub-path):\n\n```bash\n# Reply to a specific review comment\ngh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/comments \\\n  -f body=\"Your reply here\" \\\n  -F in_reply_to={COMMENT_ID}\n```\n\n## The fix-push-review cycle\n\nAfter evaluating comments:\n\n1. Fix all real issues in one batch\n2. Reply to all dismissed comments with reasoning\n3. Think about what patterns Codex might flag next — check similar code paths proactively\n4. Commit and push\n5. Check that Codex reviews the new commit\n6. Repeat until Codex gives a clean review (thumbs-up) or only has dismissible comments\n\n## Responding to stale comments\n\nCodex sometimes re-posts old comments that reference code you've already fixed (they appear on the old commit's diff). These are stale — verify the fix is in the latest commit and reply noting the fix is already in place.\n\n## When a PR is ready\n\nA PR is ready for human review when:\n- All Codex comments are either fixed or replied to with dismissal reasoning\n- CI checks pass\n- The diff is clean and focused on the stated purpose\n"
  },
  {
    "path": ".coderabbit.yaml",
    "content": "reviews:\n  path_filters:\n    - \"!docs/python-sdk/**\"\n"
  },
  {
    "path": ".cursor/rules/core-mcp-objects.mdc",
    "content": "---\ndescription: \nglobs: \nalwaysApply: true\n---\nThere are four major MCP object types:\n\n- Tools (src/tools/)\n- Resources (src/resources/)\n- Resource Templates (src/resources/)\n- Prompts (src/prompts)\n\nWhile these have slightly different semantics and implementations, in general changes that affect interactions with any one (like adding tags, importing, etc.) will need to be adopted, applied, and tested on all others. Note that while resources and resource templates are different objects, they are both in `src/resources/`."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: 🐛 Bug Report\ndescription: Report a bug or unexpected behavior in FastMCP\nlabels: [bug, pending]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for reporting a bug!\n\n        A good bug report is one of the most valuable contributions you can make — see [CONTRIBUTING.md](../../CONTRIBUTING.md). If the fix is straightforward, a PR is also welcome.\n\n        ### Before you submit\n\n        - Make sure you're testing on the **latest version** of FastMCP — many issues are already fixed in newer releases\n        - Check if someone else has **already reported this** or if it's been fixed on the main branch\n        - You **must** include a copy/pasteable, properly formatted MRE (minimal reproducible example) or your issue may be closed without response\n        - **The ideal issue is a clear problem description and an MRE — that's it.** If you've done genuine investigation and have a non-obvious insight into the root cause, include it. But please don't speculate or ask an LLM to generate a diagnosis. We have LLMs too, and an incorrect analysis is harder to work with than none at all.\n        - **Keep it short.** A clear description plus a concise MRE is ideal — aim to fit in a single screen. Issues that include unsolicited root cause analysis, proposed fixes, or multi-section diagnostic writeups will be labeled `too-long` and not triaged until condensed.\n        - **Using an LLM?** Great — but it must follow these guidelines. Generic LLM output that ignores our contributing conventions will be closed. See [CONTRIBUTING.md](../../CONTRIBUTING.md).\n\n  - type: textarea\n    id: description\n    attributes:\n      label: What happened?\n      description: |\n        Describe the bug in a few sentences. What did you do, what happened, and what did you expect instead?\n\n        Do NOT include root cause analysis, proposed fixes, or diagnostic writeups — just describe the problem.\n    validations:\n      required: true\n\n  - type: textarea\n    id: example\n    attributes:\n      label: Example Code\n      description: >\n        If applicable, please provide a self-contained,\n        [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)\n        demonstrating the bug. If possible, your example should be a single-file script.\n\n      placeholder: |\n        import asyncio\n        from fastmcp import FastMCP, Client\n\n        mcp = FastMCP()\n\n        async def demo():\n            async with Client(mcp) as client:\n                ... # show the bug here\n\n        if __name__ == \"__main__\":\n            asyncio.run(demo())\n      render: Python\n\n  - type: textarea\n    id: version\n    attributes:\n      label: Version Information\n      description: |\n        Please tell us about your FastMCP version, MCP version, Python version, and OS, as well as any other relevant details about your environment.\n\n        To get the basic information, run the following command in your terminal and paste the output below:\n\n        ```bash\n        fastmcp version --copy\n        ```\n      render: Text\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: FastMCP Documentation\n    url: https://gofastmcp.com\n    about: Please review the documentation before opening an issue.\n  - name: MCP Python SDK\n    url: https://github.com/modelcontextprotocol/python-sdk/issues\n    about: Issues related to the low-level MCP Python SDK, including the FastMCP 1.0 module that is included in the `mcp` package, should be filed on the official MCP repository.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement.yml",
    "content": "name: 💡 Enhancement Request\ndescription: Suggest an idea or improvement for FastMCP\nlabels: [enhancement, pending]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for suggesting an improvement to FastMCP!\n\n        Enhancement issues are the **primary way** features and improvements get into FastMCP. Maintainers use well-written issues to implement changes that fit the codebase's patterns and ship quickly. A clear issue here is more impactful than a PR — see [CONTRIBUTING.md](../../CONTRIBUTING.md) for why.\n\n        ### Before you submit\n\n        - 🔍 **Check if this has already been requested** — search existing issues first\n        - 🎯 **Describe the problem you're trying to solve**, not the solution you want — we'll figure out the best implementation\n        - ✂️ **Keep it short.** A motivating description and a concrete use case is the ideal request — aim to fit in a single screen. Skip proposed implementations, API designs, or multi-option analyses — maintainers will figure out the approach. Requests that are difficult to parse will be labeled `too-long` and not triaged until condensed.\n        - 🤖 **Using an LLM?** Great — but it must follow these guidelines. Generic LLM output that ignores our contributing conventions will be closed. See [CONTRIBUTING.md](../../CONTRIBUTING.md).\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Enhancement\n      description: |\n        What problem or use case does this solve? How does current behavior fall short?\n\n        Focus on the *what* and *why* — the motivating scenario. You don't need to propose an API or implementation.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/actions/run-claude/action.yml",
    "content": "# Composite Action for running Claude Code Action\n#\n# Wraps anthropics/claude-code-action with MCP server configuration.\n# Template based on elastic/ai-github-actions base action.\n#\n# Usage:\n#   - uses: ./.github/actions/run-claude\n#     with:\n#       prompt: \"Your prompt here\"\n#       claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n#       github-token: ${{ steps.marvin-token.outputs.token }}\n#       allowed-tools: \"Edit,Read,Write,Bash(*),mcp__github__add_issue_comment\"\n#\nname: \"Run Claude\"\ndescription: \"Run Claude Code with MCP servers\"\nauthor: \"FastMCP\"\n\nbranding:\n  icon: \"cpu\"\n  color: \"orange\"\n\ninputs:\n  prompt:\n    description: \"Prompt to pass to Claude\"\n    required: true\n\n  claude-oauth-token:\n    description: \"Claude Code OAuth token for authentication\"\n    required: true\n\n  github-token:\n    description: \"GitHub token for Claude to operate with\"\n    required: true\n\n  allowed-tools:\n    description: \"Comma-separated list of allowed tools (e.g. Edit,Write,Bash(npm test))\"\n    required: false\n    default: \"\"\n\n  model:\n    description: \"Model to use for Claude\"\n    required: false\n    default: \"claude-opus-4-6\"\n\n  allowed-bots:\n    description: \"Allowed bot usernames, or '*' for all bots\"\n    required: false\n    default: \"\"\n\n  track-progress:\n    description: \"Whether Claude should track progress\"\n    required: false\n    default: \"true\"\n\n  mcp-servers:\n    description: \"MCP server configuration JSON\"\n    required: false\n    default: '{\"mcpServers\":{\"agents-md-generator\":{\"type\":\"http\",\"url\":\"https://agents-md-generator.fastmcp.app/mcp\"},\"public-code-search\":{\"type\":\"http\",\"url\":\"https://public-code-search.fastmcp.app/mcp\"}}}'\n\n  trigger-phrase:\n    description: \"Trigger phrase (for mention workflows)\"\n    required: false\n    default: \"/marvin\"\n\noutputs:\n  conclusion:\n    description: \"The conclusion of the Claude Code run\"\n    value: ${{ steps.claude.outputs.conclusion }}\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Clean up stale Claude locks\n      shell: bash\n      run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true\n\n    - name: Run Claude Code\n      id: claude\n      env:\n        GITHUB_TOKEN: ${{ inputs.github-token }}\n      uses: anthropics/claude-code-action@v1\n      with:\n        github_token: ${{ inputs.github-token }}\n        claude_code_oauth_token: ${{ inputs.claude-oauth-token }}\n        bot_name: \"Marvin Context Protocol\"\n        trigger_phrase: ${{ inputs.trigger-phrase }}\n        allowed_bots: ${{ inputs.allowed-bots }}\n        track_progress: ${{ inputs.track-progress }}\n        prompt: ${{ inputs.prompt }}\n        claude_args: |\n          ${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools {0}{1}', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }}\n          ${{ inputs.mcp-servers != '' && format('--mcp-config ''{0}''', inputs.mcp-servers) || '' }}\n          --model ${{ inputs.model }}\n        settings: |\n          {\"model\": \"${{ inputs.model }}\"}\n"
  },
  {
    "path": ".github/actions/run-pytest/action.yml",
    "content": "name: \"Run Pytest\"\ndescription: \"Run pytest with appropriate flags for the test type and platform\"\n\ninputs:\n  test-type:\n    description: \"Type of tests to run: unit, integration, or client_process\"\n    required: false\n    default: \"unit\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Run pytest\n      shell: bash\n      run: |\n        if [ \"${{ inputs.test-type }}\" == \"integration\" ]; then\n          MARKER=\"integration\"\n          TIMEOUT=\"30\"\n          MAX_PROCS=\"2\"\n          EXTRA_FLAGS=\"\"\n        elif [ \"${{ inputs.test-type }}\" == \"client_process\" ]; then\n          MARKER=\"client_process\"\n          TIMEOUT=\"5\"\n          MAX_PROCS=\"0\"\n          EXTRA_FLAGS=\"-x\"\n        else\n          MARKER=\"not integration and not client_process\"\n          TIMEOUT=\"5\"\n          MAX_PROCS=\"4\"\n          EXTRA_FLAGS=\"\"\n        fi\n\n        PARALLEL_FLAGS=\"\"\n        if [ \"$MAX_PROCS\" != \"0\" ] && [ \"${{ runner.os }}\" != \"Windows\" ]; then\n          PARALLEL_FLAGS=\"--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal\"\n        fi\n\n        uv run --no-sync pytest \\\n          --inline-snapshot=disable \\\n          --timeout=$TIMEOUT \\\n          --durations=50 \\\n          -m \"$MARKER\" \\\n          $PARALLEL_FLAGS \\\n          $EXTRA_FLAGS \\\n          tests\n"
  },
  {
    "path": ".github/actions/setup-uv/action.yml",
    "content": "name: \"Setup UV Environment\"\ndescription: \"Install uv and dependencies (requires checkout first)\"\n\ninputs:\n  python-version:\n    description: \"Python version to use\"\n    required: false\n    default: \"3.10\"\n  resolution:\n    description: \"Dependency resolution: locked, upgrade, or lowest-direct\"\n    required: false\n    default: \"locked\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Install uv\n      uses: astral-sh/setup-uv@v7\n      with:\n        enable-cache: true\n        cache-dependency-glob: \"uv.lock\"\n        python-version: ${{ inputs.python-version }}\n\n    - name: Install dependencies\n      shell: bash\n      run: |\n        if [ \"${{ inputs.resolution }}\" == \"locked\" ]; then\n          uv sync --locked\n        elif [ \"${{ inputs.resolution }}\" == \"upgrade\" ]; then\n          uv sync --upgrade\n        else\n          uv sync --resolution ${{ inputs.resolution }}\n        fi\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    labels:\n      - \"dependencies\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    labels:\n      - \"dependencies\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Description\n\n<!-- What does this PR do? Link to the issue it addresses. -->\n\nCloses #\n\n## Contribution type\n\n<!-- Check the one that applies. If you're unsure whether your change is welcome, please open an issue first — see CONTRIBUTING.md. -->\n\n- [ ] Bug fix (simple, well-scoped fix for a clearly broken behavior)\n- [ ] Documentation improvement\n- [ ] Enhancement (maintainers typically implement enhancements — see [CONTRIBUTING.md](../CONTRIBUTING.md))\n\n## Checklist\n\n- [ ] This PR addresses an existing issue (or fixes a self-evident bug)\n- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md)\n- [ ] I have added tests that cover my changes\n- [ ] I have run `uv run prek run --all-files` and all checks pass\n- [ ] I have self-reviewed my changes\n- [ ] If I used an LLM, it followed the repo's contributing conventions (not generic output)\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  exclude:\n    labels:\n      - ignore in release notes\n\n  categories:\n    - title: New Features 🎉\n      labels:\n        - feature\n\n    - title: Breaking Changes ⚠️\n      labels:\n        - breaking change\n      exclude:\n        labels:\n          - contrib\n          - security\n\n    - title: Enhancements ✨\n      labels:\n        - enhancement\n      exclude:\n        labels:\n          - breaking change\n          - security\n\n    - title: Security 🔒\n      labels:\n        - security\n\n    - title: Fixes 🐞\n      labels:\n        - bug\n      exclude:\n        labels:\n          - contrib\n          - security\n\n    - title: Docs 📚\n      labels:\n        - documentation\n\n    - title: Examples & Contrib 💡\n      labels:\n        - example\n        - contrib\n\n    - title: Dependencies 📦\n      labels:\n        - dependencies\n      exclude:\n        labels:\n          - security\n\n    - title: Other Changes 🦾\n      labels:\n        - \"*\"\n"
  },
  {
    "path": ".github/scripts/mention/gh-get-review-threads.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# Get PR review threads with comments via GitHub GraphQL API\n#\n# Usage:\n#   gh-get-review-threads.sh [FILTER]\n#\n# Arguments:\n#   FILTER - Optional: filter for unresolved threads from specific author\n#\n# Environment (set by composite action):\n#   MENTION_REPO      - Repository (owner/repo format)\n#   MENTION_PR_NUMBER - Pull request number\n#   GITHUB_TOKEN      - GitHub API token\n#\n# Output:\n#   JSON array of review threads with nested comments\n\n# Parse OWNER and REPO from MENTION_REPO\nREPO_FULL=\"${MENTION_REPO:?MENTION_REPO environment variable is required}\"\nOWNER=\"${REPO_FULL%/*}\"\nREPO=\"${REPO_FULL#*/}\"\nPR_NUMBER=\"${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}\"\nFILTER=\"${1:-}\"\n\ngh api graphql -f query='\n  query($owner: String!, $repo: String!, $prNumber: Int!) {\n    repository(owner: $owner, name: $repo) {\n      pullRequest(number: $prNumber) {\n        reviewThreads(first: 100) {\n          nodes {\n            id\n            isResolved\n            isOutdated\n            path\n            line\n            comments(first: 50) {\n              nodes {\n                id\n                body\n                author { login }\n                createdAt\n              }\n            }\n          }\n        }\n      }\n    }\n  }' -F owner=\"$OWNER\" \\\n     -F repo=\"$REPO\" \\\n     -F prNumber=\"$PR_NUMBER\" \\\n     --jq '.data.repository.pullRequest.reviewThreads.nodes' | \\\nif [ -n \"$FILTER\" ]; then\n  jq --arg author \"$FILTER\" '\n    map(select(\n      .isResolved == false and\n      .comments.nodes | any(.author.login == $author)\n    ))'\nelse\n  cat\nfi\n"
  },
  {
    "path": ".github/scripts/mention/gh-resolve-review-thread.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# Resolve a GitHub PR review thread, optionally posting a comment first\n#\n# Usage:\n#   gh-resolve-review-thread.sh THREAD_ID [COMMENT]\n#\n# Arguments:\n#   THREAD_ID - The GraphQL node ID of the review thread to resolve\n#   COMMENT   - Optional: Comment body to post before resolving\n#\n# Environment (set by composite action):\n#   MENTION_REPO      - Repository (owner/repo format)\n#   MENTION_PR_NUMBER - Pull request number\n#   GITHUB_TOKEN      - GitHub API token\n#\n# Behavior:\n#   1. If COMMENT is provided, posts it as a reply to the thread\n#   2. Resolves the thread\n\n# Validate required environment variables\n: \"${MENTION_REPO:?MENTION_REPO environment variable is required}\"\n: \"${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}\"\nTHREAD_ID=\"${1:?Thread ID required}\"\nCOMMENT=\"${2:-}\"\n\n# Step 1: Post comment if provided\nif [ -n \"$COMMENT\" ]; then\n  echo \"Posting comment to thread...\" >&2\n  COMMENT_RESULT=$(gh api graphql -f query='\n    mutation($threadId: ID!, $body: String!) {\n      addPullRequestReviewThreadReply(input: {\n        pullRequestReviewThreadId: $threadId,\n        body: $body\n      }) {\n        comment {\n          id\n        }\n      }\n    }' -f threadId=\"$THREAD_ID\" -f body=\"$COMMENT\")\n  if echo \"$COMMENT_RESULT\" | jq -e '.errors' > /dev/null 2>&1; then\n    echo \"Error posting comment: $COMMENT_RESULT\" >&2\n    exit 1\n  fi\nfi\n\n# Step 2: Resolve the thread\necho \"Resolving thread...\" >&2\nRESOLVE_RESULT=$(gh api graphql -f query='\n  mutation($threadId: ID!) {\n    resolveReviewThread(input: {threadId: $threadId}) {\n      thread {\n        id\n        isResolved\n      }\n    }\n  }' -f threadId=\"$THREAD_ID\" --jq '.data.resolveReviewThread.thread')\n\necho \"$RESOLVE_RESULT\"\necho \"✓ Thread resolved\" >&2\n"
  },
  {
    "path": ".github/scripts/pr-review/pr-comment.sh",
    "content": "#!/bin/bash\n# pr-comment.sh - Queue a structured inline review comment for the PR review\n#\n# Usage:\n#   pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> [suggestion via stdin]\n#   pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> --no-suggestion\n#\n# Arguments:\n#   file              File path (required)\n#   line              Line number (required)\n#   --severity        Severity level: critical, high, medium, low, nitpick (required)\n#   --title           Brief description for comment heading (required)\n#   --why             One sentence explaining the risk/impact (required)\n#   --no-suggestion   Explicitly skip suggestion (use for architectural issues)\n#\n# The suggestion code is read from stdin (use heredoc). If no stdin and no --no-suggestion, errors.\n#\n# Examples:\n#   # With suggestion (preferred)\n#   pr-comment.sh src/main.go 42 --severity high --title \"Missing error check\" --why \"Errors are silently ignored\" <<'EOF'\n#   if err != nil {\n#       return fmt.Errorf(\"operation failed: %w\", err)\n#   }\n#   EOF\n#\n#   # Without suggestion (for issues requiring broader changes)\n#   pr-comment.sh src/main.go 42 --severity medium --title \"Consider extracting to function\" \\\n#     --why \"This logic is duplicated in 3 places\" --no-suggestion\n#\n# Environment variables (set by the composite action):\n#   PR_REVIEW_REPO          - Repository (owner/repo)\n#   PR_REVIEW_PR_NUMBER     - Pull request number\n#   PR_REVIEW_COMMENTS_DIR  - Directory to cache comments (default: /tmp/pr-review-comments)\n\nset -e\n\n# Configuration from environment\nREPO=\"${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}\"\nPR_NUMBER=\"${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}\"\nCOMMENTS_DIR=\"${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}\"\n\n# Severity emoji mapping\ndeclare -A SEVERITY_EMOJI=(\n  [critical]=\"🔴 CRITICAL\"\n  [high]=\"🟠 HIGH\"\n  [medium]=\"🟡 MEDIUM\"\n  [low]=\"⚪ LOW\"\n  [nitpick]=\"💬 NITPICK\"\n)\n\n# Parse arguments\nFILE=\"\"\nLINE=\"\"\nSEVERITY=\"\"\nTITLE=\"\"\nWHY=\"\"\nNO_SUGGESTION=false\n\n# First two positional args are file and line\nif [ $# -lt 2 ]; then\n  echo \"Error: file and line are required\"\n  echo \"Usage: pr-comment.sh <file> <line> --severity <level> --title <desc> --why <reason> [<<'EOF' ... EOF]\"\n  exit 1\nfi\n\nFILE=\"$1\"\nLINE=\"$2\"\nshift 2\n\n# Parse named arguments\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    --severity)\n      SEVERITY=\"$2\"\n      shift 2\n      ;;\n    --title)\n      TITLE=\"$2\"\n      shift 2\n      ;;\n    --why)\n      WHY=\"$2\"\n      shift 2\n      ;;\n    --no-suggestion)\n      NO_SUGGESTION=true\n      shift\n      ;;\n    *)\n      echo \"Error: Unknown argument: $1\"\n      exit 1\n      ;;\n  esac\ndone\n\n# Read suggestion from stdin if available\nSUGGESTION=\"\"\nif [ ! -t 0 ]; then\n  SUGGESTION=$(cat)\nfi\n\n# Validate required arguments\nif [ -z \"$SEVERITY\" ]; then\n  echo \"Error: --severity is required (critical, high, medium, low, nitpick)\"\n  exit 1\nfi\n\nif [ -z \"$TITLE\" ]; then\n  echo \"Error: --title is required\"\n  exit 1\nfi\n\nif [ -z \"$WHY\" ]; then\n  echo \"Error: --why is required\"\n  exit 1\nfi\n\n# Validate severity level\nif [ -z \"${SEVERITY_EMOJI[$SEVERITY]}\" ]; then\n  echo \"Error: Invalid severity '$SEVERITY'. Must be one of: critical, high, medium, low, nitpick\"\n  exit 1\nfi\n\n# Require either suggestion or explicit --no-suggestion\nif [ -z \"$SUGGESTION\" ] && [ \"$NO_SUGGESTION\" = false ]; then\n  echo \"Error: Suggestion required. Provide code via stdin (heredoc) or use --no-suggestion\"\n  echo \"\"\n  echo \"Example with suggestion:\"\n  echo \"  pr-comment.sh file.go 42 --severity high --title \\\"desc\\\" --why \\\"reason\\\" <<'EOF'\"\n  echo \"  fixed code here\"\n  echo \"  EOF\"\n  echo \"\"\n  echo \"Example without suggestion:\"\n  echo \"  pr-comment.sh file.go 42 --severity medium --title \\\"desc\\\" --why \\\"reason\\\" --no-suggestion\"\n  exit 1\nfi\n\n# Validate line is a positive integer (>= 1)\nif ! [[ \"$LINE\" =~ ^[1-9][0-9]*$ ]]; then\n  echo \"Error: Line number must be a positive integer (>= 1), got: $LINE\"\n  exit 1\nfi\n\n# Get the diff for this file to validate the comment location\nDIFF_DATA=$(gh api \"repos/${REPO}/pulls/${PR_NUMBER}/files\" --paginate | jq --arg f \"$FILE\" '.[] | select(.filename==$f)')\n\nif [ -z \"$DIFF_DATA\" ]; then\n  echo \"Error: File '${FILE}' not found in PR diff\"\n  echo \"\"\n  echo \"Files changed in this PR:\"\n  gh api \"repos/${REPO}/pulls/${PR_NUMBER}/files\" --paginate --jq '.[].filename'\n  exit 1\nfi\n\nPATCH=$(echo \"$DIFF_DATA\" | jq -r '.patch // empty')\n\nif [ -z \"$PATCH\" ]; then\n  echo \"Error: No patch data for file '${FILE}' (file may be binary or too large)\"\n  exit 1\nfi\n\n# Verify the line exists in the diff\nLINE_IN_DIFF=$(echo \"$PATCH\" | awk -v target_line=\"$LINE\" '\nBEGIN { current_line = 0; found = 0 }\n/^@@/ {\n  line = $0\n  gsub(/.*\\+/, \"\", line)\n  gsub(/[^0-9].*/, \"\", line)\n  current_line = line - 1\n  next\n}\n{\n  if (substr($0, 1, 1) != \"-\") {\n    current_line++\n    if (current_line == target_line) {\n      found = 1\n      exit\n    }\n  }\n}\nEND { if (found) print \"1\"; else print \"0\" }\n')\n\nif [ \"$LINE_IN_DIFF\" != \"1\" ]; then\n  echo \"Error: Line ${LINE} not found in the diff for '${FILE}'\"\n  echo \"\"\n  echo \"Note: You can only comment on lines that appear in the diff (added, modified, or context lines)\"\n  echo \"\"\n  echo \"First 50 lines of diff for this file:\"\n  echo \"$PATCH\" | head -50\n  exit 1\nfi\n\n# Create comments directory if it doesn't exist\nmkdir -p \"${COMMENTS_DIR}\"\n\n# Assemble the comment body\nSEVERITY_LABEL=\"${SEVERITY_EMOJI[$SEVERITY]}\"\n\nBODY=\"**${SEVERITY_LABEL}** ${TITLE}\n\nWhy: ${WHY}\"\n\n# Add suggestion block if provided\nif [ -n \"$SUGGESTION\" ]; then\n  BODY=\"${BODY}\n\n\\`\\`\\`suggestion\n${SUGGESTION}\n\\`\\`\\`\"\nfi\n\n# Append standard footer\nFOOTER='\n\n---\nMarvin Context Protocol | Type `/marvin` to interact further\n\nGive us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'\n\nBODY_WITH_FOOTER=\"${BODY}${FOOTER}\"\n\n# Generate unique comment ID\nCOMMENT_ID=\"comment-$(date +%s)-$(od -An -N4 -tu4 /dev/urandom | tr -d ' ')\"\nCOMMENT_FILE=\"${COMMENTS_DIR}/${COMMENT_ID}.json\"\n\n# Create the comment JSON object\njq -n \\\n  --arg path \"$FILE\" \\\n  --argjson line \"$LINE\" \\\n  --arg side \"RIGHT\" \\\n  --arg body \"$BODY_WITH_FOOTER\" \\\n  --arg id \"$COMMENT_ID\" \\\n  '{\n    path: $path,\n    line: $line,\n    side: $side,\n    body: $body,\n    _meta: {\n      id: $id,\n      file: $path,\n      line: $line\n    }\n  }' > \"${COMMENT_FILE}\"\n\necho \"✓ Queued review comment for ${FILE}:${LINE}\"\necho \"  Severity: ${SEVERITY_LABEL}\"\necho \"  Title: ${TITLE}\"\necho \"  Comment ID: ${COMMENT_ID}\"\necho \"  Comment will be submitted with pr-review.sh\"\necho \"  Remove with: pr-remove-comment.sh ${FILE} ${LINE}\"\n"
  },
  {
    "path": ".github/scripts/pr-review/pr-diff.sh",
    "content": "#!/bin/bash\n# pr-diff.sh - Show changed files or diff for a specific file\n#\n# Usage: \n#   pr-diff.sh           - List all changed files (shows full diff if small enough)\n#   pr-diff.sh <file>    - Show diff for a specific file with line numbers\n#\n# Environment variables (set by the composite action):\n#   PR_REVIEW_REPO       - Repository (owner/repo)\n#   PR_REVIEW_PR_NUMBER  - Pull request number\n\nset -e\n\n# Configuration from environment\nREPO=\"${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}\"\nPR_NUMBER=\"${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}\"\nEXPECTED_HEAD=\"${PR_REVIEW_HEAD_SHA:-}\"\n\n# Check if HEAD has changed since review started (race condition detection)\nif [ -n \"$EXPECTED_HEAD\" ]; then\n  CURRENT_HEAD=$(gh api \"repos/${REPO}/pulls/${PR_NUMBER}\" --jq '.head.sha')\n  if [ \"$CURRENT_HEAD\" != \"$EXPECTED_HEAD\" ]; then\n    echo \"⚠️  WARNING: PR head has changed since review started!\"\n    echo \"   Review started at: ${EXPECTED_HEAD:0:7}\"\n    echo \"   Current head:      ${CURRENT_HEAD:0:7}\"\n    echo \"   Line numbers below may not match the commit being reviewed.\"\n    echo \"\"\n  fi\nfi\n\n# Thresholds for \"too big\" - show file list only if exceeded\nMAX_FILES=25\nMAX_TOTAL_LINES=1500\n\nFILE=\"$1\"\n\n# Function to add line numbers to a patch\n# Format: [LINE] +added | [LINE]  context | [----] -deleted\nadd_line_numbers() {\n  awk '\n  BEGIN { new_line = 0 }\n  /^@@/ {\n    # Parse hunk header: @@ -old_start,old_count +new_start,new_count @@\n    match($0, /\\+([0-9]+)/)\n    new_line = substr($0, RSTART+1, RLENGTH-1) - 1\n    print \"\"\n    print $0\n    next\n  }\n  /^-/ {\n    # Deleted line - cannot comment on these\n    printf \"[----] %s\\n\", $0\n    next\n  }\n  /^\\+/ {\n    # Added line - can comment, show line number\n    new_line++\n    printf \"[%4d] %s\\n\", new_line, $0\n    next\n  }\n  {\n    # Context line (space prefix) - can comment, show line number\n    new_line++\n    printf \"[%4d] %s\\n\", new_line, $0\n  }\n  '\n}\n\nif [ -z \"$FILE\" ]; then\n  # Get file list with stats\n  FILES_DATA=$(gh api \"repos/${REPO}/pulls/${PR_NUMBER}/files\" --paginate)\n  \n  FILE_COUNT=$(echo \"$FILES_DATA\" | jq 'length')\n  TOTAL_ADDITIONS=$(echo \"$FILES_DATA\" | jq '[.[].additions] | add // 0')\n  TOTAL_DELETIONS=$(echo \"$FILES_DATA\" | jq '[.[].deletions] | add // 0')\n  TOTAL_LINES=$((TOTAL_ADDITIONS + TOTAL_DELETIONS))\n  \n  echo \"PR #${PR_NUMBER} Summary: ${FILE_COUNT} files changed (+${TOTAL_ADDITIONS}/-${TOTAL_DELETIONS})\"\n  echo \"\"\n  \n  # Check if diff is too large\n  if [ \"$FILE_COUNT\" -gt \"$MAX_FILES\" ] || [ \"$TOTAL_LINES\" -gt \"$MAX_TOTAL_LINES\" ]; then\n    echo \"⚠️  Large diff detected (>${MAX_FILES} files or >${MAX_TOTAL_LINES} lines changed)\"\n    echo \"   Review files individually using: pr-diff.sh <filename>\"\n    echo \"\"\n    echo \"Files changed:\"\n    echo \"$FILES_DATA\" | jq -r '.[] | \"  \\(.filename) (+\\(.additions)/-\\(.deletions))\"'\n  else\n    # Small enough - show all diffs with line numbers\n    echo \"Files changed:\"\n    echo \"$FILES_DATA\" | jq -r '.[] | \"  \\(.filename) (+\\(.additions)/-\\(.deletions))\"'\n    echo \"\"\n    echo \"─────────────────────────────────────────────────────────────────────\"\n    echo \"\"\n    \n    # Show each file's diff by iterating over indices\n    for i in $(seq 0 $((FILE_COUNT - 1))); do\n      FNAME=$(echo \"$FILES_DATA\" | jq -r \".[$i].filename\")\n      PATCH=$(echo \"$FILES_DATA\" | jq -r \".[$i].patch // empty\")\n      \n      if [ -n \"$PATCH\" ]; then\n        echo \"## ${FNAME}\"\n        echo \"Use: pr-comment.sh ${FNAME} <LINE> --severity <level> --title \\\"desc\\\" --why \\\"reason\\\" <<'EOF' ... EOF\"\n        echo \"Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)\"\n        echo \"$PATCH\" | add_line_numbers\n        echo \"\"\n        echo \"─────────────────────────────────────────────────────────────────────\"\n        echo \"\"\n      fi\n    done\n  fi\nelse\n  # Show specific file diff\n  PATCH=$(gh api \"repos/${REPO}/pulls/${PR_NUMBER}/files\" --paginate --jq --arg file \"$FILE\" '.[] | select(.filename==$file) | .patch')\n  \n  if [ -z \"$PATCH\" ]; then\n    echo \"Error: File '${FILE}' not found in PR diff\"\n    echo \"\"\n    echo \"Files changed in this PR:\"\n    gh api \"repos/${REPO}/pulls/${PR_NUMBER}/files\" --paginate --jq '.[].filename'\n    exit 1\n  fi\n  \n  echo \"## ${FILE}\"\n  echo \"Use: pr-comment.sh ${FILE} <LINE> --severity <level> --title \\\"desc\\\" --why \\\"reason\\\" <<'EOF' ... EOF\"\n  echo \"Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)\"\n  echo \"$PATCH\" | add_line_numbers\nfi\n"
  },
  {
    "path": ".github/scripts/pr-review/pr-existing-comments.sh",
    "content": "#!/bin/bash\n# pr-existing-comments.sh - Fetch existing review threads on a PR\n#\n# Usage:\n#   pr-existing-comments.sh              - Show all review threads with full details\n#   pr-existing-comments.sh --summary    - Show per-file summary only (for large PRs)\n#   pr-existing-comments.sh --unresolved - Show only unresolved threads\n#   pr-existing-comments.sh --file <path> - Show threads for a specific file\n#   pr-existing-comments.sh --full       - Show full comment text (no truncation)\n#\n# Output: Formatted summary of existing review threads grouped by file,\n# showing thread status, comments, and whether issues were addressed.\n#\n# For large PRs, use --summary first to see the overview, then --file <path>\n# to get full thread details when reviewing each file.\n#\n# Environment variables (set by the composite action):\n#   PR_REVIEW_REPO       - Repository (owner/repo)\n#   PR_REVIEW_PR_NUMBER  - Pull request number\n\nset -e\n\n# Configuration from environment\nREPO=\"${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}\"\nPR_NUMBER=\"${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}\"\n\nOWNER=\"${REPO%/*}\"\nREPO_NAME=\"${REPO#*/}\"\n\n# Parse arguments\nFILTER_UNRESOLVED=false\nFILTER_FILE=\"\"\nSUMMARY_ONLY=false\nFULL_TEXT=false\n\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    --unresolved)\n      FILTER_UNRESOLVED=true\n      shift\n      ;;\n    --file)\n      FILTER_FILE=\"$2\"\n      shift 2\n      ;;\n    --summary)\n      SUMMARY_ONLY=true\n      shift\n      ;;\n    --full)\n      FULL_TEXT=true\n      shift\n      ;;\n    *)\n      echo \"Usage: pr-existing-comments.sh [--summary] [--unresolved] [--file <path>] [--full]\"\n      exit 1\n      ;;\n  esac\ndone\n\n# Fetch review threads via GraphQL\nTHREADS=$(gh api graphql -f query='\n  query($owner: String!, $repo: String!, $prNumber: Int!) {\n    repository(owner: $owner, name: $repo) {\n      pullRequest(number: $prNumber) {\n        reviewThreads(first: 100) {\n          nodes {\n            id\n            isResolved\n            isOutdated\n            path\n            line\n            originalLine\n            startLine\n            originalStartLine\n            diffSide\n            comments(first: 50) {\n              nodes {\n                id\n                body\n                author { login }\n                createdAt\n                originalCommit { abbreviatedOid }\n              }\n            }\n          }\n        }\n      }\n    }\n  }' -F owner=\"$OWNER\" \\\n     -F repo=\"$REPO_NAME\" \\\n     -F prNumber=\"$PR_NUMBER\" \\\n     --jq '.data.repository.pullRequest.reviewThreads.nodes')\n\nif [ -z \"$THREADS\" ] || [ \"$THREADS\" = \"null\" ]; then\n  echo \"No existing review threads found.\"\n  exit 0\nfi\n\n# Apply filters\nFILTERED=\"$THREADS\"\n\nif [ \"$FILTER_UNRESOLVED\" = true ]; then\n  FILTERED=$(echo \"$FILTERED\" | jq '[.[] | select(.isResolved == false)]')\nfi\n\nif [ -n \"$FILTER_FILE\" ]; then\n  FILTERED=$(echo \"$FILTERED\" | jq --arg file \"$FILTER_FILE\" '[.[] | select(.path == $file)]')\nfi\n\nTHREAD_COUNT=$(echo \"$FILTERED\" | jq 'length')\n\nif [ \"$THREAD_COUNT\" -eq 0 ]; then\n  if [ \"$FILTER_UNRESOLVED\" = true ]; then\n    echo \"No unresolved review threads found.\"\n  elif [ -n \"$FILTER_FILE\" ]; then\n    echo \"No review threads found for ${FILTER_FILE}.\"\n  else\n    echo \"No existing review threads found.\"\n  fi\n  exit 0\nfi\n\n# Count resolved vs unresolved\nRESOLVED_COUNT=$(echo \"$FILTERED\" | jq '[.[] | select(.isResolved == true)] | length')\nUNRESOLVED_COUNT=$(echo \"$FILTERED\" | jq '[.[] | select(.isResolved == false)] | length')\nOUTDATED_COUNT=$(echo \"$FILTERED\" | jq '[.[] | select(.isOutdated == true)] | length')\n\necho \"Existing review threads: ${THREAD_COUNT} total (${UNRESOLVED_COUNT} unresolved, ${RESOLVED_COUNT} resolved, ${OUTDATED_COUNT} outdated)\"\necho \"\"\n\n# Summary mode: show per-file counts only\nif [ \"$SUMMARY_ONLY\" = true ]; then\n  echo \"Threads by file:\"\n  echo \"$FILTERED\" | jq -r '\n    group_by(.path) | .[] |\n    . as $threads |\n    ($threads | length) as $total |\n    ([$threads[] | select(.isResolved == false)] | length) as $unresolved |\n    ([$threads[] | select(.isResolved == true)] | length) as $resolved |\n    ([$threads[] | select(.isOutdated == true)] | length) as $outdated |\n    ([$threads[] | select(.comments.nodes | length > 1)] | length) as $has_replies |\n    \"  \" + $threads[0].path +\n    \" — \" + ($total | tostring) + \" threads\" +\n    \" (\" + ($unresolved | tostring) + \" unresolved, \" + ($resolved | tostring) + \" resolved\" +\n    (if $outdated > 0 then \", \" + ($outdated | tostring) + \" outdated\" else \"\" end) +\n    \")\" +\n    (if $has_replies > 0 then \" ⚠️ \" + ($has_replies | tostring) + \" with replies\" else \"\" end)\n  '\n  echo \"\"\n  echo \"Use: pr-existing-comments.sh --file <path>  to see full thread details for a file\"\n  exit 0\nfi\n\n# Full detail mode: output threads grouped by file\n# Show full conversation for threads with replies\nFIRST_LIMIT=200\nREPLY_LIMIT=300\nif [ \"$FULL_TEXT\" = true ]; then\n  FIRST_LIMIT=999999\n  REPLY_LIMIT=999999\nfi\n\necho \"$FILTERED\" | jq -r --argjson first_limit \"$FIRST_LIMIT\" --argjson reply_limit \"$REPLY_LIMIT\" '\n  group_by(.path) | .[] |\n  \"## \" + .[0].path + \" (\" + (length | tostring) + \" threads)\\n\" +\n  ([.[] |\n    \"  \" +\n    (if .isResolved then \"✅ RESOLVED\" elif .isOutdated then \"⚠️  OUTDATED\" else \"🔴 UNRESOLVED\" end) +\n    \" (line \" + (if .line then (.line | tostring) elif .startLine then (.startLine | tostring) elif .originalLine then (\"~\" + (.originalLine | tostring)) elif .originalStartLine then (\"~\" + (.originalStartLine | tostring)) else \"?\" end) + \")\" +\n    # Show the commit the comment was originally made on\n    (if .comments.nodes[0].originalCommit.abbreviatedOid then \" [\" + .comments.nodes[0].originalCommit.abbreviatedOid + \"]\" else \"\" end) +\n    # Flag threads with replies — indicates a conversation happened\n    (if (.comments.nodes | length) > 1 then \" ← has replies\" else \"\" end) +\n    \"\\n\" +\n    ([.comments.nodes | to_entries[] |\n      .value as $comment |\n      .key as $idx |\n      ($comment.body | gsub(\"\\n\"; \" \")) as $flat |\n      if $idx == 0 then\n        \"    @\" + ($comment.author.login // \"unknown\") + \": \" + $flat[0:$first_limit] +\n        (if ($flat | length) > $first_limit then \" [truncated]\" else \"\" end)\n      else\n        \"    ↳ @\" + ($comment.author.login // \"unknown\") + \": \" + $flat[0:$reply_limit] +\n        (if ($flat | length) > $reply_limit then \" [truncated]\" else \"\" end)\n      end\n    ] | join(\"\\n\")) +\n    \"\\n\"\n  ] | join(\"\\n\"))\n'\n"
  },
  {
    "path": ".github/scripts/pr-review/pr-remove-comment.sh",
    "content": "#!/bin/bash\n# pr-remove-comment.sh - Remove a queued review comment\n#\n# Usage:\n#   pr-remove-comment.sh <file> <line-number>\n#   pr-remove-comment.sh <comment-id>\n#\n# Examples:\n#   pr-remove-comment.sh src/main.go 42\n#   pr-remove-comment.sh comment-1234567890-1234567890\n#\n# This script removes a previously queued comment before it's submitted.\n# Useful if the agent realizes it made a mistake or wants to update a comment.\n#\n# Environment variables (set by the composite action):\n#   PR_REVIEW_COMMENTS_DIR - Directory containing comment files (default: /tmp/pr-review-comments)\n\nset -e\n\nCOMMENTS_DIR=\"${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}\"\n\nif [ ! -d \"${COMMENTS_DIR}\" ]; then\n  echo \"No comments directory found: ${COMMENTS_DIR}\"\n  exit 0\nfi\n\n# Check if first argument looks like a comment ID\nif [[ \"$1\" =~ ^comment- ]]; then\n  COMMENT_ID=\"$1\"\n  COMMENT_FILE=\"${COMMENTS_DIR}/${COMMENT_ID}.json\"\n  \n  if [ -f \"${COMMENT_FILE}\" ]; then\n    FILE=$(jq -r '._meta.file // .path' \"${COMMENT_FILE}\")\n    LINE=$(jq -r '._meta.line // .line' \"${COMMENT_FILE}\")\n    rm -f \"${COMMENT_FILE}\"\n    echo \"✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}\"\n  else\n    echo \"Comment not found: ${COMMENT_ID}\"\n    exit 1\n  fi\nelse\n  # Treat as file and line number\n  FILE=\"$1\"\n  LINE=\"$2\"\n  \n  if [ -z \"$FILE\" ] || [ -z \"$LINE\" ]; then\n    echo \"Usage:\"\n    echo \"  pr-remove-comment.sh <file> <line-number>\"\n    echo \"  pr-remove-comment.sh <comment-id>\"\n    echo \"\"\n    echo \"Examples:\"\n    echo \"  pr-remove-comment.sh src/main.go 42\"\n    echo \"  pr-remove-comment.sh comment-1234567890-1234567890\"\n    exit 1\n  fi\n  \n  # Validate line is a positive integer (>= 1)\n  if ! [[ \"$LINE\" =~ ^[1-9][0-9]*$ ]]; then\n    echo \"Error: Line number must be a positive integer (>= 1), got: $LINE\"\n    exit 1\n  fi\n  \n  # Find and remove matching comment files\n  # Use nullglob to handle case where no files match\n  shopt -s nullglob\n  REMOVED=0\n  for COMMENT_FILE in \"${COMMENTS_DIR}\"/comment-*.json; do\n    \n    COMMENT_FILE_PATH=$(jq -r '._meta.file // .path' \"${COMMENT_FILE}\")\n    COMMENT_LINE=$(jq -r '._meta.line // .line' \"${COMMENT_FILE}\")\n    \n    if [ \"$COMMENT_FILE_PATH\" = \"$FILE\" ] && [ \"$COMMENT_LINE\" = \"$LINE\" ]; then\n      COMMENT_ID=$(basename \"${COMMENT_FILE}\" .json)\n      rm -f \"${COMMENT_FILE}\"\n      echo \"✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}\"\n      REMOVED=$((REMOVED + 1))\n    fi\n  done\n  \n  if [ \"$REMOVED\" -eq 0 ]; then\n    echo \"No comment found for ${FILE}:${LINE}\"\n    exit 1\n  fi\nfi\n"
  },
  {
    "path": ".github/scripts/pr-review/pr-review.sh",
    "content": "#!/bin/bash\n# pr-review.sh - Submit a PR review (approve, request changes, or comment)\n#\n# Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]\n# Example: pr-review.sh REQUEST_CHANGES \"Please fix the issues noted above\"\n#\n# This script creates and submits a review with any queued inline comments.\n# Comments are read from individual files in PR_REVIEW_COMMENTS_DIR (created by pr-comment.sh).\n#\n# The review body can contain special characters (backticks, dollar signs, etc.)\n# and will be safely passed to the GitHub API without shell interpretation.\n#\n# Environment variables (set by the composite action):\n#   PR_REVIEW_REPO          - Repository (owner/repo)\n#   PR_REVIEW_PR_NUMBER     - Pull request number\n#   PR_REVIEW_HEAD_SHA      - HEAD commit SHA\n#   PR_REVIEW_COMMENTS_DIR  - Directory containing queued comment files (default: /tmp/pr-review-comments)\n\nset -e\n\n# Configuration from environment\nREPO=\"${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}\"\nPR_NUMBER=\"${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}\"\nHEAD_SHA=\"${PR_REVIEW_HEAD_SHA:?PR_REVIEW_HEAD_SHA environment variable is required}\"\nCOMMENTS_DIR=\"${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}\"\n\n# Arguments\nEVENT=\"$1\"\nshift 2>/dev/null || true\n\n# Read body from remaining arguments\n# Join all remaining arguments with spaces, preserving the string as-is\nBODY=\"$*\"\n\nif [ -z \"$EVENT\" ]; then\n  echo \"Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]\"\n  echo \"Example: pr-review.sh REQUEST_CHANGES 'Please fix the issues noted in the inline comments'\"\n  exit 1\nfi\n\n# Validate event type\ncase \"$EVENT\" in\n  APPROVE|REQUEST_CHANGES|COMMENT)\n    ;;\n  *)\n    echo \"Error: Invalid event type '${EVENT}'\"\n    echo \"Must be one of: APPROVE, REQUEST_CHANGES, COMMENT\"\n    exit 1\n    ;;\nesac\n\n# Read queued comments from individual files\nCOMMENTS=\"[]\"\nCOMMENT_COUNT=0\n\nif [ -d \"${COMMENTS_DIR}\" ]; then\n  # Collect all comment files and merge into a single JSON array\n  # Remove _meta fields before submitting (they're only for internal use)\n  COMMENT_FILES=(\"${COMMENTS_DIR}\"/comment-*.json)\n  \n  if [ -f \"${COMMENT_FILES[0]}\" ]; then\n    # Use jq to read all comment files, extract the comment data (without _meta), and combine\n    COMMENTS=$(jq -s '[.[] | del(._meta)]' \"${COMMENTS_DIR}\"/comment-*.json)\n    COMMENT_COUNT=$(echo \"$COMMENTS\" | jq 'length')\n    if [ \"$COMMENT_COUNT\" -gt 0 ]; then\n      echo \"Found ${COMMENT_COUNT} queued inline comment(s)\"\n    fi\n  fi\nfi\n\n# Append standard footer to the review body (if body is provided)\nFOOTER='\n\n---\nMarvin Context Protocol | Type `/marvin` to interact further\n\nGive us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'\n\nif [ -n \"$BODY\" ]; then\n  BODY_WITH_FOOTER=\"${BODY}${FOOTER}\"\nelse\n  BODY_WITH_FOOTER=\"\"\nfi\n\n# Build the review request JSON\n# Use jq to safely construct the JSON with all special characters handled\nREVIEW_JSON=$(jq -n \\\n  --arg commit_id \"$HEAD_SHA\" \\\n  --arg event \"$EVENT\" \\\n  --arg body \"$BODY_WITH_FOOTER\" \\\n  --argjson comments \"$COMMENTS\" \\\n  '{\n    commit_id: $commit_id,\n    event: $event,\n    comments: $comments\n  } + (if $body != \"\" then {body: $body} else {} end)')\n\n# Check if HEAD has changed since review started (race condition detection)\nCURRENT_HEAD=$(gh api \"repos/${REPO}/pulls/${PR_NUMBER}\" --jq '.head.sha')\nif [ \"$CURRENT_HEAD\" != \"$HEAD_SHA\" ]; then\n  echo \"⚠️  WARNING: PR head has changed since review started!\"\n  echo \"   Review started at: ${HEAD_SHA:0:7}\"\n  echo \"   Current head:      ${CURRENT_HEAD:0:7}\"\n  echo \"\"\n  echo \"   New commits may have shifted line numbers. Review will be submitted\"\n  echo \"   against the original commit (${HEAD_SHA:0:7}) but comments may be outdated.\"\n  echo \"\"\nfi\n\necho \"Submitting ${EVENT} review for commit ${HEAD_SHA:0:7}...\"\n\n# Create and submit the review in one API call\n# Use a temp file to safely pass the JSON body\nTEMP_JSON=$(mktemp)\ntrap \"rm -f ${TEMP_JSON}\" EXIT\necho \"$REVIEW_JSON\" > \"${TEMP_JSON}\"\n\nRESPONSE=$(gh api \"repos/${REPO}/pulls/${PR_NUMBER}/reviews\" \\\n  -X POST \\\n  --input \"${TEMP_JSON}\" 2>&1) || {\n  echo \"Error submitting review:\"\n  echo \"$RESPONSE\"\n  exit 1\n}\n\n# Clean up the comments directory after successful submission\nif [ -d \"${COMMENTS_DIR}\" ] && [ \"$COMMENT_COUNT\" -gt 0 ]; then\n  rm -f \"${COMMENTS_DIR}\"/comment-*.json\n  # Remove directory if empty\n  rmdir \"${COMMENTS_DIR}\" 2>/dev/null || true\nfi\n\nREVIEW_URL=$(echo \"$RESPONSE\" | jq -r '.html_url // empty')\nREVIEW_STATE=$(echo \"$RESPONSE\" | jq -r '.state // empty')\n\nif [ -n \"$REVIEW_URL\" ]; then\n  echo \"✓ Review submitted (${REVIEW_STATE}): ${REVIEW_URL}\"\n  if [ \"$COMMENT_COUNT\" -gt 0 ]; then\n    echo \"  Included ${COMMENT_COUNT} inline comment(s)\"\n  fi\nelse\n  echo \"✓ Review submitted successfully\"\nfi\n"
  },
  {
    "path": ".github/workflows/auto-close-duplicates.yml",
    "content": "name: Auto-close duplicate issues\ndescription: Auto-closes issues that are duplicates of existing issues\non:\n  schedule:\n    - cron: \"0 9 * * *\" # Run daily at 9 AM UTC\n  workflow_dispatch:\n\njobs:\n  auto-close-duplicates:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      issues: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n\n      - name: Auto-close duplicate issues\n        run: uv run scripts/auto_close_duplicates.py\n        env:\n          GITHUB_TOKEN: ${{ steps.marvin-token.outputs.token }}\n          GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}\n          GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}\n"
  },
  {
    "path": ".github/workflows/auto-close-needs-mre.yml",
    "content": "name: Auto-close needs MRE issues\ndescription: Auto-closes issues that need minimal reproducible examples after 7 days of author inactivity\non:\n  schedule:\n    - cron: \"0 9 * * *\" # Run daily at 9 AM UTC\n  workflow_dispatch:\n\njobs:\n  auto-close-needs-mre:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      issues: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n\n      - name: Auto-close needs MRE issues\n        run: uv run scripts/auto_close_needs_mre.py\n        env:\n          GITHUB_TOKEN: ${{ steps.marvin-token.outputs.token }}\n          GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}\n          GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}\n"
  },
  {
    "path": ".github/workflows/martian-test-failure.yml",
    "content": "name: Marvin Test Failure Analysis\n\non:\n  workflow_run:\n    workflows: [\"Tests\", \"Run static analysis\"]\n    types:\n      - completed\n\nconcurrency:\n  group: marvin-test-failure-${{ github.event.workflow_run.head_branch }}\n  cancel-in-progress: true\n\njobs:\n  martian-test-failure:\n    # Only run if the test workflow failed\n    if: ${{ github.event.workflow_run.conclusion == 'failure' }}\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 1\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: Set up Python 3.10\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.10\"\n\n      # Install UV package manager\n      - name: Install UV\n        uses: astral-sh/setup-uv@v7\n\n      # Install dependencies\n      - name: Install dependencies\n        run: uv sync --all-packages --group dev\n\n      - name: Set analysis prompt\n        id: analysis-prompt\n        run: |\n          cat >> $GITHUB_OUTPUT << 'EOF'\n          PROMPT<<PROMPT_END\n          You're a test failure analysis assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients.\n\n          # Your Task\n          A GitHub Actions workflow has failed. Your job is to:\n          1. Analyze the test failure(s) to understand what went wrong\n          2. Identify the root cause of the failure(s)\n          3. Suggest a clear, actionable solution to fix the failure(s)\n\n          # Getting Started\n          1. Call the generate_agents_md tool to get a high-level summary of the project\n          2. Get the pull request associated with this workflow run from the GitHub repository: ${{ github.repository }}\n             - The workflow run ID is: ${{ github.event.workflow_run.id }}\n             - The workflow run was triggered by: ${{ github.event.workflow_run.event }}\n             - Use GitHub MCP tools to get PR details and workflow run information\n          3. Use the GitHub MCP tools to fetch job logs and failure information:\n             - Use get_workflow_run to get details about the failed workflow\n             - Use list_workflow_jobs to see which jobs failed\n             - Use get_job_logs with failed_only=true to get logs for failed jobs\n             - Use summarize_run_log_failures to get an AI summary of what failed\n          4. Analyze the failures to understand the root cause\n          5. Search the codebase for relevant files, tests, and implementations\n\n          # Your Response\n          Post a comment on the pull request with your analysis. Your comment should include:\n\n          ## Test Failure Analysis\n\n          **Summary**: A brief 1-2 sentence summary of what failed.\n\n          **Root Cause**: A clear explanation of why the tests failed, based on your analysis of the logs and code.\n\n          **Suggested Solution**: Specific, actionable steps to fix the failure(s). Include:\n          - Which files need to be modified\n          - What changes are needed\n          - Why these changes will fix the issue\n\n          <details>\n          <summary>Detailed Analysis</summary>\n\n          Include here:\n          - Relevant log excerpts showing the failure\n          - Code snippets that are causing the issue\n          - Any related issues or PRs that might be relevant\n          </details>\n\n          <details>\n          <summary>Related Files</summary>\n\n          List files that are relevant to the failure with brief explanations of their relevance.\n          </details>\n\n          # Important Guidelines\n          - Be concise and actionable - developers want to quickly understand and fix the issue. Provide\n            additional context, references, etc in collapsible details blocks to ensure that the comment you're adding\n            is short and easy to read but additional information is a click away.\n          - Focus on facts from the logs and code, not speculation\n          - If you can't determine the root cause, say so clearly\n          - If your only suggestion is a bad suggestion (disable the test, change the timeout, etc), indicate that you've run out of ideas and\n            that they probably don't want to do that.\n          - Provide specific file names, line numbers, and code references when possible\n          - You can run make commands (e.g., `make lint`, `make typecheck`, `make sync`) to build, test, or lint the code\n          - You can also run git commands (e.g., `git status`, `git log`, `git diff`) to inspect the repository\n          - You can use WebSearch and WebFetch to research errors, stack traces, or related issues\n          - For bash commands, you are limited to make and git commands only\n\n          # CRITICAL: ANGRY USERS\n          **IMPORTANT**: If the user is angry with you, the triage bot, don't respond. Just exit immediately without further action.\n            If at any point in the conversation the user has asked you to stop replying to the thread, just exit immediately.\n\n          If you are posting the same suggestion as you have previously made, do not post the suggestion again.\n\n          # IMPORTANT: EDIT YOUR COMMENT\n          Do not post a new comment every time you triage a failing workflow. If a previous comment has been posted by you (marvin)\n          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\n          your comment to reflect the latest analysis. Don't worry about keeping the old content around, there's comment history for \n          that.\n\n          # Problems Encountered\n          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.\n          PROMPT_END\n          EOF\n\n      - name: Setup GitHub MCP Server\n        run: |\n          mkdir -p /tmp/mcp-config\n          cat > /tmp/mcp-config/mcp-servers.json << 'EOF'\n          {\n            \"mcpServers\": {\n              \"repository-summary\": {\n                \"type\": \"http\",\n                \"url\": \"https://agents-md-generator.fastmcp.app/mcp\"\n              },\n              \"code-search\": {\n                \"type\": \"http\",\n                \"url\": \"https://public-code-search.fastmcp.app/mcp\"\n              },\n              \"github-research\": {\n                \"type\": \"stdio\",\n                \"command\": \"uvx\",\n                \"args\": [\n                  \"github-research-mcp\"\n                ],\n                \"env\": {\n                  \"DISABLE_SUMMARIES\": \"true\",\n                  \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"${{ secrets.GITHUB_TOKEN }}\"\n                }\n              }\n            }\n          }\n          EOF\n\n      - name: Clean up stale Claude locks\n        run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          github_token: ${{ steps.marvin-token.outputs.token }}\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}\n          bot_name: \"Marvin Context Protocol\"\n\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          additional_permissions: |\n            actions: read\n\n          prompt: ${{ steps.analysis-prompt.outputs.PROMPT }}\n          claude_args: |\n            --allowed-tools mcp__repository-summary,mcp__code-search,mcp__github-research,WebSearch,WebFetch,Bash(make:*,git:*)\n            --mcp-config /tmp/mcp-config/mcp-servers.json\n"
  },
  {
    "path": ".github/workflows/martian-triage-issue.yml",
    "content": "# Triage new issues: investigate, recommend, apply labels\n# Calls run-claude directly with triage prompt (elastic issue-triage style)\n\nname: Triage Issue\n\non:\n  issues:\n    types: [opened]\n\njobs:\n  triage:\n    if: |\n      github.event.issue.user.login == 'strawgate' ||\n      (github.event.issue.user.login == 'jlowin' && contains(toJSON(github.event.issue.labels.*.name), 'bug'))\n    concurrency:\n      group: triage-issue-${{ github.event.issue.number }}\n      cancel-in-progress: true\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      issues: write\n      pull-requests: read\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ github.repository }}\n          ref: ${{ github.event.repository.default_branch }}\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: React to issue with eyes\n        env:\n          GH_TOKEN: ${{ steps.marvin-token.outputs.token }}\n        run: |\n          gh api \"repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions\" -f content=eyes 2>/dev/null || true\n\n      - name: Run Claude for Triage\n        uses: ./.github/actions/run-claude\n        with:\n          claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          github-token: ${{ steps.marvin-token.outputs.token }}\n          allowed-tools: \"Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code\"\n          prompt: |\n            <context>\n            Repository: ${{ github.repository }}\n            Issue Number: #${{ github.event.issue.number }}\n            Issue Title: ${{ github.event.issue.title }}\n            Issue Author: ${{ github.event.issue.user.login }}\n            </context>\n\n            <issue_body>\n            ${{ github.event.issue.body }}\n            </issue_body>\n\n            <task>\n            Triage this new GitHub issue and provide a helpful, actionable response. You can write files and execute commands to test, verify, or investigate the issue.\n            </task>\n\n            <constraints>\n            This workflow is for investigation, testing, and planning.\n\n            You CANNOT: Create branches, checkout branches, commit code to the repository\n            Do not push changes to the repository.\n            You CAN: Read/analyze code, search repository, review git history, search for similar issues, write files, verify behavior, provide analysis and recommendations\n            </constraints>\n\n            <allowed_tools>\n            You have access to the following tools (comma-separated list):\n\n            Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code\n\n            You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.\n            </allowed_tools>\n\n            <getting_started>\n            Use `mcp__agents-md-generator__generate_agents_md` to get repository context before triaging.\n            </getting_started>\n\n            <investigation_tools>\n            - `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)\n            - `WebSearch`: Search the web for documentation, best practices, or solutions\n            - `WebFetch`: Fetch and read content from URLs\n            - Git commands: You have access to git commands, but write commands (commit, push, checkout, branch creation) are blocked\n            - Write: You can write files (e.g., test files, temporary files for verification)\n            - Execution: See `<allowed_tools>` section above for exact list of available execution commands\n            </investigation_tools>\n\n            <execution_guidelines>\n            If execution commands are available (check `<allowed_tools>` section), you can:\n            - Run tests to verify reported bugs or test proposed solutions\n            - Execute scripts to understand behavior\n            - Run linters or static analysis tools\n            - Verify environment setup or dependencies\n            - Test specific code paths or scenarios\n            - Write test files to confirm behavior\n\n            When executing commands:\n            - Explain what you're testing and why\n            - Include command output in your response when relevant\n            - Use execution to validate your findings and recommendations\n            - Only use commands that are explicitly listed in `<allowed_tools>`\n            </execution_guidelines>\n\n            <response_goals>\n            Your number one priority is to provide a great response to the issue. A great response is a response that is clear, concise, accurate, and actionable. You will avoid long paragraphs, flowery language, and overly verbose responses. Your readers have limited time and attention, so you will be concise and to the point.\n\n            In priority order your goal is to:\n              1. Provide context about the request or issue (related issues, pull requests, files, etc.)\n              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\n              3. Provide a high quality and detailed plan that a junior developer could follow to implement the recommendation\n              4. Use execution to verify findings when appropriate (check `<allowed_tools>` section for available commands)\n            </response_goals>\n\n            <response_sections>\n            Populate the following sections in your response:\n              Recommendation (or \"No recommendation\" with reason)\n              Findings\n              Verification (if you executed tests or commands - check `<allowed_tools>` section)\n              Detailed Action Plan\n              Related Items\n              Related Files\n              Related Webpages\n\n            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.\n\n            When formulating your response, you will never \"bury the lede\", you will always provide a clear and concise tl;dr as the first thing in your response. As your response grows in length you can organize the more detailed parts of your response collapsible sections using <details> and <summary> tags. You shouldn't put everything in collapsible sections, especially if the response is short. Use your discretion to determine when to use collapsible sections to avoid overwhelming the reader with too much detail -- think of them like an appendix that can be expanded if the reader is interested.\n\n            </response_sections>\n            <response_examples>\n            # Example output for \"Recommendation\" part of the response\n            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.\n\n            <details>\n            <summary>Findings</summary>\n            ...details from the code analysis that are relevant to the issue and the recommendation...\n            </details>\n\n            <details>\n            <summary>Verification</summary>\n            I ran the existing tests (if execution commands are available in `<allowed_tools>`) and confirmed the current behavior:\n            ```bash\n            $ pytest test_calculator.py::test_divide_by_zero\n            FAILED - raises ValueError instead of DivisionByZeroError\n            ```\n            This confirms the issue report is accurate.\n            </details>\n\n            <details>\n            <summary>Detailed Action Plan</summary>\n            ...a detailed plan that a junior developer could follow to implement the recommendation...\n            </details>\n\n            # Example Output for \"Related Items\" part of the response\n\n            <details>\n            <summary>Related Issues and Pull Requests</summary>\n\n            | Repository | Issue or PR | Relevance |\n            | --- | --- | --- |\n            | 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. |\n            | 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. |\n            </details>\n\n            <details>\n            <summary>Related Files</summary>\n\n            | Repository | File | Relevance | Sections |\n            | --- | --- | --- | --- |\n            | 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) |\n            | 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) |\n            </details>\n\n            <details>\n            <summary>Related Webpages</summary>\n\n            | Name | URL | Relevance |\n            | --- | --- | --- |\n            | 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. |\n            </details>\n            </response_examples>\n\n            <response_footer>\n            Always end your comment with a new line, three dashes, and the footer message:\n            <exact_content>\n\n            ---\n            Marvin Context Protocol | Type `/marvin` to interact further\n\n            Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.\n            </exact_content>\n            </response_footer>\n\n            <github_formatting>\n            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.\n            </github_formatting>\n"
  },
  {
    "path": ".github/workflows/marvin-comment-on-issue.yml",
    "content": "# Respond to /marvin mentions in issue comments (elastic mention-in-issue style)\n# Calls run-claude directly\n\nname: Comment on Issue\n\non:\n  issue_comment:\n    types: [created]\n\npermissions:\n  actions: read\n  contents: write\n  issues: write\n  pull-requests: write\n  id-token: write\n\njobs:\n  comment:\n    if: |\n      !github.event.issue.pull_request &&\n      contains(github.event.comment.body, '/marvin') &&\n      contains(fromJSON('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.comment.author_association)\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Install UV\n        uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Install dependencies\n        run: uv sync --python 3.12\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: React to comment with eyes\n        env:\n          GH_TOKEN: ${{ steps.marvin-token.outputs.token }}\n        run: |\n          gh api \"repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions\" -f content=eyes 2>/dev/null || true\n\n      - name: Run Claude for Issue Comment\n        uses: ./.github/actions/run-claude\n        with:\n          claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          github-token: ${{ steps.marvin-token.outputs.token }}\n          trigger-phrase: \"/marvin\"\n          allowed-bots: \"*\"\n          allowed-tools: \"Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code\"\n          prompt: |\n            <context>\n            Repository: ${{ github.repository }}\n            Issue Number: #${{ github.event.issue.number }}\n            Issue Title: ${{ github.event.issue.title }}\n            Issue Author: ${{ github.event.issue.user.login }}\n            Comment Author: ${{ github.event.comment.user.login }}\n            </context>\n\n            <user_request>\n            ${{ github.event.comment.body }}\n            </user_request>\n\n            <task>\n            You have been mentioned in a GitHub issue comment. Understand the request, gather context, complete the task, and respond with results.\n            </task>\n\n            <constraints>\n            You CAN: Read/analyze code, modify files, write code, run tests, execute commands\n            You CAN: Commit code, push changes, create branches, create pull requests\n\n            </constraints>\n\n            <allowed_tools>\n            You have access to the following tools (comma-separated list):\n\n            Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code\n\n            You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.\n            </allowed_tools>\n\n            <getting_started>\n            Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.\n            </getting_started>\n\n            <investigation_approach>\n            Be thorough in your investigations:\n            - Understand the full context of the repository\n            - Review related code, issues, and PRs\n            - Consider edge cases and implications\n            - Gather all relevant information before responding\n\n            Available tools:\n            - `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)\n            - `WebSearch`: Search the web for documentation, best practices, or solutions\n            - `WebFetch`: Fetch and read content from URLs\n            </investigation_approach>\n\n            <common_tasks>\n            - Answer questions about the codebase\n            - Help debug reported problems (make changes locally to test, cannot push)\n            - Suggest solutions or workarounds\n            - Provide code examples\n            - Help clarify requirements\n            - Link to relevant documentation or code\n            </common_tasks>\n\n            <response_guidelines>\n            - Be concise and actionable\n            - If the request is unclear, ask clarifying questions\n            - If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do\n            - When making code changes, explain that they are local only and cannot be pushed\n            </response_guidelines>\n\n            <response_footer>\n            Always end your comment with a new line, three dashes, and the footer message:\n            <exact_content>\n\n            ---\n            Marvin Context Protocol | Type `/marvin` to interact further\n\n            Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.\n            </exact_content>\n            </response_footer>\n\n            <github_formatting>\n            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.\n            </github_formatting>\n"
  },
  {
    "path": ".github/workflows/marvin-comment-on-pr.yml",
    "content": "# Respond to /marvin mentions in PR review comments and issue comments on PRs\n# Calls run-claude directly\n\nname: Comment on PR\n\non:\n  issue_comment:\n    types: [created]\n\npermissions:\n  contents: write\n  pull-requests: write\n  issues: read\n  id-token: write\n\njobs:\n  comment:\n    if: |\n      github.event.issue.pull_request &&\n      contains(github.event.comment.body, '/marvin') &&\n      contains(fromJSON('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.comment.author_association)\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n      - name: Checkout PR head branch\n        uses: actions/checkout@v6\n        with:\n          # do not set to pull_request.head.ref, claude will pull the branch if needed\n          fetch-depth: 0\n\n      - name: Install UV\n        uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Install dependencies\n        run: uv sync --python 3.12\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: React to comment with eyes\n        env:\n          GH_TOKEN: ${{ steps.marvin-token.outputs.token }}\n        run: |\n          gh api \"repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions\" -f content=eyes 2>/dev/null || true\n\n      - name: Get PR HEAD SHA\n        id: pr-info\n        env:\n          GH_TOKEN: ${{ steps.marvin-token.outputs.token }}\n        run: |\n          PR_NUMBER=\"${{ github.event.issue.number }}\"\n          HEAD_SHA=$(gh api \"repos/${{ github.repository }}/pulls/${PR_NUMBER}\" --jq '.head.sha')\n          echo \"head_sha=${HEAD_SHA}\" >> \"$GITHUB_OUTPUT\"\n          echo \"pr_number=${PR_NUMBER}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Run Claude for PR Comment\n        uses: ./.github/actions/run-claude\n        env:\n          MENTION_REPO: ${{ github.repository }}\n          MENTION_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}\n          MENTION_SCRIPTS: ${{ github.workspace }}/.github/scripts/mention\n          PR_REVIEW_REPO: ${{ github.repository }}\n          PR_REVIEW_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}\n          PR_REVIEW_HEAD_SHA: ${{ steps.pr-info.outputs.head_sha }}\n          PR_REVIEW_COMMENTS_DIR: /tmp/pr-review-comments\n          PR_REVIEW_HELPERS_DIR: ${{ github.workspace }}/.github/scripts/pr-review\n        with:\n          claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          github-token: ${{ steps.marvin-token.outputs.token }}\n          trigger-phrase: \"/marvin\"\n          allowed-bots: \"*\"\n          allowed-tools: \"Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code\"\n          prompt: |\n            <context>\n            Repository: ${{ github.repository }}\n            PR Number: #${{ steps.pr-info.outputs.pr_number }}\n            PR Title: ${{ github.event.issue.title }}\n            PR Author: ${{ github.event.issue.user.login }}\n            Comment Author: ${{ github.event.comment.user.login }}\n\n            **Note**: The PR head branch has already been checked out. The workspace is ready - you can immediately start working on the PR code.\n            </context>\n\n            <user_request>\n            ${{ github.event.comment.body }}\n            </user_request>\n\n            <task>\n            You have been mentioned in a Pull Request comment. Understand the request, gather context, complete the task, and respond with results.\n            </task>\n\n            <constraints>\n            This workflow allows read, write, and execute capabilities but cannot push changes.\n\n            You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads\n            You CANNOT: Commit code, push changes, create branches, checkout branches, create pull requests\n\n            **Important**: You cannot push changes to the repository - you can only make changes locally and provide feedback or recommendations.\n            </constraints>\n\n            <allowed_tools>\n            You have access to the following tools (comma-separated list):\n\n            Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code\n\n            You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.\n            </allowed_tools>\n\n            <getting_started>\n            Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.\n            </getting_started>\n\n            <investigation_approach>\n            Be thorough in your investigations:\n            - Understand the full context of the repository\n            - Review related code, issues, and PRs\n            - Consider edge cases and implications\n            - Gather all relevant information before responding\n\n            Available tools:\n            - `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)\n            - `WebSearch`: Search the web for documentation, best practices, or solutions\n            - `WebFetch`: Fetch and read content from URLs\n            </investigation_approach>\n\n            <common_tasks>\n            - Address review feedback and fix issues (make changes locally, cannot push)\n            - Answer questions about the changes\n            - Make additional code changes (local only)\n            - Resolve review threads after addressing feedback (if changes are made separately)\n            - Perform PR reviews when asked (use the PR review process below)\n            </common_tasks>\n\n            <pr_review_guidance>\n            When asked to review this PR, follow this structured review process.\n            The `$PR_REVIEW_HELPERS_DIR` environment variable is pre-configured for all scripts below.\n\n            <review_process>\n            Follow these steps in order:\n\n            **Step 1: Gather context**\n            - Use `mcp__agents-md-generator__generate_agents_md` to get repository context\n              (if this fails, explore the repository to understand the codebase — read key files like README, CONTRIBUTING, etc.)\n            - Run `$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --summary` to see existing review threads per file\n            - Run `$PR_REVIEW_HELPERS_DIR/pr-diff.sh` to see changed files with line-numbered diffs\n              (for large PRs, this lists files only — review each with `pr-diff.sh <filename>`)\n\n            **Step 2: Review each file**\n            For each changed file:\n            a. If the summary showed existing threads for this file, first run:\n               `$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --file <path>`\n               Read the full thread details. The output uses these conventions:\n               - `← has replies` — a conversation happened; read carefully before commenting\n               - `[truncated]` — comment was cut short; add `--full` if you need the complete text to understand the comment\n               - `[abc1234]` — commit the comment was made on; use `git show abc1234` if needed\n               - `~42` — approximate line from an older revision (exact line no longer maps to current diff)\n            b. Review the diff. Use `Read` to see full file contents when you need more context.\n               Identify issues matching review_criteria. Do NOT flag:\n               - Issues in unchanged code (only review the diff)\n               - Style preferences handled by linters\n               - Pre-existing issues not introduced by this PR\n               - Issues already covered by existing threads (see below)\n\n            **Existing thread rules** (check BEFORE leaving any comment):\n            - Resolved with reviewer reply → reviewer's decision is final. Do NOT re-flag.\n              Examples: \"It should remain as X\", \"This is intentional\", \"No need to do this change\"\n            - Resolved without reply → author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.\n            - Unresolved → already flagged. Do NOT re-comment. Mention in review body if you have more to add.\n            - Outdated → code changed. Only re-flag if the issue still applies to the current diff.\n            When in doubt, do not duplicate. Redundant comments erode trust in the review process.\n\n            **Step 3: Leave comments for NEW issues only**\n            For each genuinely new issue not covered by existing threads:\n            ```bash\n            $PR_REVIEW_HELPERS_DIR/pr-comment.sh <file> <line> \\\n              --severity <critical|high|medium|low|nitpick> \\\n              --title \"Brief description\" \\\n              --why \"Risk or impact\" <<'EOF'\n            corrected code here\n            EOF\n            ```\n            Always provide suggestion code. Use `--no-suggestion` only when the fix requires\n            changes across multiple locations. Broader architectural concerns belong in the\n            review body, not inline comments.\n\n            To remove a queued comment: `$PR_REVIEW_HELPERS_DIR/pr-remove-comment.sh <file> <line>`\n\n            **Step 4: Submit the review**\n            ```bash\n            $PR_REVIEW_HELPERS_DIR/pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> \"<review body>\"\n            ```\n            - REQUEST_CHANGES: Any 🔴 CRITICAL or 🟠 HIGH issues found\n            - COMMENT: 🟡 MEDIUM issues found (but no critical/high)\n            - APPROVE: No issues, or only ⚪ LOW / 💬 NITPICK suggestions\n\n            The review body should include broader architectural concerns not suited for inline comments.\n            Avoid summarizing the PR or offering praise. If approving with no issues, omit the review body.\n            A standard footer is automatically appended to all comments and reviews.\n            </review_process>\n\n            <severity_classification>\n            🔴 CRITICAL - Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)\n            🟠 HIGH - Should fix before merge (logic errors, missing validation, significant performance issues)\n            🟡 MEDIUM - Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)\n            ⚪ LOW - Author discretion, non-blocking (minor improvements, documentation, style not covered by linters)\n            💬 NITPICK - Truly optional (stylistic preferences, alternative approaches — safe to ignore)\n            </severity_classification>\n\n            <review_criteria>\n            Focus on these categories, in priority order:\n            1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)\n            2. Logic bugs that could cause runtime failures or incorrect behavior\n            3. Data integrity issues (race conditions, missing transactions, corruption risk)\n            4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)\n            5. Error handling gaps (unhandled exceptions, missing validation)\n            6. Breaking changes to public APIs without migration path\n            7. Missing or incorrect test coverage for critical paths\n            </review_criteria>\n            </pr_review_guidance>\n\n            <review_thread_tools>\n            View unresolved review threads:\n            ```bash\n            $MENTION_SCRIPTS/gh-get-review-threads.sh\n            ```\n\n            Filter for unresolved threads from a specific reviewer:\n            ```bash\n            $MENTION_SCRIPTS/gh-get-review-threads.sh \"reviewer-username\"\n            ```\n\n            Resolve a review thread after addressing feedback:\n            ```bash\n            $MENTION_SCRIPTS/gh-resolve-review-thread.sh \"THREAD_ID\" \"Fixed by updating the error handling\"\n            ```\n            - `THREAD_ID` is the GraphQL node ID from the review threads output (e.g., `PRRT_kwDOABC123`)\n            - The comment is optional - use it to explain what you did\n\n            Note: Since you cannot push changes, you can resolve threads to acknowledge feedback, but actual fixes would need to be applied separately.\n            </review_thread_tools>\n\n            <response_guidelines>\n            - Be concise and actionable\n            - If the request is unclear, ask clarifying questions\n            - If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do\n            - When making code changes, explain that they are local only and cannot be pushed\n\n            **When performing a PR review**: Your substantive feedback belongs in the PR review submission\n            (via pr-review.sh), not in the comment response. The comment should only report:\n            - That you've submitted the review (with the outcome: approved, requested changes, etc.)\n            - Any issues encountered during the review process\n            - Brief status updates\n\n            Do NOT duplicate the review content in your comment - the review itself contains all the details.\n            Keep the comment short, e.g., \"I've submitted my review requesting changes. See the review for details.\"\n            </response_guidelines>\n\n            <response_footer>\n            Always end your comment with a new line, three dashes, and the footer message:\n            <exact_content>\n\n            ---\n            Marvin Context Protocol | Type `/marvin` to interact further\n\n            Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.\n            </exact_content>\n            </response_footer>\n\n            <github_formatting>\n            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.\n            </github_formatting>\n"
  },
  {
    "path": ".github/workflows/marvin-dedupe-issues.yml",
    "content": "name: Marvin Issue Dedupe\n# description: Automatically dedupe GitHub issues using Marvin\non:\n  issues:\n    types: [opened]\n  workflow_dispatch:\n    inputs:\n      issue_number:\n        description: \"Issue number to process for duplicate detection\"\n        required: true\n        type: string\n\njobs:\n  marvin-dedupe-issues:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      issues: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - name: Set dedupe prompt\n        id: dedupe-prompt\n        run: |\n          cat >> $GITHUB_OUTPUT << 'EOF'\n          PROMPT<<PROMPT_END\n          Find up to 3 likely duplicate issues for GitHub issue ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}.\n\n          Follow these steps precisely:\n\n          1. Check if the GitHub issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.\n\n          2. View the GitHub issue and produce a summary of the issue\n\n          3. Then, launch 3 parallel agents using the Task tool to search GitHub for duplicates of this issue, using diverse keywords and search approaches, using the summary from step 2\n\n          4. Next, consider the results from steps 2 and 3 and filter out false positives that are likely not actually duplicates of the original issue. Be conservative — only flag issues that describe the same underlying problem, not issues that merely share keywords or involve the same subsystem. If there are no duplicates remaining, do not proceed.\n\n          5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates). If there are no duplicates, DO NOT COMMENT. Just exit. Do NOT add any labels — labeling is handled by a later workflow step.\n\n          Notes for your agents:\n          - Use `gh` to interact with GitHub, rather than web fetch\n          - Do not use other tools, beyond `gh` and Task (eg. don't use other MCP servers, file edit, etc.)\n          - Make a todo list first\n          - Never include this issue as a duplicate of itself\n\n          For your comment, follow this format precisely (example with 3 suspected duplicates):\n\n          ---\n          Found 3 possible duplicate issues:\n\n          1. #123: Issue title here\n          2. #456: Another issue title\n          3. #789: Third issue title\n\n          This issue will be automatically closed as a duplicate in 3 days.\n\n          - If your issue is a duplicate, please close it and 👍 the existing issue instead\n          - To prevent auto-closure, add a comment or 👎 this comment\n\n          ---\n          PROMPT_END\n          EOF\n\n      - name: Clean up stale Claude locks\n        run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true\n\n      - name: Run Marvin dedupe command\n        uses: anthropics/claude-code-action@v1\n        with:\n          github_token: ${{ steps.marvin-token.outputs.token }}\n          bot_name: \"Marvin Context Protocol\"\n          prompt: ${{ steps.dedupe-prompt.outputs.PROMPT }}\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}\n          allowed_non_write_users: \"*\"\n          claude_args: |\n            --allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task\n          settings: |\n            {\n              \"model\": \"claude-sonnet-4-6\",\n              \"env\": {\n                \"GH_TOKEN\": \"${{ steps.marvin-token.outputs.token }}\"\n              }\n            }\n\n      - name: Add potential-duplicate label if bot commented in this run\n        env:\n          GH_TOKEN: ${{ steps.marvin-token.outputs.token }}\n        run: |\n          ISSUE=${{ github.event.issue.number || inputs.issue_number }}\n          # Only match bot comments created in the last 10 minutes (this run)\n          CUTOFF=$(date -u -d '10 minutes ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \\\n            || date -u -v-10M '+%Y-%m-%dT%H:%M:%SZ')\n          HAS_RECENT=$(gh api \"repos/${{ github.repository }}/issues/${ISSUE}/comments?sort=created&direction=desc&per_page=10\" \\\n            --jq \"[.[] | select(\n              .user.type == \\\"Bot\\\" and\n              (.body | test(\\\"possible duplicate issues\\\"; \\\"i\\\")) and\n              .created_at >= \\\"${CUTOFF}\\\"\n            )] | length\")\n          if [ \"$HAS_RECENT\" -gt 0 ]; then\n            gh issue edit \"$ISSUE\" --add-label \"potential-duplicate\" -R \"${{ github.repository }}\"\n            echo \"Added potential-duplicate label to #${ISSUE}\"\n          else\n            echo \"No recent duplicate comment found, skipping label\"\n          fi\n"
  },
  {
    "path": ".github/workflows/marvin-label-triage.yml",
    "content": "name: Marvin Label Triage\n# Automatically triage GitHub issues and PRs using Marvin\n\non:\n  issues:\n    types: [opened]\n  pull_request_target:\n    types: [opened]\n  workflow_dispatch:\n    inputs:\n      issue_number:\n        description: \"Issue or PR number to triage\"\n        required: true\n        type: string\n\nconcurrency:\n  group: triage-${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}\n  cancel-in-progress: false\n\njobs:\n  label-issue-or-pr:\n    if: github.actor != 'dependabot[bot]'\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      issues: write\n      pull-requests: write\n\n    steps:\n      - name: Checkout base repository\n        uses: actions/checkout@v6\n        with:\n          repository: ${{ github.repository }}\n          ref: ${{ github.event.repository.default_branch }}\n\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n          owner: PrefectHQ\n\n      - name: Set triage prompt\n        id: triage-prompt\n        run: |\n          cat >> $GITHUB_OUTPUT << 'EOF'\n          PROMPT<<PROMPT_END\n          You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients. Your task is to analyze issues/PRs and apply appropriate labels.\n\n          IMPORTANT: Your primary action should be to apply labels using mcp__github__update_issue. DO NOT post comments EXCEPT when applying the too-long label (see below).\n\n          Issue/PR Information:\n          - REPO: ${{ github.repository }}\n          - NUMBER: ${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}\n          - TYPE: ${{ github.event.issue && 'issue' || (github.event.pull_request && 'pull_request') || 'unknown' }}\n\n          TRIAGE PROCESS:\n\n          1. Get available labels:\n             Run: `gh label list`\n\n          2. Retrieve issue/PR details using GitHub tools:\n             - mcp__github__get_issue: Get the issue/PR details\n             - mcp__github__get_issue_comments: Read any discussion\n             - If the issue/PR mentions other issues (e.g., \"fixes #123\", \"related to #456\"), use mcp__github__get_issue to read those linked issues for additional context\n\n          3. Analyze and apply labels based on these guidelines:\n\n          CORE CATEGORIES (apply EXACTLY ONE - these are mutually exclusive; skip if applying too-long):\n          - bug: Reports of broken functionality OR PRs that fix bugs\n          - enhancement: New functions/endpoints, improvements to existing features, internal tooling, workflow improvements, minor new capabilities\n          - feature: ONLY for major headline functionality worthy of a blog post announcement (2-4 per release, never for issues)\n          - documentation: Primary change is to user-facing docs, examples, or guides\n\n          SPECIAL DOCUMENTATION RULES:\n          - DO NOT apply \"documentation\" label if PR only updates auto-generated SDK docs (docs/python-sdk/**)\n          - DO apply \"documentation\" label for significant user-facing documentation changes (guides, examples, API docs)\n          - Auto-generated docs updates should get appropriate category label (enhancement, bug, etc.) based on the underlying code changes\n\n          FEATURE vs ENHANCEMENT guidance:\n          - feature: Major systems like new auth systems, MCP composition, proxying MCP servers, major CLI commands that transform workflows\n          - enhancement: New functions/endpoints, internal workflows, CI improvements, developer tooling, refactoring, utilities, typical new CLI commands\n          - If unsure between feature/enhancement, choose enhancement\n\n          Note: If a PR fixes a bug, label it \"bug\" not \"enhancement\"\n\n          SPECIAL CATEGORY (can be combined with above):\n          - breaking change: Changes that break backward compatibility (in addition to core category)\n\n          PRIORITY (apply if clearly evident):\n          - high-priority: Critical bugs affecting many users, security issues, or blocking core functionality\n          - low-priority: Edge cases, nice-to-have improvements, or cosmetic issues\n          - Default to no priority label if unclear\n\n          STATUS (apply if applicable):\n          - needs more info: Issue lacks reproduction steps, error messages, or clear description\n          - invalid: Spam, completely off-topic, or nonsensical (often LLM-generated)\n          - too-long: Issue/PR goes beyond what the contributor guidelines ask for. Typical signs: \"Root cause\" or \"Fix\" sections, proposed code changes, multi-step diagnostic writeups, speculative analysis, or structured reports that read like LLM output. The contributor guidelines ask for a short problem description and an MRE — anything beyond that (unless it reflects genuine, non-obvious investigation) is too much. This applies even when the extra content is accurate — unsolicited diagnosis transfers triage burden to maintainers. Apply this label and do not apply other triage labels. The author needs to condense before triage is worthwhile.\n\n          WHEN APPLYING too-long: After labeling, post a brief comment using mcp__github__add_issue_comment:\n            \"Thanks for the report. This issue goes beyond what our contributor guidelines ask for — we just need a short problem description and an MRE. Please see [our pinned guidelines](https://github.com/PrefectHQ/fastmcp/issues/3506) and condense this issue. We'll triage it once it's trimmed down.\"\n            Use this exact text (or very close to it). Do not editorialize or add details.\n\n          AREA LABELS (apply ONLY when thematically central to the issue):\n          - cli: Issues primarily about FastMCP CLI commands (run, dev, install)\n          - client: Issues primarily about the Client SDK or client-side functionality\n          - server: Issues primarily about FastMCP server implementation\n          - auth: Authentication is the main concern (Bearer, JWT, OAuth, WorkOS)\n          - openapi: OpenAPI integration/parsing is the primary topic\n          - http: HTTP transport or networking is the main issue\n          - contrib: Specifically about community contributions in src/contrib/\n          - tests: Issues primarily about testing infrastructure, CI/CD workflows, or test coverage\n          - security: Apply ONLY when the issue/PR addresses an exploitable vulnerability or hardens against one. Examples: SSRF, LFI, path traversal, injection, auth bypass allowing unauthorized access, scope escalation, open redirects. Do NOT apply for ordinary auth bugs (wrong scopes returned, token refresh logic, OAuth flow correctness) unless an attacker could exploit the bug to bypass access controls or escalate privileges. The key question: \"Could a malicious actor exploit this?\" If the answer is just \"it breaks for legitimate users,\" that's a bug, not a security issue.\n\n          IMPORTANT LABELING RULES:\n          - Be selective - only apply labels that are clearly relevant\n          - Don't apply area labels just because a file in that area is mentioned\n          - The issue must be PRIMARILY about that area to get the label\n          - When in doubt, don't apply the label\n          - Apply 2-5 labels total typically (category + maybe priority + maybe 1-2 areas)\n\n          META LABELS (rarely needed for issues):\n          - dependencies: Only for dependabot PRs or issues specifically about package updates\n          - DON'T MERGE: Only if PR author explicitly states it's not ready\n\n          4. Apply selected labels:\n             Use mcp__github__update_issue to apply your selected labels\n             DO NOT post any comments unless applying too-long (see above)\n          PROMPT_END\n          EOF\n\n      - name: Clean up stale Claude locks\n        run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true\n\n      - name: Run Marvin for Issue Triage\n        uses: anthropics/claude-code-action@v1\n        with:\n          github_token: ${{ steps.marvin-token.outputs.token }}\n          bot_name: \"Marvin Context Protocol\"\n          prompt: ${{ steps.triage-prompt.outputs.PROMPT }}\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}\n          allowed_non_write_users: \"*\"\n          allowed_bots: \"marvin-context-protocol\"\n          claude_args: |\n            --allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__add_issue_comment,mcp__github__get_pull_request_files\n          settings: |\n            {\n              \"model\": \"claude-sonnet-4-6\",\n              \"env\": {\n                \"GH_TOKEN\": \"${{ steps.marvin-token.outputs.token }}\"\n              }\n            }\n"
  },
  {
    "path": ".github/workflows/minimize-resolved-reviews.yml",
    "content": "# Minimize resolved PR review comments to reduce noise.\n#\n# Runs automatically on review activity for same-repo PRs. Fork PRs are\n# skipped because GITHUB_TOKEN is read-only in that context. Collaborators\n# can comment \"/tidy\" on any PR (including forks) to trigger manually.\n\nname: Minimize Resolved Reviews\n\non:\n  pull_request_review:\n    types: [submitted]\n  pull_request_review_comment:\n    types: [created, edited]\n  issue_comment:\n    types: [created]\n\nconcurrency:\n  group: minimize-reviews-${{ github.event.pull_request.number || github.event.issue.number }}\n  cancel-in-progress: true\n\npermissions:\n  pull-requests: write\n\njobs:\n  minimize:\n    # /tidy comment: collaborators can trigger on any PR (token has write access)\n    # Review events: skip fork PRs where GITHUB_TOKEN lacks write permissions\n    if: >-\n      (\n        github.event_name == 'issue_comment' &&\n        github.event.issue.pull_request &&\n        contains(github.event.comment.body, '/tidy') &&\n        contains(fromJSON('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.comment.author_association)\n      ) || (\n        github.event_name != 'issue_comment' &&\n        github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name\n      )\n    runs-on: ubuntu-latest\n    steps:\n      - name: Minimize resolved review comments\n        uses: strawgate/minimize-resolved-pr-reviews@v0\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish FastMCP to PyPI\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  pypi-publish:\n    name: Upload to PyPI\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write # For PyPI's trusted publishing\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: \"Install uv\"\n        uses: astral-sh/setup-uv@v7\n\n      - name: Build\n        run: uv build\n\n      - name: Publish to PyPi\n        run: uv publish -v dist/*\n"
  },
  {
    "path": ".github/workflows/run-static.yml",
    "content": "name: Run static analysis\n\nenv:\n  PY_COLORS: 1\n\non:\n  push:\n    branches: [\"main\"]\n    paths:\n      - \"src/**\"\n      - \"tests/**\"\n      - \"uv.lock\"\n      - \"pyproject.toml\"\n      - \".github/workflows/**\"\n\n  # run on all pull requests because these checks are required and will block merges otherwise\n  pull_request:\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  static_analysis:\n    timeout-minutes: 2\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv\n        uses: ./.github/actions/setup-uv\n        with:\n          resolution: locked\n\n      - name: Run prek\n        uses: j178/prek-action@v1\n        env:\n          SKIP: no-commit-to-branch\n"
  },
  {
    "path": ".github/workflows/run-tests.yml",
    "content": "name: Tests\n\nenv:\n  PY_COLORS: 1\n\non:\n  push:\n    branches: [\"main\"]\n    paths:\n      - \"src/**\"\n      - \"tests/**\"\n      - \"uv.lock\"\n      - \"pyproject.toml\"\n      - \".github/workflows/**\"\n\n  # run on all pull requests because these checks are required and will block merges otherwise\n  pull_request:\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  run_tests:\n    name: \"Tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}\"\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n        python-version: [\"3.10\"]\n        include:\n          - os: ubuntu-latest\n            python-version: \"3.13\"\n      fail-fast: false\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv\n        uses: ./.github/actions/setup-uv\n        with:\n          python-version: ${{ matrix.python-version }}\n          resolution: locked\n\n      - name: Run unit tests\n        uses: ./.github/actions/run-pytest\n\n      - name: Run client process tests\n        uses: ./.github/actions/run-pytest\n        with:\n          test-type: client_process\n\n  run_tests_lowest_direct:\n    name: \"Tests with lowest-direct dependencies\"\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv (lowest-direct)\n        uses: ./.github/actions/setup-uv\n        with:\n          resolution: lowest-direct\n\n      - name: Run unit tests\n        uses: ./.github/actions/run-pytest\n\n      - name: Run client process tests\n        uses: ./.github/actions/run-pytest\n        with:\n          test-type: client_process\n\n  run_integration_tests:\n    name: \"Integration tests\"\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv\n        uses: ./.github/actions/setup-uv\n        with:\n          resolution: locked\n\n      - name: Run integration tests\n        uses: ./.github/actions/run-pytest\n        with:\n          test-type: integration\n        env:\n          FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}\n          FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}\n          FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}\n"
  },
  {
    "path": ".github/workflows/run-upgrade-checks.yml",
    "content": "name: Upgrade checks\n\nenv:\n  PY_COLORS: 1\n\non:\n  push:\n    branches: [\"main\"]\n    paths:\n      - \"src/**\"\n      - \"tests/**\"\n      - \"uv.lock\"\n      - \"pyproject.toml\"\n      - \".github/workflows/**\"\n\n  schedule:\n    # Run daily at 2 AM UTC\n    - cron: \"0 2 * * *\"\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  issues: write\n\njobs:\n  static_analysis:\n    name: Static analysis\n    timeout-minutes: 2\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv (upgrade)\n        uses: ./.github/actions/setup-uv\n        with:\n          resolution: upgrade\n\n      - name: Run prek\n        uses: j178/prek-action@v1\n        env:\n          SKIP: no-commit-to-branch\n\n  run_tests:\n    name: \"Tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}\"\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n        python-version: [\"3.10\"]\n        include:\n          - os: ubuntu-latest\n            python-version: \"3.13\"\n      fail-fast: false\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv (upgrade)\n        uses: ./.github/actions/setup-uv\n        with:\n          python-version: ${{ matrix.python-version }}\n          resolution: upgrade\n\n      - name: Run unit tests\n        uses: ./.github/actions/run-pytest\n\n      - name: Run client process tests\n        uses: ./.github/actions/run-pytest\n        with:\n          test-type: client_process\n\n  run_integration_tests:\n    name: \"Integration tests\"\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup uv (upgrade)\n        uses: ./.github/actions/setup-uv\n        with:\n          resolution: upgrade\n\n      - name: Run integration tests\n        uses: ./.github/actions/run-pytest\n        with:\n          test-type: integration\n        env:\n          FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}\n          FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}\n          FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}\n\n  notify:\n    name: Notify on failure\n    needs: [static_analysis, run_tests, run_integration_tests]\n    if: failure() && github.event.pull_request == null\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Create or update failure issue\n        uses: jayqi/failed-build-issue-action@v1\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          label: \"build failed\"\n          title-template: \"Upgrade checks failing on main branch\"\n          body-template: |\n            ## Upgrade Checks Failure on Main Branch\n\n            The upgrade checks workflow has failed on the main branch.\n\n            **Workflow Run**: [#{{runNumber}}]({{serverUrl}}/{{repo.owner}}/{{repo.repo}}/actions/runs/{{runId}})\n            **Commit**: {{sha}}\n            **Branch**: {{ref}}\n            **Event**: {{eventName}}\n\n            ### Common causes\n\n            - **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`.\n            - **ruff**: New lint rules or stricter defaults in a ruff upgrade.\n            - **mcp SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).\n\n            ### What to do\n\n            1. Check the workflow logs to identify which job failed (static analysis vs tests)\n            2. Reproduce locally with `uv sync --upgrade && uv run prek run --all-files && uv run pytest -n auto`\n            3. Fix the code or adjust dependency constraints as needed\n\n            ---\n            *This issue was automatically created by a GitHub Action.*\n\n  close-on-success:\n    name: Close issue on success\n    needs: [static_analysis, run_tests, run_integration_tests]\n    if: success() && github.event.pull_request == null && github.ref == 'refs/heads/main'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Close resolved failure issue\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          issue=$(gh issue list \\\n            --repo \"$GITHUB_REPOSITORY\" \\\n            --label \"build failed\" \\\n            --state open \\\n            --json number \\\n            --jq '.[0].number // empty')\n\n          if [ -n \"$issue\" ]; then\n            gh issue close \"$issue\" \\\n              --repo \"$GITHUB_REPOSITORY\" \\\n              --comment \"Upgrade checks are passing again as of [\\`${GITHUB_SHA::7}\\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}).\"\n          fi\n"
  },
  {
    "path": ".github/workflows/update-config-schema.yml",
    "content": "name: Update MCPServerConfig Schema\n\n# Regenerates config schema on pushes to main and opens a long-lived PR\n# with the changes, so contributor PRs stay clean.\n\non:\n  push:\n    branches: [\"main\"]\n    paths:\n      - \"src/fastmcp/utilities/mcp_server_config/**\"\n      - \"!src/fastmcp/utilities/mcp_server_config/v1/schema.json\"\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  update-config-schema:\n    timeout-minutes: 5\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - uses: actions/checkout@v6\n        with:\n          token: ${{ steps.marvin-token.outputs.token }}\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Install dependencies\n        run: uv sync --python 3.12\n\n      - name: Generate config schema\n        run: |\n          uv run python -c \"\n          from fastmcp.utilities.mcp_server_config import generate_schema\n          generate_schema('docs/public/schemas/fastmcp.json/latest.json')\n          generate_schema('docs/public/schemas/fastmcp.json/v1.json')\n          generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')\n          \"\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v8\n        with:\n          token: ${{ steps.marvin-token.outputs.token }}\n          commit-message: \"chore: Update fastmcp.json schema\"\n          title: \"chore: Update fastmcp.json schema\"\n          body: |\n            This PR updates the fastmcp.json schema files to match the current source code.\n\n            The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.\n\n            **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.\n\n            🤖 Generated by Marvin\n          branch: marvin/update-config-schema\n          labels: |\n            ignore in release notes\n          delete-branch: true\n          author: \"marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>\"\n          committer: \"marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>\"\n"
  },
  {
    "path": ".github/workflows/update-sdk-docs.yml",
    "content": "name: Update SDK Documentation\n\n# Regenerates SDK docs on pushes to main and opens a long-lived PR\n# with the changes, so contributor PRs stay clean.\n\non:\n  push:\n    branches: [\"main\"]\n    paths:\n      - \"src/**\"\n      - \"pyproject.toml\"\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  update-sdk-docs:\n    timeout-minutes: 5\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Generate Marvin App token\n        id: marvin-token\n        uses: actions/create-github-app-token@v3\n        with:\n          app-id: ${{ secrets.MARVIN_APP_ID }}\n          private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}\n\n      - uses: actions/checkout@v6\n        with:\n          token: ${{ steps.marvin-token.outputs.token }}\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: true\n          cache-dependency-glob: \"uv.lock\"\n\n      - name: Install dependencies\n        run: uv sync --python 3.12\n\n      - name: Install just\n        uses: extractions/setup-just@v3\n\n      - name: Generate SDK documentation\n        run: just api-ref-all\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v8\n        with:\n          token: ${{ steps.marvin-token.outputs.token }}\n          commit-message: \"chore: Update SDK documentation\"\n          title: \"chore: Update SDK documentation\"\n          body: |\n            This PR updates the auto-generated SDK documentation to reflect the latest source code changes.\n\n            📚 Documentation is automatically generated from the source code docstrings and type annotations.\n\n            **Note:** This PR is fully automated and will update itself with any subsequent changes to the SDK, or close automatically if the documentation becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.\n\n            🤖 Generated by Marvin\n          branch: marvin/update-sdk-docs\n          labels: |\n            ignore in release notes\n          delete-branch: true\n          author: \"marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>\"\n          committer: \"marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Python-generated files\n__pycache__/\n*.py[cod]\n*$py.class\nbuild/\ndist/\nwheels/\n*.egg-info/\n*.egg\nMANIFEST\n.pytest_cache/\n.loq_cache\n.coverage\nhtmlcov/\n.tox/\nnosetests.xml\ncoverage.xml\n*.cover\n\n# Virtual environments\n.venv\nvenv/\nenv/\nENV/\n.env\n\n# System files\n.DS_Store\n\n# Version file\nsrc/fastmcp/_version.py\n\n# Editors and IDEs\n.cursorrules\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n.project\n.pydevproject\n.settings/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# Type checking\n.mypy_cache/\n.dmypy.json\ndmypy.json\n.pyre/\n.pytype/\n\n# Local development\n.python-version\n.envrc\n.envrc.private\n.direnv/\n\n# Logs and databases\n*.log\n*.sqlite\n*.db\n*.ddb\n\n# Claude worktree management\n.claude-wt/worktrees\n.claude/worktrees/\n\n# Agents\n/PLAN.md\n/TODO.md\n/STATUS.md\nplans/\n\n# Common FastMCP test files\n/test.py\n/server.py\n/client.py\n/test.json\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "fail_fast: false\n\nrepos:\n  - repo: https://github.com/abravalheri/validate-pyproject\n    rev: v0.24.1\n    hooks:\n      - id: validate-pyproject\n\n  - repo: https://github.com/pre-commit/mirrors-prettier\n    rev: v3.1.0\n    hooks:\n      - id: prettier\n        types_or: [yaml, json5]\n\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    # Ruff version.\n    rev: v0.14.10\n    hooks:\n      # Run the linter.\n      - id: ruff-check\n        args: [--fix, --exit-non-zero-on-fix]\n      # Run the formatter.\n      - id: ruff-format\n\n  - repo: local\n    hooks:\n      - id: ty\n        name: ty check\n        entry: uv run --isolated ty check\n        language: system\n        types: [python]\n        files: ^src/|^tests/\n        pass_filenames: false\n        require_serial: true\n\n      - id: loq\n        name: loq (file size limits)\n        entry: bash -c 'uv run loq || printf \"\\nloq violations not enforced... yet!\\n\"'\n        language: system\n        pass_filenames: false\n        verbose: true\n\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: no-commit-to-branch\n        name: prevent commits to main\n        args: [--branch, main]\n\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.4.1\n    hooks:\n      - id: codespell # See pyproject.toml for args\n        additional_dependencies:\n          - tomli\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# FastMCP Development Guidelines\n\n> **Audience**: LLM-driven engineering agents and human developers\n\n> **Note**: `AGENTS.md` is a symlink to this file. Edit `CLAUDE.md` directly.\n\nFastMCP is a comprehensive Python framework (Python ≥3.10) for building Model Context Protocol (MCP) servers and clients. This is the actively maintained v2.0 providing a complete toolkit for the MCP ecosystem.\n\n## Required Development Workflow\n\n**CRITICAL**: Always run these commands in sequence before committing.\n\n```bash\nuv sync                              # Install dependencies\nuv run pytest -n auto                # Run full test suite\n```\n\nIn addition, you must pass static checks. This is generally done as a pre-commit hook with `prek` but you can run it manually with:\n\n```bash\nuv run prek run --all-files          # Ruff + Prettier + ty\n```\n\n**Tests must pass and lint/typing must be clean before committing.**\n\n## Repository Structure\n\n| Path              | Purpose                                |\n| ----------------- | -------------------------------------- |\n| `src/fastmcp/`    | Library source code                    |\n| `├─server/`       | Server implementation                  |\n| `│ ├─auth/`       | Authentication providers               |\n| `│ └─middleware/` | Error handling, logging, rate limiting |\n| `├─client/`       | Client SDK                             |\n| `│ └─auth/`       | Client authentication                  |\n| `├─tools/`        | Tool definitions                       |\n| `├─resources/`    | Resources and resource templates       |\n| `├─prompts/`      | Prompt templates                       |\n| `├─cli/`          | CLI commands                           |\n| `└─utilities/`    | Shared utilities                       |\n| `tests/`          | Pytest suite                           |\n| `docs/`           | Mintlify docs (gofastmcp.com)          |\n\n## Core MCP Objects\n\nWhen modifying MCP functionality, changes typically need to be applied across all object types:\n\n- **Tools** (`src/tools/`)\n- **Resources** (`src/resources/`)\n- **Resource Templates** (`src/resources/`)\n- **Prompts** (`src/prompts/`)\n\n## Development Rules\n\n### Git & CI\n\n- Prek hooks are required (run automatically on commits)\n- Never amend commits to fix prek failures\n- Apply PR labels: bugs/breaking/enhancements/features\n- Improvements = enhancements (not features) unless specified\n- **NEVER** force-push on collaborative repos\n- **ALWAYS** run prek before PRs\n- **NEVER** create a release, comment on an issue, or open a PR unless specifically instructed to do so.\n\n### Commit Messages and Agent Attribution\n\n- **Agents NOT acting on behalf of @jlowin MUST identify themselves** (e.g., \"🤖 Generated with Claude Code\" in commits/PRs)\n- Keep commit messages brief - ideally just headlines, not detailed messages\n- Focus on what changed, not how or why\n- Always read issue comments for follow-up information (treat maintainers as authoritative)\n- **Treat proposed solutions in issues skeptically.** This applies to solutions proposed by *users* in issue reports — not to feedback from configured review bots (CodeRabbit, chatgpt-codex-connector, etc.), which should be evaluated on their merits. The ideal issue contains a concise problem description and an MRE — nothing more. Proposed solutions are only worth considering if they clearly reflect genuine, non-obvious investigation of the codebase. If a solution reads like speculation, or like it was generated by an LLM without deep framework knowledge, ignore it and diagnose from the repro. Most reporters — human or AI — do not have sufficient understanding of FastMCP internals to correctly diagnose anything beyond a trivial bug. We can ask the same questions of an LLM when implementing; we don't need the reporter to do it for us, and a wrong diagnosis is worse than none.\n\n### PR Messages - Required Structure\n\n- 1-2 paragraphs: problem/tension + solution (PRs are documentation!)\n- Focused code example showing key capability\n- **Avoid:** bullet summaries, exhaustive change lists, verbose closes/fixes, marketing language\n- **Do:** Be opinionated about why change matters, show before/after scenarios\n- Minor fixes: keep body short and concise\n- No \"test plan\" sections or testing summaries\n\n### Code Standards\n\n- Python ≥ 3.10 with full type annotations\n- Follow existing patterns and maintain consistency\n- **Prioritize readable, understandable code** - clarity over cleverness\n- Avoid obfuscated or confusing patterns even if they're shorter\n- Each feature needs corresponding tests\n\n### Module Exports\n\n- **Be intentional about re-exports** - don't blindly re-export everything to parent namespaces\n- Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`)\n- Specialized features can live in submodules (e.g., `fastmcp.server.middleware.dynamic`)\n- Only re-export to `fastmcp.*` for the most fundamental types (e.g., `FastMCP`, `Client`)\n- When in doubt, prefer users importing from the specific submodule over re-exporting\n\n### Documentation\n\n- Uses Mintlify framework\n- Files must be in docs.json to be included\n- 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.\n- 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.\n- **Core Principle:** A feature doesn't exist unless it is documented!\n- When adding or modifying settings in `src/fastmcp/settings.py`, update `docs/more/settings.mdx` to match.\n\n### Documentation Guidelines\n\n- **Code Examples:** Explain before showing code, make blocks fully runnable (include imports)\n- **Code Formatting:** Keep code blocks visually clean — avoid deeply nested function calls. Extract intermediate values into named variables rather than inlining everything into one expression. Code in docs is read more than it's run; optimize for scannability.\n- **Structure:** Headers form navigation guide, logical H2/H3 hierarchy\n- **Content:** User-focused sections, motivate features (why) before mechanics (how)\n- **Style:** Prose over code comments for important information\n- **Docstrings:** FastMCP docstrings are automatically compiled into MDX documents. Use markdown (single backticks, fenced code blocks), not RST (no double backticks). Bare `{}` in examples will be interpreted as JSX — wrap in backticks instead.\n\n## Critical Patterns\n\n- Never use bare `except` - be specific with exception types\n- File sizes enforced by [loq](https://github.com/jakekaplan/loq). Edit `loq.toml` to raise limits; `loq baseline` to ratchet down.\n- Always `uv sync` first when debugging build issues\n- Default test timeout is 5s - optimize or mark as integration tests\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nchris@prefect.io.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to FastMCP\n\nFastMCP is an actively maintained, high-traffic project. We welcome contributions — but the most impactful way to contribute might not be what you expect.\n\n## The best contribution is a great issue\n\nFastMCP is an opinionated framework, and its maintainers use AI-assisted tooling that is deeply tuned to those opinions — the design philosophy, the API patterns, the way the framework is meant to evolve. A well-written issue with a clear problem description is often more valuable than a pull request, because it lets maintainers produce a solution that isn't just correct, but consistent with how the framework wants to work. That matters more than speed, though it's faster too.\n\n**A great issue looks like this:**\n\n1. A short, motivating description of the problem or gap\n2. A minimal reproducible example (for bugs) or a concrete use case (for enhancements)\n3. A brief note on expected vs. actual behavior\n\nThat's it. No need to diagnose root causes, propose API designs, or suggest implementations. If you've done genuine investigation and have a non-obvious insight, include it.\n\n## Using AI to contribute\n\nWe encourage you to use LLMs to help identify bugs, write MREs, and prepare contributions. But if you do, your LLM must take into account the conventions and contributing guidelines of this repo — including how we want issues formatted and when it's appropriate to open a PR. Generic LLM output that ignores these guidelines tells us the contribution wasn't made thoughtfully, and we will close it. A good AI-assisted contribution is indistinguishable from a good human one. A bad one is obvious.\n\n## When to open a pull request\n\n**Bug fixes** — PRs are welcome for simple, well-scoped bug fixes where the problem and solution are both straightforward. \"The function raises `TypeError` when passed `None` because of a missing guard\" is a good candidate. If the fix requires design decisions or touches multiple subsystems, open an issue instead.\n\n**Documentation** — Typo fixes, clarifications, and improvements to examples are always welcome as PRs.\n\n**Enhancements and features** — For changes that affect the behavior or design of the framework, please open an issue first. Maintainers will typically implement these themselves. FastMCP is opinionated, and enhancements need to reflect those opinions — not just solve the problem, but solve it in a way that's consistent with the framework's design. That's hard to do from the outside, and it's why a clear problem description is more useful than a proposed solution.\n\n**Integrations** — FastMCP generally does not accept PRs that add third-party integrations (custom middleware, provider-specific adapters, etc.). If you're building something for your users, ship it as a standalone package — that's a feature, not a limitation. Authentication providers are an exception, since auth is tightly coupled to the framework.\n\n## PR guidelines\n\nIf you do open a PR:\n\n- **Reference an issue.** Every PR should address a tracked issue. If there isn't one, open an issue first. This isn't a permission step — you don't need to wait for a response. But the issue gives us context on the problem, and if a maintainer is already working on it, we can let you know before you invest time in code.\n- **Keep it focused.** One logical change per PR. Don't bundle unrelated fixes or refactors.\n- **Match existing patterns.** Follow the code style, type annotation conventions, and test patterns you see in the codebase. Run `uv run prek run --all-files` before submitting.\n- **Write tests.** Bug fixes should include a test that fails without the fix. Enhancements should include tests for the new behavior.\n- **Don't submit generated boilerplate.** We review every line. PRs that read like unedited LLM output — verbose descriptions, speculative changes, shotgun-style fixes — will be closed.\n\n## What we'll close without review\n\nTo keep the project maintainable, we will close PRs that:\n\n- Don't reference an issue or address a clearly self-evident bug\n- Make sweeping changes without prior discussion\n- Add third-party integrations that belong in a separate package\n- Are difficult to review due to size, scope, or generated content\n\nThis isn't personal. FastMCP receives a high volume of contributions and we need to focus maintainer time where it has the most impact — which is why a good issue is often the best thing you can do for the project.\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<!-- omit in toc -->\n\n<picture>\n  <source width=\"550\" media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/brand/f-watercolor-waves-4-dark.png\">\n  <source width=\"550\" media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/brand/f-watercolor-waves-4.png\">\n  <img width=\"550\" alt=\"FastMCP Logo\" src=\"https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/brand/f-watercolor-waves-2.png\">\n</picture>\n\n# FastMCP 🚀\n\n<strong>Move fast and make things.</strong>\n\n*Made with 💙 by [Prefect](https://www.prefect.io/)*\n\n[![Docs](https://img.shields.io/badge/docs-gofastmcp.com-blue)](https://gofastmcp.com)\n[![Discord](https://img.shields.io/badge/community-discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/uu8dJCgttd)\n[![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp)\n[![Tests](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml)\n[![License](https://img.shields.io/github/license/PrefectHQ/fastmcp.svg)](https://github.com/PrefectHQ/fastmcp/blob/main/LICENSE)\n\n<a href=\"https://trendshift.io/repositories/13266\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/13266\" alt=\"prefecthq%2Ffastmcp | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n---\n\nThe [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:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Demo 🚀\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Why FastMCP\n\nBuilding 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.**\n\n**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.\n\nFastMCP has three pillars:\n\n<table>\n<tr>\n<td align=\"center\" valign=\"top\" width=\"33%\">\n<a href=\"https://gofastmcp.com/servers/server\">\n<img src=\"https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/images/servers-card.png\" alt=\"Servers\" />\n<br /><strong>Servers</strong>\n</a>\n<br />Expose tools, resources, and prompts to LLMs.\n</td>\n<td align=\"center\" valign=\"top\" width=\"33%\">\n<a href=\"https://gofastmcp.com/apps/overview\">\n<img src=\"https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/images/apps-card.png\" alt=\"Apps\" />\n<br /><strong>Apps</strong>\n</a>\n<br />Give your tools interactive UIs rendered directly in the conversation.\n</td>\n<td align=\"center\" valign=\"top\" width=\"33%\">\n<a href=\"https://gofastmcp.com/clients/client\">\n<img src=\"https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/images/clients-card.png\" alt=\"Clients\" />\n<br /><strong>Clients</strong>\n</a>\n<br />Connect to any MCP server — local or remote, programmatic or CLI.\n</td>\n</tr>\n</table>\n\n**[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.\n\nReady to build? Start with the [installation guide](https://gofastmcp.com/getting-started/installation) or jump straight to the [quickstart](https://gofastmcp.com/getting-started/quickstart). When you're ready to deploy, [Prefect Horizon](https://www.prefect.io/horizon) offers free hosting for FastMCP users.\n\n## Installation\n\nWe recommend installing FastMCP with [uv](https://docs.astral.sh/uv/):\n\n```bash\nuv pip install fastmcp\n```\n\nFor full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).\n\n**Upgrading?** We have guides for:\n- [Upgrading from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)\n- [Upgrading from the MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk)\n- [Upgrading from the low-level SDK](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk)\n\n## 📚 Documentation\n\nFastMCP's complete documentation is available at **[gofastmcp.com](https://gofastmcp.com)**, including detailed guides, API references, and advanced patterns.\n\nDocumentation is also available in [llms.txt format](https://llmstxt.org/), which is a simple markdown standard that LLMs can consume easily:\n\n- [`llms.txt`](https://gofastmcp.com/llms.txt) is essentially a sitemap, listing all the pages in the documentation.\n- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the entire documentation. Note this may exceed the context window of your LLM.\n\n**Community:** Join our [Discord server](https://discord.gg/uu8dJCgttd) to connect with other FastMCP developers and share what you're building.\n\n## Contributing\n\nWe welcome contributions! See the [Contributing Guide](https://gofastmcp.com/development/contributing) for setup instructions, testing requirements, and PR guidelines.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 3.x     | :white_check_mark: |\n| 2.x     | :x:                |\n| 1.x     | :x:                |\n| 0.x     | :x:                |\n\n## Reporting a Vulnerability\n\nPlease report security vulnerabilities privately using [GitHub's security advisory feature](https://github.com/PrefectHQ/fastmcp/security/advisories/new). Do not open public issues for security concerns.\n\n## Scope\n\nWe accept reports for vulnerabilities in FastMCP itself — the library code in this repository.\n\nThe following are **out of scope**:\n\n- Vulnerabilities in third-party dependencies or the MCP SDK itself. We'll bump version floors for known CVEs, but the fix belongs upstream.\n- Limitations of upstream identity providers that FastMCP cannot control.\n- Issues that require the attacker to already have server-side access or control of the MCP server configuration.\n\n## Disclosure Process\n\nWhen we receive a valid report:\n\n1. We triage the report and determine whether it affects FastMCP directly.\n2. We develop and test a fix on a private branch.\n3. We coordinate CVE assignment through GitHub's advisory process when warranted.\n4. We publish the advisory and release a patched version.\n5. We credit the reporter in the advisory (unless they prefer otherwise).\n"
  },
  {
    "path": "docs/.ccignore",
    "content": "changelog.mdx\npython-sdk/\n"
  },
  {
    "path": "docs/.cursor/rules/mintlify.mdc",
    "content": "---\ndescription: \nglobs: *.mdx\nalwaysApply: false\n---\n# Mintlify technical writing assistant\n\nYou are an AI writing assistant specialized in creating exceptional technical documentation using Mintlify components and following industry-leading technical writing practices.\n\n## Core writing principles\n\n### Language and style requirements\n- Use clear, direct language appropriate for technical audiences\n- Write in second person (\"you\") for instructions and procedures\n- Use active voice over passive voice\n- Employ present tense for current states, future tense for outcomes\n- Maintain consistent terminology throughout all documentation\n- Keep sentences concise while providing necessary context\n- Use parallel structure in lists, headings, and procedures\n\n### Content organization standards\n- Lead with the most important information (inverted pyramid structure)\n- Use progressive disclosure: basic concepts before advanced ones\n- Break complex procedures into numbered steps\n- Include prerequisites and context before instructions\n- Provide expected outcomes for each major step\n- End sections with next steps or related information\n- Use descriptive, keyword-rich headings for navigation and SEO\n\n### User-centered approach\n- Focus on user goals and outcomes rather than system features\n- Anticipate common questions and address them proactively\n- Include troubleshooting for likely failure points\n- Provide multiple pathways when appropriate (beginner vs advanced), but offer an opinionated path for people to follow to avoid overwhelming with options\n\n## Mintlify component reference\n\n### Callout components\n\n#### Note - Additional helpful information\n\n<Note>\nSupplementary information that supports the main content without interrupting flow\n</Note>\n\n#### Tip - Best practices and pro tips\n\n<Tip>\nExpert advice, shortcuts, or best practices that enhance user success\n</Tip>\n\n#### Warning - Important cautions\n\n<Warning>\nCritical information about potential issues, breaking changes, or destructive actions\n</Warning>\n\n#### Info - Neutral contextual information\n\n<Info>\nBackground information, context, or neutral announcements\n</Info>\n\n#### Check - Success confirmations\n\n<Check>\nPositive confirmations, successful completions, or achievement indicators\n</Check>\n\n### Code components\n\n#### Single code block\n\n```javascript config.js\nconst apiConfig = {\nbaseURL: 'https://api.example.com',\ntimeout: 5000,\nheaders: {\n    'Authorization': `Bearer ${process.env.API_TOKEN}`\n}\n};\n```\n\n#### Code group with multiple languages\n\n<CodeGroup>\n```javascript Node.js\nconst response = await fetch('/api/endpoint', {\n    headers: { Authorization: `Bearer ${apiKey}` }\n});\n```\n\n```python Python\nimport requests\nresponse = requests.get('/api/endpoint', \n    headers={'Authorization': f'Bearer {api_key}'})\n```\n\n```curl cURL\ncurl -X GET '/api/endpoint' \\\n    -H 'Authorization: Bearer YOUR_API_KEY'\n```\n</CodeGroup>\n\n#### Request/Response examples\n\n<RequestExample>\n```bash cURL\ncurl -X POST 'https://api.example.com/users' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}'\n```\n</RequestExample>\n\n<ResponseExample>\n```json Success\n{\n    \"id\": \"user_123\",\n    \"name\": \"John Doe\", \n    \"email\": \"john@example.com\",\n    \"created_at\": \"2024-01-15T10:30:00Z\"\n}\n```\n</ResponseExample>\n\n### Structural components\n\n#### Steps for procedures\n\n<Steps>\n<Step title=\"Install dependencies\">\n    Run `npm install` to install required packages.\n    \n    <Check>\n    Verify installation by running `npm list`.\n    </Check>\n</Step>\n\n<Step title=\"Configure environment\">\n    Create a `.env` file with your API credentials.\n    \n    ```bash\n    API_KEY=your_api_key_here\n    ```\n    \n    <Warning>\n    Never commit API keys to version control.\n    </Warning>\n</Step>\n</Steps>\n\n#### Tabs for alternative content\n\n<Tabs>\n<Tab title=\"macOS\">\n    ```bash\n    brew install node\n    npm install -g package-name\n    ```\n</Tab>\n\n<Tab title=\"Windows\">\n    ```powershell\n    choco install nodejs\n    npm install -g package-name\n    ```\n</Tab>\n\n<Tab title=\"Linux\">\n    ```bash\n    sudo apt install nodejs npm\n    npm install -g package-name\n    ```\n</Tab>\n</Tabs>\n\n#### Accordions for collapsible content\n\n<AccordionGroup>\n<Accordion title=\"Troubleshooting connection issues\">\n    - **Firewall blocking**: Ensure ports 80 and 443 are open\n    - **Proxy configuration**: Set HTTP_PROXY environment variable\n    - **DNS resolution**: Try using 8.8.8.8 as DNS server\n</Accordion>\n\n<Accordion title=\"Advanced configuration\">\n    ```javascript\n    const config = {\n    performance: { cache: true, timeout: 30000 },\n    security: { encryption: 'AES-256' }\n    };\n    ```\n</Accordion>\n</AccordionGroup>\n\n### API documentation components\n\n#### Parameter fields\n\n<ParamField path=\"user_id\" type=\"string\" required>\nUnique identifier for the user. Must be a valid UUID v4 format.\n</ParamField>\n\n<ParamField body=\"email\" type=\"string\" required>\nUser's email address. Must be valid and unique within the system.\n</ParamField>\n\n<ParamField query=\"limit\" type=\"integer\" default=\"10\">\nMaximum number of results to return. Range: 1-100.\n</ParamField>\n\n<ParamField header=\"Authorization\" type=\"string\" required>\nBearer token for API authentication. Format: `Bearer YOUR_API_KEY`\n</ParamField>\n\n#### Response fields\n\n<ResponseField name=\"user_id\" type=\"string\" required>\nUnique identifier assigned to the newly created user.\n</ResponseField>\n\n<ResponseField name=\"created_at\" type=\"timestamp\">\nISO 8601 formatted timestamp of when the user was created.\n</ResponseField>\n\n<ResponseField name=\"permissions\" type=\"array\">\nList of permission strings assigned to this user.\n</ResponseField>\n\n#### Expandable nested fields\n\n<ResponseField name=\"user\" type=\"object\">\nComplete user object with all associated data.\n\n<Expandable title=\"User properties\">\n    <ResponseField name=\"profile\" type=\"object\">\n    User profile information including personal details.\n    \n    <Expandable title=\"Profile details\">\n        <ResponseField name=\"first_name\" type=\"string\">\n        User's first name as entered during registration.\n        </ResponseField>\n        \n        <ResponseField name=\"avatar_url\" type=\"string | null\">\n        URL to user's profile picture. Returns null if no avatar is set.\n        </ResponseField>\n    </Expandable>\n    </ResponseField>\n</Expandable>\n</ResponseField>\n\n### Interactive components\n\n#### Cards for navigation\n\n<Card title=\"Getting started guide\" icon=\"rocket\" href=\"/quickstart\">\nComplete walkthrough from installation to your first API call in under 10 minutes.\n</Card>\n\n<CardGroup cols={2}>\n<Card title=\"Authentication\" icon=\"key\" href=\"/auth\">\n    Learn how to authenticate requests using API keys or JWT tokens.\n</Card>\n\n<Card title=\"Rate limiting\" icon=\"clock\" href=\"/rate-limits\">\n    Understand rate limits and best practices for high-volume usage.\n</Card>\n</CardGroup>\n\n### Media and advanced components\n\n#### Frames for images\n\nWrap all images in frames.\n\n<Frame>\n<img src=\"/images/dashboard.png\" alt=\"Main dashboard showing analytics overview\" />\n</Frame>\n\n<Frame caption=\"The analytics dashboard provides real-time insights\">\n<img src=\"/images/analytics.png\" alt=\"Analytics dashboard with charts\" />\n</Frame>\n\n#### Tooltips and updates\n\n<Tooltip tip=\"Application Programming Interface - protocols for building software\">\nAPI\n</Tooltip>\n\n<Update label=\"Version 2.1.0\" description=\"Released March 15, 2024\">\n## New features\n- Added bulk user import functionality\n- Improved error messages with actionable suggestions\n\n## Bug fixes\n- Fixed pagination issue with large datasets\n- Resolved authentication timeout problems\n</Update>\n\n## Required page structure\n\nEvery documentation page must begin with YAML frontmatter:\n\n```yaml\n---\ntitle: \"Clear, specific, keyword-rich title\"\ndescription: \"Concise description explaining page purpose and value\"\n---\n```\n\n## Content quality standards\n\n### Code examples requirements\n- Always include complete, runnable examples that users can copy and execute\n- Show proper error handling and edge case management\n- Use realistic data instead of placeholder values\n- Include expected outputs and results for verification\n- Test all code examples thoroughly before publishing\n- Specify language and include filename when relevant\n- Add explanatory comments for complex logic\n\n### API documentation requirements\n- Document all parameters including optional ones with clear descriptions\n- Show both success and error response examples with realistic data\n- Include rate limiting information with specific limits\n- Provide authentication examples showing proper format\n- Explain all HTTP status codes and error handling\n- Cover complete request/response cycles\n\n### Accessibility requirements\n- Include descriptive alt text for all images and diagrams\n- Use specific, actionable link text instead of \"click here\"\n- Ensure proper heading hierarchy starting with H2\n- Provide keyboard navigation considerations\n- Use sufficient color contrast in examples and visuals\n- Structure content for easy scanning with headers and lists\n\n## AI assistant instructions\n\n### Component selection logic\n- Use **Steps** for procedures, tutorials, setup guides, and sequential instructions\n- Use **Tabs** for platform-specific content or alternative approaches\n- Use **CodeGroup** when showing the same concept in multiple languages\n- Use **Accordions** for supplementary information that might interrupt flow\n- Use **Cards and CardGroup** for navigation, feature overviews, and related resources\n- Use **RequestExample/ResponseExample** specifically for API endpoint documentation\n- Use **ParamField** for API parameters, **ResponseField** for API responses\n- Use **Expandable** for nested object properties or hierarchical information\n\n### Quality assurance checklist\n- Verify all code examples are syntactically correct and executable\n- Test all links to ensure they are functional and lead to relevant content\n- Validate Mintlify component syntax with all required properties\n- Confirm proper heading hierarchy with H2 for main sections, H3 for subsections\n- Ensure content flows logically from basic concepts to advanced topics\n- Check for consistency in terminology, formatting, and component usage\n\n### Error prevention strategies\n- Always include realistic error handling in code examples\n- Provide dedicated troubleshooting sections for complex procedures\n- Explain prerequisites clearly before beginning instructions\n- Include verification and testing steps with expected outcomes\n- Add appropriate warnings for destructive or security-sensitive actions\n- Validate all technical information through testing before publication"
  },
  {
    "path": "docs/apps/development.mdx",
    "content": "---\ntitle: Development\nsidebarTitle: Development\ndescription: Preview and test your app tools locally without a full MCP host.\nicon: flask\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.2.0\" />\n\n`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.\n\nThis works with both [Prefab apps](/apps/prefab) and [custom HTML apps](/apps/low-level).\n\n## Quick Start\n\n```bash\npip install \"fastmcp[apps]\"\nfastmcp dev apps server.py\n```\n\nThe 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.\n\n## How It Works\n\nThe dev server does three things:\n\nThe **picker page** connects to your MCP server, finds all tools with UI metadata, and renders a form for each one. The forms are auto-generated from the tool's input schema — text fields, dropdowns, checkboxes, all wired up.\n\nWhen you submit a form, the dev server **calls your tool** via the MCP protocol and opens the result in a new tab. The result page loads the tool's UI resource (the Prefab renderer or your custom HTML) inside an AppBridge — the same protocol that real MCP hosts use.\n\nA **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.\n\n## Options\n\n```bash\nfastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload\n```\n\n| Option | Flag | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| MCP Port | `--mcp-port` | `8000` | Port for your MCP server |\n| Dev Port | `--dev-port` | `8080` | Port for the dev UI |\n| Auto-Reload | `--reload` / `--no-reload` | On | Watch files and restart the server on changes |\n\n## Multiple Tools\n\nIf 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.\n\n```bash\n# Server with multiple app tools\nfastmcp dev apps examples/apps/contacts/contacts_server.py\n```\n"
  },
  {
    "path": "docs/apps/low-level.mdx",
    "content": "---\ntitle: Custom HTML Apps\nsidebarTitle: Custom HTML\ndescription: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.\nicon: code\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nThe [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.\n\nThis 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.\n\n## How It Works\n\nAn MCP App has two parts:\n\n1. A **tool** that does the work and returns data\n2. A **`ui://` resource** containing the HTML that renders that data\n\nThe tool declares which resource to use via `AppConfig`. When the host calls the tool, it also fetches the linked resource, renders it in a sandboxed iframe, and pushes the tool result into the app via `postMessage`. The app can also call tools back, enabling interactive workflows.\n\n```python\nimport json\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.apps import AppConfig, ResourceCSP\n\nmcp = FastMCP(\"My App Server\")\n\n# The tool does the work\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\"))\ndef generate_chart(data: list[float]) -> str:\n    return json.dumps({\"values\": data})\n\n# The resource provides the UI\n@mcp.resource(\"ui://my-app/view.html\")\ndef chart_view() -> str:\n    return \"<html>...</html>\"\n```\n\n## AppConfig\n\n`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`:\n\n```python\nfrom fastmcp.server.apps import AppConfig\n```\n\nOn **tools**, you'll typically set `resource_uri` to point to the UI resource:\n\n```python\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\"))\ndef my_tool() -> str:\n    return \"result\"\n```\n\nYou can also pass a raw dict with camelCase keys, matching the wire format:\n\n```python\n@mcp.tool(app={\"resourceUri\": \"ui://my-app/view.html\"})\ndef my_tool() -> str:\n    return \"result\"\n```\n\n### Tool Visibility\n\nThe `visibility` field controls where a tool appears:\n\n- `[\"model\"]` — visible to the LLM (the default behavior)\n- `[\"app\"]` — only callable from within the app UI, hidden from the LLM\n- `[\"model\", \"app\"]` — both\n\nThis is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions.\n\n```python\n@mcp.tool(\n    app=AppConfig(\n        resource_uri=\"ui://my-app/view.html\",\n        visibility=[\"app\"],\n    )\n)\ndef refresh_data() -> str:\n    \"\"\"Only callable from the app UI, not by the LLM.\"\"\"\n    return fetch_latest()\n```\n\n### AppConfig Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `resource_uri` | `str` | URI of the UI resource. Tools only. |\n| `visibility` | `list[str]` | Where the tool appears: `\"model\"`, `\"app\"`, or both. Tools only. |\n| `csp` | `ResourceCSP` | Content Security Policy for the iframe. |\n| `permissions` | `ResourcePermissions` | Iframe sandbox permissions. |\n| `domain` | `str` | Stable sandbox origin for the iframe. |\n| `prefers_border` | `bool` | Whether the UI prefers a visible border. |\n\n<Note>\nOn **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.\n</Note>\n\n## UI Resources\n\nResources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. You don't need to set this manually.\n\n```python\n@mcp.resource(\"ui://my-app/view.html\")\ndef my_view() -> str:\n    return \"<html>...</html>\"\n```\n\nThe 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.\n\n### Writing the App HTML\n\nYour 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:\n\n```html\n<script type=\"module\">\n  import { App } from \"https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps\";\n\n  const app = new App({ name: \"My App\", version: \"1.0.0\" });\n\n  // Receive tool results pushed by the host\n  app.ontoolresult = ({ content }) => {\n    const text = content?.find(c => c.type === 'text');\n    if (text) {\n      document.getElementById('output').textContent = text.text;\n    }\n  };\n\n  // Connect to the host\n  await app.connect();\n</script>\n```\n\nThe `App` object provides:\n\n- **`app.ontoolresult`** — callback that receives tool results pushed by the host\n- **`app.callServerTool({name, arguments})`** — call a tool on the server from within the app\n- **`app.onhostcontextchanged`** — callback for host context changes (e.g., safe area insets)\n- **`app.getHostContext()`** — get current host context\n\n<Note>\nIf your HTML loads external scripts, styles, or makes API calls, you need to declare those domains in the CSP configuration. See [Security](#security) below.\n</Note>\n\n## Security\n\nApps run in sandboxed iframes with a deny-by-default Content Security Policy. By default, only inline scripts and styles are allowed — no external network access.\n\n### Content Security Policy\n\nIf your app needs to load external resources (CDN scripts, API calls, embedded iframes), declare the allowed domains with `ResourceCSP`:\n\n```python\nfrom fastmcp.server.apps import AppConfig, ResourceCSP\n\n@mcp.resource(\n    \"ui://my-app/view.html\",\n    app=AppConfig(\n        csp=ResourceCSP(\n            resource_domains=[\"https://unpkg.com\", \"https://cdn.example.com\"],\n            connect_domains=[\"https://api.example.com\"],\n        )\n    ),\n)\ndef my_view() -> str:\n    return \"<html>...</html>\"\n```\n\n| CSP Field | Controls |\n|-----------|----------|\n| `connect_domains` | `fetch`, XHR, WebSocket (`connect-src`) |\n| `resource_domains` | Scripts, images, styles, fonts (`script-src`, etc.) |\n| `frame_domains` | Nested iframes (`frame-src`) |\n| `base_uri_domains` | Document base URI (`base-uri`) |\n\n### Permissions\n\nIf your app needs browser capabilities like camera or clipboard access, request them via `ResourcePermissions`:\n\n```python\nfrom fastmcp.server.apps import AppConfig, ResourcePermissions\n\n@mcp.resource(\n    \"ui://my-app/view.html\",\n    app=AppConfig(\n        permissions=ResourcePermissions(\n            camera={},\n            clipboard_write={},\n        )\n    ),\n)\ndef my_view() -> str:\n    return \"<html>...</html>\"\n```\n\nHosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.\n\n## Example: QR Code Server\n\nThis 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.\n\n```python expandable\nimport base64\nimport io\n\nimport qrcode\nfrom mcp import types\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.apps import AppConfig, ResourceCSP\nfrom fastmcp.tools import ToolResult\n\nmcp = FastMCP(\"QR Code Server\")\n\nVIEW_URI = \"ui://qr-server/view.html\"\n\n\n@mcp.tool(app=AppConfig(resource_uri=VIEW_URI))\ndef generate_qr(text: str = \"https://gofastmcp.com\") -> ToolResult:\n    \"\"\"Generate a QR code from text.\"\"\"\n    qr = qrcode.QRCode(version=1, box_size=10, border=4)\n    qr.add_data(text)\n    qr.make(fit=True)\n\n    img = qr.make_image()\n    buffer = io.BytesIO()\n    img.save(buffer, format=\"PNG\")\n    b64 = base64.b64encode(buffer.getvalue()).decode()\n\n    return ToolResult(\n        content=[types.ImageContent(type=\"image\", data=b64, mimeType=\"image/png\")]\n    )\n\n\n@mcp.resource(\n    VIEW_URI,\n    app=AppConfig(csp=ResourceCSP(resource_domains=[\"https://unpkg.com\"])),\n)\ndef view() -> str:\n    \"\"\"Interactive QR code viewer.\"\"\"\n    return \"\"\"\\\n<!DOCTYPE html>\n<html>\n<head>\n  <meta name=\"color-scheme\" content=\"light dark\">\n  <style>\n    body { display: flex; justify-content: center;\n           align-items: center; height: 340px; width: 340px;\n           margin: 0; background: transparent; }\n    img  { width: 300px; height: 300px; border-radius: 8px;\n           box-shadow: 0 2px 8px rgba(0,0,0,0.1); }\n  </style>\n</head>\n<body>\n  <div id=\"qr\"></div>\n  <script type=\"module\">\n    import { App } from\n      \"https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps\";\n\n    const app = new App({ name: \"QR View\", version: \"1.0.0\" });\n\n    app.ontoolresult = ({ content }) => {\n      const img = content?.find(c => c.type === 'image');\n      if (img) {\n        const el = document.createElement('img');\n        el.src = `data:${img.mimeType};base64,${img.data}`;\n        el.alt = \"QR Code\";\n        document.getElementById('qr').replaceChildren(el);\n      }\n    };\n\n    await app.connect();\n  </script>\n</body>\n</html>\"\"\"\n```\n\nThe 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.\n\n## Checking Client Support\n\nNot all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):\n\n```python\nfrom fastmcp import Context\nfrom fastmcp.server.apps import AppConfig, UI_EXTENSION_ID\n\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\"))\nasync def my_tool(ctx: Context) -> str:\n    if ctx.client_supports_extension(UI_EXTENSION_ID):\n        # Return data optimized for UI rendering\n        return rich_response()\n    else:\n        # Fall back to plain text\n        return plain_text_response()\n```\n"
  },
  {
    "path": "docs/apps/overview.mdx",
    "content": "---\ntitle: Apps\nsidebarTitle: Overview\ndescription: Give your tools interactive UIs rendered directly in the conversation.\nicon: grid-2\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nMCP Apps let your tools return interactive UIs — rendered in a sandboxed iframe right inside the host client's conversation. Instead of returning plain text, a tool can show a chart, a sortable table, a form, or anything you can build with HTML.\n\nFastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and provides two approaches:\n\n## Prefab Apps (Recommended)\n\n<VersionBadge version=\"3.1.0\" />\n\n<Tip>\n[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always [pin `prefab-ui` to a specific version](/apps/prefab#getting-started) in your dependencies.\n</Tip>\n\n[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe layouts, charts, tables, forms, and interactive behaviors using a Python DSL — and the framework compiles them to a JSON protocol that a shared renderer interprets. It started as a component library inside FastMCP and grew into its own framework with [comprehensive documentation](https://prefab.prefect.io).\n\n```python\nfrom prefab_ui.components import Column, Heading, BarChart, ChartSeries\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Dashboard\")\n\n@mcp.tool(app=True)\ndef sales_chart(year: int) -> PrefabApp:\n    \"\"\"Show sales data as an interactive chart.\"\"\"\n    data = get_sales_data(year)\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(f\"{year} Sales\")\n        BarChart(\n            data=data,\n            series=[ChartSeries(data_key=\"revenue\", label=\"Revenue\")],\n            x_axis=\"month\",\n        )\n\n    return PrefabApp(view=view)\n```\n\nInstall with `pip install \"fastmcp[apps]\"` and see [Prefab Apps](/apps/prefab) for the integration guide.\n\n## Custom HTML Apps\n\nThe [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol, and you can use it directly when you need full control. You write your own HTML/CSS/JavaScript and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK.\n\nThis is the right choice for custom rendering (maps, 3D, video), specific JavaScript frameworks, or capabilities beyond what the component library offers.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.apps import AppConfig, ResourceCSP\n\nmcp = FastMCP(\"Custom App\")\n\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\"))\ndef my_tool() -> str:\n    return '{\"values\": [1, 2, 3]}'\n\n@mcp.resource(\n    \"ui://my-app/view.html\",\n    app=AppConfig(csp=ResourceCSP(resource_domains=[\"https://unpkg.com\"])),\n)\ndef view() -> str:\n    return \"<html>...</html>\"\n```\n\nSee [Custom HTML Apps](/apps/low-level) for the full reference.\n"
  },
  {
    "path": "docs/apps/patterns.mdx",
    "content": "---\ntitle: Patterns\nsidebarTitle: Patterns\ndescription: Charts, tables, forms, and other common tool UIs.\nicon: grid-2-plus\ntag: SOON\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.1.0\" />\n\n<Tip>\n[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always pin `prefab-ui` to a specific version in your dependencies.\n</Tip>\n\nThe most common use of Prefab is giving your tools a visual representation — a chart instead of raw numbers, a sortable table instead of a text dump, a status dashboard instead of a list of booleans. Each pattern below is a complete, copy-pasteable tool.\n\n## Charts\n\nPrefab includes [bar, line, area, pie, radar, and radial charts](https://prefab.prefect.io/docs/components/charts). They all render client-side with tooltips, legends, and responsive sizing.\n\n### Bar Chart\n\n```python\nfrom prefab_ui.components import Column, Heading, BarChart, ChartSeries\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Charts\")\n\n\n@mcp.tool(app=True)\ndef quarterly_revenue(year: int) -> PrefabApp:\n    \"\"\"Show quarterly revenue as a bar chart.\"\"\"\n    data = [\n        {\"quarter\": \"Q1\", \"revenue\": 42000, \"costs\": 28000},\n        {\"quarter\": \"Q2\", \"revenue\": 51000, \"costs\": 31000},\n        {\"quarter\": \"Q3\", \"revenue\": 47000, \"costs\": 29000},\n        {\"quarter\": \"Q4\", \"revenue\": 63000, \"costs\": 35000},\n    ]\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(f\"{year} Revenue vs Costs\")\n        BarChart(\n            data=data,\n            series=[\n                ChartSeries(data_key=\"revenue\", label=\"Revenue\"),\n                ChartSeries(data_key=\"costs\", label=\"Costs\"),\n            ],\n            x_axis=\"quarter\",\n            show_legend=True,\n        )\n\n    return PrefabApp(view=view)\n```\n\nMultiple `ChartSeries` entries plot different data keys. Add `stacked=True` to stack bars, or `horizontal=True` to flip the axes.\n\n### Area Chart\n\n`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation (`\"linear\"`, `\"smooth\"`, `\"step\"`) and `show_dots` for data points:\n\n```python\nfrom prefab_ui.components import Column, Heading, AreaChart, ChartSeries\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Charts\")\n\n\n@mcp.tool(app=True)\ndef usage_trend() -> PrefabApp:\n    \"\"\"Show API usage over time.\"\"\"\n    data = [\n        {\"date\": \"Feb 1\", \"requests\": 1200},\n        {\"date\": \"Feb 2\", \"requests\": 1350},\n        {\"date\": \"Feb 3\", \"requests\": 980},\n        {\"date\": \"Feb 4\", \"requests\": 1500},\n        {\"date\": \"Feb 5\", \"requests\": 1420},\n    ]\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"API Usage\")\n        AreaChart(\n            data=data,\n            series=[ChartSeries(data_key=\"requests\", label=\"Requests\")],\n            x_axis=\"date\",\n            curve=\"smooth\",\n            height=250,\n        )\n\n    return PrefabApp(view=view)\n```\n\n### Pie and Donut Charts\n\n`PieChart` uses `data_key` (the numeric value) and `name_key` (the label) instead of series. Set `inner_radius` for a donut:\n\n```python\nfrom prefab_ui.components import Column, Heading, PieChart\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Charts\")\n\n\n@mcp.tool(app=True)\ndef ticket_breakdown() -> PrefabApp:\n    \"\"\"Show open tickets by category.\"\"\"\n    data = [\n        {\"category\": \"Bug\", \"count\": 23},\n        {\"category\": \"Feature\", \"count\": 15},\n        {\"category\": \"Docs\", \"count\": 8},\n        {\"category\": \"Infra\", \"count\": 12},\n    ]\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Open Tickets\")\n        PieChart(\n            data=data,\n            data_key=\"count\",\n            name_key=\"category\",\n            show_legend=True,\n            inner_radius=60,\n        )\n\n    return PrefabApp(view=view)\n```\n\n## Data Tables\n\n[DataTable](https://prefab.prefect.io/docs/components/data-display/data-table) provides sortable columns, full-text search, and pagination — all running client-side in the browser.\n\n```python\nfrom prefab_ui.components import Column, Heading, DataTable, DataTableColumn\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Directory\")\n\n\n@mcp.tool(app=True)\ndef employee_directory() -> PrefabApp:\n    \"\"\"Show a searchable, sortable employee directory.\"\"\"\n    employees = [\n        {\"name\": \"Alice Chen\", \"department\": \"Engineering\", \"role\": \"Staff Engineer\", \"location\": \"SF\"},\n        {\"name\": \"Bob Martinez\", \"department\": \"Design\", \"role\": \"Lead Designer\", \"location\": \"NYC\"},\n        {\"name\": \"Carol Johnson\", \"department\": \"Engineering\", \"role\": \"Senior Engineer\", \"location\": \"London\"},\n        {\"name\": \"David Kim\", \"department\": \"Product\", \"role\": \"Product Manager\", \"location\": \"SF\"},\n        {\"name\": \"Eva Müller\", \"department\": \"Engineering\", \"role\": \"Engineer\", \"location\": \"Berlin\"},\n    ]\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Employee Directory\")\n        DataTable(\n            columns=[\n                DataTableColumn(key=\"name\", header=\"Name\", sortable=True),\n                DataTableColumn(key=\"department\", header=\"Department\", sortable=True),\n                DataTableColumn(key=\"role\", header=\"Role\"),\n                DataTableColumn(key=\"location\", header=\"Office\", sortable=True),\n            ],\n            rows=employees,\n            searchable=True,\n            paginated=True,\n            page_size=15,\n        )\n\n    return PrefabApp(view=view)\n```\n\n## Forms\n\nA form collects input, but it needs somewhere to send that input. The [`CallTool`](https://prefab.prefect.io/docs/concepts/actions) action connects a form to a tool on your MCP server — so you need two tools: one that renders the form, and one that handles the submission.\n\n```python\nfrom prefab_ui.components import (\n    Column, Heading, Row, Muted, Badge, Input, Select,\n    Textarea, Button, Form, ForEach, Separator,\n)\nfrom prefab_ui.actions import ShowToast\nfrom prefab_ui.actions.mcp import CallTool\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Contacts\")\n\ncontacts_db: list[dict] = [\n    {\"name\": \"Zaphod Beeblebrox\", \"email\": \"zaphod@galaxy.gov\", \"category\": \"Partner\"},\n]\n\n\n@mcp.tool(app=True)\ndef contact_form() -> PrefabApp:\n    \"\"\"Show a contact list with a form to add new contacts.\"\"\"\n    with Column(gap=6, css_class=\"p-6\") as view:\n        Heading(\"Contacts\")\n\n        with ForEach(\"contacts\"):\n            with Row(gap=2, align=\"center\"):\n                Muted(\"{{ name }}\")\n                Muted(\"{{ email }}\")\n                Badge(\"{{ category }}\")\n\n        Separator()\n\n        with Form(\n            on_submit=CallTool(\n                \"save_contact\",\n                result_key=\"contacts\",\n                on_success=ShowToast(\"Contact saved!\", variant=\"success\"),\n                on_error=ShowToast(\"{{ $error }}\", variant=\"error\"),\n            )\n        ):\n            Input(name=\"name\", label=\"Full Name\", required=True)\n            Input(name=\"email\", label=\"Email\", input_type=\"email\", required=True)\n            Select(\n                name=\"category\",\n                label=\"Category\",\n                options=[\"Customer\", \"Vendor\", \"Partner\", \"Other\"],\n            )\n            Textarea(name=\"notes\", label=\"Notes\", placeholder=\"Optional notes...\")\n            Button(\"Save Contact\")\n\n    return PrefabApp(view=view, state={\"contacts\": list(contacts_db)})\n\n\n@mcp.tool\ndef save_contact(\n    name: str,\n    email: str,\n    category: str = \"Other\",\n    notes: str = \"\",\n) -> list[dict]:\n    \"\"\"Save a new contact and return the updated list.\"\"\"\n    contacts_db.append({\"name\": name, \"email\": email, \"category\": category, \"notes\": notes})\n    return list(contacts_db)\n```\n\nWhen the user submits the form, the renderer calls `save_contact` on the server with all named input values as arguments. Because `result_key=\"contacts\"` is set, the returned list replaces the `contacts` state — and the `ForEach` re-renders with the new data automatically.\n\nThe `save_contact` tool is a regular MCP tool. The LLM can also call it directly in conversation. Your UI actions and your conversational tools are the same thing.\n\n### Pydantic Model Forms\n\nFor complex forms, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, validation, and submit wiring:\n\n```python\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\nfrom prefab_ui.components import Column, Heading, Form\nfrom prefab_ui.actions.mcp import CallTool\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Bug Tracker\")\n\n\nclass BugReport(BaseModel):\n    title: str = Field(title=\"Bug Title\")\n    severity: Literal[\"low\", \"medium\", \"high\", \"critical\"] = Field(\n        title=\"Severity\", default=\"medium\"\n    )\n    description: str = Field(title=\"Description\")\n    steps_to_reproduce: str = Field(title=\"Steps to Reproduce\")\n\n\n@mcp.tool(app=True)\ndef report_bug() -> PrefabApp:\n    \"\"\"Show a bug report form.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Report a Bug\")\n        Form.from_model(BugReport, on_submit=CallTool(\"create_bug_report\"))\n\n    return PrefabApp(view=view)\n\n\n@mcp.tool\ndef create_bug_report(data: dict) -> str:\n    \"\"\"Create a bug report from the form submission.\"\"\"\n    report = BugReport(**data)\n    # save to database...\n    return f\"Created bug report: {report.title}\"\n```\n\n`str` fields become text inputs, `Literal` becomes a select, `bool` becomes a checkbox. The `on_submit` CallTool receives all field values under a `data` key.\n\n## Status Displays\n\nCards, badges, progress bars, and grids combine naturally for dashboards. See the [Prefab layout](https://prefab.prefect.io/docs/concepts/composition) and [container](https://prefab.prefect.io/docs/components/containers) docs for the full set of layout and display components.\n\n```python\nfrom prefab_ui.components import (\n    Column, Row, Grid, Heading, Text, Muted, Badge,\n    Card, CardContent, Progress, Separator,\n)\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Monitoring\")\n\n\n@mcp.tool(app=True)\ndef system_status() -> PrefabApp:\n    \"\"\"Show current system health.\"\"\"\n    services = [\n        {\"name\": \"API Gateway\", \"status\": \"healthy\", \"ok\": True, \"latency_ms\": 12, \"uptime_pct\": 99.9},\n        {\"name\": \"Database\", \"status\": \"healthy\", \"ok\": True, \"latency_ms\": 3, \"uptime_pct\": 99.99},\n        {\"name\": \"Cache\", \"status\": \"degraded\", \"ok\": False, \"latency_ms\": 45, \"uptime_pct\": 98.2},\n        {\"name\": \"Queue\", \"status\": \"healthy\", \"ok\": True, \"latency_ms\": 8, \"uptime_pct\": 99.8},\n    ]\n    all_ok = all(s[\"ok\"] for s in services)\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        with Row(gap=2, align=\"center\"):\n            Heading(\"System Status\")\n            Badge(\n                \"All Healthy\" if all_ok else \"Degraded\",\n                variant=\"success\" if all_ok else \"destructive\",\n            )\n\n        Separator()\n\n        with Grid(columns=2, gap=4):\n            for svc in services:\n                with Card():\n                    with CardContent():\n                        with Row(gap=2, align=\"center\"):\n                            Text(svc[\"name\"], css_class=\"font-medium\")\n                            Badge(\n                                svc[\"status\"],\n                                variant=\"success\" if svc[\"ok\"] else \"destructive\",\n                            )\n                        Muted(f\"Response: {svc['latency_ms']}ms\")\n                        Progress(value=svc[\"uptime_pct\"])\n\n    return PrefabApp(view=view)\n```\n\n## Conditional Content\n\n[`If`, `Elif`, and `Else`](https://prefab.prefect.io/docs/concepts/composition#conditional-rendering) show or hide content based on state. Changes are instant — no server round-trip.\n\n```python\nfrom prefab_ui.components import Column, Heading, Switch, Separator, Alert, If\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Flags\")\n\n\n@mcp.tool(app=True)\ndef feature_flags() -> PrefabApp:\n    \"\"\"Toggle feature flags with live preview.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Feature Flags\")\n\n        Switch(name=\"dark_mode\", label=\"Dark Mode\")\n        Switch(name=\"beta_features\", label=\"Beta Features\")\n\n        Separator()\n\n        with If(\"{{ dark_mode }}\"):\n            Alert(title=\"Dark mode enabled\", description=\"UI will use dark theme.\")\n        with If(\"{{ beta_features }}\"):\n            Alert(\n                title=\"Beta features active\",\n                description=\"Experimental features are now visible.\",\n                variant=\"warning\",\n            )\n\n    return PrefabApp(view=view, state={\"dark_mode\": False, \"beta_features\": False})\n```\n\n## Tabs\n\n[Tabs](https://prefab.prefect.io/docs/components/containers/tabs) organize content into switchable views. Switching is client-side — no server round-trip.\n\n```python\nfrom prefab_ui.components import (\n    Column, Heading, Text, Muted, Badge, Row,\n    DataTable, DataTableColumn, Tabs, Tab, ForEach,\n)\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Projects\")\n\n\n@mcp.tool(app=True)\ndef project_overview(project_id: str) -> PrefabApp:\n    \"\"\"Show project details organized in tabs.\"\"\"\n    project = {\n        \"name\": \"FastMCP v3\",\n        \"description\": \"Next generation MCP framework with Apps support.\",\n        \"status\": \"Active\",\n        \"created_at\": \"2025-01-15\",\n        \"members\": [\n            {\"name\": \"Alice Chen\", \"role\": \"Lead\"},\n            {\"name\": \"Bob Martinez\", \"role\": \"Design\"},\n        ],\n        \"activity\": [\n            {\"timestamp\": \"2 hours ago\", \"message\": \"Merged PR #342\"},\n            {\"timestamp\": \"1 day ago\", \"message\": \"Released v3.0.1\"},\n        ],\n    }\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(project[\"name\"])\n\n        with Tabs():\n            with Tab(\"Overview\"):\n                Text(project[\"description\"])\n                with Row(gap=4):\n                    Badge(project[\"status\"])\n                    Muted(f\"Created: {project['created_at']}\")\n\n            with Tab(\"Members\"):\n                DataTable(\n                    columns=[\n                        DataTableColumn(key=\"name\", header=\"Name\", sortable=True),\n                        DataTableColumn(key=\"role\", header=\"Role\"),\n                    ],\n                    rows=project[\"members\"],\n                )\n\n            with Tab(\"Activity\"):\n                with ForEach(\"activity\"):\n                    with Row(gap=2):\n                        Muted(\"{{ timestamp }}\")\n                        Text(\"{{ message }}\")\n\n    return PrefabApp(view=view, state={\"activity\": project[\"activity\"]})\n```\n\n## Accordion\n\n[Accordion](https://prefab.prefect.io/docs/components/containers/accordion) collapses sections to save space. `multiple=True` lets users expand several items at once:\n\n```python\nfrom prefab_ui.components import (\n    Column, Heading, Row, Text, Badge, Progress,\n    Accordion, AccordionItem,\n)\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"API Monitor\")\n\n\n@mcp.tool(app=True)\ndef api_health() -> PrefabApp:\n    \"\"\"Show health details for each API endpoint.\"\"\"\n    endpoints = [\n        {\"path\": \"/api/users\", \"status\": 200, \"healthy\": True, \"avg_ms\": 45, \"p99_ms\": 120, \"uptime_pct\": 99.9},\n        {\"path\": \"/api/orders\", \"status\": 200, \"healthy\": True, \"avg_ms\": 82, \"p99_ms\": 250, \"uptime_pct\": 99.7},\n        {\"path\": \"/api/search\", \"status\": 200, \"healthy\": True, \"avg_ms\": 150, \"p99_ms\": 500, \"uptime_pct\": 99.5},\n        {\"path\": \"/api/webhooks\", \"status\": 503, \"healthy\": False, \"avg_ms\": 2000, \"p99_ms\": 5000, \"uptime_pct\": 95.1},\n    ]\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"API Health\")\n\n        with Accordion(multiple=True):\n            for ep in endpoints:\n                with AccordionItem(ep[\"path\"]):\n                    with Row(gap=4):\n                        Badge(\n                            f\"{ep['status']}\",\n                            variant=\"success\" if ep[\"healthy\"] else \"destructive\",\n                        )\n                        Text(f\"Avg: {ep['avg_ms']}ms\")\n                        Text(f\"P99: {ep['p99_ms']}ms\")\n                    Progress(value=ep[\"uptime_pct\"])\n\n    return PrefabApp(view=view)\n```\n\n## Next Steps\n\n- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript\n- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions\n"
  },
  {
    "path": "docs/apps/prefab.mdx",
    "content": "---\ntitle: Prefab Apps\nsidebarTitle: Prefab Apps\ndescription: Build interactive tool UIs in pure Python — no HTML or JavaScript required.\nicon: palette\ntag: SOON\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.1.0\" />\n\n<Tip>\n[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always pin `prefab-ui` to a specific version in your dependencies (see below).\n</Tip>\n\n[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe what your interface should look like — a chart, a table, a form — and return it from your tool. FastMCP takes care of everything else: registering the renderer, wiring the protocol metadata, and delivering the component tree to the host.\n\nPrefab started as a component library inside FastMCP and grew into a full framework for building interactive applications — with its own state management, reactive expression system, and action model. The [Prefab documentation](https://prefab.prefect.io) covers all of this in depth. This page focuses on the FastMCP integration: what you return from a tool, and what FastMCP does with it.\n\n```bash\npip install \"fastmcp[apps]\"\n```\n\n<Tip>\nPrefab UI is in active early development and its API changes frequently. We strongly recommend pinning `prefab-ui` to a specific version in your project's dependencies. Installing `fastmcp[apps]` pulls in `prefab-ui` but won't pin it — so a routine `pip install --upgrade` could introduce breaking changes.\n\n```toml\n# pyproject.toml\ndependencies = [\n    \"fastmcp[apps]\",\n    \"prefab-ui==0.8.0\",  # pin to a known working version\n]\n```\n</Tip>\n\nHere's the simplest possible Prefab App — a tool that returns a bar chart:\n\n```python\nfrom prefab_ui.components import Column, Heading, BarChart, ChartSeries\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Dashboard\")\n\n\n@mcp.tool(app=True)\ndef revenue_chart(year: int) -> PrefabApp:\n    \"\"\"Show annual revenue as an interactive bar chart.\"\"\"\n    data = [\n        {\"quarter\": \"Q1\", \"revenue\": 42000},\n        {\"quarter\": \"Q2\", \"revenue\": 51000},\n        {\"quarter\": \"Q3\", \"revenue\": 47000},\n        {\"quarter\": \"Q4\", \"revenue\": 63000},\n    ]\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(f\"{year} Revenue\")\n        BarChart(\n            data=data,\n            series=[ChartSeries(data_key=\"revenue\", label=\"Revenue\")],\n            x_axis=\"quarter\",\n        )\n\n    return PrefabApp(view=view)\n```\n\nThat's it — you declare a layout using Python's `with` statement, and return it. When the host calls this tool, the user sees an interactive bar chart instead of a JSON blob. The [Patterns](/apps/patterns) page has more examples: area charts, data tables, forms, status dashboards, and more.\n\n## What You Return\n\n### Components\n\nThe simplest way to get started. If you're returning a visual representation of data and don't need Prefab's more advanced features like initial state or stylesheets, just return the components directly. FastMCP wraps them in a `PrefabApp` automatically:\n\n```python\nfrom prefab_ui.components import Column, Heading, Badge\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Status\")\n\n\n@mcp.tool(app=True)\ndef status_badge() -> Column:\n    \"\"\"Show system status.\"\"\"\n    with Column(gap=2) as view:\n        Heading(\"All Systems Operational\")\n        Badge(\"Healthy\", variant=\"success\")\n    return view\n```\n\nWant a chart? Return a chart. Want a table? Return a table. FastMCP handles the wiring.\n\n### PrefabApp\n\nWhen you need more control — setting initial state values that components can read and react to, or configuring the rendering engine — return a `PrefabApp` explicitly:\n\n```python\nfrom prefab_ui.components import Column, Heading, Text, Button, If, Badge\nfrom prefab_ui.actions import ToggleState\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool(app=True)\ndef toggle_demo() -> PrefabApp:\n    \"\"\"Interactive toggle with state.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Button(\"Toggle\", on_click=ToggleState(\"show\"))\n        with If(\"{{ show }}\"):\n            Badge(\"Visible!\", variant=\"success\")\n\n    return PrefabApp(view=view, state={\"show\": False})\n```\n\nThe `state` dict provides the initial values. Components reference state with `{{ expression }}` templates. State mutations like `ToggleState` happen entirely in the browser — no server round-trip. The [Prefab state guide](https://prefab.prefect.io/docs/concepts/state) covers this in detail.\n\n### ToolResult\n\nEvery tool result has two audiences: the renderer (which displays the UI) and the LLM (which reads the text content to understand what happened). By default, Prefab Apps send `\"[Rendered Prefab UI]\"` as the text content, which tells the LLM almost nothing.\n\nIf you want the LLM to understand the result — so it can reference the data in conversation, summarize it, or decide what to do next — wrap your return in a `ToolResult` with a meaningful `content` string:\n\n```python\nfrom prefab_ui.components import Column, Heading, BarChart, ChartSeries\nfrom prefab_ui.app import PrefabApp\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import ToolResult\n\nmcp = FastMCP(\"Sales\")\n\n\n@mcp.tool(app=True)\ndef sales_overview(year: int) -> ToolResult:\n    \"\"\"Show sales data visually and summarize for the model.\"\"\"\n    data = get_sales_data(year)\n    total = sum(row[\"revenue\"] for row in data)\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Sales Overview\")\n        BarChart(data=data, series=[ChartSeries(data_key=\"revenue\")])\n\n    return ToolResult(\n        content=f\"Total revenue for {year}: ${total:,} across {len(data)} quarters\",\n        structured_content=view,\n    )\n```\n\nThe user sees the chart. The LLM sees `\"Total revenue for 2025: $203,000 across 4 quarters\"` and can reason about it.\n\n## Type Inference\n\nIf your tool's return type annotation is a Prefab type — `PrefabApp`, `Component`, or their `Optional` variants — FastMCP detects this and enables app rendering automatically:\n\n```python\n@mcp.tool\ndef greet(name: str) -> PrefabApp:\n    return PrefabApp(view=Heading(f\"Hello, {name}!\"))\n```\n\nThis is equivalent to `@mcp.tool(app=True)`. Explicit `app=True` is recommended for clarity, and is required when the return type doesn't reveal a Prefab type (e.g., `-> ToolResult`).\n\n## How It Works\n\nBehind the scenes, when a tool returns a Prefab component or `PrefabApp`, FastMCP:\n\n1. **Registers a shared renderer** — a `ui://prefab/renderer.html` resource containing the JavaScript rendering engine, fetched once by the host and reused across all your Prefab tools.\n2. **Wires the tool metadata** — so the host knows to load the renderer iframe when displaying the tool result.\n3. **Serializes the component tree** — your Python components become `structuredContent` on the tool result, which the renderer interprets and displays.\n\nNone of this requires any configuration. The `app=True` flag (or type inference) is the only thing you need.\n\n## Mixing with Custom HTML Apps\n\nPrefab tools and [custom HTML tools](/apps/low-level) coexist in the same server. Prefab tools share a single renderer resource; custom tools point to their own. Both use the same MCP Apps protocol:\n\n```python\nfrom fastmcp.server.apps import AppConfig\n\n@mcp.tool(app=True)\ndef team_directory() -> PrefabApp:\n    ...\n\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/map.html\"))\ndef map_view() -> str:\n    ...\n```\n\n## Next Steps\n\n- **[Patterns](/apps/patterns)** — Charts, tables, forms, and other common tool UIs\n- **[Development](/apps/development)** — Preview and test app tools locally with `fastmcp dev apps`\n- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript\n- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions\n"
  },
  {
    "path": "docs/assets/schemas/mcp_server_config/latest.json",
    "content": "{\n  \"$defs\": {\n    \"Deployment\": {\n      \"description\": \"Configuration for server deployment and runtime settings.\",\n      \"properties\": {\n        \"transport\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"stdio\",\n                \"http\",\n                \"sse\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Transport protocol to use\",\n          \"title\": \"Transport\"\n        },\n        \"host\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Host to bind to when using HTTP transport\",\n          \"examples\": [\n            \"127.0.0.1\",\n            \"0.0.0.0\",\n            \"localhost\"\n          ],\n          \"title\": \"Host\"\n        },\n        \"port\": {\n          \"anyOf\": [\n            {\n              \"type\": \"integer\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Port to bind to when using HTTP transport\",\n          \"examples\": [\n            8000,\n            3000,\n            5000\n          ],\n          \"title\": \"Port\"\n        },\n        \"path\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"URL path for the server endpoint\",\n          \"examples\": [\n            \"/mcp/\",\n            \"/api/mcp/\",\n            \"/sse/\"\n          ],\n          \"title\": \"Path\"\n        },\n        \"log_level\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"DEBUG\",\n                \"INFO\",\n                \"WARNING\",\n                \"ERROR\",\n                \"CRITICAL\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Log level for the server\",\n          \"title\": \"Log Level\"\n        },\n        \"cwd\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Working directory for the server process\",\n          \"examples\": [\n            \".\",\n            \"./src\",\n            \"/app\"\n          ],\n          \"title\": \"Cwd\"\n        },\n        \"env\": {\n          \"anyOf\": [\n            {\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"object\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Environment variables to set when running the server\",\n          \"examples\": [\n            {\n              \"API_KEY\": \"secret\",\n              \"DEBUG\": \"true\"\n            }\n          ],\n          \"title\": \"Env\"\n        },\n        \"args\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Arguments to pass to the server (after --)\",\n          \"examples\": [\n            [\n              \"--config\",\n              \"config.json\",\n              \"--debug\"\n            ]\n          ],\n          \"title\": \"Args\"\n        }\n      },\n      \"title\": \"Deployment\",\n      \"type\": \"object\"\n    },\n    \"Environment\": {\n      \"description\": \"Configuration for Python environment setup.\",\n      \"properties\": {\n        \"python\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python version constraint\",\n          \"examples\": [\n            \"3.10\",\n            \"3.11\",\n            \"3.12\"\n          ],\n          \"title\": \"Python\"\n        },\n        \"dependencies\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python packages to install with PEP 508 specifiers\",\n          \"examples\": [\n            [\n              \"fastmcp>=2.0,<3\",\n              \"httpx\",\n              \"pandas>=2.0\"\n            ]\n          ],\n          \"title\": \"Dependencies\"\n        },\n        \"requirements\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to requirements.txt file\",\n          \"examples\": [\n            \"requirements.txt\",\n            \"../requirements/prod.txt\"\n          ],\n          \"title\": \"Requirements\"\n        },\n        \"project\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to project directory containing pyproject.toml\",\n          \"examples\": [\n            \".\",\n            \"../my-project\"\n          ],\n          \"title\": \"Project\"\n        },\n        \"editable\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Directory to install in editable mode\",\n          \"examples\": [\n            \".\",\n            \"../my-package\"\n          ],\n          \"title\": \"Editable\"\n        }\n      },\n      \"title\": \"Environment\",\n      \"type\": \"object\"\n    },\n    \"FileSystemSource\": {\n      \"description\": \"Source for local Python files.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"filesystem\",\n          \"default\": \"filesystem\",\n          \"description\": \"Source type\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"path\": {\n          \"description\": \"Path to Python file containing the server\",\n          \"title\": \"Path\",\n          \"type\": \"string\"\n        },\n        \"entrypoint\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Name of server instance or factory function (a no-arg function that returns a FastMCP server)\",\n          \"title\": \"Entrypoint\"\n        }\n      },\n      \"required\": [\n        \"path\"\n      ],\n      \"title\": \"FileSystemSource\",\n      \"type\": \"object\"\n    }\n  },\n  \"description\": \"Configuration file for FastMCP servers\",\n  \"properties\": {\n    \"$schema\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ],\n      \"default\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n      \"description\": \"JSON schema for IDE support and validation\",\n      \"title\": \"$Schema\"\n    },\n    \"source\": {\n      \"$ref\": \"#/$defs/FileSystemSource\",\n      \"description\": \"Source configuration for the server\",\n      \"examples\": [\n        {\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"app\",\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"mcp\",\n          \"path\": \"src/server.py\",\n          \"type\": \"filesystem\"\n        }\n      ]\n    },\n    \"environment\": {\n      \"$ref\": \"#/$defs/Environment\",\n      \"description\": \"Python environment setup configuration\"\n    },\n    \"deployment\": {\n      \"$ref\": \"#/$defs/Deployment\",\n      \"description\": \"Server deployment and runtime settings\"\n    }\n  },\n  \"required\": [\n    \"source\"\n  ],\n  \"title\": \"FastMCP Configuration\",\n  \"type\": \"object\",\n  \"$id\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n}\n"
  },
  {
    "path": "docs/assets/schemas/mcp_server_config/v1.json",
    "content": "{\n  \"$defs\": {\n    \"Deployment\": {\n      \"description\": \"Configuration for server deployment and runtime settings.\",\n      \"properties\": {\n        \"transport\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"stdio\",\n                \"http\",\n                \"sse\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Transport protocol to use\",\n          \"title\": \"Transport\"\n        },\n        \"host\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Host to bind to when using HTTP transport\",\n          \"examples\": [\n            \"127.0.0.1\",\n            \"0.0.0.0\",\n            \"localhost\"\n          ],\n          \"title\": \"Host\"\n        },\n        \"port\": {\n          \"anyOf\": [\n            {\n              \"type\": \"integer\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Port to bind to when using HTTP transport\",\n          \"examples\": [\n            8000,\n            3000,\n            5000\n          ],\n          \"title\": \"Port\"\n        },\n        \"path\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"URL path for the server endpoint\",\n          \"examples\": [\n            \"/mcp/\",\n            \"/api/mcp/\",\n            \"/sse/\"\n          ],\n          \"title\": \"Path\"\n        },\n        \"log_level\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"DEBUG\",\n                \"INFO\",\n                \"WARNING\",\n                \"ERROR\",\n                \"CRITICAL\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Log level for the server\",\n          \"title\": \"Log Level\"\n        },\n        \"cwd\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Working directory for the server process\",\n          \"examples\": [\n            \".\",\n            \"./src\",\n            \"/app\"\n          ],\n          \"title\": \"Cwd\"\n        },\n        \"env\": {\n          \"anyOf\": [\n            {\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"object\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Environment variables to set when running the server\",\n          \"examples\": [\n            {\n              \"API_KEY\": \"secret\",\n              \"DEBUG\": \"true\"\n            }\n          ],\n          \"title\": \"Env\"\n        },\n        \"args\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Arguments to pass to the server (after --)\",\n          \"examples\": [\n            [\n              \"--config\",\n              \"config.json\",\n              \"--debug\"\n            ]\n          ],\n          \"title\": \"Args\"\n        }\n      },\n      \"title\": \"Deployment\",\n      \"type\": \"object\"\n    },\n    \"Environment\": {\n      \"description\": \"Configuration for Python environment setup.\",\n      \"properties\": {\n        \"python\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python version constraint\",\n          \"examples\": [\n            \"3.10\",\n            \"3.11\",\n            \"3.12\"\n          ],\n          \"title\": \"Python\"\n        },\n        \"dependencies\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python packages to install with PEP 508 specifiers\",\n          \"examples\": [\n            [\n              \"fastmcp>=2.0,<3\",\n              \"httpx\",\n              \"pandas>=2.0\"\n            ]\n          ],\n          \"title\": \"Dependencies\"\n        },\n        \"requirements\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to requirements.txt file\",\n          \"examples\": [\n            \"requirements.txt\",\n            \"../requirements/prod.txt\"\n          ],\n          \"title\": \"Requirements\"\n        },\n        \"project\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to project directory containing pyproject.toml\",\n          \"examples\": [\n            \".\",\n            \"../my-project\"\n          ],\n          \"title\": \"Project\"\n        },\n        \"editable\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Directory to install in editable mode\",\n          \"examples\": [\n            \".\",\n            \"../my-package\"\n          ],\n          \"title\": \"Editable\"\n        }\n      },\n      \"title\": \"Environment\",\n      \"type\": \"object\"\n    },\n    \"FileSystemSource\": {\n      \"description\": \"Source for local Python files.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"filesystem\",\n          \"default\": \"filesystem\",\n          \"description\": \"Source type\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"path\": {\n          \"description\": \"Path to Python file containing the server\",\n          \"title\": \"Path\",\n          \"type\": \"string\"\n        },\n        \"entrypoint\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Name of server instance or factory function (a no-arg function that returns a FastMCP server)\",\n          \"title\": \"Entrypoint\"\n        }\n      },\n      \"required\": [\n        \"path\"\n      ],\n      \"title\": \"FileSystemSource\",\n      \"type\": \"object\"\n    }\n  },\n  \"description\": \"Configuration file for FastMCP servers\",\n  \"properties\": {\n    \"$schema\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ],\n      \"default\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n      \"description\": \"JSON schema for IDE support and validation\",\n      \"title\": \"$Schema\"\n    },\n    \"source\": {\n      \"$ref\": \"#/$defs/FileSystemSource\",\n      \"description\": \"Source configuration for the server\",\n      \"examples\": [\n        {\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"app\",\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"mcp\",\n          \"path\": \"src/server.py\",\n          \"type\": \"filesystem\"\n        }\n      ]\n    },\n    \"environment\": {\n      \"$ref\": \"#/$defs/Environment\",\n      \"description\": \"Python environment setup configuration\"\n    },\n    \"deployment\": {\n      \"$ref\": \"#/$defs/Deployment\",\n      \"description\": \"Server deployment and runtime settings\"\n    }\n  },\n  \"required\": [\n    \"source\"\n  ],\n  \"title\": \"FastMCP Configuration\",\n  \"type\": \"object\",\n  \"$id\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n}\n"
  },
  {
    "path": "docs/changelog.mdx",
    "content": "---\ntitle: \"Changelog\"\nicon: \"list-check\"\nrss: true\ntag: NEW\n---\n\n<Update label=\"v3.0.2\" description=\"2026-02-22\">\n\n**[v3.0.2: Threecovery Mode II](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.2)**\n\nTwo community-contributed fixes: auth headers from MCP transport no longer leak through to downstream OpenAPI APIs, and background task workers now correctly receive the originating request ID. Plus a new docs example for context-aware tool factories.\n\n### Fixes 🐞\n* fix: prevent MCP transport auth header from leaking to downstream OpenAPI APIs by [@stakeswky](https://github.com/stakeswky) in [#3262](https://github.com/PrefectHQ/fastmcp/pull/3262)\n* fix: propagate origin_request_id to background task workers by [@gfortaine](https://github.com/gfortaine) in [#3175](https://github.com/PrefectHQ/fastmcp/pull/3175)\n### Docs 📚\n* Add v3.0.1 release notes by [@jlowin](https://github.com/jlowin) in [#3259](https://github.com/PrefectHQ/fastmcp/pull/3259)\n* docs: add context-aware tool factory example by [@machov](https://github.com/machov) in [#3264](https://github.com/PrefectHQ/fastmcp/pull/3264)\n\n**Full Changelog**: [v3.0.1...v3.0.2](https://github.com/PrefectHQ/fastmcp/compare/v3.0.1...v3.0.2)\n\n</Update>\n\n<Update label=\"v3.0.1\" description=\"2026-02-20\">\n\n**[v3.0.1: Three-covery Mode](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.1)**\n\nFirst patch after 3.0 — mostly smoothing out rough edges discovered in the wild. The big ones: middleware state that wasn't surviving the trip to tool handlers now does, `Tool.from_tool()` accepts callables again, OpenAPI schemas with circular references no longer crash discovery, and decorator overloads now return the correct types in function mode. Also adds `verify_id_token` to OIDCProxy for providers (like some Azure AD configs) that issue opaque access tokens but standard JWT id_tokens.\n\n### Enhancements 🔧\n* Add verify_id_token option to OIDCProxy by [@jlowin](https://github.com/jlowin) in [#3248](https://github.com/PrefectHQ/fastmcp/pull/3248)\n### Fixes 🐞\n* Fix v3.0.0 changelog compare link by [@jlowin](https://github.com/jlowin) in [#3223](https://github.com/PrefectHQ/fastmcp/pull/3223)\n* Fix MDX parse error in upgrade guide prompts by [@jlowin](https://github.com/jlowin) in [#3227](https://github.com/PrefectHQ/fastmcp/pull/3227)\n* Fix non-serializable state lost between middleware and tools by [@jlowin](https://github.com/jlowin) in [#3234](https://github.com/PrefectHQ/fastmcp/pull/3234)\n* Accept callables in Tool.from_tool() by [@jlowin](https://github.com/jlowin) in [#3235](https://github.com/PrefectHQ/fastmcp/pull/3235)\n* Preserve skill metadata through provider wrapping by [@jlowin](https://github.com/jlowin) in [#3237](https://github.com/PrefectHQ/fastmcp/pull/3237)\n* Fix circular reference crash in OpenAPI schemas by [@jlowin](https://github.com/jlowin) in [#3245](https://github.com/PrefectHQ/fastmcp/pull/3245)\n* Fix NameError with future annotations and Context/Depends parameters by [@jlowin](https://github.com/jlowin) in [#3243](https://github.com/PrefectHQ/fastmcp/pull/3243)\n* Fix ty ignore syntax in OpenAPI provider by [@jlowin](https://github.com/jlowin) in [#3253](https://github.com/PrefectHQ/fastmcp/pull/3253)\n* Use max_completion_tokens instead of deprecated max_tokens in OpenAI handler by [@jlowin](https://github.com/jlowin) in [#3254](https://github.com/PrefectHQ/fastmcp/pull/3254)\n* Fix ty compatibility with upgraded deps by [@jlowin](https://github.com/jlowin) in [#3257](https://github.com/PrefectHQ/fastmcp/pull/3257)\n* Fix decorator overload return types for function mode by [@jlowin](https://github.com/jlowin) in [#3258](https://github.com/PrefectHQ/fastmcp/pull/3258)\n\n\n### Docs 📚\n* Sync README with welcome.mdx, fix install count by [@jlowin](https://github.com/jlowin) in [#3224](https://github.com/PrefectHQ/fastmcp/pull/3224)\n* Document dict-to-Message prompt migration in upgrade guides by [@jlowin](https://github.com/jlowin) in [#3225](https://github.com/PrefectHQ/fastmcp/pull/3225)\n* Fix v2 upgrade guide: remove incorrect v1 import advice by [@jlowin](https://github.com/jlowin) in [#3226](https://github.com/PrefectHQ/fastmcp/pull/3226)\n* Animated banner by [@jlowin](https://github.com/jlowin) in [#3231](https://github.com/PrefectHQ/fastmcp/pull/3231)\n* Document mounted server state store isolation in upgrade guide by [@jlowin](https://github.com/jlowin) in [#3236](https://github.com/PrefectHQ/fastmcp/pull/3236)\n\n**Full Changelog**: [v3.0.0...v3.0.1](https://github.com/PrefectHQ/fastmcp/compare/v3.0.0...v3.0.1)\n\n</Update>\n\n<Update label=\"v3.0.0\" description=\"2026-02-18\">\n\n**[v3.0.0: Three at Last](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0)**\n\nFastMCP 3.0 is stable. Two betas, two release candidates, 21 new contributors, and more than 100,000 pre-release installs later — the architecture held up, the upgrade path was smooth, and we're shipping it.\n\nThe surface API is largely unchanged — `@mcp.tool()` still works exactly as before. What changed is everything underneath: a provider/transform architecture that makes FastMCP extensible, observable, and composable in ways v2 couldn't support. If we did our jobs right, you'll barely notice the redesign. You'll just notice that more is possible.\n\nThis is also the release where FastMCP moves from [jlowin/fastmcp](https://github.com/jlowin/fastmcp) to [PrefectHQ/fastmcp](https://github.com/PrefectHQ/fastmcp). GitHub forwards all links, PyPI is the same, imports are the same. A major version felt like the right moment to make it official.\n\n### Build servers from anything\n\n🔌 Components no longer have to live in one file with one server. `FileSystemProvider` discovers tools from directories with hot-reload. `OpenAPIProvider` wraps REST APIs. `ProxyProvider` proxies remote MCP servers. `SkillsProvider` delivers agent skills as resources. Write your own provider for whatever source makes sense. Compose multiple providers into one server, share one across many, or chain them with **transforms** that rename, namespace, filter, version, and secure components as they flow to clients. `ResourcesAsTools` and `PromptsAsTools` expose non-tool components to tool-only clients.\n\n### Ship to production\n\n🔐 Component versioning: serve `@tool(version=\"2.0\")` alongside older versions from one codebase. Granular authorization on individual components with async auth checks, server-wide policies via `AuthMiddleware`, and scope-based access control. OAuth gets CIMD, Static Client Registration, Azure OBO via dependency injection, JWT audience validation, and confused-deputy protections. OpenTelemetry tracing with MCP semantic conventions. Response size limiting. Background tasks with distributed Redis notification and `ctx.elicit()` relay. Security fixes include dropping `diskcache` (CVE-2025-69872) and upgrading `python-multipart` and `protobuf` for additional CVEs.\n\n### Adapt per session\n\n💾 Session state persists across requests via `ctx.set_state()` / `ctx.get_state()`. `ctx.enable_components()` and `ctx.disable_components()` let servers adapt dynamically per client — show admin tools after authentication, progressively reveal capabilities, or scope access by role.\n\n### Develop faster\n\n⚡ `--reload` auto-restarts on file changes. Standalone decorators return the original function, so decorated tools stay callable in tests and non-MCP contexts. Sync functions auto-dispatch to a threadpool. Tool timeouts, MCP-compliant pagination, composable lifespans, `PingMiddleware` for keepalive, and concurrent tool execution when the LLM returns multiple calls in one response.\n\n### Use FastMCP as a CLI\n\n🖥️ `fastmcp list` and `fastmcp call` query and invoke tools on any server from a terminal. `fastmcp discover` scans your editor configs (Claude Desktop, Cursor, Goose, Gemini CLI) and finds configured servers by name. `fastmcp generate-cli` writes a standalone typed CLI where every tool is a subcommand. `fastmcp install` registers your server with Claude Desktop, Cursor, or Goose in one command.\n\n### Build apps (3.1 preview)\n\n📱 Spec-level support for MCP Apps is in: `ui://` resource scheme, typed UI metadata via `AppConfig`, extension negotiation, and runtime detection. The full Apps experience lands in 3.1.\n\n---\n\nIf you hit 3.0 because you didn't pin your dependencies and something breaks — the [upgrade guides](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2) will get you sorted. We minimized breaking changes, but a major version is a major version.\n\n```bash\npip install fastmcp -U\n```\n\n📖 [Documentation](https://gofastmcp.com)\n🚀 [Upgrade from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)\n🔀 [Upgrade from MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk)\n\n## What's Changed\n### New Features 🎉\n* Refactor resource behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2611](https://github.com/PrefectHQ/fastmcp/pull/2611)\n* Refactor prompt behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2610](https://github.com/PrefectHQ/fastmcp/pull/2610)\n* feat: Provider abstraction for dynamic MCP components by [@jlowin](https://github.com/jlowin) in [#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)\n* Unify component storage in LocalProvider by [@jlowin](https://github.com/jlowin) in [#2680](https://github.com/PrefectHQ/fastmcp/pull/2680)\n* Introduce ResourceResult as canonical resource return type by [@jlowin](https://github.com/jlowin) in [#2734](https://github.com/PrefectHQ/fastmcp/pull/2734)\n* Introduce Message and PromptResult as canonical prompt types by [@jlowin](https://github.com/jlowin) in [#2738](https://github.com/PrefectHQ/fastmcp/pull/2738)\n* Add --reload flag for auto-restart on file changes by [@jlowin](https://github.com/jlowin) in [#2816](https://github.com/PrefectHQ/fastmcp/pull/2816)\n* Add FileSystemProvider for filesystem-based component discovery by [@jlowin](https://github.com/jlowin) in [#2823](https://github.com/PrefectHQ/fastmcp/pull/2823)\n* Add standalone decorators and eliminate fastmcp.fs module by [@jlowin](https://github.com/jlowin) in [#2832](https://github.com/PrefectHQ/fastmcp/pull/2832)\n* Add authorization checks to components and servers by [@jlowin](https://github.com/jlowin) in [#2855](https://github.com/PrefectHQ/fastmcp/pull/2855)\n* Decorators return functions instead of component objects by [@jlowin](https://github.com/jlowin) in [#2856](https://github.com/PrefectHQ/fastmcp/pull/2856)\n* Add transform system for modifying components in provider chains by [@jlowin](https://github.com/jlowin) in [#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)\n* Add OpenTelemetry tracing support by [@chrisguidry](https://github.com/chrisguidry) in [#2869](https://github.com/PrefectHQ/fastmcp/pull/2869)\n* Add component versioning and VersionFilter transform by [@jlowin](https://github.com/jlowin) in [#2894](https://github.com/PrefectHQ/fastmcp/pull/2894)\n* Add version discovery and calling a certain version for components by [@jlowin](https://github.com/jlowin) in [#2897](https://github.com/PrefectHQ/fastmcp/pull/2897)\n* Refactor visibility to mark-based enabled system by [@jlowin](https://github.com/jlowin) in [#2912](https://github.com/PrefectHQ/fastmcp/pull/2912)\n* Add session-specific visibility control via Context by [@jlowin](https://github.com/jlowin) in [#2917](https://github.com/PrefectHQ/fastmcp/pull/2917)\n* Add Skills Provider for exposing agent skills as MCP resources by [@jlowin](https://github.com/jlowin) in [#2944](https://github.com/PrefectHQ/fastmcp/pull/2944)\n* Add MCP Apps Phase 1 — SDK compatibility (SEP-1865) by [@jlowin](https://github.com/jlowin) in [#3009](https://github.com/PrefectHQ/fastmcp/pull/3009)\n* Add `fastmcp list` and `fastmcp call` CLI commands by [@jlowin](https://github.com/jlowin) in [#3054](https://github.com/PrefectHQ/fastmcp/pull/3054)\n* Add `fastmcp generate-cli` command by [@jlowin](https://github.com/jlowin) in [#3065](https://github.com/PrefectHQ/fastmcp/pull/3065)\n* Add CIMD (Client ID Metadata Document) support for OAuth by [@jlowin](https://github.com/jlowin) in [#2871](https://github.com/PrefectHQ/fastmcp/pull/2871)\n\n\n### Enhancements 🔧\n* Convert mounted servers to MountedProvider by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)\n* Simplify .key as computed property by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)\n* Refactor MountedProvider into FastMCPProvider + TransformingProvider by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)\n* Enable background task support for custom component subclasses by [@jlowin](https://github.com/jlowin) in [#2657](https://github.com/PrefectHQ/fastmcp/pull/2657)\n* Use CreateTaskResult for background task creation by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)\n* Refactor provider execution: components own their execution by [@jlowin](https://github.com/jlowin) in [#2663](https://github.com/PrefectHQ/fastmcp/pull/2663)\n* Add supports_tasks() method to replace string mode checks by [@jlowin](https://github.com/jlowin) in [#2664](https://github.com/PrefectHQ/fastmcp/pull/2664)\n* Replace type: ignore[attr-defined] with isinstance assertions in tests by [@jlowin](https://github.com/jlowin) in [#2665](https://github.com/PrefectHQ/fastmcp/pull/2665)\n* Add poll_interval to TaskConfig by [@jlowin](https://github.com/jlowin) in [#2666](https://github.com/PrefectHQ/fastmcp/pull/2666)\n* Refactor task module: rename protocol.py to requests.py and reduce redundancy by [@jlowin](https://github.com/jlowin) in [#2667](https://github.com/PrefectHQ/fastmcp/pull/2667)\n* Refactor FastMCPProxy into ProxyProvider by [@jlowin](https://github.com/jlowin) in [#2669](https://github.com/PrefectHQ/fastmcp/pull/2669)\n* Move OpenAPI to providers/openapi submodule by [@jlowin](https://github.com/jlowin) in [#2672](https://github.com/PrefectHQ/fastmcp/pull/2672)\n* Use ergonomic provider initialization pattern by [@jlowin](https://github.com/jlowin) in [#2675](https://github.com/PrefectHQ/fastmcp/pull/2675)\n* Fix ty 0.0.5 type errors by [@jlowin](https://github.com/jlowin) in [#2676](https://github.com/PrefectHQ/fastmcp/pull/2676)\n* Remove execution methods from Provider base class by [@jlowin](https://github.com/jlowin) in [#2681](https://github.com/PrefectHQ/fastmcp/pull/2681)\n* Add type-prefixed keys for globally unique component identification by [@jlowin](https://github.com/jlowin) in [#2704](https://github.com/PrefectHQ/fastmcp/pull/2704)\n* Consolidate notification system with unified API by [@jlowin](https://github.com/jlowin) in [#2710](https://github.com/PrefectHQ/fastmcp/pull/2710)\n* Parallelize provider operations by [@jlowin](https://github.com/jlowin) in [#2716](https://github.com/PrefectHQ/fastmcp/pull/2716)\n* Consolidate get_* and _list_* methods into single API by [@jlowin](https://github.com/jlowin) in [#2719](https://github.com/PrefectHQ/fastmcp/pull/2719)\n* Consolidate execution method chains into single public API by [@jlowin](https://github.com/jlowin) in [#2728](https://github.com/PrefectHQ/fastmcp/pull/2728)\n* Parallelize list_* calls in Provider.get_tasks() by [@jlowin](https://github.com/jlowin) in [#2731](https://github.com/PrefectHQ/fastmcp/pull/2731)\n* Consistent decorator-based MCP handler registration by [@jlowin](https://github.com/jlowin) in [#2732](https://github.com/PrefectHQ/fastmcp/pull/2732)\n* Make ToolResult a BaseModel for serialization support by [@jlowin](https://github.com/jlowin) in [#2736](https://github.com/PrefectHQ/fastmcp/pull/2736)\n* Align prompt handler with resource pattern by [@jlowin](https://github.com/jlowin) in [#2740](https://github.com/PrefectHQ/fastmcp/pull/2740)\n* Update classes to inherit from FastMCPBaseModel instead of BaseModel by [@jlowin](https://github.com/jlowin) in [#2739](https://github.com/PrefectHQ/fastmcp/pull/2739)\n* Add explicit task_meta parameter to FastMCP.call_tool() by [@jlowin](https://github.com/jlowin) in [#2749](https://github.com/PrefectHQ/fastmcp/pull/2749)\n* Add task_meta parameter to read_resource() for explicit task control by [@jlowin](https://github.com/jlowin) in [#2750](https://github.com/PrefectHQ/fastmcp/pull/2750)\n* Add task_meta to prompts and centralize fn_key enrichment by [@jlowin](https://github.com/jlowin) in [#2751](https://github.com/PrefectHQ/fastmcp/pull/2751)\n* Remove unused include_tags/exclude_tags settings by [@jlowin](https://github.com/jlowin) in [#2756](https://github.com/PrefectHQ/fastmcp/pull/2756)\n* Parallelize provider access when executing components by [@jlowin](https://github.com/jlowin) in [#2744](https://github.com/PrefectHQ/fastmcp/pull/2744)\n* Deprecate tool_serializer parameter by [@jlowin](https://github.com/jlowin) in [#2753](https://github.com/PrefectHQ/fastmcp/pull/2753)\n* Feature/supabase custom auth route by [@EloiZalczer](https://github.com/EloiZalczer) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)\n* Remove deprecated WSTransport by [@jlowin](https://github.com/jlowin) in [#2826](https://github.com/PrefectHQ/fastmcp/pull/2826)\n* Add composable lifespans by [@jlowin](https://github.com/jlowin) in [#2828](https://github.com/PrefectHQ/fastmcp/pull/2828)\n* Replace FastMCP.as_proxy() with create_proxy() function by [@jlowin](https://github.com/jlowin) in [#2829](https://github.com/PrefectHQ/fastmcp/pull/2829)\n* Add PingMiddleware for keepalive connections by [@jlowin](https://github.com/jlowin) in [#2838](https://github.com/PrefectHQ/fastmcp/pull/2838)\n* Run sync tools/resources/prompts in threadpool automatically by [@jlowin](https://github.com/jlowin) in [#2865](https://github.com/PrefectHQ/fastmcp/pull/2865)\n* Add timeout parameter for tool foreground execution by [@jlowin](https://github.com/jlowin) in [#2872](https://github.com/PrefectHQ/fastmcp/pull/2872)\n* Adopt OpenTelemetry MCP semantic conventions by [@chrisguidry](https://github.com/chrisguidry) in [#2886](https://github.com/PrefectHQ/fastmcp/pull/2886)\n* Add client_secret_post authentication to IntrospectionTokenVerifier by [@shulkx](https://github.com/shulkx) in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)\n* Add enable_rich_logging setting to disable rich formatting by [@strawgate](https://github.com/strawgate) in [#2893](https://github.com/PrefectHQ/fastmcp/pull/2893)\n* Rename _fastmcp metadata namespace to fastmcp and make non-optional by [@jlowin](https://github.com/jlowin) in [#2895](https://github.com/PrefectHQ/fastmcp/pull/2895)\n* Refactor FastMCP to inherit from Provider by [@jlowin](https://github.com/jlowin) in [#2901](https://github.com/PrefectHQ/fastmcp/pull/2901)\n* Swap public/private method naming in Provider by [@jlowin](https://github.com/jlowin) in [#2902](https://github.com/PrefectHQ/fastmcp/pull/2902)\n* Add MCP-compliant pagination support by [@jlowin](https://github.com/jlowin) in [#2903](https://github.com/PrefectHQ/fastmcp/pull/2903)\n* Support VersionSpec in enable/disable for range-based filtering by [@jlowin](https://github.com/jlowin) in [#2914](https://github.com/PrefectHQ/fastmcp/pull/2914)\n* Immutable transform wrapping for providers by [@jlowin](https://github.com/jlowin) in [#2913](https://github.com/PrefectHQ/fastmcp/pull/2913)\n* Unify discovery API: deduplicate at protocol layer only by [@jlowin](https://github.com/jlowin) in [#2919](https://github.com/PrefectHQ/fastmcp/pull/2919)\n* Add ResourcesAsTools transform by [@jlowin](https://github.com/jlowin) in [#2943](https://github.com/PrefectHQ/fastmcp/pull/2943)\n* Add PromptsAsTools transform by [@jlowin](https://github.com/jlowin) in [#2946](https://github.com/PrefectHQ/fastmcp/pull/2946)\n* Rename Enabled transform to Visibility by [@jlowin](https://github.com/jlowin) in [#2950](https://github.com/PrefectHQ/fastmcp/pull/2950)\n* feat: option to add upstream claims to the FastMCP proxy JWT by [@JonasKs](https://github.com/JonasKs) in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)\n* fix: automatically include offline_access as a scope in the Azure provider by [@JonasKs](https://github.com/JonasKs) in [#3001](https://github.com/PrefectHQ/fastmcp/pull/3001)\n* feat: expand --reload to watch frontend file types by [@jlowin](https://github.com/jlowin) in [#3028](https://github.com/PrefectHQ/fastmcp/pull/3028)\n* Add `fastmcp install stdio` command by [@jlowin](https://github.com/jlowin) in [#3032](https://github.com/PrefectHQ/fastmcp/pull/3032)\n* feat: Goose integration + dedicated install command by [@jlowin](https://github.com/jlowin) in [#3040](https://github.com/PrefectHQ/fastmcp/pull/3040)\n* Add `fastmcp discover` and name-based server resolution by [@jlowin](https://github.com/jlowin) in [#3055](https://github.com/PrefectHQ/fastmcp/pull/3055)\n* feat(context): Add background task support for Context by [@gfortaine](https://github.com/gfortaine) in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)\n* Add server version to banner by [@richardkmichael](https://github.com/richardkmichael) in [#3076](https://github.com/PrefectHQ/fastmcp/pull/3076)\n* Add @handle_tool_errors decorator for standardized error handling by [@dgenio](https://github.com/dgenio) in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)\n* Add ResponseLimitingMiddleware for tool response size control by [@dgenio](https://github.com/dgenio) in [#3072](https://github.com/PrefectHQ/fastmcp/pull/3072)\n* Infer MIME types from OpenAPI response definitions by [@jlowin](https://github.com/jlowin) in [#3101](https://github.com/PrefectHQ/fastmcp/pull/3101)\n* Remove require_auth in favor of scope-based authorization by [@jlowin](https://github.com/jlowin) in [#3103](https://github.com/PrefectHQ/fastmcp/pull/3103)\n* generate-cli: auto-generate SKILL.md agent skill by [@jlowin](https://github.com/jlowin) in [#3115](https://github.com/PrefectHQ/fastmcp/pull/3115)\n* Add Azure OBO dependencies, auth token injection, and documentation by [@jlowin](https://github.com/jlowin) in [#2918](https://github.com/PrefectHQ/fastmcp/pull/2918)\n* feat: add Static Client Registration by [@martimfasantos](https://github.com/martimfasantos) in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)\n* Add concurrent tool execution with sequential flag by [@strawgate](https://github.com/strawgate) in [#3022](https://github.com/PrefectHQ/fastmcp/pull/3022)\n* Add validate_output option for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#3134](https://github.com/PrefectHQ/fastmcp/pull/3134)\n* Relay task elicitation through standard MCP protocol by [@chrisguidry](https://github.com/chrisguidry) in [#3136](https://github.com/PrefectHQ/fastmcp/pull/3136)\n* Support async auth checks by [@jlowin](https://github.com/jlowin) in [#3152](https://github.com/PrefectHQ/fastmcp/pull/3152)\n* Make $ref dereferencing optional via FastMCP(dereference_refs=...) by [@jlowin](https://github.com/jlowin) in [#3151](https://github.com/PrefectHQ/fastmcp/pull/3151)\n* Expose local_provider property, deprecate FastMCP.remove_tool() by [@jlowin](https://github.com/jlowin) in [#3155](https://github.com/PrefectHQ/fastmcp/pull/3155)\n* Add helpers for converting FunctionTool and TransformedTool to SamplingTool by [@strawgate](https://github.com/strawgate) in [#3062](https://github.com/PrefectHQ/fastmcp/pull/3062)\n### Fixes 🐞\n* Let FastMCPError propagate from dependencies by [@chrisguidry](https://github.com/chrisguidry) in [#2646](https://github.com/PrefectHQ/fastmcp/pull/2646)\n* Fix task execution for tools with custom names by [@chrisguidry](https://github.com/chrisguidry) in [#2645](https://github.com/PrefectHQ/fastmcp/pull/2645)\n* fix: check the cause of the tool error by [@rjolaverria](https://github.com/rjolaverria) in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)\n* Fix uvicorn 0.39+ test timeouts and FastMCPError propagation by [@jlowin](https://github.com/jlowin) in [#2699](https://github.com/PrefectHQ/fastmcp/pull/2699)\n* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)\n* Fix Proxy provider to return all resource contents by [@jlowin](https://github.com/jlowin) in [#2742](https://github.com/PrefectHQ/fastmcp/pull/2742)\n* fix: Client OAuth async_auth_flow() method causing MCP-SDK lock error by [@lgndluke](https://github.com/lgndluke) in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)\n* Fix rate limit detection during teardown phase by [@jlowin](https://github.com/jlowin) in [#2757](https://github.com/PrefectHQ/fastmcp/pull/2757)\n* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2764](https://github.com/PrefectHQ/fastmcp/pull/2764)\n* Fix `openapi_version` check so 3.1 is included by [@deeleeramone](https://github.com/deeleeramone) in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)\n* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)\n* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2784](https://github.com/PrefectHQ/fastmcp/pull/2784)\n* Fix OAuth token storage TTL calculation by [@jlowin](https://github.com/jlowin) in [#2796](https://github.com/PrefectHQ/fastmcp/pull/2796)\n* Fix client hanging on HTTP 4xx/5xx errors by [@jlowin](https://github.com/jlowin) in [#2803](https://github.com/PrefectHQ/fastmcp/pull/2803)\n* Fix keep_alive passthrough in StdioMCPServer.to_transport() by [@jlowin](https://github.com/jlowin) in [#2791](https://github.com/PrefectHQ/fastmcp/pull/2791)\n* Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2808](https://github.com/PrefectHQ/fastmcp/pull/2808)\n* Fix timeout not propagating to proxy clients in multi-server MCPConfig by [@jlowin](https://github.com/jlowin) in [#2809](https://github.com/PrefectHQ/fastmcp/pull/2809)\n* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2844](https://github.com/PrefectHQ/fastmcp/pull/2844)\n* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2849](https://github.com/PrefectHQ/fastmcp/pull/2849)\n* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2875](https://github.com/PrefectHQ/fastmcp/pull/2875)\n* fix: broaden combine_lifespans type to accept Mapping return types by [@aminsamir45](https://github.com/aminsamir45) in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)\n* fix: correctly send resource when exchanging code for upstream by [@JonasKs](https://github.com/JonasKs) in [#3013](https://github.com/PrefectHQ/fastmcp/pull/3013)\n* chore: upgrade python-multipart to 0.0.22 (CVE-2026-24486) by [@jlowin](https://github.com/jlowin) in [#3042](https://github.com/PrefectHQ/fastmcp/pull/3042)\n* chore: upgrade protobuf to 6.33.5 (CVE-2026-0994) by [@jlowin](https://github.com/jlowin) in [#3043](https://github.com/PrefectHQ/fastmcp/pull/3043)\n* fix: use MCP spec error code -32002 for resource not found by [@jlowin](https://github.com/jlowin) in [#3041](https://github.com/PrefectHQ/fastmcp/pull/3041)\n* Fix tool_choice reset for structured output sampling by [@strawgate](https://github.com/strawgate) in [#3014](https://github.com/PrefectHQ/fastmcp/pull/3014)\n* fix: Preserve metadata in FastMCPProvider component wrappers by [@NeelayS](https://github.com/NeelayS) in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)\n* fix: enforce redirect URI validation when allowed_client_redirect_uris is supplied by [@nathanwelsh8](https://github.com/nathanwelsh8) in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)\n* Fix --reload port conflict when using explicit port by [@jlowin](https://github.com/jlowin) in [#3070](https://github.com/PrefectHQ/fastmcp/pull/3070)\n* Fix compress_schema to preserve additionalProperties: false by [@jlowin](https://github.com/jlowin) in [#3102](https://github.com/PrefectHQ/fastmcp/pull/3102)\n* Fix CIMD redirect allowlist bypass and cache revalidation by [@jlowin](https://github.com/jlowin) in [#3098](https://github.com/PrefectHQ/fastmcp/pull/3098)\n* Fix session visibility marks leaking across sessions by [@jlowin](https://github.com/jlowin) in [#3132](https://github.com/PrefectHQ/fastmcp/pull/3132)\n* Fix unhandled exceptions in OpenAPI POST tool calls by [@jlowin](https://github.com/jlowin) in [#3133](https://github.com/PrefectHQ/fastmcp/pull/3133)\n* feat: distributed notification queue + BLPOP elicitation for background tasks by [@gfortaine](https://github.com/gfortaine) in [#2906](https://github.com/PrefectHQ/fastmcp/pull/2906)\n* fix: snapshot access token for background tasks by [@gfortaine](https://github.com/gfortaine) in [#3138](https://github.com/PrefectHQ/fastmcp/pull/3138)\n* fix: guard client pagination loops against misbehaving servers by [@jlowin](https://github.com/jlowin) in [#3167](https://github.com/PrefectHQ/fastmcp/pull/3167)\n* Support non-serializable values in Context.set_state by [@jlowin](https://github.com/jlowin) in [#3171](https://github.com/PrefectHQ/fastmcp/pull/3171)\n* Fix stale request context in StatefulProxyClient handlers by [@jlowin](https://github.com/jlowin) in [#3172](https://github.com/PrefectHQ/fastmcp/pull/3172)\n* Drop diskcache dependency (CVE-2025-69872) by [@jlowin](https://github.com/jlowin) in [#3185](https://github.com/PrefectHQ/fastmcp/pull/3185)\n* Fix confused deputy attack via consent binding cookie by [@jlowin](https://github.com/jlowin) in [#3201](https://github.com/PrefectHQ/fastmcp/pull/3201)\n* Add JWT audience validation and RFC 8707 warnings to auth providers by [@jlowin](https://github.com/jlowin) in [#3204](https://github.com/PrefectHQ/fastmcp/pull/3204)\n* Cache OBO credentials on AzureProvider for token reuse by [@jlowin](https://github.com/jlowin) in [#3212](https://github.com/PrefectHQ/fastmcp/pull/3212)\n* Fix invalid uv add command in upgrade guide by [@jlowin](https://github.com/jlowin) in [#3217](https://github.com/PrefectHQ/fastmcp/pull/3217)\n* Use standard traceparent/tracestate keys per OTel MCP semconv by [@chrisguidry](https://github.com/chrisguidry) in [#3221](https://github.com/PrefectHQ/fastmcp/pull/3221)\n### Breaking Changes 🛫\n* Add VisibilityFilter for hierarchical enable/disable by [@jlowin](https://github.com/jlowin) in [#2708](https://github.com/PrefectHQ/fastmcp/pull/2708)\n* Remove automatic environment variable loading from auth providers by [@jlowin](https://github.com/jlowin) in [#2752](https://github.com/PrefectHQ/fastmcp/pull/2752)\n* Make pydocket optional and unify DI systems by [@jlowin](https://github.com/jlowin) in [#2835](https://github.com/PrefectHQ/fastmcp/pull/2835)\n* Add session-scoped state persistence by [@jlowin](https://github.com/jlowin) in [#2873](https://github.com/PrefectHQ/fastmcp/pull/2873)\n* Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig by [@jlowin](https://github.com/jlowin) in [#3117](https://github.com/PrefectHQ/fastmcp/pull/3117)\n* Remove deprecated FastMCP() constructor kwargs by [@jlowin](https://github.com/jlowin) in [#3148](https://github.com/PrefectHQ/fastmcp/pull/3148)\n* Move `fastmcp dev` to `fastmcp dev inspector` by [@jlowin](https://github.com/jlowin) in [#3188](https://github.com/PrefectHQ/fastmcp/pull/3188)\n\n## New Contributors\n* [@ivanbelenky](https://github.com/ivanbelenky) made their first contribution in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)\n* [@rjolaverria](https://github.com/rjolaverria) made their first contribution in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)\n* [@mgoldsborough](https://github.com/mgoldsborough) made their first contribution in [#2701](https://github.com/PrefectHQ/fastmcp/pull/2701)\n* [@Ashif4354](https://github.com/Ashif4354) made their first contribution in [#2707](https://github.com/PrefectHQ/fastmcp/pull/2707)\n* [@majiayu000](https://github.com/majiayu000) made their first contribution in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)\n* [@lgndluke](https://github.com/lgndluke) made their first contribution in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)\n* [@EloiZalczer](https://github.com/EloiZalczer) made their first contribution in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)\n* [@deeleeramone](https://github.com/deeleeramone) made their first contribution in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)\n* [@shea-parkes](https://github.com/shea-parkes) made their first contribution in [#2781](https://github.com/PrefectHQ/fastmcp/pull/2781)\n* [@bryankthompson](https://github.com/bryankthompson) made their first contribution in [#2777](https://github.com/PrefectHQ/fastmcp/pull/2777)\n* [@bhbs](https://github.com/bhbs) made their first contribution in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)\n* [@shulkx](https://github.com/shulkx) made their first contribution in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)\n* [@abhijeethp](https://github.com/abhijeethp) made their first contribution in [#2967](https://github.com/PrefectHQ/fastmcp/pull/2967)\n* [@aminsamir45](https://github.com/aminsamir45) made their first contribution in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)\n* [@JonasKs](https://github.com/JonasKs) made their first contribution in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)\n* [@NeelayS](https://github.com/NeelayS) made their first contribution in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)\n* [@gfortaine](https://github.com/gfortaine) made their first contribution in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)\n* [@nathanwelsh8](https://github.com/nathanwelsh8) made their first contribution in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)\n* [@dgenio](https://github.com/dgenio) made their first contribution in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)\n* [@martimfasantos](https://github.com/martimfasantos) made their first contribution in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)\n* [@jfBiswajit](https://github.com/jfBiswajit) made their first contribution in [#3193](https://github.com/PrefectHQ/fastmcp/pull/3193)\n\n**Full Changelog**: https://github.com/PrefectHQ/fastmcp/compare/v2.14.5...v3.0.0\n\n</Update>\n\n<Update label=\"v3.0.0rc1\" description=\"2026-02-12\">\n\n**[v3.0.0rc1: RC-ing is Believing](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0rc1)**\n\nFastMCP 3 RC1 means we believe the API is stable. Beta 2 drew a wave of real-world adoption — production deployments, migration reports, integration testing — and the feedback overwhelmingly confirmed that the architecture works. This release closes gaps that surfaced under load: auth flows that needed to be async, background tasks that needed reliable notification delivery, and APIs still carrying beta-era naming. If nothing unexpected surfaces, this is what 3.0.0 looks like.\n\n🚨 **Breaking Changes** — The `ui=` parameter is now `app=` with a unified `AppConfig` class (matching the feature's actual name), and 16 `FastMCP()` constructor kwargs have finally been removed. If you've been ignoring months of deprecation warnings, you'll get a `TypeError` with specific migration instructions.\n\n🔐 **Auth Improvements** — Three changes that together round out FastMCP's auth story for production. `auth=` checks can now be `async`, so you can hit databases or external services during authorization — previously, passing an async function silently passed because the unawaited coroutine was truthy. Static Client Registration lets clients provide a pre-registered `client_id`/`client_secret` directly, bypassing DCR for servers that don't support it. And Azure OBO flows are now declarative via dependency injection:\n\n```python\nfrom fastmcp.server.auth.providers.azure import EntraOBOToken\n\n@mcp.tool()\nasync def get_emails(\n    graph_token: str = EntraOBOToken([\"https://graph.microsoft.com/Mail.Read\"]),\n):\n    # OBO exchange already happened — just use the token\n    ...\n```\n\n⚡ **Concurrent Sampling** — When an LLM returns multiple tool calls in a single response, `context.sample()` can now execute them in parallel. Opt in with `tool_concurrency=0` for unlimited parallelism, or set a bound. Tools that aren't safe to parallelize can declare `sequential=True`.\n\n📡 **Background Task Notifications** — Background tasks now reliably push progress updates and elicit user input through the standard MCP protocol. A distributed Redis queue replaces polling (7,200 round-trips/hour → one blocking call), and `ctx.elicit()` in background tasks automatically relays through the client's standard `elicitation_handler`.\n\n✅ **OpenAPI Output Validation** — When backends don't conform to their own OpenAPI schemas, the MCP SDK rejects the response and the tool fails. `validate_output=False` disables strict schema checking while still passing structured JSON to clients — a necessary escape hatch for imperfect APIs.\n\n## What's Changed\n### Enhancements 🔧\n* generate-cli: auto-generate SKILL.md agent skill by [@jlowin](https://github.com/jlowin) in [#3115](https://github.com/PrefectHQ/fastmcp/pull/3115)\n* Scope Martian triage to bug-labeled issues for jlowin by [@jlowin](https://github.com/jlowin) in [#3124](https://github.com/PrefectHQ/fastmcp/pull/3124)\n* Add Azure OBO dependencies, auth token injection, and documentation by [@jlowin](https://github.com/jlowin) in [#2918](https://github.com/PrefectHQ/fastmcp/pull/2918)\n* feat: add Static Client Registration (#3085) by [@martimfasantos](https://github.com/martimfasantos) in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)\n* Add concurrent tool execution with sequential flag by [@strawgate](https://github.com/strawgate) in [#3022](https://github.com/PrefectHQ/fastmcp/pull/3022)\n* Add validate_output option for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#3134](https://github.com/PrefectHQ/fastmcp/pull/3134)\n* Relay task elicitation through standard MCP protocol by [@chrisguidry](https://github.com/chrisguidry) in [#3136](https://github.com/PrefectHQ/fastmcp/pull/3136)\n* Bump py-key-value-aio to `>=0.4.0,<0.5.0` by [@strawgate](https://github.com/strawgate) in [#3143](https://github.com/PrefectHQ/fastmcp/pull/3143)\n* Support async auth checks by [@jlowin](https://github.com/jlowin) in [#3152](https://github.com/PrefectHQ/fastmcp/pull/3152)\n* Make $ref dereferencing optional via FastMCP(dereference_refs=...) by [@jlowin](https://github.com/jlowin) in [#3151](https://github.com/PrefectHQ/fastmcp/pull/3151)\n* Expose local_provider property, deprecate FastMCP.remove_tool() by [@jlowin](https://github.com/jlowin) in [#3155](https://github.com/PrefectHQ/fastmcp/pull/3155)\n* Add helpers for converting FunctionTool and TransformedTool to SamplingTool by [@strawgate](https://github.com/strawgate) in [#3062](https://github.com/PrefectHQ/fastmcp/pull/3062)\n* Updates to github actions / workflows for claude by [@strawgate](https://github.com/strawgate) in [#3157](https://github.com/PrefectHQ/fastmcp/pull/3157)\n### Fixes 🐞\n* Updated deprecation URL for V3 by [@SrzStephen](https://github.com/SrzStephen) in [#3108](https://github.com/PrefectHQ/fastmcp/pull/3108)\n* Fix Windows test timeouts in OAuth proxy provider tests by [@strawgate](https://github.com/strawgate) in [#3123](https://github.com/PrefectHQ/fastmcp/pull/3123)\n* Fix session visibility marks leaking across sessions by [@jlowin](https://github.com/jlowin) in [#3132](https://github.com/PrefectHQ/fastmcp/pull/3132)\n* Fix unhandled exceptions in OpenAPI POST tool calls by [@jlowin](https://github.com/jlowin) in [#3133](https://github.com/PrefectHQ/fastmcp/pull/3133)\n* feat: distributed notification queue + BLPOP elicitation for background tasks by [@gfortaine](https://github.com/gfortaine) in [#2906](https://github.com/PrefectHQ/fastmcp/pull/2906)\n* fix: snapshot access token for background tasks (#3095) by [@gfortaine](https://github.com/gfortaine) in [#3138](https://github.com/PrefectHQ/fastmcp/pull/3138)\n* Stop duplicating path parameter descriptions into tool prose by [@jlowin](https://github.com/jlowin) in [#3149](https://github.com/PrefectHQ/fastmcp/pull/3149)\n* fix: guard client pagination loops against misbehaving servers by [@jlowin](https://github.com/jlowin) in [#3167](https://github.com/PrefectHQ/fastmcp/pull/3167)\n* Fix stale get_* references in docs and examples by [@jlowin](https://github.com/jlowin) in [#3168](https://github.com/PrefectHQ/fastmcp/pull/3168)\n* Support non-serializable values in Context.set_state by [@jlowin](https://github.com/jlowin) in [#3171](https://github.com/PrefectHQ/fastmcp/pull/3171)\n* Fix stale request context in StatefulProxyClient handlers by [@jlowin](https://github.com/jlowin) in [#3172](https://github.com/PrefectHQ/fastmcp/pull/3172)\n### Breaking Changes 🛫\n* Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig by [@jlowin](https://github.com/jlowin) in [#3117](https://github.com/PrefectHQ/fastmcp/pull/3117)\n* Remove deprecated FastMCP() constructor kwargs by [@jlowin](https://github.com/jlowin) in [#3148](https://github.com/PrefectHQ/fastmcp/pull/3148)\n### Docs 📚\n* Update docs to reference beta 2 by [@jlowin](https://github.com/jlowin) in [#3112](https://github.com/PrefectHQ/fastmcp/pull/3112)\n* docs: add pre-registered OAuth clients to v3-features by [@jlowin](https://github.com/jlowin) in [#3129](https://github.com/PrefectHQ/fastmcp/pull/3129)\n### Dependencies 📦\n* chore(deps): bump cryptography from 46.0.3 to 46.0.5 in /examples/testing_demo in the uv group across 1 directory by @dependabot in [#3140](https://github.com/PrefectHQ/fastmcp/pull/3140)\n### Other Changes 🦾\n* docs: add v3.0.0rc1 features to v3-features tracking by [@jlowin](https://github.com/jlowin) in [#3145](https://github.com/PrefectHQ/fastmcp/pull/3145)\n* docs: remove nonexistent MSALApp from rc1 notes by [@jlowin](https://github.com/jlowin) in [#3146](https://github.com/PrefectHQ/fastmcp/pull/3146)\n\n## New Contributors\n* [@martimfasantos](https://github.com/martimfasantos) made their first contribution in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)\n\n**Full Changelog**: https://github.com/PrefectHQ/fastmcp/compare/v3.0.0b2...v3.0.0rc1\n\n</Update>\n\n<Update label=\"v3.0.0b2\" description=\"2026-02-07\">\n\n**[v3.0.0b2: 2 Fast 2 Beta](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0b2)**\n\nFastMCP 3 Beta 2 reflects the huge number of people that kicked the tires on Beta 1. Seven new contributors landed changes in this release, and early migration reports went smoother than expected, including teams on Prefect Horizon upgrading from v2. Most of Beta 2 is refinement: fixing what people found, filling gaps from real usage, hardening edges. But a few new features did land along the way.\n\n🖥️ **Client CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, and `fastmcp generate-cli` turn any MCP server into something you can poke at from a terminal. Discover servers configured in Claude Desktop, Cursor, Goose, or project-level `mcp.json` files and reference them by name. `generate-cli` reads a server's schemas and writes a standalone typed CLI script where every tool is a proper subcommand with flags and help text.\n\n🔐 **CIMD** (Client ID Metadata Documents) adds an alternative to Dynamic Client Registration for OAuth. Clients host a static JSON document at an HTTPS URL; that URL becomes the `client_id`. Server-side support includes SSRF-hardened fetching, cache-aware revalidation, and `private_key_jwt` validation. Enabled by default on `OAuthProxy`.\n\n📱 **MCP Apps** — Spec-level compliance for the MCP Apps extension: `ui://` resource scheme, typed UI metadata on tools and resources, extension negotiation, and `ctx.client_supports_extension()` for runtime detection.\n\n⏳ **Background Task Context** — `Context` now works transparently in Docket workers. `ctx.elicit()` routes through Redis-based coordination so background tasks can pause for user input without any code changes.\n\n🛡️ **ResponseLimitingMiddleware** caps tool response sizes with UTF-8-safe truncation for text and schema-aware error handling for structured outputs.\n\n🪿 **Goose Integration** — `fastmcp install goose` generates deeplink URLs for one-command server installation into Goose.\n\n## What's Changed\n### New Features 🎉\n* Add MCP Apps Phase 1 — SDK compatibility (SEP-1865) by [@jlowin](https://github.com/jlowin) in [#3009](https://github.com/PrefectHQ/fastmcp/pull/3009)\n* Add `fastmcp list` and `fastmcp call` CLI commands by [@jlowin](https://github.com/jlowin) in [#3054](https://github.com/PrefectHQ/fastmcp/pull/3054)\n* Add `fastmcp generate-cli` command by [@jlowin](https://github.com/jlowin) in [#3065](https://github.com/PrefectHQ/fastmcp/pull/3065)\n* Add CIMD (Client ID Metadata Document) support for OAuth by [@jlowin](https://github.com/jlowin) in [#2871](https://github.com/PrefectHQ/fastmcp/pull/2871)\n### Enhancements 🔧\n* Make duplicate bot less aggressive by [@jlowin](https://github.com/jlowin) in [#2981](https://github.com/PrefectHQ/fastmcp/pull/2981)\n* Remove uv lockfile monitoring from Dependabot by [@jlowin](https://github.com/jlowin) in [#2986](https://github.com/PrefectHQ/fastmcp/pull/2986)\n* Run static checks with --upgrade, remove lockfile check by [@jlowin](https://github.com/jlowin) in [#2988](https://github.com/PrefectHQ/fastmcp/pull/2988)\n* Adjust workflow triggers for Marvin by [@strawgate](https://github.com/strawgate) in [#3010](https://github.com/PrefectHQ/fastmcp/pull/3010)\n* Move tests to a reusable action and enable nightly checks by [@strawgate](https://github.com/strawgate) in [#3017](https://github.com/PrefectHQ/fastmcp/pull/3017)\n* feat: option to add upstream claims to the FastMCP proxy JWT by [@JonasKs](https://github.com/JonasKs) in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)\n* Fix ty 0.0.14 compatibility and upgrade dependencies by [@jlowin](https://github.com/jlowin) in [#3027](https://github.com/PrefectHQ/fastmcp/pull/3027)\n* fix: automatically include offline_access as a scope in the Azure provider to enable automatic token refreshing by [@JonasKs](https://github.com/JonasKs) in [#3001](https://github.com/PrefectHQ/fastmcp/pull/3001)\n* feat: expand --reload to watch frontend file types by [@jlowin](https://github.com/jlowin) in [#3028](https://github.com/PrefectHQ/fastmcp/pull/3028)\n* Add `fastmcp install stdio` command by [@jlowin](https://github.com/jlowin) in [#3032](https://github.com/PrefectHQ/fastmcp/pull/3032)\n* Update martian-issue-triage.yml for Workflow editing guidance by [@strawgate](https://github.com/strawgate) in [#3033](https://github.com/PrefectHQ/fastmcp/pull/3033)\n* feat: Goose integration + dedicated install command by [@jlowin](https://github.com/jlowin) in [#3040](https://github.com/PrefectHQ/fastmcp/pull/3040)\n* Fixing spelling issues in multiple files by [@didier-durand](https://github.com/didier-durand) in [#2996](https://github.com/PrefectHQ/fastmcp/pull/2996)\n* Add `fastmcp discover` and name-based server resolution by [@jlowin](https://github.com/jlowin) in [#3055](https://github.com/PrefectHQ/fastmcp/pull/3055)\n* feat(context): Add background task support for Context (SEP-1686) by [@gfortaine](https://github.com/gfortaine) in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)\n* Add server version to banner by [@richardkmichael](https://github.com/richardkmichael) in [#3076](https://github.com/PrefectHQ/fastmcp/pull/3076)\n* Add @handle_tool_errors decorator for standardized error handling by [@dgenio](https://github.com/dgenio) in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)\n* Update Anthropic and OpenAI clients to use Omit instead of NotGiven by [@jlowin](https://github.com/jlowin) in [#3088](https://github.com/PrefectHQ/fastmcp/pull/3088)\n* Add ResponseLimitingMiddleware for tool response size control by [@dgenio](https://github.com/dgenio) in [#3072](https://github.com/PrefectHQ/fastmcp/pull/3072)\n* Infer MIME types from OpenAPI response definitions by [@jlowin](https://github.com/jlowin) in [#3101](https://github.com/PrefectHQ/fastmcp/pull/3101)\n* Remove require_auth in favor of scope-based authorization by [@jlowin](https://github.com/jlowin) in [#3103](https://github.com/PrefectHQ/fastmcp/pull/3103)\n### Fixes 🐞\n* Fix FastAPI mounting examples in docs by [@jlowin](https://github.com/jlowin) in [#2962](https://github.com/PrefectHQ/fastmcp/pull/2962)\n* Remove outdated 'FastMCP 3.0 is coming!' CLI banner by [@jlowin](https://github.com/jlowin) in [#2974](https://github.com/PrefectHQ/fastmcp/pull/2974)\n* Pin httpx `< 1.0` and simplify beta install docs by [@jlowin](https://github.com/jlowin) in [#2975](https://github.com/PrefectHQ/fastmcp/pull/2975)\n* Add enabled field to ToolTransformConfig by [@jlowin](https://github.com/jlowin) in [#2991](https://github.com/PrefectHQ/fastmcp/pull/2991)\n* fix phue2 import in smart_home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#2999](https://github.com/PrefectHQ/fastmcp/pull/2999)\n* fix: broaden combine_lifespans type to accept Mapping return types by [@aminsamir45](https://github.com/aminsamir45) in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)\n* fix: type narrowing for skills resource contents by [@strawgate](https://github.com/strawgate) in [#3023](https://github.com/PrefectHQ/fastmcp/pull/3023)\n* fix: correctly send resource when exchanging code for the upstream by [@JonasKs](https://github.com/JonasKs) in [#3013](https://github.com/PrefectHQ/fastmcp/pull/3013)\n* MCP Apps: structured CSP/permissions types, resource meta propagation fix, QR example by [@jlowin](https://github.com/jlowin) in [#3031](https://github.com/PrefectHQ/fastmcp/pull/3031)\n* chore: upgrade python-multipart to 0.0.22 (CVE-2026-24486) by [@jlowin](https://github.com/jlowin) in [#3042](https://github.com/PrefectHQ/fastmcp/pull/3042)\n* chore: upgrade protobuf to 6.33.5 (CVE-2026-0994) by [@jlowin](https://github.com/jlowin) in [#3043](https://github.com/PrefectHQ/fastmcp/pull/3043)\n* fix: use MCP spec error code -32002 for resource not found by [@jlowin](https://github.com/jlowin) in [#3041](https://github.com/PrefectHQ/fastmcp/pull/3041)\n* Fix tool_choice reset for structured output sampling by [@strawgate](https://github.com/strawgate) in [#3014](https://github.com/PrefectHQ/fastmcp/pull/3014)\n* Fix workflow notification URL formatting in upgrade checks by [@strawgate](https://github.com/strawgate) in [#3047](https://github.com/PrefectHQ/fastmcp/pull/3047)\n* Fix Field() handling in prompts by [@strawgate](https://github.com/strawgate) in [#3050](https://github.com/PrefectHQ/fastmcp/pull/3050)\n* fix: use SkipJsonSchema to exclude callable fields from JSON schema generation by [@strawgate](https://github.com/strawgate) in [#3048](https://github.com/PrefectHQ/fastmcp/pull/3048)\n* fix: Preserve metadata in FastMCPProvider component wrappers by [@NeelayS](https://github.com/NeelayS) in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)\n* Mock network calls in CLI tests and use MemoryStore for OAuth tests by [@strawgate](https://github.com/strawgate) in [#3051](https://github.com/PrefectHQ/fastmcp/pull/3051)\n* Remove OpenAPI timeout parameter, make client optional, surface timeout errors by [@jlowin](https://github.com/jlowin) in [#3067](https://github.com/PrefectHQ/fastmcp/pull/3067)\n* fix: enforce redirect URI validation when allowed_client_redirect_uris is supplied by [@nathanwelsh8](https://github.com/nathanwelsh8) in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)\n* Fix --reload port conflict when using explicit port by [@jlowin](https://github.com/jlowin) in [#3070](https://github.com/PrefectHQ/fastmcp/pull/3070)\n* Fix compress_schema to preserve additionalProperties: false for MCP compatibility by [@jlowin](https://github.com/jlowin) in [#3102](https://github.com/PrefectHQ/fastmcp/pull/3102)\n* Fix CIMD redirect allowlist bypass and cache revalidation by [@jlowin](https://github.com/jlowin) in [#3098](https://github.com/PrefectHQ/fastmcp/pull/3098)\n* Exclude content-type from get_http_headers() to prevent HTTP 415 errors by [@jlowin](https://github.com/jlowin) in [#3104](https://github.com/PrefectHQ/fastmcp/pull/3104)\n### Docs 📚\n* Prepare docs for v3.0 beta release by [@jlowin](https://github.com/jlowin) in [#2954](https://github.com/PrefectHQ/fastmcp/pull/2954)\n* Restructure docs: move transforms to dedicated section by [@jlowin](https://github.com/jlowin) in [#2956](https://github.com/PrefectHQ/fastmcp/pull/2956)\n* Remove unnecessary pip warning by [@jlowin](https://github.com/jlowin) in [#2958](https://github.com/PrefectHQ/fastmcp/pull/2958)\n* Update example MCP version in installation docs by [@jlowin](https://github.com/jlowin) in [#2959](https://github.com/PrefectHQ/fastmcp/pull/2959)\n* Update brand images by [@jlowin](https://github.com/jlowin) in [#2960](https://github.com/PrefectHQ/fastmcp/pull/2960)\n* Restructure README and welcome page with motivated narrative by [@jlowin](https://github.com/jlowin) in [#2963](https://github.com/PrefectHQ/fastmcp/pull/2963)\n* Restructure README and docs with motivated narrative by [@jlowin](https://github.com/jlowin) in [#2964](https://github.com/PrefectHQ/fastmcp/pull/2964)\n* Favicon update and Prefect Horizon docs by [@jlowin](https://github.com/jlowin) in [#2978](https://github.com/PrefectHQ/fastmcp/pull/2978)\n* Add dependency injection documentation and DI-style dependencies by [@jlowin](https://github.com/jlowin) in [#2980](https://github.com/PrefectHQ/fastmcp/pull/2980)\n* docs: document expanded reload behavior and restructure beta sections by [@jlowin](https://github.com/jlowin) in [#3039](https://github.com/PrefectHQ/fastmcp/pull/3039)\n* Add output_schema caveat to response limiting docs by [@jlowin](https://github.com/jlowin) in [#3099](https://github.com/PrefectHQ/fastmcp/pull/3099)\n* Document token passthrough security in OAuth Proxy docs by [@jlowin](https://github.com/jlowin) in [#3100](https://github.com/PrefectHQ/fastmcp/pull/3100)\n### Dependencies 📦\n* Bump ty from 0.0.12 to 0.0.13 by @dependabot in [#2984](https://github.com/PrefectHQ/fastmcp/pull/2984)\n* Bump prek from 0.2.30 to 0.3.0 by @dependabot in [#2982](https://github.com/PrefectHQ/fastmcp/pull/2982)\n### Other Changes 🦾\n* Normalize resource URLs before comparison to support RFC 8707 query parameters by [@abhijeethp](https://github.com/abhijeethp) in [#2967](https://github.com/PrefectHQ/fastmcp/pull/2967)\n* Bump pydocket to 0.17.2 (memory leak fix) by [@chrisguidry](https://github.com/chrisguidry) in [#2998](https://github.com/PrefectHQ/fastmcp/pull/2998)\n* Add AzureJWTVerifier for Managed Identity token verification by [@jlowin](https://github.com/jlowin) in [#3058](https://github.com/PrefectHQ/fastmcp/pull/3058)\n* Add release notes for v2.14.4 and v2.14.5 by [@jlowin](https://github.com/jlowin) in [#3064](https://github.com/PrefectHQ/fastmcp/pull/3064)\n* Add missing beta2 features to v3 release tracking by [@jlowin](https://github.com/jlowin) in [#3105](https://github.com/PrefectHQ/fastmcp/pull/3105)\n\n## New Contributors\n* [@abhijeethp](https://github.com/abhijeethp) made their first contribution in [#2967](https://github.com/PrefectHQ/fastmcp/pull/2967)\n* [@aminsamir45](https://github.com/aminsamir45) made their first contribution in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)\n* [@JonasKs](https://github.com/JonasKs) made their first contribution in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)\n* [@NeelayS](https://github.com/NeelayS) made their first contribution in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)\n* [@gfortaine](https://github.com/gfortaine) made their first contribution in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)\n* [@nathanwelsh8](https://github.com/nathanwelsh8) made their first contribution in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)\n* [@dgenio](https://github.com/dgenio) made their first contribution in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)\n\n**Full Changelog**: https://github.com/PrefectHQ/fastmcp/compare/v3.0.0b1...v3.0.0b2\n\n</Update>\n\n<Update label=\"v3.0.0b1\" description=\"2026-01-20\">\n\n**[v3.0.0b1: This Beta Work](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0b1)**\n\nFastMCP 3.0 rebuilds the framework around three primitives: components, providers, and transforms. Providers source components dynamically—from decorators, filesystems, OpenAPI specs, remote servers, or anywhere else. Transforms modify components as they flow to clients—renaming, namespacing, filtering, securing. The features that required specialized subsystems in v2 now compose naturally from these building blocks.\n\n🔌 **Provider Architecture** unifies how components are sourced. `FileSystemProvider` discovers decorated functions from directories with optional hot-reload. `SkillsProvider` exposes agent skill files as MCP resources. `OpenAPIProvider` and `ProxyProvider` get cleaner integrations. Providers are composable—share one across servers, or attach many to one server.\n\n🔄 **Transforms** add middleware for components. Namespace mounted servers, rename verbose tools, filter by version, control visibility—all without touching source code. `ResourcesAsTools` and `PromptsAsTools` expose non-tool components to tool-only clients.\n\n📋 **Component Versioning** lets you register `@tool(version=\"2.0\")` alongside older versions. Clients see the highest version by default but can request specific versions. `VersionFilter` serves different API versions from one codebase.\n\n💾 **Session-Scoped State** persists across requests. `await ctx.set_state()` and `await ctx.get_state()` now survive the full session. Per-session visibility via `ctx.enable_components()` lets servers adapt dynamically to each client.\n\n⚡ **DX Improvements** include `--reload` for auto-restart during development, automatic threadpool dispatch for sync functions, tool timeouts, pagination for large component lists, and OpenTelemetry tracing.\n\n🔐 **Component Authorization** via `@tool(auth=require_scopes(\"admin\"))` and `AuthMiddleware` for server-wide policies.\n\nBreaking changes are minimal: for most servers, updating the import statement is all you need. See the [migration guide](https://github.com/PrefectHQ/fastmcp/blob/main/docs/getting-started/upgrading/from-fastmcp-2.mdx) for details.\n\n## What's Changed\n### New Features 🎉\n* Refactor resource behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2611](https://github.com/PrefectHQ/fastmcp/pull/2611)\n* Refactor prompt behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2610](https://github.com/PrefectHQ/fastmcp/pull/2610)\n* feat: Provider abstraction for dynamic MCP components by [@jlowin](https://github.com/jlowin) in [#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)\n* Unify component storage in LocalProvider by [@jlowin](https://github.com/jlowin) in [#2680](https://github.com/PrefectHQ/fastmcp/pull/2680)\n* Introduce ResourceResult as canonical resource return type by [@jlowin](https://github.com/jlowin) in [#2734](https://github.com/PrefectHQ/fastmcp/pull/2734)\n* Introduce Message and PromptResult as canonical prompt types by [@jlowin](https://github.com/jlowin) in [#2738](https://github.com/PrefectHQ/fastmcp/pull/2738)\n* Add --reload flag for auto-restart on file changes by [@jlowin](https://github.com/jlowin) in [#2816](https://github.com/PrefectHQ/fastmcp/pull/2816)\n* Add FileSystemProvider for filesystem-based component discovery by [@jlowin](https://github.com/jlowin) in [#2823](https://github.com/PrefectHQ/fastmcp/pull/2823)\n* Add standalone decorators and eliminate fastmcp.fs module by [@jlowin](https://github.com/jlowin) in [#2832](https://github.com/PrefectHQ/fastmcp/pull/2832)\n* Add authorization checks to components and servers by [@jlowin](https://github.com/jlowin) in [#2855](https://github.com/PrefectHQ/fastmcp/pull/2855)\n* Decorators return functions instead of component objects by [@jlowin](https://github.com/jlowin) in [#2856](https://github.com/PrefectHQ/fastmcp/pull/2856)\n* Add transform system for modifying components in provider chains by [@jlowin](https://github.com/jlowin) in [#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)\n* Add OpenTelemetry tracing support by [@chrisguidry](https://github.com/chrisguidry) in [#2869](https://github.com/PrefectHQ/fastmcp/pull/2869)\n* Add component versioning and VersionFilter transform by [@jlowin](https://github.com/jlowin) in [#2894](https://github.com/PrefectHQ/fastmcp/pull/2894)\n* Add version discovery and calling a certain version for components by [@jlowin](https://github.com/jlowin) in [#2897](https://github.com/PrefectHQ/fastmcp/pull/2897)\n* Refactor visibility to mark-based enabled system by [@jlowin](https://github.com/jlowin) in [#2912](https://github.com/PrefectHQ/fastmcp/pull/2912)\n* Add session-specific visibility control via Context by [@jlowin](https://github.com/jlowin) in [#2917](https://github.com/PrefectHQ/fastmcp/pull/2917)\n* Add Skills Provider for exposing agent skills as MCP resources by [@jlowin](https://github.com/jlowin) in [#2944](https://github.com/PrefectHQ/fastmcp/pull/2944)\n### Enhancements 🔧\n* Convert mounted servers to MountedProvider by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)\n* Simplify .key as computed property by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)\n* Refactor MountedProvider into FastMCPProvider + TransformingProvider by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)\n* Enable background task support for custom component subclasses by [@jlowin](https://github.com/jlowin) in [#2657](https://github.com/PrefectHQ/fastmcp/pull/2657)\n* Use CreateTaskResult for background task creation by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)\n* Refactor provider execution: components own their execution by [@jlowin](https://github.com/jlowin) in [#2663](https://github.com/PrefectHQ/fastmcp/pull/2663)\n* Add supports_tasks() method to replace string mode checks by [@jlowin](https://github.com/jlowin) in [#2664](https://github.com/PrefectHQ/fastmcp/pull/2664)\n* Replace type: ignore[attr-defined] with isinstance assertions in tests by [@jlowin](https://github.com/jlowin) in [#2665](https://github.com/PrefectHQ/fastmcp/pull/2665)\n* Add poll_interval to TaskConfig by [@jlowin](https://github.com/jlowin) in [#2666](https://github.com/PrefectHQ/fastmcp/pull/2666)\n* Refactor task module: rename protocol.py to requests.py and reduce redundancy by [@jlowin](https://github.com/jlowin) in [#2667](https://github.com/PrefectHQ/fastmcp/pull/2667)\n* Refactor FastMCPProxy into ProxyProvider by [@jlowin](https://github.com/jlowin) in [#2669](https://github.com/PrefectHQ/fastmcp/pull/2669)\n* Move OpenAPI to providers/openapi submodule by [@jlowin](https://github.com/jlowin) in [#2672](https://github.com/PrefectHQ/fastmcp/pull/2672)\n* Use ergonomic provider initialization pattern by [@jlowin](https://github.com/jlowin) in [#2675](https://github.com/PrefectHQ/fastmcp/pull/2675)\n* Fix ty 0.0.5 type errors by [@jlowin](https://github.com/jlowin) in [#2676](https://github.com/PrefectHQ/fastmcp/pull/2676)\n* Remove execution methods from Provider base class by [@jlowin](https://github.com/jlowin) in [#2681](https://github.com/PrefectHQ/fastmcp/pull/2681)\n* Add type-prefixed keys for globally unique component identification by [@jlowin](https://github.com/jlowin) in [#2704](https://github.com/PrefectHQ/fastmcp/pull/2704)\n* Skip parallel MCP config test on Windows by [@jlowin](https://github.com/jlowin) in [#2711](https://github.com/PrefectHQ/fastmcp/pull/2711)\n* Consolidate notification system with unified API by [@jlowin](https://github.com/jlowin) in [#2710](https://github.com/PrefectHQ/fastmcp/pull/2710)\n* Skip test_multi_client on Windows by [@jlowin](https://github.com/jlowin) in [#2714](https://github.com/PrefectHQ/fastmcp/pull/2714)\n* Parallelize provider operations by [@jlowin](https://github.com/jlowin) in [#2716](https://github.com/PrefectHQ/fastmcp/pull/2716)\n* Consolidate get_* and _list_* methods into single API by [@jlowin](https://github.com/jlowin) in [#2719](https://github.com/PrefectHQ/fastmcp/pull/2719)\n* Consolidate execution method chains into single public API by [@jlowin](https://github.com/jlowin) in [#2728](https://github.com/PrefectHQ/fastmcp/pull/2728)\n* Add documentation check to required PR workflow by [@jlowin](https://github.com/jlowin) in [#2730](https://github.com/PrefectHQ/fastmcp/pull/2730)\n* Parallelize list_* calls in Provider.get_tasks() by [@jlowin](https://github.com/jlowin) in [#2731](https://github.com/PrefectHQ/fastmcp/pull/2731)\n* Consistent decorator-based MCP handler registration by [@jlowin](https://github.com/jlowin) in [#2732](https://github.com/PrefectHQ/fastmcp/pull/2732)\n* Make ToolResult a BaseModel for serialization support by [@jlowin](https://github.com/jlowin) in [#2736](https://github.com/PrefectHQ/fastmcp/pull/2736)\n* Align prompt handler with resource pattern by [@jlowin](https://github.com/jlowin) in [#2740](https://github.com/PrefectHQ/fastmcp/pull/2740)\n* Update classes to inherit from FastMCPBaseModel instead of BaseModel by [@jlowin](https://github.com/jlowin) in [#2739](https://github.com/PrefectHQ/fastmcp/pull/2739)\n* Convert provider tests to use direct server calls by [@jlowin](https://github.com/jlowin) in [#2748](https://github.com/PrefectHQ/fastmcp/pull/2748)\n* Add explicit task_meta parameter to FastMCP.call_tool() by [@jlowin](https://github.com/jlowin) in [#2749](https://github.com/PrefectHQ/fastmcp/pull/2749)\n* Add task_meta parameter to read_resource() for explicit task control by [@jlowin](https://github.com/jlowin) in [#2750](https://github.com/PrefectHQ/fastmcp/pull/2750)\n* Add task_meta to prompts and centralize fn_key enrichment by [@jlowin](https://github.com/jlowin) in [#2751](https://github.com/PrefectHQ/fastmcp/pull/2751)\n* Remove unused include_tags/exclude_tags settings by [@jlowin](https://github.com/jlowin) in [#2756](https://github.com/PrefectHQ/fastmcp/pull/2756)\n* Parallelize provider access when executing components by [@jlowin](https://github.com/jlowin) in [#2744](https://github.com/PrefectHQ/fastmcp/pull/2744)\n* Add tests for OAuth generator cleanup and use aclosing by [@jlowin](https://github.com/jlowin) in [#2759](https://github.com/PrefectHQ/fastmcp/pull/2759)\n* Deprecate tool_serializer parameter by [@jlowin](https://github.com/jlowin) in [#2753](https://github.com/PrefectHQ/fastmcp/pull/2753)\n* Feature/supabase custom auth route by [@EloiZalczer](https://github.com/EloiZalczer) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)\n* Add regression tests for caching with mounted server prefixes by [@jlowin](https://github.com/jlowin) in [#2762](https://github.com/PrefectHQ/fastmcp/pull/2762)\n* Update CLI banner with FastMCP 3.0 notice by [@jlowin](https://github.com/jlowin) in [#2766](https://github.com/PrefectHQ/fastmcp/pull/2766)\n* Make FASTMCP_SHOW_SERVER_BANNER apply to all server startup methods by [@jlowin](https://github.com/jlowin) in [#2771](https://github.com/PrefectHQ/fastmcp/pull/2771)\n* Add MCP tool annotations to smart_home example by [@triepod-ai](https://github.com/triepod-ai) in [#2777](https://github.com/PrefectHQ/fastmcp/pull/2777)\n* Cherry-pick debug logging for OAuth token expiry to main by [@jlowin](https://github.com/jlowin) in [#2797](https://github.com/PrefectHQ/fastmcp/pull/2797)\n* Turn off negative CLI flags by default by [@jlowin](https://github.com/jlowin) in [#2801](https://github.com/PrefectHQ/fastmcp/pull/2801)\n* Configure ty to fail on warnings by [@jlowin](https://github.com/jlowin) in [#2804](https://github.com/PrefectHQ/fastmcp/pull/2804)\n* Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2814](https://github.com/PrefectHQ/fastmcp/pull/2814)\n* Add v3.0 feature tracking document by [@jlowin](https://github.com/jlowin) in [#2822](https://github.com/PrefectHQ/fastmcp/pull/2822)\n* Remove deprecated WSTransport by [@jlowin](https://github.com/jlowin) in [#2826](https://github.com/PrefectHQ/fastmcp/pull/2826)\n* Add composable lifespans by [@jlowin](https://github.com/jlowin) in [#2828](https://github.com/PrefectHQ/fastmcp/pull/2828)\n* Replace FastMCP.as_proxy() with create_proxy() function by [@jlowin](https://github.com/jlowin) in [#2829](https://github.com/PrefectHQ/fastmcp/pull/2829)\n* Add docs-broken-links command and fix docstring markdown parsing by [@jlowin](https://github.com/jlowin) in [#2830](https://github.com/PrefectHQ/fastmcp/pull/2830)\n* Add PingMiddleware for keepalive connections by [@jlowin](https://github.com/jlowin) in [#2838](https://github.com/PrefectHQ/fastmcp/pull/2838)\n* Add CLI update notifications by [@jlowin](https://github.com/jlowin) in [#2840](https://github.com/PrefectHQ/fastmcp/pull/2840)\n* Add agent skills for testing and code review by [@jlowin](https://github.com/jlowin) in [#2846](https://github.com/PrefectHQ/fastmcp/pull/2846)\n* Add loq pre-commit hook for file size enforcement by [@jlowin](https://github.com/jlowin) in [#2847](https://github.com/PrefectHQ/fastmcp/pull/2847)\n* Add transport property to Context by [@jlowin](https://github.com/jlowin) in [#2850](https://github.com/PrefectHQ/fastmcp/pull/2850)\n* Add loq file size limits and clean up type ignores by [@jlowin](https://github.com/jlowin) in [#2859](https://github.com/PrefectHQ/fastmcp/pull/2859)\n* Run sync tools/resources/prompts in threadpool automatically by [@jlowin](https://github.com/jlowin) in [#2865](https://github.com/PrefectHQ/fastmcp/pull/2865)\n* Add timeout parameter for tool foreground execution by [@jlowin](https://github.com/jlowin) in [#2872](https://github.com/PrefectHQ/fastmcp/pull/2872)\n* Adopt OpenTelemetry MCP semantic conventions by [@chrisguidry](https://github.com/chrisguidry) in [#2886](https://github.com/PrefectHQ/fastmcp/pull/2886)\n* Add client_secret_post authentication to IntrospectionTokenVerifier by [@shulkx](https://github.com/shulkx) in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)\n* Add enable_rich_logging setting to disable rich formatting by [@strawgate](https://github.com/strawgate) in [#2893](https://github.com/PrefectHQ/fastmcp/pull/2893)\n* Rename _fastmcp metadata namespace to fastmcp and make non-optional by [@jlowin](https://github.com/jlowin) in [#2895](https://github.com/PrefectHQ/fastmcp/pull/2895)\n* Refactor FastMCP to inherit from Provider by [@jlowin](https://github.com/jlowin) in [#2901](https://github.com/PrefectHQ/fastmcp/pull/2901)\n* Swap public/private method naming in Provider by [@jlowin](https://github.com/jlowin) in [#2902](https://github.com/PrefectHQ/fastmcp/pull/2902)\n* Add MCP-compliant pagination support by [@jlowin](https://github.com/jlowin) in [#2903](https://github.com/PrefectHQ/fastmcp/pull/2903)\n* Support VersionSpec in enable/disable for range-based filtering by [@jlowin](https://github.com/jlowin) in [#2914](https://github.com/PrefectHQ/fastmcp/pull/2914)\n* Remove sync notification infrastructure by [@jlowin](https://github.com/jlowin) in [#2915](https://github.com/PrefectHQ/fastmcp/pull/2915)\n* Immutable transform wrapping for providers by [@jlowin](https://github.com/jlowin) in [#2913](https://github.com/PrefectHQ/fastmcp/pull/2913)\n* Unify discovery API: deduplicate at protocol layer only by [@jlowin](https://github.com/jlowin) in [#2919](https://github.com/PrefectHQ/fastmcp/pull/2919)\n* Split transports.py into modular structure by [@jlowin](https://github.com/jlowin) in [#2921](https://github.com/PrefectHQ/fastmcp/pull/2921)\n* Move session visibility logic to enabled.py by [@jlowin](https://github.com/jlowin) in [#2924](https://github.com/PrefectHQ/fastmcp/pull/2924)\n* Refactor Client class into mixins and add timeout utilities by [@jlowin](https://github.com/jlowin) in [#2933](https://github.com/PrefectHQ/fastmcp/pull/2933)\n* Refactor OAuthProxy into focused modules by [@jlowin](https://github.com/jlowin) in [#2935](https://github.com/PrefectHQ/fastmcp/pull/2935)\n* Refactor LocalProvider into mixin modules by [@jlowin](https://github.com/jlowin) in [#2936](https://github.com/PrefectHQ/fastmcp/pull/2936)\n* Refactor server.py into mixins by [@jlowin](https://github.com/jlowin) in [#2939](https://github.com/PrefectHQ/fastmcp/pull/2939)\n* Consolidate test fixtures and refactor large test files by [@jlowin](https://github.com/jlowin) in [#2941](https://github.com/PrefectHQ/fastmcp/pull/2941)\n* Refactor transform list methods to pure function pattern by [@jlowin](https://github.com/jlowin) in [#2942](https://github.com/PrefectHQ/fastmcp/pull/2942)\n* Add ResourcesAsTools transform by [@jlowin](https://github.com/jlowin) in [#2943](https://github.com/PrefectHQ/fastmcp/pull/2943)\n* Add PromptsAsTools transform by [@jlowin](https://github.com/jlowin) in [#2946](https://github.com/PrefectHQ/fastmcp/pull/2946)\n* Add client utilities for downloading skills by [@jlowin](https://github.com/jlowin) in [#2948](https://github.com/PrefectHQ/fastmcp/pull/2948)\n* Rename Enabled transform to Visibility by [@jlowin](https://github.com/jlowin) in [#2950](https://github.com/PrefectHQ/fastmcp/pull/2950)\n### Fixes 🐞\n* Let FastMCPError propagate from dependencies by [@chrisguidry](https://github.com/chrisguidry) in [#2646](https://github.com/PrefectHQ/fastmcp/pull/2646)\n* Fix task execution for tools with custom names by [@chrisguidry](https://github.com/chrisguidry) in [#2645](https://github.com/PrefectHQ/fastmcp/pull/2645)\n* fix: check the cause of the tool error by [@rjolaverria](https://github.com/rjolaverria) in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)\n* Bump pydocket to 0.16.3 for task cancellation support by [@chrisguidry](https://github.com/chrisguidry) in [#2683](https://github.com/PrefectHQ/fastmcp/pull/2683)\n* Fix uvicorn 0.39+ test timeouts and FastMCPError propagation by [@jlowin](https://github.com/jlowin) in [#2699](https://github.com/PrefectHQ/fastmcp/pull/2699)\n* Fix Prefect website URL in docs footer by [@mgoldsborough](https://github.com/mgoldsborough) in [#2701](https://github.com/PrefectHQ/fastmcp/pull/2701)\n* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)\n* Fix Provider.get_tasks() to include custom component subclasses by [@jlowin](https://github.com/jlowin) in [#2729](https://github.com/PrefectHQ/fastmcp/pull/2729)\n* Fix Proxy provider to return all resource contents by [@jlowin](https://github.com/jlowin) in [#2742](https://github.com/PrefectHQ/fastmcp/pull/2742)\n* Fix prompt return type documentation by [@jlowin](https://github.com/jlowin) in [#2741](https://github.com/PrefectHQ/fastmcp/pull/2741)\n* fix: Client OAuth async_auth_flow() method causing MCP-SDK self.context.lock error. by [@lgndluke](https://github.com/lgndluke) in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)\n* Fix rate limit detection during teardown phase by [@jlowin](https://github.com/jlowin) in [#2757](https://github.com/PrefectHQ/fastmcp/pull/2757)\n* fix: set pytest-asyncio default fixture loop scope to function by [@jlowin](https://github.com/jlowin) in [#2758](https://github.com/PrefectHQ/fastmcp/pull/2758)\n* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2764](https://github.com/PrefectHQ/fastmcp/pull/2764)\n* [BugFix] Fix `openapi_version` Check So 3.1 Is Included by [@deeleeramone](https://github.com/deeleeramone) in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)\n* Fix titled enum elicitation schema to comply with MCP spec by [@jlowin](https://github.com/jlowin) in [#2773](https://github.com/PrefectHQ/fastmcp/pull/2773)\n* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)\n* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2784](https://github.com/PrefectHQ/fastmcp/pull/2784)\n* Fix OAuth token storage TTL calculation by [@jlowin](https://github.com/jlowin) in [#2796](https://github.com/PrefectHQ/fastmcp/pull/2796)\n* Use consistent refresh_ttl for JTI mapping store by [@jlowin](https://github.com/jlowin) in [#2799](https://github.com/PrefectHQ/fastmcp/pull/2799)\n* Return 401 for invalid_grant token errors per MCP spec by [@jlowin](https://github.com/jlowin) in [#2800](https://github.com/PrefectHQ/fastmcp/pull/2800)\n* Fix client hanging on HTTP 4xx/5xx errors by [@jlowin](https://github.com/jlowin) in [#2803](https://github.com/PrefectHQ/fastmcp/pull/2803)\n* Fix unawaited coroutine warning and treat as test error by [@jlowin](https://github.com/jlowin) in [#2806](https://github.com/PrefectHQ/fastmcp/pull/2806)\n* Fix keep_alive passthrough in StdioMCPServer.to_transport() by [@jlowin](https://github.com/jlowin) in [#2791](https://github.com/PrefectHQ/fastmcp/pull/2791)\n* Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2808](https://github.com/PrefectHQ/fastmcp/pull/2808)\n* Prefix Redis keys with docket name for ACL isolation by [@chrisguidry](https://github.com/chrisguidry) in [#2811](https://github.com/PrefectHQ/fastmcp/pull/2811)\n* fix smart_home example: HueAttributes schema and deprecated prefix by [@zzstoatzz](https://github.com/zzstoatzz) in [#2818](https://github.com/PrefectHQ/fastmcp/pull/2818)\n* Fix redirect URI validation docs to match implementation by [@jlowin](https://github.com/jlowin) in [#2824](https://github.com/PrefectHQ/fastmcp/pull/2824)\n* Fix timeout not propagating to proxy clients in multi-server MCPConfig by [@jlowin](https://github.com/jlowin) in [#2809](https://github.com/PrefectHQ/fastmcp/pull/2809)\n* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2844](https://github.com/PrefectHQ/fastmcp/pull/2844)\n* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2849](https://github.com/PrefectHQ/fastmcp/pull/2849)\n* Fix decorator error messages to link to correct doc pages by [@jlowin](https://github.com/jlowin) in [#2858](https://github.com/PrefectHQ/fastmcp/pull/2858)\n* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2875](https://github.com/PrefectHQ/fastmcp/pull/2875)\n* Bump the uv group across 1 directory with 2 updates by [@dependabot](https://github.com/dependabot)\\[bot\\] in [#2890](https://github.com/PrefectHQ/fastmcp/pull/2890)\n### Breaking Changes 🛫\n* Add VisibilityFilter for hierarchical enable/disable by [@jlowin](https://github.com/jlowin) in [#2708](https://github.com/PrefectHQ/fastmcp/pull/2708)\n* Remove automatic environment variable loading from auth providers by [@jlowin](https://github.com/jlowin) in [#2752](https://github.com/PrefectHQ/fastmcp/pull/2752)\n* Make pydocket optional and unify DI systems by [@jlowin](https://github.com/jlowin) in [#2835](https://github.com/PrefectHQ/fastmcp/pull/2835)\n* Add session-scoped state persistence by [@jlowin](https://github.com/jlowin) in [#2873](https://github.com/PrefectHQ/fastmcp/pull/2873)\n### Docs 📚\n* Undocumented `McpError` exceptions by [@ivanbelenky](https://github.com/ivanbelenky) in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)\n* docs(server): add http to transport options in run() method docstring by [@Ashif4354](https://github.com/Ashif4354) in [#2707](https://github.com/PrefectHQ/fastmcp/pull/2707)\n* Add v3 breaking changes notice to README by [@jlowin](https://github.com/jlowin) in [#2712](https://github.com/PrefectHQ/fastmcp/pull/2712)\n* Add changelog entries for v2.13.1 through v2.14.1 by [@jlowin](https://github.com/jlowin) in [#2725](https://github.com/PrefectHQ/fastmcp/pull/2725)\n* Reorganize docs around provider architecture by [@jlowin](https://github.com/jlowin) in [#2723](https://github.com/PrefectHQ/fastmcp/pull/2723)\n* Fix documentation to use 'meta' instead of '_meta' for MCP spec field by [@jlowin](https://github.com/jlowin) in [#2735](https://github.com/PrefectHQ/fastmcp/pull/2735)\n* Enhance documentation on tool transformation by [@shea-parkes](https://github.com/shea-parkes) in [#2781](https://github.com/PrefectHQ/fastmcp/pull/2781)\n* Add FastMCP 4.0 preview to documentation by [@jlowin](https://github.com/jlowin) in [#2831](https://github.com/PrefectHQ/fastmcp/pull/2831)\n* Add release notes for v2.14.2 and v2.14.3 by [@jlowin](https://github.com/jlowin) in [#2852](https://github.com/PrefectHQ/fastmcp/pull/2852)\n* Add missing 3.0.0 version badges and document tasks extra by [@jlowin](https://github.com/jlowin) in [#2866](https://github.com/PrefectHQ/fastmcp/pull/2866)\n* Fix custom provider docs to show correct interface by [@jlowin](https://github.com/jlowin) in [#2920](https://github.com/PrefectHQ/fastmcp/pull/2920)\n* Update v3 features that were missed in PRs by [@jlowin](https://github.com/jlowin) in [#2947](https://github.com/PrefectHQ/fastmcp/pull/2947)\n* Restructure documentation for FastMCP 3.0 by [@jlowin](https://github.com/jlowin) in [#2951](https://github.com/PrefectHQ/fastmcp/pull/2951)\n* Fix broken documentation links by [@jlowin](https://github.com/jlowin) in [#2952](https://github.com/PrefectHQ/fastmcp/pull/2952)\n* Clarify installation for FastMCP 3.0 beta by [@jlowin](https://github.com/jlowin) in [#2953](https://github.com/PrefectHQ/fastmcp/pull/2953)\n### Dependencies 📦\n* Bump peter-evans/create-pull-request from 7 to 8 by [@dependabot](https://github.com/dependabot)\\[bot\\] in [#2623](https://github.com/PrefectHQ/fastmcp/pull/2623)\n* Bump ty to 0.0.7+ by [@jlowin](https://github.com/jlowin) in [#2737](https://github.com/PrefectHQ/fastmcp/pull/2737)\n* Bump the uv group across 1 directory with 4 updates by [@dependabot](https://github.com/dependabot)\\[bot\\] in [#2891](https://github.com/PrefectHQ/fastmcp/pull/2891)\n\n## New Contributors\n* [@ivanbelenky](https://github.com/ivanbelenky) made their first contribution in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)\n* [@rjolaverria](https://github.com/rjolaverria) made their first contribution in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)\n* [@mgoldsborough](https://github.com/mgoldsborough) made their first contribution in [#2701](https://github.com/PrefectHQ/fastmcp/pull/2701)\n* [@Ashif4354](https://github.com/Ashif4354) made their first contribution in [#2707](https://github.com/PrefectHQ/fastmcp/pull/2707)\n* [@majiayu000](https://github.com/majiayu000) made their first contribution in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)\n* [@lgndluke](https://github.com/lgndluke) made their first contribution in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)\n* [@EloiZalczer](https://github.com/EloiZalczer) made their first contribution in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)\n* [@deeleeramone](https://github.com/deeleeramone) made their first contribution in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)\n* [@shea-parkes](https://github.com/shea-parkes) made their first contribution in [#2781](https://github.com/PrefectHQ/fastmcp/pull/2781)\n* [@triepod-ai](https://github.com/triepod-ai) made their first contribution in [#2777](https://github.com/PrefectHQ/fastmcp/pull/2777)\n* [@bhbs](https://github.com/bhbs) made their first contribution in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)\n* [@shulkx](https://github.com/shulkx) made their first contribution in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)\n\n**Full Changelog**: [v2.14.1...v3.0.0b1](https://github.com/PrefectHQ/fastmcp/compare/v2.14.1...v3.0.0b1)\n\n</Update>\n\n<Update label=\"v2.14.5\" description=\"2026-02-03\">\n\n**[v2.14.5: Sealed Docket](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5)**\n\nFixes a memory leak in the memory:// docket broker where cancelled tasks accumulated instead of being cleaned up. Bumps pydocket to ≥0.17.2.\n\n## What's Changed\n### Enhancements 🔧\n* Bump pydocket to 0.17.2 (memory leak fix) by [@chrisguidry](https://github.com/chrisguidry) in [#2992](https://github.com/PrefectHQ/fastmcp/pull/2992)\n\n**Full Changelog**: [v2.14.4...v2.14.5](https://github.com/PrefectHQ/fastmcp/compare/v2.14.4...v2.14.5)\n\n</Update>\n\n<Update label=\"v2.14.4\" description=\"2026-01-22\">\n\n**[v2.14.4: Package Deal](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.4)**\n\nFixes a fresh install bug where the packaging library was missing as a direct dependency, plus backports from 3.x for $ref dereferencing in tool schemas and a task capabilities location fix.\n\n## What's Changed\n### Enhancements 🔧\n* Add release notes for v2.14.2 and v2.14.3 by [@jlowin](https://github.com/jlowin) in [#2851](https://github.com/PrefectHQ/fastmcp/pull/2851)\n### Fixes 🐞\n* Backport: Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2861](https://github.com/PrefectHQ/fastmcp/pull/2861)\n* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2874](https://github.com/PrefectHQ/fastmcp/pull/2874)\n* Add missing packaging dependency by [@jlowin](https://github.com/jlowin) in [#2989](https://github.com/PrefectHQ/fastmcp/pull/2989)\n\n**Full Changelog**: [v2.14.3...v2.14.4](https://github.com/PrefectHQ/fastmcp/compare/v2.14.3...v2.14.4)\n\n</Update>\n\n<Update label=\"v2.14.3\" description=\"2026-01-12\">\n\n**[v2.14.3: Time After Timeout](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.3)**\n\nSometimes five seconds just isn't enough. This release fixes an HTTP transport bug that was cutting connections short, along with OAuth and Redis fixes, better ASGI support, and CLI update notifications so you never miss a beat.\n\n## What's Changed\n### Enhancements 🔧\n* Add debug logging for OAuth token expiry diagnostics by [@jlowin](https://github.com/jlowin) in [#2789](https://github.com/PrefectHQ/fastmcp/pull/2789)\n* Add CLI update notifications by [@jlowin](https://github.com/jlowin) in [#2839](https://github.com/PrefectHQ/fastmcp/pull/2839)\n* Use pip instead of uv pip in upgrade instructions by [@jlowin](https://github.com/jlowin) in [#2841](https://github.com/PrefectHQ/fastmcp/pull/2841)\n### Fixes 🐞\n* Backport OAuth token storage TTL fix to release/2.x by [@jlowin](https://github.com/jlowin) in [#2798](https://github.com/PrefectHQ/fastmcp/pull/2798)\n* Prefix Redis keys with docket name for ACL isolation (2.x backport) by [@chrisguidry](https://github.com/chrisguidry) in [#2812](https://github.com/PrefectHQ/fastmcp/pull/2812)\n* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2843](https://github.com/PrefectHQ/fastmcp/pull/2843)\n* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2848](https://github.com/PrefectHQ/fastmcp/pull/2848)\n\n**Full Changelog**: [v2.14.2...v2.14.3](https://github.com/PrefectHQ/fastmcp/compare/v2.14.2...v2.14.3)\n\n</Update>\n\n<Update label=\"v2.14.2\" description=\"2025-12-31\">\n\n**[v2.14.2: Port Authority](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.2)**\n\nFastMCP 2.14.2 brings a wave of community contributions safely into the 2.x line. A variety of important fixes backported from 3.0 work improve OpenAPI 3.1 compatibility, MCP spec compliance for output schemas and elicitation, and correct a subtle base_url fallback issue. The CLI now gently reminds you that FastMCP 3.0 is on the horizon.\n\n## What's Changed\n### Enhancements 🔧\n* Pin MCP under 2.x by [@jlowin](https://github.com/jlowin) in [#2709](https://github.com/PrefectHQ/fastmcp/pull/2709)\n* Add auth_route parameter to SupabaseProvider by [@EloiZalczer](https://github.com/EloiZalczer) in [#2760](https://github.com/PrefectHQ/fastmcp/pull/2760)\n* Update CLI banner with FastMCP 3.0 notice by [@jlowin](https://github.com/jlowin) in [#2765](https://github.com/PrefectHQ/fastmcp/pull/2765)\n### Fixes 🐞\n* Let FastMCPError propagate unchanged from managers by [@jlowin](https://github.com/jlowin) in [#2697](https://github.com/PrefectHQ/fastmcp/pull/2697)\n* Fix test cleanup for uvicorn 0.39+ context isolation by [@jlowin](https://github.com/jlowin) in [#2696](https://github.com/PrefectHQ/fastmcp/pull/2696)\n* Bump pydocket to 0.16.3 to fix worker cleanup race condition by [@chrisguidry](https://github.com/chrisguidry) in [#2700](https://github.com/PrefectHQ/fastmcp/pull/2700)\n* Fix Prefect website URL in docs footer by [@mgoldsborough](https://github.com/mgoldsborough) in [#2705](https://github.com/PrefectHQ/fastmcp/pull/2705)\n* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2727](https://github.com/PrefectHQ/fastmcp/pull/2727)\n* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2763](https://github.com/PrefectHQ/fastmcp/pull/2763)\n* Fix openapi_version check to include 3.1 by [@deeleeramone](https://github.com/deeleeramone) in [#2769](https://github.com/PrefectHQ/fastmcp/pull/2769)\n* Fix titled enum elicitation schema to comply with MCP spec by [@jlowin](https://github.com/jlowin) in [#2774](https://github.com/PrefectHQ/fastmcp/pull/2774)\n* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2782](https://github.com/PrefectHQ/fastmcp/pull/2782)\n* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2785](https://github.com/PrefectHQ/fastmcp/pull/2785)\n### Docs 📚\n* Add v3 breaking changes notice to README and docs by [@jlowin](https://github.com/jlowin) in [#2713](https://github.com/PrefectHQ/fastmcp/pull/2713)\n* Add changelog entries for v2.13.1 through v2.14.1 by [@jlowin](https://github.com/jlowin) in [#2724](https://github.com/PrefectHQ/fastmcp/pull/2724)\n* conference to 2.x branch by [@aaazzam](https://github.com/aaazzam) in [#2787](https://github.com/PrefectHQ/fastmcp/pull/2787)\n\n**Full Changelog**: [v2.14.1...v2.14.2](https://github.com/PrefectHQ/fastmcp/compare/v2.14.1...v2.14.2)\n\n</Update>\n\n<Update label=\"v2.14.1\" description=\"2025-12-15\">\n\n**[v2.14.1: 'Tis a Gift to Be Sample](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.1)**\n\nFastMCP 2.14.1 introduces sampling with tools (SEP-1577), enabling servers to pass tools to `ctx.sample()` for agentic workflows where the LLM can automatically execute tool calls in a loop. The new `ctx.sample_step()` method provides single LLM calls that return `SampleStep` objects for custom control flow, while `result_type` enables structured outputs via validated Pydantic models.\n\n🤖 **AnthropicSamplingHandler** joins the existing OpenAI handler, providing multi-provider sampling support out of the box.\n\n⚡ **OpenAISamplingHandler promoted** from experimental status—sampling handlers are now production-ready with a unified API.\n\n## What's Changed\n### New Features 🎉\n* Sampling with tools by [@jlowin](https://github.com/jlowin) in [#2538](https://github.com/PrefectHQ/fastmcp/pull/2538)\n* Add AnthropicSamplingHandler by [@jlowin](https://github.com/jlowin) in [#2677](https://github.com/PrefectHQ/fastmcp/pull/2677)\n### Enhancements 🔧\n* Add Python 3.13 to ubuntu CI by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)\n* Remove legacy task initialization workaround by [@jlowin](https://github.com/jlowin) in [#2649](https://github.com/PrefectHQ/fastmcp/pull/2649)\n* Consolidate session state reset logic by [@jlowin](https://github.com/jlowin) in [#2651](https://github.com/PrefectHQ/fastmcp/pull/2651)\n* Unify SamplingHandler; promote OpenAI from experimental by [@jlowin](https://github.com/jlowin) in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)\n* Add `tool_names` parameter to mount() for name customization by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)\n* Use streamable HTTP client API from MCP SDK by [@jlowin](https://github.com/jlowin) in [#2678](https://github.com/PrefectHQ/fastmcp/pull/2678)\n* Deprecate `exclude_args` in favor of Depends() by [@jlowin](https://github.com/jlowin) in [#2693](https://github.com/PrefectHQ/fastmcp/pull/2693)\n### Fixes 🐞\n* Fix prompt tasks to return mcp.types.PromptMessage by [@jlowin](https://github.com/jlowin) in [#2650](https://github.com/PrefectHQ/fastmcp/pull/2650)\n* Fix Windows test warnings by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)\n* Cleanup cancelled connection startup by [@jlowin](https://github.com/jlowin) in [#2679](https://github.com/PrefectHQ/fastmcp/pull/2679)\n* Fix tool choice bug in sampling examples by [@shawnthapa](https://github.com/shawnthapa) in [#2686](https://github.com/PrefectHQ/fastmcp/pull/2686)\n### Docs 📚\n* Simplify Docket tip wording by [@chrisguidry](https://github.com/chrisguidry) in [#2662](https://github.com/PrefectHQ/fastmcp/pull/2662)\n### Other Changes 🦾\n* Bump pydocket to ≥0.15.5 by [@jlowin](https://github.com/jlowin) in [#2694](https://github.com/PrefectHQ/fastmcp/pull/2694)\n\n## New Contributors\n* [@shawnthapa](https://github.com/shawnthapa) made their first contribution in [#2686](https://github.com/PrefectHQ/fastmcp/pull/2686)\n\n**Full Changelog**: [v2.14.0...v2.14.1](https://github.com/PrefectHQ/fastmcp/compare/v2.14.0...v2.14.1)\n\n</Update>\n\n<Update label=\"v2.14.0\" description=\"2025-12-11\">\n\n**[v2.14.0: Task and You Shall Receive](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.0)**\n\nFastMCP 2.14 begins adopting the MCP 2025-11-25 specification, introducing protocol-native background tasks (SEP-1686) that enable long-running operations to report progress without blocking clients. The experimental OpenAPI parser graduates to standard, the `OpenAISamplingHandler` is promoted from experimental, and deprecated APIs accumulated across the 2.x series are removed.\n\n⏳ **Background Tasks** let you add `task=True` to any async tool decorator to run operations in the background with progress tracking. Powered by [Docket](https://github.com/chrisguidry/docket), an enterprise task scheduler handling millions of concurrent tasks daily—in-memory backends work out-of-the-box, and Redis URLs enable persistence and horizontal scaling.\n\n🔧 **OpenAPI Parser Promoted** from experimental to standard with improved performance through single-pass schema processing and cleaner abstractions.\n\n📋 **MCP 2025-11-25 Specification Support** including SSE polling and event resumability (SEP-1699), multi-select enum elicitation schemas (SEP-1330), default values for elicitation (SEP-1034), and tool name validation at registration time (SEP-986).\n\n## Breaking Changes\n- Docket is always enabled; task execution is forbidden through proxies\n- Task protocol enabled by default\n- Removed deprecated settings, imports, and methods accumulated across 2.x series\n\n## What's Changed\n### New Features 🎉\n* OpenAPI parser is now the default by [@jlowin](https://github.com/jlowin) in [#2583](https://github.com/PrefectHQ/fastmcp/pull/2583)\n* Implement SEP-1686: Background Tasks by [@jlowin](https://github.com/jlowin) in [#2550](https://github.com/PrefectHQ/fastmcp/pull/2550)\n### Enhancements 🔧\n* Expose InitializeResult in middleware by [@jlowin](https://github.com/jlowin) in [#2562](https://github.com/PrefectHQ/fastmcp/pull/2562)\n* Update MCP SDK auth compatibility by [@jlowin](https://github.com/jlowin) in [#2574](https://github.com/PrefectHQ/fastmcp/pull/2574)\n* Validate tool names at registration (SEP-986) by [@jlowin](https://github.com/jlowin) in [#2588](https://github.com/PrefectHQ/fastmcp/pull/2588)\n* Support SEP-1034 and SEP-1330 for elicitation by [@jlowin](https://github.com/jlowin) in [#2595](https://github.com/PrefectHQ/fastmcp/pull/2595)\n* Implement SSE polling (SEP-1699) by [@jlowin](https://github.com/jlowin) in [#2612](https://github.com/PrefectHQ/fastmcp/pull/2612)\n* Expose session ID callback by [@jlowin](https://github.com/jlowin) in [#2628](https://github.com/PrefectHQ/fastmcp/pull/2628)\n### Fixes 🐞\n* Fix OAuth metadata discovery by [@jlowin](https://github.com/jlowin) in [#2565](https://github.com/PrefectHQ/fastmcp/pull/2565)\n* Fix fastapi.cli package structure by [@jlowin](https://github.com/jlowin) in [#2570](https://github.com/PrefectHQ/fastmcp/pull/2570)\n* Correct OAuth error codes by [@jlowin](https://github.com/jlowin) in [#2578](https://github.com/PrefectHQ/fastmcp/pull/2578)\n* Prevent function signature modification by [@jlowin](https://github.com/jlowin) in [#2590](https://github.com/PrefectHQ/fastmcp/pull/2590)\n* Fix proxy client kwargs by [@jlowin](https://github.com/jlowin) in [#2605](https://github.com/PrefectHQ/fastmcp/pull/2605)\n* Fix nested server routing by [@jlowin](https://github.com/jlowin) in [#2618](https://github.com/PrefectHQ/fastmcp/pull/2618)\n* Use access token expiry fallback by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)\n* Handle transport cleanup exceptions by [@jlowin](https://github.com/jlowin) in [#2642](https://github.com/PrefectHQ/fastmcp/pull/2642)\n### Docs 📚\n* Add OCI and Supabase integration docs by [@jlowin](https://github.com/jlowin) in [#2580](https://github.com/PrefectHQ/fastmcp/pull/2580)\n* Add v2.14.0 upgrade guide by [@jlowin](https://github.com/jlowin) in [#2598](https://github.com/PrefectHQ/fastmcp/pull/2598)\n* Rewrite background tasks documentation by [@jlowin](https://github.com/jlowin) in [#2620](https://github.com/PrefectHQ/fastmcp/pull/2620)\n* Document read-only tool patterns by [@jlowin](https://github.com/jlowin) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)\n\n## New Contributors\n11 total contributors including 7 first-time participants.\n\n**Full Changelog**: [v2.13.3...v2.14.0](https://github.com/PrefectHQ/fastmcp/compare/v2.13.3...v2.14.0)\n\n</Update>\n\n<Update label=\"v2.13.3\" description=\"2025-12-03\">\n\n**[v2.13.3: Pin-ish Line](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.3)**\n\nFastMCP 2.13.3 pins `mcp<1.23` as a precautionary measure. MCP SDK 1.23 introduced changes related to the November 25, 2025 MCP protocol update that break certain FastMCP patches and workarounds, particularly around OAuth implementation details. FastMCP 2.14 introduces proper support for the updated protocol and requires `mcp>=1.23`.\n\n## What's Changed\n### Fixes 🐞\n* Pin MCP SDK below 1.23 by [@jlowin](https://github.com/jlowin) in [#2545](https://github.com/PrefectHQ/fastmcp/pull/2545)\n\n**Full Changelog**: [v2.13.2...v2.13.3](https://github.com/PrefectHQ/fastmcp/compare/v2.13.2...v2.13.3)\n\n</Update>\n\n<Update label=\"v2.13.2\" description=\"2025-12-01\">\n\n**[v2.13.2: Refreshing Changes](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.2)**\n\nFastMCP 2.13.2 polishes the authentication stack with improvements to token refresh, scope handling, and multi-instance deployments. Discord was added as a built-in OAuth provider, Azure and Google token handling became more reliable, and proxy classes now properly forward icons and titles.\n\n## What's Changed\n### New Features 🎉\n* Add Discord OAuth provider by [@jlowin](https://github.com/jlowin) in [#2480](https://github.com/PrefectHQ/fastmcp/pull/2480)\n### Enhancements 🔧\n* Descope Provider updates for new well-known URLs by [@anvibanga](https://github.com/anvibanga) in [#2465](https://github.com/PrefectHQ/fastmcp/pull/2465)\n* Scalekit provider improvements by [@jlowin](https://github.com/jlowin) in [#2472](https://github.com/PrefectHQ/fastmcp/pull/2472)\n* Add CSP customization for consent screens by [@jlowin](https://github.com/jlowin) in [#2488](https://github.com/PrefectHQ/fastmcp/pull/2488)\n* Add icon support to proxy classes by [@jlowin](https://github.com/jlowin) in [#2495](https://github.com/PrefectHQ/fastmcp/pull/2495)\n### Fixes 🐞\n* Google Provider now defaults to refresh token support by [@jlowin](https://github.com/jlowin) in [#2468](https://github.com/PrefectHQ/fastmcp/pull/2468)\n* Fix Azure OAuth token refresh with unprefixed scopes by [@jlowin](https://github.com/jlowin) in [#2475](https://github.com/PrefectHQ/fastmcp/pull/2475)\n* Prevent `$defs` mutation during tool transforms by [@jlowin](https://github.com/jlowin) in [#2482](https://github.com/PrefectHQ/fastmcp/pull/2482)\n* Fix OAuth proxy refresh token storage for multi-instance deployments by [@jlowin](https://github.com/jlowin) in [#2490](https://github.com/PrefectHQ/fastmcp/pull/2490)\n* Fix stale token issue after OAuth refresh by [@jlowin](https://github.com/jlowin) in [#2498](https://github.com/PrefectHQ/fastmcp/pull/2498)\n* Fix Azure provider OIDC scope handling by [@jlowin](https://github.com/jlowin) in [#2505](https://github.com/PrefectHQ/fastmcp/pull/2505)\n\n## New Contributors\n7 new contributors made their first FastMCP contributions in this release.\n\n**Full Changelog**: [v2.13.1...v2.13.2](https://github.com/PrefectHQ/fastmcp/compare/v2.13.1...v2.13.2)\n\n</Update>\n\n<Update label=\"v2.13.1\" description=\"2025-11-15\">\n\n**[v2.13.1: Heavy Meta](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.1)**\n\nFastMCP 2.13.1 introduces meta parameter support for `ToolResult`, enabling tools to return supplementary metadata alongside results. This supports emerging use cases like OpenAI's Apps SDK. The release also brings improved OAuth functionality with custom token verifiers including a new DebugTokenVerifier, and adds OCI and Supabase authentication providers.\n\n🏷️ **Meta parameters for ToolResult** enable tools to return supplementary metadata alongside results, supporting patterns like OpenAI's Apps SDK integration.\n\n🔐 **Custom token verifiers** with DebugTokenVerifier for development, plus Azure Government support through a `base_authority` parameter and Supabase authentication algorithm configuration.\n\n🔒 **Security fixes** address CVE-2025-61920 through authlib updates and validate Cursor deeplink URLs using safer Windows APIs.\n\n## What's Changed\n### New Features 🎉\n* Add meta parameter support for ToolResult by [@jlowin](https://github.com/jlowin) in [#2350](https://github.com/PrefectHQ/fastmcp/pull/2350)\n* Add OCI authentication provider by [@jlowin](https://github.com/jlowin) in [#2365](https://github.com/PrefectHQ/fastmcp/pull/2365)\n* Add Supabase authentication provider by [@jlowin](https://github.com/jlowin) in [#2378](https://github.com/PrefectHQ/fastmcp/pull/2378)\n### Enhancements 🔧\n* Add custom token verifier support to OIDCProxy by [@jlowin](https://github.com/jlowin) in [#2355](https://github.com/PrefectHQ/fastmcp/pull/2355)\n* Add DebugTokenVerifier for development by [@jlowin](https://github.com/jlowin) in [#2362](https://github.com/PrefectHQ/fastmcp/pull/2362)\n* Add Azure Government support via base_authority parameter by [@jlowin](https://github.com/jlowin) in [#2385](https://github.com/PrefectHQ/fastmcp/pull/2385)\n* Add Supabase authentication algorithm configuration by [@jlowin](https://github.com/jlowin) in [#2392](https://github.com/PrefectHQ/fastmcp/pull/2392)\n### Fixes 🐞\n* Security: Update authlib for CVE-2025-61920 by [@jlowin](https://github.com/jlowin) in [#2398](https://github.com/PrefectHQ/fastmcp/pull/2398)\n* Validate Cursor deeplink URLs using safer Windows APIs by [@jlowin](https://github.com/jlowin) in [#2405](https://github.com/PrefectHQ/fastmcp/pull/2405)\n* Exclude MCP SDK 1.21.1 due to integration test failures by [@jlowin](https://github.com/jlowin) in [#2422](https://github.com/PrefectHQ/fastmcp/pull/2422)\n\n## New Contributors\n18 new contributors joined in this release across 70+ pull requests.\n\n**Full Changelog**: [v2.13.0...v2.13.1](https://github.com/PrefectHQ/fastmcp/compare/v2.13.0...v2.13.1)\n\n</Update>\n\n<Update label=\"v2.13.0\" description=\"2025-10-25\">\n\n**[v2.13.0: Cache Me If You Can](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.0)**\n\nFastMCP 2.13 \"Cache Me If You Can\" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.\n\n💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more! OAuth providers now automatically persist tokens across restarts, and developers can store arbitrary state without reaching for external databases. This foundation enables long-running sessions, cached credentials, and stateful applications built on MCP.\n\n🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions while providing a clean UX with customizable branding. The OAuth proxy now issues its own tokens with automatic key derivation from client secrets, and RFC 7662 token introspection support enables enterprise auth flows. Path prefix mounting enables OAuth-protected servers to integrate into existing web applications under custom paths like `/api`, and MCP 1.17+ compliance with RFC 9728 ensures protocol compatibility. Combined with improved error handling and platform-aware token storage, OAuth is now production-ready and security-hardened for serious applications.\n\nFastMCP now supports out-of-the-box authentication with:\n- **[WorkOS](https://gofastmcp.com/integrations/workos)** and **[AuthKit](https://gofastmcp.com/integrations/authkit)**\n- **[GitHub](https://gofastmcp.com/integrations/github)**\n- **[Google](https://gofastmcp.com/integrations/google)**\n- **[Azure](https://gofastmcp.com/integrations/azure)** (Entra ID)\n- **[AWS Cognito](https://gofastmcp.com/integrations/aws-cognito)**\n- **[Auth0](https://gofastmcp.com/integrations/auth0)**\n- **[Descope](https://gofastmcp.com/integrations/descope)**\n- **[Scalekit](https://gofastmcp.com/integrations/scalekit)**\n- **[JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification)**\n- **[RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol)**\n\n⚡ **Response Caching Middleware** dramatically improves performance for expensive operations. Cache tool and resource responses with configurable TTLs, reducing redundant API calls and speeding up repeated queries.\n\n🔄 **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session. This fixes a long-standing source of confusion in the MCP SDK and enables proper resource management for database connections, background tasks, and other server-level state. Note: this is a breaking behavioral change if you were using the `lifespan` parameter.\n\n✨ **Developer experience improvements** include Pydantic input validation for better type safety, icon support for richer UX, RFC 6570 query parameters for resource templates, improved Context API methods (list_resources, list_prompts, get_prompt), and async file/directory resources.\n\nThis release includes contributions from **20** new contributors and represents the largest feature set in a while. Thank you to everyone who tested preview builds and filed issues - your feedback shaped these improvements!\n\n**Full Changelog**: [v2.12.5...v2.13.0](https://github.com/PrefectHQ/fastmcp/compare/v2.12.5...v2.13.0)\n\n</Update>\n\n<Update label=\"v2.12.5\" description=\"2025-10-17\">\n\n**[v2.12.5: Safety Pin](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.5)**\n\nFastMCP 2.12.5 is a point release that pins the MCP SDK version below 1.17, which introduced a change affecting FastMCP users with auth providers mounted as part of a larger application. This ensures the `.well-known` payload appears in the expected location when using FastMCP authentication providers with composite applications.\n\n## What's Changed\n\n### Fixes 🐞\n* Pin MCP SDK version below 1.17 by [@jlowin](https://github.com/jlowin) in [a1b2c3d](https://github.com/PrefectHQ/fastmcp/commit/dab2b316ddc3883b7896a86da21cacb68da01e5c)\n\n**Full Changelog**: [v2.12.4...v2.12.5](https://github.com/PrefectHQ/fastmcp/compare/v2.12.4...v2.12.5)\n\n</Update>\n\n<Update label=\"v2.12.4\" description=\"2025-09-26\">\n\n**[v2.12.4: OIDC What You Did There](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.4)**\n\nFastMCP 2.12.4 adds comprehensive OIDC support and expands authentication options with AWS Cognito and Descope providers. The release also includes improvements to logging middleware, URL handling for nested resources, persistent OAuth client registration storage, and various fixes to the experimental OpenAPI parser.\n\n## What's Changed\n### New Features 🎉\n* feat: Add support for OIDC configuration by [@ruhulio](https://github.com/ruhulio) in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n### Enhancements 🔧\n* feat: Move the Starlette context middleware to the front by [@akkuman](https://github.com/akkuman) in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* Refactor Logging and Structured Logging Middleware by [@strawgate](https://github.com/strawgate) in [#1805](https://github.com/PrefectHQ/fastmcp/pull/1805)\n* Update pull_request_template.md by [@jlowin](https://github.com/jlowin) in [#1824](https://github.com/PrefectHQ/fastmcp/pull/1824)\n* chore: Set redirect_path default in function by [@ruhulio](https://github.com/ruhulio) in [#1833](https://github.com/PrefectHQ/fastmcp/pull/1833)\n* feat: Set instructions in code by [@attiks](https://github.com/attiks) in [#1838](https://github.com/PrefectHQ/fastmcp/pull/1838)\n* Automatically Create inline Snapshots by [@strawgate](https://github.com/strawgate) in [#1779](https://github.com/PrefectHQ/fastmcp/pull/1779)\n* chore: Cleanup Auth0 redirect_path initialization by [@ruhulio](https://github.com/ruhulio) in [#1842](https://github.com/PrefectHQ/fastmcp/pull/1842)\n* feat: Add support for Descope Authentication by [@anvibanga](https://github.com/anvibanga) in [#1853](https://github.com/PrefectHQ/fastmcp/pull/1853)\n* Update descope version badges by [@jlowin](https://github.com/jlowin) in [#1870](https://github.com/PrefectHQ/fastmcp/pull/1870)\n* Update welcome images by [@jlowin](https://github.com/jlowin) in [#1884](https://github.com/PrefectHQ/fastmcp/pull/1884)\n* Fix rounded edges of image by [@jlowin](https://github.com/jlowin) in [#1886](https://github.com/PrefectHQ/fastmcp/pull/1886)\n* optimize test suite by [@zzstoatzz](https://github.com/zzstoatzz) in [#1893](https://github.com/PrefectHQ/fastmcp/pull/1893)\n* Enhancement: client completions support context_arguments by [@isijoe](https://github.com/isijoe) in [#1906](https://github.com/PrefectHQ/fastmcp/pull/1906)\n* Update Descope icon by [@anvibanga](https://github.com/anvibanga) in [#1912](https://github.com/PrefectHQ/fastmcp/pull/1912)\n* Add AWS Cognito OAuth Provider for Enterprise Authentication by [@stephaneberle9](https://github.com/stephaneberle9) in [#1873](https://github.com/PrefectHQ/fastmcp/pull/1873)\n* Fix typos discovered by codespell by [@cclauss](https://github.com/cclauss) in [#1922](https://github.com/PrefectHQ/fastmcp/pull/1922)\n* Use lowercase namespace for fastmcp logger by [@jlowin](https://github.com/jlowin) in [#1791](https://github.com/PrefectHQ/fastmcp/pull/1791)\n### Fixes 🐞\n* Update quickstart.mdx by [@radi-dev](https://github.com/radi-dev) in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n* Remove extraneous union import by [@jlowin](https://github.com/jlowin) in [#1823](https://github.com/PrefectHQ/fastmcp/pull/1823)\n* Delay import of Provider classes until FastMCP Server Creation by [@strawgate](https://github.com/strawgate) in [#1820](https://github.com/PrefectHQ/fastmcp/pull/1820)\n* fix: correct documentation link in deprecation warning by [@strawgate](https://github.com/strawgate) in [#1828](https://github.com/PrefectHQ/fastmcp/pull/1828)\n* fix: Increase default 3s timeout on Pytest by [@dacamposol](https://github.com/dacamposol) in [#1866](https://github.com/PrefectHQ/fastmcp/pull/1866)\n* fix: Improve URL handling in OIDCConfiguration by [@ruhulio](https://github.com/ruhulio) in [#1850](https://github.com/PrefectHQ/fastmcp/pull/1850)\n* fix: correct typing for on_read_resource middleware method by [@strawgate](https://github.com/strawgate) in [#1858](https://github.com/PrefectHQ/fastmcp/pull/1858)\n* feat(experimental/openapi): replace $ref in additionalProperties; add tests by [@jlowin](https://github.com/jlowin) in [#1735](https://github.com/PrefectHQ/fastmcp/pull/1735)\n* Honor client supplied scopes during registration by [@dmikusa](https://github.com/dmikusa) in [#1860](https://github.com/PrefectHQ/fastmcp/pull/1860)\n* Fix: FastAPI list parameter parsing in experimental OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#1834](https://github.com/PrefectHQ/fastmcp/pull/1834)\n* Add log level support for stdio and HTTP transports by [@jlowin](https://github.com/jlowin) in [#1840](https://github.com/PrefectHQ/fastmcp/pull/1840)\n* Fix OAuth pre-flight check to accept HTTP 200 responses by [@jlowin](https://github.com/jlowin) in [#1874](https://github.com/PrefectHQ/fastmcp/pull/1874)\n* Fix: Preserve OpenAPI parameter descriptions in experimental parser by [@shlomo666](https://github.com/shlomo666) in [#1877](https://github.com/PrefectHQ/fastmcp/pull/1877)\n* Add persistent storage for OAuth client registrations by [@jlowin](https://github.com/jlowin) in [#1879](https://github.com/PrefectHQ/fastmcp/pull/1879)\n* docs: update release dates based on github releases by [@lodu](https://github.com/lodu) in [#1890](https://github.com/PrefectHQ/fastmcp/pull/1890)\n* Small updates to Sampling types by [@strawgate](https://github.com/strawgate) in [#1882](https://github.com/PrefectHQ/fastmcp/pull/1882)\n* remove lockfile smart_home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#1892](https://github.com/PrefectHQ/fastmcp/pull/1892)\n* Fix: Remove JSON schema title metadata while preserving parameters named 'title' by [@jlowin](https://github.com/jlowin) in [#1872](https://github.com/PrefectHQ/fastmcp/pull/1872)\n* Fix: get_resource_url nested URL handling by [@raphael-linx](https://github.com/raphael-linx) in [#1914](https://github.com/PrefectHQ/fastmcp/pull/1914)\n* Clean up code for creating the resource url by [@jlowin](https://github.com/jlowin) in [#1916](https://github.com/PrefectHQ/fastmcp/pull/1916)\n* Fix route count logging in OpenAPI server by [@zzstoatzz](https://github.com/zzstoatzz) in [#1928](https://github.com/PrefectHQ/fastmcp/pull/1928)\n### Docs 📚\n* docs: make Gemini CLI integration discoverable by [@jackwotherspoon](https://github.com/jackwotherspoon) in [#1827](https://github.com/PrefectHQ/fastmcp/pull/1827)\n* docs: update NEW tags for AI assistant integrations by [@jackwotherspoon](https://github.com/jackwotherspoon) in [#1829](https://github.com/PrefectHQ/fastmcp/pull/1829)\n* Update wordmark by [@jlowin](https://github.com/jlowin) in [#1832](https://github.com/PrefectHQ/fastmcp/pull/1832)\n* docs: improve OAuth and OIDC Proxy documentation by [@jlowin](https://github.com/jlowin) in [#1880](https://github.com/PrefectHQ/fastmcp/pull/1880)\n* Update readme + welcome docs by [@jlowin](https://github.com/jlowin) in [#1883](https://github.com/PrefectHQ/fastmcp/pull/1883)\n* Update dark mode image in README by [@jlowin](https://github.com/jlowin) in [#1885](https://github.com/PrefectHQ/fastmcp/pull/1885)\n\n## New Contributors\n* [@radi-dev](https://github.com/radi-dev) made their first contribution in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n* [@akkuman](https://github.com/akkuman) made their first contribution in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* [@ruhulio](https://github.com/ruhulio) made their first contribution in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n* [@attiks](https://github.com/attiks) made their first contribution in [#1838](https://github.com/PrefectHQ/fastmcp/pull/1838)\n* [@anvibanga](https://github.com/anvibanga) made their first contribution in [#1853](https://github.com/PrefectHQ/fastmcp/pull/1853)\n* [@shlomo666](https://github.com/shlomo666) made their first contribution in [#1877](https://github.com/PrefectHQ/fastmcp/pull/1877)\n* [@lodu](https://github.com/lodu) made their first contribution in [#1890](https://github.com/PrefectHQ/fastmcp/pull/1890)\n* [@isijoe](https://github.com/isijoe) made their first contribution in [#1906](https://github.com/PrefectHQ/fastmcp/pull/1906)\n* [@raphael-linx](https://github.com/raphael-linx) made their first contribution in [#1914](https://github.com/PrefectHQ/fastmcp/pull/1914)\n* [@stephaneberle9](https://github.com/stephaneberle9) made their first contribution in [#1873](https://github.com/PrefectHQ/fastmcp/pull/1873)\n* [@cclauss](https://github.com/cclauss) made their first contribution in [#1922](https://github.com/PrefectHQ/fastmcp/pull/1922)\n\n**Full Changelog**: [v2.12.3...v2.12.4](https://github.com/PrefectHQ/fastmcp/compare/v2.12.3...v2.12.4)\n\n</Update>\n\n<Update label=\"v2.12.3\" description=\"2025-09-17\">\n\n**[v2.12.3: Double Time](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.3)**\n\nFastMCP 2.12.3 focuses on performance and developer experience improvements based on community feedback. This release includes optimized auth provider imports that reduce server startup time, enhanced OIDC authentication flows with proper token management, and several reliability fixes for OAuth proxy configurations. The addition of automatic inline snapshot creation significantly improves the testing experience for contributors.\n\n## What's Changed\n### New Features 🎉\n* feat: Support setting MCP log level via transport configuration by [@jlowin](https://github.com/jlowin) in [#1756](https://github.com/PrefectHQ/fastmcp/pull/1756)\n### Enhancements 🔧\n* Add client-side auth support for mcp install cursor command by [@jlowin](https://github.com/jlowin) in [#1747](https://github.com/PrefectHQ/fastmcp/pull/1747)\n* Automatically Create inline Snapshots by [@strawgate](https://github.com/strawgate) in [#1779](https://github.com/PrefectHQ/fastmcp/pull/1779)\n* Use lowercase namespace for fastmcp logger by [@jlowin](https://github.com/jlowin) in [#1791](https://github.com/PrefectHQ/fastmcp/pull/1791)\n### Fixes 🐞\n* fix: correct merge mistake during auth0 refactor by [@strawgate](https://github.com/strawgate) in [#1742](https://github.com/PrefectHQ/fastmcp/pull/1742)\n* Remove extraneous union import by [@jlowin](https://github.com/jlowin) in [#1823](https://github.com/PrefectHQ/fastmcp/pull/1823)\n* Delay import of Provider classes until FastMCP Server Creation by [@strawgate](https://github.com/strawgate) in [#1820](https://github.com/PrefectHQ/fastmcp/pull/1820)\n* fix: refactor OIDC configuration provider for proper token management by [@strawgate](https://github.com/strawgate) in [#1751](https://github.com/PrefectHQ/fastmcp/pull/1751)\n* Fix smart_home example imports by [@strawgate](https://github.com/strawgate) in [#1753](https://github.com/PrefectHQ/fastmcp/pull/1753)\n* fix: correct oauth proxy initialization of client by [@strawgate](https://github.com/strawgate) in [#1759](https://github.com/PrefectHQ/fastmcp/pull/1759)\n* Fix: return empty string when prompts have no arguments by [@jlowin](https://github.com/jlowin) in [#1766](https://github.com/PrefectHQ/fastmcp/pull/1766)\n* Fix async server callbacks by [@strawgate](https://github.com/strawgate) in [#1774](https://github.com/PrefectHQ/fastmcp/pull/1774)\n* Fix error when retrieving Completion API errors by [@strawgate](https://github.com/strawgate) in [#1785](https://github.com/PrefectHQ/fastmcp/pull/1785)\n* fix: correct documentation link in deprecation warning by [@strawgate](https://github.com/strawgate) in [#1828](https://github.com/PrefectHQ/fastmcp/pull/1828)\n### Docs 📚\n* Add migration docs for 2.12 by [@jlowin](https://github.com/jlowin) in [#1745](https://github.com/PrefectHQ/fastmcp/pull/1745)\n* Update docs for default sampling implementation to mention OpenAI API Key by [@strawgate](https://github.com/strawgate) in [#1763](https://github.com/PrefectHQ/fastmcp/pull/1763)\n* Add tip about sampling prompts and user_context to sampling documentation by [@jlowin](https://github.com/jlowin) in [#1764](https://github.com/PrefectHQ/fastmcp/pull/1764)\n* Update quickstart.mdx by [@radi-dev](https://github.com/radi-dev) in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n### Other Changes 🦾\n* Replace Marvin with Claude Code in CI by [@jlowin](https://github.com/jlowin) in [#1800](https://github.com/PrefectHQ/fastmcp/pull/1800)\n* Refactor logging and structured logging middleware by [@strawgate](https://github.com/strawgate) in [#1805](https://github.com/PrefectHQ/fastmcp/pull/1805)\n* feat: Move the Starlette context middleware to the front by [@akkuman](https://github.com/akkuman) in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* feat: Add support for OIDC configuration by [@ruhulio](https://github.com/ruhulio) in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n\n## New Contributors\n* [@radi-dev](https://github.com/radi-dev) made their first contribution in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n* [@akkuman](https://github.com/akkuman) made their first contribution in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* [@ruhulio](https://github.com/ruhulio) made their first contribution in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n\n**Full Changelog**: [v2.12.2...v2.12.3](https://github.com/PrefectHQ/fastmcp/compare/v2.12.2...v2.12.3)\n\n</Update>\n\n<Update label=\"v2.12.2\" description=\"2025-09-03\">\n\n**[v2.12.2: Perchance to Stream](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.2)**\n\nThis is a hotfix for a bug where the `streamable-http` transport was not recognized as a valid option in `fastmcp.json` configuration files, despite being supported by the CLI. This resulted in a parsing error when the CLI arguments were merged against the configuration spec. \n\n## What's Changed\n### Fixes 🐞\n* Fix streamable-http transport validation in fastmcp.json config by [@jlowin](https://github.com/jlowin) in [#1739](https://github.com/PrefectHQ/fastmcp/pull/1739)\n\n**Full Changelog**: [v2.12.1...v2.12.2](https://github.com/PrefectHQ/fastmcp/compare/v2.12.1...v2.12.2)\n\n</Update>\n\n<Update label=\"v2.12.1\" description=\"2025-09-03\">\n\n**[v2.12.1: OAuth to Joy](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.1)**\n\nFastMCP 2.12.1 strengthens the OAuth proxy implementation based on extensive community testing and feedback. This release improves client storage reliability, adds PKCE forwarding for enhanced security, introduces configurable token endpoint authentication methods, and expands scope handling—all addressing real-world integration challenges discovered since 2.12.0. The enhanced test suite with mock providers ensures these improvements are robust and maintainable.\n\n## Breaking Changes\n- **OAuth Proxy**: Users of built-in IDP integrations should note that `resource_server_url` has been renamed to `base_url` for clarity and consistency\n\n## What's Changed\n### Enhancements 🔧\n* Make openai dependency optional by [@jlowin](https://github.com/jlowin) in [#1701](https://github.com/PrefectHQ/fastmcp/pull/1701)\n* Remove orphaned OAuth proxy code by [@jlowin](https://github.com/jlowin) in [#1722](https://github.com/PrefectHQ/fastmcp/pull/1722)\n* Expose valid scopes from OAuthProxy metadata by [@dmikusa](https://github.com/dmikusa) in [#1717](https://github.com/PrefectHQ/fastmcp/pull/1717)\n* OAuth proxy PKCE forwarding by [@jlowin](https://github.com/jlowin) in [#1733](https://github.com/PrefectHQ/fastmcp/pull/1733)\n* Add token_endpoint_auth_method parameter to OAuthProxy by [@jlowin](https://github.com/jlowin) in [#1736](https://github.com/PrefectHQ/fastmcp/pull/1736)\n* Clean up and enhance OAuth proxy tests with mock provider by [@jlowin](https://github.com/jlowin) in [#1738](https://github.com/PrefectHQ/fastmcp/pull/1738)\n### Fixes 🐞\n* refactor: replace auth provider registry with ImportString by [@jlowin](https://github.com/jlowin) in [#1710](https://github.com/PrefectHQ/fastmcp/pull/1710)\n* Fix OAuth resource URL handling and WWW-Authenticate header by [@jlowin](https://github.com/jlowin) in [#1706](https://github.com/PrefectHQ/fastmcp/pull/1706)\n* Fix OAuth proxy client storage and add retry logic by [@jlowin](https://github.com/jlowin) in [#1732](https://github.com/PrefectHQ/fastmcp/pull/1732)\n### Docs 📚\n* Fix documentation: use StreamableHttpTransport for headers in testing by [@jlowin](https://github.com/jlowin) in [#1702](https://github.com/PrefectHQ/fastmcp/pull/1702)\n* docs: add performance warnings for mounted servers and proxies by [@strawgate](https://github.com/strawgate) in [#1669](https://github.com/PrefectHQ/fastmcp/pull/1669)\n* Update documentation around scopes for google by [@jlowin](https://github.com/jlowin) in [#1703](https://github.com/PrefectHQ/fastmcp/pull/1703)\n* Add deployment information to quickstart by [@seanpwlms](https://github.com/seanpwlms) in [#1433](https://github.com/PrefectHQ/fastmcp/pull/1433)\n* Update quickstart by [@jlowin](https://github.com/jlowin) in [#1728](https://github.com/PrefectHQ/fastmcp/pull/1728)\n* Add development docs for FastMCP by [@jlowin](https://github.com/jlowin) in [#1719](https://github.com/PrefectHQ/fastmcp/pull/1719)\n### Other Changes 🦾\n* Set generics without bounds to default=Any by [@strawgate](https://github.com/strawgate) in [#1648](https://github.com/PrefectHQ/fastmcp/pull/1648)\n\n## New Contributors\n* [@dmikusa](https://github.com/dmikusa) made their first contribution in [#1717](https://github.com/PrefectHQ/fastmcp/pull/1717)\n* [@seanpwlms](https://github.com/seanpwlms) made their first contribution in [#1433](https://github.com/PrefectHQ/fastmcp/pull/1433)\n\n**Full Changelog**: [v2.12.0...v2.12.1](https://github.com/PrefectHQ/fastmcp/compare/v2.12.0...v2.12.1)\n\n</Update>\n\n<Update label=\"v2.12.0\" description=\"2025-08-31\">\n\n**[v2.12.0: Auth to the Races](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.0)**\n\nFastMCP 2.12 represents one of our most significant releases to date, both in scope and community involvement. After extensive testing and iteration with the community, we're shipping major improvements to authentication, configuration, and MCP feature adoption.\n\n🔐 **OAuth Proxy for Broader Provider Support** addresses a fundamental challenge: while MCP requires Dynamic Client Registration (DCR), many popular OAuth providers don't support it. The new OAuth proxy bridges this gap, enabling FastMCP servers to authenticate with providers like GitHub, Google, WorkOS, and Azure through minimal configuration. These native integrations ship today, with more providers planned based on community needs.\n\n📋 **Declarative JSON Configuration** introduces a standardized, portable way to describe and deploy MCP servers. The `fastmcp.json` configuration file becomes the single source of truth for dependencies, transport settings, entrypoints, and server metadata. This foundation sets the stage for future capabilities like transformations and remote sources, moving toward a world where MCP servers are as portable and shareable as container images.\n\n🧠 **Sampling API Fallback** tackles the chicken-and-egg problem limiting adoption of advanced MCP features. Sampling—where servers request LLM completions from clients—is powerful but underutilized due to limited client support. FastMCP now lets server authors define fallback handlers that generate sampling completions server-side when clients don't support the feature, encouraging adoption while maintaining compatibility.\n\nThis release took longer than usual to ship, and for good reason: the community's aggressive testing and feedback on the authentication system helped us reach a level of stability we're confident in. There's certainly more work ahead, but these foundations position FastMCP to handle increasingly complex use cases while remaining approachable for developers.\n\nThank you to our new contributors and everyone who tested preview builds. Your feedback directly shaped these features.\n\n## What's Changed\n### New Features 🎉\n* Add OAuth proxy that allows authentication with social IDPs without DCR support by [@jlowin](https://github.com/jlowin) in [#1434](https://github.com/PrefectHQ/fastmcp/pull/1434)\n* feat: introduce declarative JSON configuration system by [@jlowin](https://github.com/jlowin) in [#1517](https://github.com/PrefectHQ/fastmcp/pull/1517)\n* ✨ Fallback to a Completions API when Sampling is not available by [@strawgate](https://github.com/strawgate) in [#1145](https://github.com/PrefectHQ/fastmcp/pull/1145)\n* Implement typed source system for FastMCP declarative configuration by [@jlowin](https://github.com/jlowin) in [#1607](https://github.com/PrefectHQ/fastmcp/pull/1607)\n### Enhancements 🔧\n* Support importing custom_route endpoints when mounting servers by [@jlowin](https://github.com/jlowin) in [#1470](https://github.com/PrefectHQ/fastmcp/pull/1470)\n* Remove unnecessary asserts by [@jlowin](https://github.com/jlowin) in [#1484](https://github.com/PrefectHQ/fastmcp/pull/1484)\n* Add Claude issue triage by [@jlowin](https://github.com/jlowin) in [#1510](https://github.com/PrefectHQ/fastmcp/pull/1510)\n* Inline dedupe prompt by [@jlowin](https://github.com/jlowin) in [#1512](https://github.com/PrefectHQ/fastmcp/pull/1512)\n* Improve stdio and mcp_config clean-up by [@strawgate](https://github.com/strawgate) in [#1444](https://github.com/PrefectHQ/fastmcp/pull/1444)\n* involve kwargs to pass parameters on creating RichHandler for logging customization. by [@itaru2622](https://github.com/itaru2622) in [#1504](https://github.com/PrefectHQ/fastmcp/pull/1504)\n* Move SDK docs generation to post-merge workflow by [@jlowin](https://github.com/jlowin) in [#1513](https://github.com/PrefectHQ/fastmcp/pull/1513)\n* Improve label triage guidance by [@jlowin](https://github.com/jlowin) in [#1516](https://github.com/PrefectHQ/fastmcp/pull/1516)\n* Add code review guidelines for agents by [@jlowin](https://github.com/jlowin) in [#1520](https://github.com/PrefectHQ/fastmcp/pull/1520)\n* Remove trailing slash in unit tests by [@jlowin](https://github.com/jlowin) in [#1535](https://github.com/PrefectHQ/fastmcp/pull/1535)\n* Update OAuth callback UI branding by [@jlowin](https://github.com/jlowin) in [#1536](https://github.com/PrefectHQ/fastmcp/pull/1536)\n* Fix Marvin workflow to support development tools by [@jlowin](https://github.com/jlowin) in [#1537](https://github.com/PrefectHQ/fastmcp/pull/1537)\n* Add mounted_components_raise_on_load_error setting for debugging by [@jlowin](https://github.com/jlowin) in [#1534](https://github.com/PrefectHQ/fastmcp/pull/1534)\n* feat: Add --workspace flag to fastmcp install cursor by [@jlowin](https://github.com/jlowin) in [#1522](https://github.com/PrefectHQ/fastmcp/pull/1522)\n* switch from `pyright` to `ty` by [@zzstoatzz](https://github.com/zzstoatzz) in [#1545](https://github.com/PrefectHQ/fastmcp/pull/1545)\n* feat: trigger Marvin workflow on PR body content by [@jlowin](https://github.com/jlowin) in [#1549](https://github.com/PrefectHQ/fastmcp/pull/1549)\n* Add WorkOS and Azure OAuth providers by [@jlowin](https://github.com/jlowin) in [#1550](https://github.com/PrefectHQ/fastmcp/pull/1550)\n* Adjust timeout for slow MCP Server shutdown test by [@strawgate](https://github.com/strawgate) in [#1561](https://github.com/PrefectHQ/fastmcp/pull/1561)\n* Update banner by [@jlowin](https://github.com/jlowin) in [#1567](https://github.com/PrefectHQ/fastmcp/pull/1567)\n* Added import of AuthProxy to auth __init__ by [@KaliszS](https://github.com/KaliszS) in [#1568](https://github.com/PrefectHQ/fastmcp/pull/1568)\n* Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582)\n* Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588)\n* Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591)\n* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)\n* Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)\n* Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604)\n* Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581)\n* chore: Automate docs and schema updates via PRs by [@jlowin](https://github.com/jlowin) in [#1611](https://github.com/PrefectHQ/fastmcp/pull/1611)\n* Experiment with haiku for limited workflows by [@jlowin](https://github.com/jlowin) in [#1613](https://github.com/PrefectHQ/fastmcp/pull/1613)\n* feat: Improve GitHub workflow automation for schema and SDK docs by [@jlowin](https://github.com/jlowin) in [#1615](https://github.com/PrefectHQ/fastmcp/pull/1615)\n* Consolidate server loading logic into FileSystemSource by [@jlowin](https://github.com/jlowin) in [#1614](https://github.com/PrefectHQ/fastmcp/pull/1614)\n* Prevent Haiku Marvin from commenting when there are no duplicates by [@jlowin](https://github.com/jlowin) in [#1622](https://github.com/PrefectHQ/fastmcp/pull/1622)\n* chore: Add clarifying note to automated PR bodies by [@jlowin](https://github.com/jlowin) in [#1623](https://github.com/PrefectHQ/fastmcp/pull/1623)\n* feat: introduce inline snapshots by [@strawgate](https://github.com/strawgate) in [#1605](https://github.com/PrefectHQ/fastmcp/pull/1605)\n* Improve fastmcp.json environment configuration and project-based deployments by [@jlowin](https://github.com/jlowin) in [#1631](https://github.com/PrefectHQ/fastmcp/pull/1631)\n* fix: allow passing query params in OAuthProxy upstream authorization url by [@danb27](https://github.com/danb27) in [#1630](https://github.com/PrefectHQ/fastmcp/pull/1630)\n* Support multiple --with-editable flags in CLI commands by [@jlowin](https://github.com/jlowin) in [#1634](https://github.com/PrefectHQ/fastmcp/pull/1634)\n* feat: support comma separated oauth scopes by [@jlowin](https://github.com/jlowin) in [#1642](https://github.com/PrefectHQ/fastmcp/pull/1642)\n* Add allowed_client_redirect_uris to OAuth provider subclasses by [@jlowin](https://github.com/jlowin) in [#1662](https://github.com/PrefectHQ/fastmcp/pull/1662)\n* Consolidate CLI config parsing and prevent infinite loops by [@jlowin](https://github.com/jlowin) in [#1660](https://github.com/PrefectHQ/fastmcp/pull/1660)\n* Internal refactor: mcp server config by [@jlowin](https://github.com/jlowin) in [#1672](https://github.com/PrefectHQ/fastmcp/pull/1672)\n* Refactor Environment to support multiple runtime types by [@jlowin](https://github.com/jlowin) in [#1673](https://github.com/PrefectHQ/fastmcp/pull/1673)\n* Add type field to Environment base class by [@jlowin](https://github.com/jlowin) in [#1676](https://github.com/PrefectHQ/fastmcp/pull/1676)\n### Fixes 🐞\n* Fix breaking change: restore output_schema=False compatibility by [@jlowin](https://github.com/jlowin) in [#1482](https://github.com/PrefectHQ/fastmcp/pull/1482)\n* Fix #1506: Update tool filtering documentation from _meta to meta by [@maybenotconnor](https://github.com/maybenotconnor) in [#1511](https://github.com/PrefectHQ/fastmcp/pull/1511)\n* Fix pytest warnings by [@jlowin](https://github.com/jlowin) in [#1559](https://github.com/PrefectHQ/fastmcp/pull/1559)\n* nest schemas under assets by [@jlowin](https://github.com/jlowin) in [#1593](https://github.com/PrefectHQ/fastmcp/pull/1593)\n* Skip flaky windows test by [@jlowin](https://github.com/jlowin) in [#1596](https://github.com/PrefectHQ/fastmcp/pull/1596)\n* ACTUALLY move schemas to fastmcp.json by [@jlowin](https://github.com/jlowin) in [#1597](https://github.com/PrefectHQ/fastmcp/pull/1597)\n* Fix and centralize CLI path resolution by [@jlowin](https://github.com/jlowin) in [#1590](https://github.com/PrefectHQ/fastmcp/pull/1590)\n* Remove client info modifications by [@jlowin](https://github.com/jlowin) in [#1620](https://github.com/PrefectHQ/fastmcp/pull/1620)\n* Fix $defs being discarded in input schema of transformed tool by [@pldesch-chift](https://github.com/pldesch-chift) in [#1578](https://github.com/PrefectHQ/fastmcp/pull/1578)\n* Fix enum elicitation to use inline schemas for MCP compatibility by [@jlowin](https://github.com/jlowin) in [#1632](https://github.com/PrefectHQ/fastmcp/pull/1632)\n* Reuse session for `StdioTransport` in `Client.new` by [@strawgate](https://github.com/strawgate) in [#1635](https://github.com/PrefectHQ/fastmcp/pull/1635)\n* Feat: Configurable LoggingMiddleware payload serialization by [@vl-kp](https://github.com/vl-kp) in [#1636](https://github.com/PrefectHQ/fastmcp/pull/1636)\n* Fix OAuth redirect URI validation for DCR compatibility by [@jlowin](https://github.com/jlowin) in [#1661](https://github.com/PrefectHQ/fastmcp/pull/1661)\n* Add default scope handling in OAuth proxy by [@romanusyk](https://github.com/romanusyk) in [#1667](https://github.com/PrefectHQ/fastmcp/pull/1667)\n* Fix OAuth token expiry handling by [@jlowin](https://github.com/jlowin) in [#1671](https://github.com/PrefectHQ/fastmcp/pull/1671)\n* Add resource_server_url parameter to OAuth proxy providers by [@jlowin](https://github.com/jlowin) in [#1682](https://github.com/PrefectHQ/fastmcp/pull/1682)\n### Breaking Changes 🛫\n* Enhance inspect command with structured output and format options by [@jlowin](https://github.com/jlowin) in [#1481](https://github.com/PrefectHQ/fastmcp/pull/1481)\n### Docs 📚\n* Update changelog by [@jlowin](https://github.com/jlowin) in [#1453](https://github.com/PrefectHQ/fastmcp/pull/1453)\n* Update banner by [@jlowin](https://github.com/jlowin) in [#1472](https://github.com/PrefectHQ/fastmcp/pull/1472)\n* Update logo files by [@jlowin](https://github.com/jlowin) in [#1473](https://github.com/PrefectHQ/fastmcp/pull/1473)\n* Update deployment docs by [@jlowin](https://github.com/jlowin) in [#1486](https://github.com/PrefectHQ/fastmcp/pull/1486)\n* Update FastMCP Cloud screenshot by [@jlowin](https://github.com/jlowin) in [#1487](https://github.com/PrefectHQ/fastmcp/pull/1487)\n* Update authentication note in docs by [@jlowin](https://github.com/jlowin) in [#1488](https://github.com/PrefectHQ/fastmcp/pull/1488)\n* chore: Update installation.mdx version snippet by [@thomas-te](https://github.com/thomas-te) in [#1496](https://github.com/PrefectHQ/fastmcp/pull/1496)\n* Update fastmcp cloud server requirements by [@jlowin](https://github.com/jlowin) in [#1497](https://github.com/PrefectHQ/fastmcp/pull/1497)\n* Fix oauth pyright type checking by [@strawgate](https://github.com/strawgate) in [#1498](https://github.com/PrefectHQ/fastmcp/pull/1498)\n* docs: Fix type annotation in return value documentation by [@MaikelVeen](https://github.com/MaikelVeen) in [#1499](https://github.com/PrefectHQ/fastmcp/pull/1499)\n* Fix PromptMessage usage in docs example by [@jlowin](https://github.com/jlowin) in [#1515](https://github.com/PrefectHQ/fastmcp/pull/1515)\n* Create CODE_OF_CONDUCT.md by [@jlowin](https://github.com/jlowin) in [#1523](https://github.com/PrefectHQ/fastmcp/pull/1523)\n* Fixed wrong import path in new docs page by [@KaliszS](https://github.com/KaliszS) in [#1538](https://github.com/PrefectHQ/fastmcp/pull/1538)\n* Document symmetric key JWT verification support by [@jlowin](https://github.com/jlowin) in [#1586](https://github.com/PrefectHQ/fastmcp/pull/1586)\n* Update fastmcp.json schema path by [@jlowin](https://github.com/jlowin) in [#1595](https://github.com/PrefectHQ/fastmcp/pull/1595)\n### Dependencies 📦\n* Bump actions/create-github-app-token from 1 to 2 by [@dependabot](https://github.com/dependabot)[bot] in [#1436](https://github.com/PrefectHQ/fastmcp/pull/1436)\n* Bump astral-sh/setup-uv from 4 to 6 by [@dependabot](https://github.com/dependabot)[bot] in [#1532](https://github.com/PrefectHQ/fastmcp/pull/1532)\n* Bump actions/checkout from 4 to 5 by [@dependabot](https://github.com/dependabot)[bot] in [#1533](https://github.com/PrefectHQ/fastmcp/pull/1533)\n### Other Changes 🦾\n* Add dedupe workflow by [@jlowin](https://github.com/jlowin) in [#1454](https://github.com/PrefectHQ/fastmcp/pull/1454)\n* Update AGENTS.md by [@jlowin](https://github.com/jlowin) in [#1471](https://github.com/PrefectHQ/fastmcp/pull/1471)\n* Give Marvin the power of the Internet by [@strawgate](https://github.com/strawgate) in [#1475](https://github.com/PrefectHQ/fastmcp/pull/1475)\n* Update `just` error message for static checks by [@jlowin](https://github.com/jlowin) in [#1483](https://github.com/PrefectHQ/fastmcp/pull/1483)\n* Remove labeler by [@jlowin](https://github.com/jlowin) in [#1509](https://github.com/PrefectHQ/fastmcp/pull/1509)\n* update aproto server to handle rich links by [@zzstoatzz](https://github.com/zzstoatzz) in [#1556](https://github.com/PrefectHQ/fastmcp/pull/1556)\n* fix: enable triage bot for fork PRs using pull_request_target by [@jlowin](https://github.com/jlowin) in [#1557](https://github.com/PrefectHQ/fastmcp/pull/1557)\n\n## New Contributors\n* [@thomas-te](https://github.com/thomas-te) made their first contribution in [#1496](https://github.com/PrefectHQ/fastmcp/pull/1496)\n* [@maybenotconnor](https://github.com/maybenotconnor) made their first contribution in [#1511](https://github.com/PrefectHQ/fastmcp/pull/1511)\n* [@MaikelVeen](https://github.com/MaikelVeen) made their first contribution in [#1499](https://github.com/PrefectHQ/fastmcp/pull/1499)\n* [@KaliszS](https://github.com/KaliszS) made their first contribution in [#1538](https://github.com/PrefectHQ/fastmcp/pull/1538)\n* [@isra17](https://github.com/isra17) made their first contribution in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)\n* [@marvin-context-protocol](https://github.com/marvin-context-protocol)[bot] made their first contribution in [#1616](https://github.com/PrefectHQ/fastmcp/pull/1616)\n* [@pldesch-chift](https://github.com/pldesch-chift) made their first contribution in [#1578](https://github.com/PrefectHQ/fastmcp/pull/1578)\n* [@vl-kp](https://github.com/vl-kp) made their first contribution in [#1636](https://github.com/PrefectHQ/fastmcp/pull/1636)\n* [@romanusyk](https://github.com/romanusyk) made their first contribution in [#1667](https://github.com/PrefectHQ/fastmcp/pull/1667)\n\n**Full Changelog**: [v2.11.3...v2.12.0](https://github.com/PrefectHQ/fastmcp/compare/v2.11.3...v2.12.0)\n\n</Update>\n\n<Update label=\"v2.11.3\" description=\"2025-08-11\">\n\n**[v2.11.3: API-tite for Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.3)**\n\nThis release includes significant enhancements to the experimental OpenAPI parser and fixes a significant bug that led schemas not to be included in input/output schemas if they were transitive dependencies (e.g. A → B → C implies A depends on C). For users naively transforming large OpenAPI specs into MCP servers, this may result in ballooning payload sizes and necessitate curation.\n\n## What's Changed\n### Enhancements 🔧\n* Improve redirect handling to address 307's by [@jlowin](https://github.com/jlowin) in [#1387](https://github.com/PrefectHQ/fastmcp/pull/1387)\n* Ensure resource + template names are properly prefixed when importing/mounting by [@jlowin](https://github.com/jlowin) in [#1423](https://github.com/PrefectHQ/fastmcp/pull/1423)\n* fixes #1398: Add JWT claims to AccessToken by [@panargirakis](https://github.com/panargirakis) in [#1399](https://github.com/PrefectHQ/fastmcp/pull/1399)\n* Enable Protected Resource Metadata to provide resource_name and resou… by [@yannj-fr](https://github.com/yannj-fr) in [#1371](https://github.com/PrefectHQ/fastmcp/pull/1371)\n* Pin mcp SDK under 2.0 to avoid breaking changes by [@jlowin](https://github.com/jlowin) in [#1428](https://github.com/PrefectHQ/fastmcp/pull/1428)\n* Clean up complexity from PR #1426 by [@jlowin](https://github.com/jlowin) in [#1435](https://github.com/PrefectHQ/fastmcp/pull/1435)\n* Optimize OpenAPI payload size by 46% by [@jlowin](https://github.com/jlowin) in [#1452](https://github.com/PrefectHQ/fastmcp/pull/1452)\n* Update static checks by [@jlowin](https://github.com/jlowin) in [#1448](https://github.com/PrefectHQ/fastmcp/pull/1448)\n### Fixes 🐞\n* Fix client-side logging bug #1394 by [@chi2liu](https://github.com/chi2liu) in [#1397](https://github.com/PrefectHQ/fastmcp/pull/1397)\n* fix: Fix httpx_client_factory type annotation to match MCP SDK (#1402) by [@chi2liu](https://github.com/chi2liu) in [#1405](https://github.com/PrefectHQ/fastmcp/pull/1405)\n* Fix OpenAPI allOf handling at requestBody top level (#1378) by [@chi2liu](https://github.com/chi2liu) in [#1425](https://github.com/PrefectHQ/fastmcp/pull/1425)\n* Fix OpenAPI transitive references and performance (#1372) by [@jlowin](https://github.com/jlowin) in [#1426](https://github.com/PrefectHQ/fastmcp/pull/1426)\n* fix(type): lifespan is partially unknown by [@ykun9](https://github.com/ykun9) in [#1389](https://github.com/PrefectHQ/fastmcp/pull/1389)\n* Ensure transformed tools generate structured content by [@jlowin](https://github.com/jlowin) in [#1443](https://github.com/PrefectHQ/fastmcp/pull/1443)\n### Docs 📚\n* docs(client/logging): reflect corrected default log level mapping by [@jlowin](https://github.com/jlowin) in [#1403](https://github.com/PrefectHQ/fastmcp/pull/1403)\n* Add documentation for get_access_token() dependency function by [@jlowin](https://github.com/jlowin) in [#1446](https://github.com/PrefectHQ/fastmcp/pull/1446)\n### Other Changes 🦾\n* Add comprehensive tests for utilities.components module by [@chi2liu](https://github.com/chi2liu) in [#1395](https://github.com/PrefectHQ/fastmcp/pull/1395)\n* Consolidate agent instructions into AGENTS.md by [@jlowin](https://github.com/jlowin) in [#1404](https://github.com/PrefectHQ/fastmcp/pull/1404)\n* Fix performance test threshold to prevent flaky failures by [@jlowin](https://github.com/jlowin) in [#1406](https://github.com/PrefectHQ/fastmcp/pull/1406)\n* Update agents.md; add github instructions by [@jlowin](https://github.com/jlowin) in [#1410](https://github.com/PrefectHQ/fastmcp/pull/1410)\n* Add Marvin assistant by [@jlowin](https://github.com/jlowin) in [#1412](https://github.com/PrefectHQ/fastmcp/pull/1412)\n* Marvin: fix deprecated variable names by [@jlowin](https://github.com/jlowin) in [#1417](https://github.com/PrefectHQ/fastmcp/pull/1417)\n* Simplify action setup and add github tools for Marvin by [@jlowin](https://github.com/jlowin) in [#1419](https://github.com/PrefectHQ/fastmcp/pull/1419)\n* Update marvin workflow name by [@jlowin](https://github.com/jlowin) in [#1421](https://github.com/PrefectHQ/fastmcp/pull/1421)\n* Improve GitHub templates by [@jlowin](https://github.com/jlowin) in [#1422](https://github.com/PrefectHQ/fastmcp/pull/1422)\n\n## New Contributors\n* [@panargirakis](https://github.com/panargirakis) made their first contribution in [#1399](https://github.com/PrefectHQ/fastmcp/pull/1399)\n* [@ykun9](https://github.com/ykun9) made their first contribution in [#1389](https://github.com/PrefectHQ/fastmcp/pull/1389)\n* [@yannj-fr](https://github.com/yannj-fr) made their first contribution in [#1371](https://github.com/PrefectHQ/fastmcp/pull/1371)\n\n**Full Changelog**: [v2.11.2...v2.11.3](https://github.com/PrefectHQ/fastmcp/compare/v2.11.2...v2.11.3)\n\n</Update>\n\n<Update label=\"v2.11.2\" description=\"2025-08-06\">\n\n## [v2.11.2: Satis-factory](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.2)\n\n## What's Changed\n### Enhancements 🔧\n* Support factory functions in fastmcp run by [@jlowin](https://github.com/jlowin) in [#1384](https://github.com/PrefectHQ/fastmcp/pull/1384)\n* Add async support to client_factory in FastMCPProxy  (#1286) by [@bianning](https://github.com/bianning) in [#1375](https://github.com/PrefectHQ/fastmcp/pull/1375)\n### Fixes 🐞\n* Fix server_version field in inspect manifest by [@jlowin](https://github.com/jlowin) in [#1383](https://github.com/PrefectHQ/fastmcp/pull/1383)\n* Fix Settings field with both default and default_factory by [@jlowin](https://github.com/jlowin) in [#1380](https://github.com/PrefectHQ/fastmcp/pull/1380)\n### Other Changes 🦾\n* Remove unused arg by [@jlowin](https://github.com/jlowin) in [#1382](https://github.com/PrefectHQ/fastmcp/pull/1382)\n* Add remote auth provider tests by [@jlowin](https://github.com/jlowin) in [#1351](https://github.com/PrefectHQ/fastmcp/pull/1351)\n\n## New Contributors\n* [@bianning](https://github.com/bianning) made their first contribution in [#1375](https://github.com/PrefectHQ/fastmcp/pull/1375)\n\n**Full Changelog**: [v2.11.1...v2.11.2](https://github.com/PrefectHQ/fastmcp/compare/v2.11.1...v2.11.2)\n\n</Update>\n\n<Update label=\"v2.11.1\" description=\"2025-08-04\">\n\n## [v2.11.1: You're Better Auth Now](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.1)\n\n## What's Changed\n### New Features 🎉\n* Introduce `RemoteAuthProvider` for cleaner external identity provider integration, update docs by [@jlowin](https://github.com/jlowin) in [#1346](https://github.com/PrefectHQ/fastmcp/pull/1346)\n### Enhancements 🔧\n* perf: optimize string operations in OpenAPI parameter processing by [@chi2liu](https://github.com/chi2liu) in [#1342](https://github.com/PrefectHQ/fastmcp/pull/1342)\n### Fixes 🐞\n* Fix method-bound FunctionTool schemas by [@strawgate](https://github.com/strawgate) in [#1360](https://github.com/PrefectHQ/fastmcp/pull/1360)\n* Manually set `_key` after `model_copy()` to enable prefixing Transformed Tools by [@strawgate](https://github.com/strawgate) in [#1357](https://github.com/PrefectHQ/fastmcp/pull/1357)\n### Docs 📚\n* Docs updates by [@jlowin](https://github.com/jlowin) in [#1336](https://github.com/PrefectHQ/fastmcp/pull/1336)\n* Add 2.11 to changelog by [@jlowin](https://github.com/jlowin) in [#1337](https://github.com/PrefectHQ/fastmcp/pull/1337)\n* Update AuthKit vocab by [@jlowin](https://github.com/jlowin) in [#1338](https://github.com/PrefectHQ/fastmcp/pull/1338)\n* Fix typo in decorating-methods.mdx by [@Ozzuke](https://github.com/Ozzuke) in [#1344](https://github.com/PrefectHQ/fastmcp/pull/1344)\n\n## New Contributors\n* [@Ozzuke](https://github.com/Ozzuke) made their first contribution in [#1344](https://github.com/PrefectHQ/fastmcp/pull/1344)\n\n**Full Changelog**: [v2.11.0...v2.11.1](https://github.com/PrefectHQ/fastmcp/compare/v2.11.0...v2.11.1)\n\n</Update>\n\n<Update label=\"v2.11.0\" description=\"2025-08-01\">\n\n## [v2.11.0: Auth to a Good Start](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.0)\n\nFastMCP 2.11 doubles down on what developers need most: speed and simplicity. This massive release delivers significant performance improvements and a dramatically better developer experience.\n\n🔐 **Enterprise-Ready Authentication** brings comprehensive OAuth 2.1 support with WorkOS's AuthKit integration. The new AuthProvider interface leverages MCP's support for separate resource and authorization servers, handling API keys and remote authentication with Dynamic Client Registration. AuthKit integration means you can plug into existing enterprise identity systems without rebuilding your auth stack, setting the stage for plug-and-play auth that doesn't require users to become security experts overnight.\n\n⚡ The **Experimental OpenAPI Parser** delivers dramatic performance improvements through single-pass schema processing and optimized memory usage. OpenAPI integrations are now significantly faster, with cleaner, more maintainable code. _(Note: the experimental parser is disabled by default, set `FASTMCPEXPERIMENTALENABLENEWOPENAPIPARSER=1` to enable it. A message will be shown to all users on the legacy parser encouraging them to try the new one before it becomes the default.)_\n\n🧠 **Context State Management** finally gives you persistent state across tool calls with a simple dict interface, while enhanced meta support lets you expose rich component metadata to clients. Combined with improved type annotations, string-based argument descriptions, and UV transport support, this release makes FastMCP feel more intuitive than ever.\n\nThis release represents a TON of community contributions and sets the foundation for even more ambitious features ahead.\n\n## What's Changed\n### New Features 🎉\n* Introduce experimental OpenAPI parser with improved performance and maintainability by [@jlowin](https://github.com/jlowin) in [#1209](https://github.com/PrefectHQ/fastmcp/pull/1209)\n* Add state dict to Context (#1118) by [@mukulmurthy](https://github.com/mukulmurthy) in [#1160](https://github.com/PrefectHQ/fastmcp/pull/1160)\n* Expose FastMCP tags to clients via component `meta` dict by [@jlowin](https://github.com/jlowin) in [#1281](https://github.com/PrefectHQ/fastmcp/pull/1281)\n* Add _fastmcp meta namespace by [@jlowin](https://github.com/jlowin) in [#1290](https://github.com/PrefectHQ/fastmcp/pull/1290)\n* Add TokenVerifier protocol support alongside existing OAuthProvider authentication by [@jlowin](https://github.com/jlowin) in [#1297](https://github.com/PrefectHQ/fastmcp/pull/1297)\n* Add comprehensive OAuth 2.1 authentication system with WorkOS integration by [@jlowin](https://github.com/jlowin) in [#1327](https://github.com/PrefectHQ/fastmcp/pull/1327)\n### Enhancements 🔧\n* [🐶] Transform MCP Server Tools by [@strawgate](https://github.com/strawgate) in [#1132](https://github.com/PrefectHQ/fastmcp/pull/1132)\n* Add --python, --project, and --with-requirements options to CLI commands by [@jlowin](https://github.com/jlowin) in [#1190](https://github.com/PrefectHQ/fastmcp/pull/1190)\n* Support `fastmcp run mcp.json` by [@strawgate](https://github.com/strawgate) in [#1138](https://github.com/PrefectHQ/fastmcp/pull/1138)\n* Support from __future__ import annotations by [@jlowin](https://github.com/jlowin) in [#1199](https://github.com/PrefectHQ/fastmcp/pull/1199)\n* Optimize OpenAPI parser performance with single-pass schema processing by [@jlowin](https://github.com/jlowin) in [#1214](https://github.com/PrefectHQ/fastmcp/pull/1214)\n* Log tool name on transform validation error by [@strawgate](https://github.com/strawgate) in [#1238](https://github.com/PrefectHQ/fastmcp/pull/1238)\n* Refactor `get_http_request` and `context.session_id` by [@hopeful0](https://github.com/hopeful0) in [#1242](https://github.com/PrefectHQ/fastmcp/pull/1242)\n* Support creating tool argument descriptions from string annotations by [@jlowin](https://github.com/jlowin) in [#1255](https://github.com/PrefectHQ/fastmcp/pull/1255)\n* feat: Add Annotations support for resources and resource templates by [@chughtapan](https://github.com/chughtapan) in [#1260](https://github.com/PrefectHQ/fastmcp/pull/1260)\n* Add UV Transport by [@strawgate](https://github.com/strawgate) in [#1270](https://github.com/PrefectHQ/fastmcp/pull/1270)\n* Improve OpenAPI-to-JSONSchema conversion utilities by [@jlowin](https://github.com/jlowin) in [#1283](https://github.com/PrefectHQ/fastmcp/pull/1283)\n* Ensure proxy components forward meta dicts by [@jlowin](https://github.com/jlowin) in [#1282](https://github.com/PrefectHQ/fastmcp/pull/1282)\n* fix: server argument passing in CLI run command by [@chughtapan](https://github.com/chughtapan) in [#1293](https://github.com/PrefectHQ/fastmcp/pull/1293)\n* Add meta support to tool transformation utilities by [@jlowin](https://github.com/jlowin) in [#1295](https://github.com/PrefectHQ/fastmcp/pull/1295)\n* feat: Allow Resource Metadata URL as field in OAuthProvider by [@dacamposol](https://github.com/dacamposol) in [#1287](https://github.com/PrefectHQ/fastmcp/pull/1287)\n* Use a simple overwrite instead of a merge for meta by [@jlowin](https://github.com/jlowin) in [#1296](https://github.com/PrefectHQ/fastmcp/pull/1296)\n* Remove unused TimedCache by [@strawgate](https://github.com/strawgate) in [#1303](https://github.com/PrefectHQ/fastmcp/pull/1303)\n* refactor: standardize logging usage across OpenAPI utilities by [@chi2liu](https://github.com/chi2liu) in [#1322](https://github.com/PrefectHQ/fastmcp/pull/1322)\n* perf: optimize OpenAPI parsing by reducing dict copy operations by [@chi2liu](https://github.com/chi2liu) in [#1321](https://github.com/PrefectHQ/fastmcp/pull/1321)\n* Structured client-side logging by [@cjermain](https://github.com/cjermain) in [#1326](https://github.com/PrefectHQ/fastmcp/pull/1326)\n### Fixes 🐞\n* fix: preserve def reference when referenced in allOf / oneOf / anyOf by [@algirdasci](https://github.com/algirdasci) in [#1208](https://github.com/PrefectHQ/fastmcp/pull/1208)\n* fix: add type hint to custom_route decorator by [@zzstoatzz](https://github.com/zzstoatzz) in [#1210](https://github.com/PrefectHQ/fastmcp/pull/1210)\n* chore: typo by [@richardkmichael](https://github.com/richardkmichael) in [#1216](https://github.com/PrefectHQ/fastmcp/pull/1216)\n* fix: handle non-string $ref values in experimental OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#1217](https://github.com/PrefectHQ/fastmcp/pull/1217)\n* Skip repeated type conversion and validation in proxy client elicitation handler by [@chughtapan](https://github.com/chughtapan) in [#1222](https://github.com/PrefectHQ/fastmcp/pull/1222)\n* Ensure default fields are not marked nullable by [@jlowin](https://github.com/jlowin) in [#1224](https://github.com/PrefectHQ/fastmcp/pull/1224)\n* Fix stateful proxy client mixing in multi-proxies sessions by [@hopeful0](https://github.com/hopeful0) in [#1245](https://github.com/PrefectHQ/fastmcp/pull/1245)\n* Fix invalid async context manager usage in proxy documentation by [@zzstoatzz](https://github.com/zzstoatzz) in [#1246](https://github.com/PrefectHQ/fastmcp/pull/1246)\n* fix: experimental FastMCPOpenAPI server lost headers in request when __init__(client with headers) by [@itaru2622](https://github.com/itaru2622) in [#1254](https://github.com/PrefectHQ/fastmcp/pull/1254)\n* Fix typing, add tests for tool call middleware by [@jlowin](https://github.com/jlowin) in [#1269](https://github.com/PrefectHQ/fastmcp/pull/1269)\n* Fix: prune hidden parameter defs by [@muhammadkhalid-03](https://github.com/muhammadkhalid-03) in [#1257](https://github.com/PrefectHQ/fastmcp/pull/1257)\n* Fix nullable field handling in OpenAPI to JSON Schema conversion by [@jlowin](https://github.com/jlowin) in [#1279](https://github.com/PrefectHQ/fastmcp/pull/1279)\n* Ensure fastmcp run supports v1 servers by [@jlowin](https://github.com/jlowin) in [#1332](https://github.com/PrefectHQ/fastmcp/pull/1332)\n### Breaking Changes 🛫\n* Change server flag to --name by [@jlowin](https://github.com/jlowin) in [#1248](https://github.com/PrefectHQ/fastmcp/pull/1248)\n### Docs 📚\n* Remove unused import from FastAPI integration documentation by [@mariotaddeucci](https://github.com/mariotaddeucci) in [#1194](https://github.com/PrefectHQ/fastmcp/pull/1194)\n* Update fastapi docs by [@jlowin](https://github.com/jlowin) in [#1198](https://github.com/PrefectHQ/fastmcp/pull/1198)\n* Add docs for context state management by [@jlowin](https://github.com/jlowin) in [#1227](https://github.com/PrefectHQ/fastmcp/pull/1227)\n* Permit.io integration docs by [@orweis](https://github.com/orweis) in [#1226](https://github.com/PrefectHQ/fastmcp/pull/1226)\n* Update docs to reflect sync tools by [@jlowin](https://github.com/jlowin) in [#1234](https://github.com/PrefectHQ/fastmcp/pull/1234)\n* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1235](https://github.com/PrefectHQ/fastmcp/pull/1235)\n* Update SDK docs by [@jlowin](https://github.com/jlowin) in [#1236](https://github.com/PrefectHQ/fastmcp/pull/1236)\n* Update --name flag documentation for Cursor/Claude by [@adam-conway](https://github.com/adam-conway) in [#1239](https://github.com/PrefectHQ/fastmcp/pull/1239)\n* Add annotations docs by [@jlowin](https://github.com/jlowin) in [#1268](https://github.com/PrefectHQ/fastmcp/pull/1268)\n* Update openapi/fastapi URLs README.md by [@jbn](https://github.com/jbn) in [#1278](https://github.com/PrefectHQ/fastmcp/pull/1278)\n* Add 2.11 version badge for state management by [@jlowin](https://github.com/jlowin) in [#1289](https://github.com/PrefectHQ/fastmcp/pull/1289)\n* Add meta parameter support to tools, resources, templates, and prompts decorators by [@jlowin](https://github.com/jlowin) in [#1294](https://github.com/PrefectHQ/fastmcp/pull/1294)\n* docs: update get_state and set_state references by [@Maxi91f](https://github.com/Maxi91f) in [#1306](https://github.com/PrefectHQ/fastmcp/pull/1306)\n* Add unit tests and docs for denying tool calls with middleware by [@jlowin](https://github.com/jlowin) in [#1333](https://github.com/PrefectHQ/fastmcp/pull/1333)\n* Remove reference to stacked decorators by [@jlowin](https://github.com/jlowin) in [#1334](https://github.com/PrefectHQ/fastmcp/pull/1334)\n* Eunomia authorization server can run embedded within the MCP server by [@tommitt](https://github.com/tommitt) in [#1317](https://github.com/PrefectHQ/fastmcp/pull/1317)\n### Other Changes 🦾\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#1230](https://github.com/PrefectHQ/fastmcp/pull/1230)\n* Logcapture addition to test_server file by [@Sourav-Tripathy](https://github.com/Sourav-Tripathy) in [#1229](https://github.com/PrefectHQ/fastmcp/pull/1229)\n* Add tests for headers with both legacy and experimental openapi parser by [@jlowin](https://github.com/jlowin) in [#1259](https://github.com/PrefectHQ/fastmcp/pull/1259)\n* Small clean-up from MCP Tool Transform PR by [@strawgate](https://github.com/strawgate) in [#1267](https://github.com/PrefectHQ/fastmcp/pull/1267)\n* Add test for proxy tags visibility by [@jlowin](https://github.com/jlowin) in [#1302](https://github.com/PrefectHQ/fastmcp/pull/1302)\n* Add unit test for sampling with image messages by [@jlowin](https://github.com/jlowin) in [#1329](https://github.com/PrefectHQ/fastmcp/pull/1329)\n* Remove redundant resource_metadata_url assignment by [@jlowin](https://github.com/jlowin) in [#1328](https://github.com/PrefectHQ/fastmcp/pull/1328)\n* Update bug.yml by [@jlowin](https://github.com/jlowin) in [#1331](https://github.com/PrefectHQ/fastmcp/pull/1331)\n* Ensure validation errors are raised when masked by [@jlowin](https://github.com/jlowin) in [#1330](https://github.com/PrefectHQ/fastmcp/pull/1330)\n\n## New Contributors\n* [@mariotaddeucci](https://github.com/mariotaddeucci) made their first contribution in [#1194](https://github.com/PrefectHQ/fastmcp/pull/1194)\n* [@algirdasci](https://github.com/algirdasci) made their first contribution in [#1208](https://github.com/PrefectHQ/fastmcp/pull/1208)\n* [@chughtapan](https://github.com/chughtapan) made their first contribution in [#1222](https://github.com/PrefectHQ/fastmcp/pull/1222)\n* [@mukulmurthy](https://github.com/mukulmurthy) made their first contribution in [#1160](https://github.com/PrefectHQ/fastmcp/pull/1160)\n* [@orweis](https://github.com/orweis) made their first contribution in [#1226](https://github.com/PrefectHQ/fastmcp/pull/1226)\n* [@Sourav-Tripathy](https://github.com/Sourav-Tripathy) made their first contribution in [#1229](https://github.com/PrefectHQ/fastmcp/pull/1229)\n* [@adam-conway](https://github.com/adam-conway) made their first contribution in [#1239](https://github.com/PrefectHQ/fastmcp/pull/1239)\n* [@muhammadkhalid-03](https://github.com/muhammadkhalid-03) made their first contribution in [#1257](https://github.com/PrefectHQ/fastmcp/pull/1257)\n* [@jbn](https://github.com/jbn) made their first contribution in [#1278](https://github.com/PrefectHQ/fastmcp/pull/1278)\n* [@dacamposol](https://github.com/dacamposol) made their first contribution in [#1287](https://github.com/PrefectHQ/fastmcp/pull/1287)\n* [@chi2liu](https://github.com/chi2liu) made their first contribution in [#1322](https://github.com/PrefectHQ/fastmcp/pull/1322)\n* [@cjermain](https://github.com/cjermain) made their first contribution in [#1326](https://github.com/PrefectHQ/fastmcp/pull/1326)\n\n**Full Changelog**: [v2.10.6...v2.11.0](https://github.com/PrefectHQ/fastmcp/compare/v2.10.6...v2.11.0)\n\n</Update>\n\n<Update label=\"v2.10.6\" description=\"2025-07-19\">\n\n## [v2.10.6: Hymn for the Weekend](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.6)\n\nA special Saturday release with many fixes.\n\n## What's Changed\n### Enhancements 🔧\n* Resolve #1139 -- Implement include_context argument in Context.sample by [@codingjoe](https://github.com/codingjoe) in [#1141](https://github.com/PrefectHQ/fastmcp/pull/1141)\n* feat(settings): add log level normalization by [@ka2048](https://github.com/ka2048) in [#1171](https://github.com/PrefectHQ/fastmcp/pull/1171)\n* add server name to mounted server warnings by [@artificial-aidan](https://github.com/artificial-aidan) in [#1147](https://github.com/PrefectHQ/fastmcp/pull/1147)\n* Add StatefulProxyClient by [@hopeful0](https://github.com/hopeful0) in [#1109](https://github.com/PrefectHQ/fastmcp/pull/1109)\n### Fixes 🐞\n* Fix OpenAPI empty parameters by [@FabrizioSandri](https://github.com/FabrizioSandri) in [#1128](https://github.com/PrefectHQ/fastmcp/pull/1128)\n* Fix title field preservation in tool transformations by [@jlowin](https://github.com/jlowin) in [#1131](https://github.com/PrefectHQ/fastmcp/pull/1131)\n* Fix optional parameter validation in OpenAPI integration by [@jlowin](https://github.com/jlowin) in [#1135](https://github.com/PrefectHQ/fastmcp/pull/1135)\n* Do not silently exclude the \"context\" key from JSON body by [@melkamar](https://github.com/melkamar) in [#1153](https://github.com/PrefectHQ/fastmcp/pull/1153)\n* Fix tool output schema generation to respect Pydantic serialization aliases by [@zzstoatzz](https://github.com/zzstoatzz) in [#1148](https://github.com/PrefectHQ/fastmcp/pull/1148)\n* fix: _replace_ref_with_defs; ensure ref_path is string by [@itaru2622](https://github.com/itaru2622) in [#1164](https://github.com/PrefectHQ/fastmcp/pull/1164)\n* Fix nesting when making OpenAPI arrays and objects optional by [@melkamar](https://github.com/melkamar) in [#1178](https://github.com/PrefectHQ/fastmcp/pull/1178)\n* Fix `mcp-json` output format to include server name by [@jlowin](https://github.com/jlowin) in [#1185](https://github.com/PrefectHQ/fastmcp/pull/1185)\n* Only configure logging one time by [@jlowin](https://github.com/jlowin) in [#1187](https://github.com/PrefectHQ/fastmcp/pull/1187)\n### Docs 📚\n* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1127](https://github.com/PrefectHQ/fastmcp/pull/1127)\n* Eunomia Authorization with native FastMCP's Middleware by [@tommitt](https://github.com/tommitt) in [#1144](https://github.com/PrefectHQ/fastmcp/pull/1144)\n* update api ref for new `mdxify` version by [@zzstoatzz](https://github.com/zzstoatzz) in [#1182](https://github.com/PrefectHQ/fastmcp/pull/1182)\n### Other Changes 🦾\n* Expand empty parameter filtering and add comprehensive tests by [@jlowin](https://github.com/jlowin) in [#1129](https://github.com/PrefectHQ/fastmcp/pull/1129)\n* Add no-commit-to-branch hook by [@zzstoatzz](https://github.com/zzstoatzz) in [#1149](https://github.com/PrefectHQ/fastmcp/pull/1149)\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#1165](https://github.com/PrefectHQ/fastmcp/pull/1165)\n* skip on rate limit by [@zzstoatzz](https://github.com/zzstoatzz) in [#1183](https://github.com/PrefectHQ/fastmcp/pull/1183)\n* Remove deprecated proxy creation by [@jlowin](https://github.com/jlowin) in [#1186](https://github.com/PrefectHQ/fastmcp/pull/1186)\n* Separate integration tests from unit tests in CI by [@jlowin](https://github.com/jlowin) in [#1188](https://github.com/PrefectHQ/fastmcp/pull/1188)\n\n## New Contributors\n* [@FabrizioSandri](https://github.com/FabrizioSandri) made their first contribution in [#1128](https://github.com/PrefectHQ/fastmcp/pull/1128)\n* [@melkamar](https://github.com/melkamar) made their first contribution in [#1153](https://github.com/PrefectHQ/fastmcp/pull/1153)\n* [@codingjoe](https://github.com/codingjoe) made their first contribution in [#1141](https://github.com/PrefectHQ/fastmcp/pull/1141)\n* [@itaru2622](https://github.com/itaru2622) made their first contribution in [#1164](https://github.com/PrefectHQ/fastmcp/pull/1164)\n* [@ka2048](https://github.com/ka2048) made their first contribution in [#1171](https://github.com/PrefectHQ/fastmcp/pull/1171)\n* [@artificial-aidan](https://github.com/artificial-aidan) made their first contribution in [#1147](https://github.com/PrefectHQ/fastmcp/pull/1147)\n\n**Full Changelog**: [v2.10.5...v2.10.6](https://github.com/PrefectHQ/fastmcp/compare/v2.10.5...v2.10.6)\n\n</Update>\n\n<Update label=\"v2.10.5\" description=\"2025-07-11\">\n\n## [v2.10.5: Middle Management](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.5)\n\nA maintenance release focused on OpenAPI refinements and middleware fixes, plus console improvements.\n\n## What's Changed\n### Enhancements 🔧\n* Fix Claude Code CLI detection for npm global installations by [@jlowin](https://github.com/jlowin) in [#1106](https://github.com/PrefectHQ/fastmcp/pull/1106)\n* Fix OpenAPI parameter name collisions with location suffixing by [@jlowin](https://github.com/jlowin) in [#1107](https://github.com/PrefectHQ/fastmcp/pull/1107)\n* Add mirrored component support for proxy servers by [@jlowin](https://github.com/jlowin) in [#1105](https://github.com/PrefectHQ/fastmcp/pull/1105)\n### Fixes 🐞\n* Fix OpenAPI deepObject style parameter encoding by [@jlowin](https://github.com/jlowin) in [#1122](https://github.com/PrefectHQ/fastmcp/pull/1122)\n* xfail when github token is not set ('' or None) by [@jlowin](https://github.com/jlowin) in [#1123](https://github.com/PrefectHQ/fastmcp/pull/1123)\n* fix: replace oneOf with anyOf in OpenAPI output schemas by [@MagnusS0](https://github.com/MagnusS0) in [#1119](https://github.com/PrefectHQ/fastmcp/pull/1119)\n* Fix middleware list result types by [@jlowin](https://github.com/jlowin) in [#1125](https://github.com/PrefectHQ/fastmcp/pull/1125)\n* Improve console width for logo by [@jlowin](https://github.com/jlowin) in [#1126](https://github.com/PrefectHQ/fastmcp/pull/1126)\n### Docs 📚\n* Improve transport + integration docs by [@jlowin](https://github.com/jlowin) in [#1103](https://github.com/PrefectHQ/fastmcp/pull/1103)\n* Update proxy.mdx by [@coldfire-x](https://github.com/coldfire-x) in [#1108](https://github.com/PrefectHQ/fastmcp/pull/1108)\n### Other Changes 🦾\n* Update github remote server tests with secret by [@jlowin](https://github.com/jlowin) in [#1112](https://github.com/PrefectHQ/fastmcp/pull/1112)\n\n## New Contributors\n* [@coldfire-x](https://github.com/coldfire-x) made their first contribution in [#1108](https://github.com/PrefectHQ/fastmcp/pull/1108)\n* [@MagnusS0](https://github.com/MagnusS0) made their first contribution in [#1119](https://github.com/PrefectHQ/fastmcp/pull/1119)\n\n**Full Changelog**: [v2.10.4...v2.10.5](https://github.com/PrefectHQ/fastmcp/compare/v2.10.4...v2.10.5)\n\n</Update>\n\n<Update label=\"v2.10.4\" description=\"2025-07-09\">\n\n## [v2.10.4: Transport-ation](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.4)\n\nA quick fix to ensure the CLI accepts \"streamable-http\" as a valid transport option.\n\n## What's Changed\n### Fixes 🐞\n* Ensure the CLI accepts \"streamable-http\" as a valid transport by [@jlowin](https://github.com/jlowin) in [#1099](https://github.com/PrefectHQ/fastmcp/pull/1099)\n\n**Full Changelog**: [v2.10.3...v2.10.4](https://github.com/PrefectHQ/fastmcp/compare/v2.10.3...v2.10.4)\n\n</Update>\n\n<Update label=\"v2.10.3\" description=\"2025-07-09\">\n\n## [v2.10.3: CLI Me a River](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.3)\n\nA major CLI overhaul featuring a complete refactor from typer to cyclopts, new IDE integrations, and comprehensive OpenAPI improvements.\n\n## What's Changed\n### New Features 🎉\n* Refactor CLI from typer to cyclopts and add comprehensive tests by [@jlowin](https://github.com/jlowin) in [#1062](https://github.com/PrefectHQ/fastmcp/pull/1062)\n* Add output schema support for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#1073](https://github.com/PrefectHQ/fastmcp/pull/1073)\n### Enhancements 🔧\n* Add Cursor support via CLI integration by [@jlowin](https://github.com/jlowin) in [#1052](https://github.com/PrefectHQ/fastmcp/pull/1052)\n* Add Claude Code install integration by [@jlowin](https://github.com/jlowin) in [#1053](https://github.com/PrefectHQ/fastmcp/pull/1053)\n* Generate MCP JSON config output from CLI as new `fastmcp install` command by [@jlowin](https://github.com/jlowin) in [#1056](https://github.com/PrefectHQ/fastmcp/pull/1056)\n* Use isawaitable instead of iscoroutine by [@jlowin](https://github.com/jlowin) in [#1059](https://github.com/PrefectHQ/fastmcp/pull/1059)\n* feat: Add `--path` Option to CLI for HTTP/SSE Route by [@davidbk-legit](https://github.com/davidbk-legit) in [#1087](https://github.com/PrefectHQ/fastmcp/pull/1087)\n* Fix concurrent proxy client operations with session isolation by [@jlowin](https://github.com/jlowin) in [#1083](https://github.com/PrefectHQ/fastmcp/pull/1083)\n### Fixes 🐞\n* Refactor Client context management to avoid concurrency issue by [@hopeful0](https://github.com/hopeful0) in [#1054](https://github.com/PrefectHQ/fastmcp/pull/1054)\n* Keep json schema $defs on transform by [@strawgate](https://github.com/strawgate) in [#1066](https://github.com/PrefectHQ/fastmcp/pull/1066)\n* Ensure fastmcp version copy is plaintext by [@jlowin](https://github.com/jlowin) in [#1071](https://github.com/PrefectHQ/fastmcp/pull/1071)\n* Fix single-element list unwrapping in tool content by [@jlowin](https://github.com/jlowin) in [#1074](https://github.com/PrefectHQ/fastmcp/pull/1074)\n* Fix max recursion error when pruning OpenAPI definitions by [@dimitribarbot](https://github.com/dimitribarbot) in [#1092](https://github.com/PrefectHQ/fastmcp/pull/1092)\n* Fix OpenAPI tool name registration when modified by mcp_component_fn by [@jlowin](https://github.com/jlowin) in [#1096](https://github.com/PrefectHQ/fastmcp/pull/1096)\n### Docs 📚\n* Docs: add example of more concise way to use bearer auth by [@neilconway](https://github.com/neilconway) in [#1055](https://github.com/PrefectHQ/fastmcp/pull/1055)\n* Update favicon by [@jlowin](https://github.com/jlowin) in [#1058](https://github.com/PrefectHQ/fastmcp/pull/1058)\n* Update environment note by [@jlowin](https://github.com/jlowin) in [#1075](https://github.com/PrefectHQ/fastmcp/pull/1075)\n* Add fastmcp version --copy documentation by [@jlowin](https://github.com/jlowin) in [#1076](https://github.com/PrefectHQ/fastmcp/pull/1076)\n### Other Changes 🦾\n* Remove asserts and add documentation following #1054 by [@jlowin](https://github.com/jlowin) in [#1057](https://github.com/PrefectHQ/fastmcp/pull/1057)\n* Add --copy flag for fastmcp version by [@jlowin](https://github.com/jlowin) in [#1063](https://github.com/PrefectHQ/fastmcp/pull/1063)\n* Fix docstring format for fastmcp.client.Client by [@neilconway](https://github.com/neilconway) in [#1094](https://github.com/PrefectHQ/fastmcp/pull/1094)\n\n## New Contributors\n* [@neilconway](https://github.com/neilconway) made their first contribution in [#1055](https://github.com/PrefectHQ/fastmcp/pull/1055)\n* [@davidbk-legit](https://github.com/davidbk-legit) made their first contribution in [#1087](https://github.com/PrefectHQ/fastmcp/pull/1087)\n* [@dimitribarbot](https://github.com/dimitribarbot) made their first contribution in [#1092](https://github.com/PrefectHQ/fastmcp/pull/1092)\n\n**Full Changelog**: [v2.10.2...v2.10.3](https://github.com/PrefectHQ/fastmcp/compare/v2.10.2...v2.10.3)\n\n</Update>\n\n<Update label=\"v2.10.2\" description=\"2025-07-05\">\n\n## [v2.10.2: Forward March](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.2)\n\nThe headline feature of this release is the ability to \"forward\" advanced MCP interactions like logging, progress, and elicitation through proxy servers. If the remote server requests an elicitation, the proxy client will pass that request to the new, \"ultimate\" client.\n\n## What's Changed\n### New Features 🎉\n* Proxy support advanced MCP features by [@hopeful0](https://github.com/hopeful0) in [#1022](https://github.com/PrefectHQ/fastmcp/pull/1022)\n### Enhancements 🔧\n* Re-add splash screen by [@jlowin](https://github.com/jlowin) in [#1027](https://github.com/PrefectHQ/fastmcp/pull/1027)\n* Reduce banner padding by [@jlowin](https://github.com/jlowin) in [#1030](https://github.com/PrefectHQ/fastmcp/pull/1030)\n* Allow per-server timeouts in MCPConfig by [@cegersdoerfer](https://github.com/cegersdoerfer) in [#1031](https://github.com/PrefectHQ/fastmcp/pull/1031)\n* Support 'scp' claim for OAuth scopes in BearerAuthProvider by [@jlowin](https://github.com/jlowin) in [#1033](https://github.com/PrefectHQ/fastmcp/pull/1033)\n* Add path expansion to image/audio/file by [@jlowin](https://github.com/jlowin) in [#1038](https://github.com/PrefectHQ/fastmcp/pull/1038)\n* Ensure multi-client configurations use new ProxyClient by [@jlowin](https://github.com/jlowin) in [#1045](https://github.com/PrefectHQ/fastmcp/pull/1045)\n### Fixes 🐞\n* Expose stateless_http kwarg for mcp.run() by [@jlowin](https://github.com/jlowin) in [#1018](https://github.com/PrefectHQ/fastmcp/pull/1018)\n* Avoid propagating logs by [@jlowin](https://github.com/jlowin) in [#1042](https://github.com/PrefectHQ/fastmcp/pull/1042)\n### Docs 📚\n* Clean up docs by [@jlowin](https://github.com/jlowin) in [#1028](https://github.com/PrefectHQ/fastmcp/pull/1028)\n* Docs: clarify server URL paths for ChatGPT integration by [@thap2331](https://github.com/thap2331) in [#1017](https://github.com/PrefectHQ/fastmcp/pull/1017)\n### Other Changes 🦾\n* Split giant openapi test file into smaller files by [@jlowin](https://github.com/jlowin) in [#1034](https://github.com/PrefectHQ/fastmcp/pull/1034)\n* Add comprehensive OpenAPI 3.0 vs 3.1 compatibility tests by [@jlowin](https://github.com/jlowin) in [#1035](https://github.com/PrefectHQ/fastmcp/pull/1035)\n* Update banner and use console.log by [@jlowin](https://github.com/jlowin) in [#1041](https://github.com/PrefectHQ/fastmcp/pull/1041)\n\n## New Contributors\n* [@cegersdoerfer](https://github.com/cegersdoerfer) made their first contribution in [#1031](https://github.com/PrefectHQ/fastmcp/pull/1031)\n* [@hopeful0](https://github.com/hopeful0) made their first contribution in [#1022](https://github.com/PrefectHQ/fastmcp/pull/1022)\n* [@thap2331](https://github.com/thap2331) made their first contribution in [#1017](https://github.com/PrefectHQ/fastmcp/pull/1017)\n\n**Full Changelog**: [v2.10.1...v2.10.2](https://github.com/PrefectHQ/fastmcp/compare/v2.10.1...v2.10.2)\n\n</Update>\n\n<Update label=\"v2.10.1\" description=\"2025-07-02\">\n\n## [v2.10.1: Revert to Sender](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.1)\n\nA quick patch to revert the CLI banner that was added in v2.10.0.\n\n## What's Changed\n### Docs 📚\n* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1009](https://github.com/PrefectHQ/fastmcp/pull/1009)\n* Revert \"Add CLI banner\" by [@jlowin](https://github.com/jlowin) in [#1011](https://github.com/PrefectHQ/fastmcp/pull/1011)\n\n**Full Changelog**: [v2.10.0...v2.10.1](https://github.com/PrefectHQ/fastmcp/compare/v2.10.0...v2.10.1)\n\n</Update>\n\n<Update label=\"v2.10.0\" description=\"2024-07-01\">\n\n## [v2.10.0: Great Spec-tations](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.0)\n\nFastMCP 2.10 brings full compliance with the 6/18/2025 MCP spec update, introducing elicitation support for dynamic server-client communication and output schemas for structured tool responses. Please note that due to these changes, this release also includes a breaking change to the return signature of `client.call_tool()`.\n\n### Elicitation Support\nElicitation allows MCP servers to request additional information from clients during tool execution, enabling more interactive and dynamic server behavior. This opens up new possibilities for tools that need user input or confirmation during execution.\n\n### Output Schemas\nTools can now define structured output schemas, ensuring that responses conform to expected formats and making tool integration more predictable and type-safe.\n\n## What's Changed\n### New Features 🎉\n* MCP 6/18/25: Add output schema to tools by [@jlowin](https://github.com/jlowin) in [#901](https://github.com/PrefectHQ/fastmcp/pull/901)\n* MCP 6/18/25: Elicitation support by [@jlowin](https://github.com/jlowin) in [#889](https://github.com/PrefectHQ/fastmcp/pull/889)\n### Enhancements 🔧\n* Update types + tests for SDK changes by [@jlowin](https://github.com/jlowin) in [#888](https://github.com/PrefectHQ/fastmcp/pull/888)\n* MCP 6/18/25: Update auth primitives by [@jlowin](https://github.com/jlowin) in [#966](https://github.com/PrefectHQ/fastmcp/pull/966)\n* Add OpenAPI extensions support to HTTPRoute by [@maddymanu](https://github.com/maddymanu) in [#977](https://github.com/PrefectHQ/fastmcp/pull/977)\n* Add title field support to FastMCP components by [@jlowin](https://github.com/jlowin) in [#982](https://github.com/PrefectHQ/fastmcp/pull/982)\n* Support implicit Elicitation acceptance by [@jlowin](https://github.com/jlowin) in [#983](https://github.com/PrefectHQ/fastmcp/pull/983)\n* Support 'no response' elicitation requests by [@jlowin](https://github.com/jlowin) in [#992](https://github.com/PrefectHQ/fastmcp/pull/992)\n* Add Support for Configurable Algorithms by [@sstene1](https://github.com/sstene1) in [#997](https://github.com/PrefectHQ/fastmcp/pull/997)\n### Fixes 🐞\n* Improve stdio error handling to raise connection failures immediately by [@jlowin](https://github.com/jlowin) in [#984](https://github.com/PrefectHQ/fastmcp/pull/984)\n* Fix type hints for FunctionResource:fn by [@CfirTsabari](https://github.com/CfirTsabari) in [#986](https://github.com/PrefectHQ/fastmcp/pull/986)\n* Update link to OpenAI MCP example by [@mossbanay](https://github.com/mossbanay) in [#985](https://github.com/PrefectHQ/fastmcp/pull/985)\n* Fix output schema generation edge case by [@jlowin](https://github.com/jlowin) in [#995](https://github.com/PrefectHQ/fastmcp/pull/995)\n* Refactor array parameter formatting to reduce code duplication by [@jlowin](https://github.com/jlowin) in [#1007](https://github.com/PrefectHQ/fastmcp/pull/1007)\n* Fix OpenAPI array parameter explode handling by [@jlowin](https://github.com/jlowin) in [#1008](https://github.com/PrefectHQ/fastmcp/pull/1008)\n### Breaking Changes 🛫\n* MCP 6/18/25: Upgrade to mcp 1.10 by [@jlowin](https://github.com/jlowin) in [#887](https://github.com/PrefectHQ/fastmcp/pull/887)\n### Docs 📚\n* Update middleware imports and documentation by [@jlowin](https://github.com/jlowin) in [#999](https://github.com/PrefectHQ/fastmcp/pull/999)\n* Update OpenAI docs by [@jlowin](https://github.com/jlowin) in [#1001](https://github.com/PrefectHQ/fastmcp/pull/1001)\n* Add CLI banner by [@jlowin](https://github.com/jlowin) in [#1005](https://github.com/PrefectHQ/fastmcp/pull/1005)\n### Examples & Contrib 💡\n* Component Manager by [@gorocode](https://github.com/gorocode) in [#976](https://github.com/PrefectHQ/fastmcp/pull/976)\n### Other Changes 🦾\n* Minor auth improvements by [@jlowin](https://github.com/jlowin) in [#967](https://github.com/PrefectHQ/fastmcp/pull/967)\n* Add .ccignore for copychat by [@jlowin](https://github.com/jlowin) in [#1000](https://github.com/PrefectHQ/fastmcp/pull/1000)\n\n## New Contributors\n* [@maddymanu](https://github.com/maddymanu) made their first contribution in [#977](https://github.com/PrefectHQ/fastmcp/pull/977)\n* [@github0hello](https://github.com/github0hello) made their first contribution in [#979](https://github.com/PrefectHQ/fastmcp/pull/979)\n* [@tommitt](https://github.com/tommitt) made their first contribution in [#975](https://github.com/PrefectHQ/fastmcp/pull/975)\n* [@CfirTsabari](https://github.com/CfirTsabari) made their first contribution in [#986](https://github.com/PrefectHQ/fastmcp/pull/986)\n* [@mossbanay](https://github.com/mossbanay) made their first contribution in [#985](https://github.com/PrefectHQ/fastmcp/pull/985)\n* [@sstene1](https://github.com/sstene1) made their first contribution in [#997](https://github.com/PrefectHQ/fastmcp/pull/997)\n\n**Full Changelog**: [v2.9.2...v2.10.0](https://github.com/PrefectHQ/fastmcp/compare/v2.9.2...v2.10.0)\n\n</Update>\n\n<Update label=\"v2.9.2\" description=\"2024-06-26\">\n\n## [v2.9.2: Safety Pin](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.2)\n\nThis is a patch release to pin `mcp` below 1.10, which includes changes related to the 6/18/2025 MCP spec update and could potentially break functionality for some FastMCP users.\n\n## What's Changed\n### Docs 📚\n* Fix version badge for messages by [@jlowin](https://github.com/jlowin) in [#960](https://github.com/PrefectHQ/fastmcp/pull/960)\n### Dependencies 📦\n* Pin mcp dependency by [@jlowin](https://github.com/jlowin) in [#962](https://github.com/PrefectHQ/fastmcp/pull/962)\n\n**Full Changelog**: [v2.9.1...v2.9.2](https://github.com/PrefectHQ/fastmcp/compare/v2.9.1...v2.9.2)\n\n</Update>\n\n<Update label=\"v2.9.1\" description=\"2024-06-26\">\n\n## [v2.9.1: Call Me Maybe](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.1)\n\nFastMCP 2.9.1 introduces automatic MCP list change notifications, allowing servers to notify clients when tools, resources, or prompts are dynamically updated. This enables more responsive and adaptive MCP integrations.\n\n## What's Changed\n### New Features 🎉\n* Add automatic MCP list change notifications and client message handling by [@jlowin](https://github.com/jlowin) in [#939](https://github.com/PrefectHQ/fastmcp/pull/939)\n### Enhancements 🔧\n* Add debug logging to bearer token authentication by [@jlowin](https://github.com/jlowin) in [#952](https://github.com/PrefectHQ/fastmcp/pull/952)\n### Fixes 🐞\n* Fix duplicate error logging in exception handlers by [@jlowin](https://github.com/jlowin) in [#938](https://github.com/PrefectHQ/fastmcp/pull/938)\n* Fix parameter location enum handling in OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#953](https://github.com/PrefectHQ/fastmcp/pull/953)\n* Fix external schema reference handling in OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#954](https://github.com/PrefectHQ/fastmcp/pull/954)\n### Docs 📚\n* Update changelog for 2.9 release by [@jlowin](https://github.com/jlowin) in [#929](https://github.com/PrefectHQ/fastmcp/pull/929)\n* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#935](https://github.com/PrefectHQ/fastmcp/pull/935)\n* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#947](https://github.com/PrefectHQ/fastmcp/pull/947)\n* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#949](https://github.com/PrefectHQ/fastmcp/pull/949)\n### Examples & Contrib 💡\n* Add `create_thread` tool to bsky MCP server by [@zzstoatzz](https://github.com/zzstoatzz) in [#927](https://github.com/PrefectHQ/fastmcp/pull/927)\n* Update `mount_example.py` to work with current fastmcp API by [@rajephon](https://github.com/rajephon) in [#957](https://github.com/PrefectHQ/fastmcp/pull/957)\n\n## New Contributors\n* [@rajephon](https://github.com/rajephon) made their first contribution in [#957](https://github.com/PrefectHQ/fastmcp/pull/957)\n\n**Full Changelog**: [v2.9.0...v2.9.1](https://github.com/PrefectHQ/fastmcp/compare/v2.9.0...v2.9.1)\n\n</Update>\n\n<Update label=\"v2.9.0\" description=\"2024-06-23\">\n\n## [v2.9.0: Stuck in the Middleware With You](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.0)\n\nFastMCP 2.9 introduces two important features that push beyond the basic MCP protocol: MCP Middleware and server-side type conversion.\n\n### MCP Middleware\nMCP middleware lets you intercept and modify requests and responses at the protocol level, giving you powerful capabilities for logging, authentication, validation, and more. This is particularly useful for building production-ready MCP servers that need sophisticated request handling.\n\n### Server-side Type Conversion\nThis release also introduces server-side type conversion for prompt arguments, ensuring that data is properly formatted before being passed to your functions. This reduces the burden on individual tools and prompts to handle type validation and conversion.\n\n## What's Changed\n### New Features 🎉\n* Add File utility for binary data by [@gorocode](https://github.com/gorocode) in [#843](https://github.com/PrefectHQ/fastmcp/pull/843)\n* Consolidate prefix logic into FastMCP methods by [@jlowin](https://github.com/jlowin) in [#861](https://github.com/PrefectHQ/fastmcp/pull/861)\n* Add MCP Middleware by [@jlowin](https://github.com/jlowin) in [#870](https://github.com/PrefectHQ/fastmcp/pull/870)\n* Implement server-side type conversion for prompt arguments by [@jlowin](https://github.com/jlowin) in [#908](https://github.com/PrefectHQ/fastmcp/pull/908)\n### Enhancements 🔧\n* Fix tool description indentation issue by [@zfflxx](https://github.com/zfflxx) in [#845](https://github.com/PrefectHQ/fastmcp/pull/845)\n* Add version parameter to FastMCP constructor by [@mkyutani](https://github.com/mkyutani) in [#842](https://github.com/PrefectHQ/fastmcp/pull/842)\n* Update version to not be positional by [@jlowin](https://github.com/jlowin) in [#848](https://github.com/PrefectHQ/fastmcp/pull/848)\n* Add key to component by [@jlowin](https://github.com/jlowin) in [#869](https://github.com/PrefectHQ/fastmcp/pull/869)\n* Add session_id property to Context for data sharing by [@jlowin](https://github.com/jlowin) in [#881](https://github.com/PrefectHQ/fastmcp/pull/881)\n* Fix CORS documentation example by [@jlowin](https://github.com/jlowin) in [#895](https://github.com/PrefectHQ/fastmcp/pull/895)\n### Fixes 🐞\n* \"report_progress missing passing related_request_id causes notifications not working\" by [@alexsee](https://github.com/alexsee) in [#838](https://github.com/PrefectHQ/fastmcp/pull/838)\n* Fix JWT issuer validation to support string values per RFC 7519 by [@jlowin](https://github.com/jlowin) in [#892](https://github.com/PrefectHQ/fastmcp/pull/892)\n* Fix BearerAuthProvider audience type annotations by [@jlowin](https://github.com/jlowin) in [#894](https://github.com/PrefectHQ/fastmcp/pull/894)\n### Docs 📚\n* Add CLAUDE.md development guidelines by [@jlowin](https://github.com/jlowin) in [#880](https://github.com/PrefectHQ/fastmcp/pull/880)\n* Update context docs for session_id property by [@jlowin](https://github.com/jlowin) in [#882](https://github.com/PrefectHQ/fastmcp/pull/882)\n* Add API reference by [@zzstoatzz](https://github.com/zzstoatzz) in [#893](https://github.com/PrefectHQ/fastmcp/pull/893)\n* Fix API ref rendering by [@zzstoatzz](https://github.com/zzstoatzz) in [#900](https://github.com/PrefectHQ/fastmcp/pull/900)\n* Simplify docs nav by [@jlowin](https://github.com/jlowin) in [#902](https://github.com/PrefectHQ/fastmcp/pull/902)\n* Add fastmcp inspect command by [@jlowin](https://github.com/jlowin) in [#904](https://github.com/PrefectHQ/fastmcp/pull/904)\n* Update client docs by [@jlowin](https://github.com/jlowin) in [#912](https://github.com/PrefectHQ/fastmcp/pull/912)\n* Update docs nav by [@jlowin](https://github.com/jlowin) in [#913](https://github.com/PrefectHQ/fastmcp/pull/913)\n* Update integration documentation for Claude Desktop, ChatGPT, and Claude Code by [@jlowin](https://github.com/jlowin) in [#915](https://github.com/PrefectHQ/fastmcp/pull/915)\n* Add http as an alias for streamable http by [@jlowin](https://github.com/jlowin) in [#917](https://github.com/PrefectHQ/fastmcp/pull/917)\n* Clean up parameter documentation by [@jlowin](https://github.com/jlowin) in [#918](https://github.com/PrefectHQ/fastmcp/pull/918)\n* Add middleware examples for timing, logging, rate limiting, and error handling by [@jlowin](https://github.com/jlowin) in [#919](https://github.com/PrefectHQ/fastmcp/pull/919)\n* ControlFlow → FastMCP rename by [@jlowin](https://github.com/jlowin) in [#922](https://github.com/PrefectHQ/fastmcp/pull/922)\n### Examples & Contrib 💡\n* Add contrib.mcp_mixin support for annotations by [@rsp2k](https://github.com/rsp2k) in [#860](https://github.com/PrefectHQ/fastmcp/pull/860)\n* Add ATProto (Bluesky) MCP Server Example by [@zzstoatzz](https://github.com/zzstoatzz) in [#916](https://github.com/PrefectHQ/fastmcp/pull/916)\n* Fix path in atproto example pyproject by [@zzstoatzz](https://github.com/zzstoatzz) in [#920](https://github.com/PrefectHQ/fastmcp/pull/920)\n* Remove uv source in example by [@zzstoatzz](https://github.com/zzstoatzz) in [#921](https://github.com/PrefectHQ/fastmcp/pull/921)\n\n## New Contributors\n* [@alexsee](https://github.com/alexsee) made their first contribution in [#838](https://github.com/PrefectHQ/fastmcp/pull/838)\n* [@zfflxx](https://github.com/zfflxx) made their first contribution in [#845](https://github.com/PrefectHQ/fastmcp/pull/845)\n* [@mkyutani](https://github.com/mkyutani) made their first contribution in [#842](https://github.com/PrefectHQ/fastmcp/pull/842)\n* [@gorocode](https://github.com/gorocode) made their first contribution in [#843](https://github.com/PrefectHQ/fastmcp/pull/843)\n* [@rsp2k](https://github.com/rsp2k) made their first contribution in [#860](https://github.com/PrefectHQ/fastmcp/pull/860)\n* [@owtaylor](https://github.com/owtaylor) made their first contribution in [#897](https://github.com/PrefectHQ/fastmcp/pull/897)\n* [@Jason-CKY](https://github.com/Jason-CKY) made their first contribution in [#906](https://github.com/PrefectHQ/fastmcp/pull/906)\n\n**Full Changelog**: [v2.8.1...v2.9.0](https://github.com/PrefectHQ/fastmcp/compare/v2.8.1...v2.9.0)\n\n</Update>\n\n<Update label=\"v2.8.1\" description=\"2024-06-15\">\n\n## [v2.8.1: Sound Judgement](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.8.1)\n\n2.8.1 introduces audio support, as well as minor fixes and updates for deprecated features.\n\n### Audio Support\nThis release adds support for audio content in MCP tools and resources, expanding FastMCP's multimedia capabilities beyond text and images.\n\n## What's Changed\n### New Features 🎉\n* Add audio support by [@jlowin](https://github.com/jlowin) in [#833](https://github.com/PrefectHQ/fastmcp/pull/833)\n### Enhancements 🔧\n* Add flag for disabling deprecation warnings by [@jlowin](https://github.com/jlowin) in [#802](https://github.com/PrefectHQ/fastmcp/pull/802)\n* Add examples to Tool Arg Param transformation by [@strawgate](https://github.com/strawgate) in [#806](https://github.com/PrefectHQ/fastmcp/pull/806)\n### Fixes 🐞\n* Restore .settings access as deprecated by [@jlowin](https://github.com/jlowin) in [#800](https://github.com/PrefectHQ/fastmcp/pull/800)\n* Ensure handling of false http kwargs correctly; removed unused kwarg by [@jlowin](https://github.com/jlowin) in [#804](https://github.com/PrefectHQ/fastmcp/pull/804)\n* Bump mcp 1.9.4 by [@jlowin](https://github.com/jlowin) in [#835](https://github.com/PrefectHQ/fastmcp/pull/835)\n### Docs 📚\n* Update changelog for 2.8.0 by [@jlowin](https://github.com/jlowin) in [#794](https://github.com/PrefectHQ/fastmcp/pull/794)\n* Update welcome docs by [@jlowin](https://github.com/jlowin) in [#808](https://github.com/PrefectHQ/fastmcp/pull/808)\n* Update headers in docs by [@jlowin](https://github.com/jlowin) in [#809](https://github.com/PrefectHQ/fastmcp/pull/809)\n* Add MCP group to tutorials by [@jlowin](https://github.com/jlowin) in [#810](https://github.com/PrefectHQ/fastmcp/pull/810)\n* Add Community section to documentation by [@zzstoatzz](https://github.com/zzstoatzz) in [#819](https://github.com/PrefectHQ/fastmcp/pull/819)\n* Add 2.8 update by [@jlowin](https://github.com/jlowin) in [#821](https://github.com/PrefectHQ/fastmcp/pull/821)\n* Embed YouTube videos in community showcase by [@zzstoatzz](https://github.com/zzstoatzz) in [#820](https://github.com/PrefectHQ/fastmcp/pull/820)\n### Other Changes 🦾\n* Ensure http args are passed through by [@jlowin](https://github.com/jlowin) in [#803](https://github.com/PrefectHQ/fastmcp/pull/803)\n* Fix install link in readme by [@jlowin](https://github.com/jlowin) in [#836](https://github.com/PrefectHQ/fastmcp/pull/836)\n\n**Full Changelog**: [v2.8.0...v2.8.1](https://github.com/PrefectHQ/fastmcp/compare/v2.8.0...v2.8.1)\n\n</Update>\n\n<Update label=\"v2.8.0\" description=\"2024-06-10\">\n\n## [v2.8.0: Transform and Roll Out](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.8.0)\n\nFastMCP 2.8.0 introduces powerful new ways to customize and control your MCP servers! \n\n### Tool Transformation\n\nThe 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.\n\n### Component Control\nThis 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.\n\n### Tools-by-Default\nFinally, to improve compatibility with a wider range of LLM clients, this release changes the default behavior for OpenAPI integration: all API endpoints are now converted to `Tools` by default. This is a **breaking change** but pragmatically necessitated by the fact that the majority of MCP clients available today are, sadly, only compatible with MCP tools. Therefore, this change significantly simplifies the out-of-the-box experience and ensures your entire API is immediately accessible to any tool-using agent.\n\n## What's Changed\n### New Features 🎉\n* First-class tool transformation by [@jlowin](https://github.com/jlowin) in [#745](https://github.com/PrefectHQ/fastmcp/pull/745)\n* Support enable/disable for all FastMCP components (tools, prompts, resources, templates) by [@jlowin](https://github.com/jlowin) in [#781](https://github.com/PrefectHQ/fastmcp/pull/781)\n* Add support for tag-based component filtering by [@jlowin](https://github.com/jlowin) in [#748](https://github.com/PrefectHQ/fastmcp/pull/748)\n* Allow tag assignments for OpenAPI by [@jlowin](https://github.com/jlowin) in [#791](https://github.com/PrefectHQ/fastmcp/pull/791)\n### Enhancements 🔧\n* Create common base class for components by [@jlowin](https://github.com/jlowin) in [#776](https://github.com/PrefectHQ/fastmcp/pull/776)\n* Move components to own file; add resource by [@jlowin](https://github.com/jlowin) in [#777](https://github.com/PrefectHQ/fastmcp/pull/777)\n* Update FastMCP component with __eq__ and __repr__ by [@jlowin](https://github.com/jlowin) in [#779](https://github.com/PrefectHQ/fastmcp/pull/779)\n* Remove open-ended and server-specific settings by [@jlowin](https://github.com/jlowin) in [#750](https://github.com/PrefectHQ/fastmcp/pull/750)\n### Fixes 🐞\n* Ensure client is only initialized once by [@jlowin](https://github.com/jlowin) in [#758](https://github.com/PrefectHQ/fastmcp/pull/758)\n* Fix field validator for resource by [@jlowin](https://github.com/jlowin) in [#778](https://github.com/PrefectHQ/fastmcp/pull/778)\n* Ensure proxies can overwrite remote tools without falling back to the remote by [@jlowin](https://github.com/jlowin) in [#782](https://github.com/PrefectHQ/fastmcp/pull/782)\n### Breaking Changes 🛫\n* Treat all openapi routes as tools by [@jlowin](https://github.com/jlowin) in [#788](https://github.com/PrefectHQ/fastmcp/pull/788)\n* Fix issue with global OpenAPI tags by [@jlowin](https://github.com/jlowin) in [#792](https://github.com/PrefectHQ/fastmcp/pull/792)\n### Docs 📚\n* Minor docs updates by [@jlowin](https://github.com/jlowin) in [#755](https://github.com/PrefectHQ/fastmcp/pull/755)\n* Add 2.7 update by [@jlowin](https://github.com/jlowin) in [#756](https://github.com/PrefectHQ/fastmcp/pull/756)\n* Reduce 2.7 image size by [@jlowin](https://github.com/jlowin) in [#757](https://github.com/PrefectHQ/fastmcp/pull/757)\n* Update updates.mdx by [@jlowin](https://github.com/jlowin) in [#765](https://github.com/PrefectHQ/fastmcp/pull/765)\n* Hide docs sidebar scrollbar by default by [@jlowin](https://github.com/jlowin) in [#766](https://github.com/PrefectHQ/fastmcp/pull/766)\n* Add \"stop vibe testing\" to tutorials by [@jlowin](https://github.com/jlowin) in [#767](https://github.com/PrefectHQ/fastmcp/pull/767)\n* Add docs links by [@jlowin](https://github.com/jlowin) in [#768](https://github.com/PrefectHQ/fastmcp/pull/768)\n* Fix: updated variable name under Gemini remote client by [@yrangana](https://github.com/yrangana) in [#769](https://github.com/PrefectHQ/fastmcp/pull/769)\n* Revert \"Hide docs sidebar scrollbar by default\" by [@jlowin](https://github.com/jlowin) in [#770](https://github.com/PrefectHQ/fastmcp/pull/770)\n* Add updates by [@jlowin](https://github.com/jlowin) in [#773](https://github.com/PrefectHQ/fastmcp/pull/773)\n* Add tutorials by [@jlowin](https://github.com/jlowin) in [#783](https://github.com/PrefectHQ/fastmcp/pull/783)\n* Update LLM-friendly docs by [@jlowin](https://github.com/jlowin) in [#784](https://github.com/PrefectHQ/fastmcp/pull/784)\n* Update oauth.mdx by [@JeremyCraigMartinez](https://github.com/JeremyCraigMartinez) in [#787](https://github.com/PrefectHQ/fastmcp/pull/787)\n* Add changelog by [@jlowin](https://github.com/jlowin) in [#789](https://github.com/PrefectHQ/fastmcp/pull/789)\n* Add tutorials by [@jlowin](https://github.com/jlowin) in [#790](https://github.com/PrefectHQ/fastmcp/pull/790)\n* Add docs for tag-based filtering by [@jlowin](https://github.com/jlowin) in [#793](https://github.com/PrefectHQ/fastmcp/pull/793)\n### Other Changes 🦾\n* Create dependabot.yml by [@jlowin](https://github.com/jlowin) in [#759](https://github.com/PrefectHQ/fastmcp/pull/759)\n* Bump astral-sh/setup-uv from 3 to 6 by [@dependabot](https://github.com/dependabot) in [#760](https://github.com/PrefectHQ/fastmcp/pull/760)\n* Add dependencies section to release by [@jlowin](https://github.com/jlowin) in [#761](https://github.com/PrefectHQ/fastmcp/pull/761)\n* Remove extra imports for MCPConfig by [@Maanas-Verma](https://github.com/Maanas-Verma) in [#763](https://github.com/PrefectHQ/fastmcp/pull/763)\n* Split out enhancements in release notes by [@jlowin](https://github.com/jlowin) in [#764](https://github.com/PrefectHQ/fastmcp/pull/764)\n\n## New Contributors\n* [@dependabot](https://github.com/dependabot) made their first contribution in [#760](https://github.com/PrefectHQ/fastmcp/pull/760)\n* [@Maanas-Verma](https://github.com/Maanas-Verma) made their first contribution in [#763](https://github.com/PrefectHQ/fastmcp/pull/763)\n* [@JeremyCraigMartinez](https://github.com/JeremyCraigMartinez) made their first contribution in [#787](https://github.com/PrefectHQ/fastmcp/pull/787)\n\n**Full Changelog**: [v2.7.1...v2.8.0](https://github.com/PrefectHQ/fastmcp/compare/v2.7.1...v2.8.0)\n\n</Update>\n\n<Update label=\"v2.7.1\" description=\"2024-06-08\">\n\n## [v2.7.1: The Bearer Necessities](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.1)\n\nThis release primarily contains a fix for parsing string tokens that are provided to FastMCP clients.\n\n### New Features 🎉\n\n* Respect cache setting, set default to 1 second by [@jlowin](https://github.com/jlowin) in [#747](https://github.com/PrefectHQ/fastmcp/pull/747)\n\n### Fixes 🐞\n\n* Ensure event store is properly typed by [@jlowin](https://github.com/jlowin) in [#753](https://github.com/PrefectHQ/fastmcp/pull/753)\n* Fix passing token string to client auth & add auth to MCPConfig clients by [@jlowin](https://github.com/jlowin) in [#754](https://github.com/PrefectHQ/fastmcp/pull/754)\n\n### Docs 📚\n\n* Docs : fix client to mcp\\_client in Gemini example by [@yrangana](https://github.com/yrangana) in [#734](https://github.com/PrefectHQ/fastmcp/pull/734)\n* update add tool docstring by [@strawgate](https://github.com/strawgate) in [#739](https://github.com/PrefectHQ/fastmcp/pull/739)\n* Fix contrib link by [@richardkmichael](https://github.com/richardkmichael) in [#749](https://github.com/PrefectHQ/fastmcp/pull/749)\n\n### Other Changes 🦾\n\n* Switch Pydantic defaults to kwargs by [@strawgate](https://github.com/strawgate) in [#731](https://github.com/PrefectHQ/fastmcp/pull/731)\n* Fix Typo in CLI module by [@wfclark5](https://github.com/wfclark5) in [#737](https://github.com/PrefectHQ/fastmcp/pull/737)\n* chore: fix prompt docstring by [@danb27](https://github.com/danb27) in [#752](https://github.com/PrefectHQ/fastmcp/pull/752)\n* Add accept to excluded headers by [@jlowin](https://github.com/jlowin) in [#751](https://github.com/PrefectHQ/fastmcp/pull/751)\n\n### New Contributors\n\n* [@wfclark5](https://github.com/wfclark5) made their first contribution in [#737](https://github.com/PrefectHQ/fastmcp/pull/737)\n* [@richardkmichael](https://github.com/richardkmichael) made their first contribution in [#749](https://github.com/PrefectHQ/fastmcp/pull/749)\n* [@danb27](https://github.com/danb27) made their first contribution in [#752](https://github.com/PrefectHQ/fastmcp/pull/752)\n\n**Full Changelog**: [v2.7.0...v2.7.1](https://github.com/PrefectHQ/fastmcp/compare/v2.7.0...v2.7.1)\n</Update>\n\n<Update label=\"v2.7.0\" description=\"2024-06-05\">\n\n## [v2.7.0: Pare Programming](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.0)\n\nThis is primarily a housekeeping release to remove or deprecate cruft that's accumulated since v1. Primarily, this release refactors FastMCP's internals in preparation for features planned in the next few major releases. However please note that as a result, this release has some minor breaking changes (which is why it's 2.7, not 2.6.2, in accordance with repo guidelines) though not to the core user-facing APIs.\n\n### Breaking Changes 🛫\n\n* decorators return the objects they create, not the decorated function\n* websockets is an optional dependency\n* methods on the server for automatically converting functions into tools/resources/prompts have been deprecated in favor of using the decorators directly\n\n### New Features 🎉\n\n* allow passing flags to servers by [@zzstoatzz](https://github.com/zzstoatzz) in [#690](https://github.com/PrefectHQ/fastmcp/pull/690)\n* replace $ref pointing to `#/components/schemas/` with `#/$defs/` by [@phateffect](https://github.com/phateffect) in [#697](https://github.com/PrefectHQ/fastmcp/pull/697)\n* Split Tool into Tool and FunctionTool by [@jlowin](https://github.com/jlowin) in [#700](https://github.com/PrefectHQ/fastmcp/pull/700)\n* Use strict basemodel for Prompt; relax from\\_function deprecation by [@jlowin](https://github.com/jlowin) in [#701](https://github.com/PrefectHQ/fastmcp/pull/701)\n* Formalize resource/functionresource replationship by [@jlowin](https://github.com/jlowin) in [#702](https://github.com/PrefectHQ/fastmcp/pull/702)\n* Formalize template/functiontemplate split by [@jlowin](https://github.com/jlowin) in [#703](https://github.com/PrefectHQ/fastmcp/pull/703)\n* Support flexible @tool decorator call patterns by [@jlowin](https://github.com/jlowin) in [#706](https://github.com/PrefectHQ/fastmcp/pull/706)\n* Ensure deprecation warnings have stacklevel=2 by [@jlowin](https://github.com/jlowin) in [#710](https://github.com/PrefectHQ/fastmcp/pull/710)\n* Allow naked prompt decorator by [@jlowin](https://github.com/jlowin) in [#711](https://github.com/PrefectHQ/fastmcp/pull/711)\n\n### Fixes 🐞\n\n* Updates / Fixes for Tool Content Conversion by [@strawgate](https://github.com/strawgate) in [#642](https://github.com/PrefectHQ/fastmcp/pull/642)\n* Fix pr labeler permissions by [@jlowin](https://github.com/jlowin) in [#708](https://github.com/PrefectHQ/fastmcp/pull/708)\n* remove -n auto by [@jlowin](https://github.com/jlowin) in [#709](https://github.com/PrefectHQ/fastmcp/pull/709)\n* Fix links in README.md by [@alainivars](https://github.com/alainivars) in [#723](https://github.com/PrefectHQ/fastmcp/pull/723)\n\nHappily, this release DOES permit the use of \"naked\" decorators to align with Pythonic practice:\n\n```python\n@mcp.tool\ndef my_tool():\n    ...\n```\n\n**Full Changelog**: [v2.6.2...v2.7.0](https://github.com/PrefectHQ/fastmcp/compare/v2.6.2...v2.7.0)\n</Update>\n\n<Update label=\"v2.6.1\" description=\"2024-06-03\">\n\n## [v2.6.1: Blast Auth (second ignition)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.6.1)\n\nThis is a patch release to restore py.typed in #686.\n\n### Docs 📚\n\n* Update readme by [@jlowin](https://github.com/jlowin) in [#679](https://github.com/PrefectHQ/fastmcp/pull/679)\n* Add gemini tutorial by [@jlowin](https://github.com/jlowin) in [#680](https://github.com/PrefectHQ/fastmcp/pull/680)\n* Fix : fix path error to CLI Documentation by [@yrangana](https://github.com/yrangana) in [#684](https://github.com/PrefectHQ/fastmcp/pull/684)\n* Update auth docs by [@jlowin](https://github.com/jlowin) in [#687](https://github.com/PrefectHQ/fastmcp/pull/687)\n\n### Other Changes 🦾\n\n* Remove deprecation notice by [@jlowin](https://github.com/jlowin) in [#677](https://github.com/PrefectHQ/fastmcp/pull/677)\n* Delete server.py by [@jlowin](https://github.com/jlowin) in [#681](https://github.com/PrefectHQ/fastmcp/pull/681)\n* Restore py.typed by [@jlowin](https://github.com/jlowin) in [#686](https://github.com/PrefectHQ/fastmcp/pull/686)\n\n### New Contributors\n\n* [@yrangana](https://github.com/yrangana) made their first contribution in [#684](https://github.com/PrefectHQ/fastmcp/pull/684)\n\n**Full Changelog**: [v2.6.0...v2.6.1](https://github.com/PrefectHQ/fastmcp/compare/v2.6.0...v2.6.1)\n</Update>\n\n<Update label=\"v2.6.0\" description=\"2024-06-02\">\n\n## [v2.6.0: Blast Auth](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.6.0)\n\n### New Features 🎉\n\n* Introduce MCP client oauth flow by [@jlowin](https://github.com/jlowin) in [#478](https://github.com/PrefectHQ/fastmcp/pull/478)\n* Support providing tools at init by [@jlowin](https://github.com/jlowin) in [#647](https://github.com/PrefectHQ/fastmcp/pull/647)\n* Simplify code for running servers in processes during tests by [@jlowin](https://github.com/jlowin) in [#649](https://github.com/PrefectHQ/fastmcp/pull/649)\n* Add basic bearer auth for server and client by [@jlowin](https://github.com/jlowin) in [#650](https://github.com/PrefectHQ/fastmcp/pull/650)\n* Support configuring bearer auth from env vars by [@jlowin](https://github.com/jlowin) in [#652](https://github.com/PrefectHQ/fastmcp/pull/652)\n* feat(tool): add support for excluding arguments from tool definition by [@deepak-stratforge](https://github.com/deepak-stratforge) in [#626](https://github.com/PrefectHQ/fastmcp/pull/626)\n* Add docs for server + client auth by [@jlowin](https://github.com/jlowin) in [#655](https://github.com/PrefectHQ/fastmcp/pull/655)\n\n### Fixes 🐞\n\n* fix: Support concurrency in FastMcpProxy (and Client) by [@Sillocan](https://github.com/Sillocan) in [#635](https://github.com/PrefectHQ/fastmcp/pull/635)\n* Ensure Client.close() cleans up client context appropriately by [@jlowin](https://github.com/jlowin) in [#643](https://github.com/PrefectHQ/fastmcp/pull/643)\n* Update client.mdx: ClientError namespace by [@mjkaye](https://github.com/mjkaye) in [#657](https://github.com/PrefectHQ/fastmcp/pull/657)\n\n### Docs 📚\n\n* Make FastMCPTransport support simulated Streamable HTTP Transport (didn't work) by [@jlowin](https://github.com/jlowin) in [#645](https://github.com/PrefectHQ/fastmcp/pull/645)\n* Document exclude\\_args by [@jlowin](https://github.com/jlowin) in [#653](https://github.com/PrefectHQ/fastmcp/pull/653)\n* Update welcome by [@jlowin](https://github.com/jlowin) in [#673](https://github.com/PrefectHQ/fastmcp/pull/673)\n* Add Anthropic + Claude desktop integration guides by [@jlowin](https://github.com/jlowin) in [#674](https://github.com/PrefectHQ/fastmcp/pull/674)\n* Minor docs design updates by [@jlowin](https://github.com/jlowin) in [#676](https://github.com/PrefectHQ/fastmcp/pull/676)\n\n### Other Changes 🦾\n\n* Update test typing by [@jlowin](https://github.com/jlowin) in [#646](https://github.com/PrefectHQ/fastmcp/pull/646)\n* Add OpenAI integration docs by [@jlowin](https://github.com/jlowin) in [#660](https://github.com/PrefectHQ/fastmcp/pull/660)\n\n### New Contributors\n\n* [@Sillocan](https://github.com/Sillocan) made their first contribution in [#635](https://github.com/PrefectHQ/fastmcp/pull/635)\n* [@deepak-stratforge](https://github.com/deepak-stratforge) made their first contribution in [#626](https://github.com/PrefectHQ/fastmcp/pull/626)\n* [@mjkaye](https://github.com/mjkaye) made their first contribution in [#657](https://github.com/PrefectHQ/fastmcp/pull/657)\n\n**Full Changelog**: [v2.5.2...v2.6.0](https://github.com/PrefectHQ/fastmcp/compare/v2.5.2...v2.6.0)\n</Update>\n\n<Update label=\"v2.5.2\" description=\"2024-05-29\">\n\n## [v2.5.2: Stayin' Alive](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.2)\n\n### New Features 🎉\n\n* Add graceful error handling for unreachable mounted servers by [@davenpi](https://github.com/davenpi) in [#605](https://github.com/PrefectHQ/fastmcp/pull/605)\n* Improve type inference from client transport by [@jlowin](https://github.com/jlowin) in [#623](https://github.com/PrefectHQ/fastmcp/pull/623)\n* Add keep\\_alive param to reuse subprocess by [@jlowin](https://github.com/jlowin) in [#624](https://github.com/PrefectHQ/fastmcp/pull/624)\n\n### Fixes 🐞\n\n* Fix handling tools without descriptions by [@jlowin](https://github.com/jlowin) in [#610](https://github.com/PrefectHQ/fastmcp/pull/610)\n* Don't print env vars to console when format is wrong by [@jlowin](https://github.com/jlowin) in [#615](https://github.com/PrefectHQ/fastmcp/pull/615)\n* Ensure behavior-affecting headers are excluded when forwarding proxies/openapi by [@jlowin](https://github.com/jlowin) in [#620](https://github.com/PrefectHQ/fastmcp/pull/620)\n\n### Docs 📚\n\n* Add notes about uv and claude desktop by [@jlowin](https://github.com/jlowin) in [#597](https://github.com/PrefectHQ/fastmcp/pull/597)\n\n### Other Changes 🦾\n\n* add init\\_timeout for mcp client by [@jfouret](https://github.com/jfouret) in [#607](https://github.com/PrefectHQ/fastmcp/pull/607)\n* Add init\\_timeout for mcp client (incl settings) by [@jlowin](https://github.com/jlowin) in [#609](https://github.com/PrefectHQ/fastmcp/pull/609)\n* Support for uppercase letters at the log level by [@ksawaray](https://github.com/ksawaray) in [#625](https://github.com/PrefectHQ/fastmcp/pull/625)\n\n### New Contributors\n\n* [@jfouret](https://github.com/jfouret) made their first contribution in [#607](https://github.com/PrefectHQ/fastmcp/pull/607)\n* [@ksawaray](https://github.com/ksawaray) made their first contribution in [#625](https://github.com/PrefectHQ/fastmcp/pull/625)\n\n**Full Changelog**: [v2.5.1...v2.5.2](https://github.com/PrefectHQ/fastmcp/compare/v2.5.1...v2.5.2)\n</Update>\n\n<Update label=\"v2.5.1\" description=\"2024-05-24\">\n\n## [v2.5.1: Route Awakening (Part 2)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.1)\n\n### Fixes 🐞\n\n* Ensure content-length is always stripped from client headers by [@jlowin](https://github.com/jlowin) in [#589](https://github.com/PrefectHQ/fastmcp/pull/589)\n\n### Docs 📚\n\n* Fix redundant section of docs by [@jlowin](https://github.com/jlowin) in [#583](https://github.com/PrefectHQ/fastmcp/pull/583)\n\n**Full Changelog**: [v2.5.0...v2.5.1](https://github.com/PrefectHQ/fastmcp/compare/v2.5.0...v2.5.1)\n</Update>\n\n<Update label=\"v2.5.0\" description=\"2024-05-24\">\n\n## [v2.5.0: Route Awakening](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.0)\n\nThis release introduces completely new tools for generating and customizing MCP servers from OpenAPI specs and FastAPI apps, including popular requests like mechanisms for determining what routes map to what MCP components; renaming routes; and customizing the generated MCP components.\n\n### New Features 🎉\n\n* Add FastMCP 1.0 server support for in-memory Client / Testing by [@jlowin](https://github.com/jlowin) in [#539](https://github.com/PrefectHQ/fastmcp/pull/539)\n* Minor addition: add transport to stdio server in mcpconfig, with default by [@jlowin](https://github.com/jlowin) in [#555](https://github.com/PrefectHQ/fastmcp/pull/555)\n* Raise an error if a Client is created with no servers in config by [@jlowin](https://github.com/jlowin) in [#554](https://github.com/PrefectHQ/fastmcp/pull/554)\n* Expose model preferences in `Context.sample` for flexible model selection. by [@davenpi](https://github.com/davenpi) in [#542](https://github.com/PrefectHQ/fastmcp/pull/542)\n* Ensure custom routes are respected by [@jlowin](https://github.com/jlowin) in [#558](https://github.com/PrefectHQ/fastmcp/pull/558)\n* Add client method to send cancellation notifications by [@davenpi](https://github.com/davenpi) in [#563](https://github.com/PrefectHQ/fastmcp/pull/563)\n* Enhance route map logic for include/exclude OpenAPI routes by [@jlowin](https://github.com/jlowin) in [#564](https://github.com/PrefectHQ/fastmcp/pull/564)\n* Add tag-based route maps by [@jlowin](https://github.com/jlowin) in [#565](https://github.com/PrefectHQ/fastmcp/pull/565)\n* Add advanced control of openAPI route creation by [@jlowin](https://github.com/jlowin) in [#566](https://github.com/PrefectHQ/fastmcp/pull/566)\n* Make error masking configurable by [@jlowin](https://github.com/jlowin) in [#550](https://github.com/PrefectHQ/fastmcp/pull/550)\n* Ensure client headers are passed through to remote servers by [@jlowin](https://github.com/jlowin) in [#575](https://github.com/PrefectHQ/fastmcp/pull/575)\n* Use lowercase name for headers when comparing by [@jlowin](https://github.com/jlowin) in [#576](https://github.com/PrefectHQ/fastmcp/pull/576)\n* Permit more flexible name generation for OpenAPI servers by [@jlowin](https://github.com/jlowin) in [#578](https://github.com/PrefectHQ/fastmcp/pull/578)\n* Ensure that tools/templates/prompts are compatible with callable objects by [@jlowin](https://github.com/jlowin) in [#579](https://github.com/PrefectHQ/fastmcp/pull/579)\n\n### Docs 📚\n\n* Add version badge for prefix formats by [@jlowin](https://github.com/jlowin) in [#537](https://github.com/PrefectHQ/fastmcp/pull/537)\n* Add versioning note to docs by [@jlowin](https://github.com/jlowin) in [#551](https://github.com/PrefectHQ/fastmcp/pull/551)\n* Bump 2.3.6 references to 2.4.0 by [@jlowin](https://github.com/jlowin) in [#567](https://github.com/PrefectHQ/fastmcp/pull/567)\n\n**Full Changelog**: [v2.4.0...v2.5.0](https://github.com/PrefectHQ/fastmcp/compare/v2.4.0...v2.5.0)\n</Update>\n\n<Update label=\"v2.4.0\" description=\"2024-05-21\">\n\n## [v2.4.0: Config and Conquer](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.4.0)\n\n**Note**: this release includes a backwards-incompatible change to how resources are prefixed when mounted in composed servers. However, it is only backwards-incompatible if users were running tests or manually loading resources by prefixed key; LLMs should not have any issue discovering the new route.\n\n### New Features 🎉\n\n* Allow \\* Methods and all routes as tools shortcuts by [@jlowin](https://github.com/jlowin) in [#520](https://github.com/PrefectHQ/fastmcp/pull/520)\n* Improved support for config dicts by [@jlowin](https://github.com/jlowin) in [#522](https://github.com/PrefectHQ/fastmcp/pull/522)\n* Support creating clients from MCP config dicts, including multi-server clients by [@jlowin](https://github.com/jlowin) in [#527](https://github.com/PrefectHQ/fastmcp/pull/527)\n* Make resource prefix format configurable by [@jlowin](https://github.com/jlowin) in [#534](https://github.com/PrefectHQ/fastmcp/pull/534)\n\n### Fixes 🐞\n\n* Avoid hanging on initializing server session by [@jlowin](https://github.com/jlowin) in [#523](https://github.com/PrefectHQ/fastmcp/pull/523)\n\n### Breaking Changes 🛫\n\n* Remove customizable separators; improve resource separator by [@jlowin](https://github.com/jlowin) in [#526](https://github.com/PrefectHQ/fastmcp/pull/526)\n\n### Docs 📚\n\n* Improve client documentation by [@jlowin](https://github.com/jlowin) in [#517](https://github.com/PrefectHQ/fastmcp/pull/517)\n\n### Other Changes 🦾\n\n* Ensure openapi path params are handled properly by [@jlowin](https://github.com/jlowin) in [#519](https://github.com/PrefectHQ/fastmcp/pull/519)\n* better error when missing lifespan by [@zzstoatzz](https://github.com/zzstoatzz) in [#521](https://github.com/PrefectHQ/fastmcp/pull/521)\n\n**Full Changelog**: [v2.3.5...v2.4.0](https://github.com/PrefectHQ/fastmcp/compare/v2.3.5...v2.4.0)\n</Update>\n\n<Update label=\"v2.3.5\" description=\"2024-05-20\">\n\n## [v2.3.5: Making Progress](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.5)\n\n### New Features 🎉\n\n* support messages in progress notifications by [@rickygenhealth](https://github.com/rickygenhealth) in [#471](https://github.com/PrefectHQ/fastmcp/pull/471)\n* feat: Add middleware option in server.run by [@Maxi91f](https://github.com/Maxi91f) in [#475](https://github.com/PrefectHQ/fastmcp/pull/475)\n* Add lifespan property to app by [@jlowin](https://github.com/jlowin) in [#483](https://github.com/PrefectHQ/fastmcp/pull/483)\n* Update `fastmcp run` to work with remote servers by [@jlowin](https://github.com/jlowin) in [#491](https://github.com/PrefectHQ/fastmcp/pull/491)\n* Add FastMCP.as\\_proxy() by [@jlowin](https://github.com/jlowin) in [#490](https://github.com/PrefectHQ/fastmcp/pull/490)\n* Infer sse transport from urls containing /sse by [@jlowin](https://github.com/jlowin) in [#512](https://github.com/PrefectHQ/fastmcp/pull/512)\n* Add progress handler to client by [@jlowin](https://github.com/jlowin) in [#513](https://github.com/PrefectHQ/fastmcp/pull/513)\n* Store the initialize result on the client by [@jlowin](https://github.com/jlowin) in [#509](https://github.com/PrefectHQ/fastmcp/pull/509)\n\n### Fixes 🐞\n\n* Remove patch and use upstream SSEServerTransport by [@jlowin](https://github.com/jlowin) in [#425](https://github.com/PrefectHQ/fastmcp/pull/425)\n\n### Docs 📚\n\n* Update transport docs by [@jlowin](https://github.com/jlowin) in [#458](https://github.com/PrefectHQ/fastmcp/pull/458)\n* update proxy docs + example by [@zzstoatzz](https://github.com/zzstoatzz) in [#460](https://github.com/PrefectHQ/fastmcp/pull/460)\n* doc(asgi): Change custom route example to PlainTextResponse by [@mcw0933](https://github.com/mcw0933) in [#477](https://github.com/PrefectHQ/fastmcp/pull/477)\n* Store FastMCP instance on app.state.fastmcp\\_server by [@jlowin](https://github.com/jlowin) in [#489](https://github.com/PrefectHQ/fastmcp/pull/489)\n* Improve AGENTS.md overview by [@jlowin](https://github.com/jlowin) in [#492](https://github.com/PrefectHQ/fastmcp/pull/492)\n* Update release numbers for anticipated version by [@jlowin](https://github.com/jlowin) in [#516](https://github.com/PrefectHQ/fastmcp/pull/516)\n\n### Other Changes 🦾\n\n* run tests on all PRs by [@jlowin](https://github.com/jlowin) in [#468](https://github.com/PrefectHQ/fastmcp/pull/468)\n* add null check by [@zzstoatzz](https://github.com/zzstoatzz) in [#473](https://github.com/PrefectHQ/fastmcp/pull/473)\n* strict typing for `server.py` by [@zzstoatzz](https://github.com/zzstoatzz) in [#476](https://github.com/PrefectHQ/fastmcp/pull/476)\n* Doc(quickstart): Fix import statements by [@mai-nakagawa](https://github.com/mai-nakagawa) in [#479](https://github.com/PrefectHQ/fastmcp/pull/479)\n* Add labeler by [@jlowin](https://github.com/jlowin) in [#484](https://github.com/PrefectHQ/fastmcp/pull/484)\n* Fix flaky timeout test by increasing timeout (#474) by [@davenpi](https://github.com/davenpi) in [#486](https://github.com/PrefectHQ/fastmcp/pull/486)\n* Skipping `test_permission_error` if runner is root. by [@ZiadAmerr](https://github.com/ZiadAmerr) in [#502](https://github.com/PrefectHQ/fastmcp/pull/502)\n* allow passing full uvicorn config by [@zzstoatzz](https://github.com/zzstoatzz) in [#504](https://github.com/PrefectHQ/fastmcp/pull/504)\n* Skip timeout tests on windows by [@jlowin](https://github.com/jlowin) in [#514](https://github.com/PrefectHQ/fastmcp/pull/514)\n\n### New Contributors\n\n* [@rickygenhealth](https://github.com/rickygenhealth) made their first contribution in [#471](https://github.com/PrefectHQ/fastmcp/pull/471)\n* [@Maxi91f](https://github.com/Maxi91f) made their first contribution in [#475](https://github.com/PrefectHQ/fastmcp/pull/475)\n* [@mcw0933](https://github.com/mcw0933) made their first contribution in [#477](https://github.com/PrefectHQ/fastmcp/pull/477)\n* [@mai-nakagawa](https://github.com/mai-nakagawa) made their first contribution in [#479](https://github.com/PrefectHQ/fastmcp/pull/479)\n* [@ZiadAmerr](https://github.com/ZiadAmerr) made their first contribution in [#502](https://github.com/PrefectHQ/fastmcp/pull/502)\n\n**Full Changelog**: [v2.3.4...v2.3.5](https://github.com/PrefectHQ/fastmcp/compare/v2.3.4...v2.3.5)\n</Update>\n\n<Update label=\"v2.3.4\" description=\"2024-05-15\">\n\n## [v2.3.4: Error Today, Gone Tomorrow](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.4)\n\n### New Features 🎉\n\n* logging stack trace for easier debugging by [@jbkoh](https://github.com/jbkoh) in [#413](https://github.com/PrefectHQ/fastmcp/pull/413)\n* add missing StreamableHttpTransport in client exports by [@yihuang](https://github.com/yihuang) in [#408](https://github.com/PrefectHQ/fastmcp/pull/408)\n* Improve error handling for tools and resources by [@jlowin](https://github.com/jlowin) in [#434](https://github.com/PrefectHQ/fastmcp/pull/434)\n* feat: add support for removing tools from server by [@davenpi](https://github.com/davenpi) in [#437](https://github.com/PrefectHQ/fastmcp/pull/437)\n* Prune titles from JSONSchemas by [@jlowin](https://github.com/jlowin) in [#449](https://github.com/PrefectHQ/fastmcp/pull/449)\n* Declare toolsChanged capability for stdio server. by [@davenpi](https://github.com/davenpi) in [#450](https://github.com/PrefectHQ/fastmcp/pull/450)\n* Improve handling of exceptiongroups when raised in clients by [@jlowin](https://github.com/jlowin) in [#452](https://github.com/PrefectHQ/fastmcp/pull/452)\n* Add timeout support to client by [@jlowin](https://github.com/jlowin) in [#455](https://github.com/PrefectHQ/fastmcp/pull/455)\n\n### Fixes 🐞\n\n* Pin to mcp 1.8.1 to resolve callback deadlocks with SHTTP by [@jlowin](https://github.com/jlowin) in [#427](https://github.com/PrefectHQ/fastmcp/pull/427)\n* Add reprs for OpenAPI objects by [@jlowin](https://github.com/jlowin) in [#447](https://github.com/PrefectHQ/fastmcp/pull/447)\n* Ensure openapi defs for structured objects are loaded properly by [@jlowin](https://github.com/jlowin) in [#448](https://github.com/PrefectHQ/fastmcp/pull/448)\n* Ensure tests run against correct python version by [@jlowin](https://github.com/jlowin) in [#454](https://github.com/PrefectHQ/fastmcp/pull/454)\n* Ensure result is only returned if a new key was found by [@jlowin](https://github.com/jlowin) in [#456](https://github.com/PrefectHQ/fastmcp/pull/456)\n\n### Docs 📚\n\n* Add documentation for tool removal by [@jlowin](https://github.com/jlowin) in [#440](https://github.com/PrefectHQ/fastmcp/pull/440)\n\n### Other Changes 🦾\n\n* Deprecate passing settings to the FastMCP instance by [@jlowin](https://github.com/jlowin) in [#424](https://github.com/PrefectHQ/fastmcp/pull/424)\n* Add path prefix to test by [@jlowin](https://github.com/jlowin) in [#432](https://github.com/PrefectHQ/fastmcp/pull/432)\n\n### New Contributors\n\n* [@jbkoh](https://github.com/jbkoh) made their first contribution in [#413](https://github.com/PrefectHQ/fastmcp/pull/413)\n* [@davenpi](https://github.com/davenpi) made their first contribution in [#437](https://github.com/PrefectHQ/fastmcp/pull/437)\n\n**Full Changelog**: [v2.3.3...v2.3.4](https://github.com/PrefectHQ/fastmcp/compare/v2.3.3...v2.3.4)\n</Update>\n\n<Update label=\"v2.3.3\" description=\"2024-05-10\">\n\n## [v2.3.3: SSE you later](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.3)\n\nThis is a hotfix for a bug introduced in 2.3.2 that broke SSE servers\n\n### Fixes 🐞\n\n* Fix bug that sets message path and sse path to same value by [@jlowin](https://github.com/jlowin) in [#405](https://github.com/PrefectHQ/fastmcp/pull/405)\n\n### Docs 📚\n\n* Update composition docs by [@jlowin](https://github.com/jlowin) in [#403](https://github.com/PrefectHQ/fastmcp/pull/403)\n\n### Other Changes 🦾\n\n* Add test for no prefix when importing by [@jlowin](https://github.com/jlowin) in [#404](https://github.com/PrefectHQ/fastmcp/pull/404)\n\n**Full Changelog**: [v2.3.2...v2.3.3](https://github.com/PrefectHQ/fastmcp/compare/v2.3.2...v2.3.3)\n</Update>\n\n<Update label=\"v2.3.2\" description=\"2024-05-10\">\n\n## [v2.3.2: Stuck in the Middleware With You](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.2)\n\n### New Features 🎉\n\n* Allow users to pass middleware to starlette app constructors by [@jlowin](https://github.com/jlowin) in [#398](https://github.com/PrefectHQ/fastmcp/pull/398)\n* Deprecate transport-specific methods on FastMCP server by [@jlowin](https://github.com/jlowin) in [#401](https://github.com/PrefectHQ/fastmcp/pull/401)\n\n### Docs 📚\n\n* Update CLI docs by [@jlowin](https://github.com/jlowin) in [#402](https://github.com/PrefectHQ/fastmcp/pull/402)\n\n### Other Changes 🦾\n\n* Adding 23 tests for CLI by [@didier-durand](https://github.com/didier-durand) in [#394](https://github.com/PrefectHQ/fastmcp/pull/394)\n\n**Full Changelog**: [v2.3.1...v2.3.2](https://github.com/PrefectHQ/fastmcp/compare/v2.3.1...v2.3.2)\n</Update>\n\n<Update label=\"v2.3.1\" description=\"2024-05-09\">\n\n## [v2.3.1: For Good-nests Sake](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.1)\n\nThis release primarily patches a long-standing bug with nested ASGI SSE servers.\n\n### Fixes 🐞\n\n* Fix tool result serialization when the tool returns a list by [@strawgate](https://github.com/strawgate) in [#379](https://github.com/PrefectHQ/fastmcp/pull/379)\n* Ensure FastMCP handles nested SSE and SHTTP apps properly in ASGI frameworks by [@jlowin](https://github.com/jlowin) in [#390](https://github.com/PrefectHQ/fastmcp/pull/390)\n\n### Docs 📚\n\n* Update transport docs by [@jlowin](https://github.com/jlowin) in [#377](https://github.com/PrefectHQ/fastmcp/pull/377)\n* Add llms.txt to docs by [@jlowin](https://github.com/jlowin) in [#384](https://github.com/PrefectHQ/fastmcp/pull/384)\n* Fixing various text typos by [@didier-durand](https://github.com/didier-durand) in [#385](https://github.com/PrefectHQ/fastmcp/pull/385)\n\n### Other Changes 🦾\n\n* Adding a few tests to Image type by [@didier-durand](https://github.com/didier-durand) in [#387](https://github.com/PrefectHQ/fastmcp/pull/387)\n* Adding tests for TimedCache by [@didier-durand](https://github.com/didier-durand) in [#388](https://github.com/PrefectHQ/fastmcp/pull/388)\n\n### New Contributors\n\n* [@didier-durand](https://github.com/didier-durand) made their first contribution in [#385](https://github.com/PrefectHQ/fastmcp/pull/385)\n\n**Full Changelog**: [v2.3.0...v2.3.1](https://github.com/PrefectHQ/fastmcp/compare/v2.3.0...v2.3.1)\n</Update>\n\n<Update label=\"v2.3.0\" description=\"2024-05-08\">\n\n## [v2.3.0: Stream Me Up, Scotty](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.0)\n\n### New Features 🎉\n\n* Add streaming support for HTTP transport by [@jlowin](https://github.com/jlowin) in [#365](https://github.com/PrefectHQ/fastmcp/pull/365)\n* Support streaming HTTP transport in clients by [@jlowin](https://github.com/jlowin) in [#366](https://github.com/PrefectHQ/fastmcp/pull/366)\n* Add streaming support to CLI by [@jlowin](https://github.com/jlowin) in [#367](https://github.com/PrefectHQ/fastmcp/pull/367)\n\n### Fixes 🐞\n\n* Fix streaming transport initialization by [@jlowin](https://github.com/jlowin) in [#368](https://github.com/PrefectHQ/fastmcp/pull/368)\n\n### Docs 📚\n\n* Update transport documentation for streaming support by [@jlowin](https://github.com/jlowin) in [#369](https://github.com/PrefectHQ/fastmcp/pull/369)\n\n**Full Changelog**: [v2.2.10...v2.3.0](https://github.com/PrefectHQ/fastmcp/compare/v2.2.10...v2.3.0)\n</Update>\n\n<Update label=\"v2.2.10\" description=\"2024-05-06\">\n\n## [v2.2.10: That's JSON Bourne](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.10)\n\n### Fixes 🐞\n\n* Disable automatic JSON parsing of tool args by [@jlowin](https://github.com/jlowin) in [#341](https://github.com/PrefectHQ/fastmcp/pull/341)\n* Fix prompt test by [@jlowin](https://github.com/jlowin) in [#342](https://github.com/PrefectHQ/fastmcp/pull/342)\n\n### Other Changes 🦾\n\n* Update docs.json by [@jlowin](https://github.com/jlowin) in [#338](https://github.com/PrefectHQ/fastmcp/pull/338)\n* Add test coverage + tests on 4 examples by [@alainivars](https://github.com/alainivars) in [#306](https://github.com/PrefectHQ/fastmcp/pull/306)\n\n### New Contributors\n\n* [@alainivars](https://github.com/alainivars) made their first contribution in [#306](https://github.com/PrefectHQ/fastmcp/pull/306)\n\n**Full Changelog**: [v2.2.9...v2.2.10](https://github.com/PrefectHQ/fastmcp/compare/v2.2.9...v2.2.10)\n</Update>\n\n<Update label=\"v2.2.9\" description=\"2024-05-06\">\n\n## [v2.2.9: Str-ing the Pot (Hotfix)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.9)\n\nThis release is a hotfix for the issue detailed in #330\n\n### Fixes 🐞\n\n* Prevent invalid resource URIs by [@jlowin](https://github.com/jlowin) in [#336](https://github.com/PrefectHQ/fastmcp/pull/336)\n* Coerce numbers to str by [@jlowin](https://github.com/jlowin) in [#337](https://github.com/PrefectHQ/fastmcp/pull/337)\n\n### Docs 📚\n\n* Add client badge by [@jlowin](https://github.com/jlowin) in [#327](https://github.com/PrefectHQ/fastmcp/pull/327)\n* Update bug.yml by [@jlowin](https://github.com/jlowin) in [#328](https://github.com/PrefectHQ/fastmcp/pull/328)\n\n### Other Changes 🦾\n\n* Update quickstart.mdx example to include import by [@discdiver](https://github.com/discdiver) in [#329](https://github.com/PrefectHQ/fastmcp/pull/329)\n\n### New Contributors\n\n* [@discdiver](https://github.com/discdiver) made their first contribution in [#329](https://github.com/PrefectHQ/fastmcp/pull/329)\n\n**Full Changelog**: [v2.2.8...v2.2.9](https://github.com/PrefectHQ/fastmcp/compare/v2.2.8...v2.2.9)\n</Update>\n\n<Update label=\"v2.2.8\" description=\"2024-05-05\">\n\n## [v2.2.8: Parse and Recreation](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.8)\n\n### New Features 🎉\n\n* Replace custom parsing with TypeAdapter by [@jlowin](https://github.com/jlowin) in [#314](https://github.com/PrefectHQ/fastmcp/pull/314)\n* Handle \\*args/\\*\\*kwargs appropriately for various components by [@jlowin](https://github.com/jlowin) in [#317](https://github.com/PrefectHQ/fastmcp/pull/317)\n* Add timeout-graceful-shutdown as a default config for SSE app by [@jlowin](https://github.com/jlowin) in [#323](https://github.com/PrefectHQ/fastmcp/pull/323)\n* Ensure prompts return descriptions by [@jlowin](https://github.com/jlowin) in [#325](https://github.com/PrefectHQ/fastmcp/pull/325)\n\n### Fixes 🐞\n\n* Ensure that tool serialization has a graceful fallback by [@jlowin](https://github.com/jlowin) in [#310](https://github.com/PrefectHQ/fastmcp/pull/310)\n\n### Docs 📚\n\n* Update docs for clarity by [@jlowin](https://github.com/jlowin) in [#312](https://github.com/PrefectHQ/fastmcp/pull/312)\n\n### Other Changes 🦾\n\n* Remove is\\_async attribute by [@jlowin](https://github.com/jlowin) in [#315](https://github.com/PrefectHQ/fastmcp/pull/315)\n* Dry out retrieving context kwarg by [@jlowin](https://github.com/jlowin) in [#316](https://github.com/PrefectHQ/fastmcp/pull/316)\n\n**Full Changelog**: [v2.2.7...v2.2.8](https://github.com/PrefectHQ/fastmcp/compare/v2.2.7...v2.2.8)\n</Update>\n\n<Update label=\"v2.2.7\" description=\"2024-05-03\">\n\n## [v2.2.7: You Auth to Know Better](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.7)\n\n### New Features 🎉\n\n* use pydantic\\_core.to\\_json by [@jlowin](https://github.com/jlowin) in [#290](https://github.com/PrefectHQ/fastmcp/pull/290)\n* Ensure openapi descriptions are included in tool details by [@jlowin](https://github.com/jlowin) in [#293](https://github.com/PrefectHQ/fastmcp/pull/293)\n* Bump mcp to 1.7.1 by [@jlowin](https://github.com/jlowin) in [#298](https://github.com/PrefectHQ/fastmcp/pull/298)\n* Add support for tool annotations by [@jlowin](https://github.com/jlowin) in [#299](https://github.com/PrefectHQ/fastmcp/pull/299)\n* Add auth support by [@jlowin](https://github.com/jlowin) in [#300](https://github.com/PrefectHQ/fastmcp/pull/300)\n* Add low-level methods to client by [@jlowin](https://github.com/jlowin) in [#301](https://github.com/PrefectHQ/fastmcp/pull/301)\n* Add method for retrieving current starlette request to FastMCP context by [@jlowin](https://github.com/jlowin) in [#302](https://github.com/PrefectHQ/fastmcp/pull/302)\n* get\\_starlette\\_request → get\\_http\\_request by [@jlowin](https://github.com/jlowin) in [#303](https://github.com/PrefectHQ/fastmcp/pull/303)\n* Support custom Serializer for Tools by [@strawgate](https://github.com/strawgate) in [#308](https://github.com/PrefectHQ/fastmcp/pull/308)\n* Support proxy mount by [@jlowin](https://github.com/jlowin) in [#309](https://github.com/PrefectHQ/fastmcp/pull/309)\n\n### Other Changes 🦾\n\n* Improve context injection type checks by [@jlowin](https://github.com/jlowin) in [#291](https://github.com/PrefectHQ/fastmcp/pull/291)\n* add readme to smarthome example by [@zzstoatzz](https://github.com/zzstoatzz) in [#294](https://github.com/PrefectHQ/fastmcp/pull/294)\n\n**Full Changelog**: [v2.2.6...v2.2.7](https://github.com/PrefectHQ/fastmcp/compare/v2.2.6...v2.2.7)\n</Update>\n\n<Update label=\"v2.2.6\" description=\"2024-04-30\">\n\n## [v2.2.6: The REST is History](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.6)\n\n### New Features 🎉\n\n* Added feature : Load MCP server using config by [@sandipan1](https://github.com/sandipan1) in [#260](https://github.com/PrefectHQ/fastmcp/pull/260)\n* small typing fixes by [@zzstoatzz](https://github.com/zzstoatzz) in [#237](https://github.com/PrefectHQ/fastmcp/pull/237)\n* Expose configurable timeout for OpenAPI by [@jlowin](https://github.com/jlowin) in [#279](https://github.com/PrefectHQ/fastmcp/pull/279)\n* Lower websockets pin for compatibility by [@jlowin](https://github.com/jlowin) in [#286](https://github.com/PrefectHQ/fastmcp/pull/286)\n* Improve OpenAPI param handling by [@jlowin](https://github.com/jlowin) in [#287](https://github.com/PrefectHQ/fastmcp/pull/287)\n\n### Fixes 🐞\n\n* Ensure openapi tool responses are properly converted by [@jlowin](https://github.com/jlowin) in [#283](https://github.com/PrefectHQ/fastmcp/pull/283)\n* Fix OpenAPI examples by [@jlowin](https://github.com/jlowin) in [#285](https://github.com/PrefectHQ/fastmcp/pull/285)\n* Fix client docs for advanced features, add tests for logging by [@jlowin](https://github.com/jlowin) in [#284](https://github.com/PrefectHQ/fastmcp/pull/284)\n\n### Other Changes 🦾\n\n* add testing doc by [@jlowin](https://github.com/jlowin) in [#264](https://github.com/PrefectHQ/fastmcp/pull/264)\n* #267 Fix openapi template resource to support multiple path parameters by [@jeger-at](https://github.com/jeger-at) in [#278](https://github.com/PrefectHQ/fastmcp/pull/278)\n\n### New Contributors\n\n* [@sandipan1](https://github.com/sandipan1) made their first contribution in [#260](https://github.com/PrefectHQ/fastmcp/pull/260)\n* [@jeger-at](https://github.com/jeger-at) made their first contribution in [#278](https://github.com/PrefectHQ/fastmcp/pull/278)\n\n**Full Changelog**: [v2.2.5...v2.2.6](https://github.com/PrefectHQ/fastmcp/compare/v2.2.5...v2.2.6)\n</Update>\n\n<Update label=\"v2.2.5\" description=\"2024-04-26\">\n\n## [v2.2.5: Context Switching](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.5)\n\n### New Features 🎉\n\n* Add tests for tool return types; improve serialization behavior by [@jlowin](https://github.com/jlowin) in [#262](https://github.com/PrefectHQ/fastmcp/pull/262)\n* Support context injection in resources, templates, and prompts (like tools) by [@jlowin](https://github.com/jlowin) in [#263](https://github.com/PrefectHQ/fastmcp/pull/263)\n\n### Docs 📚\n\n* Update wildcards to 2.2.4 by [@jlowin](https://github.com/jlowin) in [#257](https://github.com/PrefectHQ/fastmcp/pull/257)\n* Update note in templates docs by [@jlowin](https://github.com/jlowin) in [#258](https://github.com/PrefectHQ/fastmcp/pull/258)\n* Significant documentation and test expansion for tool input types by [@jlowin](https://github.com/jlowin) in [#261](https://github.com/PrefectHQ/fastmcp/pull/261)\n\n**Full Changelog**: [v2.2.4...v2.2.5](https://github.com/PrefectHQ/fastmcp/compare/v2.2.4...v2.2.5)\n</Update>\n\n<Update label=\"v2.2.4\" description=\"2024-04-25\">\n\n## [v2.2.4: The Wild Side, Actually](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.4)\n\nThe wildcard URI templates exposed in v2.2.3 were blocked by a server-level check which is removed in this release.\n\n### New Features 🎉\n\n* Allow customization of inspector proxy port, ui port, and version by [@jlowin](https://github.com/jlowin) in [#253](https://github.com/PrefectHQ/fastmcp/pull/253)\n\n### Fixes 🐞\n\n* fix: unintended type convert by [@cutekibry](https://github.com/cutekibry) in [#252](https://github.com/PrefectHQ/fastmcp/pull/252)\n* Ensure openapi resources return valid responses by [@jlowin](https://github.com/jlowin) in [#254](https://github.com/PrefectHQ/fastmcp/pull/254)\n* Ensure servers expose template wildcards by [@jlowin](https://github.com/jlowin) in [#256](https://github.com/PrefectHQ/fastmcp/pull/256)\n\n### Docs 📚\n\n* Update README.md Grammar error by [@TechWithTy](https://github.com/TechWithTy) in [#249](https://github.com/PrefectHQ/fastmcp/pull/249)\n\n### Other Changes 🦾\n\n* Add resource template tests by [@jlowin](https://github.com/jlowin) in [#255](https://github.com/PrefectHQ/fastmcp/pull/255)\n\n### New Contributors\n\n* [@TechWithTy](https://github.com/TechWithTy) made their first contribution in [#249](https://github.com/PrefectHQ/fastmcp/pull/249)\n* [@cutekibry](https://github.com/cutekibry) made their first contribution in [#252](https://github.com/PrefectHQ/fastmcp/pull/252)\n\n**Full Changelog**: [v2.2.3...v2.2.4](https://github.com/PrefectHQ/fastmcp/compare/v2.2.3...v2.2.4)\n</Update>\n\n<Update label=\"v2.2.3\" description=\"2024-04-25\">\n\n## [v2.2.3: The Wild Side](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.3)\n\n### New Features 🎉\n\n* Add wildcard params for resource templates by [@jlowin](https://github.com/jlowin) in [#246](https://github.com/PrefectHQ/fastmcp/pull/246)\n\n### Docs 📚\n\n* Indicate that Image class is for returns by [@jlowin](https://github.com/jlowin) in [#242](https://github.com/PrefectHQ/fastmcp/pull/242)\n* Update mermaid diagram by [@jlowin](https://github.com/jlowin) in [#243](https://github.com/PrefectHQ/fastmcp/pull/243)\n\n### Other Changes 🦾\n\n* update version badges by [@jlowin](https://github.com/jlowin) in [#248](https://github.com/PrefectHQ/fastmcp/pull/248)\n\n**Full Changelog**: [v2.2.2...v2.2.3](https://github.com/PrefectHQ/fastmcp/compare/v2.2.2...v2.2.3)\n</Update>\n\n<Update label=\"v2.2.2\" description=\"2024-04-24\">\n\n## [v2.2.2: Prompt and Circumstance](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.2)\n\n### New Features 🎉\n\n* Add prompt support by [@jlowin](https://github.com/jlowin) in [#235](https://github.com/PrefectHQ/fastmcp/pull/235)\n\n### Fixes 🐞\n\n* Ensure that resource templates are properly exposed by [@jlowin](https://github.com/jlowin) in [#238](https://github.com/PrefectHQ/fastmcp/pull/238)\n\n### Docs 📚\n\n* Update docs for prompts by [@jlowin](https://github.com/jlowin) in [#236](https://github.com/PrefectHQ/fastmcp/pull/236)\n\n### Other Changes 🦾\n\n* Add prompt tests by [@jlowin](https://github.com/jlowin) in [#239](https://github.com/PrefectHQ/fastmcp/pull/239)\n\n**Full Changelog**: [v2.2.1...v2.2.2](https://github.com/PrefectHQ/fastmcp/compare/v2.2.1...v2.2.2)\n</Update>\n\n<Update label=\"v2.2.1\" description=\"2024-04-23\">\n\n## [v2.2.1: Template for Success](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.1)\n\n### New Features 🎉\n\n* Add resource templates by [@jlowin](https://github.com/jlowin) in [#230](https://github.com/PrefectHQ/fastmcp/pull/230)\n\n### Fixes 🐞\n\n* Ensure that resource templates are properly exposed by [@jlowin](https://github.com/jlowin) in [#231](https://github.com/PrefectHQ/fastmcp/pull/231)\n\n### Docs 📚\n\n* Update docs for resource templates by [@jlowin](https://github.com/jlowin) in [#232](https://github.com/PrefectHQ/fastmcp/pull/232)\n\n### Other Changes 🦾\n\n* Add resource template tests by [@jlowin](https://github.com/jlowin) in [#233](https://github.com/PrefectHQ/fastmcp/pull/233)\n\n**Full Changelog**: [v2.2.0...v2.2.1](https://github.com/PrefectHQ/fastmcp/compare/v2.2.0...v2.2.1)\n</Update>\n\n<Update label=\"v2.2.0\" description=\"2024-04-22\">\n\n## [v2.2.0: Compose Yourself](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.0)\n\n### New Features 🎉\n\n* Add support for mounting FastMCP servers by [@jlowin](https://github.com/jlowin) in [#175](https://github.com/PrefectHQ/fastmcp/pull/175)\n* Add support for duplicate behavior == ignore by [@jlowin](https://github.com/jlowin) in [#169](https://github.com/PrefectHQ/fastmcp/pull/169)\n\n### Breaking Changes 🛫\n\n* Refactor MCP composition by [@jlowin](https://github.com/jlowin) in [#176](https://github.com/PrefectHQ/fastmcp/pull/176)\n\n### Docs 📚\n\n* Improve integration documentation by [@jlowin](https://github.com/jlowin) in [#184](https://github.com/PrefectHQ/fastmcp/pull/184)\n* Improve documentation by [@jlowin](https://github.com/jlowin) in [#185](https://github.com/PrefectHQ/fastmcp/pull/185)\n\n### Other Changes 🦾\n\n* Add transport kwargs for mcp.run() and fastmcp run by [@jlowin](https://github.com/jlowin) in [#161](https://github.com/PrefectHQ/fastmcp/pull/161)\n* Allow resource templates to have optional / excluded arguments by [@jlowin](https://github.com/jlowin) in [#164](https://github.com/PrefectHQ/fastmcp/pull/164)\n* Update resources.mdx by [@jlowin](https://github.com/jlowin) in [#165](https://github.com/PrefectHQ/fastmcp/pull/165)\n\n### New Contributors\n\n* [@kongqi404](https://github.com/kongqi404) made their first contribution in [#181](https://github.com/PrefectHQ/fastmcp/pull/181)\n\n**Full Changelog**: [v2.1.2...v2.2.0](https://github.com/PrefectHQ/fastmcp/compare/v2.1.2...v2.2.0)\n</Update>\n\n<Update label=\"v2.1.2\" description=\"2024-04-14\">\n\n## [v2.1.2: Copy That, Good Buddy](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.2)\n\nThe main improvement in this release is a fix that allows FastAPI / OpenAPI-generated servers to be mounted as sub-servers.\n\n### Fixes 🐞\n\n* Ensure objects are copied properly and test mounting fastapi by [@jlowin](https://github.com/jlowin) in [#153](https://github.com/PrefectHQ/fastmcp/pull/153)\n\n### Docs 📚\n\n* Fix broken links in docs by [@jlowin](https://github.com/jlowin) in [#154](https://github.com/PrefectHQ/fastmcp/pull/154)\n\n### Other Changes 🦾\n\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#149](https://github.com/PrefectHQ/fastmcp/pull/149)\n* Only apply log config to FastMCP loggers by [@jlowin](https://github.com/jlowin) in [#155](https://github.com/PrefectHQ/fastmcp/pull/155)\n* Update pyproject.toml by [@jlowin](https://github.com/jlowin) in [#156](https://github.com/PrefectHQ/fastmcp/pull/156)\n\n**Full Changelog**: [v2.1.1...v2.1.2](https://github.com/PrefectHQ/fastmcp/compare/v2.1.1...v2.1.2)\n</Update>\n\n<Update label=\"v2.1.1\" description=\"2024-04-14\">\n\n## [v2.1.1: Doc Holiday](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.1)\n\nFastMCP's docs are now available at gofastmcp.com.\n\n### Docs 📚\n\n* Add docs by [@jlowin](https://github.com/jlowin) in [#136](https://github.com/PrefectHQ/fastmcp/pull/136)\n* Add docs link to readme by [@jlowin](https://github.com/jlowin) in [#137](https://github.com/PrefectHQ/fastmcp/pull/137)\n* Minor docs updates by [@jlowin](https://github.com/jlowin) in [#138](https://github.com/PrefectHQ/fastmcp/pull/138)\n\n### Fixes 🐞\n\n* fix branch name in example by [@zzstoatzz](https://github.com/zzstoatzz) in [#140](https://github.com/PrefectHQ/fastmcp/pull/140)\n\n### Other Changes 🦾\n\n* smart home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#115](https://github.com/PrefectHQ/fastmcp/pull/115)\n* Remove mac os tests by [@jlowin](https://github.com/jlowin) in [#142](https://github.com/PrefectHQ/fastmcp/pull/142)\n* Expand support for various method interactions by [@jlowin](https://github.com/jlowin) in [#143](https://github.com/PrefectHQ/fastmcp/pull/143)\n* Update docs and add\\_resource\\_fn by [@jlowin](https://github.com/jlowin) in [#144](https://github.com/PrefectHQ/fastmcp/pull/144)\n* Update description by [@jlowin](https://github.com/jlowin) in [#145](https://github.com/PrefectHQ/fastmcp/pull/145)\n* Support openapi 3.0 and 3.1 by [@jlowin](https://github.com/jlowin) in [#147](https://github.com/PrefectHQ/fastmcp/pull/147)\n\n**Full Changelog**: [v2.1.0...v2.1.1](https://github.com/PrefectHQ/fastmcp/compare/v2.1.0...v2.1.1)\n</Update>\n\n<Update label=\"v2.1.0\" description=\"2024-04-13\">\n\n## [v2.1.0: Tag, You're It](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.0)\n\nThe primary motivation for this release is the fix in #128 for Claude desktop compatibility, but the primary new feature of this release is per-object tags. Currently these are for bookkeeping only but will become useful in future releases.\n\n### New Features 🎉\n\n* Add tags for all core MCP objects by [@jlowin](https://github.com/jlowin) in [#121](https://github.com/PrefectHQ/fastmcp/pull/121)\n* Ensure that openapi tags are transferred to MCP objects by [@jlowin](https://github.com/jlowin) in [#124](https://github.com/PrefectHQ/fastmcp/pull/124)\n\n### Fixes 🐞\n\n* Change default mounted tool separator from / to \\_ by [@jlowin](https://github.com/jlowin) in [#128](https://github.com/PrefectHQ/fastmcp/pull/128)\n* Enter mounted app lifespans by [@jlowin](https://github.com/jlowin) in [#129](https://github.com/PrefectHQ/fastmcp/pull/129)\n* Fix CLI that called mcp instead of fastmcp by [@jlowin](https://github.com/jlowin) in [#128](https://github.com/PrefectHQ/fastmcp/pull/128)\n\n### Breaking Changes 🛫\n\n* Changed configuration for duplicate resources/tools/prompts by [@jlowin](https://github.com/jlowin) in [#121](https://github.com/PrefectHQ/fastmcp/pull/121)\n* Improve client return types by [@jlowin](https://github.com/jlowin) in [#123](https://github.com/PrefectHQ/fastmcp/pull/123)\n\n### Other Changes 🦾\n\n* Add tests for tags in server decorators by [@jlowin](https://github.com/jlowin) in [#122](https://github.com/PrefectHQ/fastmcp/pull/122)\n* Clean up server tests by [@jlowin](https://github.com/jlowin) in [#125](https://github.com/PrefectHQ/fastmcp/pull/125)\n\n**Full Changelog**: [v2.0.0...v2.1.0](https://github.com/PrefectHQ/fastmcp/compare/v2.0.0...v2.1.0)\n</Update>\n\n<Update label=\"v2.0.0\" description=\"2024-04-11\">\n\n## [v2.0.0: Second to None](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.0.0)\n\n### New Features 🎉\n\n* Support mounting FastMCP instances as sub-MCPs by [@jlowin](https://github.com/jlowin) in [#99](https://github.com/PrefectHQ/fastmcp/pull/99)\n* Add in-memory client for calling FastMCP servers (and tests) by [@jlowin](https://github.com/jlowin) in [#100](https://github.com/PrefectHQ/fastmcp/pull/100)\n* Add MCP proxy server by [@jlowin](https://github.com/jlowin) in [#105](https://github.com/PrefectHQ/fastmcp/pull/105)\n* Update FastMCP for upstream changes by [@jlowin](https://github.com/jlowin) in [#107](https://github.com/PrefectHQ/fastmcp/pull/107)\n* Generate FastMCP servers from OpenAPI specs and FastAPI by [@jlowin](https://github.com/jlowin) in [#110](https://github.com/PrefectHQ/fastmcp/pull/110)\n* Reorganize all client / transports by [@jlowin](https://github.com/jlowin) in [#111](https://github.com/PrefectHQ/fastmcp/pull/111)\n* Add sampling and roots by [@jlowin](https://github.com/jlowin) in [#117](https://github.com/PrefectHQ/fastmcp/pull/117)\n\n### Fixes 🐞\n\n* Fix bug with tools that return lists by [@jlowin](https://github.com/jlowin) in [#116](https://github.com/PrefectHQ/fastmcp/pull/116)\n\n### Other Changes 🦾\n\n* Add back FastMCP CLI by [@jlowin](https://github.com/jlowin) in [#108](https://github.com/PrefectHQ/fastmcp/pull/108)\n* Update Readme for v2 by [@jlowin](https://github.com/jlowin) in [#112](https://github.com/PrefectHQ/fastmcp/pull/112)\n* fix deprecation warnings by [@zzstoatzz](https://github.com/zzstoatzz) in [#113](https://github.com/PrefectHQ/fastmcp/pull/113)\n* Readme by [@jlowin](https://github.com/jlowin) in [#118](https://github.com/PrefectHQ/fastmcp/pull/118)\n* FastMCP 2.0 by [@jlowin](https://github.com/jlowin) in [#119](https://github.com/PrefectHQ/fastmcp/pull/119)\n\n**Full Changelog**: [v1.0...v2.0.0](https://github.com/PrefectHQ/fastmcp/compare/v1.0...v2.0.0)\n</Update>\n\n<Update label=\"v1.0\" description=\"2024-04-11\">\n\n## [v1.0: It's Official](https://github.com/PrefectHQ/fastmcp/releases/tag/v1.0)\n\nThis release commemorates FastMCP 1.0, which is included in the official Model Context Protocol SDK:\n\n```python\nfrom mcp.server.fastmcp import FastMCP\n```\n\nTo the best of my knowledge, v1 is identical to the upstream version included with `mcp`.\n\n### Docs 📚\n\n* Update readme to redirect to the official SDK by [@jlowin](https://github.com/jlowin) in [#79](https://github.com/PrefectHQ/fastmcp/pull/79)\n\n### Other Changes 🦾\n\n* fix: use Mount instead of Route for SSE message handling by [@samihamine](https://github.com/samihamine) in [#77](https://github.com/PrefectHQ/fastmcp/pull/77)\n\n### New Contributors\n\n* [@samihamine](https://github.com/samihamine) made their first contribution in [#77](https://github.com/PrefectHQ/fastmcp/pull/77)\n\n**Full Changelog**: [v0.4.1...v1.0](https://github.com/PrefectHQ/fastmcp/compare/v0.4.1...v1.0)\n</Update>\n\n<Update label=\"v0.4.1\" description=\"2024-12-09\">\n\n## [v0.4.1: String Theory](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.4.1)\n\n### Fixes 🐞\n\n* fix: handle strings containing numbers correctly by [@sd2k](https://github.com/sd2k) in [#63](https://github.com/PrefectHQ/fastmcp/pull/63)\n\n### Docs 📚\n\n* patch: Update pyproject.toml license by [@leonkozlowski](https://github.com/leonkozlowski) in [#67](https://github.com/PrefectHQ/fastmcp/pull/67)\n\n### Other Changes 🦾\n\n* Avoid new try\\_eval\\_type unavailable with older pydantic by [@jurasofish](https://github.com/jurasofish) in [#57](https://github.com/PrefectHQ/fastmcp/pull/57)\n* Decorator typing by [@jurasofish](https://github.com/jurasofish) in [#56](https://github.com/PrefectHQ/fastmcp/pull/56)\n\n### New Contributors\n\n* [@leonkozlowski](https://github.com/leonkozlowski) made their first contribution in [#67](https://github.com/PrefectHQ/fastmcp/pull/67)\n\n**Full Changelog**: [v0.4.0...v0.4.1](https://github.com/PrefectHQ/fastmcp/compare/v0.4.0...v0.4.1)\n</Update>\n\n<Update label=\"v0.4.0\" description=\"2024-12-05\">\n\n## [v0.4.0: Nice to MIT You](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.4.0)\n\nThis is a relatively small release in terms of features, but the version is bumped to 0.4 to reflect that the code is being relicensed from Apache 2.0 to MIT. This is to facilitate FastMCP's inclusion in the official MCP SDK.\n\n### New Features 🎉\n\n* Add pyright + tests by [@jlowin](https://github.com/jlowin) in [#52](https://github.com/PrefectHQ/fastmcp/pull/52)\n* add pgvector memory example by [@zzstoatzz](https://github.com/zzstoatzz) in [#49](https://github.com/PrefectHQ/fastmcp/pull/49)\n\n### Fixes 🐞\n\n* fix: use stderr for logging by [@sd2k](https://github.com/sd2k) in [#51](https://github.com/PrefectHQ/fastmcp/pull/51)\n\n### Docs 📚\n\n* Update ai-labeler.yml by [@jlowin](https://github.com/jlowin) in [#48](https://github.com/PrefectHQ/fastmcp/pull/48)\n* Relicense from Apache 2.0 to MIT by [@jlowin](https://github.com/jlowin) in [#54](https://github.com/PrefectHQ/fastmcp/pull/54)\n\n### Other Changes 🦾\n\n* fix warning and flake by [@zzstoatzz](https://github.com/zzstoatzz) in [#47](https://github.com/PrefectHQ/fastmcp/pull/47)\n\n### New Contributors\n\n* [@sd2k](https://github.com/sd2k) made their first contribution in [#51](https://github.com/PrefectHQ/fastmcp/pull/51)\n\n**Full Changelog**: [v0.3.5...v0.4.0](https://github.com/PrefectHQ/fastmcp/compare/v0.3.5...v0.4.0)\n</Update>\n\n<Update label=\"v0.3.5\" description=\"2024-12-03\">\n\n## [v0.3.5: Windows of Opportunity](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.5)\n\nThis release is highlighted by the ability to handle complex JSON objects as MCP inputs and improved Windows compatibility.\n\n### New Features 🎉\n\n* Set up multiple os tests by [@jlowin](https://github.com/jlowin) in [#44](https://github.com/PrefectHQ/fastmcp/pull/44)\n* Changes to accommodate windows users. by [@justjoehere](https://github.com/justjoehere) in [#42](https://github.com/PrefectHQ/fastmcp/pull/42)\n* Handle complex inputs by [@jurasofish](https://github.com/jurasofish) in [#31](https://github.com/PrefectHQ/fastmcp/pull/31)\n\n### Docs 📚\n\n* Make AI labeler more conservative by [@jlowin](https://github.com/jlowin) in [#46](https://github.com/PrefectHQ/fastmcp/pull/46)\n\n### Other Changes 🦾\n\n* Additional Windows Fixes for Dev running and for importing modules in a server by [@justjoehere](https://github.com/justjoehere) in [#43](https://github.com/PrefectHQ/fastmcp/pull/43)\n\n### New Contributors\n\n* [@justjoehere](https://github.com/justjoehere) made their first contribution in [#42](https://github.com/PrefectHQ/fastmcp/pull/42)\n* [@jurasofish](https://github.com/jurasofish) made their first contribution in [#31](https://github.com/PrefectHQ/fastmcp/pull/31)\n\n**Full Changelog**: [v0.3.4...v0.3.5](https://github.com/PrefectHQ/fastmcp/compare/v0.3.4...v0.3.5)\n</Update>\n\n<Update label=\"v0.3.4\" description=\"2024-12-02\">\n\n## [v0.3.4: URL's Well That Ends Well](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.4)\n\n### Fixes 🐞\n\n* Handle missing config file when installing by [@jlowin](https://github.com/jlowin) in [#37](https://github.com/PrefectHQ/fastmcp/pull/37)\n* Remove BaseURL reference and use AnyURL by [@jlowin](https://github.com/jlowin) in [#40](https://github.com/PrefectHQ/fastmcp/pull/40)\n\n**Full Changelog**: [v0.3.3...v0.3.4](https://github.com/PrefectHQ/fastmcp/compare/v0.3.3...v0.3.4)\n</Update>\n\n<Update label=\"v0.3.3\" description=\"2024-12-02\">\n\n## [v0.3.3: Dependence Day](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.3)\n\n### New Features 🎉\n\n* Surge example by [@zzstoatzz](https://github.com/zzstoatzz) in [#29](https://github.com/PrefectHQ/fastmcp/pull/29)\n* Support Python dependencies in Server by [@jlowin](https://github.com/jlowin) in [#34](https://github.com/PrefectHQ/fastmcp/pull/34)\n\n### Docs 📚\n\n* add `Contributing` section to README by [@zzstoatzz](https://github.com/zzstoatzz) in [#32](https://github.com/PrefectHQ/fastmcp/pull/32)\n\n**Full Changelog**: [v0.3.2...v0.3.3](https://github.com/PrefectHQ/fastmcp/compare/v0.3.2...v0.3.3)\n</Update>\n\n<Update label=\"v0.3.2\" date=\"2024-12-01\" description=\"Green with ENVy\">\n\n## [v0.3.2: Green with ENVy](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.2)\n\n### New Features 🎉\n\n* Support env vars when installing by [@jlowin](https://github.com/jlowin) in [#27](https://github.com/PrefectHQ/fastmcp/pull/27)\n\n### Docs 📚\n\n* Remove top level env var by [@jlowin](https://github.com/jlowin) in [#28](https://github.com/PrefectHQ/fastmcp/pull/28)\n\n**Full Changelog**: [v0.3.1...v0.3.2](https://github.com/PrefectHQ/fastmcp/compare/v0.3.1...v0.3.2)\n</Update>\n\n<Update label=\"v0.3.1\" description=\"2024-12-01\">\n\n## [v0.3.1](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.1)\n\n### New Features 🎉\n\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#23](https://github.com/PrefectHQ/fastmcp/pull/23)\n* add rich handler and dotenv loading for settings by [@zzstoatzz](https://github.com/zzstoatzz) in [#22](https://github.com/PrefectHQ/fastmcp/pull/22)\n* print exception when server can't start by [@jlowin](https://github.com/jlowin) in [#25](https://github.com/PrefectHQ/fastmcp/pull/25)\n\n### Docs 📚\n\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#24](https://github.com/PrefectHQ/fastmcp/pull/24)\n\n### Other Changes 🦾\n\n* Remove log by [@jlowin](https://github.com/jlowin) in [#26](https://github.com/PrefectHQ/fastmcp/pull/26)\n\n**Full Changelog**: [v0.3.0...v0.3.1](https://github.com/PrefectHQ/fastmcp/compare/v0.3.0...v0.3.1)\n</Update>\n\n<Update label=\"v0.3.0\" description=\"2024-12-01\">\n\n## [v0.3.0: Prompt and Circumstance](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.0)\n\n### New Features 🎉\n\n* Update README by [@jlowin](https://github.com/jlowin) in [#3](https://github.com/PrefectHQ/fastmcp/pull/3)\n* Make log levels strings by [@jlowin](https://github.com/jlowin) in [#4](https://github.com/PrefectHQ/fastmcp/pull/4)\n* Make content method a function by [@jlowin](https://github.com/jlowin) in [#5](https://github.com/PrefectHQ/fastmcp/pull/5)\n* Add template support by [@jlowin](https://github.com/jlowin) in [#6](https://github.com/PrefectHQ/fastmcp/pull/6)\n* Refactor resources module by [@jlowin](https://github.com/jlowin) in [#7](https://github.com/PrefectHQ/fastmcp/pull/7)\n* Clean up cli imports by [@jlowin](https://github.com/jlowin) in [#8](https://github.com/PrefectHQ/fastmcp/pull/8)\n* Prepare to list templates by [@jlowin](https://github.com/jlowin) in [#11](https://github.com/PrefectHQ/fastmcp/pull/11)\n* Move image to separate module by [@jlowin](https://github.com/jlowin) in [#9](https://github.com/PrefectHQ/fastmcp/pull/9)\n* Add support for request context, progress, logging, etc. by [@jlowin](https://github.com/jlowin) in [#12](https://github.com/PrefectHQ/fastmcp/pull/12)\n* Add context tests and better runtime loads by [@jlowin](https://github.com/jlowin) in [#13](https://github.com/PrefectHQ/fastmcp/pull/13)\n* Refactor tools + resourcemanager by [@jlowin](https://github.com/jlowin) in [#14](https://github.com/PrefectHQ/fastmcp/pull/14)\n* func → fn everywhere by [@jlowin](https://github.com/jlowin) in [#15](https://github.com/PrefectHQ/fastmcp/pull/15)\n* Add support for prompts by [@jlowin](https://github.com/jlowin) in [#16](https://github.com/PrefectHQ/fastmcp/pull/16)\n* Create LICENSE by [@jlowin](https://github.com/jlowin) in [#18](https://github.com/PrefectHQ/fastmcp/pull/18)\n* Update cli file spec by [@jlowin](https://github.com/jlowin) in [#19](https://github.com/PrefectHQ/fastmcp/pull/19)\n* Update readmeUpdate README by [@jlowin](https://github.com/jlowin) in [#20](https://github.com/PrefectHQ/fastmcp/pull/20)\n* Use hatchling for version by [@jlowin](https://github.com/jlowin) in [#21](https://github.com/PrefectHQ/fastmcp/pull/21)\n\n### Other Changes 🦾\n\n* Add echo server by [@jlowin](https://github.com/jlowin) in [#1](https://github.com/PrefectHQ/fastmcp/pull/1)\n* Add github workflows by [@jlowin](https://github.com/jlowin) in [#2](https://github.com/PrefectHQ/fastmcp/pull/2)\n* typing updates by [@zzstoatzz](https://github.com/zzstoatzz) in [#17](https://github.com/PrefectHQ/fastmcp/pull/17)\n\n### New Contributors\n\n* [@jlowin](https://github.com/jlowin) made their first contribution in [#1](https://github.com/PrefectHQ/fastmcp/pull/1)\n* [@zzstoatzz](https://github.com/zzstoatzz) made their first contribution in [#17](https://github.com/PrefectHQ/fastmcp/pull/17)\n\n**Full Changelog**: [v0.2.0...v0.3.0](https://github.com/PrefectHQ/fastmcp/compare/v0.2.0...v0.3.0)\n</Update>\n\n<Update label=\"v0.2.0\" description=\"2024-11-30\">\n\n## [v0.2.0](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.2.0)\n\n**Full Changelog**: [v0.1.0...v0.2.0](https://github.com/PrefectHQ/fastmcp/compare/v0.1.0...v0.2.0)\n</Update>\n\n<Update label=\"v0.1.0\" description=\"2024-11-30\">\n\n## [v0.1.0](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.1.0)\n\nThe very first release of FastMCP! 🎉\n\n**Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0)\n</Update>"
  },
  {
    "path": "docs/cli/auth.mdx",
    "content": "---\ntitle: Auth Utilities\nsidebarTitle: Auth\ndescription: Create and validate CIMD documents for OAuth\nicon: key\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nThe `fastmcp auth` commands help with CIMD (Client ID Metadata Document) management — part of MCP's OAuth authentication flow. A CIMD is a JSON document you host at an HTTPS URL to identify your client application to MCP servers.\n\n## Creating a CIMD\n\n`fastmcp auth cimd create` generates a CIMD document:\n\n```bash\nfastmcp auth cimd create \\\n  --name \"My App\" \\\n  --redirect-uri \"http://localhost:*/callback\"\n```\n\n```json\n{\n  \"client_id\": \"https://your-domain.com/oauth/client.json\",\n  \"client_name\": \"My App\",\n  \"redirect_uris\": [\"http://localhost:*/callback\"],\n  \"token_endpoint_auth_method\": \"none\"\n}\n```\n\nThe generated document includes a placeholder `client_id` — update it to match the URL where you'll host the document before deploying.\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Name | `--name` | **Required.** Human-readable client name |\n| Redirect URI | `--redirect-uri` | **Required.** Allowed redirect URIs (repeatable) |\n| Client URI | `--client-uri` | Client's home page URL |\n| Logo URI | `--logo-uri` | Client's logo URL |\n| Scope | `--scope` | Space-separated list of scopes |\n| Output | `--output`, `-o` | Save to file (default: stdout) |\n| Pretty | `--pretty` | Pretty-print JSON (default: true) |\n\n### Example\n\n```bash\nfastmcp auth cimd create \\\n  --name \"My Production App\" \\\n  --redirect-uri \"http://localhost:*/callback\" \\\n  --redirect-uri \"https://myapp.example.com/callback\" \\\n  --client-uri \"https://myapp.example.com\" \\\n  --scope \"read write\" \\\n  --output client.json\n```\n\n## Validating a CIMD\n\n`fastmcp auth cimd validate` fetches a hosted CIMD and verifies it conforms to the spec:\n\n```bash\nfastmcp auth cimd validate https://myapp.example.com/oauth/client.json\n```\n\nThe validator checks that the URL is valid (HTTPS, non-root path), the document is valid JSON, the `client_id` matches the URL, and no shared-secret auth methods are used.\n\nOn success:\n\n```\n→ Fetching https://myapp.example.com/oauth/client.json...\n✓ Valid CIMD document\n\nDocument details:\n  client_id: https://myapp.example.com/oauth/client.json\n  client_name: My App\n  token_endpoint_auth_method: none\n  redirect_uris:\n    • http://localhost:*/callback\n```\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Timeout | `--timeout`, `-t` | HTTP request timeout in seconds (default: 10) |\n"
  },
  {
    "path": "docs/cli/client.mdx",
    "content": "---\ntitle: Client Commands\nsidebarTitle: Client\ndescription: List tools, call them, and discover configured servers\nicon: satellite-dish\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nThe CLI can act as an MCP client — connecting to any server (local or remote) to list what it exposes and call its tools directly. This is useful for development, debugging, scripting, and giving shell-capable LLM agents access to MCP servers.\n\n## Listing Tools\n\n`fastmcp list` connects to a server and prints its tools as function signatures, showing parameter names, types, and descriptions at a glance:\n\n```bash\nfastmcp list http://localhost:8000/mcp\nfastmcp list server.py\nfastmcp list weather  # name-based resolution\n```\n\nWhen you need the full JSON Schema for a tool's inputs or outputs — for understanding nested objects, enum constraints, or complex types — opt in with `--input-schema` or `--output-schema`:\n\n```bash\nfastmcp list server.py --input-schema\n```\n\n### Resources and Prompts\n\nBy default, only tools are shown. Add `--resources` or `--prompts` to include those:\n\n```bash\nfastmcp list server.py --resources --prompts\n```\n\n### Machine-Readable Output\n\nThe `--json` flag switches to structured JSON with full schemas included. This is the format to use when feeding tool definitions to an LLM or building automation:\n\n```bash\nfastmcp list server.py --json\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Command | `--command` | Connect via stdio (e.g., `'npx -y @mcp/server'`) |\n| Transport | `--transport`, `-t` | Force `http` or `sse` for URL targets |\n| Resources | `--resources` | Include resources in output |\n| Prompts | `--prompts` | Include prompts in output |\n| Input Schema | `--input-schema` | Show full input schemas |\n| Output Schema | `--output-schema` | Show full output schemas |\n| JSON | `--json` | Structured JSON output |\n| Timeout | `--timeout` | Connection timeout in seconds |\n| Auth | `--auth` | `oauth` (default for HTTP), a bearer token, or `none` |\n\n## Calling Tools\n\n`fastmcp call` invokes a single tool on a server. Pass arguments as `key=value` pairs — the CLI fetches the tool's schema and coerces your string values to the right types automatically:\n\n```bash\nfastmcp call server.py greet name=World\nfastmcp call http://localhost:8000/mcp search query=hello limit=5\n```\n\nType coercion is schema-driven: `\"5\"` becomes the integer `5` when the schema expects an integer. Booleans accept `true`/`false`, `yes`/`no`, and `1`/`0`. Arrays and objects are parsed as JSON.\n\n### Complex Arguments\n\nFor tools with nested or structured parameters, `key=value` syntax gets awkward. Pass a single JSON object instead:\n\n```bash\nfastmcp call server.py create_item '{\"name\": \"Widget\", \"tags\": [\"sale\"], \"metadata\": {\"color\": \"blue\"}}'\n```\n\nOr use `--input-json` to provide a base dictionary, then override individual keys with `key=value` pairs:\n\n```bash\nfastmcp call server.py search --input-json '{\"query\": \"hello\", \"limit\": 5}' limit=10\n```\n\n### Error Handling\n\nIf you misspell a tool name, the CLI suggests corrections via fuzzy matching. Missing required arguments produce a clear message with the tool's signature as a reminder. Tool execution errors are printed with a non-zero exit code, making the CLI straightforward to use in scripts.\n\n### Structured Output\n\n`--json` emits the raw result including content blocks, error status, and structured content:\n\n```bash\nfastmcp call server.py get_weather city=London --json\n```\n\n### Interactive Elicitation\n\nSome tools request additional input during execution through MCP's elicitation mechanism. When this happens, the CLI prompts you in the terminal — showing each field's name, type, and whether it's required. You can type `decline` to skip a question or `cancel` to abort the call entirely.\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Command | `--command` | Connect via stdio |\n| Transport | `--transport`, `-t` | Force `http` or `sse` |\n| Input JSON | `--input-json` | Base arguments as JSON (merged with `key=value`) |\n| JSON | `--json` | Raw JSON output |\n| Timeout | `--timeout` | Connection timeout in seconds |\n| Auth | `--auth` | `oauth`, a bearer token, or `none` |\n\n## Discovering Configured Servers\n\n`fastmcp discover` scans your machine for MCP servers configured in editors and tools. It checks:\n\n- **Claude Desktop** — `claude_desktop_config.json`\n- **Claude Code** — `~/.claude.json`\n- **Cursor** — `.cursor/mcp.json` (walks up from current directory)\n- **Gemini CLI** — `~/.gemini/settings.json`\n- **Goose** — `~/.config/goose/config.yaml`\n- **Project** — `./mcp.json` in the current directory\n\n```bash\nfastmcp discover\n```\n\nThe output groups servers by source, showing each server's name and transport. Filter by source or get machine-readable output:\n\n```bash\nfastmcp discover --source claude-code\nfastmcp discover --source cursor --source gemini --json\n```\n\nAny server that appears here can be used by name with `list`, `call`, and other commands — so you can go from \"I have a server in Claude Code\" to querying it without copying URLs or paths.\n\n## LLM Agent Integration\n\nFor LLM agents that can execute shell commands but don't have native MCP support, the CLI provides a clean bridge. The agent calls `fastmcp list --json` to discover available tools with full schemas, then `fastmcp call --json` to invoke them with structured results.\n\nBecause the CLI handles connection management, transport selection, and type coercion internally, the agent doesn't need to understand MCP protocol details — it just reads JSON and constructs shell commands.\n"
  },
  {
    "path": "docs/cli/generate-cli.mdx",
    "content": "---\ntitle: Generate CLI\nsidebarTitle: Generate CLI\ndescription: Scaffold a standalone typed CLI from any MCP server\nicon: wand-magic-sparkles\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\n`fastmcp list` and `fastmcp call` are general-purpose — you always specify the server, the tool name, and the arguments from scratch. `fastmcp generate-cli` goes further: it connects to a server, reads its tool schemas, and writes a standalone Python script where every tool is a proper subcommand with typed flags, help text, and tab completion. The result is a CLI that feels hand-written for that specific server.\n\nMCP tool schemas already contain everything a CLI framework needs — parameter names, types, descriptions, required/optional status, and defaults. `generate-cli` maps that into [cyclopts](https://cyclopts.readthedocs.io/) commands, so JSON Schema types become Python type annotations, descriptions become `--help` text, and required parameters become mandatory flags.\n\n## Generating a Script\n\nPoint the command at any [server target](/cli/overview#server-targets) and it writes a CLI script:\n\n```bash\nfastmcp generate-cli weather\nfastmcp generate-cli http://localhost:8000/mcp\nfastmcp generate-cli server.py my_weather_cli.py\n```\n\nThe second positional argument sets the output path (defaults to `cli.py`). If the file already exists, pass `-f` to overwrite:\n\n```bash\nfastmcp generate-cli weather -f\n```\n\n## What You Get\n\nThe generated script is a regular Python file — executable, editable, and yours:\n\n```\n$ python cli.py call-tool --help\nUsage: weather-cli call-tool COMMAND\n\nCall a tool on the server\n\nCommands:\n  get_forecast  Get the weather forecast for a city.\n  search_city   Search for a city by name.\n```\n\nEach tool has typed parameters with help text pulled directly from the server's schema:\n\n```\n$ python cli.py call-tool get_forecast --help\nUsage: weather-cli call-tool get_forecast [OPTIONS]\n\nGet the weather forecast for a city.\n\nOptions:\n  --city    [str]  City name (required)\n  --days    [int]  Number of forecast days (default: 3)\n```\n\nBeyond tool commands, the script includes generic MCP operations — `list-tools`, `list-resources`, `read-resource`, `list-prompts`, and `get-prompt` — that always reflect the server's current state, even if tools have changed since generation.\n\n## Parameter Handling\n\nParameters are mapped based on their JSON Schema type:\n\n**Simple types** (`string`, `integer`, `number`, `boolean`) become typed flags:\n\n```bash\npython cli.py call-tool get_forecast --city London --days 3\n```\n\n**Arrays of simple types** become repeatable flags:\n\n```bash\npython cli.py call-tool tag_items --tags python --tags fastapi --tags mcp\n```\n\n**Complex types** (objects, nested arrays, unions) accept JSON strings. The `--help` output shows the full schema so you know what structure to pass:\n\n```bash\npython cli.py call-tool create_user \\\n  --name John \\\n  --metadata '{\"role\": \"admin\", \"dept\": \"engineering\"}'\n```\n\n## Agent Skill\n\nAlongside the CLI script, `generate-cli` writes a `SKILL.md` file — a [Claude Code agent skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) that documents every tool's exact invocation syntax, parameter flags, types, and descriptions. An agent can pick up the CLI immediately without running `--help` or experimenting with flag names.\n\nTo skip skill generation:\n\n```bash\nfastmcp generate-cli weather --no-skill\n```\n\n## How It Works\n\nThe generated script is a *client*, not a server — it connects to the server on every invocation rather than bundling it. A `CLIENT_SPEC` variable at the top holds the resolved transport (a URL string or `StdioTransport` with baked-in command and arguments).\n\nThe most common edit is changing `CLIENT_SPEC` — for example, pointing a script generated from a dev server at production. Beyond that, the helper functions (`_call_tool`, `_print_tool_result`) are thin wrappers around `fastmcp.Client` that are easy to adapt.\n\nThe script requires `fastmcp` as a dependency. If it lives outside a project that already has FastMCP installed:\n\n```bash\nuv run --with fastmcp python cli.py call-tool get_forecast --city London\n```\n"
  },
  {
    "path": "docs/cli/inspecting.mdx",
    "content": "---\ntitle: Inspecting Servers\nsidebarTitle: Inspecting\ndescription: View a server's components and metadata\nicon: magnifying-glass\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.9.0\" />\n\n`fastmcp inspect` loads a server and reports what it contains — its tools, resources, prompts, version, and metadata. The default output is a human-readable summary:\n\n```bash\nfastmcp inspect server.py\n```\n\n```\nServer: MyServer\nInstructions: A helpful MCP server\nVersion: 1.0.0\n\nComponents:\n  Tools: 5\n  Prompts: 2\n  Resources: 3\n  Templates: 1\n\nEnvironment:\n  FastMCP: 2.0.0\n  MCP: 1.0.0\n\nUse --format [fastmcp|mcp] for complete JSON output\n```\n\n## JSON Output\n\nFor programmatic use, two JSON formats are available:\n\n**FastMCP format** (`--format fastmcp`) includes everything FastMCP knows about the server — tool tags, enabled status, output schemas, annotations, and custom metadata. Field names use `snake_case`. This is the format for debugging and introspecting FastMCP servers.\n\n**MCP protocol format** (`--format mcp`) shows exactly what MCP clients see through the protocol — only standard MCP fields, `camelCase` names, no FastMCP-specific extensions. This is the format for verifying client compatibility and debugging what clients actually receive.\n\n```bash\n# Full FastMCP metadata to stdout\nfastmcp inspect server.py --format fastmcp\n\n# MCP protocol view saved to file\nfastmcp inspect server.py --format mcp -o manifest.json\n```\n\n## Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Format | `--format`, `-f` | `fastmcp` or `mcp` (required when using `-o`) |\n| Output File | `--output`, `-o` | Save to file instead of stdout |\n\n## Entrypoints\n\nThe `inspect` command supports the same local entrypoints as [`fastmcp run`](/cli/running): inferred instances, explicit entrypoints, factory functions, and `fastmcp.json` configs.\n\n```bash\nfastmcp inspect server.py                  # inferred instance\nfastmcp inspect server.py:my_server        # explicit entrypoint\nfastmcp inspect server.py:create_server    # factory function\nfastmcp inspect fastmcp.json               # config file\n```\n\n<Warning>\n`inspect` only works with local files and `fastmcp.json` — it doesn't connect to remote URLs or standard MCP config files.\n</Warning>\n"
  },
  {
    "path": "docs/cli/install-mcp.mdx",
    "content": "---\ntitle: Install MCP Servers\nsidebarTitle: Install MCPs\ndescription: Install MCP servers into Claude, Cursor, Gemini, and other clients\nicon: download\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.10.3\" />\n\n`fastmcp install` registers a server with an MCP client application so the client can launch it automatically. Each MCP client runs servers in its own isolated environment, which means dependencies need to be explicitly declared — you can't rely on whatever happens to be installed locally.\n\n```bash\nfastmcp install claude-desktop server.py\nfastmcp install claude-code server.py --with pandas --with matplotlib\nfastmcp install cursor server.py -e .\n```\n\n<Warning>\n`uv` must be installed and available in your system PATH. Both Claude Desktop and Cursor run servers in isolated environments managed by `uv`. On macOS, install it globally with Homebrew for Claude Desktop compatibility: `brew install uv`.\n</Warning>\n\n## Supported Clients\n\n| Client | Install method |\n| ------ | -------------- |\n| `claude-code` | Claude Code's built-in MCP management |\n| `claude-desktop` | Direct config file modification |\n| `cursor` | Deeplink that opens Cursor for confirmation |\n| `gemini-cli` | Gemini CLI's built-in MCP management |\n| `goose` | Deeplink that opens Goose for confirmation (uses `uvx`) |\n| `mcp-json` | Generates standard MCP JSON config for manual use |\n| `stdio` | Outputs the shell command to run via stdio |\n\n## Declaring Dependencies\n\nBecause MCP clients run servers in isolation, you need to tell the install command what your server needs. There are two approaches:\n\n**Command-line flags** let you specify dependencies directly:\n\n```bash\nfastmcp install claude-desktop server.py --with pandas --with \"sqlalchemy>=2.0\"\nfastmcp install cursor server.py -e . --with-requirements requirements.txt\n```\n\n**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file, dependencies are picked up automatically:\n\n```bash\nfastmcp install claude-desktop fastmcp.json\nfastmcp install claude-desktop  # auto-detects fastmcp.json in current directory\n```\n\nSee [Server Configuration](/deployment/server-configuration) for the full config format.\n\n## Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Server Name | `--server-name`, `-n` | Custom name for the server |\n| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |\n| Extra Packages | `--with` | Additional packages (repeatable) |\n| Environment Variables | `--env` | `KEY=VALUE` pairs (repeatable) |\n| Environment File | `--env-file`, `-f` | Load env vars from a `.env` file |\n| Python | `--python` | Python version (e.g., `3.11`) |\n| Project | `--project` | Run within a uv project directory |\n| Requirements | `--with-requirements` | Install from a requirements file |\n| Config Path | `--config-path` | Custom path to Claude Desktop config directory (`claude-desktop` only) |\n\n## Examples\n\n```bash\n# Basic install with auto-detected server instance\nfastmcp install claude-desktop server.py\n\n# Install from fastmcp.json with auto-detection\nfastmcp install claude-desktop\n\n# Explicit entrypoint with dependencies\nfastmcp install claude-desktop server.py:my_server \\\n  --server-name \"My Analysis Server\" \\\n  --with pandas\n\n# With environment variables\nfastmcp install claude-code server.py \\\n  --env API_KEY=secret \\\n  --env DEBUG=true\n\n# With env file\nfastmcp install cursor server.py --env-file .env\n\n# Specific Python version and requirements file\nfastmcp install claude-desktop server.py \\\n  --python 3.11 \\\n  --with-requirements requirements.txt\n\n# With custom config path (claude-desktop only)\nfastmcp install claude-desktop server.py \\\n  --config-path \"C:\\Users\\username\\AppData\\Local\\Packages\\Claude_xyz\\LocalCache\\Roaming\\Claude\"\n```\n\n## Generating MCP JSON\n\nThe `mcp-json` target generates standard MCP configuration JSON instead of installing into a specific client. This is useful for clients that FastMCP doesn't directly support, for CI/CD environments, or for sharing server configs:\n\n```bash\nfastmcp install mcp-json server.py\n```\n\nThe output follows the standard format used by Claude Desktop, Cursor, and other MCP clients:\n\n```json\n{\n  \"server-name\": {\n    \"command\": \"uv\",\n    \"args\": [\"run\", \"--with\", \"fastmcp\", \"fastmcp\", \"run\", \"/path/to/server.py\"],\n    \"env\": {\n      \"API_KEY\": \"value\"\n    }\n  }\n}\n```\n\nUse `--copy` to send it to your clipboard instead of stdout.\n\n## Generating Stdio Commands\n\nThe `stdio` target outputs the shell command an MCP host would use to start your server over stdio:\n\n```bash\nfastmcp install stdio server.py\n# Output: uv run --with fastmcp fastmcp run /absolute/path/to/server.py\n```\n\nWhen installing from a `fastmcp.json`, dependencies from the config are included automatically:\n\n```bash\nfastmcp install stdio fastmcp.json\n# Output: uv run --with fastmcp --with pillow --with 'qrcode[pil]>=8.0' fastmcp run /path/to/server.py\n```\n\nUse `--copy` to copy to clipboard.\n\n<Tip>\n`fastmcp install` is designed for local server files with stdio transport. For remote servers running over HTTP, use your client's native configuration — FastMCP's value here is simplifying the complex local setup with `uv`, dependencies, and environment variables.\n</Tip>\n"
  },
  {
    "path": "docs/cli/overview.mdx",
    "content": "---\ntitle: CLI\nsidebarTitle: Overview\ndescription: The fastmcp command-line interface\nicon: terminal\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nThe `fastmcp` CLI is installed automatically with FastMCP. It's the primary way to run, test, install, and interact with MCP servers from your terminal.\n\n```bash\nfastmcp --help\n```\n\n## Commands at a Glance\n\n| Command | What it does |\n| ------- | ------------ |\n| [`run`](/cli/running) | Run a server (local file, factory function, remote URL, or config file) |\n| [`dev apps`](/cli/running#previewing-apps) | Launch a browser-based preview UI for Prefab App tools |\n| [`dev inspector`](/cli/running#development-with-the-inspector) | Launch a server inside the MCP Inspector for interactive testing |\n| [`install`](/cli/install-mcp) | Install a server into Claude Code, Claude Desktop, Cursor, Gemini CLI, or Goose |\n| [`inspect`](/cli/inspecting) | Print a server's tools, resources, and prompts as a summary or JSON report |\n| [`list`](/cli/client) | List a server's tools (and optionally resources and prompts) |\n| [`call`](/cli/client#calling-tools) | Call a single tool with arguments |\n| [`discover`](/cli/client#discovering-configured-servers) | Find MCP servers configured in your editors and tools |\n| [`generate-cli`](/cli/generate-cli) | Scaffold a standalone typed CLI from a server's tool schemas |\n| [`project prepare`](/cli/running#pre-building-environments) | Pre-install dependencies into a reusable uv project |\n| [`auth cimd`](/cli/auth) | Create and validate CIMD documents for OAuth |\n| `version` | Print version info (`--copy` to copy to clipboard) |\n\n## Server Targets\n\nMost commands need to know *which server* to talk to. You pass a \"server spec\" as the first argument, and FastMCP resolves the right transport automatically.\n\n**URLs** connect to a running HTTP server:\n\n```bash\nfastmcp list http://localhost:8000/mcp\nfastmcp call http://localhost:8000/mcp get_forecast city=London\n```\n\n**Python files** are loaded directly — no `mcp.run()` boilerplate needed. FastMCP finds a server instance named `mcp`, `server`, or `app` in the file, or you can specify one explicitly:\n\n```bash\nfastmcp list server.py\nfastmcp run server.py:my_custom_server\n```\n\n**Config files** work too — both FastMCP's own `fastmcp.json` format and standard MCP config files with an `mcpServers` key:\n\n```bash\nfastmcp run fastmcp.json\nfastmcp list mcp-config.json\n```\n\n**Stdio commands** connect to any MCP server that speaks over standard I/O. Use `--command` instead of a positional argument:\n\n```bash\nfastmcp list --command 'npx -y @modelcontextprotocol/server-github'\n```\n\n### Name-Based Resolution\n\nIf your servers are already configured in an editor or tool, you can refer to them by name. FastMCP scans configs from Claude Desktop, Claude Code, Cursor, Gemini CLI, and Goose:\n\n```bash\nfastmcp list weather\nfastmcp call weather get_forecast city=London\n```\n\nWhen the same name appears in multiple configs, use the `source:name` form to be specific:\n\n```bash\nfastmcp list claude-code:my-server\nfastmcp call cursor:weather get_forecast city=London\n```\n\nRun [`fastmcp discover`](/cli/client#discovering-configured-servers) to see what's available on your machine.\n\n## Authentication\n\nWhen targeting an HTTP URL, the CLI enables OAuth authentication by default. If the server requires it, you'll be guided through the flow (typically opening a browser). If it doesn't, the setup is a silent no-op.\n\nTo skip authentication entirely — useful for local development servers — pass `--auth none`:\n\n```bash\nfastmcp call http://localhost:8000/mcp my_tool --auth none\n```\n\nYou can also pass a bearer token directly:\n\n```bash\nfastmcp list http://localhost:8000/mcp --auth \"Bearer sk-...\"\n```\n\n## Transport Override\n\nFastMCP defaults to Streamable HTTP for URL targets. If the server only supports Server-Sent Events (SSE), force the older transport:\n\n```bash\nfastmcp list http://localhost:8000 --transport sse\n```\n"
  },
  {
    "path": "docs/cli/running.mdx",
    "content": "---\ntitle: Running Servers\nsidebarTitle: Running\ndescription: Start, develop, and configure servers from the command line\nicon: play\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n## Starting a Server\n\n`fastmcp run` starts a server. Point it at a Python file, a factory function, a remote URL, or a config file:\n\n```bash\nfastmcp run server.py\nfastmcp run server.py:create_server\nfastmcp run https://example.com/mcp\nfastmcp run fastmcp.json\n```\n\nBy default, the server runs over **stdio** — the transport that MCP clients like Claude Desktop expect. To serve over HTTP instead, specify the transport:\n\n```bash\nfastmcp run server.py --transport http\nfastmcp run server.py --transport http --host 0.0.0.0 --port 9000\n```\n\n### Entrypoints\n\nFastMCP supports several ways to locate and start your server:\n\n**Inferred instance** — FastMCP imports the file and looks for a variable named `mcp`, `server`, or `app`:\n\n```bash\nfastmcp run server.py\n```\n\n**Explicit instance** — point at a specific variable:\n\n```bash\nfastmcp run server.py:my_server\n```\n\n**Factory function** — FastMCP calls the function and uses the returned server. Useful when your server needs async setup or configuration that runs before startup:\n\n```bash\nfastmcp run server.py:create_server\n```\n\n**Remote URL** — starts a local proxy that bridges to a remote server. Handy for local development against a deployed server, or for bridging a remote HTTP server to stdio:\n\n```bash\nfastmcp run https://example.com/mcp\n```\n\n**FastMCP config** — uses a `fastmcp.json` file that declaratively specifies the server, its dependencies, and deployment settings. When you run `fastmcp run` with no arguments, it auto-detects `fastmcp.json` in the current directory:\n\n```bash\nfastmcp run\nfastmcp run my-config.fastmcp.json\n```\n\nSee [Server Configuration](/deployment/server-configuration) for the full `fastmcp.json` format.\n\n**MCP config** — runs servers defined in a standard MCP configuration file (any `.json` with an `mcpServers` key):\n\n```bash\nfastmcp run mcp.json\n```\n\n<Warning>\n`fastmcp run` completely ignores the `if __name__ == \"__main__\"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](/cli/overview#factory-functions).\n</Warning>\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Transport | `--transport`, `-t` | `stdio` (default), `http`, or `sse` |\n| Host | `--host` | Bind address for HTTP (default: `127.0.0.1`) |\n| Port | `--port`, `-p` | Bind port for HTTP (default: `8000`) |\n| Path | `--path` | URL path for HTTP (default: `/mcp/`) |\n| Log Level | `--log-level`, `-l` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |\n| No Banner | `--no-banner` | Suppress the startup banner |\n| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes and restart automatically |\n| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |\n| Skip Env | `--skip-env` | Don't set up a uv environment (use when already in one) |\n| Python | `--python` | Python version to use (e.g., `3.11`) |\n| Extra Packages | `--with` | Additional packages to install (repeatable) |\n| Project | `--project` | Run within a specific uv project directory |\n| Requirements | `--with-requirements` | Install from a requirements file |\n\n### Dependency Management\n\nBy default, `fastmcp run` uses your current Python environment directly. When you pass `--python`, `--with`, `--project`, or `--with-requirements`, it switches to running via `uv run` in a subprocess, which handles dependency isolation automatically.\n\nThe `--skip-env` flag is useful when you're already inside an activated venv, a Docker container with pre-installed dependencies, or a uv-managed project — it prevents uv from trying to set up another environment layer.\n\n## Previewing Apps\n\n<VersionBadge version=\"3.2.0\" />\n\n`fastmcp dev apps` launches a browser-based preview UI for servers with [Prefab App tools](/apps/prefab). It starts your MCP server on one port and a local dev UI on another — giving you a live, interactive picker where you can call app tools and see their rendered output without needing a full MCP host client.\n\n```bash\nfastmcp dev apps server.py\nfastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090\n```\n\nThe picker auto-generates a form from each tool's input schema. Submit the form and the result opens in a new tab as a rendered Prefab UI.\n\nAuto-reload is on by default — save a file and the MCP server restarts automatically.\n\n<Tip>\n`fastmcp dev apps` requires `fastmcp[apps]` — install with `pip install \"fastmcp[apps]\"`.\n</Tip>\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| MCP Port | `--mcp-port` | Port for the MCP server (default: `8000`) |\n| Dev Port | `--dev-port` | Port for the dev UI (default: `8080`) |\n| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes (default: on) |\n\n## Development with the Inspector\n\n`fastmcp dev inspector` launches your server inside the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a browser-based tool for interactively testing MCP servers. Auto-reload is on by default, so your server restarts when you save changes.\n\n```bash\nfastmcp dev inspector server.py\nfastmcp dev inspector server.py -e . --with pandas\n```\n\n<Tip>\nThe Inspector always runs your server via `uv run` in a subprocess — it never uses your local environment directly. Specify dependencies with `--with`, `--with-editable`, `--with-requirements`, or through a `fastmcp.json` file.\n</Tip>\n\n<Warning>\nThe Inspector connects over **stdio only**. When it launches, you may need to select \"STDIO\" from the transport dropdown and click connect. To test a server over HTTP, start it separately with `fastmcp run server.py --transport http` and point the Inspector at the URL.\n</Warning>\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |\n| Extra Packages | `--with` | Additional packages (repeatable) |\n| Inspector Version | `--inspector-version` | MCP Inspector version to use |\n| UI Port | `--ui-port` | Port for the Inspector UI |\n| Server Port | `--server-port` | Port for the Inspector proxy |\n| Auto-Reload | `--reload` / `--no-reload` | File watching (default: on) |\n| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |\n| Python | `--python` | Python version |\n| Project | `--project` | Run within a uv project directory |\n| Requirements | `--with-requirements` | Install from a requirements file |\n\n## Pre-Building Environments\n\n`fastmcp project prepare` creates a persistent uv project from a `fastmcp.json` file, pre-installing all dependencies. This separates environment setup from server execution — install once, run many times.\n\n```bash\n# Step 1: Build the environment (slow, does dependency resolution)\nfastmcp project prepare fastmcp.json --output-dir ./env\n\n# Step 2: Run using the prepared environment (fast, no install step)\nfastmcp run fastmcp.json --project ./env\n```\n\nThe prepared directory contains a `pyproject.toml`, a `.venv` with all packages installed, and a `uv.lock` for reproducibility. This is particularly useful in deployment scenarios where you want deterministic, pre-built environments.\n"
  },
  {
    "path": "docs/clients/auth/bearer.mdx",
    "content": "---\ntitle: Bearer Token Authentication\nsidebarTitle: Bearer Auth\ndescription: Authenticate your FastMCP client with a Bearer token.\nicon: key\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.6.0\" />\n\n<Tip>\nBearer Token authentication is only relevant for HTTP-based transports.\n</Tip>\n\nYou can configure your FastMCP client to use **bearer authentication** by supplying a valid access token. This is most appropriate for service accounts, long-lived API keys, CI/CD, applications where authentication is managed separately, or other non-interactive authentication methods.\n\nA Bearer token is a JSON Web Token (JWT) that is used to authenticate a request. It is most commonly used in the `Authorization` header of an HTTP request, using the `Bearer` scheme:\n\n```http\nAuthorization: Bearer <token>\n```\n\n\n## Client Usage\n\nThe most straightforward way to use a pre-existing Bearer token is to provide it as a string to the `auth` parameter of the `fastmcp.Client` or transport instance. FastMCP will automatically format it correctly for the `Authorization` header and bearer scheme.\n\n<Tip>\nIf you're using a string token, do not include the `Bearer` prefix. FastMCP will add it for you.\n</Tip>\n\n```python {5}\nfrom fastmcp import Client\n\nasync with Client(\n    \"https://your-server.fastmcp.app/mcp\", \n    auth=\"<your-token>\",\n) as client:\n    await client.ping()\n```\n\nYou can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:\n\n```python {6}\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\ntransport = StreamableHttpTransport(\n    \"http://your-server.fastmcp.app/mcp\", \n    auth=\"<your-token>\",\n)\n\nasync with Client(transport) as client:\n    await client.ping()\n```\n\n## `BearerAuth` Helper\n\nIf you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface.\n\n```python {6}\nfrom fastmcp import Client\nfrom fastmcp.client.auth import BearerAuth\n\nasync with Client(\n    \"https://your-server.fastmcp.app/mcp\", \n    auth=BearerAuth(token=\"<your-token>\"),\n) as client:\n    await client.ping()\n```\n\n## Custom Headers\n\nIf the MCP server expects a custom header or token scheme, you can manually set the client's `headers` instead of using the `auth` parameter by setting them on your transport:\n\n```python {5}\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\nasync with Client(\n    transport=StreamableHttpTransport(\n        \"https://your-server.fastmcp.app/mcp\", \n        headers={\"X-API-Key\": \"<your-token>\"},\n    ),\n) as client:\n    await client.ping()\n```\n"
  },
  {
    "path": "docs/clients/auth/cimd.mdx",
    "content": "---\ntitle: CIMD Authentication\nsidebarTitle: CIMD\ndescription: Use Client ID Metadata Documents for verifiable, domain-based client identity.\nicon: id-badge\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"3.0.0\" />\n\n<Tip>\nCIMD authentication is only relevant for HTTP-based transports and requires a server that advertises CIMD support.\n</Tip>\n\nWith standard OAuth, your client registers dynamically with every server it connects to, receiving a fresh `client_id` each time. This works, but the server has no way to verify *who* your client actually is — any client can claim any name during registration.\n\nCIMD (Client ID Metadata Documents) flips this around. You host a small JSON document at an HTTPS URL you control, and that URL becomes your `client_id`. When your client connects to a server, the server fetches your metadata document and can verify your identity through your domain ownership. Users see a verified domain badge in the consent screen instead of an unverified client name.\n\n## Client Usage\n\nPass your CIMD document URL to the `client_metadata_url` parameter of `OAuth`:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\nasync with Client(\n    \"https://mcp-server.example.com/mcp\",\n    auth=OAuth(\n        client_metadata_url=\"https://myapp.example.com/oauth/client.json\",\n    ),\n) as client:\n    await client.ping()\n```\n\nWhen the server supports CIMD, the client uses your metadata URL as its `client_id` instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.\n\n<Note>\nYou don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` — the transport provides the server URL automatically.\n</Note>\n\n## Creating a CIMD Document\n\nA CIMD document is a JSON file that describes your client. The most important field is `client_id`, which must exactly match the URL where you host the document.\n\nUse the FastMCP CLI to generate one:\n\n```bash\nfastmcp auth cimd create \\\n    --name \"My Application\" \\\n    --redirect-uri \"http://localhost:*/callback\" \\\n    --client-id \"https://myapp.example.com/oauth/client.json\"\n```\n\nThis produces:\n\n```json\n{\n  \"client_id\": \"https://myapp.example.com/oauth/client.json\",\n  \"client_name\": \"My Application\",\n  \"redirect_uris\": [\"http://localhost:*/callback\"],\n  \"token_endpoint_auth_method\": \"none\",\n  \"grant_types\": [\"authorization_code\"],\n  \"response_types\": [\"code\"]\n}\n```\n\nIf you omit `--client-id`, the CLI generates a placeholder value and reminds you to update it before hosting.\n\n### CLI Options\n\nThe `create` command accepts these flags:\n\n| Flag | Description |\n|------|-------------|\n| `--name` | Human-readable client name (required) |\n| `--redirect-uri`, `-r` | Allowed redirect URIs — can be specified multiple times (required) |\n| `--client-id` | The URL where you'll host this document (sets `client_id` directly) |\n| `--output`, `-o` | Write to a file instead of stdout |\n| `--scope` | Space-separated list of scopes the client may request |\n| `--client-uri` | URL of the client's home page |\n| `--logo-uri` | URL of the client's logo image |\n| `--no-pretty` | Output compact JSON |\n\n### Redirect URIs\n\nThe `redirect_uris` field supports wildcard port matching for localhost. The pattern `http://localhost:*/callback` matches any port, which is useful for development clients that bind to random available ports (which is what FastMCP's `OAuth` helper does by default).\n\n## Hosting Requirements\n\nCIMD documents must be hosted at a publicly accessible HTTPS URL with a non-root path:\n\n- **HTTPS required** — HTTP URLs are rejected for security\n- **Non-root path** — The URL must have a path component (e.g., `/oauth/client.json`, not just `/`)\n- **Public accessibility** — The server must be able to fetch the document over the internet\n- **Matching `client_id`** — The `client_id` field in the document must exactly match the hosting URL\n\nCommon hosting options include static file hosting services like GitHub Pages, Cloudflare Pages, Vercel, or S3 — anywhere you can serve a JSON file over HTTPS.\n\n## Validating Your Document\n\nBefore deploying, verify your hosted document passes validation:\n\n```bash\nfastmcp auth cimd validate https://myapp.example.com/oauth/client.json\n```\n\nThe validator fetches the document and checks that:\n- The URL is valid (HTTPS, non-root path)\n- The document is well-formed JSON conforming to the CIMD schema\n- The `client_id` in the document matches the URL it was fetched from\n\n## How It Works\n\nWhen your client connects to a CIMD-enabled server, the flow works like this:\n\n<Steps>\n<Step title=\"Client Presents Metadata URL\">\nYour client sends its `client_metadata_url` as the `client_id` in the OAuth authorization request.\n</Step>\n<Step title=\"Server Recognizes CIMD URL\">\nThe server sees that the `client_id` is an HTTPS URL with a path — the signature of a CIMD client — and skips Dynamic Client Registration.\n</Step>\n<Step title=\"Server Fetches and Validates\">\nThe server fetches your JSON document from the URL, validates that `client_id` matches the URL, and extracts your client metadata (name, redirect URIs, scopes).\n</Step>\n<Step title=\"Authorization Proceeds\">\nThe standard OAuth flow continues: browser opens for user consent, authorization code exchange, token issuance. The consent screen shows your verified domain.\n</Step>\n</Steps>\n\nThe server caches your CIMD document according to HTTP cache headers, so subsequent requests don't require re-fetching.\n\n## Server Configuration\n\nCIMD is a server-side feature that your MCP server must support. FastMCP's OAuth proxy providers (GitHub, Google, Auth0, etc.) support CIMD by default. See the [OAuth Proxy CIMD documentation](/servers/auth/oauth-proxy#cimd-support) for server-side configuration, including private key JWT authentication and security details.\n"
  },
  {
    "path": "docs/clients/auth/oauth.mdx",
    "content": "---\ntitle: OAuth Authentication\nsidebarTitle: OAuth\ndescription: Authenticate your FastMCP client via OAuth 2.1.\nicon: window\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.6.0\" />\n\n<Tip>\nOAuth authentication is only relevant for HTTP-based transports and requires user interaction via a web browser.\n</Tip>\n\nWhen your FastMCP client needs to access an MCP server protected by OAuth 2.1, and the process requires user interaction (like logging in and granting consent), you should use the Authorization Code Flow. FastMCP provides the `fastmcp.client.auth.OAuth` helper to simplify this entire process.\n\nThis flow is common for user-facing applications where the application acts on behalf of the user.\n\n## Client Usage\n\n\n### Default Configuration\n\nThe simplest way to use OAuth is to pass the string `\"oauth\"` to the `auth` parameter of the `Client` or transport instance. FastMCP will automatically configure the client to use OAuth with default settings:\n\n```python {4}\nfrom fastmcp import Client\n\n# Uses default OAuth settings\nasync with Client(\"https://your-server.fastmcp.app/mcp\", auth=\"oauth\") as client:\n    await client.ping()\n```\n\n\n### `OAuth` Helper\n\nTo fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx.Auth` interface.\n\n```python {2, 4, 6}\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\noauth = OAuth(scopes=[\"user\"])\n\nasync with Client(\"https://your-server.fastmcp.app/mcp\", auth=oauth) as client:\n    await client.ping()\n```\n\n<Note>\nYou don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` — the transport provides the server URL automatically.\n</Note>\n\n#### `OAuth` Parameters\n\n- **`scopes`** (`str | list[str]`, optional): OAuth scopes to request. Can be space-separated string or list of strings\n- **`client_name`** (`str`, optional): Client name for dynamic registration. Defaults to `\"FastMCP Client\"`\n- **`client_id`** (`str`, optional): Pre-registered OAuth client ID. When provided, skips Dynamic Client Registration entirely. See [Pre-Registered Clients](#pre-registered-clients)\n- **`client_secret`** (`str`, optional): OAuth client secret for pre-registered clients. Optional — public clients that rely on PKCE can omit this\n- **`client_metadata_url`** (`str`, optional): URL-based client identity (CIMD). See [CIMD Authentication](/clients/auth/cimd) for details\n- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options\n- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration\n- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port\n- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx clients\n\n\n## OAuth Flow\n\nThe OAuth flow is triggered when you use a FastMCP `Client` configured to use OAuth.\n\n<Steps>\n<Step title=\"Token Check\">\nThe client first checks the configured `token_storage` backend for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client.\n</Step>\n<Step title=\"OAuth Server Discovery\">\nIf no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.\n</Step>\n<Step title=\"Client Registration\">\nIf a `client_id` is provided, the client uses those pre-registered credentials directly and skips this step entirely. Otherwise, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity. As a fallback, the client performs Dynamic Client Registration (RFC 7591) if the server supports it.\n</Step>\n<Step title=\"Local Callback Server\">\nA temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:<port>/callback`) acts as the `redirect_uri` for the OAuth flow.\n</Step>\n<Step title=\"Browser Interaction\">\nThe user's default web browser is automatically opened, directing them to the OAuth server's authorization endpoint. The user logs in and grants (or denies) the requested `scopes`.\n</Step>\n<Step title=\"Authorization Code & Token Exchange\">\nUpon approval, the OAuth server redirects the user's browser to the local callback server with an `authorization_code`. The client captures this code and exchanges it with the OAuth server's token endpoint for an `access_token` (and often a `refresh_token`) using PKCE for security.\n</Step>\n<Step title=\"Token Caching\">\nThe obtained tokens are saved to the configured `token_storage` backend for future use, eliminating the need for repeated browser interactions.\n</Step>\n<Step title=\"Authenticated Requests\">\nThe access token is automatically included in the `Authorization` header for requests to the MCP server.\n</Step>\n<Step title=\"Refresh Token\">\nIf the access token expires, the client will automatically use the refresh token to get a new access token.\n</Step>\n</Steps>\n\n## Token Storage\n\n<VersionBadge version=\"2.13.0\" />\n\nBy default, tokens are stored in memory and lost when your application restarts. For persistent storage, pass an `AsyncKeyValue`-compatible storage backend to the `token_storage` parameter.\n\n<Warning>\n**Security Consideration**: Use encrypted storage for production. MCP clients can accumulate OAuth credentials for many servers over time, and a compromised token store could expose access to multiple services.\n</Warning>\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\nfrom key_value.aio.stores.disk import DiskStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\nimport os\n\n# Create encrypted disk storage\nencrypted_storage = FernetEncryptionWrapper(\n    key_value=DiskStore(directory=\"~/.fastmcp/oauth-tokens\"),\n    fernet=Fernet(os.environ[\"OAUTH_STORAGE_ENCRYPTION_KEY\"])\n)\n\noauth = OAuth(token_storage=encrypted_storage)\n\nasync with Client(\"https://your-server.fastmcp.app/mcp\", auth=oauth) as client:\n    await client.ping()\n```\n\nYou can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.\n\n<Note>\nWhen selecting a storage backend, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have constraints that affect production suitability.\n</Note>\n\n## CIMD Authentication\n\n<VersionBadge version=\"3.0.0\" />\n\nClient ID Metadata Documents (CIMD) provide an alternative to Dynamic Client Registration. Instead of registering with each server, your client hosts a static JSON document at an HTTPS URL. That URL becomes your client's identity, and servers can verify who you are through your domain ownership.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\nasync with Client(\n    \"https://mcp-server.example.com/mcp\",\n    auth=OAuth(\n        client_metadata_url=\"https://myapp.example.com/oauth/client.json\",\n    ),\n) as client:\n    await client.ping()\n```\n\nSee the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.\n\n## Pre-Registered Clients\n\n<VersionBadge version=\"3.0.0\" />\n\nSome OAuth servers don't support Dynamic Client Registration — the MCP spec explicitly makes DCR optional. If your client has been pre-registered with the server (you already have a `client_id` and optionally a `client_secret`), you can provide them directly to skip DCR entirely.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\nasync with Client(\n    \"https://mcp-server.example.com/mcp\",\n    auth=OAuth(\n        client_id=\"my-registered-client-id\",\n        client_secret=\"my-client-secret\",\n    ),\n) as client:\n    await client.ping()\n```\n\nPublic clients that rely on PKCE for security can omit `client_secret`:\n\n```python\noauth = OAuth(client_id=\"my-public-client-id\")\n```\n\n<Note>\nWhen using pre-registered credentials, the client will not attempt Dynamic Client Registration. If the server rejects the credentials, the error is surfaced immediately rather than falling back to DCR.\n</Note>\n"
  },
  {
    "path": "docs/clients/cli.mdx",
    "content": "---\ntitle: Client CLI\nsidebarTitle: CLI\ndescription: Query and invoke MCP server tools directly from the terminal with fastmcp list and fastmcp call.\nicon: terminal\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nMCP servers are designed for programmatic consumption by AI assistants and applications. But during development, you often want to poke at a server directly: check what tools it exposes, call one with test arguments, or verify that a deployment is responding correctly. The FastMCP CLI gives you that direct access with two commands, `fastmcp list` and `fastmcp call`, so you can query and invoke any MCP server without writing a single line of Python.\n\nThese commands are also valuable for LLM-based agents that lack native MCP support. An agent that can execute shell commands can use `fastmcp list --json` to discover available tools and `fastmcp call --json` to invoke them, with structured JSON output designed for programmatic consumption.\n\n## Server Targets\n\nBoth commands need to know which server to talk to. You provide a \"server spec\" as the first argument, and FastMCP figures out the transport automatically. You can point at an HTTP URL for a running server, a Python file that defines one, a JSON configuration file that describes one, or a JavaScript file. The CLI resolves the right connection mechanism so you can focus on the query.\n\n```bash\nfastmcp list http://localhost:8000/mcp\nfastmcp list server.py\nfastmcp list mcp-config.json\n```\n\nPython files are handled with particular care. Rather than requiring your script to call `mcp.run()` at the bottom, the CLI routes it through `fastmcp run` internally, which means any Python file that defines a FastMCP server object works as a target with no boilerplate.\n\nFor servers that communicate over stdio (common with Node.js-based MCP servers), use the `--command` flag instead of a positional server spec. The string is shell-split into a command and arguments.\n\n```bash\nfastmcp list --command 'npx -y @modelcontextprotocol/server-github'\n```\n\n### Name-Based Resolution\n\nIf your MCP servers are already configured in an editor or tool, you can refer to them by name instead of spelling out URLs or file paths. The CLI scans config files from Claude Desktop, Claude Code, Cursor, Gemini CLI, and Goose, and matches the name you provide.\n\n```bash\nfastmcp list weather\nfastmcp call weather get_forecast city=London\n```\n\nYou can also use the `source:name` form to target a specific source directly, which is useful when the same server name appears in multiple configs or when you want to be explicit about which config you mean.\n\n```bash\nfastmcp list claude-code:my-server\nfastmcp call cursor:weather get_forecast city=London\n```\n\nThe available source names are `claude-desktop`, `claude-code`, `cursor`, `gemini`, `goose`, and `project` (for `./mcp.json`). Run `fastmcp discover` to see what's available.\n\n## Discovering Configured Servers\n\n`fastmcp discover` scans your local editor and project configurations for MCP server definitions. It checks Claude Desktop, Claude Code (`~/.claude.json`), Cursor workspace configs (walking up from the current directory), Gemini CLI (`~/.gemini/settings.json`), Goose (`~/.config/goose/config.yaml`), and `mcp.json` in the current directory.\n\n```bash\nfastmcp discover\n```\n\nThe output groups servers by source, showing each server's name and transport. Use `--source` to filter to specific sources, and `--json` for machine-readable output.\n\n```bash\nfastmcp discover --source claude-code\nfastmcp discover --source cursor --source gemini --json\n```\n\nAny server that appears here can be used by name (or `source:name`) with `fastmcp list` and `fastmcp call`, which means you can go from \"I have a server configured in Claude Code\" to querying it without copying any URLs or paths.\n\n## Discovering Tools\n\n`fastmcp list` connects to a server and prints every tool it exposes. The default output is compact: each tool appears as a function signature with its parameter names, types, and a description.\n\n```bash\nfastmcp list http://localhost:8000/mcp\n```\n\nThe output looks like a Python function signature, making it easy to see at a glance what a tool expects and what it returns. Required parameters appear with just their type annotation, while optional ones show their defaults.\n\nWhen you need the full JSON Schema for a tool's inputs or outputs -- useful for understanding nested object structures or enum constraints -- opt into them with `--input-schema` or `--output-schema`. These print the raw schema beneath each tool signature.\n\n### Beyond Tools\n\nMCP servers can expose resources and prompts alongside tools. By default, `fastmcp list` only shows tools because they are the most common interaction point. Add `--resources` or `--prompts` to include those in the output.\n\n```bash\nfastmcp list server.py --resources --prompts\n```\n\nResources appear with their URIs and descriptions. Prompts appear with their argument names so you can see what parameters they accept.\n\n### Machine-Readable Output\n\nThe `--json` flag switches from human-friendly text to structured JSON. Each tool includes its name, description, and full input schema (and output schema when present). When combined with `--resources` or `--prompts`, those are included as additional top-level keys.\n\n```bash\nfastmcp list server.py --json\n```\n\nThis is the format to use when building automation around MCP servers or feeding tool definitions to an LLM agent that needs to decide which tool to call.\n\n## Calling Tools\n\n`fastmcp call` invokes a single tool on a server. You provide the server spec, the tool name, and arguments as `key=value` pairs. The CLI fetches the tool's schema, coerces your string values to the correct types (integers, floats, booleans, arrays, objects), and makes the call.\n\n```bash\nfastmcp call http://localhost:8000/mcp search query=hello limit=5\n```\n\nType coercion is driven by the tool's JSON Schema. If a parameter is declared as an integer, the string `\"5\"` becomes the integer `5`. Booleans accept `true`/`false`, `yes`/`no`, and `1`/`0`. Array and object parameters are parsed as JSON.\n\nFor tools with complex or deeply nested arguments, the `key=value` syntax gets unwieldy. You can pass a single JSON object as the argument instead, and the CLI treats it as the full input dictionary.\n\n```bash\nfastmcp call server.py create_item '{\"name\": \"Widget\", \"tags\": [\"sale\", \"new\"], \"metadata\": {\"color\": \"blue\"}}'\n```\n\nAlternatively, `--input-json` provides the base argument dictionary. Any `key=value` pairs you add alongside it override keys from the JSON, which is useful for templating a complex call and varying one parameter at a time.\n\n### Error Handling\n\nThe CLI validates your call before sending it. If you misspell a tool name, it uses fuzzy matching to suggest corrections. If you omit a required argument, it tells you which ones are missing and prints the tool's signature as a reminder.\n\nWhen a tool call itself returns an error (the server executed the tool but it failed), the error message is printed and the CLI exits with a non-zero status code, making it straightforward to use in scripts.\n\n### Structured Output\n\nLike `fastmcp list`, the `--json` flag on `fastmcp call` emits structured JSON instead of formatted text. The output includes the content blocks, error status, and structured content when the server provides it. Use this when you need to parse tool results programmatically.\n\n```bash\nfastmcp call server.py get_weather city=London --json\n```\n\n## Authentication\n\nWhen the server target is an HTTP URL, the CLI automatically enables OAuth authentication. If the server requires it, you will be guided through the OAuth flow (typically opening a browser for authorization). If the server has no auth requirements, the OAuth setup is a silent no-op.\n\nTo explicitly disable authentication -- for example, when connecting to a local development server where OAuth setup would just slow you down -- pass `--auth none`.\n\n```bash\nfastmcp call http://localhost:8000/mcp my_tool --auth none\n```\n\n## Transport Override\n\nFastMCP defaults to Streamable HTTP for URL targets. If you are connecting to a server that only supports Server-Sent Events (SSE), use `--transport sse` to force the older transport. This appends `/sse` to the URL path automatically so the client picks the correct protocol.\n\n```bash\nfastmcp list http://localhost:8000 --transport sse\n```\n\n## Interactive Elicitation\n\nSome MCP tools request additional input from the user during execution through a mechanism called elicitation. When a tool sends an elicitation request, the CLI prints the server's question to the terminal and prompts you to respond. Each field in the elicitation schema is presented with its name and expected type, and required fields are clearly marked.\n\nYou can type `decline` to skip a question or `cancel` to abort the tool call entirely. This interactive behavior means the CLI works naturally with tools that have multi-step or conversational workflows.\n\n## LLM Agent Integration\n\nFor LLM agents that can execute shell commands but lack built-in MCP support, the CLI provides a clean integration path. The agent calls `fastmcp list --json` to get a structured description of every available tool, including full input schemas, and then calls `fastmcp call --json` with the chosen tool and arguments. Both commands return well-formed JSON that is straightforward to parse.\n\nBecause the CLI handles connection management, transport selection, and type coercion internally, the agent does not need to understand MCP protocol details. It just needs to read JSON and construct shell commands.\n"
  },
  {
    "path": "docs/clients/client.mdx",
    "content": "---\ntitle: The FastMCP Client\nsidebarTitle: Overview\ndescription: Programmatic client for interacting with MCP servers through a well-typed, Pythonic interface.\nicon: user-robot\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nThe `fastmcp.Client` class provides a programmatic interface for interacting with any MCP server. It handles protocol details and connection management automatically, letting you focus on the operations you want to perform.\n\nThe FastMCP Client is designed for deterministic, controlled interactions rather than autonomous behavior, making it ideal for testing MCP servers during development, building deterministic applications that need reliable MCP interactions, and creating the foundation for agentic or LLM-based clients with structured, type-safe operations.\n\n<Note>\nThis is a programmatic client that requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems.\n</Note>\n\n## Creating a Client\n\nYou provide a server source and the client automatically infers the appropriate transport mechanism.\n\n```python\nimport asyncio\nfrom fastmcp import Client, FastMCP\n\n# In-memory server (ideal for testing)\nserver = FastMCP(\"TestServer\")\nclient = Client(server)\n\n# HTTP server\nclient = Client(\"https://example.com/mcp\")\n\n# Local Python script\nclient = Client(\"my_mcp_server.py\")\n\nasync def main():\n    async with client:\n        # Basic server interaction\n        await client.ping()\n\n        # List available operations\n        tools = await client.list_tools()\n        resources = await client.list_resources()\n        prompts = await client.list_prompts()\n\n        # Execute operations\n        result = await client.call_tool(\"example_tool\", {\"param\": \"value\"})\n        print(result)\n\nasyncio.run(main())\n```\n\nAll client operations require using the `async with` context manager for proper connection lifecycle management.\n\n## Choosing a Transport\n\nThe client automatically selects a transport based on what you pass to it, but different transports have different characteristics that matter for your use case.\n\n**In-memory transport** connects directly to a FastMCP server instance within the same Python process. Use this for testing and development where you want to eliminate subprocess and network complexity. The server shares your process's environment and memory space.\n\n```python\nfrom fastmcp import Client, FastMCP\n\nserver = FastMCP(\"TestServer\")\nclient = Client(server)  # In-memory, no network or subprocess\n```\n\n**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. The subprocess runs in an isolated environment, so you must explicitly pass any environment variables the server needs.\n\n```python\nfrom fastmcp import Client\n\n# Simple inference from file path\nclient = Client(\"my_server.py\")\n\n# With explicit environment configuration\nclient = Client(\"my_server.py\", env={\"API_KEY\": \"secret\"})\n```\n\n**HTTP transport** connects to servers running as web services. Use this for production deployments where the server runs independently and manages its own lifecycle.\n\n```python\nfrom fastmcp import Client\n\nclient = Client(\"https://api.example.com/mcp\")\n```\n\nSee [Transports](/clients/transports) for detailed configuration options including authentication headers, session persistence, and multi-server configurations.\n\n## Configuration-Based Clients\n\n<VersionBadge version=\"2.4.0\" />\n\nCreate clients from MCP configuration dictionaries, which can include multiple servers. While there is no official standard for MCP configuration format, FastMCP follows established conventions used by tools like Claude Desktop.\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather-api.example.com/mcp\"\n        },\n        \"assistant\": {\n            \"command\": \"python\",\n            \"args\": [\"./assistant_server.py\"]\n        }\n    }\n}\n\nclient = Client(config)\n\nasync with client:\n    # Tools are prefixed with server names\n    weather_data = await client.call_tool(\"weather_get_forecast\", {\"city\": \"London\"})\n    response = await client.call_tool(\"assistant_answer_question\", {\"question\": \"What's the capital of France?\"})\n\n    # Resources use prefixed URIs\n    icons = await client.read_resource(\"weather://weather/icons/sunny\")\n```\n\n## Connection Lifecycle\n\nThe client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.\n\n```python\nfrom fastmcp import Client, FastMCP\n\nmcp = FastMCP(name=\"MyServer\", instructions=\"Use the greet tool to say hello!\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet a user by name.\"\"\"\n    return f\"Hello, {name}!\"\n\nasync with Client(mcp) as client:\n    # Initialization already happened automatically\n    print(f\"Server: {client.initialize_result.serverInfo.name}\")\n    print(f\"Instructions: {client.initialize_result.instructions}\")\n    print(f\"Capabilities: {client.initialize_result.capabilities.tools}\")\n```\n\nFor advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:\n\n```python\nfrom fastmcp import Client\n\nclient = Client(\"my_mcp_server.py\", auto_initialize=False)\n\nasync with client:\n    # Connection established, but not initialized yet\n    print(f\"Connected: {client.is_connected()}\")\n    print(f\"Initialized: {client.initialize_result is not None}\")  # False\n\n    # Initialize manually with custom timeout\n    result = await client.initialize(timeout=10.0)\n    print(f\"Server: {result.serverInfo.name}\")\n\n    # Now ready for operations\n    tools = await client.list_tools()\n```\n\n## Operations\n\nFastMCP clients interact with three types of server components.\n\n**Tools** are server-side functions that the client can execute with arguments. Call them with `call_tool()` and receive structured results.\n\n```python\nasync with client:\n    tools = await client.list_tools()\n    result = await client.call_tool(\"multiply\", {\"a\": 5, \"b\": 3})\n    print(result.data)  # 15\n```\n\nSee [Tools](/clients/tools) for detailed documentation including version selection, error handling, and structured output.\n\n**Resources** are data sources that the client can read, either static or templated. Access them with `read_resource()` using URIs.\n\n```python\nasync with client:\n    resources = await client.list_resources()\n    content = await client.read_resource(\"file:///config/settings.json\")\n    print(content[0].text)\n```\n\nSee [Resources](/clients/resources) for detailed documentation including templates and binary content.\n\n**Prompts** are reusable message templates that can accept arguments. Retrieve rendered prompts with `get_prompt()`.\n\n```python\nasync with client:\n    prompts = await client.list_prompts()\n    messages = await client.get_prompt(\"analyze_data\", {\"data\": [1, 2, 3]})\n    print(messages.messages)\n```\n\nSee [Prompts](/clients/prompts) for detailed documentation including argument serialization.\n\n## Callback Handlers\n\nThe client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.logging import LogMessage\n\nasync def log_handler(message: LogMessage):\n    print(f\"Server log: {message.data}\")\n\nasync def progress_handler(progress: float, total: float | None, message: str | None):\n    print(f\"Progress: {progress}/{total} - {message}\")\n\nasync def sampling_handler(messages, params, context):\n    # Integrate with your LLM service here\n    return \"Generated response\"\n\nclient = Client(\n    \"my_mcp_server.py\",\n    log_handler=log_handler,\n    progress_handler=progress_handler,\n    sampling_handler=sampling_handler,\n    timeout=30.0\n)\n```\n\nEach handler type has its own documentation:\n\n- **[Sampling](/clients/sampling)** - Respond to server LLM requests\n- **[Elicitation](/clients/elicitation)** - Handle server requests for user input\n- **[Progress](/clients/progress)** - Monitor long-running operations\n- **[Logging](/clients/logging)** - Handle server log messages\n- **[Roots](/clients/roots)** - Provide local context to servers\n\n<Tip>\nThe FastMCP Client is designed as a foundational tool. Use it directly for deterministic operations, or build higher-level agentic systems on top of its reliable, type-safe interface.\n</Tip>\n"
  },
  {
    "path": "docs/clients/elicitation.mdx",
    "content": "---\ntitle: User Elicitation\nsidebarTitle: Elicitation\ndescription: Handle server requests for structured user input.\nicon: message-question\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.10.0\" />\n\nUse this when you need to respond to server requests for user input during tool execution.\n\nElicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.\n\n## Handler Template\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.elicitation import ElicitResult, ElicitRequestParams, RequestContext\n\nasync def elicitation_handler(\n    message: str,\n    response_type: type | None,\n    params: ElicitRequestParams,\n    context: RequestContext\n) -> ElicitResult | object:\n    \"\"\"\n    Handle server requests for user input.\n\n    Args:\n        message: The prompt to display to the user\n        response_type: Python dataclass type for the response (None if no data expected)\n        params: Original MCP elicitation parameters including raw JSON schema\n        context: Request context with metadata\n\n    Returns:\n        - Data directly (implicitly accepts the elicitation)\n        - ElicitResult for explicit control over the action\n    \"\"\"\n    # Present the message and collect input\n    user_input = input(f\"{message}: \")\n\n    if not user_input:\n        return ElicitResult(action=\"decline\")\n\n    # Create response using the provided dataclass type\n    return response_type(value=user_input)\n\nclient = Client(\n    \"my_mcp_server.py\",\n    elicitation_handler=elicitation_handler,\n)\n```\n\n## How It Works\n\nWhen a server needs user input, it sends an elicitation request with a message prompt and a JSON schema describing the expected response structure. FastMCP automatically converts this schema into a Python dataclass type, making it easy to construct properly typed responses without manually parsing JSON schemas.\n\nThe handler receives four parameters:\n\n<Card icon=\"code\" title=\"Handler Parameters\">\n<ResponseField name=\"message\" type=\"str\">\n  The prompt message to display to the user\n</ResponseField>\n\n<ResponseField name=\"response_type\" type=\"type | None\">\n  A Python dataclass type that FastMCP created from the server's JSON schema. Use this to construct your response with proper typing. If the server requests an empty object, this will be `None`.\n</ResponseField>\n\n<ResponseField name=\"params\" type=\"ElicitRequestParams\">\n  The original MCP elicitation parameters, including the raw JSON schema in `params.requestedSchema`\n</ResponseField>\n\n<ResponseField name=\"context\" type=\"RequestContext\">\n  Request context containing metadata about the elicitation request\n</ResponseField>\n</Card>\n\n## Response Actions\n\nYou can return data directly, which implicitly accepts the elicitation:\n\n```python\nasync def elicitation_handler(message, response_type, params, context):\n    user_input = input(f\"{message}: \")\n    return response_type(value=user_input)  # Implicit accept\n```\n\nOr return an `ElicitResult` for explicit control over the action:\n\n```python\nfrom fastmcp.client.elicitation import ElicitResult\n\nasync def elicitation_handler(message, response_type, params, context):\n    user_input = input(f\"{message}: \")\n\n    if not user_input:\n        return ElicitResult(action=\"decline\")  # User declined\n\n    if user_input == \"cancel\":\n        return ElicitResult(action=\"cancel\")   # Cancel entire operation\n\n    return ElicitResult(\n        action=\"accept\",\n        content=response_type(value=user_input)\n    )\n```\n\n**Action types:**\n- **`accept`**: User provided valid input. Include the data in the `content` field.\n- **`decline`**: User chose not to provide the requested information. Omit `content`.\n- **`cancel`**: User cancelled the entire operation. Omit `content`.\n\n## Example\n\nA file management tool might ask which directory to create:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.elicitation import ElicitResult\n\nasync def elicitation_handler(message, response_type, params, context):\n    print(f\"Server asks: {message}\")\n\n    user_response = input(\"Your response: \")\n\n    if not user_response:\n        return ElicitResult(action=\"decline\")\n\n    # Use the response_type dataclass to create a properly structured response\n    return response_type(value=user_response)\n\nclient = Client(\n    \"my_mcp_server.py\",\n    elicitation_handler=elicitation_handler\n)\n```\n"
  },
  {
    "path": "docs/clients/generate-cli.mdx",
    "content": "---\ntitle: Generate CLI\nsidebarTitle: Generate CLI\ndescription: Turn any MCP server into a standalone, typed command-line tool.\nicon: wand-magic-sparkles\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\n`fastmcp list` and `fastmcp call` let you poke at a server interactively, but they're developer tools — you always have to spell out the server spec, the tool name, and the arguments. `fastmcp generate-cli` takes the next step: it connects to a server, reads its schemas, and writes a standalone Python script where every tool is a proper subcommand with typed flags, help text, and tab completion. The result is a CLI that feels like it was hand-written for that specific server.\n\nThe key insight is that MCP tool schemas already contain everything a CLI framework needs: parameter names, types, descriptions, required/optional status, and defaults. `generate-cli` maps that schema into [cyclopts](https://cyclopts.readthedocs.io/) commands, so JSON Schema types become Python type annotations, descriptions become `--help` text, and required parameters become mandatory flags.\n\n## Generating a Script\n\nPoint the command at any server spec — URLs, Python files, discovered server names, MCPConfig JSON — and it writes a CLI script:\n\n```bash\nfastmcp generate-cli weather\nfastmcp generate-cli http://localhost:8000/mcp\nfastmcp generate-cli server.py my_weather_cli.py\n```\n\nThe second positional argument sets the output path. When omitted, it defaults to `cli.py`. If either the CLI file or its companion `SKILL.md` already exists, the command refuses to overwrite unless you pass `-f`:\n\n```bash\nfastmcp generate-cli weather -f\nfastmcp generate-cli weather my_cli.py -f\n```\n\nName-based resolution works here too, so if you have a server configured in Claude Desktop, Cursor, or any other supported editor, you can reference it by name. Run [`fastmcp discover`](/clients/cli#discovering-configured-servers) to see what's available.\n\n```bash\nfastmcp generate-cli claude-code:my-server output.py\n```\n\nThe `--timeout` and `--auth` flags work the same way they do in `fastmcp list` and `fastmcp call`.\n\n## What You Get\n\nThe generated script is a regular Python file — executable, editable, and yours. Here's what it looks like in practice:\n\n```\n$ python cli.py --help\nUsage: weather-cli COMMAND\n\nCLI for weather MCP server\n\nCommands:\n  call-tool       Call a tool on the server\n  list-tools      List available tools.\n  list-resources  List available resources.\n  read-resource   Read a resource by URI.\n  list-prompts    List available prompts.\n  get-prompt      Get a prompt by name. Pass arguments as key=value pairs.\n```\n\nThe `call-tool` subcommand is where the generated code lives. Each tool on the server becomes its own command:\n\n```\n$ python cli.py call-tool --help\nUsage: weather-cli call-tool COMMAND\n\nCall a tool on the server\n\nCommands:\n  get_forecast  Get the weather forecast for a city.\n  search_city   Search for a city by name.\n```\n\nAnd each tool has typed parameters with help text pulled directly from the server's schema:\n\n```\n$ python cli.py call-tool get_forecast --help\nUsage: weather-cli call-tool get_forecast [OPTIONS]\n\nGet the weather forecast for a city.\n\nOptions:\n  --city    [str]  City name (required)\n  --days    [int]  Number of forecast days (default: 3)\n```\n\nTool names are preserved exactly as the server defines them — underscores stay as underscores, so `call-tool get_forecast` matches what the server expects.\n\n## Agent Skill\n\nAlongside the CLI script, `generate-cli` also writes a `SKILL.md` file — a [Claude Code agent skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) that documents the generated CLI. The skill includes every tool's exact invocation syntax, parameter flags with types and descriptions, and the utility commands, so an agent can use the CLI immediately without running `--help` or experimenting with flag names.\n\nThe skill is written to the same directory as the CLI script. For a weather server, it looks something like:\n\n````markdown\n---\nname: \"weather-cli\"\ndescription: \"CLI for the weather MCP server. Call tools, list resources, and get prompts.\"\n---\n\n# weather CLI\n\n## Tool Commands\n\n### get_forecast\n\nGet the weather forecast for a city.\n\n```bash\nuv run --with fastmcp python cli.py call-tool get_forecast --city <value> --days <value>\n```\n\n| Flag | Type | Required | Description |\n|------|------|----------|-------------|\n| `--city` | string | yes | City name |\n| `--days` | integer | no | Number of forecast days |\n````\n\nTo skip skill generation, pass `--no-skill`:\n\n```bash\nfastmcp generate-cli weather --no-skill\n```\n\n## How It Works\n\nThe generated script is a client, not a server. It doesn't bundle or embed the MCP server — it connects to it on every invocation. For URL-based servers, the server needs to be running. For stdio-based servers, the command specified in `CLIENT_SPEC` must be available on the system's `PATH`.\n\nAt the top of the generated file, a `CLIENT_SPEC` variable holds the resolved transport: either a URL string or a `StdioTransport` with the command and arguments baked in. Every invocation connects through this spec, so the script works without any external configuration.\n\n### Parameter Handling\n\nParameters are mapped intelligently based on their complexity:\n\n**Simple types** (`string`, `integer`, `number`, `boolean`) become typed Python parameters with clean flags:\n```bash\npython cli.py call-tool get_forecast --city London --days 3\n```\n\n**Arrays of simple types** (`array` with `string`/`integer`/`number`/`boolean` items) become `list[T]` parameters that accept multiple flags:\n```bash\npython cli.py call-tool tag_items --tags python --tags fastapi --tags mcp\n```\n\n**Complex types** (objects, nested arrays, or unions) accept JSON strings. The tool's `--help` displays the full JSON schema so you know exactly what structure to pass:\n```bash\npython cli.py call-tool create_user \\\n  --name John \\\n  --metadata '{\"role\": \"admin\", \"dept\": \"engineering\"}'\n```\n\nRequired parameters are mandatory flags; optional ones default to their schema default or `None`. Empty values are filtered out before calling the server.\n\nBeyond tool commands, the script includes generic commands that work regardless of what the server exposes: `list-tools`, `list-resources`, `read-resource`, `list-prompts`, and `get-prompt`. These connect to the server at runtime, so they always reflect the server's current state even if the tools have changed since generation.\n\n## Editing the Output\n\nThe most common edit is changing `CLIENT_SPEC`. If you generated from a local dev server and want to point at production, just change the string. If you generated from a discovered name and want to pin the transport, replace it with an explicit URL or `StdioTransport`.\n\nBeyond that, it's a regular Python file. You can add commands, change the output formatting, integrate it into a larger application, or strip out the parts you don't need. The helper functions (`_call_tool`, `_print_tool_result`) are thin wrappers around `fastmcp.Client` that are easy to adapt.\n\nThe generated script requires `fastmcp` as a dependency. If the script lives outside a project that already has fastmcp installed, `uv run` is the easiest way to run it without permanent installation:\n\n```bash\nuv run --with fastmcp python cli.py call-tool get_forecast --city London\n```\n"
  },
  {
    "path": "docs/clients/logging.mdx",
    "content": "---\ntitle: Server Logging\nsidebarTitle: Logging\ndescription: Receive and handle log messages from MCP servers.\nicon: receipt\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nUse this when you need to capture or process log messages sent by the server.\n\nMCP servers can emit log messages to clients. The client handles these through a log handler callback.\n\n## Log Handler\n\nProvide a `log_handler` function when creating the client:\n\n```python\nimport logging\nfrom fastmcp import Client\nfrom fastmcp.client.logging import LogMessage\n\nlogging.basicConfig(\n    level=logging.INFO,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'\n)\n\nlogger = logging.getLogger(__name__)\nLOGGING_LEVEL_MAP = logging.getLevelNamesMapping()\n\nasync def log_handler(message: LogMessage):\n    \"\"\"Forward MCP server logs to Python's logging system.\"\"\"\n    msg = message.data.get('msg')\n    extra = message.data.get('extra')\n\n    level = LOGGING_LEVEL_MAP.get(message.level.upper(), logging.INFO)\n    logger.log(level, msg, extra=extra)\n\nclient = Client(\n    \"my_mcp_server.py\",\n    log_handler=log_handler,\n)\n```\n\nThe handler receives a `LogMessage` object:\n\n<Card icon=\"code\" title=\"LogMessage\">\n<ResponseField name=\"level\" type='Literal[\"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\", \"alert\", \"emergency\"]'>\n  The log level\n</ResponseField>\n\n<ResponseField name=\"logger\" type=\"str | None\">\n  The logger name (may be None)\n</ResponseField>\n\n<ResponseField name=\"data\" type=\"dict\">\n  The log payload, containing `msg` and `extra` keys\n</ResponseField>\n</Card>\n\n## Structured Logs\n\nThe `message.data` attribute is a dictionary containing the log payload. This enables structured logging with rich contextual information.\n\n```python\nasync def detailed_log_handler(message: LogMessage):\n    msg = message.data.get('msg')\n    extra = message.data.get('extra')\n\n    if message.level == \"error\":\n        print(f\"ERROR: {msg} | Details: {extra}\")\n    elif message.level == \"warning\":\n        print(f\"WARNING: {msg} | Details: {extra}\")\n    else:\n        print(f\"{message.level.upper()}: {msg}\")\n```\n\nThis structure is preserved even when logs are forwarded through a FastMCP proxy, making it useful for debugging multi-server applications.\n\n## Default Behavior\n\nIf you do not provide a custom `log_handler`, FastMCP's default handler routes server logs to Python's logging system at the appropriate severity level. The MCP levels map as follows: `notice` becomes INFO; `alert` and `emergency` become CRITICAL.\n\n```python\nclient = Client(\"my_mcp_server.py\")\n\nasync with client:\n    # Server logs are forwarded at proper severity automatically\n    await client.call_tool(\"some_tool\")\n```\n"
  },
  {
    "path": "docs/clients/notifications.mdx",
    "content": "---\ntitle: Notifications\nsidebarTitle: Notifications\ndescription: Handle server-sent notifications for list changes and other events.\nicon: envelope\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.9.1\" />\n\nUse this when you need to react to server-side changes like tool list updates or resource modifications.\n\nMCP servers can send notifications to inform clients about state changes. The message handler provides a unified way to process these notifications.\n\n## Handling Notifications\n\nThe simplest approach is a function that receives all messages and filters for the notifications you care about:\n\n```python\nfrom fastmcp import Client\n\nasync def message_handler(message):\n    \"\"\"Handle MCP notifications from the server.\"\"\"\n    if hasattr(message, 'root'):\n        method = message.root.method\n\n        if method == \"notifications/tools/list_changed\":\n            print(\"Tools have changed - refresh tool cache\")\n        elif method == \"notifications/resources/list_changed\":\n            print(\"Resources have changed\")\n        elif method == \"notifications/prompts/list_changed\":\n            print(\"Prompts have changed\")\n\nclient = Client(\n    \"my_mcp_server.py\",\n    message_handler=message_handler,\n)\n```\n\n## MessageHandler Class\n\nFor fine-grained targeting, subclass `MessageHandler` to use specific hooks:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.messages import MessageHandler\nimport mcp.types\n\nclass MyMessageHandler(MessageHandler):\n    async def on_tool_list_changed(\n        self, notification: mcp.types.ToolListChangedNotification\n    ) -> None:\n        \"\"\"Handle tool list changes.\"\"\"\n        print(\"Tool list changed - refreshing available tools\")\n\n    async def on_resource_list_changed(\n        self, notification: mcp.types.ResourceListChangedNotification\n    ) -> None:\n        \"\"\"Handle resource list changes.\"\"\"\n        print(\"Resource list changed\")\n\n    async def on_prompt_list_changed(\n        self, notification: mcp.types.PromptListChangedNotification\n    ) -> None:\n        \"\"\"Handle prompt list changes.\"\"\"\n        print(\"Prompt list changed\")\n\nclient = Client(\n    \"my_mcp_server.py\",\n    message_handler=MyMessageHandler(),\n)\n```\n\n### Handler Template\n\n```python\nfrom fastmcp.client.messages import MessageHandler\nimport mcp.types\n\nclass MyMessageHandler(MessageHandler):\n    async def on_message(self, message) -> None:\n        \"\"\"Called for ALL messages (requests and notifications).\"\"\"\n        pass\n\n    async def on_notification(\n        self, notification: mcp.types.ServerNotification\n    ) -> None:\n        \"\"\"Called for notifications (fire-and-forget).\"\"\"\n        pass\n\n    async def on_tool_list_changed(\n        self, notification: mcp.types.ToolListChangedNotification\n    ) -> None:\n        \"\"\"Called when the server's tool list changes.\"\"\"\n        pass\n\n    async def on_resource_list_changed(\n        self, notification: mcp.types.ResourceListChangedNotification\n    ) -> None:\n        \"\"\"Called when the server's resource list changes.\"\"\"\n        pass\n\n    async def on_prompt_list_changed(\n        self, notification: mcp.types.PromptListChangedNotification\n    ) -> None:\n        \"\"\"Called when the server's prompt list changes.\"\"\"\n        pass\n\n    async def on_progress(\n        self, notification: mcp.types.ProgressNotification\n    ) -> None:\n        \"\"\"Called for progress updates during long-running operations.\"\"\"\n        pass\n\n    async def on_logging_message(\n        self, notification: mcp.types.LoggingMessageNotification\n    ) -> None:\n        \"\"\"Called for log messages from the server.\"\"\"\n        pass\n```\n\n## List Change Notifications\n\nA practical example of maintaining a tool cache that refreshes when tools change:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.messages import MessageHandler\nimport mcp.types\n\nclass ToolCacheHandler(MessageHandler):\n    def __init__(self):\n        self.cached_tools = []\n\n    async def on_tool_list_changed(\n        self, notification: mcp.types.ToolListChangedNotification\n    ) -> None:\n        \"\"\"Clear tool cache when tools change.\"\"\"\n        print(\"Tools changed - clearing cache\")\n        self.cached_tools = []  # Force refresh on next access\n\nclient = Client(\"server.py\", message_handler=ToolCacheHandler())\n```\n\n## Server Requests\n\nWhile the message handler receives server-initiated requests, you should use dedicated callback parameters for most interactive scenarios:\n\n- **Sampling requests**: Use [`sampling_handler`](/clients/sampling)\n- **Elicitation requests**: Use [`elicitation_handler`](/clients/elicitation)\n- **Progress updates**: Use [`progress_handler`](/clients/progress)\n- **Log messages**: Use [`log_handler`](/clients/logging)\n\nThe message handler is primarily for monitoring and handling notifications rather than responding to requests.\n"
  },
  {
    "path": "docs/clients/progress.mdx",
    "content": "---\ntitle: Progress Monitoring\nsidebarTitle: Progress\ndescription: Handle progress notifications from long-running server operations.\nicon: bars-progress\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.3.5\" />\n\nUse this when you need to track progress of long-running operations.\n\nMCP servers can report progress during operations. The client receives these updates through a progress handler.\n\n## Progress Handler\n\nSet a handler when creating the client:\n\n```python\nfrom fastmcp import Client\n\nasync def progress_handler(\n    progress: float,\n    total: float | None,\n    message: str | None\n) -> None:\n    if total is not None:\n        percentage = (progress / total) * 100\n        print(f\"Progress: {percentage:.1f}% - {message or ''}\")\n    else:\n        print(f\"Progress: {progress} - {message or ''}\")\n\nclient = Client(\n    \"my_mcp_server.py\",\n    progress_handler=progress_handler\n)\n```\n\nThe handler receives three parameters:\n\n<Card icon=\"code\" title=\"Handler Parameters\">\n<ResponseField name=\"progress\" type=\"float\">\n  Current progress value\n</ResponseField>\n\n<ResponseField name=\"total\" type=\"float | None\">\n  Expected total value (may be None if unknown)\n</ResponseField>\n\n<ResponseField name=\"message\" type=\"str | None\">\n  Optional status message\n</ResponseField>\n</Card>\n\n## Per-Call Handler\n\nOverride the client-level handler for specific tool calls:\n\n```python\nasync with client:\n    result = await client.call_tool(\n        \"long_running_task\",\n        {\"param\": \"value\"},\n        progress_handler=my_progress_handler\n    )\n```\n"
  },
  {
    "path": "docs/clients/prompts.mdx",
    "content": "---\ntitle: Getting Prompts\nsidebarTitle: Prompts\ndescription: Retrieve rendered message templates with automatic argument serialization.\nicon: message-lines\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nUse this when you need to retrieve server-defined message templates for LLM interactions.\n\nPrompts are reusable message templates exposed by MCP servers. They can accept arguments to generate personalized message sequences for LLM interactions.\n\n## Basic Usage\n\nRequest a rendered prompt with `get_prompt()`:\n\n```python\nasync with client:\n    # Simple prompt without arguments\n    result = await client.get_prompt(\"welcome_message\")\n    # result -> mcp.types.GetPromptResult\n\n    # Access the generated messages\n    for message in result.messages:\n        print(f\"Role: {message.role}\")\n        print(f\"Content: {message.content}\")\n```\n\nPass arguments to customize the prompt:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"user_greeting\", {\n        \"name\": \"Alice\",\n        \"role\": \"administrator\"\n    })\n\n    for message in result.messages:\n        print(f\"Generated message: {message.content}\")\n```\n\n## Argument Serialization\n\n<VersionBadge version=\"2.9.0\" />\n\nFastMCP automatically serializes complex arguments to JSON strings as required by the MCP specification. You can pass typed objects directly:\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass UserData:\n    name: str\n    age: int\n\nasync with client:\n    result = await client.get_prompt(\"analyze_user\", {\n        \"user\": UserData(name=\"Alice\", age=30),     # Automatically serialized\n        \"preferences\": {\"theme\": \"dark\"},           # Dict serialized\n        \"scores\": [85, 92, 78],                     # List serialized\n        \"simple_name\": \"Bob\"                        # Strings unchanged\n    })\n```\n\nThe client handles serialization using `pydantic_core.to_json()` for consistent formatting. FastMCP servers automatically deserialize these JSON strings back to the expected types.\n\n## Working with Results\n\nThe `get_prompt()` method returns a `GetPromptResult` containing a list of messages:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"conversation_starter\", {\"topic\": \"climate\"})\n\n    for i, message in enumerate(result.messages):\n        print(f\"Message {i + 1}:\")\n        print(f\"  Role: {message.role}\")\n        print(f\"  Content: {message.content.text if hasattr(message.content, 'text') else message.content}\")\n```\n\nPrompts can generate different message types. System messages configure LLM behavior:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"system_configuration\", {\n        \"role\": \"helpful assistant\",\n        \"expertise\": \"python programming\"\n    })\n\n    # Access the returned messages\n    message = result.messages[0]\n    print(f\"Prompt: {message.content}\")\n```\n\nConversation templates generate multi-turn flows:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"interview_template\", {\n        \"candidate_name\": \"Alice\",\n        \"position\": \"Senior Developer\"\n    })\n\n    # Multiple messages for a conversation flow\n    for message in result.messages:\n        print(f\"{message.role}: {message.content}\")\n```\n\n## Version Selection\n\n<VersionBadge version=\"3.0.0\" />\n\nWhen a server exposes multiple versions of a prompt, you can request a specific version:\n\n```python\nasync with client:\n    # Get the highest version (default)\n    result = await client.get_prompt(\"summarize\", {\"text\": \"...\"})\n\n    # Get a specific version\n    result_v1 = await client.get_prompt(\"summarize\", {\"text\": \"...\"}, version=\"1.0\")\n```\n\nSee [Metadata](/servers/versioning#version-discovery) for how to discover available versions.\n\n## Multi-Server Clients\n\nWhen using multi-server clients, prompts are accessible directly without prefixing:\n\n```python\nasync with client:  # Multi-server client\n    result1 = await client.get_prompt(\"weather_prompt\", {\"city\": \"London\"})\n    result2 = await client.get_prompt(\"assistant_prompt\", {\"query\": \"help\"})\n```\n\n## Raw Protocol Access\n\nFor complete control, use `get_prompt_mcp()` which returns the full MCP protocol object:\n\n```python\nasync with client:\n    result = await client.get_prompt_mcp(\"example_prompt\", {\"arg\": \"value\"})\n    # result -> mcp.types.GetPromptResult\n```\n"
  },
  {
    "path": "docs/clients/resources.mdx",
    "content": "---\ntitle: Reading Resources\nsidebarTitle: Resources\ndescription: Access static and templated data sources from MCP servers.\nicon: folder-open\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nUse this when you need to read data from server-exposed resources like configuration files, generated content, or external data sources.\n\nResources are data sources exposed by MCP servers. They can be static files with fixed content, or dynamic templates that generate content based on parameters in the URI.\n\n## Reading Resources\n\nRead a resource using its URI:\n\n```python\nasync with client:\n    content = await client.read_resource(\"file:///path/to/README.md\")\n    # content -> list[TextResourceContents | BlobResourceContents]\n\n    # Access text content\n    if hasattr(content[0], 'text'):\n        print(content[0].text)\n\n    # Access binary content\n    if hasattr(content[0], 'blob'):\n        print(f\"Binary data: {len(content[0].blob)} bytes\")\n```\n\nResource templates generate content based on URI parameters. The template defines a pattern like `weather://{{city}}/current`, and you fill in the parameters when reading:\n\n```python\nasync with client:\n    # Read from a resource template\n    weather_content = await client.read_resource(\"weather://london/current\")\n    print(weather_content[0].text)\n```\n\n## Content Types\n\nResources return different content types depending on what they expose.\n\nText resources include configuration files, JSON data, and other human-readable content:\n\n```python\nasync with client:\n    content = await client.read_resource(\"resource://config/settings.json\")\n\n    for item in content:\n        if hasattr(item, 'text'):\n            print(f\"Text content: {item.text}\")\n            print(f\"MIME type: {item.mimeType}\")\n```\n\nBinary resources include images, PDFs, and other non-text data:\n\n```python\nasync with client:\n    content = await client.read_resource(\"resource://images/logo.png\")\n\n    for item in content:\n        if hasattr(item, 'blob'):\n            print(f\"Binary content: {len(item.blob)} bytes\")\n            print(f\"MIME type: {item.mimeType}\")\n\n            # Save to file\n            with open(\"downloaded_logo.png\", \"wb\") as f:\n                f.write(item.blob)\n```\n\n## Multi-Server Clients\n\nWhen using multi-server clients, resource URIs are prefixed with the server name:\n\n```python\nasync with client:  # Multi-server client\n    weather_icons = await client.read_resource(\"weather://weather/icons/sunny\")\n    templates = await client.read_resource(\"resource://assistant/templates/list\")\n```\n\n## Version Selection\n\n<VersionBadge version=\"3.0.0\" />\n\nWhen a server exposes multiple versions of a resource, you can request a specific version:\n\n```python\nasync with client:\n    # Read the highest version (default)\n    content = await client.read_resource(\"data://config\")\n\n    # Read a specific version\n    content_v1 = await client.read_resource(\"data://config\", version=\"1.0\")\n```\n\nSee [Metadata](/servers/versioning#version-discovery) for how to discover available versions.\n\n## Raw Protocol Access\n\nFor complete control, use `read_resource_mcp()` which returns the full MCP protocol object:\n\n```python\nasync with client:\n    result = await client.read_resource_mcp(\"resource://example\")\n    # result -> mcp.types.ReadResourceResult\n```\n"
  },
  {
    "path": "docs/clients/roots.mdx",
    "content": "---\ntitle: Client Roots\nsidebarTitle: Roots\ndescription: Provide local context and resource boundaries to MCP servers.\nicon: folder-tree\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nUse this when you need to tell servers what local resources the client has access to.\n\nRoots inform servers about resources the client can provide. Servers can use this information to adjust behavior or provide more relevant responses.\n\n## Static Roots\n\nProvide a list of roots when creating the client:\n\n```python\nfrom fastmcp import Client\n\nclient = Client(\n    \"my_mcp_server.py\",\n    roots=[\"/path/to/root1\", \"/path/to/root2\"]\n)\n```\n\n## Dynamic Roots\n\nUse a callback to compute roots dynamically when the server requests them:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.roots import RequestContext\n\nasync def roots_callback(context: RequestContext) -> list[str]:\n    print(f\"Server requested roots (Request ID: {context.request_id})\")\n    return [\"/path/to/root1\", \"/path/to/root2\"]\n\nclient = Client(\n    \"my_mcp_server.py\",\n    roots=roots_callback\n)\n```\n"
  },
  {
    "path": "docs/clients/sampling.mdx",
    "content": "---\ntitle: LLM Sampling\nsidebarTitle: Sampling\ndescription: Handle server-initiated LLM completion requests.\nicon: robot\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.0.0\" />\n\nUse this when you need to respond to server requests for LLM completions.\n\nMCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.\n\n## Handler Template\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext\n\nasync def sampling_handler(\n    messages: list[SamplingMessage],\n    params: SamplingParams,\n    context: RequestContext\n) -> str:\n    \"\"\"\n    Handle server requests for LLM completions.\n\n    Args:\n        messages: Conversation messages to send to the LLM\n        params: Sampling parameters (temperature, max_tokens, etc.)\n        context: Request context with metadata\n\n    Returns:\n        Generated text response from your LLM\n    \"\"\"\n    # Extract message content\n    conversation = []\n    for message in messages:\n        content = message.content.text if hasattr(message.content, 'text') else str(message.content)\n        conversation.append(f\"{message.role}: {content}\")\n\n    # Use the system prompt if provided\n    system_prompt = params.systemPrompt or \"You are a helpful assistant.\"\n\n    # Integrate with your LLM service here\n    return \"Generated response based on the messages\"\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=sampling_handler,\n)\n```\n\n## Handler Parameters\n\n<Card icon=\"code\" title=\"SamplingMessage\">\n<ResponseField name=\"role\" type='Literal[\"user\", \"assistant\"]'>\n  The role of the message\n</ResponseField>\n\n<ResponseField name=\"content\" type=\"TextContent | ImageContent | AudioContent\">\n  The content of the message. TextContent has a `.text` attribute.\n</ResponseField>\n</Card>\n\n<Card icon=\"code\" title=\"SamplingParams\">\n<ResponseField name=\"systemPrompt\" type=\"str | None\">\n  Optional system prompt the server wants to use\n</ResponseField>\n\n<ResponseField name=\"modelPreferences\" type=\"ModelPreferences | None\">\n  Server preferences for model selection (hints, cost/speed/intelligence priorities)\n</ResponseField>\n\n<ResponseField name=\"temperature\" type=\"float | None\">\n  Sampling temperature\n</ResponseField>\n\n<ResponseField name=\"maxTokens\" type=\"int\">\n  Maximum tokens to generate\n</ResponseField>\n\n<ResponseField name=\"stopSequences\" type=\"list[str] | None\">\n  Stop sequences for sampling\n</ResponseField>\n\n<ResponseField name=\"tools\" type=\"list[Tool] | None\">\n  Tools the LLM can use during sampling\n</ResponseField>\n\n<ResponseField name=\"toolChoice\" type=\"ToolChoice | None\">\n  Tool usage behavior (`auto`, `required`, or `none`)\n</ResponseField>\n</Card>\n\n## Built-in Handlers\n\nFastMCP provides built-in handlers for OpenAI, Anthropic, and Google Gemini APIs that support the full sampling API including tool use.\n\n### OpenAI Handler\n\n<VersionBadge version=\"2.11.0\" />\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=OpenAISamplingHandler(default_model=\"gpt-4o\"),\n)\n```\n\nFor OpenAI-compatible APIs (like local models):\n\n```python\nfrom openai import AsyncOpenAI\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=OpenAISamplingHandler(\n        default_model=\"llama-3.1-70b\",\n        client=AsyncOpenAI(base_url=\"http://localhost:8000/v1\"),\n    ),\n)\n```\n\n<Note>\nInstall the OpenAI handler with `pip install fastmcp[openai]`.\n</Note>\n\n### Anthropic Handler\n\n<VersionBadge version=\"2.14.1\" />\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=AnthropicSamplingHandler(default_model=\"claude-sonnet-4-5\"),\n)\n```\n\n<Note>\nInstall the Anthropic handler with `pip install fastmcp[anthropic]`.\n</Note>\n\n### Google Gemini Handler\n\n<VersionBadge version=\"3.1.0\" />\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling.handlers.google_genai import GoogleGenAISamplingHandler\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=GoogleGenAISamplingHandler(default_model=\"gemini-2.0-flash\"),\n)\n```\n\n<Note>\nInstall the Google Gemini handler with `pip install fastmcp[gemini]`.\n</Note>\n\n## Sampling Capabilities\n\nWhen you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support for simpler handlers:\n\n```python\nfrom mcp.types import SamplingCapability\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=basic_handler,\n    sampling_capabilities=SamplingCapability(),  # No tool support\n)\n```\n\n## Tool Execution\n\nTool 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.\n\n<Tip>\nTo 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.\n</Tip>\n"
  },
  {
    "path": "docs/clients/tasks.mdx",
    "content": "---\ntitle: Background Tasks\nsidebarTitle: Tasks\ndescription: Execute operations asynchronously and track their progress.\nicon: clock\ntag: \"NEW\"\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.14.0\" />\n\nUse this when you need to run long operations asynchronously while doing other work.\n\nThe MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results.\n\n## Requesting Background Execution\n\nPass `task=True` to run an operation as a background task:\n\n```python\nfrom fastmcp import Client\n\nasync with Client(server) as client:\n    # Start a background task\n    task = await client.call_tool(\"slow_computation\", {\"duration\": 10}, task=True)\n\n    print(f\"Task started: {task.task_id}\")\n\n    # Do other work while it runs...\n\n    # Get the result when ready\n    result = await task.result()\n```\n\nThis works with tools, resources, and prompts:\n\n```python\ntool_task = await client.call_tool(\"my_tool\", args, task=True)\nresource_task = await client.read_resource(\"file://large.txt\", task=True)\nprompt_task = await client.get_prompt(\"my_prompt\", args, task=True)\n```\n\n## Task API\n\nAll task types share a common interface.\n\n### Getting Results\n\nCall `await task.result()` or simply `await task` to block until the task completes:\n\n```python\ntask = await client.call_tool(\"analyze\", {\"text\": \"hello\"}, task=True)\n\n# Wait for result (blocking)\nresult = await task.result()\n# or: result = await task\n```\n\n### Checking Status\n\nCheck the current status without blocking:\n\n```python\nstatus = await task.status()\nprint(f\"{status.status}: {status.statusMessage}\")\n# status.status is \"working\", \"completed\", \"failed\", or \"cancelled\"\n```\n\n### Waiting with Control\n\nUse `task.wait()` for more control over waiting:\n\n```python\n# Wait up to 30 seconds for completion\nstatus = await task.wait(timeout=30.0)\n\n# Wait for a specific state\nstatus = await task.wait(state=\"completed\", timeout=30.0)\n```\n\n### Cancellation\n\nCancel a running task:\n\n```python\nawait task.cancel()\n```\n\n## Status Updates\n\nRegister callbacks to receive real-time status updates as the server reports progress:\n\n```python\ndef on_status_change(status):\n    print(f\"Task {status.taskId}: {status.status} - {status.statusMessage}\")\n\ntask.on_status_change(on_status_change)\n\n# Async callbacks work too\nasync def on_status_async(status):\n    await log_status(status)\n\ntask.on_status_change(on_status_async)\n```\n\n### Handler Template\n\n```python\nfrom fastmcp import Client\n\ndef status_handler(status):\n    \"\"\"\n    Handle task status updates.\n\n    Args:\n        status: Task status object with:\n            - taskId: Unique task identifier\n            - status: \"working\", \"completed\", \"failed\", or \"cancelled\"\n            - statusMessage: Optional progress message from server\n    \"\"\"\n    if status.status == \"working\":\n        print(f\"Progress: {status.statusMessage}\")\n    elif status.status == \"completed\":\n        print(\"Task completed\")\n    elif status.status == \"failed\":\n        print(f\"Task failed: {status.statusMessage}\")\n\ntask.on_status_change(status_handler)\n```\n\n## Graceful Degradation\n\nYou can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline.\n\n```python\ntask = await client.call_tool(\"my_tool\", args, task=True)\n\nif task.returned_immediately:\n    print(\"Server executed immediately (no background support)\")\nelse:\n    print(\"Running in background\")\n\n# Either way, this works\nresult = await task.result()\n```\n\nThis lets you write task-aware client code without worrying about server capabilities.\n\n## Example\n\n```python\nimport asyncio\nfrom fastmcp import Client\n\nasync def main():\n    async with Client(server) as client:\n        # Start background task\n        task = await client.call_tool(\n            \"slow_computation\",\n            {\"duration\": 10},\n            task=True,\n        )\n\n        # Subscribe to updates\n        def on_update(status):\n            print(f\"Progress: {status.statusMessage}\")\n\n        task.on_status_change(on_update)\n\n        # Do other work while task runs\n        print(\"Doing other work...\")\n        await asyncio.sleep(2)\n\n        # Wait for completion and get result\n        result = await task.result()\n        print(f\"Result: {result.content}\")\n\nasyncio.run(main())\n```\n\nSee [Server Background Tasks](/servers/tasks) for how to enable background task support on the server side.\n"
  },
  {
    "path": "docs/clients/tools.mdx",
    "content": "---\ntitle: Calling Tools\nsidebarTitle: Tools\ndescription: Execute server-side tools and handle structured results.\nicon: wrench\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nUse this when you need to execute server-side functions and process their results.\n\nTools are executable functions exposed by MCP servers. The client's `call_tool()` method executes a tool by name with arguments and returns structured results.\n\n## Basic Execution\n\n```python\nasync with client:\n    result = await client.call_tool(\"add\", {\"a\": 5, \"b\": 3})\n    # result -> CallToolResult with structured and unstructured data\n\n    # Access structured data (automatically deserialized)\n    print(result.data)  # 8\n\n    # Access traditional content blocks\n    print(result.content[0].text)  # \"8\"\n```\n\nArguments are passed as a dictionary. For multi-server clients, tool names are automatically prefixed with the server name (e.g., `weather_get_forecast` for a tool named `get_forecast` on the `weather` server).\n\n## Execution Options\n\nThe `call_tool()` method supports timeout control and progress monitoring:\n\n```python\nasync with client:\n    # With timeout (aborts if execution takes longer than 2 seconds)\n    result = await client.call_tool(\n        \"long_running_task\",\n        {\"param\": \"value\"},\n        timeout=2.0\n    )\n\n    # With progress handler\n    result = await client.call_tool(\n        \"long_running_task\",\n        {\"param\": \"value\"},\n        progress_handler=my_progress_handler\n    )\n```\n\n## Structured Results\n\n<VersionBadge version=\"2.10.0\" />\n\nTool execution returns a `CallToolResult` object. The `.data` property provides fully hydrated Python objects including complex types like datetimes and UUIDs, reconstructed from the server's output schema.\n\n```python\nfrom datetime import datetime\nfrom uuid import UUID\n\nasync with client:\n    result = await client.call_tool(\"get_weather\", {\"city\": \"London\"})\n\n    # FastMCP reconstructs complete Python objects\n    weather = result.data\n    print(f\"Temperature: {weather.temperature}C at {weather.timestamp}\")\n\n    # Complex types are properly deserialized\n    assert isinstance(weather.timestamp, datetime)\n    assert isinstance(weather.station_id, UUID)\n\n    # Raw structured JSON is also available\n    print(f\"Raw JSON: {result.structured_content}\")\n```\n\n<Card icon=\"code\" title=\"CallToolResult Properties\">\n<ResponseField name=\".data\" type=\"Any\">\n  Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). FastMCP exclusive.\n</ResponseField>\n\n<ResponseField name=\".content\" type=\"list[mcp.types.ContentBlock]\">\n  Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.).\n</ResponseField>\n\n<ResponseField name=\".structured_content\" type=\"dict[str, Any] | None\">\n  Standard MCP structured JSON data as sent by the server.\n</ResponseField>\n\n<ResponseField name=\".is_error\" type=\"bool\">\n  Boolean indicating if the tool execution failed.\n</ResponseField>\n</Card>\n\nFor tools without output schemas or when deserialization fails, `.data` will be `None`. Fall back to content blocks in that case:\n\n```python\nasync with client:\n    result = await client.call_tool(\"legacy_tool\", {\"param\": \"value\"})\n\n    if result.data is not None:\n        print(f\"Structured: {result.data}\")\n    else:\n        for content in result.content:\n            if hasattr(content, 'text'):\n                print(f\"Text result: {content.text}\")\n```\n\n<Tip>\nFastMCP servers automatically wrap primitive results (like `int`, `str`, `bool`) in a `{\"result\": value}` structure. FastMCP clients automatically unwrap this, so you get the original value in `.data`.\n</Tip>\n\n## Error Handling\n\nBy default, `call_tool()` raises a `ToolError` if the tool execution fails:\n\n```python\nfrom fastmcp.exceptions import ToolError\n\nasync with client:\n    try:\n        result = await client.call_tool(\"potentially_failing_tool\", {\"param\": \"value\"})\n        print(\"Tool succeeded:\", result.data)\n    except ToolError as e:\n        print(f\"Tool failed: {e}\")\n```\n\nTo handle errors manually instead of catching exceptions, disable automatic error raising:\n\n```python\nasync with client:\n    result = await client.call_tool(\n        \"potentially_failing_tool\",\n        {\"param\": \"value\"},\n        raise_on_error=False\n    )\n\n    if result.is_error:\n        print(f\"Tool failed: {result.content[0].text}\")\n    else:\n        print(f\"Tool succeeded: {result.data}\")\n```\n\n## Sending Metadata\n\n<VersionBadge version=\"2.13.1\" />\n\nThe `meta` parameter sends ancillary information alongside tool calls for observability, debugging, or client identification:\n\n```python\nasync with client:\n    result = await client.call_tool(\n        name=\"send_email\",\n        arguments={\n            \"to\": \"user@example.com\",\n            \"subject\": \"Hello\",\n            \"body\": \"Welcome!\"\n        },\n        meta={\n            \"trace_id\": \"abc-123\",\n            \"request_source\": \"mobile_app\"\n        }\n    )\n```\n\nSee [Client Metadata](/servers/context#client-metadata) to learn how servers access this data.\n\n## Raw Protocol Access\n\nFor complete control, use `call_tool_mcp()` which returns the raw MCP protocol object:\n\n```python\nasync with client:\n    result = await client.call_tool_mcp(\"my_tool\", {\"param\": \"value\"})\n    # result -> mcp.types.CallToolResult\n\n    if result.isError:\n        print(f\"Tool failed: {result.content}\")\n    else:\n        print(f\"Tool succeeded: {result.content}\")\n        # Note: No automatic deserialization with call_tool_mcp()\n```\n"
  },
  {
    "path": "docs/clients/transports.mdx",
    "content": "---\ntitle: Client Transports\nsidebarTitle: Transports\ndescription: Configure how clients connect to and communicate with MCP servers.\nicon: link\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.0.0\" />\n\nTransports handle the underlying connection between your client and MCP servers. While the client can automatically select a transport based on what you pass to it, instantiating transports explicitly gives you full control over configuration.\n\n## STDIO Transport\n\nSTDIO transport communicates with MCP servers through subprocess pipes. When using STDIO, your client launches and manages the server process, controlling its lifecycle and environment.\n\n<Warning>\nSTDIO servers run in isolated environments by default. They do not inherit your shell's environment variables. You must explicitly pass any configuration the server needs.\n</Warning>\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StdioTransport\n\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"my_server.py\", \"--verbose\"],\n    env={\"API_KEY\": \"secret\", \"LOG_LEVEL\": \"DEBUG\"},\n    cwd=\"/path/to/server\"\n)\nclient = Client(transport)\n```\n\nFor convenience, the client can infer STDIO transport from file paths, though this limits configuration options:\n\n```python\nfrom fastmcp import Client\n\nclient = Client(\"my_server.py\")  # Limited - no configuration options\n```\n\n### Environment Variables\n\nSince STDIO servers do not inherit your environment, you need strategies for passing configuration.\n\n**Selective forwarding** passes only the variables your server needs:\n\n```python\nimport os\nfrom fastmcp.client.transports import StdioTransport\n\nrequired_vars = [\"API_KEY\", \"DATABASE_URL\", \"REDIS_HOST\"]\nenv = {var: os.environ[var] for var in required_vars if var in os.environ}\n\ntransport = StdioTransport(command=\"python\", args=[\"server.py\"], env=env)\nclient = Client(transport)\n```\n\n**Loading from .env files** keeps configuration separate from code:\n\n```python\nfrom dotenv import dotenv_values\nfrom fastmcp.client.transports import StdioTransport\n\nenv = dotenv_values(\".env\")\ntransport = StdioTransport(command=\"python\", args=[\"server.py\"], env=env)\nclient = Client(transport)\n```\n\n### Session Persistence\n\nSTDIO transports maintain sessions across multiple client contexts by default (`keep_alive=True`). This reuses the same subprocess for multiple connections, improving performance.\n\n```python\nfrom fastmcp.client.transports import StdioTransport\n\ntransport = StdioTransport(command=\"python\", args=[\"server.py\"])\nclient = Client(transport)\n\nasync def efficient_multiple_operations():\n    async with client:\n        await client.ping()\n\n    async with client:  # Reuses the same subprocess\n        await client.call_tool(\"process_data\", {\"file\": \"data.csv\"})\n```\n\nFor complete isolation between connections, disable session persistence:\n\n```python\ntransport = StdioTransport(command=\"python\", args=[\"server.py\"], keep_alive=False)\n```\n\n## HTTP Transport\n\n<VersionBadge version=\"2.3.0\" />\n\nHTTP transport connects to MCP servers running as web services. This is the recommended transport for production deployments.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\ntransport = StreamableHttpTransport(\n    url=\"https://api.example.com/mcp\",\n    headers={\n        \"Authorization\": \"Bearer your-token-here\",\n        \"X-Custom-Header\": \"value\"\n    }\n)\nclient = Client(transport)\n```\n\nFastMCP also provides authentication helpers:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import BearerAuth\n\nclient = Client(\n    \"https://api.example.com/mcp\",\n    auth=BearerAuth(\"your-token-here\")\n)\n```\n\n### SSL Verification\n\nBy default, HTTPS connections verify the server's SSL certificate. You can customize this behavior with the `verify` parameter, which accepts the same values as [httpx](https://www.python-httpx.org/advanced/ssl/):\n\n```python\nfrom fastmcp import Client\n\n# Disable SSL verification (e.g., for self-signed certs in development)\nclient = Client(\"https://dev-server.internal/mcp\", verify=False)\n\n# Use a custom CA bundle\nclient = Client(\"https://corp-server.internal/mcp\", verify=\"/path/to/ca-bundle.pem\")\n\n# Use a custom SSL context for full control\nimport ssl\nctx = ssl.create_default_context()\nctx.load_verify_locations(\"/path/to/internal-ca.pem\")\nclient = Client(\"https://corp-server.internal/mcp\", verify=ctx)\n```\n\nThe `verify` parameter is also available directly on `StreamableHttpTransport` and `SSETransport`:\n\n```python\nfrom fastmcp.client.transports import StreamableHttpTransport\n\ntransport = StreamableHttpTransport(\n    url=\"https://dev-server.internal/mcp\",\n    verify=False,\n)\nclient = Client(transport)\n```\n\n### SSE Transport\n\nServer-Sent Events transport is maintained for backward compatibility. Use Streamable HTTP for new deployments unless you have specific infrastructure requirements.\n\n```python\nfrom fastmcp.client.transports import SSETransport\n\ntransport = SSETransport(\n    url=\"https://api.example.com/sse\",\n    headers={\"Authorization\": \"Bearer token\"}\n)\nclient = Client(transport)\n```\n\n## In-Memory Transport\n\nIn-memory transport connects directly to a FastMCP server instance within the same Python process. This eliminates both subprocess management and network overhead, making it ideal for testing.\n\n```python\nfrom fastmcp import FastMCP, Client\nimport os\n\nmcp = FastMCP(\"TestServer\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    prefix = os.environ.get(\"GREETING_PREFIX\", \"Hello\")\n    return f\"{prefix}, {name}!\"\n\nclient = Client(mcp)\n\nasync with client:\n    result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n```\n\n<Note>\nUnlike STDIO transports, in-memory servers share the same memory space and environment variables as your client code.\n</Note>\n\n## Multi-Server Configuration\n\n<VersionBadge version=\"2.4.0\" />\n\nConnect to multiple servers defined in a configuration dictionary:\n\n```python\nfrom fastmcp import Client\n\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\"\n        },\n        \"assistant\": {\n            \"command\": \"python\",\n            \"args\": [\"./assistant.py\"],\n            \"env\": {\"LOG_LEVEL\": \"INFO\"}\n        }\n    }\n}\n\nclient = Client(config)\n\nasync with client:\n    # Tools are namespaced by server\n    weather = await client.call_tool(\"weather_get_forecast\", {\"city\": \"NYC\"})\n    answer = await client.call_tool(\"assistant_ask\", {\"question\": \"What?\"})\n```\n\n### Tool Transformations\n\nFastMCP supports tool transformations within the configuration. You can change names, descriptions, tags, and arguments for tools from a server.\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\",\n            \"tools\": {\n                \"weather_get_forecast\": {\n                    \"name\": \"miami_weather\",\n                    \"description\": \"Get the weather for Miami\",\n                    \"arguments\": {\n                        \"city\": {\n                            \"default\": \"Miami\",\n                            \"hide\": True,\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\nTo filter tools by tag, use `include_tags` or `exclude_tags` at the server level:\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"include_tags\": [\"forecast\"]  # Only tools with this tag\n        }\n    }\n}\n```\n"
  },
  {
    "path": "docs/community/README.md",
    "content": "# Community Section\n\nThis directory contains community-contributed content and showcases for FastMCP.\n\n## Structure\n\n- `showcase.mdx` - Main community showcase page featuring high-quality projects and examples\n\n## Adding Content\n\nTo add new community content:\n1. Create a new MDX file in this directory\n2. Update `docs.json` to include it in the navigation\n3. Follow the existing format for consistency\n\n## Guidelines\n\nCommunity content should:\n- Demonstrate best practices\n- Provide educational value\n- Include proper documentation\n- Be maintained and up-to-date"
  },
  {
    "path": "docs/community/showcase.mdx",
    "content": "---\ntitle: 'Community Showcase'\ndescription: 'High-quality projects and examples from the FastMCP community'\nicon: 'users'\n---\n\nimport { YouTubeEmbed } from '/snippets/youtube-embed.mdx'\n\n## Join the Community\n\n<Card title=\"FastMCP Discord\" icon=\"discord\" href=\"https://discord.gg/uu8dJCgttd\">\n  Connect with other FastMCP developers, share your projects, and discuss ideas.\n</Card>\n\n## Featured Projects\n\nDiscover exemplary MCP servers and implementations created by our community. These projects demonstrate best practices and innovative uses of FastMCP.\n\n### Learning Resources\n\n<Card title=\"MCP Dummy Server\" icon=\"graduation-cap\" href=\"https://github.com/WaiYanNyeinNaing/mcp-dummy-server\">\n  A comprehensive educational example demonstrating FastMCP best practices with professional dual-transport server implementation, interactive test client, and detailed documentation.\n</Card>\n\n#### Video Tutorials\n\n**Build Remote MCP Servers w/ Python & FastMCP** - Claude Integrations Tutorial by Greg + Code\n\n<YouTubeEmbed \n  videoId=\"bOYkbXP-GGo\" \n  title=\"Build Remote MCP Servers w/ Python & FastMCP\" \n/>\n\n**FastMCP — the best way to build an MCP server with Python** - Tutorial by ZazenCodes\n\n<YouTubeEmbed \n  videoId=\"rnljvmHorQw\" \n  title=\"FastMCP — the best way to build an MCP server with Python\" \n/>\n\n**Speedrun a MCP server for Claude Desktop (fastmcp)** - Tutorial by Nate from Prefect\n\n<YouTubeEmbed \n  videoId=\"67ZwpkUEtSI\" \n  title=\"Speedrun a MCP server for Claude Desktop (fastmcp)\" \n/>\n\n### Community Examples\n\nHave you built something interesting with FastMCP? We'd love to feature high-quality examples here! Start a [discussion on GitHub](https://github.com/PrefectHQ/fastmcp/discussions) to share your project.\n\n## Contributing\n\nTo get your project featured:\n\n1. Ensure your project demonstrates best practices\n2. Include comprehensive documentation\n3. Add clear usage examples\n4. Open a discussion in our [GitHub Discussions](https://github.com/PrefectHQ/fastmcp/discussions)\n\nWe review submissions regularly and feature projects that provide value to the FastMCP community.\n\n## Further Reading\n\n- [Contrib Modules](/patterns/contrib) - Community-contributed modules that are distributed with FastMCP itself"
  },
  {
    "path": "docs/css/banner.css",
    "content": "/* Banner styling -- improve readability with better contrast */\n#banner {\n  background: #f1f5f9 !important;\n  color: #1e293b !important;\n  font-size: 0.95rem !important;\n  font-weight: 600 !important;\n  padding-top: 12px !important;\n  padding-bottom: 12px !important;\n  overflow: hidden !important;\n}\n\n#banner::before {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background: linear-gradient(\n    90deg,\n    rgba(6, 182, 212, 0.25) 0%,\n    rgba(6, 182, 212, 0.05) 25%,\n    rgba(6, 182, 212, 0.35) 50%,\n    rgba(6, 182, 212, 0.08) 75%,\n    rgba(6, 182, 212, 0.28) 100%\n  );\n  background-size: 300% 100%;\n  animation: colorWave 14s ease-in-out infinite alternate;\n  pointer-events: none;\n}\n\n.dark #banner {\n  background: #475569 !important;\n  color: #f1f5f9 !important;\n}\n\n.dark #banner::before {\n  background: linear-gradient(\n    90deg,\n    rgba(247, 37, 133, 0.35) 0%,\n    rgba(247, 37, 133, 0.08) 25%,\n    rgba(247, 37, 133, 0.45) 50%,\n    rgba(247, 37, 133, 0.12) 75%,\n    rgba(247, 37, 133, 0.38) 100%\n  );\n  background-size: 300% 100%;\n}\n\n@keyframes colorWave {\n  0% {\n    background-position: 0% 0%;\n  }\n  100% {\n    background-position: 100% 0%;\n  }\n}\n\n#banner * {\n  color: #1e293b !important;\n  margin: 0 !important;\n}\n\n.dark #banner * {\n  color: #f1f5f9 !important;\n}\n\n@media (max-width: 767px) {\n  #banner {\n    font-size: 0.8rem !important;\n    padding-top: 8px !important;\n    padding-bottom: 8px !important;\n  }\n}\n\n"
  },
  {
    "path": "docs/css/python-sdk.css",
    "content": "a:has(svg.icon) {\n  border: none !important;\n}"
  },
  {
    "path": "docs/css/style.css",
    "content": "html:not([data-page-mode=\"wide\"]) #content-area {\n  max-width: 44rem !important;\n}\n\nimg.nav-logo {\n  max-width: 200px;\n}\n\n/* Code highlighting -- target only inline code elements, not code blocks */\np code:not(pre code),\ntable code:not(pre code),\n.prose code:not(pre code),\nli code:not(pre code),\nh1 code:not(pre code),\nh2 code:not(pre code),\nh3 code:not(pre code),\nh4 code:not(pre code),\nh5 code:not(pre code),\nh6 code:not(pre code) {\n  color: #f72585 !important;\n  background-color: rgba(247, 37, 133, 0.09);\n}\n\n/* V2 banner - inside content-container, breaks out of padding with negative margins */\n#v2-banner {\n  display: block;\n  background: linear-gradient(135deg, #4cc9f0 0%, #2d00f7 100%);\n  color: white;\n  text-align: center;\n  padding: 10px 16px;\n  font-size: 0.875rem;\n  font-weight: 600;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  margin: -2rem -2rem 1.5rem -2rem;\n  width: calc(100% + 4rem);\n  border-radius: 8px 8px 0 0;\n}\n\n#v2-banner a {\n  color: white;\n  text-decoration: underline;\n  font-weight: 700;\n}\n\n#v2-banner a:hover {\n  opacity: 0.9;\n}\n\n@media (min-width: 1024px) {\n  #v2-banner {\n    margin: -3rem -4rem 1.5rem -4rem;\n    width: calc(100% + 8rem);\n  }\n}\n\n.dark #v2-banner {\n  background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);\n}\n\n\n\n\n\n"
  },
  {
    "path": "docs/css/version-badge.css",
    "content": "/* Version badge -- display a badge with the current version of the documentation */\n.version-badge {\n  --color-text: #ff5400 !important;\n  --color-bg: #fef2f2 !important;\n  color: #ff5400 !important;\n  background: #fef2f2 !important;\n  border: 1px solid rgba(220, 38, 38, 0.3) !important;\n  transition: box-shadow 0.2s, transform 0.15s;\n}\n\n.version-badge:hover {\n  box-shadow: 0 2px 8px 0 rgba(160, 132, 252, 0.1);\n  transform: translateY(-1px) scale(1.03);\n}\n\n.dark .version-badge {\n  --color-text: #f1f5f9 !important;\n  --color-bg: #334155 !important;\n  color: #f1f5f9 !important;\n  background: #334155 !important;\n  border: 1px solid #64748b !important;\n}\n\n"
  },
  {
    "path": "docs/deployment/http.mdx",
    "content": "---\ntitle: HTTP Deployment\nsidebarTitle: HTTP Deployment\ndescription: Deploy your FastMCP server over HTTP for remote access\nicon: server\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<Tip>\nSTDIO transport is perfect for local development and desktop applications. But to unlock the full potential of MCP—centralized services, multi-client access, and network availability—you need remote HTTP deployment.\n</Tip>\n\nThis guide walks you through deploying your FastMCP server as a remote MCP service that's accessible via a URL. Once deployed, your MCP server will be available over the network, allowing multiple clients to connect simultaneously and enabling integration with cloud-based LLM applications. This guide focuses specifically on remote MCP deployment, not local STDIO servers.\n\n## Choosing Your Approach\n\nFastMCP provides two ways to deploy your server as an HTTP service. Understanding the trade-offs helps you choose the right approach for your needs.\n\nThe **direct HTTP server** approach is simpler and perfect for getting started quickly. You modify your server's `run()` method to use HTTP transport, and FastMCP handles all the web server configuration. This approach works well for standalone deployments where you want your MCP server to be the only service running on a port.\n\nThe **ASGI application** approach gives you more control and flexibility. Instead of running the server directly, you create an ASGI application that can be served by Uvicorn. This approach is better when you need advanced server features like multiple workers, custom middleware, or when you're integrating with existing web applications.\n\n### Direct HTTP Server\n\nThe simplest way to get your MCP server online is to use the built-in `run()` method with HTTP transport. This approach handles all the server configuration for you and is ideal when you want a standalone MCP server without additional complexity.\n\n```python server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\ndef process_data(input: str) -> str:\n    \"\"\"Process data on the server\"\"\"\n    return f\"Processed: {input}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", host=\"0.0.0.0\", port=8000)\n```\n\nRun your server with a simple Python command:\n```bash\npython server.py\n```\n\nYour server is now accessible at `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).\n\nThis approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.\n\n### ASGI Application\n\nFor production deployments, you'll often want more control over how your server runs. FastMCP can create a standard ASGI application that works with any ASGI server like Uvicorn, Gunicorn, or Hypercorn. This approach is particularly useful when you need to configure advanced server options, run multiple workers, or integrate with existing infrastructure.\n\n```python app.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\ndef process_data(input: str) -> str:\n    \"\"\"Process data on the server\"\"\"\n    return f\"Processed: {input}\"\n\n# Create ASGI application\napp = mcp.http_app()\n```\n\nRun with any ASGI server - here's an example with Uvicorn:\n```bash\nuvicorn app:app --host 0.0.0.0 --port 8000\n```\n\nYour server is accessible at the same URL: `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).\n\nThe ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application.\n\n## Configuring Your Server\n\n### Custom Path\n\nBy default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.\n\n```python\n# Option 1: With mcp.run()\nmcp.run(transport=\"http\", host=\"0.0.0.0\", port=8000, path=\"/api/mcp/\")\n\n# Option 2: With ASGI app\napp = mcp.http_app(path=\"/api/mcp/\")\n```\n\nNow your server is accessible at `http://localhost:8000/api/mcp/`.\n\n### Authentication\n\n<Warning>\nAuthentication is **highly recommended** for remote MCP servers. Some LLM clients require authentication for remote servers and will refuse to connect without it.\n</Warning>\n\nFastMCP supports multiple authentication methods to secure your remote server. See the [Authentication Overview](/servers/auth/authentication) for complete configuration options including Bearer tokens, JWT, and OAuth.\n\nIf you're mounting an authenticated server under a path prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) below for important routing considerations.\n\n### Health Checks\n\nHealth check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.\n\n```python\nfrom starlette.responses import JSONResponse\n\n@mcp.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check(request):\n    return JSONResponse({\"status\": \"healthy\", \"service\": \"mcp-server\"})\n```\n\nThis health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.\n\n### Custom Middleware\n\n\n<VersionBadge version=\"2.3.2\" />\n\nAdd custom Starlette middleware to your FastMCP ASGI apps:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.cors import CORSMiddleware\n\n# Create your FastMCP server\nmcp = FastMCP(\"MyServer\")\n\n# Define middleware\nmiddleware = [\n    Middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n]\n\n# Create ASGI app with middleware\nhttp_app = mcp.http_app(middleware=middleware)\n```\n\n### CORS for Browser-Based Clients\n\n<Tip>\nMost MCP clients, including those that you access through a browser like ChatGPT or Claude, don't need CORS configuration. Only enable CORS if you're working with an MCP client that connects directly from a browser, such as debugging tools or inspectors.\n</Tip>\n\nCORS (Cross-Origin Resource Sharing) is needed when JavaScript running in a web browser connects directly to your MCP server. This is different from using an LLM through a browser—in that case, the browser connects to the LLM service, and the LLM service connects to your MCP server (no CORS needed).\n\nBrowser-based MCP clients that need CORS include:\n\n- **MCP Inspector** - Browser-based debugging tool for testing MCP servers\n- **Custom browser-based MCP clients** - If you're building a web app that directly connects to MCP servers\n\nFor these scenarios, add CORS middleware with the specific headers required for MCP protocol:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.cors import CORSMiddleware\n\nmcp = FastMCP(\"MyServer\")\n\n# Configure CORS for browser-based clients\nmiddleware = [\n    Middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],  # Allow all origins; use specific origins for security\n        allow_methods=[\"GET\", \"POST\", \"DELETE\", \"OPTIONS\"],\n        allow_headers=[\n            \"mcp-protocol-version\",\n            \"mcp-session-id\",\n            \"Authorization\",\n            \"Content-Type\",\n        ],\n        expose_headers=[\"mcp-session-id\"],\n    )\n]\n\napp = mcp.http_app(middleware=middleware)\n```\n\n**Key configuration details:**\n\n- **`allow_origins`**: Specify exact origins (e.g., `[\"http://localhost:3000\"]`) rather than `[\"*\"]` for production deployments\n- **`allow_headers`**: Must include `mcp-protocol-version`, `mcp-session-id`, and `Authorization` (for authenticated servers)\n- **`expose_headers`**: Must include `mcp-session-id` so JavaScript can read the session ID from responses and send it in subsequent requests\n\nWithout `expose_headers=[\"mcp-session-id\"]`, browsers will receive the session ID but JavaScript won't be able to access it, causing session management to fail.\n\n<Warning>\n**Production Security**: Never use `allow_origins=[\"*\"]` in production. Specify the exact origins of your browser-based clients. Using wildcards exposes your server to unauthorized access from any website.\n</Warning>\n\n### SSE Polling for Long-Running Operations\n\n<VersionBadge version=\"2.14.0\" />\n\n<Note>\nThis feature only applies to the **StreamableHTTP transport** (the default for `http_app()`). It does not apply to the legacy SSE transport (`transport=\"sse\"`).\n</Note>\n\nWhen running tools that take a long time to complete, you may encounter issues with load balancers or proxies terminating connections that stay idle too long. [SEP-1699](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699) introduces SSE polling to solve this by allowing the server to gracefully close connections and have clients automatically reconnect.\n\nTo enable SSE polling, configure an `EventStore` when creating your HTTP application:\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom fastmcp.server.event_store import EventStore\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\nasync def long_running_task(ctx: Context) -> str:\n    \"\"\"A task that takes several minutes to complete.\"\"\"\n    for i in range(100):\n        await ctx.report_progress(i, 100)\n\n        # Periodically close the connection to avoid load balancer timeouts\n        # Client will automatically reconnect and resume receiving progress\n        if i % 30 == 0 and i > 0:\n            await ctx.close_sse_stream()\n\n        await do_expensive_work()\n\n    return \"Done!\"\n\n# Configure with EventStore for resumability\nevent_store = EventStore()\napp = mcp.http_app(\n    event_store=event_store,\n    retry_interval=2000,  # Client reconnects after 2 seconds\n)\n```\n\n**How it works:**\n\n1. When `event_store` is configured, the server stores all events (progress updates, results) with unique IDs\n2. Calling `ctx.close_sse_stream()` gracefully closes the HTTP connection\n3. The client automatically reconnects with a `Last-Event-ID` header\n4. The server replays any events the client missed during the disconnection\n\nThe `retry_interval` parameter (in milliseconds) controls how long clients wait before reconnecting. Choose a value that balances responsiveness with server load.\n\n<Note>\n`close_sse_stream()` is a no-op if called without an `EventStore` configured, so you can safely include it in tools that may run in different deployment configurations.\n</Note>\n\n#### Custom Storage Backends\n\nBy default, `EventStore` uses in-memory storage. For production deployments with multiple server instances, you can provide a custom storage backend using the `key_value` package:\n\n```python\nfrom fastmcp.server.event_store import EventStore\nfrom key_value.aio.stores.redis import RedisStore\n\n# Use Redis for distributed deployments\nredis_store = RedisStore(url=\"redis://localhost:6379\")\nevent_store = EventStore(\n    storage=redis_store,\n    max_events_per_stream=100,  # Keep last 100 events per stream\n    ttl=3600,  # Events expire after 1 hour\n)\n\napp = mcp.http_app(event_store=event_store)\n```\n\n## Integration with Web Frameworks\n\nIf you already have a web application running, you can add MCP capabilities by mounting a FastMCP server as a sub-application. This allows you to expose MCP tools alongside your existing API endpoints, sharing the same domain and infrastructure. The MCP server becomes just another route in your application, making it easy to manage and deploy.\n\n### Mounting in Starlette\n\nMount your FastMCP server in a Starlette application:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\n# Create your FastMCP server\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef analyze(data: str) -> dict:\n    return {\"result\": f\"Analyzed: {data}\"}\n\n# Create the ASGI app\nmcp_app = mcp.http_app(path='/mcp')\n\n# Create a Starlette app and mount the MCP server\napp = Starlette(\n    routes=[\n        Mount(\"/mcp-server\", app=mcp_app),\n        # Add other routes as needed\n    ],\n    lifespan=mcp_app.lifespan,\n)\n```\n\nThe MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.\n\n<Warning>\nFor Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.\n</Warning>\n\n#### Nested Mounts\n\nYou can create complex routing structures by nesting mounts:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\n# Create your FastMCP server\nmcp = FastMCP(\"MyServer\")\n\n# Create the ASGI app\nmcp_app = mcp.http_app(path='/mcp')\n\n# Create nested application structure\ninner_app = Starlette(routes=[Mount(\"/inner\", app=mcp_app)])\napp = Starlette(\n    routes=[Mount(\"/outer\", app=inner_app)],\n    lifespan=mcp_app.lifespan,\n)\n```\n\nIn this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.\n\n### FastAPI Integration\n\nFor FastAPI-specific integration patterns including both mounting MCP servers into FastAPI apps and generating MCP servers from FastAPI apps, see the [FastAPI Integration guide](/integrations/fastapi).\n\nHere's a quick example showing how to add MCP to an existing FastAPI application:\n\n```python\nfrom fastapi import FastAPI\nfrom fastmcp import FastMCP\n\n# Create your MCP server\nmcp = FastMCP(\"API Tools\")\n\n@mcp.tool\ndef query_database(query: str) -> dict:\n    \"\"\"Run a database query\"\"\"\n    return {\"result\": \"data\"}\n\n# Create the MCP ASGI app with path=\"/\" since we'll mount at /mcp\nmcp_app = mcp.http_app(path=\"/\")\n\n# Create FastAPI app with MCP lifespan (required for session management)\napi = FastAPI(lifespan=mcp_app.lifespan)\n\n@api.get(\"/api/status\")\ndef status():\n    return {\"status\": \"ok\"}\n\n# Mount MCP at /mcp\napi.mount(\"/mcp\", mcp_app)\n\n# Run with: uvicorn app:api --host 0.0.0.0 --port 8000\n```\n\nYour existing API remains at `http://localhost:8000/api` while MCP is available at `http://localhost:8000/mcp`.\n\n<Warning>\nJust like with Starlette, you **must** pass the lifespan from the MCP app to FastAPI. Without this, the session manager won't initialize properly and requests will fail.\n</Warning>\n\n## Mounting Authenticated Servers\n\n<VersionBadge version=\"2.13.0\" />\n\n<Tip>\nThis section only applies if you're **mounting an OAuth-protected FastMCP server under a path prefix** (like `/api`) inside another application using `Mount()`.\n\nIf you're deploying your FastMCP server at root level without any `Mount()` prefix, the well-known routes are automatically included in `mcp.http_app()` and you don't need to do anything special.\n</Tip>\n\nOAuth specifications (RFC 8414 and RFC 9728) require discovery metadata to be accessible at well-known paths under the root level of your domain. When you mount an OAuth-protected FastMCP server under a path prefix like `/api`, this creates a routing challenge: your operational OAuth endpoints move under the prefix, but discovery endpoints must remain at the root.\n\n<Warning>\n**Common Mistakes to Avoid:**\n\n1. **Forgetting to mount `.well-known` routes at root** - FastMCP cannot do this automatically when your server is mounted under a path prefix. You must explicitly mount well-known routes at the root level.\n\n2. **Including mount prefix in both base_url AND mcp_path** - The mount prefix (like `/api`) should only be in `base_url`, not in `mcp_path`. Otherwise you'll get double paths.\n\n   ✅ **Correct:**\n   ```python\n   base_url = \"http://localhost:8000/api\"\n   mcp_path = \"/mcp\"\n   # Result: /api/mcp\n   ```\n\n   ❌ **Wrong:**\n   ```python\n   base_url = \"http://localhost:8000/api\"\n   mcp_path = \"/api/mcp\"\n   # Result: /api/api/mcp (double prefix!)\n   ```\n\nFollow the configuration instructions below to set up mounting correctly.\n</Warning>\n\n<Warning>\n**CORS Middleware Conflicts:**\n\nIf you're integrating FastMCP into an existing application with its own CORS middleware, be aware that layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).\n\nFastMCP and the MCP SDK already handle CORS for OAuth routes. If you need CORS on your own application routes, consider using the sub-app pattern: mount FastMCP and your routes as separate apps, each with their own middleware, rather than adding application-wide CORS middleware.\n</Warning>\n\n### Route Types\n\nOAuth-protected MCP servers expose two categories of routes:\n\n**Operational routes** handle the OAuth flow and MCP protocol:\n- `/authorize` - OAuth authorization endpoint\n- `/token` - Token exchange endpoint\n- `/auth/callback` - OAuth callback handler\n- `/mcp` - MCP protocol endpoint\n\n**Discovery routes** provide metadata for OAuth clients:\n- `/.well-known/oauth-authorization-server` - Authorization server metadata\n- `/.well-known/oauth-protected-resource/*` - Protected resource metadata\n\nWhen you mount your MCP app under a prefix, operational routes move with it, but discovery routes must stay at root level for RFC compliance.\n\n### Configuration Parameters\n\nThree parameters control where routes are located and how they combine:\n\n**`base_url`** tells clients where to find operational endpoints. This includes any Starlette `Mount()` path prefix (e.g., `/api`):\n\n```python\nbase_url=\"http://localhost:8000/api\"  # Includes mount prefix\n```\n\n**`mcp_path`** is the internal FastMCP endpoint path, which gets appended to `base_url`:\n\n```python\nmcp_path=\"/mcp\"  # Internal MCP path, NOT the mount prefix\n```\n\n**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.\n\n```python\n# Usually not needed - just set base_url and it works\nissuer_url=\"http://localhost:8000\"  # Only if you want root-level discovery\n```\n\nWhen `issuer_url` has a path (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.\n\n**Key Invariant:** `base_url + mcp_path = actual externally-accessible MCP URL`\n\nExample:\n- `base_url`: `http://localhost:8000/api` (mount prefix `/api`)\n- `mcp_path`: `/mcp` (internal path)\n- Result: `http://localhost:8000/api/mcp` (final MCP endpoint)\n\nNote that the mount prefix (`/api` from `Mount(\"/api\", ...)`) goes in `base_url`, while `mcp_path` is just the internal MCP route. Don't include the mount prefix in both places or you'll get `/api/api/mcp`.\n\n### Mounting Strategy\n\nWhen mounting an OAuth-protected server under a path prefix, declare your URLs upfront to make the relationships clear:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\n# Define the routing structure\nROOT_URL = \"http://localhost:8000\"\nMOUNT_PREFIX = \"/api\"\nMCP_PATH = \"/mcp\"\n```\n\nCreate the auth provider with `base_url`:\n\n```python\nauth = GitHubProvider(\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n    base_url=f\"{ROOT_URL}{MOUNT_PREFIX}\",  # Operational endpoints under prefix\n    # issuer_url defaults to base_url - path-aware discovery works automatically\n)\n```\n\nCreate the MCP app, which generates operational routes at the specified path:\n\n```python\nmcp = FastMCP(\"Protected Server\", auth=auth)\nmcp_app = mcp.http_app(path=MCP_PATH)\n```\n\nRetrieve the discovery routes from the auth provider. The `mcp_path` argument should match the path used when creating the MCP app:\n\n```python\nwell_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)\n```\n\nFinally, mount everything in the Starlette app with discovery routes at root and the MCP app under the prefix:\n\n```python\napp = Starlette(\n    routes=[\n        *well_known_routes,  # Discovery routes at root level\n        Mount(MOUNT_PREFIX, app=mcp_app),  # Operational routes under prefix\n    ],\n    lifespan=mcp_app.lifespan,\n)\n```\n\nThis configuration produces the following URL structure:\n\n- MCP endpoint: `http://localhost:8000/api/mcp`\n- OAuth authorization: `http://localhost:8000/api/authorize`\n- OAuth callback: `http://localhost:8000/api/auth/callback`\n- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server/api`\n- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp`\n\nBoth discovery endpoints use path-aware URLs per RFC 8414 and RFC 9728, matching the `base_url` path.\n\n### Complete Example\n\nHere's a complete working example showing all the pieces together:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\nimport uvicorn\n\n# Define routing structure\nROOT_URL = \"http://localhost:8000\"\nMOUNT_PREFIX = \"/api\"\nMCP_PATH = \"/mcp\"\n\n# Create OAuth provider\nauth = GitHubProvider(\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n    base_url=f\"{ROOT_URL}{MOUNT_PREFIX}\",\n    # issuer_url defaults to base_url - path-aware discovery works automatically\n)\n\n# Create MCP server\nmcp = FastMCP(\"Protected Server\", auth=auth)\n\n@mcp.tool\ndef analyze(data: str) -> dict:\n    return {\"result\": f\"Analyzed: {data}\"}\n\n# Create MCP app\nmcp_app = mcp.http_app(path=MCP_PATH)\n\n# Get discovery routes for root level\nwell_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)\n\n# Assemble the application\napp = Starlette(\n    routes=[\n        *well_known_routes,\n        Mount(MOUNT_PREFIX, app=mcp_app),\n    ],\n    lifespan=mcp_app.lifespan,\n)\n\nif __name__ == \"__main__\":\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n```\n\nFor more details on OAuth authentication, see the [Authentication guide](/servers/auth).\n\n## Production Deployment\n\n### Running with Uvicorn\n\nWhen deploying to production, you'll want to optimize your server for performance and reliability. Uvicorn provides several options to improve your server's capabilities:\n\n```bash\n# Run with basic configuration\nuvicorn app:app --host 0.0.0.0 --port 8000\n\n# Run with multiple workers for production (requires stateless mode - see below)\nuvicorn app:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n### Horizontal Scaling\n\n<VersionBadge version=\"2.10.2\" />\n\nWhen deploying FastMCP behind a load balancer or running multiple server instances, you need to understand how the HTTP transport handles sessions and configure your server appropriately.\n\n#### Understanding Sessions\n\nBy default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/servers/elicitation) and [sampling](/servers/sampling), where the server needs to maintain context across multiple requests from the same client.\n\nThis works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.\n\n#### Without Stateless Mode\n\nWhen running multiple server instances behind a load balancer (Traefik, nginx, HAProxy, Kubernetes, etc.), requests from the same client may be routed to different instances:\n\n1. Client connects to Instance A → session created on Instance A\n2. Next request routes to Instance B → session doesn't exist → **request fails**\n\nYou might expect sticky sessions (session affinity) to solve this, but they don't work reliably with MCP clients.\n\n<Warning>\n**Why sticky sessions don't work:** Most MCP clients—including Cursor and Claude Code—use `fetch()` internally and don't properly forward `Set-Cookie` headers. Without cookies, load balancers can't identify which instance should handle subsequent requests. This is a limitation in how these clients implement HTTP, not something you can fix with load balancer configuration.\n</Warning>\n\n#### Enabling Stateless Mode\n\nFor horizontally scaled deployments, enable stateless HTTP mode. In stateless mode, each request creates a fresh transport context, eliminating the need for session affinity entirely.\n\n**Option 1: Via constructor**\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\napp = mcp.http_app(stateless_http=True)\n```\n\n**Option 2: Via `run()`**\n\n```python\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", stateless_http=True)\n```\n\n**Option 3: Via environment variable**\n\n```bash\nFASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n### Environment Variables\n\nProduction 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.\n\nHere's an example using bearer token authentication (though OAuth is recommended for production):\n\n```python\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import BearerTokenAuth\n\n# Read configuration from environment\nauth_token = os.environ.get(\"MCP_AUTH_TOKEN\")\nif auth_token:\n    auth = BearerTokenAuth(token=auth_token)\n    mcp = FastMCP(\"Production Server\", auth=auth)\nelse:\n    mcp = FastMCP(\"Production Server\")\n\napp = mcp.http_app()\n```\n\nDeploy with your secrets safely stored in environment variables:\n```bash\nMCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000\n```\n\n### OAuth Token Security\n\n<VersionBadge version=\"2.13.0\" />\n\nIf you're using the [OAuth Proxy](/servers/auth/oauth-proxy), FastMCP issues its own JWT tokens to clients instead of forwarding upstream provider tokens. This maintains proper OAuth 2.0 token boundaries.\n\n**Default Behavior (Development Only):**\n\nBy default, FastMCP automatically manages cryptographic keys:\n- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.\n- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.\n\nThis automatic approach is convenient for development but not suitable for production deployments.\n\n**For Production:**\n\nProduction requires explicit key management to ensure tokens survive restarts and can be shared across multiple server instances. This requires the following two things working together:\n\n1. **Explicit JWT signing key** for signing tokens issued to clients\n3. **Persistent network-accessible storage** for upstream tokens (wrapped in `FernetEncryptionWrapper` to encrypt sensitive data at rest)\n\n**Configuration:**\n\nAdd two parameters to your auth provider:\n\n```python {8-12}\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(host=\"redis.example.com\", port=6379),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    ),\n    base_url=\"https://your-server.com\"  # use HTTPS\n)\n```\n\nBoth parameters are required for production. Without an explicit signing key, keys are signed using a key derived from the client_secret, which will cause invalidation upon rotation of the client secret. Without persistent storage, tokens are local to the server and won't be trusted across hosts. **Wrap your storage backend in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without encryption, tokens are stored in plaintext.\n\nFor more details on the token architecture and key management, see [OAuth Proxy Key and Storage Management](/servers/auth/oauth-proxy#key-and-storage-management).\n\n## Reverse Proxy (nginx)\n\nIn production, you'll typically run your FastMCP server behind a reverse proxy like nginx. A reverse proxy provides TLS termination, domain-based routing, static file serving, and an additional layer of security between the internet and your application.\n\n### Running FastMCP as a Linux Service\n\nBefore configuring nginx, you need your FastMCP server running as a background service. A systemd unit file ensures your server starts automatically and restarts on failure.\n\nCreate a file at `/etc/systemd/system/fastmcp.service`:\n\n```ini\n[Unit]\nDescription=FastMCP Server\nAfter=network.target\n\n[Service]\nUser=www-data\nGroup=www-data\nWorkingDirectory=/opt/fastmcp\nExecStart=/opt/fastmcp/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000\nRestart=always\nRestartSec=5\nEnvironment=\"PATH=/opt/fastmcp/.venv/bin\"\n\n[Install]\nWantedBy=multi-user.target\n```\n\nEnable and start the service:\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable fastmcp\nsudo systemctl start fastmcp\n```\n\nThis assumes your ASGI application is in `/opt/fastmcp/app.py` with a virtual environment at `/opt/fastmcp/.venv`. Adjust paths to match your deployment layout.\n\n### nginx Configuration\n\nFastMCP's Streamable HTTP transport uses Server-Sent Events (SSE) for streaming responses. This requires specific nginx settings to prevent buffering from breaking the event stream.\n\nCreate a site configuration at `/etc/nginx/sites-available/fastmcp`:\n\n```nginx\nserver {\n    listen 80;\n    server_name mcp.example.com;\n\n    # Redirect HTTP to HTTPS\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name mcp.example.com;\n\n    ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;\n\n    location / {\n        proxy_pass http://127.0.0.1:8000;\n        proxy_http_version 1.1;\n        proxy_set_header Connection '';\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # Required for SSE (Server-Sent Events) streaming\n        proxy_buffering off;\n        proxy_cache off;\n\n        # Allow long-lived connections for streaming responses\n        proxy_read_timeout 300s;\n        proxy_send_timeout 300s;\n    }\n}\n```\n\nEnable the site and reload nginx:\n\n```bash\nsudo ln -s /etc/nginx/sites-available/fastmcp /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n```\n\nYour FastMCP server is now accessible at `https://mcp.example.com/mcp`.\n\n<Warning>\n**SSE buffering is the most common issue.** If clients connect but never receive streaming responses (progress updates, tool results), verify that `proxy_buffering off` is set. Without it, nginx buffers the entire SSE stream and delivers it only when the connection closes, which breaks real-time communication.\n</Warning>\n\n### Key Considerations\n\nWhen deploying FastMCP behind a reverse proxy, keep these points in mind:\n\n- **Disable buffering**: SSE requires `proxy_buffering off` so events reach clients immediately. This is the single most important setting.\n- **Increase timeouts**: The default nginx `proxy_read_timeout` is 60 seconds. Long-running MCP tools will cause the connection to drop. Set timeouts to at least 300 seconds, or higher if your tools run longer. For tools that may exceed any timeout, use [SSE Polling](#sse-polling-for-long-running-operations) to gracefully handle proxy disconnections.\n- **Use HTTP/1.1**: Set `proxy_http_version 1.1` and `proxy_set_header Connection ''` to enable keep-alive connections between nginx and your server. Clearing the `Connection` header prevents clients from sending `Connection: close` to your upstream, which would break SSE streams. Both settings are required for proper SSE support.\n- **Forward headers**: Pass `X-Forwarded-For` and `X-Forwarded-Proto` so your FastMCP server can determine the real client IP and protocol. This is important for logging and for OAuth redirect URLs.\n- **TLS termination**: Let nginx handle TLS certificates (e.g., via Let's Encrypt with Certbot). Your FastMCP server can then run on plain HTTP internally.\n\n### Mounting Under a Path Prefix\n\nIf you want your MCP server available at a subpath like `https://example.com/api/mcp` instead of at the root domain, adjust the nginx `location` block:\n\n```nginx\nlocation /api/ {\n    proxy_pass http://127.0.0.1:8000/;\n    proxy_http_version 1.1;\n    proxy_set_header Connection '';\n    proxy_set_header Host $host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n\n    # Required for SSE streaming\n    proxy_buffering off;\n    proxy_cache off;\n    proxy_read_timeout 300s;\n    proxy_send_timeout 300s;\n}\n```\n\nNote the trailing `/` on both `location /api/` and `proxy_pass http://127.0.0.1:8000/` — this ensures nginx strips the `/api` prefix before forwarding to your server. If you're using OAuth authentication with a mount prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) for additional configuration.\n\n## Testing Your Deployment\n\nOnce your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/development/tests) guide.\n\n## Hosting Your Server\n\nThis guide has shown you how to create an HTTP-accessible MCP server, but you'll still need a hosting provider to make it available on the internet. Your FastMCP server can run anywhere that supports Python web applications:\n\n- **Cloud VMs** (AWS EC2, Google Compute Engine, Azure VMs)\n- **Container platforms** (Cloud Run, Container Instances, ECS)  \n- **Platform-as-a-Service** (Railway, Render, Vercel)\n- **Edge platforms** (Cloudflare Workers)\n- **Kubernetes clusters** (self-managed or managed)\n\nThe key requirements are Python 3.10+ support and the ability to expose an HTTP port. Most providers will require you to package your server (requirements.txt, Dockerfile, etc.) according to their deployment format. For managed, zero-configuration deployment, see [Prefect Horizon](/deployment/prefect-horizon).\n"
  },
  {
    "path": "docs/deployment/prefect-horizon.mdx",
    "content": "---\ntitle: Prefect Horizon\nsidebarTitle: Prefect Horizon\ndescription: The MCP platform from the FastMCP team\nicon: cloud\n---\n\n[Prefect Horizon](https://www.prefect.io/horizon) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.\n\nHorizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication.\n\n<Info>\nHorizon is free for personal projects. Enterprise governance features are available for teams deploying to thousands of users.\n</Info>\n\n## The Platform\n\nHorizon is organized into four integrated pillars:\n\n- **Deploy**: Managed hosting with CI/CD, scaling, monitoring, and rollbacks. Push code and get a live, governed endpoint in 60 seconds.\n- **Registry**: A central catalog of MCP servers across your organization—first-party, third-party, and curated remix servers composed from multiple sources.\n- **Gateway**: Role-based access control, authentication, and audit logs. Define what agents can see and do at the tool level.\n- **Agents**: A permissioned chat interface for interacting with any MCP server or curated combination of servers.\n\nThis guide focuses on **Horizon Deploy**, the managed hosting layer that gives you the fastest path from a FastMCP server to a production URL.\n\n## Prerequisites\n\nTo use Horizon, you'll need a [GitHub](https://github.com) account and a GitHub repo containing a FastMCP server. If you don't have one yet, Horizon can create a starter repo for you during onboarding.\n\nYour repo can be public or private, but must include at least a Python file containing a FastMCP server instance.\n\n<Tip>\nTo verify your file is compatible with Horizon, run `fastmcp inspect <file.py:server_object>` to see what Horizon will see when it runs your server.\n</Tip>\n\nIf you have a `requirements.txt` or `pyproject.toml` in the repo, Horizon will automatically detect your server's dependencies and install them. Your file *can* have an `if __name__ == \"__main__\"` block, but it will be ignored by Horizon.\n\nFor example, a minimal server file might look like:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n```\n\n## Getting Started\n\nThere are just three steps to deploying a server to Horizon:\n\n### Step 1: Select a Repository\n\nVisit [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.\n\n<img src=\"/assets/images/horizon/select-repo.png\" alt=\"Horizon repository selection\" />\n\n### Step 2: Configure Your Server\n\nNext, you'll configure how Horizon should build and deploy your server.\n\n<img src=\"/assets/images/horizon/configure-server.png\" alt=\"Horizon server configuration\" />\n\nThe configuration screen lets you specify:\n- **Server name**: A unique name for your server. This determines your server's URL.\n- **Description**: A brief description of what your server does.\n- **Entrypoint**: The Python file containing your FastMCP server (e.g., `main.py`). This field has the same syntax as the `fastmcp run` command—use `main.py:mcp` to specify a specific object in the file.\n- **Authentication**: When enabled, only authenticated users in your organization can connect. Horizon handles all the OAuth complexity for you.\n\nHorizon will automatically detect your server's Python dependencies from either a `requirements.txt` or `pyproject.toml` file.\n\n### Step 3: Deploy and Connect\n\nClick **Deploy Server** and Horizon will clone your repository, build your server, and deploy it to a unique URL—typically in under 60 seconds.\n\n<img src=\"/assets/images/horizon/deployment-live.png\" alt=\"Horizon deployment view showing live server\" />\n\nOnce deployed, your server is accessible at a URL like:\n\n```\nhttps://your-server-name.fastmcp.app/mcp\n```\n\nHorizon monitors your repo and redeploys automatically whenever you push to `main`. It also builds preview deployments for every PR, so you can test changes before they go live.\n\n## Testing Your Server\n\nHorizon provides two ways to verify your server is working before connecting external clients.\n\n### Inspector\n\nThe Inspector gives you a structured view of everything your server exposes—tools, resources, and prompts. You can click any tool, fill in the inputs, execute it, and see the output. This is useful for systematically validating each capability and debugging specific behaviors.\n\n### ChatMCP\n\nFor quick end-to-end testing, ChatMCP lets you interact with your server conversationally. It uses a fast model optimized for rapid iteration—you can verify the server works, test tool calls in context, and confirm the overall behavior before sharing it with others.\n\n<img src=\"/assets/images/horizon/chat.png\" alt=\"Horizon ChatMCP interface\" />\n\nChatMCP is designed for testing, not as a daily work environment. Once you've confirmed your server works, you can copy connection snippets for Claude Desktop, Cursor, Claude Code, and other MCP clients—or use the FastMCP client library to connect programmatically.\n\n## Horizon Agents\n\nBeyond testing individual servers, Horizon lets you create **Agents**—chat interfaces backed by one or more MCP servers. While ChatMCP tests a single server, Agents let you compose capabilities from multiple servers into a unified experience.\n\n<img src=\"/assets/images/horizon/agent-detail.png\" alt=\"Horizon Agent configuration\" />\n\nTo create an agent:\n1. Navigate to **Agents** in the sidebar\n2. Click **Create Agent** and give it a name and description\n3. Add MCP servers to the agent—these can be servers you've deployed to Horizon or external servers in the registry\n\nOnce configured, you can chat with your agent directly in Horizon:\n\n<img src=\"/assets/images/horizon/agent-chat.png\" alt=\"Chatting with a Horizon Agent\" />\n\nAgents are useful for creating purpose-built interfaces that combine tools from different servers. For example, you might create an agent that has access to both your company's internal data server and a general-purpose utilities server.\n"
  },
  {
    "path": "docs/deployment/running-server.mdx",
    "content": "---\ntitle: Running Your Server\nsidebarTitle: Running Your Server\ndescription: Learn how to run your FastMCP server locally for development and testing\nicon: circle-play\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nFastMCP servers can be run in different ways depending on your needs. This guide focuses on running servers locally for development and testing. For production deployment to a URL, see the [HTTP Deployment](/deployment/http) guide.\n\n## The `run()` Method\n\nEvery FastMCP server needs to be started to accept connections. The simplest way to run a server is by calling the `run()` method on your FastMCP instance. This method starts the server and blocks until it's stopped, handling all the connection management for you.\n\n<Tip>\nFor maximum compatibility, it's best practice to place the `run()` call within an `if __name__ == \"__main__\":` block. This ensures the server starts only when the script is executed directly, not when imported as a module.\n</Tip>\n\n```python {9-10} my_server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\nYou can now run this MCP server by executing `python my_server.py`.\n\n## Transport Protocols\n\nMCP servers communicate with clients through different transport protocols. Think of transports as the \"language\" your server speaks to communicate with clients. FastMCP supports three main transport protocols, each designed for specific use cases and deployment scenarios.\n\nThe choice of transport determines how clients connect to your server, what network capabilities are available, and how many clients can connect simultaneously. Understanding these transports helps you choose the right approach for your application.\n\n### STDIO Transport (Default)\n\nSTDIO (Standard Input/Output) is the default transport for FastMCP servers. When you call `run()` without arguments, your server uses STDIO transport. This transport communicates through standard input and output streams, making it perfect for command-line tools and desktop applications like Claude Desktop.\n\nWith STDIO transport, the client spawns a new server process for each session and manages its lifecycle. The server reads MCP messages from stdin and writes responses to stdout. This is why STDIO servers don't stay running - they're started on-demand by the client.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()  # Uses STDIO transport by default\n```\n\nSTDIO is ideal for:\n- Local development and testing\n- Claude Desktop integration\n- Command-line tools\n- Single-user applications\n\n### HTTP Transport (Streamable)\n\nHTTP transport turns your MCP server into a web service accessible via a URL. This transport uses the Streamable HTTP protocol, which allows clients to connect over the network. Unlike STDIO where each client gets its own process, an HTTP server can handle multiple clients simultaneously.\n\nThe Streamable HTTP protocol provides full bidirectional communication between client and server, supporting all MCP operations including streaming responses. This makes it the recommended choice for network-based deployments.\n\nTo use HTTP transport, specify it in the `run()` method along with networking options:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    # Start an HTTP server on port 8000\n    mcp.run(transport=\"http\", host=\"127.0.0.1\", port=8000)\n```\n\nYour server is now accessible at `http://localhost:8000/mcp`. This URL is the MCP endpoint that clients will connect to. HTTP transport enables:\n- Network accessibility\n- Multiple concurrent clients\n- Integration with web infrastructure\n- Remote deployment capabilities\n\nFor production HTTP deployment with authentication and advanced configuration, see the [HTTP Deployment](/deployment/http) guide.\n\n### SSE Transport (Legacy)\n\nServer-Sent Events (SSE) transport was the original HTTP-based transport for MCP. While still supported for backward compatibility, it has limitations compared to the newer Streamable HTTP transport. SSE only supports server-to-client streaming, making it less efficient for bidirectional communication.\n\n```python\nif __name__ == \"__main__\":\n    # SSE transport - use HTTP instead for new projects\n    mcp.run(transport=\"sse\", host=\"127.0.0.1\", port=8000)\n```\n\nWe recommend using HTTP transport instead of SSE for all new projects. SSE remains available only for compatibility with older clients that haven't upgraded to Streamable HTTP.\n\n### Choosing the Right Transport\n\nEach transport serves different needs. STDIO is perfect when you need simple, local execution - it's what Claude Desktop and most command-line tools expect. HTTP transport is essential when you need network access, want to serve multiple clients, or plan to deploy your server remotely. SSE exists only for backward compatibility and shouldn't be used in new projects.\n\nConsider your deployment scenario: Are you building a tool for local use? STDIO is your best choice. Need a centralized service that multiple clients can access? HTTP transport is the way to go.\n\n## The FastMCP CLI\n\nFastMCP provides a powerful command-line interface for running servers without modifying the source code. The CLI can automatically find and run your server with different transports, manage dependencies, and handle development workflows:\n\n```bash\nfastmcp run server.py\n```\n\nThe CLI automatically finds a FastMCP instance in your file (named `mcp`, `server`, or `app`) and runs it with the specified options. This is particularly useful for testing different transports or configurations without changing your code.\n\n### Dependency Management\n\nThe CLI integrates with `uv` to manage Python environments and dependencies:\n\n```bash\n# Run with a specific Python version\nfastmcp run server.py --python 3.11\n\n# Run with additional packages\nfastmcp run server.py --with pandas --with numpy\n\n# Run with dependencies from a requirements file\nfastmcp run server.py --with-requirements requirements.txt\n\n# Combine multiple options\nfastmcp run server.py --python 3.10 --with httpx --transport http\n\n# Run within a specific project directory\nfastmcp run server.py --project /path/to/project\n```\n\n<Note>\nWhen using `--python`, `--with`, `--project`, or `--with-requirements`, the server runs via `uv run` subprocess instead of using your local environment.\n</Note>\n\n### Passing Arguments to Servers\n\nWhen servers accept command line arguments (using argparse, click, or other libraries), you can pass them after `--`:\n\n```bash\nfastmcp run config_server.py -- --config config.json\nfastmcp run database_server.py -- --database-path /tmp/db.sqlite --debug\n```\n\nThis is useful for servers that need configuration files, database paths, API keys, or other runtime options.\n\nFor more CLI features including development mode with the MCP Inspector, see the [CLI documentation](/cli/running).\n\n### Auto-Reload for Development\n\n<VersionBadge version=\"3.0.0\" />\n\nDuring development, you can use the `--reload` flag to automatically restart your server when source files change:\n\n```bash\nfastmcp run server.py --reload\n```\n\nThe server watches for changes to Python files in the current directory and restarts automatically when you save changes. This provides a fast feedback loop during development without manually stopping and starting the server.\n\n```bash\n# Watch specific directories for changes\nfastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib\n\n# Combine with other options\nfastmcp run server.py --reload --transport http --port 8080\n```\n\n<Note>\nAuto-reload uses stateless mode to enable seamless restarts. For stdio transport, this is fully featured. For HTTP transport, some bidirectional features like elicitation are not available during reload mode.\n</Note>\n\nSSE transport does not support auto-reload due to session limitations. Use HTTP transport instead if you need both network access and auto-reload.\n\n### Async Usage\n\nFastMCP servers are built on async Python, but the framework provides both synchronous and asynchronous APIs to fit your application's needs. The `run()` method we've been using is actually a synchronous wrapper around the async server implementation.\n\nFor applications that are already running in an async context, FastMCP provides the `run_async()` method:\n\n```python {10-12}\nfrom fastmcp import FastMCP\nimport asyncio\n\nmcp = FastMCP(name=\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nasync def main():\n    # Use run_async() in async contexts\n    await mcp.run_async(transport=\"http\", port=8000)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n<Warning>\nThe `run()` method cannot be called from inside an async function because it creates its own async event loop internally. If you attempt to call `run()` from inside an async function, you'll get an error about the event loop already running.\n\nAlways use `run_async()` inside async functions and `run()` in synchronous contexts.\n</Warning>\n\nBoth `run()` and `run_async()` accept the same transport arguments, so all the examples above apply to both methods.\n\n## Custom Routes\n\nWhen using HTTP transport, you might want to add custom web endpoints alongside your MCP server. This is useful for health checks, status pages, or simple APIs. FastMCP lets you add custom routes using the `@custom_route` decorator:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.requests import Request\nfrom starlette.responses import PlainTextResponse\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check(request: Request) -> PlainTextResponse:\n    return PlainTextResponse(\"OK\")\n\n@mcp.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")  # Health check at http://localhost:8000/health\n```\n\nCustom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).\n\n## Alternative Initialization Patterns\n\nThe `if __name__ == \"__main__\"` pattern works well for standalone scripts, but some deployment scenarios require different approaches. FastMCP handles these cases automatically.\n\n### CLI-Only Servers\n\nWhen using the FastMCP CLI, you don't need the `if __name__` block at all. The CLI will find your FastMCP instance and run it:\n\n```python\n# server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")  # CLI looks for 'mcp', 'server', or 'app'\n\n@mcp.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\n# No if __name__ block needed - CLI will find and run 'mcp'\n```\n\n### ASGI Applications\n\nFor ASGI deployment (running with Uvicorn or similar), you'll want to create an ASGI application object. This approach is common in production deployments where you need more control over the server configuration:\n\n```python\n# app.py\nfrom fastmcp import FastMCP\n\ndef create_app():\n    mcp = FastMCP(\"MyServer\")\n    \n    @mcp.tool\n    def process(data: str) -> str:\n        return f\"Processed: {data}\"\n    \n    return mcp.http_app()\n\napp = create_app()  # Uvicorn will use this\n```\n\nSee the [HTTP Deployment](/deployment/http) guide for more ASGI deployment patterns."
  },
  {
    "path": "docs/deployment/server-configuration.mdx",
    "content": "---\ntitle: \"Project Configuration\"\nsidebarTitle: \"Project Configuration\"\ndescription: Use fastmcp.json for portable, declarative project configuration\nicon: file-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nFastMCP supports declarative configuration through `fastmcp.json` files. This is the canonical and preferred way to configure FastMCP projects, providing a single source of truth for server settings, dependencies, and deployment options that replaces complex command-line arguments.\n\nThe `fastmcp.json` file is designed to be a portable description of your server configuration that can be shared across environments and teams. When running from a `fastmcp.json` file, you can override any configuration values using CLI arguments.\n\n## Overview\n\nThe `fastmcp.json` configuration file allows you to define all aspects of your FastMCP server in a structured, shareable format. Instead of remembering command-line arguments or writing shell scripts, you declare your server's configuration once and use it everywhere.\n\nWhen you have a `fastmcp.json` file, running your server becomes as simple as:\n\n```bash\n# Run the server using the configuration\nfastmcp run fastmcp.json\n\n# Or if fastmcp.json exists in the current directory\nfastmcp run\n```\n\nThis configuration approach ensures reproducible deployments across different environments, from local development to production servers. It works seamlessly with Claude Desktop, VS Code extensions, and any MCP-compatible client.\n\n## File Structure\n\nThe `fastmcp.json` configuration answers three fundamental questions about your server:\n\n- **Source** = WHERE does your server code live?\n- **Environment** = WHAT environment setup does it require?\n- **Deployment** = HOW should the server run?\n\nThis conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    // WHERE: Location of your server code\n    \"type\": \"filesystem\",  // Optional, defaults to \"filesystem\"\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    // WHAT: Environment setup and dependencies\n    \"type\": \"uv\",  // Optional, defaults to \"uv\"\n    \"python\": \">=3.10\",\n    \"dependencies\": [\"pandas\", \"numpy\"]\n  },\n  \"deployment\": {\n    // HOW: Runtime configuration\n    \"transport\": \"stdio\",\n    \"log_level\": \"INFO\"\n  }\n}\n```\n\nOnly the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.\n\n### JSON Schema Support\n\nFastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  }\n}\n```\n\nTwo schema URLs are available:\n- **Version-specific**: `https://gofastmcp.com/public/schemas/fastmcp.json/v1.json`\n- **Latest version**: `https://gofastmcp.com/public/schemas/fastmcp.json/latest.json`\n\nModern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.\n\n### Source Configuration\n\nThe source configuration determines **WHERE** your server code lives. It tells FastMCP how to find and load your server, whether it's a local Python file, a remote repository, or hosted in the cloud. This section is required and forms the foundation of your configuration.\n\n<Card icon=\"code\" title=\"Source\">\n<ParamField body=\"source\" type=\"object\" required>\n  The server source configuration that determines where your server code lives.\n  \n  <ParamField body=\"type\" type=\"string\" default=\"filesystem\">\n    The source type identifier that determines which implementation to use. Currently supports `\"filesystem\"` for local files. Future releases will add support for `\"git\"` and `\"cloud\"` source types.\n  </ParamField>\n  \n  <Expandable title=\"FileSystemSource\">\n    When `type` is `\"filesystem\"` (or omitted), the source points to a local Python file containing your FastMCP server:\n    \n    <ParamField body=\"path\" type=\"string\" required>\n      Path to the Python file containing your FastMCP server. \n    </ParamField>\n    \n    <ParamField body=\"entrypoint\" type=\"string\">\n      Name of the server instance or factory function within the module:\n      - Can be a FastMCP server instance (e.g., `mcp = FastMCP(\"MyServer\")`)\n      - Can be a function with no arguments that returns a FastMCP server\n      - If not specified, FastMCP searches for common names: `mcp`, `server`, or `app`\n    </ParamField>\n    \n    **Example:**\n    ```json\n    \"source\": {\n      \"type\": \"filesystem\",\n      \"path\": \"src/server.py\",\n      \"entrypoint\": \"mcp\"\n    }\n    ```\n    \n    Note: File paths are resolved relative to the configuration file's location.\n  </Expandable>\n</ParamField>\n</Card>\n\n<Note>\n**Future Source Types**\n\nFuture releases will support additional source types:\n- **Git repositories** (`type: \"git\"`) for loading server code directly from version control\n- **Prefect Horizon** (`type: \"cloud\"`) for hosted servers with automatic scaling and management\n</Note>\n\n### Environment Configuration\n\nThe environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment, ensuring your server runs with the exact Python version and dependencies it requires. This section creates isolated, reproducible environments across different systems.\n\nFastMCP uses an extensible environment system with a base `Environment` class that can be implemented by different environment providers. Currently, FastMCP supports the `UVEnvironment` for Python environment management using `uv`'s powerful dependency resolver.\n\n<Card icon=\"code\" title=\"Environment\">\n<ParamField body=\"environment\" type=\"object\">\n  Optional environment configuration. When specified, FastMCP uses the appropriate environment implementation to set up your server's runtime.\n  \n  <ParamField body=\"type\" type=\"string\" default=\"uv\">\n    The environment type identifier that determines which implementation to use. Currently supports `\"uv\"` for Python environments managed by uv. If omitted, defaults to `\"uv\"`.\n  </ParamField>\n  \n  <Expandable title=\"UVEnvironment\">\n    When `type` is `\"uv\"` (or omitted), the environment uses uv to manage Python dependencies:\n    \n    <ParamField body=\"python\" type=\"string\">\n      Python version constraint. Examples:\n      - Exact version: `\"3.12\"`\n      - Minimum version: `\">=3.10\"`\n      - Version range: `\">=3.10,<3.13\"`\n    </ParamField>\n    \n    <ParamField body=\"dependencies\" type=\"list[str]\">\n      List of pip packages with optional version specifiers (PEP 508 format).\n      ```json\n      \"dependencies\": [\"pandas>=2.0\", \"requests\", \"httpx\"]\n      ```\n    </ParamField>\n    \n    <ParamField body=\"requirements\" type=\"string\">\n      Path to a requirements.txt file, resolved relative to the config file location.\n      ```json\n      \"requirements\": \"requirements.txt\"\n      ```\n    </ParamField>\n    \n    <ParamField body=\"project\" type=\"string\">\n      Path to a project directory containing pyproject.toml for uv project management.\n      ```json\n      \"project\": \".\"\n      ```\n    </ParamField>\n    \n    <ParamField body=\"editable\" type=\"list[string]\">\n      List of paths to packages to install in editable/development mode. Useful for local development when you want changes to be reflected immediately. Supports multiple packages for monorepo setups or shared libraries.\n      ```json\n      \"editable\": [\".\"]\n      ```\n      Or with multiple packages:\n      ```json\n      \"editable\": [\".\", \"../shared-lib\", \"/path/to/another-package\"]\n      ```\n    </ParamField>\n    \n    **Example:**\n    ```json\n    \"environment\": {\n      \"type\": \"uv\",\n      \"python\": \">=3.10\",\n      \"dependencies\": [\"pandas\", \"numpy\"],\n      \"editable\": [\".\"]\n    }\n    ```\n    \n    Note: When any UVEnvironment field is specified, FastMCP automatically creates an isolated environment using `uv` before running your server.\n  </Expandable>\n</ParamField>\n</Card>\n\nWhen environment configuration is provided, FastMCP:\n1. Detects the environment type (defaults to `\"uv\"` if not specified)\n2. Creates an isolated environment using the appropriate provider\n3. Installs the specified dependencies\n4. Runs your server in this clean environment\n\nThis build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.\n\n<Note>\n**Future Environment Types**\n\nSimilar to source types, future releases may support additional environment types for different runtime requirements, such as Docker containers or language-specific environments beyond Python.\n</Note>\n\n### Deployment Configuration\n\nThe deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.\n\nEnvironment variables are included in this section because they're runtime configuration that affects how your server behaves when it executes, not how its environment is built. The deployment configuration is applied every time your server starts, controlling its operational characteristics.\n\n<Card icon=\"code\" title=\"Deployment Fields\">\n<ParamField body=\"deployment\" type=\"object\">\n  Optional runtime configuration for the server.\n  \n  <Expandable title=\"Deployment Fields\">\n    <ParamField body=\"transport\" type=\"string\" default=\"stdio\">\n      Protocol for client communication:\n      - `\"stdio\"`: Standard input/output for desktop clients\n      - `\"http\"`: Network-accessible HTTP server\n      - `\"sse\"`: Server-sent events\n    </ParamField>\n    \n    <ParamField body=\"host\" type=\"string\" default=\"127.0.0.1\">\n      Network interface to bind (HTTP transport only):\n      - `\"127.0.0.1\"`: Local connections only\n      - `\"0.0.0.0\"`: All network interfaces\n    </ParamField>\n    \n    <ParamField body=\"port\" type=\"integer\" default=\"3000\">\n      Port number for HTTP transport.\n    </ParamField>\n    \n    <ParamField body=\"path\" type=\"string\" default=\"/mcp/\">\n      URL path for the MCP endpoint when using HTTP transport.\n    </ParamField>\n    \n    <ParamField body=\"log_level\" type=\"string\" default=\"INFO\">\n      Server logging verbosity. Options:\n      - `\"DEBUG\"`: Detailed debugging information\n      - `\"INFO\"`: General informational messages\n      - `\"WARNING\"`: Warning messages\n      - `\"ERROR\"`: Error messages only\n      - `\"CRITICAL\"`: Critical errors only\n    </ParamField>\n    \n    <ParamField body=\"env\" type=\"object\">\n      Environment variables to set when running the server. Supports `${VAR_NAME}` syntax for runtime interpolation.\n      ```json\n      \"env\": {\n        \"API_KEY\": \"secret-key\",\n        \"DATABASE_URL\": \"postgres://${DB_USER}@${DB_HOST}/mydb\"\n      }\n      ```\n    </ParamField>\n    \n    <ParamField body=\"cwd\" type=\"string\">\n      Working directory for the server process. Relative paths are resolved from the config file location.\n    </ParamField>\n    \n    <ParamField body=\"args\" type=\"list[str]\">\n      Command-line arguments to pass to the server, passed after `--` to the server's argument parser.\n      ```json\n      \"args\": [\"--config\", \"server-config.json\"]\n      ```\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\n#### Environment Variable Interpolation\n\nThe `env` field in deployment configuration supports runtime interpolation of environment variables using `${VAR_NAME}` syntax. This enables dynamic configuration based on your deployment environment:\n\n```json\n{\n  \"deployment\": {\n    \"env\": {\n      \"API_URL\": \"https://api.${ENVIRONMENT}.example.com\",\n      \"DATABASE_URL\": \"postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/myapp\",\n      \"CACHE_KEY\": \"myapp_${ENVIRONMENT}_${VERSION}\"\n    }\n  }\n}\n```\n\nWhen the server starts, FastMCP replaces `${ENVIRONMENT}`, `${DB_USER}`, etc. with values from your system's environment variables. If a variable doesn't exist, the placeholder is preserved as-is.\n\n**Example**: If your system has `ENVIRONMENT=production` and `DB_HOST=db.example.com`:\n```json\n// Configuration\n{\n  \"deployment\": {\n    \"env\": {\n      \"API_URL\": \"https://api.${ENVIRONMENT}.example.com\",\n      \"DB_HOST\": \"${DB_HOST}\"\n    }\n  }\n}\n\n// Result at runtime\n{\n  \"API_URL\": \"https://api.production.example.com\",\n  \"DB_HOST\": \"db.example.com\"\n}\n```\n\nThis feature is particularly useful for:\n- Deploying the same configuration across development, staging, and production\n- Keeping sensitive values out of configuration files\n- Building dynamic URLs and connection strings\n- Creating environment-specific prefixes or suffixes\n\n## Usage with CLI Commands\n\nFastMCP automatically detects and uses a file specifically named `fastmcp.json` in the current directory, making server execution simple and consistent. Files with FastMCP configuration format but different names are not auto-detected and must be specified explicitly:\n\n```bash\n# Auto-detect fastmcp.json in current directory\ncd my-project\nfastmcp run  # No arguments needed!\n\n# Or specify a configuration file explicitly\nfastmcp run prod.fastmcp.json\n\n# Skip environment setup when already in a uv environment\nfastmcp run fastmcp.json --skip-env\n\n# Skip source preparation when source is already prepared\nfastmcp run fastmcp.json --skip-source\n\n# Skip both environment and source preparation\nfastmcp run fastmcp.json --skip-env --skip-source\n```\n\n### Pre-building Environments\n\nYou can use `fastmcp project prepare` to create a persistent uv project with all dependencies pre-installed:\n\n```bash\n# Create a persistent environment\nfastmcp project prepare fastmcp.json --output-dir ./env\n\n# Use the pre-built environment to run the server\nfastmcp run fastmcp.json --project ./env\n```\n\nThis pattern separates environment setup (slow) from server execution (fast), useful for deployment scenarios.\n\n### Using an Existing Environment\n\nBy default, FastMCP creates an isolated environment with `uv` based on your configuration. When you already have a suitable Python environment, use the `--skip-env` flag to skip environment creation:\n\n```bash\nfastmcp run fastmcp.json --skip-env\n```\n\n**When you already have an environment:**\n- You're in an activated virtual environment with all dependencies installed\n- You're inside a Docker container with pre-installed dependencies  \n- You're in a CI/CD pipeline that pre-builds the environment\n- You're using a system-wide installation with all required packages\n- You're in a uv-managed environment (prevents infinite recursion)\n\nThis flag tells FastMCP: \"I already have everything installed, just run the server.\"\n\n### Using an Existing Source\n\nWhen working with source types that require preparation (future support for git repositories or cloud sources), use the `--skip-source` flag when you already have the source code available:\n\n```bash\nfastmcp run fastmcp.json --skip-source\n```\n\n**When you already have the source:**\n- You've previously cloned a git repository and don't need to re-fetch\n- You have a cached copy of a cloud-hosted server\n- You're in a CI/CD pipeline where source checkout is a separate step\n- You're iterating locally on already-downloaded code\n\nThis flag tells FastMCP: \"I already have the source code, skip any download/clone steps.\"\n\nNote: For filesystem sources (local Python files), this flag has no effect since they don't require preparation.\n\nThe configuration file works with all FastMCP commands:\n- **`run`** - Start the server in production mode\n- **`dev`** - Launch with the Inspector UI for development  \n- **`inspect`** - View server capabilities and configuration\n- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients\n\nWhen no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.\n\n### CLI Override Behavior\n\nCommand-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:\n\n```bash\n# Config specifies port 3000, CLI overrides to 8080\nfastmcp run fastmcp.json --port 8080\n\n# Config specifies stdio, CLI overrides to HTTP\nfastmcp run fastmcp.json --transport http\n\n# Add extra dependencies not in config\nfastmcp run fastmcp.json --with requests --with httpx\n```\n\nThis precedence order enables:\n- Quick testing of different settings\n- Environment-specific overrides in deployment scripts\n- Debugging with increased log levels\n- Temporary configuration changes\n\n### Custom Naming Patterns\n\nYou can use different configuration files for different environments:\n\n- `fastmcp.json` - Default configuration\n- `dev.fastmcp.json` - Development settings\n- `prod.fastmcp.json` - Production settings\n- `test_fastmcp.json` - Test configuration\n\nAny file with \"fastmcp.json\" in the name is recognized as a configuration file.\n\n## Examples\n\n<Tabs>\n<Tab title=\"Basic Configuration\">\n\nA minimal configuration for a simple server:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  }\n}\n```\nThis configuration explicitly specifies the server entrypoint (`mcp`), making it clear which server instance or factory function to use. Uses all defaults: STDIO transport, no special dependencies, standard logging.\n</Tab>\n<Tab title=\"Development Configuration\">\n\nA configuration optimized for local development:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  // WHERE does the server live?\n  \"source\": {\n    \"path\": \"src/server.py\",\n    \"entrypoint\": \"app\"\n  },\n  // WHAT dependencies does it need?\n  \"environment\": {\n    \"type\": \"uv\",\n    \"python\": \"3.12\",\n    \"dependencies\": [\"fastmcp[dev]\"],\n    \"editable\": \".\"\n  },\n  // HOW should it run?\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8000,\n    \"log_level\": \"DEBUG\",\n    \"env\": {\n      \"DEBUG\": \"true\",\n      \"ENV\": \"development\"\n    }\n  }\n}\n```\n</Tab>\n<Tab title=\"Production Configuration\">\n\nA production-ready configuration with full dependency management:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  // WHERE does the server live?\n  \"source\": {\n    \"path\": \"app/main.py\",\n    \"entrypoint\": \"mcp_server\"\n  },\n  // WHAT dependencies does it need?\n  \"environment\": {\n    \"python\": \"3.11\",\n    \"requirements\": \"requirements/production.txt\",\n    \"project\": \".\"\n  },\n  // HOW should it run?\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"0.0.0.0\",\n    \"port\": 3000,\n    \"path\": \"/api/mcp/\",\n    \"log_level\": \"INFO\",\n    \"env\": {\n      \"ENV\": \"production\",\n      \"API_BASE_URL\": \"https://api.example.com\",\n      \"DATABASE_URL\": \"postgresql://user:pass@db.example.com/prod\"\n    },\n    \"cwd\": \"/app\",\n    \"args\": [\"--workers\", \"4\"]\n  }\n}\n```\n</Tab>\n<Tab title=\"Data Science Server\">\n\nConfiguration for a data analysis server with scientific packages:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"analysis_server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"python\": \"3.11\",\n    \"dependencies\": [\n      \"pandas>=2.0\",\n      \"numpy\",\n      \"scikit-learn\",\n      \"matplotlib\",\n      \"jupyterlab\"\n    ]\n  },\n  \"deployment\": {\n    \"transport\": \"stdio\",\n    \"env\": {\n      \"MATPLOTLIB_BACKEND\": \"Agg\",\n      \"DATA_PATH\": \"./datasets\"\n    }\n  }\n}\n```\n</Tab>\n<Tab title=\"Multi-Environment Setup\"> \n\nYou can maintain multiple configuration files for different environments:\n\n**dev.fastmcp.json**:\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"log_level\": \"DEBUG\"\n  }\n}\n```\n\n**prod.fastmcp.json**:\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"requirements\": \"requirements/production.txt\"\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"0.0.0.0\",\n    \"log_level\": \"WARNING\"\n  }\n}\n```\n\nRun different configurations:\n```bash\nfastmcp run dev.fastmcp.json   # Development\nfastmcp run prod.fastmcp.json  # Production\n```\n</Tab>\n</Tabs>\n\n## Migrating from CLI Arguments\n\nIf you're currently using command-line arguments or shell scripts, migrating to `fastmcp.json` simplifies your workflow. Here's how common CLI patterns map to configuration:\n\n**CLI Command**:\n```bash\nuv run --with pandas --with requests \\\n  fastmcp run server.py \\\n  --transport http \\\n  --port 8000 \\\n  --log-level INFO\n```\n\n**Equivalent fastmcp.json**:\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"port\": 8000,\n    \"log_level\": \"INFO\"\n  }\n}\n```\n\nNow simply run:\n```bash\nfastmcp run  # Automatically finds and uses fastmcp.json\n```\n\nThe configuration file approach provides better documentation, easier sharing, and consistent execution across different environments while maintaining the flexibility to override settings when needed."
  },
  {
    "path": "docs/development/contributing.mdx",
    "content": "---\ntitle: \"Contributing\"\ndescription: \"Development workflow for FastMCP contributors\"\nicon: code-pull-request\n---\n\nContributing to FastMCP means joining a community that values clean, maintainable code and thoughtful API design. All contributions are valued - from fixing typos in documentation to implementing major features.\n\n## Design Principles\n\nEvery contribution should advance these principles:\n\n- 🚀 **Fast** — High-level interfaces mean less code and faster development\n- 🍀 **Simple** — Minimal boilerplate; the obvious way should be the right way\n- 🐍 **Pythonic** — Feels natural to Python developers; no surprising patterns\n- 🔍 **Complete** — Everything needed for production: auth, testing, deployment, observability\n\nPRs are evaluated against these principles. Code that makes FastMCP slower, harder to reason about, less Pythonic, or less complete will be rejected.\n\n## Issues\n\n### Issue First, Code Second\n\n**Every pull request requires a corresponding issue - no exceptions.** This requirement creates a collaborative space where approach, scope, and alignment are established before code is written. Issues serve as design documents where maintainers and contributors discuss implementation strategy, identify potential conflicts with existing patterns, and ensure proposed changes advance FastMCP's vision.\n\n**FastMCP is an opinionated framework, not a kitchen sink.** The maintainers have strong beliefs about what FastMCP should and shouldn't do. Just because something takes N lines of code and you want it in fewer lines doesn't mean FastMCP should take on the maintenance burden or endorse that pattern. This is judged at the maintainers' discretion.\n\nUse issues to understand scope BEFORE opening PRs. The issue discussion determines whether a feature belongs in core, contrib, or not at all.\n\n### Writing Good Issues\n\nFastMCP is an extremely highly-trafficked repository maintained by a very small team. Issues that appear to transfer burden to maintainers without any effort to validate the problem will be closed. Please help the maintainers help you by always providing a minimal reproducible example and clearly describing the problem.\n\n**LLM-generated issues will be closed immediately.** Issues that contain paragraphs of unnecessary explanation, verbose problem descriptions, or obvious LLM authorship patterns obfuscate the actual problem and transfer burden to maintainers.\n\nWrite clear, concise issues that:\n- State the problem directly\n- Provide a minimal reproducible example\n- Skip unnecessary background or context\n- Take responsibility for clear communication\n\nIssues may be labeled \"Invalid\" simply due to confusion caused by verbosity or not adhering to the guidelines outlined here.\n\n## Pull Requests\n\nPRs that deviate from FastMCP's core principles will be rejected regardless of implementation quality. **PRs are NOT for iterating on ideas** - they should only be opened for ideas that already have a bias toward acceptance based on issue discussion.\n\n\n### Development Environment\n\n#### Installation\n\nTo contribute to FastMCP, you'll need to set up a development environment with all necessary tools and dependencies.\n\n```bash\n# Clone the repository\ngit clone https://github.com/PrefectHQ/fastmcp.git\ncd fastmcp\n\n# Install all dependencies including dev tools\nuv sync\n\n# Install prek hooks\nuv run prek install\n```\n\nIn addition, some development commands require [just](https://github.com/casey/just) to be installed.\n\nPrek hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.\n\n### Development Standards\n\n#### Scope\n\nLarge pull requests create review bottlenecks and quality risks. Unless you're fixing a discrete bug or making an incredibly well-scoped change, keep PRs small and focused. \n\nA PR that changes 50 lines across 3 files can be thoroughly reviewed in minutes. A PR that changes 500 lines across 20 files requires hours of careful analysis and often hides subtle issues.\n\nBreaking large features into smaller PRs:\n- Creates better review experiences\n- Makes git history clear\n- Simplifies debugging with bisect\n- Reduces merge conflicts\n- Gets your code merged faster\n\n#### Code Quality\n\nFastMCP values clarity over cleverness. Every line you write will be maintained by someone else - possibly years from now, possibly without context about your decisions.\n\n**PRs can be rejected for two opposing reasons:**\n1. **Insufficient quality** - Code that doesn't meet our standards for clarity, maintainability, or idiomaticity\n2. **Overengineering** - Code that is overbearing, unnecessarily complex, or tries to be too clever\n\nThe focus is on idiomatic, high-quality Python. FastMCP uses patterns like `NotSet` type as an alternative to `None` in certain situations - follow existing patterns.\n\n#### Required Practices\n\n**Full type annotations** on all functions and methods. They catch bugs before runtime and serve as inline documentation.\n\n**Async/await patterns** for all I/O operations. Even if your specific use case doesn't need concurrency, consistency means users can compose features without worrying about blocking operations.\n\n**Descriptive names** make code self-documenting. `auth_token` is clear; `tok` requires mental translation.\n\n**Specific exception types** make error handling predictable. Catching `ValueError` tells readers exactly what error you expect. Never use bare `except` clauses.\n\n#### Anti-Patterns to Avoid\n\n**Complex one-liners** are hard to debug and modify. Break operations into clear steps.\n\n**Mutable default arguments** cause subtle bugs. Use `None` as the default and create the mutable object inside the function.\n\n**Breaking established patterns** confuses readers. If you must deviate, discuss in the issue first.\n\n### Prek Checks\n\n```bash\n# Runs automatically on commit, or manually:\nuv run prek run --all-files\n```\n\nThis runs three critical tools:\n- **Ruff**: Linting and formatting\n- **Prettier**: Code formatting\n- **ty**: Static type checking\n\nPytest runs separately as a distinct workflow step after prek checks pass. CI will reject PRs that fail these checks. Always run them locally first.\n\n### Testing\n\nTests are documentation that shows how features work. Good tests give reviewers confidence and help future maintainers understand intent.\n\n```bash\n# Run specific test directory\nuv run pytest tests/server/ -v\n\n# Run all tests before submitting PR\nuv run pytest\n```\n\nEvery new feature needs tests. See the [Testing Guide](/development/tests) for patterns and requirements.\n\n### Documentation\n\nA feature doesn't exist unless it's documented. Note that FastMCP's hosted documentation always tracks the main branch - users who want historical documentation can clone the repo, checkout a specific tag, and host it themselves.\n\n```bash\n# Preview documentation locally\njust docs\n```\n\nDocumentation requirements:\n- **Explain concepts in prose first** - Code without context is just syntax\n- **Complete, runnable examples** - Every code block should be copy-pasteable\n- **Register in docs.json** - Makes pages appear in navigation\n- **Version badges** - Mark when features were added using `<VersionBadge />`\n\n#### SDK Documentation\n\nFastMCP's SDK documentation is auto-generated from the source code docstrings and type annotations. It is automatically updated on every merge to main by a GitHub Actions workflow, so users are *not* responsible for keeping the documentation up to date. However, to generate it proactively, you can use the following command:\n\n```bash\njust api-ref-all\n```\n\n### Submitting Your PR\n\n#### Before Submitting\n\n1. **Run all checks**: `uv run prek run --all-files && uv run pytest`\n2. **Keep scope small**: One feature or fix per PR\n3. **Write clear description**: Your PR description becomes permanent documentation\n4. **Update docs**: Include documentation for API changes\n\n#### PR Description\n\nWrite PR descriptions that explain:\n- What problem you're solving\n- Why you chose this approach  \n- Any trade-offs or alternatives considered\n- Migration path for breaking changes\n\nFocus on the \"why\" - the code shows the \"what\". Keep it concise but complete.\n\n#### What We Look For\n\n**Framework Philosophy**: FastMCP is NOT trying to do all things or provide all shortcuts. Features are rejected when they don't align with the framework's vision, even if perfectly implemented. The burden of proof is on the PR to demonstrate value.\n\n**Code Quality**: We verify code follows existing patterns. Consistency reduces cognitive load. When every module works similarly, developers understand new code quickly.\n\n**Test Coverage**: Not every line needs testing, but every behavior does. Tests document intent and protect against regressions.\n\n**Breaking Changes**: May be acceptable in minor versions but must be clearly documented. See the [versioning policy](/development/releases#versioning-policy).\n\n## Special Modules\n\n**`contrib`**: Community-maintained patterns and utilities. Original authors maintain their contributions. Not representative of the core framework.\n\n**`experimental`**: Maintainer-developed features that may preview future functionality. Can break or be deleted at any time without notice. Pin your FastMCP version when using these features."
  },
  {
    "path": "docs/development/releases.mdx",
    "content": "---\ntitle: \"Releases\"\ndescription: \"FastMCP versioning and release process\"\nicon: \"truck-fast\"\n---\n\nFastMCP releases frequently to deliver features quickly in the rapidly evolving MCP ecosystem. We use semantic versioning pragmatically - the Model Context Protocol is young, patterns are still emerging, and waiting for perfect stability would mean missing opportunities to empower developers with better tools.\n\n## Versioning Policy\n\n### Semantic Versioning\n\n**Major (x.0.0)**: Complete API redesigns\n\nMajor versions represent fundamental shifts. FastMCP 2.x is entirely different from 1.x in both implementation and design philosophy.\n\n**Minor (2.x.0)**: New features and evolution\n\n<Warning>\nUnlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.\n</Warning>\n\nFastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.\n\n**Patch (2.0.x)**: Bug fixes and refinements\n\nPatch versions contain only bug fixes without breaking changes. These are safe updates you can apply with confidence.\n\n### Breaking Changes\n\nWe permit breaking changes in minor versions because the MCP ecosystem is rapidly evolving. Refusing to break problematic APIs would accumulate design debt that eventually makes the framework unusable. Each breaking change represents a deliberate decision to keep FastMCP aligned with the ecosystem's evolution.\n\nWhen breaking changes occur:\n- They only happen in minor versions (e.g., 2.3.x to 2.4.0)\n- Release notes explain what changed and how to migrate\n- We provide deprecation warnings at least 1 minor version in advance when possible\n- Changes must substantially benefit users to justify disruption\n\nThe public API is what's covered by our compatibility guarantees - these are the parts of FastMCP you can rely on to remain stable within a minor version. The public API consists of:\n- `FastMCP` server class, `Client` class, and FastMCP `Context`\n- Core MCP components: `Tool`, `Prompt`, `Resource`, `ResourceTemplate`, and transports\n- Their public methods and documented behaviors\n\nEverything else (utilities, private methods, internal modules) may change without notice. This boundary lets us refactor internals and improve implementation details without breaking your code. For production stability, pin to specific versions.\n\n<Warning>\nThe `fastmcp.server.auth` module was introduced in 2.12.0 and is exempted from this policy temporarily, meaning it is *expected* to have breaking changes even on patch versions. This is because auth is a rapidly evolving part of the MCP spec and it would be dangerous to be beholden to old decisions. Please pin your FastMCP version if using authentication in production.\n\nWe expect this exemption to last through at least the 2.12.x and 2.13.x release series. \n</Warning>\n\n### Production Use\n\nPin to exact versions:\n```\nfastmcp==2.11.0  # Good\nfastmcp>=2.11.0  # Bad - will install breaking changes\n```\n\n## Creating Releases\n\nOur release process is intentionally simple:\n\n1. Create GitHub release with tag `vMAJOR.MINOR.PATCH` (e.g., `v2.11.0`)\n2. Generate release notes automatically, and curate or add additional editorial information as needed\n3. GitHub releases automatically trigger PyPI deployments\n\nThis automation lets maintainers focus on code quality rather than release mechanics.\n\n### Release Cadence\n\nWe follow a feature-driven release cadence rather than a fixed schedule. Minor versions ship approximately every 3-4 weeks when significant functionality is ready.\n\nPatch releases ship promptly for:\n- Critical bug fixes\n- Security updates (immediate release)\n- Regression fixes\n\nThis approach means you get improvements as soon as they're ready rather than waiting for arbitrary release dates.\n"
  },
  {
    "path": "docs/development/tests.mdx",
    "content": "---\ntitle: \"Tests\"\ndescription: \"Testing patterns and requirements for FastMCP\"\nicon: vial\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nGood tests are the foundation of reliable software. In FastMCP, we treat tests as first-class documentation that demonstrates how features work while protecting against regressions. Every new capability needs comprehensive tests that demonstrate correctness.\n\n## FastMCP Tests\n\n### Running Tests\n\n```bash\n# Run all tests\nuv run pytest\n\n# Run specific test file\nuv run pytest tests/server/test_auth.py\n\n# Run with coverage\nuv run pytest --cov=fastmcp\n\n# Skip integration tests for faster runs\nuv run pytest -m \"not integration\"\n\n# Skip tests that spawn processes\nuv run pytest -m \"not integration and not client_process\"\n```\n\nTests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.\n\n### Test Organization\n\nOur 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.\n\n### Test Markers\n\nWe use pytest markers to categorize tests that require special resources or take longer to run:\n\n```python\n@pytest.mark.integration\nasync def test_github_api_integration():\n    \"\"\"Test GitHub API integration with real service.\"\"\"\n    token = os.getenv(\"FASTMCP_GITHUB_TOKEN\")\n    if not token:\n        pytest.skip(\"FASTMCP_GITHUB_TOKEN not available\")\n    \n    # Test against real GitHub API\n    client = GitHubClient(token)\n    repos = await client.list_repos(\"prefecthq\")\n    assert \"fastmcp\" in [repo.name for repo in repos]\n\n@pytest.mark.client_process\nasync def test_stdio_transport():\n    \"\"\"Test STDIO transport with separate process.\"\"\"\n    # This spawns a subprocess\n    async with Client(\"python examples/simple_echo.py\") as client:\n        result = await client.call_tool(\"echo\", {\"message\": \"test\"})\n        assert result.content[0].text == \"test\"\n```\n\n## Writing Tests\n\n\n### Test Requirements\n\nFollowing these practices creates maintainable, debuggable test suites that serve as both documentation and regression protection.\n\n#### Single Behavior Per Test\n\nEach test should verify exactly one behavior. When it fails, you need to know immediately what broke. A test that checks five things gives you five potential failure points to investigate. A test that checks one thing points directly to the problem.\n\n<CodeGroup>\n\n```python Good: Atomic Test\nasync def test_tool_registration():\n    \"\"\"Test that tools are properly registered with the server.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n    \n    tools = mcp.list_tools()\n    assert len(tools) == 1\n    assert tools[0].name == \"add\"\n```\n\n```python Bad: Multi-Behavior Test\nasync def test_server_functionality():\n    \"\"\"Test multiple server features at once.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    # Tool registration\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n    \n    # Resource creation\n    @mcp.resource(\"config://app\")\n    def get_config():\n        return {\"version\": \"1.0\"}\n    \n    # Authentication setup\n    mcp.auth = BearerTokenProvider({\"token\": \"user\"})\n    \n    # What exactly are we testing? If this fails, what broke?\n    assert mcp.list_tools()\n    assert mcp.list_resources()\n    assert mcp.auth is not None\n```\n\n</CodeGroup>\n\n#### Self-Contained Setup\n\nEvery test must create its own setup. Tests should be runnable in any order, in parallel, or in isolation. When a test fails, you should be able to run just that test to reproduce the issue.\n\n<CodeGroup>\n\n```python Good: Self-Contained\nasync def test_tool_execution_with_error():\n    \"\"\"Test that tool errors are properly handled.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    @mcp.tool\n    def divide(a: int, b: int) -> float:\n        if b == 0:\n            raise ValueError(\"Cannot divide by zero\")\n        return a / b\n    \n    async with Client(mcp) as client:\n        with pytest.raises(Exception):\n            await client.call_tool(\"divide\", {\"a\": 10, \"b\": 0})\n```\n\n```python Bad: Test Dependencies\n# Global state that tests depend on\ntest_server = None\n\ndef test_setup_server():\n    \"\"\"Setup for other tests.\"\"\"\n    global test_server\n    test_server = FastMCP(\"shared-server\")\n\ndef test_server_works():\n    \"\"\"Test server functionality.\"\"\"\n    # Depends on test_setup_server running first\n    assert test_server is not None\n```\n\n</CodeGroup>\n\n#### Clear Intent\n\nTest names and assertions should make the verified behavior obvious. A developer reading your test should understand what feature it validates and how that feature should behave.\n\n```python\nasync def test_authenticated_tool_requires_valid_token():\n    \"\"\"Test that authenticated users can access protected tools.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    mcp.auth = BearerTokenProvider({\"secret-token\": \"test-user\"})\n    \n    @mcp.tool\n    def protected_action() -> str:\n        return \"success\"\n    \n    async with Client(mcp, auth=BearerAuth(\"secret-token\")) as client:\n        result = await client.call_tool(\"protected_action\", {})\n        assert result.content[0].text == \"success\"\n```\n\n#### Using Fixtures\n\nUse fixtures to create reusable data, server configurations, or other resources for your tests. Note that you should **not** open FastMCP clients in your fixtures as it can create hard-to-diagnose issues with event loops.\n\n```python\nimport pytest\nfrom fastmcp import FastMCP, Client\n\n@pytest.fixture\ndef weather_server():\n    server = FastMCP(\"WeatherServer\")\n    \n    @server.tool\n    def get_temperature(city: str) -> dict:\n        temps = {\"NYC\": 72, \"LA\": 85, \"Chicago\": 68}\n        return {\"city\": city, \"temp\": temps.get(city, 70)}\n    \n    return server\n\nasync def test_temperature_tool(weather_server):\n    async with Client(weather_server) as client:\n        result = await client.call_tool(\"get_temperature\", {\"city\": \"LA\"})\n        assert result.data == {\"city\": \"LA\", \"temp\": 85}\n```\n\n#### Effective Assertions\n\nAssertions should be specific and provide context on failure. When a test fails during CI, the assertion message should tell you exactly what went wrong.\n\n```python\n# Basic assertion - minimal context on failure\nassert result.status == \"success\"\n\n# Better - explains what was expected\nassert result.status == \"success\", f\"Expected successful operation, got {result.status}: {result.error}\"\n```\n\nTry not to have too many assertions in a single test unless you truly need to check various aspects of the same behavior. In general, assertions of different behaviors should be in separate tests.\n\n#### Inline Snapshots\n\nFastMCP uses `inline-snapshot` for testing complex data structures. On first run of `pytest --inline-snapshot=create` with an empty `snapshot()`, pytest will auto-populate the expected value. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.\n\n```python\nfrom inline_snapshot import snapshot\n\nasync def test_tool_schema_generation():\n    \"\"\"Test that tool schemas are generated correctly.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    @mcp.tool\n    def calculate_tax(amount: float, rate: float = 0.1) -> dict:\n        \"\"\"Calculate tax on an amount.\"\"\"\n        return {\"amount\": amount, \"tax\": amount * rate, \"total\": amount * (1 + rate)}\n    \n    tools = mcp.list_tools()\n    schema = tools[0].inputSchema\n    \n    # First run: snapshot() is empty, gets auto-populated\n    # Subsequent runs: compares against stored snapshot\n    assert schema == snapshot({\n        \"type\": \"object\", \n        \"properties\": {\n            \"amount\": {\"type\": \"number\"}, \n            \"rate\": {\"type\": \"number\", \"default\": 0.1}\n        }, \n        \"required\": [\"amount\"]\n    })\n```\n\n### In-Memory Testing\n\nFastMCP uses in-memory transport for testing, where servers and clients communicate directly. The majority of functionality can be tested in a deterministic fashion this way. We use more complex setups only when testing transports themselves.\n\nThe in-memory transport runs the real MCP protocol implementation without network overhead. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. Everything runs in the same Python process - you can set breakpoints anywhere and step through with your debugger.\n\n```python\nfrom fastmcp import FastMCP, Client\n\n# Create your server\nserver = FastMCP(\"WeatherServer\")\n\n@server.tool\ndef get_temperature(city: str) -> dict:\n    \"\"\"Get current temperature for a city\"\"\"\n    temps = {\"NYC\": 72, \"LA\": 85, \"Chicago\": 68}\n    return {\"city\": city, \"temp\": temps.get(city, 70)}\n\nasync def test_weather_operations():\n    # Pass server directly - no deployment needed\n    async with Client(server) as client:\n        result = await client.call_tool(\"get_temperature\", {\"city\": \"NYC\"})\n        assert result.data == {\"city\": \"NYC\", \"temp\": 72}\n```\n\nThis pattern makes tests deterministic and fast - typically completing in milliseconds rather than seconds.\n\n### Mocking External Dependencies\n\nFastMCP servers are standard Python objects, so you can mock external dependencies using your preferred approach:\n\n```python\nfrom unittest.mock import AsyncMock\n\nasync def test_database_tool():\n    server = FastMCP(\"DataServer\")\n    \n    # Mock the database\n    mock_db = AsyncMock()\n    mock_db.fetch_users.return_value = [\n        {\"id\": 1, \"name\": \"Alice\"},\n        {\"id\": 2, \"name\": \"Bob\"}\n    ]\n    \n    @server.tool\n    async def list_users() -> list:\n        return await mock_db.fetch_users()\n    \n    async with Client(server) as client:\n        result = await client.call_tool(\"list_users\", {})\n        assert len(result.data) == 2\n        assert result.data[0][\"name\"] == \"Alice\"\n        mock_db.fetch_users.assert_called_once()\n```\n\n### Testing Network Transports\n\nWhile in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports like HTTP or SSE. FastMCP provides two approaches: in-process async servers (preferred), and separate subprocess servers (for special cases).\n\n#### In-Process Network Testing (Preferred)\n\n<VersionBadge version=\"2.13.0\" />\n\nFor most network transport tests, use `run_server_async` as an async context manager. This runs the server as a task in the same process, providing fast, deterministic tests with full debugger support:\n\n```python\nimport pytest\nfrom fastmcp import FastMCP, Client\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.utilities.tests import run_server_async\n\ndef create_test_server() -> FastMCP:\n    \"\"\"Create a test server instance.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    return server\n\n@pytest.fixture\nasync def http_server() -> str:\n    \"\"\"Start server in-process for testing.\"\"\"\n    server = create_test_server()\n    async with run_server_async(server) as url:\n        yield url\n\nasync def test_http_transport(http_server: str):\n    \"\"\"Test actual HTTP transport behavior.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(http_server)\n    ) as client:\n        result = await client.ping()\n        assert result is True\n\n        greeting = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        assert greeting.data == \"Hello, World!\"\n```\n\nThe `run_server_async` context manager automatically handles server lifecycle and cleanup. This approach is faster than subprocess-based testing and provides better error messages.\n\n#### Subprocess Testing (Special Cases)\n\nFor tests that require complete process isolation (like STDIO transport or testing subprocess behavior), use `run_server_in_process`:\n\n```python\nimport pytest\nfrom fastmcp.utilities.tests import run_server_in_process\nfrom fastmcp import FastMCP, Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\ndef run_server(host: str, port: int) -> None:\n    \"\"\"Function to run in subprocess.\"\"\"\n    server = FastMCP(\"TestServer\")\n    \n    @server.tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n    \n    server.run(host=host, port=port)\n\n@pytest.fixture\nasync def http_server():\n    \"\"\"Fixture that runs server in subprocess.\"\"\"\n    with run_server_in_process(run_server, transport=\"http\") as url:\n        yield f\"{url}/mcp\"\n\nasync def test_http_transport(http_server: str):\n    \"\"\"Test actual HTTP transport behavior.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(http_server)\n    ) as client:\n        result = await client.ping()\n        assert result is True\n```\n\nThe `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.\n\n### Documentation Testing\n\nDocumentation requires the same validation as code. The `just docs` command launches a local Mintlify server that renders your documentation exactly as users will see it:\n\n```bash\n# Start local documentation server with hot reload\njust docs\n\n# Or run Mintlify directly\nmintlify dev\n```\n\nThe local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it."
  },
  {
    "path": "docs/development/v3-notes/auth-provider-env-vars.mdx",
    "content": "---\ntitle: Auth Provider Environment Variables\n---\n\n## Decision: Remove automatic environment variable loading from auth providers\n\nYou can still use environment variables for configuration - you just read them yourself with `os.environ` instead of relying on FastMCP's automatic loading.\n\n**Status:** Implemented in v3.0.0\n\n### Background\n\nAuth providers in v2.x used `pydantic-settings` to automatically load configuration from environment variables with a `FASTMCP_SERVER_AUTH_<PROVIDER>_` prefix. For example, `GitHubProvider` would read from:\n\n- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID`\n- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET`\n- `FASTMCP_SERVER_AUTH_GITHUB_BASE_URL`\n- etc.\n\nThis was implemented via a `*ProviderSettings(BaseSettings)` class in each provider, combined with a `NotSet` sentinel pattern to distinguish between \"not provided\" and `None`.\n\n### Why remove it\n\n1. **Maintenance burden**: Every new provider needed to implement the settings class, validators, and the `NotSet` merging logic. This was ~50-100 lines of boilerplate per provider.\n\n2. **Documentation complexity**: Each provider needed documentation explaining both the parameter and the corresponding environment variable. This doubled the surface area to document and maintain.\n\n3. **Contributor friction**: New contributors adding providers had to understand and replicate this pattern, which was a source of inconsistency and bugs.\n\n4. **Marginal user value**: Python developers are comfortable with `os.environ[\"VAR\"]` or `os.environ.get(\"VAR\", default)`. The automatic loading saved a single line of code per parameter while adding significant complexity.\n\n5. **Implicit behavior**: Magic environment variable loading makes it harder to understand where values come from. Explicit `os.environ` calls are more traceable.\n\n### Migration path\n\nThe migration is trivial - users add explicit environment variable reads:\n\n```python\n# Before (v2.x)\nauth = GitHubProvider()  # Relied on env vars\n\n# After (v3.0)\nimport os\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=os.environ[\"MY_BASE_URL\"],\n)\n```\n\nUsers can also use `os.environ.get()` with defaults, or any other configuration library they prefer (dotenv, dynaconf, etc.).\n\n### Backwards compatibility\n\nWe chose not to provide backwards compatibility because:\n\n1. This is a major version bump (v3.0), which is the appropriate time for breaking changes\n2. The migration is straightforward (add `os.environ` calls)\n3. Maintaining compatibility would require keeping all the boilerplate we're trying to remove\n4. The pattern was likely not heavily used - most production deployments pass secrets explicitly rather than relying on magic prefixes\n\n### What was removed\n\n- `*ProviderSettings(BaseSettings)` classes from all auth providers\n- `NotSet` sentinel usage in provider constructors\n- `pydantic-settings` dependency for auth providers\n- Environment variable documentation from provider docs\n- Related test cases for env var loading\n\n### Result\n\nProvider constructors are now simple and explicit. Required parameters are actually required (Python raises `TypeError` if missing), and optional parameters have clear defaults. The code is more readable and easier to maintain.\n"
  },
  {
    "path": "docs/development/v3-notes/v3-features.mdx",
    "content": "---\ntitle: v3.0 Feature Tracking\n---\n\nThis document tracks major features in FastMCP v3.0 for release notes preparation.\n\n## 3.0.0rc1\n\n### SamplingTool Conversion Helpers\n\nServer tools (FunctionTool and TransformedTool) can now be passed directly to sampling methods via `SamplingTool.from_callable_tool()` ([#3062](https://github.com/PrefectHQ/fastmcp/pull/3062)). Previously, tools defined with `@mcp.tool` had to be recreated as functions for use in `ctx.sample()`. Now `ctx.sample()` and `ctx.sample_step()` accept these tool instances directly.\n\n```python\n@mcp.tool\ndef search(query: str) -> str:\n    \"\"\"Search the web.\"\"\"\n    return do_search(query)\n\n# Use tool directly in sampling\nresult = await ctx.sample(\n    \"Research Python frameworks\",\n    tools=[search]  # FunctionTool works directly!\n)\n```\n\n### Google GenAI Sampling Handler\n\nFastMCP now includes a sampling handler for Google's Gemini models ([#2977](https://github.com/jlowin/fastmcp/pull/2977)). This enables MCP clients to use Google's GenAI models with the sampling protocol, including full tool calling support.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling.handlers import GoogleGenaiSamplingHandler\nfrom google.genai import Client as GoogleGenaiClient\n\n# Initialize the handler\nhandler = GoogleGenaiSamplingHandler(\n    default_model=\"gemini-2.0-flash-exp\",\n    client=GoogleGenaiClient(),  # Optional - creates one if not provided\n)\n\n# Use with MCP sampling (handler is configured at Client construction)\nasync with Client(\"http://server/mcp\", sampling_handler=handler) as client:\n    result = await client.sample(\n        messages=[...],\n        tools=[...],\n    )\n```\n\nKey features:\n- Converts MCP tool schemas to Google's function calling format\n- Supports all Google GenAI models that implement function calling\n- Handles nullable types, nested objects, and arrays in tool schemas\n- Properly maps tool choices (`auto`, `required`, `none`) to Google's configuration\n- Preserves model preferences from MCP sampling parameters\n\nThe handler joins the existing Anthropic and OpenAI handlers, providing a consistent interface for model-agnostic sampling across providers.\n\n### Concurrent Tool Execution in Sampling\n\nWhen an LLM returns multiple tool calls in a single sampling response, they can now be executed concurrently ([#3022](https://github.com/PrefectHQ/fastmcp/pull/3022)). Default behavior remains sequential; opt in with `tool_concurrency`. Tools can declare `sequential=True` to force sequential execution even when concurrency is enabled.\n\n```python\nresult = await context.sample(\n    messages=\"Fetch weather for NYC and LA\",\n    tools=[fetch_weather],\n    tool_concurrency=0,  # Unlimited parallel execution\n)\n```\n\n### OpenAPI `validate_output` Option\n\n`OpenAPIProvider` and `FastMCP.from_openapi()` now accept `validate_output=False` to skip output schema validation ([#3134](https://github.com/PrefectHQ/fastmcp/pull/3134)). Useful when backends don't conform to their own OpenAPI response schemas — structured JSON still flows through, only the strict schema checking is disabled.\n\n```python\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    validate_output=False,\n)\n```\n\n### Auth Token Injection and Azure OBO Dependencies\n\nNew dependency injection for accessing the authenticated user's token directly in tool parameters ([#2918](https://github.com/PrefectHQ/fastmcp/pull/2918)). Works with any auth provider.\n\n```python\nfrom fastmcp.server.dependencies import CurrentAccessToken, TokenClaim\nfrom fastmcp.server.auth import AccessToken\n\n@mcp.tool()\nasync def my_tool(\n    token: AccessToken = CurrentAccessToken,\n    user_id: str = TokenClaim(\"oid\"),\n): ...\n```\n\nFor Azure/Entra, the new `fastmcp[azure]` extra adds `EntraOBOToken`, which handles the On-Behalf-Of token exchange declaratively:\n\n```python\nfrom fastmcp.server.auth.providers.azure import EntraOBOToken\n\n@mcp.tool()\nasync def get_emails(\n    graph_token: str = EntraOBOToken([\"https://graph.microsoft.com/Mail.Read\"]),\n):\n    # graph_token is ready — OBO exchange happened automatically\n    ...\n```\n\n### `generate-cli` Agent Skill Generation\n\n`fastmcp generate-cli` now produces a `SKILL.md` alongside the CLI script ([#3115](https://github.com/PrefectHQ/fastmcp/pull/3115)) — a Claude Code agent skill with pre-computed invocation syntax for every tool. Agents reading the skill can call tools immediately without running `--help`. On by default; pass `--no-skill` to opt out.\n\n### Background Task Notification Queue\n\nBackground tasks now use a distributed Redis notification queue for reliable delivery ([#2906](https://github.com/PrefectHQ/fastmcp/pull/2906)). Elicitation switches from polling to BLPOP (single blocking call instead of ~7,200 round-trips/hour), and notification delivery retries up to 3x with TTL-based expiration.\n\n### Async Auth Checks\n\nAuth check functions can now be `async`, enabling authorization decisions that depend on asynchronous operations like reading server state via `Context.get_state` or calling external services ([#3150](https://github.com/PrefectHQ/fastmcp/issues/3150)). Sync and async checks can be freely mixed. Previously, passing an async function as an auth check would silently pass (coroutine objects are truthy).\n\n### Optional `$ref` Dereferencing in Schemas\n\nSchema `$ref` dereferencing — which inlines all `$defs` for compatibility with MCP clients that don't handle `$ref` — is now controlled by the `dereference_schemas` constructor kwarg ([#3141](https://github.com/PrefectHQ/fastmcp/issues/3141)). Default is `True` (dereference on) because the non-compliant clients are popular and the failure mode is silent breakage that server authors can't diagnose. Opt out when you know your clients handle `$ref` and want smaller schemas:\n\n```python\nmcp = FastMCP(\"my-server\", dereference_schemas=False)\n```\n\nDereferencing is implemented as middleware (`DereferenceRefsMiddleware`) that runs at serve-time, so schemas are stored with `$ref` intact and only inlined when sent to clients.\n\n### Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed\n\nSixteen deprecated keyword arguments have been removed from `FastMCP.__init__`. Passing any of them now raises `TypeError` with a migration hint. Environment variables (e.g., `FASTMCP_HOST`) continue to work — only the constructor kwargs moved.\n\n**Transport/server settings** (`host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, `stateless_http`): Pass to `run()`, `run_http_async()`, or `http_app()` as appropriate, or set via environment variables.\n\n```python\n# Before\nmcp = FastMCP(\"server\", host=\"0.0.0.0\", port=8080)\nmcp.run()\n\n# After\nmcp = FastMCP(\"server\")\nmcp.run(transport=\"http\", host=\"0.0.0.0\", port=8080)\n```\n\n**Duplicate handling** (`on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts`): Use the unified `on_duplicate=` parameter.\n\n**Tag filtering** (`include_tags`, `exclude_tags`): Use `server.enable(tags=..., only=True)` and `server.disable(tags=...)` after construction.\n\n**Tool serializer** (`tool_serializer`): Return `ToolResult` from tools instead.\n\n**Tool transformations** (`tool_transformations`): Use `server.add_transform(ToolTransform(...))` after construction.\n\nThe `_deprecated_settings` attribute and `.settings` property are also removed. `ExperimentalSettings` has been deleted (dead code).\n\n### Breaking: `ui=` Renamed to `app=`\n\nThe MCP Apps decorator parameter has been renamed from `ui=ToolUI(...)` / `ui=ResourceUI(...)` to `app=AppConfig(...)` ([#3117](https://github.com/PrefectHQ/fastmcp/pull/3117)). `ToolUI` and `ResourceUI` are consolidated into a single `AppConfig` class. Wire format is unchanged. See the MCP Apps section under beta2 for full details.\n## 3.0.0beta2\n\n### CLI: `fastmcp list` and `fastmcp call`\n\nNew client-side CLI commands for querying and invoking tools on any MCP server — remote URLs, local Python files, MCPConfig JSON, or arbitrary stdio commands. Especially useful for giving LLMs that don't have built-in MCP support access to MCP tools via shell commands.\n\n```bash\n# Discover tools on a server\nfastmcp list http://localhost:8000/mcp\nfastmcp list server.py\nfastmcp list --command 'npx -y @modelcontextprotocol/server-github'\n\n# Call a tool\nfastmcp call server.py greet name=World\nfastmcp call http://localhost:8000/mcp search query=hello limit=5\nfastmcp call server.py create_item '{\"name\": \"Widget\", \"tags\": [\"a\", \"b\"]}'\n```\n\nKey features:\n- Tool arguments are auto-coerced using the tool's JSON schema (`limit=5` → int)\n- Single JSON objects work as positional args alongside `key=value` and `--input-json`\n- `--input-schema` / `--output-schema` for full JSON schemas, `--json` for machine-readable output\n- `--transport sse` for SSE servers, `--command` for stdio servers\n- Auto OAuth for HTTP targets (no-ops if server doesn't require auth)\n- Fuzzy tool name matching suggests alternatives on typos\n- Interactive terminal elicitation for tools that request user input mid-execution\n\nDocumentation: [CLI Querying](/cli/client)\n\n### CLI: `fastmcp discover` and name-based resolution\n\n`fastmcp discover` scans editor configs (Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose) and project-level `mcp.json` files for MCP server definitions. Discovered servers can be referenced by name — or `source:name` for precision — in `fastmcp list` and `fastmcp call`.\n\n```bash\n# See all configured servers\nfastmcp discover\n\n# Use a server by name\nfastmcp list weather\nfastmcp call weather get_forecast city=London\n\n# Target a specific source with source:name\nfastmcp list claude-code:my-server\nfastmcp call cursor:weather get_forecast city=London\n\n# Filter discovery to specific sources\nfastmcp discover --source claude-code --source cursor\n```\n\nDocumentation: [CLI Querying](/cli/client)\n\n### CLI: Expanded Reload File Watching\n\nThe `--reload` flag now watches a comprehensive set of file types, making it suitable for MCP apps with frontend bundles ([#3028](https://github.com/PrefectHQ/fastmcp/pull/3028)). Previously limited to `.py` files, it now watches JavaScript, TypeScript, HTML, CSS, config files, and media assets.\n\n### CLI: fastmcp install stdio\n\nThe new `fastmcp install stdio` command generates full `uv run` commands for running FastMCP servers over stdio ([#3032](https://github.com/PrefectHQ/fastmcp/pull/3032)).\n\n```bash\n# Generate command for a server\nfastmcp install stdio server.py\n\n# Outputs:\n# uv run --directory /path/to/project fastmcp run server.py\n```\n\nThe command automatically detects the project directory and generates the appropriate `uv run` invocation, making it easy to integrate FastMCP servers with MCP clients.\n\n### CIMD (Client ID Metadata Documents)\n\nCIMD provides an alternative to Dynamic Client Registration for OAuth-authenticated MCP servers. Instead of registering with each server dynamically, clients host a static JSON document at an HTTPS URL. That URL becomes the client's `client_id`, and servers verify identity through domain ownership.\n\n**Client usage:**\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\nasync with Client(\n    \"https://mcp-server.example.com/mcp\",\n    auth=OAuth(\n        client_metadata_url=\"https://myapp.example.com/oauth/client.json\",\n    ),\n) as client:\n    await client.ping()\n```\n\nThe `OAuth` helper now supports deferred binding — `mcp_url` is optional when using `OAuth` with `Client(auth=...)`, since the transport provides the server URL automatically.\n\n**CLI tools for document management:**\n\n```bash\n# Generate a CIMD document\nfastmcp auth cimd create --name \"My App\" \\\n  --redirect-uri \"http://localhost:*/callback\" \\\n  --client-id \"https://myapp.example.com/oauth/client.json\" \\\n  --output client.json\n\n# Validate a hosted document\nfastmcp auth cimd validate https://myapp.example.com/oauth/client.json\n```\n\n**Server-side support:**\n\nCIMD is enabled by default on `OAuthProxy` and its provider subclasses (GitHub, Google, etc.). The server-side implementation includes SSRF-hardened document fetching with DNS pinning, dual redirect URI validation (both CIMD document patterns and proxy patterns must match), HTTP cache-aware revalidation, and `private_key_jwt` assertion validation for clients that need stronger authentication than public client auth.\n\nKey details:\n- CIMD URLs must be HTTPS with a non-root path\n- `token_endpoint_auth_method` limited to `none` or `private_key_jwt` (no shared secrets)\n- `redirect_uris` in CIMD documents support wildcard port patterns (`http://localhost:*/callback`)\n- Servers fetch and cache documents with standard HTTP caching (ETag, Last-Modified, Cache-Control)\n- CIMD is a protocol-level feature — any auth provider implementing the spec can support it\n\nDocumentation: [CIMD Authentication](/clients/auth/cimd), [OAuth Proxy CIMD config](/servers/auth/oauth-proxy#cimd-support)\n\n### Pre-Registered OAuth Clients\n\nThe `OAuth` client helper now accepts `client_id` and `client_secret` parameters for servers where the client is already registered ([#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)). This bypasses Dynamic Client Registration entirely — useful when DCR is disabled, or when the server has pre-provisioned credentials for your application.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\nasync with Client(\n    \"https://mcp-server.example.com/mcp\",\n    auth=OAuth(\n        client_id=\"my-registered-app\",\n        client_secret=\"my-secret\",\n        scopes=[\"read\", \"write\"],\n    ),\n) as client:\n    await client.ping()\n```\n\nThe static credentials are injected before the OAuth flow begins, so the client never attempts DCR. If the server rejects the credentials, the error surfaces immediately rather than retrying with fresh registration (which can't help for fixed credentials). Public clients can omit `client_secret`.\n\nDocumentation: [Pre-Registered Clients](/clients/auth/oauth#pre-registered-clients)\n\n### CLI: `fastmcp generate-cli`\n\n`fastmcp generate-cli` connects to any MCP server, reads its tool schemas, and writes a standalone Python CLI script where every tool becomes a typed subcommand with flags, help text, and tab completion ([#3065](https://github.com/PrefectHQ/fastmcp/pull/3065)). The insight is that MCP tool schemas already contain everything a CLI framework needs — parameter names, types, descriptions, required/optional status — so the generator maps JSON Schema directly into [cyclopts](https://cyclopts.readthedocs.io/) commands.\n\n```bash\n# Generate from any server spec\nfastmcp generate-cli weather\nfastmcp generate-cli http://localhost:8000/mcp\nfastmcp generate-cli server.py my_weather_cli.py\n\n# Use the generated script\npython my_weather_cli.py call-tool get_forecast --city London --days 3\npython my_weather_cli.py list-tools\npython my_weather_cli.py read-resource docs://readme\n```\n\nThe generated script embeds the resolved transport (URL or stdio command), so it's self-contained — users don't need to know about MCP or FastMCP to use it. Supports `-f` to overwrite existing files, and name-based resolution via `fastmcp discover`.\n\nDocumentation: [Generate CLI](/cli/generate-cli)\n\n### CLI: Goose Integration\n\nNew `fastmcp install goose` command that generates a `goose://extension?...` deeplink URL and opens it, prompting Goose to install the server as a STDIO extension ([#3040](https://github.com/PrefectHQ/fastmcp/pull/3040)). Goose requires `uvx` rather than `uv run`, so the command builds the appropriate invocation automatically.\n\n```bash\nfastmcp install goose server.py\nfastmcp install goose server.py --with pandas --python 3.11\n```\n\nAlso adds a full integration guide at [Goose Integration](/integrations/goose).\n\n### ResponseLimitingMiddleware\n\nNew middleware for controlling tool response sizes, preventing large outputs from overwhelming LLM context windows ([#3072](https://github.com/PrefectHQ/fastmcp/pull/3072)). Text responses are truncated at UTF-8 character boundaries; structured responses (tools with `output_schema`) raise `ToolError` since truncation would corrupt the schema.\n\n```python\nfrom fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware\n\n# Limit all tool responses to 500KB\nmcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000))\n\n# Limit only specific tools, raise errors instead of truncating\nmcp.add_middleware(ResponseLimitingMiddleware(\n    max_size=100_000,\n    tools=[\"search\", \"fetch_data\"],\n    raise_on_unstructured=True,\n))\n```\n\nKey features:\n- Configurable size limit (default 1MB)\n- Tool-specific filtering via `tools` parameter\n- Size metadata added to result's `meta` field for monitoring\n- Configurable `raise_on_structured` and `raise_on_unstructured` behavior\n\nDocumentation: [Middleware](/servers/middleware)\n\n### Background Task Context (SEP-1686)\n\n`Context` now works transparently in background tasks running in Docket workers ([#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)). Previously, tools running as background tasks couldn't use `ctx.elicit()` because there was no active request context. Now, when a tool executes in a Docket worker, `Context` detects this via its `task_id` and routes elicitation through Redis-based coordination: the task sets its status to `input_required`, sends a `notifications/tasks/updated` notification with elicitation metadata, and waits for the client to respond via `tasks/sendInput`.\n\n```python\n@mcp.tool(task=True)\nasync def interactive_task(ctx: Context) -> str:\n    # Works transparently in both foreground and background task modes\n    result = await ctx.elicit(\"Please provide additional input\", str)\n\n    if isinstance(result, AcceptedElicitation):\n        return f\"You provided: {result.data}\"\n    else:\n        return \"Elicitation was declined or cancelled\"\n```\n\n`ctx.is_background_task` and `ctx.task_id` are available for tools that need to branch on execution mode.\n\n### `require_auth` Removed\n\nThe `require_auth` authorization check introduced in beta1 has been removed in favor of scope-based authorization via `require_scopes` ([#3103](https://github.com/PrefectHQ/fastmcp/pull/3103)). Since configuring an `AuthProvider` already rejects unauthenticated requests at the transport level, `require_auth` was redundant — `require_scopes` provides the same guarantee with better granularity. The beta1 Component Authorization section has been updated to reflect this.\n\n### MCP Apps (SDK Compatibility)\n\nSupport for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) — the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet — those are future phases.\n\n**Breaking change from beta 2:** The `ui=` parameter on `@mcp.tool()` and `@mcp.resource()` has been renamed to `app=`, and the `ToolUI`/`ResourceUI` classes have been consolidated into a single `AppConfig` class. This follows the established `task=True`/`TaskConfig` pattern. The wire format (`meta[\"ui\"]`, `_meta.ui`) is unchanged.\n\n**Registering tools with app metadata:**\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.apps import AppConfig, ResourceCSP, ResourcePermissions\n\nmcp = FastMCP(\"My Server\")\n\n# Register the HTML bundle as a ui:// resource with CSP\n@mcp.resource(\n    \"ui://my-app/view.html\",\n    app=AppConfig(\n        csp=ResourceCSP(resource_domains=[\"https://unpkg.com\"]),\n        permissions=ResourcePermissions(clipboard_write={}),\n    ),\n)\ndef app_html() -> str:\n    from pathlib import Path\n    return Path(\"./dist/index.html\").read_text()\n\n# Tool with UI — clients render an iframe alongside the result\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\"))\nasync def list_users() -> list[dict]:\n    return [{\"id\": \"1\", \"name\": \"Alice\"}]\n\n# App-only tool — visible to the UI but hidden from the model\n@mcp.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\", visibility=[\"app\"]))\nasync def delete_user(id: str) -> dict:\n    return {\"deleted\": True}\n```\n\nThe `app=` parameter accepts `True` (enable with defaults), an `AppConfig` instance, or a raw dict for forward compatibility. It merges into `meta[\"ui\"]` — alongside any other metadata you set.\n\n**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly.\n\n**Extension negotiation**: The server advertises `io.modelcontextprotocol/ui` in `capabilities.extensions`. UI metadata (`_meta.ui`) always flows through to clients — the MCP Apps spec assigns visibility enforcement to the host, not the server. Tools can check whether the connected client supports a given extension at runtime via `ctx.client_supports_extension()`:\n\n```python\nfrom fastmcp import Context\nfrom fastmcp.server.apps import AppConfig, UI_EXTENSION_ID\n\n@mcp.tool(app=AppConfig(resource_uri=\"ui://dashboard\"))\nasync def dashboard(ctx: Context) -> dict:\n    data = compute_dashboard()\n    if ctx.client_supports_extension(UI_EXTENSION_ID):\n        return data\n    return {\"summary\": format_text(data)}\n```\n\n**Key details:**\n- `AppConfig` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional). On resources, `resource_uri` and `visibility` are validated as not-applicable and will raise `ValueError` if set.\n- `csp` accepts a `ResourceCSP` model with structured domain lists: `connect_domains`, `resource_domains`, `frame_domains`, `base_uri_domains`\n- `permissions` accepts a `ResourcePermissions` model: `camera`, `microphone`, `geolocation`, `clipboard_write` (each set to `{}` to request)\n- `AppConfig` uses `extra=\"allow\"` for forward compatibility with future spec additions\n- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`, `connectDomains`, `clipboardWrite`)\n- Resource metadata (including CSP/permissions) is propagated to `resources/read` response content items so hosts can read it when rendering the iframe\n- `ctx.client_supports_extension(id)` is a general-purpose method — works for any extension, not just MCP Apps\n- `structuredContent` in tool results already works via `ToolResult` — MCP Apps clients use this to pass data into the iframe\n- The server does not strip `_meta.ui` for non-UI clients; per the spec, visibility enforcement is the host's responsibility\n\n**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.\n\nImplementation: `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).\n\n---\n\n## 3.0.0beta1\n\n### Provider-Based Architecture\n\nv3.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.\n\n**Core abstraction** (`src/fastmcp/server/providers/base.py`):\n```python\nclass Provider:\n    async def list_tools(self) -> Sequence[Tool]: ...\n    async def get_tool(self, name: str) -> Tool | None: ...\n    async def list_resources(self) -> Sequence[Resource]: ...\n    async def get_resource(self, uri: str) -> Resource | None: ...\n    async def list_resource_templates(self) -> Sequence[ResourceTemplate]: ...\n    async def get_resource_template(self, uri: str) -> ResourceTemplate | None: ...\n    async def list_prompts(self) -> Sequence[Prompt]: ...\n    async def get_prompt(self, name: str) -> Prompt | None: ...\n```\n\nProviders support:\n- **Lifecycle management**: `async def lifespan()` for setup/teardown\n- **Visibility control**: `enable()` / `disable()` with name, version, tags, components, and allowlist mode\n- **Transform stacking**: `provider.add_transform(Namespace(...))`, `provider.add_transform(ToolTransform(...))`\n\n### LocalProvider\n\n`LocalProvider` (`src/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers:\n\n```python\nfrom fastmcp.server.providers import LocalProvider\n\nprovider = LocalProvider()\n\n@provider.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# Attach to multiple servers\nserver1 = FastMCP(\"Server1\", providers=[provider])\nserver2 = FastMCP(\"Server2\", providers=[provider])\n```\n\n### ProxyProvider\n\n`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.\n\n```python\nfrom fastmcp.server import create_proxy\n\n# Create proxy to remote server\nserver = create_proxy(\"http://remote-server/mcp\")\n```\n\n### OpenAPIProvider\n\n`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.\n\n```python\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\nimport httpx\n\nclient = httpx.AsyncClient(base_url=\"https://api.example.com\")\nprovider = OpenAPIProvider(openapi_spec=spec, client=client)\n\nmcp = FastMCP(\"API Server\", providers=[provider])\n```\n\nFeatures:\n- Automatic route-to-component mapping (GET → resource, POST/PUT/DELETE → tool)\n- Custom route mappings via `route_maps` or `route_map_fn`\n- Component customization via `mcp_component_fn`\n- Name collision detection and handling\n\n### FastMCPProvider\n\n`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.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.server.transforms import Namespace\n\nmain = FastMCP(\"Main\")\nsub = FastMCP(\"Sub\")\n\n@sub.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# Mount with namespace\nprovider = FastMCPProvider(sub)\nprovider.add_transform(Namespace(\"sub\"))\nmain.add_provider(provider)\n# Tool accessible as \"sub_greet\"\n```\n\n### Transforms\n\nTransforms 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.\n\n**Built-in transforms** (`src/fastmcp/server/transforms/`):\n\n- `Namespace` - adds prefixes to names (`tool` → `api_tool`) and path segments to URIs (`data://x` → `data://api/x`)\n- `ToolTransform` - modifies tool schemas (rename, description, tags, argument transforms)\n- `Visibility` - sets visibility state on components by key or tag (backs `enable()`/`disable()` API)\n- `VersionFilter` - filters components by version range (`version_gte`, `version_lt`)\n- `ResourcesAsTools` - exposes resources as tools for tool-only clients\n- `PromptsAsTools` - exposes prompts as tools for tool-only clients\n\n```python\nfrom fastmcp.server.transforms import Namespace, ToolTransform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\n\nprovider = SomeProvider()\nprovider.add_transform(Namespace(\"api\"))\nprovider.add_transform(ToolTransform({\n    \"api_verbose_tool_name\": ToolTransformConfig(name=\"short\")\n}))\n\n# Stacking composes transformations\n# \"foo\" → \"api_foo\" (namespace) → \"short\" (rename)\n```\n\n**Custom transforms** subclass `Transform` and override needed methods:\n\n```python\nfrom collections.abc import Sequence\nfrom fastmcp.server.transforms import Transform, ListToolsNext, GetToolNext\nfrom fastmcp.tools import Tool\n\nclass TagFilter(Transform):\n    def __init__(self, required_tags: set[str]):\n        self.required_tags = required_tags\n\n    async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:\n        tools = await call_next()  # Get tools from downstream\n        return [t for t in tools if t.tags & self.required_tags]\n\n    async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:\n        tool = await call_next(name)\n        return tool if tool and tool.tags & self.required_tags else None\n```\n\nTransforms apply at two levels:\n- **Provider-level**: `provider.add_transform()` - affects only that provider's components\n- **Server-level**: `server.add_transform()` - affects all components from all providers\n\nDocumentation: `docs/servers/transforms/transforms.mdx`, `docs/servers/visibility.mdx`\n\n### ResourcesAsTools and PromptsAsTools\n\nThese transforms expose resources and prompts as tools for clients that only support the tools protocol. Each transform generates two tools that provide listing and access functionality.\n\n**ResourcesAsTools** generates `list_resources` and `read_resource` tools:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import ResourcesAsTools\n\nmcp = FastMCP(\"Server\")\n\n@mcp.resource(\"data://config\")\ndef get_config() -> dict:\n    return {\"setting\": \"value\"}\n\nmcp.add_transform(ResourcesAsTools(mcp))\n# Now has list_resources and read_resource tools\n```\n\nThe `list_resources` tool returns JSON with resource metadata. The `read_resource` tool accepts a URI and returns the resource content, preserving both text and binary data through base64 encoding.\n\n**PromptsAsTools** generates `list_prompts` and `get_prompt` tools:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import PromptsAsTools\n\nmcp = FastMCP(\"Server\")\n\n@mcp.prompt\ndef analyze_code(code: str, language: str = \"python\") -> str:\n    return f\"Analyze this {language} code:\\n{code}\"\n\nmcp.add_transform(PromptsAsTools(mcp))\n# Now has list_prompts and get_prompt tools\n```\n\nThe `list_prompts` tool returns JSON with prompt metadata including argument information. The `get_prompt` tool accepts a prompt name and optional arguments dict, returning the rendered prompt as a messages array. Non-text content (like embedded resources) is preserved as structured JSON.\n\nBoth transforms:\n- Capture a provider reference at construction for deferred querying\n- Route through `FastMCP.read_resource()` / `FastMCP.render_prompt()` when the provider is FastMCP, ensuring middleware chains execute\n- Fall back to direct provider methods for plain providers\n- Return JSON for easy parsing by tool-only clients\n\nDocumentation: `docs/servers/transforms/resources-as-tools.mdx`, `docs/servers/transforms/prompts-as-tools.mdx`\n\n---\n\n### Session-Scoped State\n\nv3.0 changes context state from request-scoped to session-scoped. State now persists across multiple tool calls within the same MCP session.\n\n```python\n@mcp.tool\nasync def increment_counter(ctx: Context) -> int:\n    count = await ctx.get_state(\"counter\") or 0\n    await ctx.set_state(\"counter\", count + 1)\n    return count + 1\n```\n\nState is automatically keyed by session ID, ensuring isolation between different clients. The implementation uses [pykeyvalue](https://github.com/strawgate/py-key-value) for pluggable storage backends:\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\n\n# Use Redis for distributed deployments\nmcp = FastMCP(\"server\", session_state_store=RedisStore(...))\n```\n\n**Key details:**\n- Methods are now async: `await ctx.get_state()`, `await ctx.set_state()`, `await ctx.delete_state()`\n- State expires after 1 day (TTL) to prevent unbounded memory growth\n- Works during `on_initialize` middleware when using the same session object\n- For distributed HTTP, session identity comes from the `mcp-session-id` header\n\nDocumentation: `docs/servers/context.mdx`\n\n---\n\n### Visibility System\n\nComponents can be enabled/disabled using the visibility system. Each `enable()` or `disable()` call adds a stateless Visibility transform that marks components via internal metadata. Later transforms override earlier ones.\n\n```python\nmcp = FastMCP(\"Server\")\n\n# Disable by name and component type\nmcp.disable(names={\"dangerous_tool\"}, components=[\"tool\"])\n\n# Disable by tag\nmcp.disable(tags={\"admin\"})\n\n# Disable by version\nmcp.disable(names={\"old_tool\"}, version=\"1.0\", components=[\"tool\"])\n\n# Allowlist mode - only show components with these tags\nmcp.enable(tags={\"public\"}, only=True)\n\n# Enable overrides earlier disable (later transform wins)\nmcp.disable(tags={\"internal\"})\nmcp.enable(names={\"safe_tool\"})  # safe_tool is visible despite internal tag\n```\n\nWorks at both server and provider level. Supports:\n- **Blocklist mode** (default): All components visible except explicitly disabled\n- **Allowlist mode** (`only=True`): Only explicitly enabled components visible\n- **Tag-based filtering**: Enable/disable groups of components by tag\n- **Override semantics**: Later transforms override earlier marks (enable after disable = enabled)\n- **Transform ordering**: Visibility transforms are injected at the point you call them, so component state is known\n\n#### Per-Session Visibility\n\nServer-level visibility changes affect all connected clients. For per-session control, use `Context` methods that apply rules only to the current session ([#2917](https://github.com/PrefectHQ/fastmcp/pull/2917)):\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(\"Server\")\n\n@mcp.tool(tags={\"premium\"})\ndef premium_analysis(data: str) -> str:\n    return f\"Premium analysis of: {data}\"\n\n@mcp.tool\nasync def unlock_premium(ctx: Context) -> str:\n    \"\"\"Unlock premium features for this session only.\"\"\"\n    await ctx.enable_components(tags={\"premium\"})\n    return \"Premium features unlocked\"\n\n@mcp.tool\nasync def reset_features(ctx: Context) -> str:\n    \"\"\"Reset to default feature set.\"\"\"\n    await ctx.reset_visibility()\n    return \"Features reset to defaults\"\n\n# Globally disabled - sessions unlock individually\nmcp.disable(tags={\"premium\"})\n```\n\nSession visibility methods:\n- `await ctx.enable_components(...)`: Enable components for this session\n- `await ctx.disable_components(...)`: Disable components for this session\n- `await ctx.reset_visibility()`: Clear session rules, return to global defaults\n\nSession rules override global transforms. FastMCP automatically sends `ToolListChangedNotification` (and resource/prompt equivalents) to affected sessions when visibility changes.\n\nDocumentation: `docs/servers/visibility.mdx`\n\n---\n\n### Component Versioning\n\nv3.0 introduces versioning support for tools, resources, and prompts. Components can declare a version, and when multiple versions of the same component exist, the highest version is automatically exposed to clients.\n\n**Declaring versions:**\n\n```python\n@mcp.tool(version=\"1.0\")\ndef add(x: int, y: int) -> int:\n    return x + y\n\n@mcp.tool(version=\"2.0\")\ndef add(x: int, y: int, z: int = 0) -> int:\n    return x + y + z\n\n# Only v2.0 is exposed to clients via list_tools()\n# Calling \"add\" invokes the v2.0 implementation\n```\n\n**Version comparison:**\n- Uses PEP 440 semantic versioning (1.10 > 1.9 > 1.2)\n- Falls back to string comparison for non-PEP 440 versions (dates like `2025-01-15` work)\n- Unversioned components sort lower than any versioned component\n- The `v` prefix is normalized (`v1.0` equals `1.0`)\n\n**Version visibility in meta:**\n\nList operations expose all available versions in the component's `meta` field:\n\n```python\ntools = await client.list_tools()\n# Each tool's meta includes:\n# - meta[\"fastmcp\"][\"version\"]: the version of this component (\"2.0\")\n# - meta[\"fastmcp\"][\"versions\"]: all available versions [\"2.0\", \"1.0\"]\n```\n\n**Retrieving and calling specific versions:**\n\n```python\n# Get the highest version (default)\ntool = await server.get_tool(\"add\")\n\n# Get a specific version\ntool_v1 = await server.get_tool(\"add\", version=\"1.0\")\n\n# Call a specific version\nresult = await server.call_tool(\"add\", {\"x\": 1, \"y\": 2}, version=\"1.0\")\n```\n\n**Client version requests:**\n\nThe FastMCP client supports version selection:\n\n```python\nasync with Client(server) as client:\n    # Call specific tool version\n    result = await client.call_tool(\"add\", {\"x\": 1, \"y\": 2}, version=\"1.0\")\n\n    # Get specific prompt version\n    prompt = await client.get_prompt(\"my_prompt\", {\"text\": \"...\"}, version=\"2.0\")\n```\n\nFor generic MCP clients, pass version via `_meta` in arguments:\n\n```json\n{\n  \"x\": 1,\n  \"y\": 2,\n  \"_meta\": {\n    \"fastmcp\": {\n      \"version\": \"1.0\"\n    }\n  }\n}\n```\n\n**VersionFilter transform:**\n\nThe `VersionFilter` transform enables serving different API versions from a single codebase:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import LocalProvider\nfrom fastmcp.server.transforms import VersionFilter\n\n# Define components on a shared provider\ncomponents = LocalProvider()\n\n@components.tool(version=\"1.0\")\ndef calculate(x: int, y: int) -> int:\n    return x + y\n\n@components.tool(version=\"2.0\")\ndef calculate(x: int, y: int, z: int = 0) -> int:\n    return x + y + z\n\n# Create servers that share the provider with different filters\napi_v1 = FastMCP(\"API v1\", providers=[components])\napi_v1.add_transform(VersionFilter(version_lt=\"2.0\"))\n\napi_v2 = FastMCP(\"API v2\", providers=[components])\napi_v2.add_transform(VersionFilter(version_gte=\"2.0\"))\n```\n\nParameters mirror comparison operators:\n- `version_gte`: Versions >= this value pass through\n- `version_lt`: Versions < this value pass through\n\n**Key format:**\n\nComponent keys now include a version suffix using `@` as a delimiter:\n- Versioned: `tool:add@1.0`, `resource:data://config@2.0`\n- Unversioned: `tool:add@`, `resource:data://config@`\n\nThe `@` is always present (even for unversioned components) to enable unambiguous parsing of URIs that may contain `@`.\n\n---\n\n### Type-Safe Canonical Results\n\nv3.0 introduces type-safe result classes that provide explicit control over component responses while supporting MCP runtime metadata: `ToolResult` ([#2736](https://github.com/PrefectHQ/fastmcp/pull/2736)), `ResourceResult` ([#2734](https://github.com/PrefectHQ/fastmcp/pull/2734)), and `PromptResult` ([#2738](https://github.com/PrefectHQ/fastmcp/pull/2738)).\n\n#### ToolResult\n\n`ToolResult` (`src/fastmcp/tools/tool.py:79`) provides structured tool responses:\n\n```python\nfrom fastmcp.tools import ToolResult\n\n@mcp.tool\ndef process(data: str) -> ToolResult:\n    return ToolResult(\n        content=[TextContent(type=\"text\", text=\"Done\")],\n        structured_content={\"status\": \"success\", \"count\": 42},\n        meta={\"processing_time_ms\": 150}\n    )\n```\n\nFields:\n- `content`: List of MCP ContentBlocks (text, images, etc.)\n- `structured_content`: Dict matching tool's output schema\n- `meta`: Runtime metadata passed to MCP as `_meta`\n\n#### ResourceResult\n\n`ResourceResult` (`src/fastmcp/resources/resource.py:117`) provides structured resource responses:\n\n```python\nfrom fastmcp.resources import ResourceResult, ResourceContent\n\n@mcp.resource(\"data://items\")\ndef get_items() -> ResourceResult:\n    return ResourceResult(\n        contents=[\n            ResourceContent({\"key\": \"value\"}),  # auto-serialized to JSON\n            ResourceContent(b\"binary data\"),\n        ],\n        meta={\"count\": 2}\n    )\n```\n\nAccepts strings, bytes, or `list[ResourceContent]` for flexible content handling.\n\n#### PromptResult\n\n`PromptResult` (`src/fastmcp/prompts/prompt.py:109`) provides structured prompt responses:\n\n```python\nfrom fastmcp.prompts import PromptResult, Message\n\n@mcp.prompt\ndef conversation() -> PromptResult:\n    return PromptResult(\n        messages=[\n            Message(\"What's the weather?\"),\n            Message(\"It's sunny today.\", role=\"assistant\"),\n        ],\n        meta={\"generated_at\": \"2024-01-01\"}\n    )\n```\n\n---\n\n### Background Tasks (SEP-1686)\n\nv3.0 implements MCP SEP-1686 for background task execution via Docket integration.\n\n**Configuration** (`src/fastmcp/server/tasks/config.py`):\n\n```python\nfrom fastmcp.server.tasks import TaskConfig\n\n@mcp.tool(task=TaskConfig(mode=\"required\"))\nasync def long_running_task():\n    # Must be executed as background task\n    ...\n\n@mcp.tool(task=TaskConfig(mode=\"optional\"))\nasync def flexible_task():\n    # Supports both sync and task execution\n    ...\n\n@mcp.tool(task=True)  # Shorthand for mode=\"optional\"\nasync def simple_task():\n    ...\n```\n\nTask modes:\n- `\"forbidden\"`: Component does not support task execution (default)\n- `\"optional\"`: Supports both synchronous and task execution\n- `\"required\"`: Must be executed as background task\n\nRequires Docket server for task scheduling and result polling.\n\n---\n\n### Decorators Return Functions\n\nv3.0 changes what decorators (`@tool`, `@resource`, `@prompt`) return ([#2856](https://github.com/PrefectHQ/fastmcp/pull/2856)). Decorators now return the original function unchanged, rather than transforming it into a component object.\n\n**v3 behavior (default):**\n```python\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# greet is still your function - call it directly\ngreet(\"World\")  # \"Hello, World!\"\n```\n\n**Why this matters:**\n- Functions stay callable - useful for testing and reuse\n- Instance methods just work: `mcp.add_tool(obj.method)`\n- Matches how Flask, FastAPI, and Typer decorators behave\n\n**For v2 compatibility:**\n\n```python\nimport fastmcp\n\n# v2 behavior: decorators return FunctionTool/FunctionResource/FunctionPrompt objects\nfastmcp.settings.decorator_mode = \"object\"\n```\n\nEnvironment variable: `FASTMCP_DECORATOR_MODE=object`\n\n---\n\n### CLI Auto-Reload\n\nThe `--reload` flag enables file watching with automatic server restarts for development ([#2816](https://github.com/PrefectHQ/fastmcp/pull/2816)).\n\n```bash\n# Watch for changes and restart\nfastmcp run server.py --reload\n\n# Watch specific directories\nfastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib\n\n# Works with any transport\nfastmcp run server.py --reload --transport http --port 8080\n```\n\nImplementation (`src/fastmcp/cli/run.py`):\n- Uses `watchfiles` for efficient file monitoring\n- Runs server as subprocess for clean restarts\n- Stateless mode for seamless reconnection after restart\n- stdio: Full MCP features including elicitation\n- HTTP: Limited bidirectional features during reload\n\nAlso available with `fastmcp dev inspector`:\n```bash\nfastmcp dev inspector server.py  # Includes --reload by default\n```\n\n---\n\n### Component Authorization\n\nv3.0 introduces callable-based authorization for tools, resources, and prompts ([#2855](https://github.com/PrefectHQ/fastmcp/pull/2855)).\n\n**Component-level auth**:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import require_scopes\n\nmcp = FastMCP()\n\n@mcp.tool(auth=require_scopes(\"write\"))\ndef protected_tool(): ...\n\n@mcp.resource(\"data://secret\", auth=require_scopes(\"read\"))\ndef secret_data(): ...\n\n@mcp.prompt(auth=require_scopes(\"admin\"))\ndef admin_prompt(): ...\n```\n\n**Server-wide auth via middleware**:\n\n```python\nfrom fastmcp.server.middleware import AuthMiddleware\nfrom fastmcp.server.auth import require_scopes, restrict_tag\n\n# Require specific scope for all components\nmcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"api\"))])\n\n# Tag-based restrictions\nmcp = FastMCP(middleware=[\n    AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))\n])\n```\n\nBuilt-in checks:\n- `require_scopes(*scopes)`: Requires specific OAuth scopes\n- `restrict_tag(tag, scopes)`: Requires scopes only for tagged components\n\nCustom checks receive `AuthContext` with `token` and `component`:\n\n```python\ndef custom_check(ctx: AuthContext) -> bool:\n    return ctx.token is not None and \"admin\" in ctx.token.scopes\n```\n\nSTDIO transport bypasses all auth checks (no OAuth concept).\n\n---\n\n### FileSystemProvider\n\nv3.0 introduces `FileSystemProvider`, a fundamentally different approach to organizing MCP servers. Instead of importing a server instance and decorating functions with `@server.tool`, you use standalone decorators in separate files and let the provider discover them.\n\n**The problem it solves**: Traditional servers require coordination between files—either tool files import the server (creating coupling) or the server imports all tool modules (creating a registry bottleneck). FileSystemProvider removes this coupling entirely.\n\n**Usage** ([#2823](https://github.com/PrefectHQ/fastmcp/pull/2823)):\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import FileSystemProvider\n\n# Scans mcp/ directory for decorated functions\nmcp = FastMCP(\"server\", providers=[FileSystemProvider(\"mcp/\")])\n```\n\n**Tool files are self-contained**:\n\n```python\n# mcp/tools/greet.py\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    \"\"\"Greet someone by name.\"\"\"\n    return f\"Hello, {name}!\"\n```\n\nFeatures:\n- **Standalone decorators**: `@tool`, `@resource`, `@prompt` from `fastmcp.tools`, `fastmcp.resources`, `fastmcp.prompts` ([#2832](https://github.com/PrefectHQ/fastmcp/pull/2832))\n- **Reload mode**: `FileSystemProvider(\"mcp/\", reload=True)` re-scans on every request for development\n- **Package support**: Directories with `__init__.py` support relative imports\n- **Warning deduplication**: Broken imports warn once per file modification\n\nDocumentation: [FileSystemProvider](/servers/providers/filesystem)\n\n---\n\n### SkillsProvider\n\nv3.0 introduces `SkillsProvider` for exposing agent skills as MCP resources ([#2944](https://github.com/PrefectHQ/fastmcp/pull/2944)). Skills are directories containing instructions and supporting files that teach AI assistants how to perform tasks—used by Claude Code, Cursor, VS Code Copilot, and other AI coding tools.\n\n**Usage**:\n\n```python\nfrom pathlib import Path\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\nmcp = FastMCP(\"Skills Server\")\nmcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / \".claude\" / \"skills\"))\n```\n\nEach subdirectory with a `SKILL.md` file becomes a discoverable skill. Clients see:\n- `skill://{name}/SKILL.md` - Main instruction file\n- `skill://{name}/_manifest` - JSON listing of all files with sizes and hashes\n- `skill://{name}/{path}` - Supporting files (via template or resources)\n\n**Two-layer architecture**:\n- `SkillProvider` - Handles a single skill folder\n- `SkillsDirectoryProvider` - Scans directories, creates a `SkillProvider` per valid skill\n\n**Vendor providers** with locked default paths:\n\n| Provider | Directory |\n|----------|-----------|\n| `ClaudeSkillsProvider` | `~/.claude/skills/` |\n| `CursorSkillsProvider` | `~/.cursor/skills/` |\n| `VSCodeSkillsProvider` | `~/.copilot/skills/` |\n| `CodexSkillsProvider` | `/etc/codex/skills/`, `~/.codex/skills/` |\n| `GeminiSkillsProvider` | `~/.gemini/skills/` |\n| `GooseSkillsProvider` | `~/.config/agents/skills/` |\n| `CopilotSkillsProvider` | `~/.copilot/skills/` |\n| `OpenCodeSkillsProvider` | `~/.config/opencode/skills/` |\n\n**Progressive disclosure**: By default, supporting files are hidden from `list_resources()` and accessed via template. Set `supporting_files=\"resources\"` for full enumeration.\n\nDocumentation: [Skills Provider](/servers/providers/skills)\n\n---\n\n### OpenTelemetry Tracing\n\nv3.0 adds OpenTelemetry instrumentation for observability into server and client operations ([#2869](https://github.com/PrefectHQ/fastmcp/pull/2869)).\n\n**Server spans**: Created for tool calls, resource reads, and prompt renders with attributes including component key, provider type, session ID, and auth context.\n\n**Client spans**: Wrap outgoing calls with W3C trace context propagation via request meta.\n\n```python\n# Tracing is passive - configure an OTel SDK to export spans\nfrom opentelemetry import trace\nfrom opentelemetry.sdk.trace import TracerProvider\nfrom opentelemetry.sdk.trace.export import BatchSpanProcessor\nfrom opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter\n\nprovider = TracerProvider()\nprovider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))\ntrace.set_tracer_provider(provider)\n\n# Use fastmcp normally - spans export to your configured backend\n```\n\nComponents provide their own span attributes through a `get_span_attributes()` method that subclasses override—this lets LocalProvider, FastMCPProvider, and ProxyProvider each include relevant context (original names, backend URIs, etc.).\n\nDocumentation: [Telemetry](/servers/telemetry)\n\n---\n\n### Pagination\n\nv3.0 adds pagination support for list operations when servers expose many components ([#2903](https://github.com/PrefectHQ/fastmcp/pull/2903)).\n\n```python\nfrom fastmcp import FastMCP\n\n# Enable pagination with 50 items per page\nserver = FastMCP(\"ComponentRegistry\", list_page_size=50)\n```\n\nWhen `list_page_size` is set, `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` paginate responses with `nextCursor` for subsequent pages.\n\n**Client behavior**: The FastMCP Client fetches all pages automatically—`list_tools()` and similar methods return the complete list. For manual pagination (memory constraints, progress reporting), use `_mcp` variants:\n\n```python\nasync with Client(server) as client:\n    result = await client.list_tools_mcp()\n    while result.nextCursor:\n        result = await client.list_tools_mcp(cursor=result.nextCursor)\n```\n\nDocumentation: [Pagination](/servers/pagination)\n\n---\n\n### Composable Lifespans\n\nLifespans can be combined with the `|` operator for modular setup/teardown ([#2828](https://github.com/PrefectHQ/fastmcp/pull/2828)):\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.lifespan import lifespan\n\n@lifespan\nasync def db_lifespan(server):\n    db = await connect_db()\n    try:\n        yield {\"db\": db}\n    finally:\n        await db.close()\n\n@lifespan\nasync def cache_lifespan(server):\n    cache = await connect_cache()\n    try:\n        yield {\"cache\": cache}\n    finally:\n        await cache.close()\n\nmcp = FastMCP(\"server\", lifespan=db_lifespan | cache_lifespan)\n```\n\nBoth enter lifespans in order and exit in reverse (LIFO). Context dicts are merged.\n\nAlso adds `combine_lifespans()` utility for FastAPI integration:\n\n```python\nfrom fastmcp.utilities.lifespan import combine_lifespans\n\napp = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))\n```\n\nDocumentation: [Lifespan](/servers/lifespan)\n\n---\n\n### Tool Timeout\n\nTools can limit foreground execution time with a `timeout` parameter ([#2872](https://github.com/PrefectHQ/fastmcp/pull/2872)):\n\n```python\n@mcp.tool(timeout=30.0)\nasync def fetch_data(url: str) -> dict:\n    \"\"\"Fetch with 30-second timeout.\"\"\"\n    ...\n```\n\nWhen exceeded, clients receive MCP error code `-32000`. Both sync and async tools are supported—sync functions run in thread pools so the timeout applies regardless of execution model.\n\nNote: This timeout applies to foreground execution only. Background tasks (`task=True`) execute in Docket workers where this timeout isn't enforced.\n\n---\n\n### PingMiddleware\n\nSends periodic server-to-client pings to keep long-lived connections alive ([#2838](https://github.com/PrefectHQ/fastmcp/pull/2838)):\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware import PingMiddleware\n\nmcp = FastMCP(\"server\")\nmcp.add_middleware(PingMiddleware(interval_ms=5000))\n```\n\nThe middleware starts a background ping task on first message from each session, using the session's existing task group for automatic cleanup when the session ends.\n\n---\n\n### Context.transport Property\n\nTools can detect which transport is active ([#2850](https://github.com/PrefectHQ/fastmcp/pull/2850)):\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"example\")\n\n@mcp.tool\ndef my_tool(ctx: Context) -> str:\n    if ctx.transport == \"stdio\":\n        return \"short response\"\n    return \"detailed response with more context\"\n```\n\nReturns `Literal[\"stdio\", \"sse\", \"streamable-http\"]` when running, or `None` outside a server context.\n\n---\n\n### Automatic Threadpool for Sync Functions\n\nSynchronous tools, resources, and prompts now automatically run in a threadpool, preventing event loop blocking during concurrent requests ([#2865](https://github.com/PrefectHQ/fastmcp/pull/2865)):\n\n```python\nimport time\n\n@mcp.tool\ndef slow_tool():\n    time.sleep(10)  # No longer blocks other requests\n    return \"done\"\n```\n\nThree concurrent calls now execute in parallel (~10s) rather than sequentially (30s). Uses `anyio.to_thread.run_sync()` which properly propagates contextvars, so `Context` and `Depends` continue to work.\n\n---\n\n### CLI Update Notifications\n\nThe CLI notifies users when a newer FastMCP version is available on PyPI ([#2840](https://github.com/PrefectHQ/fastmcp/pull/2840)).\n\n**Setting**: `FASTMCP_CHECK_FOR_UPDATES`\n- `\"stable\"` - Check for stable releases (default)\n- `\"prerelease\"` - Include alpha/beta/rc versions\n- `\"off\"` - Disable\n\n12-hour cache, 2-second timeout, fails silently on network errors.\n\n---\n\n### Deprecated Features\n\nThese emit deprecation warnings but continue to work.\n\n#### Mount Prefix Parameter\n\nThe `prefix` parameter for `mount()` renamed to `namespace`:\n\n```python\n# Deprecated\nmain.mount(subserver, prefix=\"api\")\n\n# New\nmain.mount(subserver, namespace=\"api\")\n```\n\n#### Tag Filtering, Tool Serializer, Tool Transformations Init Parameters\n\nThese constructor parameters have been **removed** (not just deprecated) as of rc1. See \"Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed\" in the rc1 section above. The `add_tool_transformation()` and `remove_tool_transformation()` methods remain as deprecated shims.\n\n---\n\n### Breaking Changes\n\n#### WSTransport Removed\n\nThe deprecated `WSTransport` client transport has been removed ([#2826](https://github.com/PrefectHQ/fastmcp/pull/2826)). Use `StreamableHttpTransport` instead.\n\n#### Decorators Return Functions\n\nDecorators (`@tool`, `@resource`, `@prompt`) now return the original function instead of component objects. Code that treats the decorated function as a `FunctionTool`, `FunctionResource`, or `FunctionPrompt` will break.\n\n```python\n# v2.x\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nisinstance(greet, FunctionTool)  # True\n\n# v3.0\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nisinstance(greet, FunctionTool)  # False\ncallable(greet)                  # True - it's still your function\ngreet(\"World\")                   # \"Hello, World!\"\n```\n\nSet `FASTMCP_DECORATOR_MODE=object` or `fastmcp.settings.decorator_mode = \"object\"` for v2 behavior.\n\n#### Component Enable/Disable Moved to Server/Provider\n\nThe `enabled` field and `enable()`/`disable()` methods removed from component objects:\n\n```python\n# v2.x\ntool = await server.get_tool(\"my_tool\")\ntool.disable()\n\n# v3.0\nserver.disable(names={\"my_tool\"}, components=[\"tool\"])\n```\n\n#### Component Lookup Methods\n\nServer lookup and listing methods have updated signatures:\n\n- Parameter names: `get_tool(name=...)`, `get_resource(uri=...)`, etc. (was `key`)\n- Plural listing methods renamed: `get_tools()` → `list_tools()`, `get_resources()` → `list_resources()`, etc.\n- Return types: `list_tools()`, `list_resources()`, etc. return lists instead of dicts\n\n```python\n# v2.x\ntools = await server.get_tools()\ntool = tools[\"my_tool\"]\n\n# v3.0\ntools = await server.list_tools()\ntool = next((t for t in tools if t.name == \"my_tool\"), None)\n```\n\n#### Prompt Return Types\n\nPrompt functions now use `Message` instead of `mcp.types.PromptMessage`:\n\n```python\n# v2.x\nfrom mcp.types import PromptMessage, TextContent\n\n@mcp.prompt\ndef my_prompt() -> PromptMessage:\n    return PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))\n\n# v3.0\nfrom fastmcp.prompts import Message\n\n@mcp.prompt\ndef my_prompt() -> Message:\n    return Message(\"Hello\")  # role defaults to \"user\"\n```\n\n#### Auth Provider Environment Variables Removed\n\nAuth providers no longer auto-load from environment variables ([#2752](https://github.com/PrefectHQ/fastmcp/pull/2752)):\n\n```python\n# v2.x - auto-loaded from FASTMCP_SERVER_AUTH_GITHUB_*\nauth = GitHubProvider()\n\n# v3.0 - explicit configuration\nimport os\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n)\n```\n\nSee `docs/development/v3-notes/auth-provider-env-vars.mdx` for rationale.\n\n#### Server Banner Environment Variable\n\n`FASTMCP_SHOW_CLI_BANNER` → `FASTMCP_SHOW_SERVER_BANNER` ([#2771](https://github.com/PrefectHQ/fastmcp/pull/2771))\n\nNow applies to all server startup methods, not just the CLI.\n\n#### Context State Methods Are Async\n\n`ctx.set_state()` and `ctx.get_state()` are now async and session-scoped:\n\n```python\n# v2.x\nctx.set_state(\"key\", \"value\")\nvalue = ctx.get_state(\"key\")\n\n# v3.0\nawait ctx.set_state(\"key\", \"value\")\nvalue = await ctx.get_state(\"key\")\n```\n\nState now persists across requests within a session. See \"Session-Scoped State\" above.\n"
  },
  {
    "path": "docs/docs.json",
    "content": "{\n  \"$schema\": \"https://mintlify.com/docs.json\",\n  \"appearance\": {\n    \"default\": \"system\",\n    \"strict\": false\n  },\n  \"background\": {\n    \"color\": {\n      \"dark\": \"#222831\",\n      \"light\": \"#EEEEEE\"\n    },\n    \"decoration\": \"gradient\"\n  },\n  \"banner\": {\n    \"content\": \"Deploy FastMCP servers for free on [Prefect Horizon](https://www.prefect.io/horizon)\"\n  },\n  \"colors\": {\n    \"dark\": \"#f72585\",\n    \"light\": \"#4cc9f0\",\n    \"primary\": \"#2d00f7\"\n  },\n  \"contextual\": {\n    \"options\": [\n      \"copy\",\n      \"view\"\n    ]\n  },\n  \"description\": \"The fast, Pythonic way to build MCP servers and clients.\",\n  \"errors\": {\n    \"404\": {\n      \"description\": \"You\\u2019ve wandered outside the context.\",\n      \"redirect\": false,\n      \"title\": \"Don't panic.\"\n    }\n  },\n  \"favicon\": {\n    \"dark\": \"/assets/brand/favicon-dark.svg\",\n    \"light\": \"/assets/brand/favicon-light.svg\"\n  },\n  \"footer\": {\n    \"socials\": {\n      \"discord\": \"https://discord.gg/uu8dJCgttd\",\n      \"github\": \"https://github.com/PrefectHQ/fastmcp\",\n      \"website\": \"https://www.prefect.io\",\n      \"x\": \"https://x.com/fastmcp\"\n    }\n  },\n  \"integrations\": {\n    \"ga4\": {\n      \"measurementId\": \"G-64R5W1TJXG\"\n    }\n  },\n  \"interaction\": {\n    \"drilldown\": false\n  },\n  \"logo\": {\n    \"dark\": \"/assets/brand/wordmark-white.png\",\n    \"light\": \"/assets/brand/wordmark.png\"\n  },\n  \"name\": \"FastMCP\",\n  \"navbar\": {\n    \"links\": [\n      {\n        \"href\": \"https://discord.gg/uu8dJCgttd\",\n        \"icon\": \"discord\",\n        \"label\": \"\"\n      },\n      {\n        \"href\": \"https://prefect.io/horizon\",\n        \"icon\": \"cloud\",\n        \"label\": \"Prefect Horizon\"\n      }\n    ],\n    \"primary\": {\n      \"href\": \"https://github.com/PrefectHQ/fastmcp\",\n      \"type\": \"github\"\n    }\n  },\n  \"navigation\": {\n    \"versions\": [\n      {\n        \"dropdowns\": [\n          {\n            \"dropdown\": \"Documentation\",\n            \"groups\": [\n              {\n                \"group\": \"Get Started\",\n                \"pages\": [\n                  \"getting-started/welcome\",\n                  \"getting-started/installation\",\n                  \"getting-started/quickstart\"\n                ]\n              },\n              {\n                \"group\": \"Servers\",\n                \"pages\": [\n                  \"servers/server\",\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Core Components\",\n                    \"icon\": \"toolbox\",\n                    \"pages\": [\n                      \"servers/tools\",\n                      \"servers/resources\",\n                      \"servers/prompts\",\n                      \"servers/context\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Features\",\n                    \"icon\": \"stars\",\n                    \"pages\": [\n                      \"servers/tasks\",\n                      \"servers/composition\",\n                      \"servers/dependency-injection\",\n                      \"servers/elicitation\",\n                      \"servers/icons\",\n                      \"servers/lifespan\",\n                      \"servers/logging\",\n                      \"servers/middleware\",\n                      \"servers/pagination\",\n                      \"servers/progress\",\n                      \"servers/sampling\",\n                      \"servers/storage-backends\",\n                      \"servers/telemetry\",\n                      \"servers/testing\",\n                      \"servers/versioning\"\n                    ],\n                    \"tag\": \"UPDATED\"\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Providers\",\n                    \"icon\": \"layer-group\",\n                    \"pages\": [\n                      \"servers/providers/overview\",\n                      \"servers/providers/local\",\n                      \"servers/providers/filesystem\",\n                      \"servers/providers/proxy\",\n                      \"servers/providers/skills\",\n                      \"servers/providers/custom\"\n                    ],\n                    \"tag\": \"NEW\"\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Transforms\",\n                    \"icon\": \"wand-magic-sparkles\",\n                    \"pages\": [\n                      \"servers/transforms/transforms\",\n                      \"servers/transforms/namespace\",\n                      \"servers/transforms/tool-transformation\",\n                      \"servers/visibility\",\n                      \"servers/transforms/code-mode\",\n                      \"servers/transforms/tool-search\",\n                      \"servers/transforms/resources-as-tools\",\n                      \"servers/transforms/prompts-as-tools\"\n                    ],\n                    \"tag\": \"NEW\"\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Authentication\",\n                    \"icon\": \"key\",\n                    \"pages\": [\n                      \"servers/auth/authentication\",\n                      \"servers/auth/token-verification\",\n                      \"servers/auth/remote-oauth\",\n                      \"servers/auth/oauth-proxy\",\n                      \"servers/auth/oidc-proxy\",\n                      \"servers/auth/full-oauth-server\",\n                      \"servers/auth/multi-auth\"\n                    ],\n                    \"tag\": \"UPDATED\"\n                  },\n                  \"servers/authorization\",\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Deployment\",\n                    \"icon\": \"rocket\",\n                    \"pages\": [\n                      \"deployment/running-server\",\n                      \"deployment/http\",\n                      \"deployment/prefect-horizon\",\n                      \"deployment/server-configuration\"\n                    ]\n                  }\n                ]\n              },\n              {\n                \"group\": \"Apps\",\n                \"pages\": [\n                  \"apps/overview\",\n                  \"apps/prefab\",\n                  \"apps/patterns\",\n                  \"apps/development\",\n                  \"apps/low-level\"\n                ]\n              },\n              {\n                \"group\": \"Clients\",\n                \"pages\": [\n                  \"clients/client\",\n                  \"clients/transports\",\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Core Operations\",\n                    \"icon\": \"toolbox\",\n                    \"pages\": [\n                      \"clients/tools\",\n                      \"clients/resources\",\n                      \"clients/prompts\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Handlers\",\n                    \"icon\": \"hand\",\n                    \"pages\": [\n                      \"clients/notifications\",\n                      \"clients/sampling\",\n                      \"clients/elicitation\",\n                      \"clients/tasks\",\n                      \"clients/progress\",\n                      \"clients/logging\",\n                      \"clients/roots\"\n                    ],\n                    \"tag\": \"UPDATED\"\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Authentication\",\n                    \"icon\": \"key\",\n                    \"pages\": [\n                      \"clients/auth/oauth\",\n                      \"clients/auth/cimd\",\n                      \"clients/auth/bearer\"\n                    ],\n                    \"tag\": \"UPDATED\"\n                  }\n                ]\n              },\n              {\n                \"group\": \"Integrations\",\n                \"pages\": [\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Auth\",\n                    \"icon\": \"key\",\n                    \"pages\": [\n                      \"integrations/auth0\",\n                      \"integrations/authkit\",\n                      \"integrations/aws-cognito\",\n                      \"integrations/azure\",\n                      \"integrations/descope\",\n                      \"integrations/discord\",\n                      \"integrations/eunomia-authorization\",\n                      \"integrations/github\",\n                      \"integrations/google\",\n                      \"integrations/oci\",\n                      \"integrations/permit\",\n                      \"integrations/propelauth\",\n                      \"integrations/scalekit\",\n                      \"integrations/supabase\",\n                      \"integrations/workos\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Web Frameworks\",\n                    \"icon\": \"code\",\n                    \"pages\": [\n                      \"integrations/fastapi\",\n                      \"integrations/openapi\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"AI Assistants\",\n                    \"icon\": \"robot\",\n                    \"pages\": [\n                      \"integrations/chatgpt\",\n                      \"integrations/claude-code\",\n                      \"integrations/claude-desktop\",\n                      \"integrations/cursor\",\n                      \"integrations/gemini-cli\",\n                      \"integrations/goose\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"AI SDKs\",\n                    \"icon\": \"microchip\",\n                    \"pages\": [\n                      \"integrations/anthropic\",\n                      \"integrations/gemini\",\n                      \"integrations/openai\"\n                    ]\n                  },\n                  \"integrations/mcp-json-configuration\"\n                ]\n              },\n              {\n                \"group\": \"CLI\",\n                \"pages\": [\n                  \"cli/overview\",\n                  \"cli/running\",\n                  \"cli/install-mcp\",\n                  \"cli/inspecting\",\n                  \"cli/client\",\n                  \"cli/generate-cli\",\n                  \"cli/auth\"\n                ]\n              },\n              {\n                \"group\": \"More\",\n                \"pages\": [\n                  \"more/settings\",\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Upgrading\",\n                    \"icon\": \"up\",\n                    \"pages\": [\n                      \"getting-started/upgrading/from-fastmcp-2\",\n                      \"getting-started/upgrading/from-mcp-sdk\",\n                      \"getting-started/upgrading/from-low-level-sdk\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"Development\",\n                    \"icon\": \"code\",\n                    \"pages\": [\n                      \"development/contributing\",\n                      \"development/tests\",\n                      \"development/releases\",\n                      \"patterns/contrib\"\n                    ]\n                  },\n                  {\n                    \"collapsed\": true,\n                    \"group\": \"What's New\",\n                    \"icon\": \"sparkles\",\n                    \"pages\": [\n                      \"updates\",\n                      \"changelog\"\n                    ]\n                  }\n                ]\n              }\n            ],\n            \"icon\": \"book\"\n          },\n          {\n            \"anchors\": [\n              {\n                \"anchor\": \"Python SDK\",\n                \"icon\": \"python\",\n                \"pages\": [\n                  \"python-sdk/fastmcp-decorators\",\n                  \"python-sdk/fastmcp-dependencies\",\n                  \"python-sdk/fastmcp-exceptions\",\n                  \"python-sdk/fastmcp-mcp_config\",\n                  \"python-sdk/fastmcp-settings\",\n                  \"python-sdk/fastmcp-telemetry\",\n                  {\n                    \"group\": \"fastmcp.cli\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-cli-__init__\",\n                      \"python-sdk/fastmcp-cli-apps_dev\",\n                      \"python-sdk/fastmcp-cli-auth\",\n                      \"python-sdk/fastmcp-cli-cimd\",\n                      \"python-sdk/fastmcp-cli-cli\",\n                      \"python-sdk/fastmcp-cli-client\",\n                      \"python-sdk/fastmcp-cli-discovery\",\n                      \"python-sdk/fastmcp-cli-generate\",\n                      {\n                        \"group\": \"install\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-cli-install-__init__\",\n                          \"python-sdk/fastmcp-cli-install-claude_code\",\n                          \"python-sdk/fastmcp-cli-install-claude_desktop\",\n                          \"python-sdk/fastmcp-cli-install-cursor\",\n                          \"python-sdk/fastmcp-cli-install-gemini_cli\",\n                          \"python-sdk/fastmcp-cli-install-goose\",\n                          \"python-sdk/fastmcp-cli-install-mcp_json\",\n                          \"python-sdk/fastmcp-cli-install-shared\",\n                          \"python-sdk/fastmcp-cli-install-stdio\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-cli-run\",\n                      \"python-sdk/fastmcp-cli-tasks\"\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.client\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-client-__init__\",\n                      {\n                        \"group\": \"auth\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-client-auth-__init__\",\n                          \"python-sdk/fastmcp-client-auth-bearer\",\n                          \"python-sdk/fastmcp-client-auth-oauth\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-client-client\",\n                      \"python-sdk/fastmcp-client-elicitation\",\n                      \"python-sdk/fastmcp-client-logging\",\n                      \"python-sdk/fastmcp-client-messages\",\n                      {\n                        \"group\": \"mixins\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-client-mixins-__init__\",\n                          \"python-sdk/fastmcp-client-mixins-prompts\",\n                          \"python-sdk/fastmcp-client-mixins-resources\",\n                          \"python-sdk/fastmcp-client-mixins-task_management\",\n                          \"python-sdk/fastmcp-client-mixins-tools\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-client-oauth_callback\",\n                      \"python-sdk/fastmcp-client-progress\",\n                      \"python-sdk/fastmcp-client-roots\",\n                      {\n                        \"group\": \"sampling\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-client-sampling-__init__\",\n                          {\n                            \"group\": \"handlers\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-client-sampling-handlers-__init__\",\n                              \"python-sdk/fastmcp-client-sampling-handlers-anthropic\",\n                              \"python-sdk/fastmcp-client-sampling-handlers-google_genai\",\n                              \"python-sdk/fastmcp-client-sampling-handlers-openai\"\n                            ]\n                          }\n                        ]\n                      },\n                      \"python-sdk/fastmcp-client-tasks\",\n                      \"python-sdk/fastmcp-client-telemetry\",\n                      {\n                        \"group\": \"transports\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-client-transports-__init__\",\n                          \"python-sdk/fastmcp-client-transports-base\",\n                          \"python-sdk/fastmcp-client-transports-config\",\n                          \"python-sdk/fastmcp-client-transports-http\",\n                          \"python-sdk/fastmcp-client-transports-inference\",\n                          \"python-sdk/fastmcp-client-transports-memory\",\n                          \"python-sdk/fastmcp-client-transports-sse\",\n                          \"python-sdk/fastmcp-client-transports-stdio\"\n                        ]\n                      }\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.experimental\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-experimental-__init__\",\n                      {\n                        \"group\": \"sampling\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-experimental-sampling-__init__\",\n                          \"python-sdk/fastmcp-experimental-sampling-handlers\"\n                        ]\n                      },\n                      {\n                        \"group\": \"transforms\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-experimental-transforms-__init__\",\n                          \"python-sdk/fastmcp-experimental-transforms-code_mode\"\n                        ]\n                      }\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.prompts\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-prompts-__init__\",\n                      \"python-sdk/fastmcp-prompts-base\",\n                      \"python-sdk/fastmcp-prompts-function_prompt\"\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.resources\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-resources-__init__\",\n                      \"python-sdk/fastmcp-resources-base\",\n                      \"python-sdk/fastmcp-resources-function_resource\",\n                      \"python-sdk/fastmcp-resources-template\",\n                      \"python-sdk/fastmcp-resources-types\"\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.server\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-server-__init__\",\n                      \"python-sdk/fastmcp-server-app\",\n                      \"python-sdk/fastmcp-server-apps\",\n                      {\n                        \"group\": \"auth\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-auth-__init__\",\n                          \"python-sdk/fastmcp-server-auth-auth\",\n                          \"python-sdk/fastmcp-server-auth-authorization\",\n                          \"python-sdk/fastmcp-server-auth-cimd\",\n                          \"python-sdk/fastmcp-server-auth-jwt_issuer\",\n                          \"python-sdk/fastmcp-server-auth-middleware\",\n                          {\n                            \"group\": \"oauth_proxy\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-server-auth-oauth_proxy-__init__\",\n                              \"python-sdk/fastmcp-server-auth-oauth_proxy-consent\",\n                              \"python-sdk/fastmcp-server-auth-oauth_proxy-models\",\n                              \"python-sdk/fastmcp-server-auth-oauth_proxy-proxy\",\n                              \"python-sdk/fastmcp-server-auth-oauth_proxy-ui\"\n                            ]\n                          },\n                          \"python-sdk/fastmcp-server-auth-oidc_proxy\",\n                          {\n                            \"group\": \"providers\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-server-auth-providers-__init__\",\n                              \"python-sdk/fastmcp-server-auth-providers-auth0\",\n                              \"python-sdk/fastmcp-server-auth-providers-aws\",\n                              \"python-sdk/fastmcp-server-auth-providers-azure\",\n                              \"python-sdk/fastmcp-server-auth-providers-debug\",\n                              \"python-sdk/fastmcp-server-auth-providers-descope\",\n                              \"python-sdk/fastmcp-server-auth-providers-discord\",\n                              \"python-sdk/fastmcp-server-auth-providers-github\",\n                              \"python-sdk/fastmcp-server-auth-providers-google\",\n                              \"python-sdk/fastmcp-server-auth-providers-in_memory\",\n                              \"python-sdk/fastmcp-server-auth-providers-introspection\",\n                              \"python-sdk/fastmcp-server-auth-providers-jwt\",\n                              \"python-sdk/fastmcp-server-auth-providers-oci\",\n                              \"python-sdk/fastmcp-server-auth-providers-propelauth\",\n                              \"python-sdk/fastmcp-server-auth-providers-scalekit\",\n                              \"python-sdk/fastmcp-server-auth-providers-supabase\",\n                              \"python-sdk/fastmcp-server-auth-providers-workos\"\n                            ]\n                          },\n                          \"python-sdk/fastmcp-server-auth-redirect_validation\",\n                          \"python-sdk/fastmcp-server-auth-ssrf\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-server-context\",\n                      \"python-sdk/fastmcp-server-dependencies\",\n                      \"python-sdk/fastmcp-server-elicitation\",\n                      \"python-sdk/fastmcp-server-event_store\",\n                      \"python-sdk/fastmcp-server-http\",\n                      \"python-sdk/fastmcp-server-lifespan\",\n                      \"python-sdk/fastmcp-server-low_level\",\n                      {\n                        \"group\": \"middleware\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-middleware-__init__\",\n                          \"python-sdk/fastmcp-server-middleware-authorization\",\n                          \"python-sdk/fastmcp-server-middleware-caching\",\n                          \"python-sdk/fastmcp-server-middleware-dereference\",\n                          \"python-sdk/fastmcp-server-middleware-error_handling\",\n                          \"python-sdk/fastmcp-server-middleware-logging\",\n                          \"python-sdk/fastmcp-server-middleware-middleware\",\n                          \"python-sdk/fastmcp-server-middleware-ping\",\n                          \"python-sdk/fastmcp-server-middleware-rate_limiting\",\n                          \"python-sdk/fastmcp-server-middleware-response_limiting\",\n                          \"python-sdk/fastmcp-server-middleware-timing\",\n                          \"python-sdk/fastmcp-server-middleware-tool_injection\"\n                        ]\n                      },\n                      {\n                        \"group\": \"mixins\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-mixins-__init__\",\n                          \"python-sdk/fastmcp-server-mixins-lifespan\",\n                          \"python-sdk/fastmcp-server-mixins-mcp_operations\",\n                          \"python-sdk/fastmcp-server-mixins-transport\"\n                        ]\n                      },\n                      {\n                        \"group\": \"openapi\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-openapi-__init__\",\n                          \"python-sdk/fastmcp-server-openapi-components\",\n                          \"python-sdk/fastmcp-server-openapi-routing\",\n                          \"python-sdk/fastmcp-server-openapi-server\"\n                        ]\n                      },\n                      {\n                        \"group\": \"providers\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-providers-__init__\",\n                          \"python-sdk/fastmcp-server-providers-aggregate\",\n                          \"python-sdk/fastmcp-server-providers-base\",\n                          \"python-sdk/fastmcp-server-providers-fastmcp_provider\",\n                          \"python-sdk/fastmcp-server-providers-filesystem\",\n                          \"python-sdk/fastmcp-server-providers-filesystem_discovery\",\n                          {\n                            \"group\": \"local_provider\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-server-providers-local_provider-__init__\",\n                              {\n                                \"group\": \"decorators\",\n                                \"pages\": [\n                                  \"python-sdk/fastmcp-server-providers-local_provider-decorators-__init__\",\n                                  \"python-sdk/fastmcp-server-providers-local_provider-decorators-prompts\",\n                                  \"python-sdk/fastmcp-server-providers-local_provider-decorators-resources\",\n                                  \"python-sdk/fastmcp-server-providers-local_provider-decorators-tools\"\n                                ]\n                              },\n                              \"python-sdk/fastmcp-server-providers-local_provider-local_provider\"\n                            ]\n                          },\n                          {\n                            \"group\": \"openapi\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-server-providers-openapi-__init__\",\n                              \"python-sdk/fastmcp-server-providers-openapi-components\",\n                              \"python-sdk/fastmcp-server-providers-openapi-provider\",\n                              \"python-sdk/fastmcp-server-providers-openapi-routing\"\n                            ]\n                          },\n                          \"python-sdk/fastmcp-server-providers-proxy\",\n                          {\n                            \"group\": \"skills\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-server-providers-skills-__init__\",\n                              \"python-sdk/fastmcp-server-providers-skills-claude_provider\",\n                              \"python-sdk/fastmcp-server-providers-skills-directory_provider\",\n                              \"python-sdk/fastmcp-server-providers-skills-skill_provider\",\n                              \"python-sdk/fastmcp-server-providers-skills-vendor_providers\"\n                            ]\n                          },\n                          \"python-sdk/fastmcp-server-providers-wrapped_provider\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-server-proxy\",\n                      {\n                        \"group\": \"sampling\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-sampling-__init__\",\n                          \"python-sdk/fastmcp-server-sampling-run\",\n                          \"python-sdk/fastmcp-server-sampling-sampling_tool\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-server-server\",\n                      {\n                        \"group\": \"tasks\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-tasks-__init__\",\n                          \"python-sdk/fastmcp-server-tasks-capabilities\",\n                          \"python-sdk/fastmcp-server-tasks-config\",\n                          \"python-sdk/fastmcp-server-tasks-elicitation\",\n                          \"python-sdk/fastmcp-server-tasks-handlers\",\n                          \"python-sdk/fastmcp-server-tasks-keys\",\n                          \"python-sdk/fastmcp-server-tasks-notifications\",\n                          \"python-sdk/fastmcp-server-tasks-requests\",\n                          \"python-sdk/fastmcp-server-tasks-routing\",\n                          \"python-sdk/fastmcp-server-tasks-subscriptions\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-server-telemetry\",\n                      {\n                        \"group\": \"transforms\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-server-transforms-__init__\",\n                          \"python-sdk/fastmcp-server-transforms-catalog\",\n                          \"python-sdk/fastmcp-server-transforms-namespace\",\n                          \"python-sdk/fastmcp-server-transforms-prompts_as_tools\",\n                          \"python-sdk/fastmcp-server-transforms-resources_as_tools\",\n                          {\n                            \"group\": \"search\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-server-transforms-search-__init__\",\n                              \"python-sdk/fastmcp-server-transforms-search-base\",\n                              \"python-sdk/fastmcp-server-transforms-search-bm25\",\n                              \"python-sdk/fastmcp-server-transforms-search-regex\"\n                            ]\n                          },\n                          \"python-sdk/fastmcp-server-transforms-tool_transform\",\n                          \"python-sdk/fastmcp-server-transforms-version_filter\",\n                          \"python-sdk/fastmcp-server-transforms-visibility\"\n                        ]\n                      }\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.tools\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-tools-__init__\",\n                      \"python-sdk/fastmcp-tools-base\",\n                      \"python-sdk/fastmcp-tools-function_parsing\",\n                      \"python-sdk/fastmcp-tools-function_tool\",\n                      \"python-sdk/fastmcp-tools-tool_transform\"\n                    ]\n                  },\n                  {\n                    \"group\": \"fastmcp.utilities\",\n                    \"pages\": [\n                      \"python-sdk/fastmcp-utilities-__init__\",\n                      \"python-sdk/fastmcp-utilities-async_utils\",\n                      \"python-sdk/fastmcp-utilities-auth\",\n                      \"python-sdk/fastmcp-utilities-cli\",\n                      \"python-sdk/fastmcp-utilities-components\",\n                      \"python-sdk/fastmcp-utilities-exceptions\",\n                      \"python-sdk/fastmcp-utilities-http\",\n                      \"python-sdk/fastmcp-utilities-inspect\",\n                      \"python-sdk/fastmcp-utilities-json_schema\",\n                      \"python-sdk/fastmcp-utilities-json_schema_type\",\n                      \"python-sdk/fastmcp-utilities-lifespan\",\n                      \"python-sdk/fastmcp-utilities-logging\",\n                      {\n                        \"group\": \"mcp_server_config\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-utilities-mcp_server_config-__init__\",\n                          {\n                            \"group\": \"v1\",\n                            \"pages\": [\n                              \"python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__\",\n                              {\n                                \"group\": \"environments\",\n                                \"pages\": [\n                                  \"python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__\",\n                                  \"python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base\",\n                                  \"python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv\"\n                                ]\n                              },\n                              \"python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config\",\n                              {\n                                \"group\": \"sources\",\n                                \"pages\": [\n                                  \"python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__\",\n                                  \"python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base\",\n                                  \"python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem\"\n                                ]\n                              }\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"group\": \"openapi\",\n                        \"pages\": [\n                          \"python-sdk/fastmcp-utilities-openapi-__init__\",\n                          \"python-sdk/fastmcp-utilities-openapi-director\",\n                          \"python-sdk/fastmcp-utilities-openapi-formatters\",\n                          \"python-sdk/fastmcp-utilities-openapi-json_schema_converter\",\n                          \"python-sdk/fastmcp-utilities-openapi-models\",\n                          \"python-sdk/fastmcp-utilities-openapi-parser\",\n                          \"python-sdk/fastmcp-utilities-openapi-schemas\"\n                        ]\n                      },\n                      \"python-sdk/fastmcp-utilities-pagination\",\n                      \"python-sdk/fastmcp-utilities-skills\",\n                      \"python-sdk/fastmcp-utilities-tests\",\n                      \"python-sdk/fastmcp-utilities-timeout\",\n                      \"python-sdk/fastmcp-utilities-token_cache\",\n                      \"python-sdk/fastmcp-utilities-types\",\n                      \"python-sdk/fastmcp-utilities-ui\",\n                      \"python-sdk/fastmcp-utilities-version_check\",\n                      \"python-sdk/fastmcp-utilities-versions\"\n                    ]\n                  }\n                ]\n              }\n            ],\n            \"dropdown\": \"SDK Reference\",\n            \"icon\": \"code\"\n          }\n        ],\n        \"version\": \"v3\"\n      },\n      {\n        \"dropdowns\": [\n          {\n            \"dropdown\": \"Documentation\",\n            \"groups\": [\n              {\n                \"group\": \"Get Started\",\n                \"pages\": [\n                  \"v2/getting-started/welcome\",\n                  \"v2/getting-started/installation\",\n                  \"v2/getting-started/quickstart\",\n                  \"v2/updates\"\n                ]\n              },\n              {\n                \"group\": \"Servers\",\n                \"pages\": [\n                  \"v2/servers/server\",\n                  {\n                    \"group\": \"Core Components\",\n                    \"icon\": \"toolbox\",\n                    \"pages\": [\n                      \"v2/servers/tools\",\n                      \"v2/servers/resources\",\n                      \"v2/servers/prompts\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Advanced Features\",\n                    \"icon\": \"stars\",\n                    \"pages\": [\n                      \"v2/servers/composition\",\n                      \"v2/servers/context\",\n                      \"v2/servers/elicitation\",\n                      \"v2/servers/icons\",\n                      \"v2/servers/logging\",\n                      \"v2/servers/middleware\",\n                      \"v2/servers/progress\",\n                      \"v2/servers/proxy\",\n                      \"v2/servers/sampling\",\n                      \"v2/servers/storage-backends\",\n                      \"v2/servers/tasks\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Authentication\",\n                    \"icon\": \"shield-check\",\n                    \"pages\": [\n                      \"v2/servers/auth/authentication\",\n                      \"v2/servers/auth/token-verification\",\n                      \"v2/servers/auth/remote-oauth\",\n                      \"v2/servers/auth/oauth-proxy\",\n                      \"v2/servers/auth/oidc-proxy\",\n                      \"v2/servers/auth/full-oauth-server\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Deployment\",\n                    \"icon\": \"rocket\",\n                    \"pages\": [\n                      \"v2/deployment/running-server\",\n                      \"v2/deployment/http\",\n                      \"deployment/prefect-horizon\",\n                      \"v2/deployment/server-configuration\"\n                    ]\n                  }\n                ]\n              },\n              {\n                \"group\": \"Clients\",\n                \"pages\": [\n                  {\n                    \"group\": \"Essentials\",\n                    \"icon\": \"cube\",\n                    \"pages\": [\n                      \"v2/clients/client\",\n                      \"v2/clients/transports\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Core Operations\",\n                    \"icon\": \"handshake\",\n                    \"pages\": [\n                      \"v2/clients/tools\",\n                      \"v2/clients/resources\",\n                      \"v2/clients/prompts\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Advanced Features\",\n                    \"icon\": \"stars\",\n                    \"pages\": [\n                      \"v2/clients/elicitation\",\n                      \"v2/clients/logging\",\n                      \"v2/clients/progress\",\n                      \"v2/clients/sampling\",\n                      \"v2/clients/tasks\",\n                      \"v2/clients/messages\",\n                      \"v2/clients/roots\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Authentication\",\n                    \"icon\": \"user-shield\",\n                    \"pages\": [\n                      \"v2/clients/auth/oauth\",\n                      \"v2/clients/auth/bearer\"\n                    ]\n                  }\n                ]\n              },\n              {\n                \"group\": \"Integrations\",\n                \"pages\": [\n                  {\n                    \"group\": \"Authentication\",\n                    \"icon\": \"key\",\n                    \"pages\": [\n                      \"v2/integrations/auth0\",\n                      \"v2/integrations/authkit\",\n                      \"v2/integrations/aws-cognito\",\n                      \"v2/integrations/azure\",\n                      \"v2/integrations/descope\",\n                      \"v2/integrations/discord\",\n                      \"v2/integrations/github\",\n                      \"v2/integrations/google\",\n                      \"v2/integrations/oci\",\n                      \"v2/integrations/scalekit\",\n                      \"v2/integrations/supabase\",\n                      \"v2/integrations/workos\"\n                    ]\n                  },\n                  {\n                    \"group\": \"Authorization\",\n                    \"icon\": \"shield-check\",\n                    \"pages\": [\n                      \"v2/integrations/eunomia-authorization\",\n                      \"v2/integrations/permit\"\n                    ]\n                  },\n                  {\n                    \"group\": \"AI Assistants\",\n                    \"icon\": \"robot\",\n                    \"pages\": [\n                      \"v2/integrations/chatgpt\",\n                      \"v2/integrations/claude-code\",\n                      \"v2/integrations/claude-desktop\",\n                      \"v2/integrations/cursor\",\n                      \"v2/integrations/gemini-cli\",\n                      \"v2/integrations/mcp-json-configuration\"\n                    ]\n                  },\n                  {\n                    \"group\": \"AI SDKs\",\n                    \"icon\": \"code\",\n                    \"pages\": [\n                      \"v2/integrations/anthropic\",\n                      \"v2/integrations/gemini\",\n                      \"v2/integrations/openai\"\n                    ]\n                  },\n                  {\n                    \"group\": \"API Integration\",\n                    \"icon\": \"globe\",\n                    \"pages\": [\n                      \"v2/integrations/fastapi\",\n                      \"v2/integrations/openapi\"\n                    ]\n                  }\n                ]\n              },\n              {\n                \"group\": \"Patterns\",\n                \"pages\": [\n                  \"v2/patterns/tool-transformation\",\n                  \"v2/patterns/decorating-methods\",\n                  \"v2/patterns/cli\",\n                  \"v2/patterns/contrib\",\n                  \"v2/patterns/testing\"\n                ]\n              },\n              {\n                \"group\": \"Development\",\n                \"pages\": [\n                  \"v2/development/contributing\",\n                  \"v2/development/tests\",\n                  \"v2/development/releases\",\n                  \"v2/development/upgrade-guide\",\n                  \"v2/changelog\"\n                ]\n              }\n            ],\n            \"icon\": \"book\"\n          }\n        ],\n        \"version\": \"v2.14.5\"\n      }\n    ]\n  },\n  \"redirects\": [\n    {\n      \"destination\": \"/cli/overview\",\n      \"source\": \"/patterns/cli\"\n    },\n    {\n      \"destination\": \"/servers/testing\",\n      \"source\": \"/patterns/testing\"\n    },\n    {\n      \"destination\": \"/cli/client\",\n      \"source\": \"/clients/cli\"\n    },\n    {\n      \"destination\": \"/cli/generate-cli\",\n      \"source\": \"/clients/generate-cli\"\n    },\n    {\n      \"destination\": \"/deployment/prefect-horizon\",\n      \"source\": \"/deployment/fastmcp-cloud\"\n    },\n    {\n      \"destination\": \"/deployment/prefect-horizon\",\n      \"source\": \"/v2/deployment/fastmcp-cloud\"\n    },\n    {\n      \"destination\": \"/v2/clients/messages\",\n      \"source\": \"/clients/messages\"\n    },\n    {\n      \"destination\": \"/v2/patterns/decorating-methods\",\n      \"source\": \"/patterns/decorating-methods\"\n    },\n    {\n      \"destination\": \"/servers/providers/proxy\",\n      \"source\": \"/patterns/proxy\"\n    },\n    {\n      \"destination\": \"/servers/composition\",\n      \"source\": \"/patterns/composition\"\n    },\n    {\n      \"destination\": \"/servers/providers/proxy\",\n      \"source\": \"/servers/proxy\"\n    },\n    {\n      \"destination\": \"/servers/composition\",\n      \"source\": \"/servers/providers/mounting\"\n    },\n    {\n      \"destination\": \"/servers/transforms/transforms\",\n      \"source\": \"/servers/providers/namespacing\"\n    },\n    {\n      \"destination\": \"/servers/transforms/transforms\",\n      \"source\": \"/patterns/tool-transformation\"\n    },\n    {\n      \"destination\": \"/getting-started/upgrading/from-fastmcp-2\",\n      \"source\": \"/development/upgrade-guide\"\n    },\n    {\n      \"destination\": \"/getting-started/upgrading/from-mcp-sdk\",\n      \"source\": \"/getting-started/upgrading-from-sdk\"\n    },\n    {\n      \"destination\": \"/getting-started/upgrading/from-low-level-sdk\",\n      \"source\": \"/getting-started/low-level-sdk\"\n    }\n  ],\n  \"search\": {\n    \"prompt\": \"Search the docs...\"\n  },\n  \"styling\": {\n    \"codeblocks\": {\n      \"theme\": {\n        \"dark\": \"dark-plus\",\n        \"light\": \"snazzy-light\"\n      }\n    }\n  },\n  \"theme\": \"almond\",\n  \"thumbnails\": {\n    \"appearance\": \"light\",\n    \"background\": \"/assets/brand/thumbnail-background-4.jpeg\"\n  }\n}"
  },
  {
    "path": "docs/getting-started/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Install FastMCP and verify your setup\nicon: arrow-down-to-line\n---\n## Install FastMCP\n\nWe recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.\n\n```bash\npip install fastmcp\n```\n\nOr with uv:\n\n```bash\nuv add fastmcp\n```\n\n### Optional Dependencies\n\nFastMCP provides optional extras for specific features. For example, to install the background tasks extra:\n\n```bash\npip install \"fastmcp[tasks]\"\n```\n\nSee [Background Tasks](/servers/tasks) for details on the task system.\n\n### Verify Installation\n\nTo verify that FastMCP is installed correctly, you can run the following command:\n\n```bash\nfastmcp version\n```\n\nYou should see output like the following:\n\n```bash\n$ fastmcp version\n\nFastMCP version:                           3.0.0\nMCP version:                               1.25.0\nPython version:                            3.12.2\nPlatform:            macOS-15.3.1-arm64-arm-64bit\nFastMCP root path:            ~/Developer/fastmcp\n```\n\n### Dependency Licensing\n\n<Info>\nFastMCP depends on Cyclopts for CLI functionality. Cyclopts v4 includes docutils as a transitive dependency, which has complex licensing that may trigger compliance reviews in some organizations.\n\nIf this is a concern, you can install Cyclopts v5 alpha which removes this dependency:\n\n```bash\npip install \"cyclopts>=5.0.0a1\"\n```\n\nAlternatively, wait for the stable v5 release. See [this issue](https://github.com/BrianPugh/cyclopts/issues/672) for details.\n</Info>\n## Upgrading\n\n### From FastMCP 2.0\n\nSee the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complete list of breaking changes and migration steps.\n\n### From the MCP SDK\n\n#### From FastMCP 1.0\n\nIf you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as  `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details.\n\n#### From the Low-Level Server API\n\nIf you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough.\n\n## Versioning Policy\n\nFastMCP follows semantic versioning with pragmatic adaptations for the rapidly evolving MCP ecosystem. Breaking changes may occur in minor versions (e.g., 2.3.x to 2.4.0) when necessary to stay current with the MCP Protocol.\n\nFor production use, always pin to exact versions:\n```\nfastmcp==3.0.0  # Good\nfastmcp>=3.0.0  # Bad - may install breaking changes\n```\n\nSee the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.\n\n## Contributing to FastMCP\n\nInterested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on:\n- Setting up your development environment\n- Running tests and pre-commit hooks\n- Submitting issues and pull requests\n- Code standards and review process\n"
  },
  {
    "path": "docs/getting-started/quickstart.mdx",
    "content": "---\ntitle: Quickstart\nicon: rocket-launch\n---\n\nWelcome! This guide will help you quickly set up FastMCP, run your first MCP server, and deploy a server to Prefect Horizon.\n\nIf you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation).\n\n## Create a FastMCP Server\n\nA FastMCP server is a collection of tools, resources, and other MCP components. To create a server, start by instantiating the `FastMCP` class. \n\nCreate a new file called `my_server.py` and add the following code:\n\n```python my_server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n```\n\n\nThat's it! You've created a FastMCP server, albeit a very boring one. Let's add a tool to make it more interesting.\n\n\n## Add a Tool\n\nTo add a tool that returns a simple greeting, write a function and decorate it with `@mcp.tool` to register it with the server:\n\n```python my_server.py {5-7}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n```\n\n\n## Run the Server\n\nThe simplest way to run your FastMCP server is to call its `run()` method. You can choose between different transports, like `stdio` for local servers, or `http` for remote access:\n\n<CodeGroup>\n\n```python my_server.py (stdio) {9, 10}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n```python my_server.py (HTTP) {9, 10}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n</CodeGroup>\n\nThis lets us run the server with `python my_server.py`. The stdio transport is the traditional way to connect MCP servers to clients, while the HTTP transport enables remote connections.\n\n<Tip>\nWhy do we need the `if __name__ == \"__main__\":` block?\n\nThe `__main__` block is recommended for consistency and compatibility, ensuring your server works with all MCP clients that execute your server file as a script. Users who will exclusively run their server with the FastMCP CLI can omit it, as the CLI imports the server object directly.\n</Tip>\n\n### Using the FastMCP CLI\n\nYou can also use the `fastmcp run` command to start your server. Note that the FastMCP CLI **does not** execute the `__main__` block of your server file. Instead, it imports your server object and runs it with whatever transport and options you provide.\n\nFor example, to run this server with the default stdio transport (no matter how you called `mcp.run()`), you can use the following command:\n```bash\nfastmcp run my_server.py:mcp\n```\n\nTo run this server with the HTTP transport, you can use the following command:\n```bash\nfastmcp run my_server.py:mcp --transport http --port 8000\n```\n\n## Call Your Server\n\nOnce your server is running with HTTP transport, you can connect to it with a FastMCP client or any LLM client that supports the MCP protocol:\n\n```python my_client.py\nimport asyncio\nfrom fastmcp import Client\n\nclient = Client(\"http://localhost:8000/mcp\")\n\nasync def call_tool(name: str):\n    async with client:\n        result = await client.call_tool(\"greet\", {\"name\": name})\n        print(result)\n\nasyncio.run(call_tool(\"Ford\"))\n```\n\nNote that:\n- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client\n- We must enter a client context (`async with client:`) before using the client\n- You can make multiple client calls within the same context\n\n## Deploy to Prefect Horizon\n\n[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.\n\n<Info>\nHorizon is **free for personal projects** and offers enterprise governance for teams.\n</Info>\n\nTo deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:\n\n1. Push your `my_server.py` file to a GitHub repository\n2. Sign in to [Prefect Horizon](https://horizon.prefect.io) with your GitHub account\n3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint\n\nThat'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.\n\nFor more details, see the [Prefect Horizon guide](/deployment/prefect-horizon).\n"
  },
  {
    "path": "docs/getting-started/upgrading/from-fastmcp-2.mdx",
    "content": "---\ntitle: Upgrading from FastMCP 2\nsidebarTitle: \"From FastMCP 2\"\ndescription: Migration instructions for upgrading between FastMCP versions\nicon: up\n---\n\nThis guide covers breaking changes and migration steps when upgrading FastMCP.\n\n## v3.0.0\n\nFor most servers, upgrading to v3 is straightforward. The breaking changes below affect deprecated constructor kwargs, sync-to-async shifts, a few renamed methods, and some less commonly used features.\n\n### Install\n\nSince you already have `fastmcp` installed, you need to explicitly request the new version — `pip install fastmcp` won't upgrade an existing installation:\n\n```bash\npip install --upgrade fastmcp\n# or\nuv add --upgrade fastmcp\n```\n\nIf you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`.\n\n<Info>\n**New repository home.** As part of the v3 release, FastMCP's GitHub repository has moved from `jlowin/fastmcp` to [`PrefectHQ/fastmcp`](https://github.com/PrefectHQ/fastmcp) under [Prefect](https://prefect.io)'s stewardship. GitHub automatically redirects existing clones and bookmarks, so nothing breaks — but you can update your local remote whenever convenient:\n\n```bash\ngit remote set-url origin https://github.com/PrefectHQ/fastmcp.git\n```\n\nIf you reference the repository URL in dependency specifications (e.g., `git+https://github.com/jlowin/fastmcp.git`), update those to the new location.\n</Info>\n\n<Prompt description=\"Copy this prompt into any LLM along with your server code to get automated upgrade guidance.\">\nYou are upgrading a FastMCP v2 server to FastMCP v3.0. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2 and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.\n\nBREAKING CHANGES (will crash at import or runtime):\n\n1. CONSTRUCTOR KWARGS REMOVED: FastMCP() no longer accepts these kwargs (raises TypeError):\n   - Transport settings: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http\n     Fix: pass to run() or run_http_async() instead, e.g. mcp.run(transport=\"http\", host=\"0.0.0.0\", port=8080)\n   - message_path: set via environment variable FASTMCP_MESSAGE_PATH only (not a run() kwarg)\n   - Duplicate handling: on_duplicate_tools, on_duplicate_resources, on_duplicate_prompts\n     Fix: use unified on_duplicate= parameter\n   - Tool settings: tool_serializer, include_tags, exclude_tags, tool_transformations\n     Fix: use ToolResult returns, server.enable()/disable(), server.add_transform()\n\n2. COMPONENT METHODS REMOVED:\n   - tool.enable()/disable() raises NotImplementedError\n     Fix: server.disable(names={\"tool_name\"}, components={\"tool\"}) or server.disable(tags={\"tag\"})\n   - get_tools()/get_resources()/get_prompts()/get_resource_templates() removed\n     Fix: use list_tools()/list_resources()/list_prompts()/list_resource_templates() — these return lists, not dicts\n\n3. ASYNC STATE: ctx.set_state() and ctx.get_state() are now async (must be awaited).\n   State values must be JSON-serializable unless serializable=False is passed.\n   Each FastMCP instance has its own state store, so serializable state set by parent middleware isn't visible to mounted tools by default.\n   Fix: pass the same session_state_store to both servers, or use serializable=False (request-scoped state is always shared).\n\n4. PROMPTS: mcp.types.PromptMessage replaced by fastmcp.prompts.Message.\n   Before: PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))\n   After:  Message(\"Hello\")  # role defaults to \"user\", accepts plain strings\n   Also: if prompts return raw dicts like `{\"role\": \"user\", \"content\": \"...\"}`, these must become Message objects.\n   v2 silently coerced dicts; v3 requires typed Message objects or plain strings.\n\n5. AUTH PROVIDERS: No longer auto-load from env vars. Pass client_id, client_secret explicitly via os.environ.\n\n6. WSTRANSPORT: Removed. Use StreamableHttpTransport.\n\n7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx.AsyncClient instead.\n\n8. METADATA: Namespace changed from \"_fastmcp\" to \"fastmcp\" in tool.meta. The include_fastmcp_meta parameter is removed (always included).\n\n9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.\n\n10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.\n   Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).\n\n11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore or add pip install 'py-key-value-aio[disk]'.\n\n12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location.\n\n13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install \"fastmcp[tasks]\".\n\nDEPRECATIONS (still work but emit warnings):\n\n- mount(prefix=\"x\") -> mount(namespace=\"x\")\n- import_server(sub) -> mount(sub)\n- FastMCP.as_proxy(url) -> from fastmcp.server import create_proxy; create_proxy(url)\n- from fastmcp.server.proxy -> from fastmcp.server.providers.proxy\n- from fastmcp.server.openapi import FastMCPOpenAPI -> from fastmcp.server.providers.openapi import OpenAPIProvider; use FastMCP(\"name\", providers=[OpenAPIProvider(...)])\n- mcp.add_tool_transformation(name, cfg) -> from fastmcp.server.transforms import ToolTransform; mcp.add_transform(ToolTransform(...))\n\nFor each issue found, show the original line, explain why it breaks, and provide the corrected code.\n</Prompt>\n\n### Breaking Changes\n\n**Transport and server settings removed from constructor**\n\nIn v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint.\n\n```python\n# Before\nmcp = FastMCP(\"server\", host=\"0.0.0.0\", port=8080)\nmcp.run()\n\n# After\nmcp = FastMCP(\"server\")\nmcp.run(transport=\"http\", host=\"0.0.0.0\", port=8080)\n```\n\nThe full list of removed kwargs and their replacements:\n\n- `host`, `port`, `log_level`, `debug`, `sse_path`, `streamable_http_path`, `json_response`, `stateless_http` — pass to `run()`, `run_http_async()`, or `http_app()`, or set via environment variables (e.g. `FASTMCP_HOST`)\n- `message_path` — set via environment variable `FASTMCP_MESSAGE_PATH` only (not a `run()` kwarg)\n- `on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts` — consolidated into a single `on_duplicate=` parameter\n- `tool_serializer` — return [`ToolResult`](/servers/tools#custom-serialization) from your tools instead\n- `include_tags` / `exclude_tags` — use `server.enable(tags=..., only=True)` / `server.disable(tags=...)` after construction\n- `tool_transformations` — use `server.add_transform(ToolTransform(...))` after construction\n\n**OAuth storage backend changed (diskcache CVE)**\n\nThe default OAuth client storage has moved from `DiskStore` to `FileTreeStore` to address a pickle deserialization vulnerability in diskcache ([CVE-2025-69872](https://github.com/PrefectHQ/fastmcp/issues/3166)).\n\nIf you were using the default storage (i.e., not passing an explicit `client_storage`), clients will need to re-register on their first connection after upgrading. This happens automatically — no user action required, and it's the same flow that already occurs whenever a server restarts with in-memory storage.\n\nIf you were passing a `DiskStore` explicitly, you can either [switch to `FileTreeStore`](/servers/storage-backends) (recommended) or keep using `DiskStore` by adding the dependency yourself:\n\n<Warning>\nKeeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-introduces the vulnerable `diskcache` package into your dependency tree.\n</Warning>\n\n**Component enable()/disable() moved to server**\n\nIn v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object:\n\n```python\n# Before\ntool = await server.get_tool(\"my_tool\")\ntool.disable()\n\n# After\nserver.disable(names={\"my_tool\"}, components={\"tool\"})\n```\n\nCalling `.enable()` or `.disable()` on a component object now raises `NotImplementedError`. See [Visibility](/servers/visibility) for the full API, including tag-based filtering and per-session visibility.\n\n**Listing methods renamed and return lists**\n\nThe `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change:\n\n```python\n# Before\ntools = await server.get_tools()\ntool = tools[\"my_tool\"]\n\n# After\ntools = await server.list_tools()\ntool = next((t for t in tools if t.name == \"my_tool\"), None)\n```\n\n**Prompts use Message class**\n\nPrompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role=\"user\"`, so most prompts become one-liners:\n\n```python\n# Before\nfrom mcp.types import PromptMessage, TextContent\n\n@mcp.prompt\ndef my_prompt() -> PromptMessage:\n    return PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))\n\n# After\nfrom fastmcp.prompts import Message\n\n@mcp.prompt\ndef my_prompt() -> Message:\n    return Message(\"Hello\")\n```\n\nIf your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages):\n\n```python\n# Before (v2 accepted this)\n@mcp.prompt\ndef my_prompt():\n    return [\n        {\"role\": \"user\", \"content\": \"Hello\"},\n        {\"role\": \"assistant\", \"content\": \"How can I help?\"},\n    ]\n\n# After\nfrom fastmcp.prompts import Message\n\n@mcp.prompt\ndef my_prompt() -> list[Message]:\n    return [\n        Message(\"Hello\"),\n        Message(\"How can I help?\", role=\"assistant\"),\n    ]\n```\n\n**Context state methods are async**\n\n`ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session:\n\n```python\n# Before\nctx.set_state(\"key\", \"value\")\nvalue = ctx.get_state(\"key\")\n\n# After\nawait ctx.set_state(\"key\", \"value\")\nvalue = await ctx.get_state(\"key\")\n```\n\nState values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call:\n\n```python\nawait ctx.set_state(\"client\", my_http_client, serializable=False)\n```\n\n**Mounted servers have isolated state stores**\n\nEach `FastMCP` instance has its own state store. In v2 this wasn't noticeable because mounted tools ran in the parent's context, but in v3's provider architecture each server is isolated. Non-serializable state (`serializable=False`) is request-scoped and automatically shared across mount boundaries. For serializable state, pass the same `session_state_store` to both servers:\n\n```python\nfrom fastmcp import FastMCP\nfrom key_value.aio.stores.memory import MemoryStore\n\nstore = MemoryStore()\nparent = FastMCP(\"Parent\", session_state_store=store)\nchild = FastMCP(\"Child\", session_state_store=store)\nparent.mount(child, namespace=\"child\")\n```\n\n**Auth provider environment variables removed**\n\nIn v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly:\n\n```python\n# Before (v2) — client_id and client_secret loaded automatically\n# from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.\nauth = GitHubProvider()\n\n# After (v3) — pass values explicitly\nimport os\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n)\n```\n\n**WSTransport removed**\n\nThe deprecated WebSocket client transport has been removed. Use `StreamableHttpTransport` instead:\n\n```python\n# Before\nfrom fastmcp.client.transports import WSTransport\ntransport = WSTransport(\"ws://localhost:8000/ws\")\n\n# After\nfrom fastmcp.client.transports import StreamableHttpTransport\ntransport = StreamableHttpTransport(\"http://localhost:8000/mcp\")\n```\n\n**OpenAPI `timeout` parameter removed**\n\n`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:\n\n```python\n# Before\nprovider = OpenAPIProvider(spec, client, timeout=60)\n\n# After\nclient = httpx.AsyncClient(base_url=\"https://api.example.com\", timeout=60)\nprovider = OpenAPIProvider(spec, client)\n```\n\n**Metadata namespace renamed**\n\nThe FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key:\n\n```python\n# Before\ntags = tool.meta.get(\"_fastmcp\", {}).get(\"tags\", [])\n\n# After\ntags = tool.meta.get(\"fastmcp\", {}).get(\"tags\", [])\n```\n\nMetadata is now always included — the `include_fastmcp_meta` parameter has been removed from `FastMCP()` and `to_mcp_tool()`, so there is no way to suppress it.\n\n**Server banner environment variable renamed**\n\n`FASTMCP_SHOW_CLI_BANNER` is now `FASTMCP_SHOW_SERVER_BANNER`.\n\n**Decorators return functions**\n\nIn v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition:\n\n```python\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\ngreet(\"World\")  # Works! Returns \"Hello, World!\"\n```\n\nIf you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.\n\n**Background tasks require optional dependency**\n\nFastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with:\n\n```bash\npip install \"fastmcp[tasks]\"\n```\n\nWithout the extra, configuring a tool with `task=True` or `TaskConfig` will raise an import error at runtime. See [Background Tasks](/servers/tasks) for details.\n\n### Deprecated Features\n\nThese still work but emit warnings. Update when convenient.\n\n**mount() prefix → namespace**\n\n```python\n# Deprecated\nmain.mount(subserver, prefix=\"api\")\n\n# New\nmain.mount(subserver, namespace=\"api\")\n```\n\n**import_server() → mount()**\n\n```python\n# Deprecated\nmain.import_server(subserver)\n\n# New\nmain.mount(subserver)\n```\n\n**Module import paths for proxy and OpenAPI**\n\nThe proxy and OpenAPI modules have moved under `providers` to reflect v3's provider-based architecture:\n\n```python\n# Deprecated\nfrom fastmcp.server.proxy import FastMCPProxy\nfrom fastmcp.server.openapi import FastMCPOpenAPI\n\n# New\nfrom fastmcp.server.providers.proxy import FastMCPProxy\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\n```\n\n`FastMCPOpenAPI` itself is deprecated — use `FastMCP` with an `OpenAPIProvider` instead:\n\n```python\n# Deprecated\nfrom fastmcp.server.openapi import FastMCPOpenAPI\nserver = FastMCPOpenAPI(spec, client)\n\n# New\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\nserver = FastMCP(\"my_api\", providers=[OpenAPIProvider(spec, client)])\n```\n\n**add_tool_transformation() → add_transform()**\n\n```python\n# Deprecated\nmcp.add_tool_transformation(\"name\", config)\n\n# New\nfrom fastmcp.server.transforms import ToolTransform\nmcp.add_transform(ToolTransform({\"name\": config}))\n```\n\n**FastMCP.as_proxy() → create_proxy()**\n\n```python\n# Deprecated\nproxy = FastMCP.as_proxy(\"http://example.com/mcp\")\n\n# New\nfrom fastmcp.server import create_proxy\nproxy = create_proxy(\"http://example.com/mcp\")\n```\n\n## v2.14.0\n\n### OpenAPI Parser Promotion\n\nThe experimental OpenAPI parser is now standard. Update imports:\n\n```python\n# Before\nfrom fastmcp.experimental.server.openapi import FastMCPOpenAPI\n\n# After\nfrom fastmcp.server.openapi import FastMCPOpenAPI\n```\n\n### Removed Deprecated Features\n\n- `BearerAuthProvider` → use `JWTVerifier`\n- `Context.get_http_request()` → use `get_http_request()` from dependencies\n- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image`\n- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration\n- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...`\n- `output_schema=False` → use `output_schema=None`\n\n## v2.13.0\n\n### OAuth Token Key Management\n\nThe OAuth proxy now issues its own JWT tokens. For production, provide explicit keys:\n\n```python\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=\"https://your-server.com\",\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=RedisStore(host=\"redis.example.com\"),\n)\n```\n\nSee [OAuth Token Security](/deployment/http#oauth-token-security) for details.\n"
  },
  {
    "path": "docs/getting-started/upgrading/from-low-level-sdk.mdx",
    "content": "---\ntitle: Upgrading from the MCP Low-Level SDK\nsidebarTitle: \"From MCP Low-Level SDK\"\ndescription: Upgrade your MCP server from the low-level Python SDK's Server class to FastMCP\nicon: up\n---\n\nIf you've been building MCP servers directly on the `mcp` package's `Server` class — writing `list_tools()` and `call_tool()` handlers, hand-crafting JSON Schema dicts, and wiring up transport boilerplate — this guide is for you. FastMCP replaces all of that machinery with a declarative, Pythonic API where your functions *are* the protocol surface.\n\nThe core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears.\n\n<Note>\nThis guide covers upgrading from **v1** of the `mcp` package. We'll provide a separate guide when v2 ships.\n</Note>\n\n<Note>\nAlready using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is simpler — see the [FastMCP 1.0 upgrade guide](/getting-started/upgrading/from-mcp-sdk) instead.\n</Note>\n\n<Prompt description=\"Copy this prompt into any LLM along with your server code to get automated upgrade guidance.\">\nYou are upgrading an MCP server from the `mcp` package's low-level Server class (v1) to FastMCP 3.0. The server currently uses `mcp.server.Server` (or `mcp.server.lowlevel.server.Server`) with manual handler registration. Analyze the provided code and rewrite it using FastMCP's high-level API. The full guide is at https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.\n\nUPGRADE RULES:\n\n1. IMPORTS: Replace all `mcp.*` imports with FastMCP equivalents.\n   - `from mcp.server import Server` or `from mcp.server.lowlevel.server import Server` → `from fastmcp import FastMCP`\n   - `import mcp.types as types` → remove (not needed for most code)\n   - `from mcp.server.stdio import stdio_server` → remove (handled by mcp.run())\n   - `from mcp.server.sse import SseServerTransport` → remove (handled by mcp.run())\n\n2. SERVER: Replace `Server(\"name\")` with `FastMCP(\"name\")`.\n\n3. TOOLS: Replace the list_tools + call_tool handler pair with individual @mcp.tool decorators.\n   - Delete the `@server.list_tools()` handler entirely\n   - Delete the `@server.call_tool()` handler entirely\n   - For each tool that was listed in list_tools and dispatched in call_tool, create a new function:\n     - Decorate it with `@mcp.tool`\n     - Use the tool name as the function name (or pass name= to the decorator)\n     - Use the docstring for the description (or pass description= to the decorator)\n     - Convert the inputSchema JSON Schema into typed Python parameters (e.g., `{\"type\": \"integer\"}` → `int`, `{\"type\": \"string\"}` → `str`, `{\"type\": \"array\", \"items\": {\"type\": \"string\"}}` → `list[str]`)\n     - Return plain Python values (`str`, `int`, `dict`, etc.) instead of `list[types.TextContent(...)]`\n     - If the tool returned `types.ImageContent` or `types.EmbeddedResource`, use `from fastmcp.utilities.types import Image` or return the appropriate type\n\n4. RESOURCES: Replace the list_resources + list_resource_templates + read_resource handler trio with individual @mcp.resource decorators.\n   - Delete all three handlers\n   - For each static resource, create a function decorated with `@mcp.resource(\"uri://...\")`\n   - For each resource template, use `@mcp.resource(\"uri://{param}/path\")` with `{param}` in the URI and a matching function parameter\n   - Return str for text content, bytes for binary content\n   - Set `mime_type=` in the decorator if needed\n\n5. PROMPTS: Replace the list_prompts + get_prompt handler pair with individual @mcp.prompt decorators.\n   - Delete both handlers\n   - For each prompt, create a function decorated with `@mcp.prompt`\n   - Convert PromptArgument definitions into typed function parameters\n   - Return str for simple single-message prompts (auto-wrapped as user message)\n   - Return `list[Message]` for multi-message prompts: `from fastmcp.prompts import Message`\n   - `Message(\"text\")` defaults to `role=\"user\"`; use `Message(\"text\", role=\"assistant\")` for assistant messages\n\n6. TRANSPORT: Replace all transport boilerplate with mcp.run().\n   - `async with stdio_server() as (r, w): await server.run(r, w, ...)` → `mcp.run()` (`stdio` is the default)\n   - SSE/Starlette setup → `mcp.run(transport=\"sse\", host=\"...\", port=...)`\n   - Streamable HTTP setup → `mcp.run(transport=\"http\", host=\"...\", port=...)`\n   - Delete asyncio.run(main()) boilerplate — use `if __name__ == \"__main__\": mcp.run()`\n\n7. CONTEXT: Replace `server.request_context` with FastMCP's Context parameter.\n   - Add `from fastmcp import Context` and add a `ctx: Context` parameter to any tool that needs it\n   - `server.request_context.session.send_log_message(...)` → `await ctx.info(\"message\")` or `await ctx.warning(\"message\")`\n   - Progress reporting → `await ctx.report_progress(current, total)`\n\nFor each change, show the original code, explain what it did, and provide the FastMCP equivalent.\n</Prompt>\n\n## Install\n\n```bash\npip install --upgrade fastmcp\n# or\nuv add fastmcp\n```\n\nFastMCP includes the `mcp` package as a transitive dependency, so you don't lose access to anything.\n\n## Server and Transport\n\nThe `Server` class requires you to choose a transport, connect streams, build initialization options, and run an event loop. FastMCP collapses all of that into a constructor and a `run()` call.\n\n<CodeGroup>\n\n```python Before\nimport asyncio\nfrom mcp.server import Server\nfrom mcp.server.stdio import stdio_server\n\nserver = Server(\"my-server\")\n\n# ... register handlers ...\n\nasync def main():\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\nasyncio.run(main())\n```\n\n```python After\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"my-server\")\n\n# ... register tools, resources, prompts ...\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n</CodeGroup>\n\nNeed HTTP instead of stdio? With the `Server` class, you'd wire up Starlette routes and `SseServerTransport` or `StreamableHTTPSessionManager`. With FastMCP:\n\n```python\nmcp.run(transport=\"http\", host=\"0.0.0.0\", port=8000)\n```\n\n## Tools\n\nThis is where the difference is most dramatic. The `Server` class requires two handlers — one to describe your tools (with hand-written JSON Schema) and another to dispatch calls by name. FastMCP eliminates both by deriving everything from your function signature.\n\n<CodeGroup>\n\n```python Before\nimport mcp.types as types\nfrom mcp.server import Server\n\nserver = Server(\"math\")\n\n@server.list_tools()\nasync def list_tools() -> list[types.Tool]:\n    return [\n        types.Tool(\n            name=\"add\",\n            description=\"Add two numbers\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"a\": {\"type\": \"number\"},\n                    \"b\": {\"type\": \"number\"},\n                },\n                \"required\": [\"a\", \"b\"],\n            },\n        ),\n        types.Tool(\n            name=\"multiply\",\n            description=\"Multiply two numbers\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"a\": {\"type\": \"number\"},\n                    \"b\": {\"type\": \"number\"},\n                },\n                \"required\": [\"a\", \"b\"],\n            },\n        ),\n    ]\n\n@server.call_tool()\nasync def call_tool(\n    name: str, arguments: dict\n) -> list[types.TextContent]:\n    if name == \"add\":\n        result = arguments[\"a\"] + arguments[\"b\"]\n        return [types.TextContent(type=\"text\", text=str(result))]\n    elif name == \"multiply\":\n        result = arguments[\"a\"] * arguments[\"b\"]\n        return [types.TextContent(type=\"text\", text=str(result))]\n    raise ValueError(f\"Unknown tool: {name}\")\n```\n\n```python After\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"math\")\n\n@mcp.tool\ndef add(a: float, b: float) -> float:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n@mcp.tool\ndef multiply(a: float, b: float) -> float:\n    \"\"\"Multiply two numbers\"\"\"\n    return a * b\n```\n\n</CodeGroup>\n\nEach `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its type annotations become the JSON Schema, and its return value is serialized automatically. No routing. No schema dictionaries. No content-type wrappers.\n\n### Type Mapping\n\nWhen converting your `inputSchema` to Python type hints:\n\n| JSON Schema | Python Type |\n|---|---|\n| `{\"type\": \"string\"}` | `str` |\n| `{\"type\": \"number\"}` | `float` |\n| `{\"type\": \"integer\"}` | `int` |\n| `{\"type\": \"boolean\"}` | `bool` |\n| `{\"type\": \"array\", \"items\": {\"type\": \"string\"}}` | `list[str]` |\n| `{\"type\": \"object\"}` | `dict` |\n| Optional property (not in `required`) | `param: str \\| None = None` |\n\n### Return Values\n\nWith the `Server` class, tools return `list[types.TextContent | types.ImageContent | ...]`. In FastMCP, return plain Python values — strings, numbers, dicts, lists, dataclasses, Pydantic models — and serialization is handled for you.\n\nFor images or other non-text content, FastMCP provides helpers:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.types import Image\n\nmcp = FastMCP(\"media\")\n\n@mcp.tool\ndef create_chart(data: list[float]) -> Image:\n    \"\"\"Generate a chart from data.\"\"\"\n    png_bytes = generate_chart(data)  # your logic\n    return Image(data=png_bytes, format=\"png\")\n```\n\n## Resources\n\nThe `Server` class uses three handlers for resources: `list_resources()` to enumerate them, `list_resource_templates()` for URI templates, and `read_resource()` to serve content — all with manual routing by URI. FastMCP replaces all three with per-resource decorators.\n\n<CodeGroup>\n\n```python Before\nimport json\nimport mcp.types as types\nfrom mcp.server import Server\nfrom pydantic import AnyUrl\n\nserver = Server(\"data\")\n\n@server.list_resources()\nasync def list_resources() -> list[types.Resource]:\n    return [\n        types.Resource(\n            uri=AnyUrl(\"config://app\"),\n            name=\"app_config\",\n            description=\"Application configuration\",\n            mimeType=\"application/json\",\n        ),\n        types.Resource(\n            uri=AnyUrl(\"config://features\"),\n            name=\"feature_flags\",\n            description=\"Active feature flags\",\n            mimeType=\"application/json\",\n        ),\n    ]\n\n@server.list_resource_templates()\nasync def list_resource_templates() -> list[types.ResourceTemplate]:\n    return [\n        types.ResourceTemplate(\n            uriTemplate=\"users://{user_id}/profile\",\n            name=\"user_profile\",\n            description=\"User profile by ID\",\n        ),\n        types.ResourceTemplate(\n            uriTemplate=\"projects://{project_id}/status\",\n            name=\"project_status\",\n            description=\"Project status by ID\",\n        ),\n    ]\n\n@server.read_resource()\nasync def read_resource(uri: AnyUrl) -> str:\n    uri_str = str(uri)\n    if uri_str == \"config://app\":\n        return json.dumps({\"debug\": False, \"version\": \"1.0\"})\n    if uri_str == \"config://features\":\n        return json.dumps({\"dark_mode\": True, \"beta\": False})\n    if uri_str.startswith(\"users://\"):\n        user_id = uri_str.split(\"/\")[2]\n        return json.dumps({\"id\": user_id, \"name\": f\"User {user_id}\"})\n    if uri_str.startswith(\"projects://\"):\n        project_id = uri_str.split(\"/\")[2]\n        return json.dumps({\"id\": project_id, \"status\": \"active\"})\n    raise ValueError(f\"Unknown resource: {uri}\")\n```\n\n```python After\nimport json\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"data\")\n\n@mcp.resource(\"config://app\", mime_type=\"application/json\")\ndef app_config() -> str:\n    \"\"\"Application configuration\"\"\"\n    return json.dumps({\"debug\": False, \"version\": \"1.0\"})\n\n@mcp.resource(\"config://features\", mime_type=\"application/json\")\ndef feature_flags() -> str:\n    \"\"\"Active feature flags\"\"\"\n    return json.dumps({\"dark_mode\": True, \"beta\": False})\n\n@mcp.resource(\"users://{user_id}/profile\")\ndef user_profile(user_id: str) -> str:\n    \"\"\"User profile by ID\"\"\"\n    return json.dumps({\"id\": user_id, \"name\": f\"User {user_id}\"})\n\n@mcp.resource(\"projects://{project_id}/status\")\ndef project_status(project_id: str) -> str:\n    \"\"\"Project status by ID\"\"\"\n    return json.dumps({\"id\": project_id, \"status\": \"active\"})\n```\n\n</CodeGroup>\n\nStatic resources and URI templates use the same `@mcp.resource` decorator — FastMCP detects `{placeholders}` in the URI and automatically registers a template. The function parameter `user_id` maps directly to the `{user_id}` placeholder.\n\n## Prompts\n\nSame pattern: the `Server` class uses `list_prompts()` and `get_prompt()` with manual routing. FastMCP uses one decorator per prompt.\n\n<CodeGroup>\n\n```python Before\nimport mcp.types as types\nfrom mcp.server import Server\n\nserver = Server(\"prompts\")\n\n@server.list_prompts()\nasync def list_prompts() -> list[types.Prompt]:\n    return [\n        types.Prompt(\n            name=\"review_code\",\n            description=\"Review code for issues\",\n            arguments=[\n                types.PromptArgument(\n                    name=\"code\",\n                    description=\"The code to review\",\n                    required=True,\n                ),\n                types.PromptArgument(\n                    name=\"language\",\n                    description=\"Programming language\",\n                    required=False,\n                ),\n            ],\n        )\n    ]\n\n@server.get_prompt()\nasync def get_prompt(\n    name: str, arguments: dict[str, str] | None\n) -> types.GetPromptResult:\n    if name == \"review_code\":\n        code = (arguments or {}).get(\"code\", \"\")\n        language = (arguments or {}).get(\"language\", \"\")\n        lang_note = f\" (written in {language})\" if language else \"\"\n        return types.GetPromptResult(\n            description=\"Code review prompt\",\n            messages=[\n                types.PromptMessage(\n                    role=\"user\",\n                    content=types.TextContent(\n                        type=\"text\",\n                        text=f\"Please review this code{lang_note}:\\n\\n{code}\",\n                    ),\n                )\n            ],\n        )\n    raise ValueError(f\"Unknown prompt: {name}\")\n```\n\n```python After\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"prompts\")\n\n@mcp.prompt\ndef review_code(code: str, language: str | None = None) -> str:\n    \"\"\"Review code for issues\"\"\"\n    lang_note = f\" (written in {language})\" if language else \"\"\n    return f\"Please review this code{lang_note}:\\n\\n{code}\"\n```\n\n</CodeGroup>\n\nReturning a `str` from a prompt function automatically wraps it as a user message. For multi-turn prompts, return a `list[Message]`:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.prompts import Message\n\nmcp = FastMCP(\"prompts\")\n\n@mcp.prompt\ndef debug_session(error: str) -> list[Message]:\n    \"\"\"Start a debugging conversation\"\"\"\n    return [\n        Message(f\"I'm seeing this error:\\n\\n{error}\"),\n        Message(\"I'll help you debug that. Can you share the relevant code?\", role=\"assistant\"),\n    ]\n```\n\n## Request Context\n\nThe `Server` class exposes request context through `server.request_context`, which gives you the raw `ServerSession` for sending notifications. FastMCP replaces this with a typed `Context` object injected into any function that declares it.\n\n<CodeGroup>\n\n```python Before\nimport mcp.types as types\nfrom mcp.server import Server\n\nserver = Server(\"worker\")\n\n@server.call_tool()\nasync def call_tool(name: str, arguments: dict):\n    if name == \"process_data\":\n        ctx = server.request_context\n        await ctx.session.send_log_message(\n            level=\"info\", data=\"Starting processing...\"\n        )\n        # ... do work ...\n        await ctx.session.send_log_message(\n            level=\"info\", data=\"Done!\"\n        )\n        return [types.TextContent(type=\"text\", text=\"Processed\")]\n```\n\n```python After\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"worker\")\n\n@mcp.tool\nasync def process_data(ctx: Context) -> str:\n    \"\"\"Process data with progress logging\"\"\"\n    await ctx.info(\"Starting processing...\")\n    # ... do work ...\n    await ctx.info(\"Done!\")\n    return \"Processed\"\n```\n\n</CodeGroup>\n\nThe `Context` object provides logging (`ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`), progress reporting (`ctx.report_progress()`), resource subscriptions, session state, and more. See [Context](/servers/context) for the full API.\n\n## Complete Example\n\nA full server upgrade, showing how all the pieces fit together:\n\n<CodeGroup>\n\n```python Before expandable\nimport asyncio\nimport json\nimport mcp.types as types\nfrom mcp.server import Server\nfrom mcp.server.stdio import stdio_server\nfrom pydantic import AnyUrl\n\nserver = Server(\"demo\")\n\n@server.list_tools()\nasync def list_tools() -> list[types.Tool]:\n    return [\n        types.Tool(\n            name=\"greet\",\n            description=\"Greet someone by name\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                },\n                \"required\": [\"name\"],\n            },\n        )\n    ]\n\n@server.call_tool()\nasync def call_tool(name: str, arguments: dict) -> list[types.TextContent]:\n    if name == \"greet\":\n        return [types.TextContent(type=\"text\", text=f\"Hello, {arguments['name']}!\")]\n    raise ValueError(f\"Unknown tool: {name}\")\n\n@server.list_resources()\nasync def list_resources() -> list[types.Resource]:\n    return [\n        types.Resource(\n            uri=AnyUrl(\"info://version\"),\n            name=\"version\",\n            description=\"Server version\",\n        )\n    ]\n\n@server.read_resource()\nasync def read_resource(uri: AnyUrl) -> str:\n    if str(uri) == \"info://version\":\n        return json.dumps({\"version\": \"1.0.0\"})\n    raise ValueError(f\"Unknown resource: {uri}\")\n\n@server.list_prompts()\nasync def list_prompts() -> list[types.Prompt]:\n    return [\n        types.Prompt(\n            name=\"summarize\",\n            description=\"Summarize text\",\n            arguments=[\n                types.PromptArgument(name=\"text\", required=True)\n            ],\n        )\n    ]\n\n@server.get_prompt()\nasync def get_prompt(\n    name: str, arguments: dict[str, str] | None\n) -> types.GetPromptResult:\n    if name == \"summarize\":\n        return types.GetPromptResult(\n            description=\"Summarize text\",\n            messages=[\n                types.PromptMessage(\n                    role=\"user\",\n                    content=types.TextContent(\n                        type=\"text\",\n                        text=f\"Summarize:\\n\\n{(arguments or {}).get('text', '')}\",\n                    ),\n                )\n            ],\n        )\n    raise ValueError(f\"Unknown prompt: {name}\")\n\nasync def main():\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream, write_stream,\n            server.create_initialization_options(),\n        )\n\nasyncio.run(main())\n```\n\n```python After\nimport json\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"demo\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet someone by name\"\"\"\n    return f\"Hello, {name}!\"\n\n@mcp.resource(\"info://version\")\ndef version() -> str:\n    \"\"\"Server version\"\"\"\n    return json.dumps({\"version\": \"1.0.0\"})\n\n@mcp.prompt\ndef summarize(text: str) -> str:\n    \"\"\"Summarize text\"\"\"\n    return f\"Summarize:\\n\\n{text}\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n</CodeGroup>\n\n## What's Next\n\nOnce you've upgraded, you have access to everything FastMCP provides beyond the basics:\n\n- **[Server composition](/servers/composition)** — Mount sub-servers to build modular applications\n- **[Middleware](/servers/middleware)** — Add logging, rate limiting, error handling, and caching\n- **[Proxy servers](/servers/providers/proxy)** — Create a proxy to any existing MCP server\n- **[OpenAPI integration](/integrations/openapi)** — Generate an MCP server from an OpenAPI spec\n- **[Authentication](/servers/auth/authentication)** — Built-in OAuth and token verification\n- **[Testing](/servers/testing)** — Test your server directly in Python without running a subprocess\n\nExplore the full documentation at [gofastmcp.com](https://gofastmcp.com).\n"
  },
  {
    "path": "docs/getting-started/upgrading/from-mcp-sdk.mdx",
    "content": "---\ntitle: Upgrading from the MCP SDK\nsidebarTitle: \"From MCP SDK\"\ndescription: Upgrade from FastMCP in the MCP Python SDK to the standalone FastMCP framework\nicon: up\n---\n\nIf your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.**\n\n```python\n# Before\nfrom mcp.server.fastmcp import FastMCP\n\n# After\nfrom fastmcp import FastMCP\n```\n\nThat's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is.\n\n<Tip>\n**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes.\n</Tip>\n\n## Install\n\n```bash\npip install --upgrade fastmcp\n# or\nuv add fastmcp\n```\n\nFastMCP includes the `mcp` package as a dependency, so you don't lose access to anything. Update your import, run your server, and if your tools work, you're done.\n\n<Prompt description=\"Copy this prompt into any LLM along with your server code to get automated upgrade guidance.\">\nYou are upgrading an MCP server from FastMCP 1.0 (bundled in the `mcp` package v1) to standalone FastMCP 3.0. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.\n\nSTEP 1 — IMPORT (required for all servers):\nChange \"from mcp.server.fastmcp import FastMCP\" to \"from fastmcp import FastMCP\".\n\nSTEP 2 — CONSTRUCTOR KWARGS (only if FastMCP() receives transport settings):\nFastMCP() no longer accepts: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http.\nFix: pass these to run() instead.\nBefore: `mcp = FastMCP(\"server\", host=\"0.0.0.0\", port=8080); mcp.run()`\nAfter:  `mcp = FastMCP(\"server\"); mcp.run(transport=\"http\", host=\"0.0.0.0\", port=8080)`\n\nSTEP 3 — PROMPTS (only if using PromptMessage directly or returning dicts):\nmcp.types.PromptMessage is replaced by fastmcp.prompts.Message.\nBefore: `PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))`\nAfter:  `Message(\"Hello\")`  — role defaults to \"user\", accepts plain strings.\nAlso: if prompts return raw dicts like `{\"role\": \"user\", \"content\": \"...\"}`, these must become Message objects or plain strings.\nThe MCP SDK's FastMCP 1.0 silently coerced dicts; standalone FastMCP requires typed returns.\n\nSTEP 4 — OTHER MCP IMPORTS (only if importing from mcp.* directly):\nDirect imports from the `mcp` package (e.g., `import mcp.types`, `from mcp.server.stdio import stdio_server`) still work because FastMCP includes `mcp` as a dependency. However, prefer FastMCP's own APIs where equivalents exist:\n- mcp.types.TextContent for tool returns → just return plain Python values (str, int, dict, etc.)\n- mcp.types.ImageContent → fastmcp.utilities.types.Image\n- from mcp.server.stdio import stdio_server → not needed, mcp.run() handles transport\n\nSTEP 5 — DECORATORS (only if treating decorated functions as objects):\n@mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name or .description on the decorated result needs updating. Set FASTMCP_DECORATOR_MODE=object temporarily to restore v1 behavior (this compat setting is itself deprecated).\n\nFor each issue found, show the original line, explain what changed, and provide the corrected code.\n</Prompt>\n\n## What Might Need Updating\n\nMost servers need nothing beyond the import change. Skim the sections below to see if any apply.\n\n### Constructor Settings\n\nIf you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed:\n\n```python\n# Before\nmcp = FastMCP(\"my-server\", host=\"0.0.0.0\", port=8080)\nmcp.run()\n\n# After\nmcp = FastMCP(\"my-server\")\nmcp.run(transport=\"http\", host=\"0.0.0.0\", port=8080)\n```\n\nIf you pass the old kwargs, you'll get a clear `TypeError` with a migration hint.\n\n### Prompts\n\nIf your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, you'll need to upgrade to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. The MCP SDK's bundled FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"prompts\")\n\n@mcp.prompt\ndef review(code: str) -> str:\n    \"\"\"Review code for issues\"\"\"\n    return f\"Please review this code:\\n\\n{code}\"\n```\n\nFor multi-turn prompts:\n\n```python\nfrom fastmcp.prompts import Message\n\n@mcp.prompt\ndef debug(error: str) -> list[Message]:\n    \"\"\"Start a debugging session\"\"\"\n    return [\n        Message(f\"I'm seeing this error:\\n\\n{error}\"),\n        Message(\"I'll help debug that. Can you share the relevant code?\", role=\"assistant\"),\n    ]\n```\n\n### Other `mcp.*` Imports\n\nIf your server imports directly from the `mcp` package — like `import mcp.types` or `from mcp.server.stdio import stdio_server` — those still work. FastMCP includes `mcp` as a dependency, so nothing breaks.\n\nWhere FastMCP provides its own API for the same thing, it's worth switching over:\n\n| mcp Package | FastMCP Equivalent |\n|---|---|\n| `mcp.types.TextContent(type=\"text\", text=str(x))` | Just return `x` from your tool |\n| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` |\n| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |\n| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |\n\nFor anything without a FastMCP equivalent (e.g., specific protocol types you use directly), the `mcp.*` import is fine to keep.\n\n### Decorated Functions\n\nIn FastMCP 1.0, `@mcp.tool` returned a `FunctionTool` object. Now decorators return your original function unchanged — so decorated functions stay callable for testing, reuse, and composition:\n\n```python\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet someone\"\"\"\n    return f\"Hello, {name}!\"\n\n# This works now — the function is still a regular function\nassert greet(\"World\") == \"Hello, World!\"\n```\n\nIf you have code that accesses `.name`, `.description`, or other attributes on the decorated result, that will need updating. This is uncommon — most servers don't interact with the tool object directly. If you need the old behavior temporarily, set `FASTMCP_DECORATOR_MODE=object` to restore it (this compatibility setting is itself deprecated and will be removed in a future release).\n\n## Verify the Upgrade\n\n```bash\n# Install\npip install --upgrade fastmcp\n\n# Check version\nfastmcp version\n\n# Run your server\npython my_server.py\n```\n\nYou can also inspect your server's registered components with the FastMCP CLI:\n\n```bash\nfastmcp inspect my_server.py\n```\n\n## Looking Ahead\n\nThe MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change.\n"
  },
  {
    "path": "docs/getting-started/welcome.mdx",
    "content": "---\ntitle: \"Welcome to FastMCP\"\nsidebarTitle: \"Welcome!\"\ndescription: The fast, Pythonic way to build MCP servers, clients, and applications.\nicon: hand-wave\nmode: center\n---\n{/* <img\n  src=\"/assets/brand/f-watercolor-waves-4.png\"\n\n  alt=\"'F' logo on a watercolor background\"\n  noZoom\n  className=\"rounded-2xl block dark:hidden\"\n  /> \n  <img\n  src=\"/assets/brand/f-watercolor-waves-4-dark.png\"\n  alt=\"'F' logo on a watercolor background\"\n  noZoom\n  className=\"rounded-2xl hidden dark:block\"\n  />\n\n  \n  */}\n<video\n  autoPlay\n  muted\n  loop\n  playsInline\n  className=\"rounded-2xl block dark:hidden\"\n  src=\"/assets/brand/f-watercolor-waves-4-animated.mp4\"\n></video>\n<video\n  autoPlay\n  muted\n  loop\n  playsInline\n  className=\"rounded-2xl hidden dark:block\"\n  src=\"/assets/brand/f-watercolor-waves-4-dark-animated.mp4\"\n></video>\n\n\n**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production — build servers that expose capabilities, connect clients to any MCP service, and give your tools interactive UIs:\n\n```python {1}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Demo 🚀\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n\n## Move Fast and Make Things\n\nThe [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets you give agents access to your tools and data. But building an effective MCP application is harder than it looks.\n\nFastMCP 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.**\n\n**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.\n\nFastMCP has three pillars:\n\n<CardGroup cols={3}>\n  <Card title=\"Servers\" img=\"/assets/images/servers-card.png\" href=\"/servers/server\">\n    Expose tools, resources, and prompts to LLMs.\n  </Card>\n  <Card title=\"Apps\" img=\"/assets/images/apps-card.png\" href=\"/apps/overview\">\n    Give your tools interactive UIs rendered directly in the conversation.\n  </Card>\n  <Card title=\"Clients\" img=\"/assets/images/clients-card.png\" href=\"/clients/client\">\n    Connect to any MCP server — local or remote, programmatic or CLI.\n  </Card>\n</CardGroup>\n\n**[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation.\n\nReady to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart). When you're ready to deploy, [Prefect Horizon](https://www.prefect.io/horizon) offers free hosting for FastMCP users.\n\nFastMCP is made with 💙 by [Prefect](https://www.prefect.io/).\n\n<Tip>\n**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 3.0.0`) to indicate when they were introduced. Note that this may include features that are not yet released.\n</Tip>\n\n## LLM-Friendly Docs\n\nThe FastMCP documentation is available in multiple LLM-friendly formats:\n\n### MCP Server\n\nThe FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`.\n\nIn fact, you can use FastMCP to search the FastMCP docs:\n\n```python\nimport asyncio\nfrom fastmcp import Client\n\nasync def main():\n    async with Client(\"https://gofastmcp.com/mcp\") as client:\n        result = await client.call_tool(\n            name=\"SearchFastMcp\",\n            arguments={\"query\": \"deploy a FastMCP server\"}\n        )\n    print(result)\n\nasyncio.run(main())\n```\n\n### Text Formats\n\nThe docs are also available in [llms.txt format](https://llmstxt.org/):\n- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages\n- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows)\n\nAny page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`.\n\nYou can also copy any page as markdown by pressing \"Cmd+C\" (or \"Ctrl+C\" on Windows) on your keyboard.\n"
  },
  {
    "path": "docs/integrations/anthropic.mdx",
    "content": "---\ntitle: Anthropic API 🤝 FastMCP\nsidebarTitle: Anthropic API\ndescription: Connect FastMCP servers to the Anthropic API\nicon: message-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n\nAnthropic's [Messages API](https://docs.anthropic.com/en/api/messages) supports MCP servers as remote tool sources. This tutorial will show you how to create a FastMCP server and deploy it to a public URL, then how to call it from the Messages API.\n\n<Tip>\nCurrently, the MCP connector only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to Claude. Other MCP features like resources and prompts are not currently supported. You can read more about the MCP connector in the [Anthropic documentation](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector).\n</Tip>\n\n## Create a Server\n\nFirst, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n## Deploy the Server\n\nYour server must be deployed to a public URL in order for Anthropic to access it. The MCP connector supports both SSE and Streamable HTTP transports.\n\nFor development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.\n\nAssuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:\n\n<CodeGroup>\n```bash FastMCP server\npython server.py\n```\n\n```bash ngrok\nngrok http 8000\n```\n</CodeGroup>\n\n<Warning>\nThis exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.\n</Warning>\n\n## Call the Server\n\nTo use the Messages API with MCP servers, you'll need to install the Anthropic Python SDK (not included with FastMCP):\n\n```bash\npip install anthropic\n```\n\nYou'll also need to authenticate with Anthropic. You can do this by setting the `ANTHROPIC_API_KEY` environment variable. Consult the Anthropic SDK documentation for more information.\n\n```bash\nexport ANTHROPIC_API_KEY=\"your-api-key\"\n```\n\nHere is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**\n\n```python {5, 13-22}\nimport anthropic\nfrom rich import print\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\nclient = anthropic.Anthropic()\n\nresponse = client.beta.messages.create(\n    model=\"claude-sonnet-4-20250514\",\n    max_tokens=1000,\n    messages=[{\"role\": \"user\", \"content\": \"Roll a few dice!\"}],\n    mcp_servers=[\n        {\n            \"type\": \"url\",\n            \"url\": f\"{url}/mcp/\",\n            \"name\": \"dice-server\",\n        }\n    ],\n    extra_headers={\n        \"anthropic-beta\": \"mcp-client-2025-04-04\"\n    }\n)\n\nprint(response.content)\n```\n\nIf you run this code, you'll see something like the following output:\n\n```text\nI'll roll some dice for you! Let me use the dice rolling tool.\n\nI rolled 3 dice and got: 4, 2, 6\n\nThe results were 4, 2, and 6. Would you like me to roll again or roll a different number of dice?\n```\n\n\n## Authentication\n\n<VersionBadge version=\"2.6.0\" />\n\nThe MCP connector supports OAuth authentication through authorization tokens, which means you can secure your server while still allowing Anthropic to access it.\n\n### Server Authentication\n\nThe simplest way to add authentication to the server is to use a bearer token scheme. \n\nFor this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/servers/auth/token-verification) documentation. \n\nWe'll start by creating an RSA key pair to sign and verify tokens.\n\n```python\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n```\n\n<Warning>\nFastMCP's `RSAKeyPair` utility is for development and testing only.\n</Warning> \n\nNext, we'll create a `JWTVerifier` to authenticate the server. \n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n```\n\nHere is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**\n\n```python server.py [expandable]\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\nimport random\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    print(f\"\\n---\\n\\n🔑 Dice Roller access token:\\n\\n{access_token}\\n\\n---\\n\")\n    mcp.run(transport=\"http\", port=8000)\n```\n\n### Client Authentication\n\nIf 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.\n\n```python\nError code: 400 - {\n    \"type\": \"error\", \n    \"error\": {\n        \"type\": \"invalid_request_error\", \n        \"message\": \"MCP server 'dice-server' requires authentication. Please provide an authorization_token.\",\n    },\n}\n```\n\nTo authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:\n\n```python {8, 21}\nimport anthropic\nfrom rich import print\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\n# Your access token (replace with your actual token)\naccess_token = 'your-access-token'\n\nclient = anthropic.Anthropic()\n\nresponse = client.beta.messages.create(\n    model=\"claude-sonnet-4-20250514\",\n    max_tokens=1000,\n    messages=[{\"role\": \"user\", \"content\": \"Roll a few dice!\"}],\n    mcp_servers=[\n        {\n            \"type\": \"url\",\n            \"url\": f\"{url}/mcp/\",\n            \"name\": \"dice-server\",\n            \"authorization_token\": access_token\n        }\n    ],\n    extra_headers={\n        \"anthropic-beta\": \"mcp-client-2025-04-04\"\n    }\n)\n\nprint(response.content)\n```\n\nYou should now see the dice roll results in the output.\n"
  },
  {
    "path": "docs/integrations/auth0.mdx",
    "content": "---\ntitle: Auth0 OAuth 🤝 FastMCP\nsidebarTitle: Auth0\ndescription: Secure your FastMCP server with Auth0 OAuth\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.4\" />\n\nThis guide shows you how to secure your FastMCP server using **Auth0 OAuth**. While Auth0 does have support for Dynamic Client Registration, it is not enabled by default so this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge Auth0's dynamic OIDC configuration with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. An **[Auth0 Account](https://auth0.com/)** with access to create Applications\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create an Auth0 Application\n\nCreate an Application in your Auth0 settings to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to Applications\">\n    Go to **Applications → Applications** in your Auth0 account.\n\n    Click **\"+ Create Application\"** to create a new application.\n</Step>\n\n<Step title=\"Create Your Application\">\n    - **Name**: Choose a name users will recognize (e.g., \"My FastMCP Server\")\n    - **Choose an application type**: Choose \"Single Page Web Applications\"\n    - Click **Create** to create the application\n</Step>\n\n<Step title=\"Configure Your Application\">\n    Select the \"Settings\" tab for your application, then find the \"Application URIs\" section.\n\n    - **Allowed Callback URLs**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    - Click **Save** to save your changes\n\n    <Warning>\n    The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.\n    </Warning>\n\n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0Provider.\n    </Tip>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After creating the app, in the \"Basic Information\" section you'll see:\n\n    - **Client ID**: A public identifier like `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`\n    - **Client Secret**: A private hidden value that should always be stored securely\n\n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n\n<Step title=\"Select Your Audience\">\n  Go to **Applications → APIs** in your Auth0 account.\n\n    - Find the API that you want to use for your application\n    - **API Audience**: A URL that uniquely identifies the API\n\n    <Tip>\n    Store this along with of the credentials above. Never commit this to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `Auth0Provider`.\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\n\n# The Auth0Provider utilizes Auth0 OIDC configuration\nauth_provider = Auth0Provider(\n    config_url=\"https://.../.well-known/openid-configuration\",  # Your Auth0 configuration URL\n    client_id=\"tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB\",               # Your Auth0 application Client ID\n    client_secret=\"vPYqbjemq...\",                               # Your Auth0 application Client Secret\n    audience=\"https://...\",                                     # Your Auth0 API audience\n    base_url=\"http://localhost:8000\",                           # Must match your application configuration\n    # redirect_path=\"/auth/callback\"                            # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"Auth0 Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_token_info() -> dict:\n    \"\"\"Returns information about the Auth0 token.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n\n    token = get_access_token()\n\n    return {\n        \"issuer\": token.claims.get(\"iss\"),\n        \"audience\": token.claims.get(\"aud\"),\n        \"scope\": token.claims.get(\"scope\")\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Auth0 authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Auth0-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Auth0 OAuth flows\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Auth0 login in your browser\n        print(\"✓ Authenticated with Auth0!\")\n\n        # Test the protected tool\n        result = await client.call_tool(\"get_token_info\")\n        print(f\"Auth0 audience: {result['audience']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Auth0's authorization page\n2. After you authorize the app, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = Auth0Provider(\n    config_url=\"https://.../.well-known/openid-configuration\",\n    client_id=\"tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB\",\n    client_secret=\"vPYqbjemq...\",\n    audience=\"https://...\",\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Auth0 App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n"
  },
  {
    "path": "docs/integrations/authkit.mdx",
    "content": "---\ntitle: AuthKit 🤝 FastMCP\nsidebarTitle: AuthKit\ndescription: Secure your FastMCP server with AuthKit by WorkOS\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nThis 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.\n\n<Warning>\nAuthKit 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.\n</Warning>\n\n## Configuration\n### Prerequisites\n\nBefore you begin, you will need:\n1.  A **[WorkOS Account](https://workos.com/)** and a new **Project**.\n2.  An **[AuthKit](https://www.authkit.com/)** instance configured within your WorkOS project.\n3.  Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`).\n\n### Step 1: AuthKit Configuration\n\nIn your WorkOS Dashboard, enable AuthKit and configure the following settings:\n\n<Steps>\n<Step title=\"Enable Dynamic Client Registration\">\n    Go to **Applications → Configuration** and enable **Dynamic Client Registration**. This allows MCP clients register with your application automatically.\n\n    ![Enable Dynamic Client Registration](./images/authkit/enable_dcr.png)\n</Step>\n\n<Step title=\"Note Your AuthKit Domain\">\n    Find your **AuthKit Domain** on the configuration page. It will look like `https://your-project-12345.authkit.app`. You'll need this for your FastMCP server configuration.\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server file and use the `AuthKitProvider` to handle all the OAuth integration automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\n# The AuthKitProvider automatically discovers WorkOS endpoints\n# and configures JWT token validation\nauth_provider = AuthKitProvider(\n    authkit_domain=\"https://your-project-12345.authkit.app\",\n    base_url=\"http://localhost:8000\"  # Use your actual server URL\n)\n\nmcp = FastMCP(name=\"AuthKit Secured App\", auth=auth_provider)\n```\n\n## Testing\n\nTo 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:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nAuthKit defaults DCR clients to `client_secret_basic` for token exchange, which conflicts with how some MCP clients send credentials. To avoid token exchange errors, register as a public client by setting `token_endpoint_auth_method` to `\"none\"`:\n\n```python client.py\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\nimport asyncio\n\nauth = OAuth(additional_client_metadata={\"token_endpoint_auth_method\": \"none\"})\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=auth) as client:\n        assert await client.ping()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Production Configuration\n\nFor production deployments, load sensitive configuration from environment variables:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\n# Load configuration from environment variables\nauth = AuthKitProvider(\n    authkit_domain=os.environ.get(\"AUTHKIT_DOMAIN\"),\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-server.com\")\n)\n\nmcp = FastMCP(name=\"AuthKit Secured App\", auth=auth)\n```\n"
  },
  {
    "path": "docs/integrations/aws-cognito.mdx",
    "content": "---\ntitle: AWS Cognito OAuth 🤝 FastMCP\nsidebarTitle: AWS Cognito\ndescription: Secure your FastMCP server with AWS Cognito user pools\nicon: aws\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.4\" />\n\nThis guide shows you how to secure your FastMCP server using **AWS Cognito user pools**. Since AWS Cognito doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge AWS Cognito's traditional OAuth with MCP's authentication requirements. It also includes robust JWT token validation, ensuring enterprise-grade authentication.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. An **[AWS Account](https://aws.amazon.com/)** with access to create AWS Cognito user pools\n2. Basic familiarity with AWS Cognito concepts (user pools, app clients)\n3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create an AWS Cognito User Pool and App Client\n\nSet up AWS Cognito user pool with an app client to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to AWS Cognito\">\n    Go to the **[AWS Cognito Console](https://console.aws.amazon.com/cognito/)** and ensure you're in your desired AWS region.\n\n    Select **\"User pools\"** from the side navigation (click on the hamburger icon at the top left in case you don't see any), and click **\"Create user pool\"** to create a new user pool.\n</Step>\n\n<Step title=\"Define Your Application\">\n    AWS Cognito now provides a streamlined setup experience:\n\n    1. **Application type**: Select **\"Traditional web application\"** (this is the correct choice for FastMCP server-side authentication)\n    2. **Name your application**: Enter a descriptive name (e.g., `FastMCP Server`)\n\n    The traditional web application type automatically configures:\n    - Server-side authentication with client secrets\n    - Authorization code grant flow\n    - Appropriate security settings for confidential clients\n\n    <Info>\n    Choose \"Traditional web application\" rather than SPA, Mobile app, or Machine-to-machine options. This ensures proper OAuth 2.0 configuration for FastMCP.\n    </Info>\n</Step>\n\n<Step title=\"Configure Options\">\n    AWS will guide you through configuration options:\n\n    - **Sign-in identifiers**: Choose how users will sign in (email, username, or phone)\n    - **Required attributes**: Select any additional user information you need\n    - **Return URL**: Add your callback URL (e.g., `http://localhost:8000/auth/callback` for development)\n\n    <Tip>\n    The simplified interface handles most OAuth security settings automatically based on your application type selection.\n    </Tip>\n</Step>\n\n<Step title=\"Review and Create\">\n    Review your configuration and click **\"Create user pool\"**.\n\n    After creation, you'll see your user pool details. Save these important values:\n    - **User pool ID** (format: `eu-central-1_XXXXXXXXX`)\n    - **Client ID** (found under → \"Applications\" → \"App clients\" in the side navigation → \\<Your application name, e.g., `FastMCP Server`\\> → \"App client information\")\n    - **Client Secret** (found under → \"Applications\" → \"App clients\" in the side navigation → \\<Your application name, e.g., `FastMCP Server`\\> → \"App client information\")\n\n    <Tip>\n    The user pool ID and app client credentials are all you need for FastMCP configuration.\n    </Tip>\n</Step>\n\n<Step title=\"Configure OAuth Settings\">\n    Under \"Login pages\" in your app client's settings, you can double check and adjust the OAuth configuration:\n\n    - **Allowed callback URLs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    - **Allowed sign-out URLs**: Optional, for logout functionality\n    - **OAuth 2.0 grant types**: Ensure \"Authorization code grant\" is selected\n    - **OpenID Connect scopes**: Select scopes your application needs (e.g., `openid`, `email`, `profile`)\n\n    <Tip>\n    For local development, you can use `http://localhost` URLs. For production, you must use HTTPS.\n    </Tip>\n</Step>\n\n<Step title=\"Configure Resource Server\">\n    AWS Cognito requires a resource server entry to support OAuth with protected resources. Without this, token exchange will fail with an `invalid_grant` error.\n\n    Navigate to **\"Branding\" → \"Domain\"** in the side navigation, then:\n\n    1. Click **\"Create resource server\"**\n    2. **Resource server name**: Enter a descriptive name (e.g., `My MCP Server`)\n    3. **Resource server identifier**: Enter your MCP endpoint URL exactly as it will be accessed (e.g., `http://localhost:8000/mcp` for development, or `https://your-server.com/mcp` for production)\n    4. Click **\"Create resource server\"**\n\n    <Warning>\n    The resource server identifier must exactly match your `base_url + mcp_path`. For the default configuration with `base_url=\"http://localhost:8000\"` and `path=\"/mcp\"`, use `http://localhost:8000/mcp`.\n    </Warning>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After setup, you'll have:\n\n    - **User Pool ID**: Format like `eu-central-1_XXXXXXXXX`\n    - **Client ID**: Your application's client identifier\n    - **Client Secret**: Generated client secret (keep secure)\n    - **AWS Region**: Where Your AWS Cognito user pool is located\n\n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or AWS Secrets Manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cognito's JWT tokens and user claims automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.aws import AWSCognitoProvider\nfrom fastmcp.server.dependencies import get_access_token\n\n# The AWSCognitoProvider handles JWT validation and user claims\nauth_provider = AWSCognitoProvider(\n    user_pool_id=\"eu-central-1_XXXXXXXXX\",   # Your AWS Cognito user pool ID\n    aws_region=\"eu-central-1\",               # AWS region (defaults to eu-central-1)\n    client_id=\"your-app-client-id\",          # Your app client ID\n    client_secret=\"your-app-client-secret\",  # Your app client Secret\n    base_url=\"http://localhost:8000\",        # Must match your callback URL\n    # redirect_path=\"/auth/callback\"         # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"AWS Cognito Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_access_token_claims() -> dict:\n    \"\"\"Get the authenticated user's access token claims.\"\"\"\n    token = get_access_token()\n    return {\n        \"sub\": token.claims.get(\"sub\"),\n        \"username\": token.claims.get(\"username\"),\n        \"cognito:groups\": token.claims.get(\"cognito:groups\", []),\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by AWS Cognito OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with Your AWS Cognito-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle AWS Cognito OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open AWS Cognito login in your browser\n        print(\"✓ Authenticated with AWS Cognito!\")\n\n        # Test the protected tool\n        print(\"Calling protected tool: get_access_token_claims\")\n        result = await client.call_tool(\"get_access_token_claims\")\n        user_data = result.data\n        print(\"Available access token claims:\")\n        print(f\"- sub: {user_data.get('sub', 'N/A')}\")\n        print(f\"- username: {user_data.get('username', 'N/A')}\")\n        print(f\"- cognito:groups: {user_data.get('cognito:groups', [])}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to AWS Cognito's hosted UI login page\n2. After you sign in (or sign up), you'll be redirected back to your MCP server\n3. The client receives the JWT token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.aws import AWSCognitoProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = AWSCognitoProvider(\n    user_pool_id=\"eu-central-1_XXXXXXXXX\",\n    aws_region=\"eu-central-1\",\n    client_id=\"your-app-client-id\",\n    client_secret=\"your-app-client-secret\",\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production AWS Cognito App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Features\n\n### JWT Token Validation\n\nThe AWS Cognito provider includes robust JWT token validation:\n\n- **Signature Verification**: Validates tokens against AWS Cognito's public keys (JWKS)\n- **Expiration Checking**: Automatically rejects expired tokens\n- **Issuer Validation**: Ensures tokens come from your specific AWS Cognito user pool\n- **Scope Enforcement**: Verifies required OAuth scopes are present\n\n### User Claims and Groups\n\nAccess rich user information from AWS Cognito JWT tokens:\n\n```python\nfrom fastmcp.server.dependencies import get_access_token\n\n@mcp.tool\nasync def admin_only_tool() -> str:\n    \"\"\"A tool only available to admin users.\"\"\"\n    token = get_access_token()\n    user_groups = token.claims.get(\"cognito:groups\", [])\n\n    if \"admin\" not in user_groups:\n        raise ValueError(\"This tool requires admin access\")\n\n    return \"Admin access granted!\"\n```\n\n### Enterprise Integration\n\nPerfect for enterprise environments with:\n\n- **Single Sign-On (SSO)**: Integrate with corporate identity providers\n- **Multi-Factor Authentication (MFA)**: Leverage AWS Cognito's built-in MFA\n- **User Groups**: Role-based access control through AWS Cognito groups\n- **Custom Attributes**: Access custom user attributes defined in your AWS Cognito user pool\n- **Compliance**: Meet enterprise security and compliance requirements"
  },
  {
    "path": "docs/integrations/azure.mdx",
    "content": "---\ntitle: Azure (Microsoft Entra ID) OAuth 🤝 FastMCP\nsidebarTitle: Azure (Entra ID)\ndescription: Secure your FastMCP server with Azure/Microsoft Entra OAuth\nicon: microsoft\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nThis guide shows you how to secure your FastMCP server using **Azure OAuth** (Microsoft Entra ID). Since Azure doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Azure's traditional OAuth with MCP's authentication requirements. FastMCP validates Azure JWTs against your application's client_id.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. An **[Azure Account](https://portal.azure.com/)** with access to create App registrations\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n3. Your Azure tenant ID (found in Azure Portal under Microsoft Entra ID)\n\n### Step 1: Create an Azure App Registration\n\nCreate an App registration in Azure Portal to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to App registrations\">\n    Go to the [Azure Portal](https://portal.azure.com) and navigate to **Microsoft Entra ID → App registrations**.\n    \n    Click **\"New registration\"** to create a new application.\n</Step>\n\n<Step title=\"Configure Your Application\">\n    Fill in the application details:\n    \n    - **Name**: Choose a name users will recognize (e.g., \"My FastMCP Server\")\n    - **Supported account types**: Choose based on your needs:\n      - **Single tenant**: Only users in your organization\n      - **Multitenant**: Users in any Microsoft Entra directory\n      - **Multitenant + personal accounts**: Any Microsoft account\n    - **Redirect URI**: Select \"Web\" and enter your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    \n    <Warning>\n    The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Azure allows `http://localhost` URLs. For production, you must use HTTPS.\n    </Warning>\n    \n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the AzureProvider.\n    </Tip>\n\n    - **Expose an API**: Configure your Application ID URI and define scopes\n      - Go to **Expose an API** in the App registration sidebar.\n      - Click **Set** next to \"Application ID URI\" and choose one of:\n        - Keep the default `api://{client_id}`\n        - Set a custom value, following the supported formats (see [Identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions))\n      - Click **Add a scope** and create a scope your app will require, for example:\n        - Scope name: `read` (or `write`, etc.)\n        - Admin consent display name/description: as appropriate for your org\n        - Who can consent: as needed (Admins only or Admins and users)\n\n    - **Configure Access Token Version**: Ensure your app uses access token v2\n      - Go to **Manifest** in the App registration sidebar.\n      - Find the `requestedAccessTokenVersion` property and set it to `2`:\n        ```json\n        \"api\": {\n            \"requestedAccessTokenVersion\": 2\n        }\n        ```\n      - Click **Save** at the top of the manifest editor.\n\n    <Warning>\n    Access token v2 is required for FastMCP's Azure integration to work correctly. If this is not set, you may encounter authentication errors.\n    </Warning>\n\n    <Note>\n    In FastMCP's `AzureProvider`, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.\n    </Note>\n\n\n</Step>\n\n\n<Step title=\"Create Client Secret\">\n    After registration, navigate to **Certificates & secrets** in your app's settings.\n    \n    - Click **\"New client secret\"**\n    - Add a description (e.g., \"FastMCP Server\")\n    - Choose an expiration period\n    - Click **\"Add\"**\n    \n    <Warning>\n    Copy the secret value immediately - it won't be shown again! You'll need to create a new secret if you lose it.\n    </Warning>\n</Step>\n\n<Step title=\"Note Your Credentials\">\n    From the **Overview** page of your app registration, note:\n    \n    - **Application (client) ID**: A UUID like `835f09b6-0f0f-40cc-85cb-f32c5829a149`\n    - **Directory (tenant) ID**: A UUID like `08541b6e-646d-43de-a0eb-834e6713d6d5`\n    - **Client Secret**: The value you copied in the previous step\n    \n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider\n\n# The AzureProvider handles Azure's token format and validation\nauth_provider = AzureProvider(\n    client_id=\"835f09b6-0f0f-40cc-85cb-f32c5829a149\",  # Your Azure App Client ID\n    client_secret=\"your-client-secret\",                 # Your Azure App Client Secret\n    tenant_id=\"08541b6e-646d-43de-a0eb-834e6713d6d5\", # Your Azure Tenant ID (REQUIRED)\n    base_url=\"http://localhost:8000\",                   # Must match your App registration\n    required_scopes=[\"your-scope\"],                 # At least one scope REQUIRED - name of scope from your App\n    # identifier_uri defaults to api://{client_id}\n    # identifier_uri=\"api://your-api-id\",\n    # Optional: request additional upstream scopes in the authorize request\n    # additional_authorize_scopes=[\"User.Read\", \"openid\", \"email\"],\n    # redirect_path=\"/auth/callback\"                  # Default value, customize if needed\n    # base_authority=\"login.microsoftonline.us\"      # For Azure Government (default: login.microsoftonline.com)\n)\n\nmcp = FastMCP(name=\"Azure Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated Azure user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n    \n    token = get_access_token()\n    # The AzureProvider stores user data in token claims\n    return {\n        \"azure_id\": token.claims.get(\"sub\"),\n        \"email\": token.claims.get(\"email\"),\n        \"name\": token.claims.get(\"name\"),\n        \"job_title\": token.claims.get(\"job_title\"),\n        \"office_location\": token.claims.get(\"office_location\")\n    }\n```\n\n<Note>\n**Important**: The `tenant_id` parameter is **REQUIRED**. Azure no longer supports using \"common\" for new applications due to security requirements. You must use one of:\n\n- **Your specific tenant ID**: Found in Azure Portal (e.g., `08541b6e-646d-43de-a0eb-834e6713d6d5`)\n- **\"organizations\"**: For work and school accounts only\n- **\"consumers\"**: For personal Microsoft accounts only\n\nUsing your specific tenant ID is recommended for better security and control.\n</Note>\n\n<Note>\n**Important**: The `required_scopes` parameter is **REQUIRED** and must include at least one scope. Azure's OAuth API requires the `scope` parameter in all authorization requests - you cannot authenticate without specifying at least one scope. Use the unprefixed scope names from your Azure App registration (e.g., `[\"read\", \"write\"]`). These scopes must be created under **Expose an API** in your App registration.\n</Note>\n\n### Scope Handling\n\nFastMCP automatically prefixes `required_scopes` with your `identifier_uri` (e.g., `api://your-client-id`) since these are your custom API scopes. Scopes in `additional_authorize_scopes` are sent as-is since they target external resources like Microsoft Graph.\n\n**`required_scopes`** — Your custom API scopes, defined in Azure \"Expose an API\":\n\n| You write | Sent to Azure | Validated on tokens |\n|-----------|---------------|---------------------|\n| `mcp-read` | `api://xxx/mcp-read` | ✓ |\n| `my.scope` | `api://xxx/my.scope` | ✓ |\n| `openid` | `openid` | ✗ (OIDC scope) |\n| `api://xxx/read` | `api://xxx/read` | ✓ |\n\n**`additional_authorize_scopes`** — External scopes (e.g., Microsoft Graph) for server-side use:\n\n| You write | Sent to Azure | Validated on tokens |\n|-----------|---------------|---------------------|\n| `User.Read` | `User.Read` | ✗ |\n| `Mail.Send` | `Mail.Send` | ✗ |\n\n<Note>\n`offline_access` is automatically included to obtain refresh tokens. FastMCP manages token refreshing automatically.\n</Note>\n\n<Info>\n**Why aren't `additional_authorize_scopes` validated?** Azure issues separate tokens per resource. The access token FastMCP receives is for *your API*—Graph scopes aren't in its `scp` claim. To call Graph APIs, your server uses the upstream Azure token in an on-behalf-of (OBO) flow.\n</Info>\n\n<Note>\nOIDC scopes (`openid`, `profile`, `email`, `offline_access`) are never prefixed and excluded from validation because Azure doesn't include them in access token `scp` claims.\n</Note>\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Azure OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Azure-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Azure OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Azure login in your browser\n        print(\"✓ Authenticated with Azure!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"Azure user: {result['email']}\")\n        print(f\"Name: {result['name']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Microsoft's authorization page\n2. Sign in with your Microsoft account (work, school, or personal based on your tenant configuration)\n3. Grant the requested permissions\n4. After authorization, you'll be redirected back\n5. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = AzureProvider(\n    client_id=\"835f09b6-0f0f-40cc-85cb-f32c5829a149\",\n    client_secret=\"your-client-secret\",\n    tenant_id=\"08541b6e-646d-43de-a0eb-834e6713d6d5\",\n    base_url=\"https://your-production-domain.com\",\n    required_scopes=[\"your-scope\"],\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Azure App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Token Verification Only (Managed Identity)\n\n<VersionBadge version=\"2.15.0\" />\n\nFor deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full `AzureProvider`.\n\nThis pattern is ideal when:\n- Your infrastructure handles authentication (e.g., Managed Identity)\n- You don't need the OAuth proxy flow (no `client_secret` required)\n- You just need to verify that incoming Azure AD tokens are valid\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import RemoteAuthProvider\nfrom fastmcp.server.auth.providers.azure import AzureJWTVerifier\nfrom pydantic import AnyHttpUrl\n\ntenant_id = \"your-tenant-id\"\nclient_id = \"your-client-id\"\n\n# AzureJWTVerifier auto-configures JWKS, issuer, and audience\nverifier = AzureJWTVerifier(\n    client_id=client_id,\n    tenant_id=tenant_id,\n    required_scopes=[\"access_as_user\"],  # Scope names from Azure Portal\n)\n\nauth = RemoteAuthProvider(\n    token_verifier=verifier,\n    authorization_servers=[\n        AnyHttpUrl(f\"https://login.microsoftonline.com/{tenant_id}/v2.0\")\n    ],\n    base_url=\"https://your-container-app.azurecontainerapps.io\",\n)\n\nmcp = FastMCP(name=\"Azure MI App\", auth=auth)\n```\n\n`AzureJWTVerifier` handles Azure's scope format automatically. You write scope names exactly as they appear in Azure Portal under **Expose an API** (e.g., `access_as_user`). The verifier validates tokens using the short-form scopes that Azure puts in the `scp` claim, while advertising the full URI scopes (e.g., `api://your-client-id/access_as_user`) in OAuth metadata so MCP clients know what to request.\n\n<Note>\nFor Azure Government, pass `base_authority=\"login.microsoftonline.us\"` to `AzureJWTVerifier`.\n</Note>\n\n## On-Behalf-Of (OBO)\n\n<VersionBadge version=\"3.0.0\" />\n\nThe On-Behalf-Of (OBO) flow allows your FastMCP server to call downstream Microsoft APIs—like Microsoft Graph—using the authenticated user's identity. When a user authenticates to your MCP server, you receive a token for your API. OBO exchanges that token for a new token that can call other services, maintaining the user's identity and permissions throughout the chain.\n\nThis pattern is useful when your tools need to access user-specific data from Microsoft services: reading emails, accessing calendar events, querying SharePoint, or any other Graph API operation that requires user context.\n\n<Note>\nOBO features require the `azure` extra:\n\n```bash\npip install 'fastmcp[azure]'\n```\n</Note>\n\n### Azure Portal Setup\n\nOBO requires additional configuration in your Azure App registration beyond basic authentication.\n\n<Steps>\n<Step title=\"Add API Permissions\">\n    In your App registration, navigate to **API permissions** and add the Microsoft Graph permissions your tools will need.\n\n    - Click **Add a permission** → **Microsoft Graph** → **Delegated permissions**\n    - Select the permissions required for your use case (e.g., `Mail.Read`, `Calendars.Read`, `User.Read`)\n    - Repeat for any other APIs you need to call\n\n    <Warning>\n    Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.\n    </Warning>\n</Step>\n\n<Step title=\"Grant Admin Consent\">\n    OBO requires admin consent for the permissions you've added. In the **API permissions** page, click **Grant admin consent for [Your Organization]**.\n\n    Without admin consent, OBO token exchanges will fail with an `AADSTS65001` error indicating the user or administrator hasn't consented to use the application.\n\n    <Tip>\n    For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.\n    </Tip>\n</Step>\n</Steps>\n\n### Configure AzureProvider for OBO\n\nThe `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider\n\nauth_provider = AzureProvider(\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n    tenant_id=\"your-tenant-id\",\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"mcp-access\"],  # Your API scope\n    # Include Graph scopes for OBO\n    additional_authorize_scopes=[\n        \"https://graph.microsoft.com/Mail.Read\",\n        \"https://graph.microsoft.com/User.Read\",\n        \"offline_access\",  # Enables refresh tokens\n    ],\n)\n\nmcp = FastMCP(name=\"Graph-Enabled Server\", auth=auth_provider)\n```\n\nScopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.\n\n<Info>\nUse fully-qualified scope URIs for downstream APIs (e.g., `https://graph.microsoft.com/Mail.Read`). Short forms like `Mail.Read` work for authorization requests, but fully-qualified URIs are clearer and avoid ambiguity.\n</Info>\n\n### EntraOBOToken Dependency\n\nThe `EntraOBOToken` dependency handles the complete OBO flow automatically. Declare it as a parameter default with the scopes you need, and FastMCP exchanges the user's token for a downstream API token before your function runs.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken\nimport httpx\n\nauth_provider = AzureProvider(\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n    tenant_id=\"your-tenant-id\",\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"mcp-access\"],\n    additional_authorize_scopes=[\n        \"https://graph.microsoft.com/Mail.Read\",\n        \"https://graph.microsoft.com/User.Read\",\n    ],\n)\n\nmcp = FastMCP(name=\"Email Reader\", auth=auth_provider)\n\n@mcp.tool\nasync def get_recent_emails(\n    count: int = 10,\n    graph_token: str = EntraOBOToken([\"https://graph.microsoft.com/Mail.Read\"]),\n) -> list[dict]:\n    \"\"\"Get the user's recent emails from Microsoft Graph.\"\"\"\n    async with httpx.AsyncClient() as client:\n        response = await client.get(\n            f\"https://graph.microsoft.com/v1.0/me/messages?$top={count}\",\n            headers={\"Authorization\": f\"Bearer {graph_token}\"},\n        )\n        response.raise_for_status()\n        data = response.json()\n\n    return [\n        {\"subject\": msg[\"subject\"], \"from\": msg[\"from\"][\"emailAddress\"][\"address\"]}\n        for msg in data.get(\"value\", [])\n    ]\n```\n\nThe `graph_token` parameter receives a ready-to-use access token for Microsoft Graph. FastMCP handles the OBO exchange transparently—your function just uses the token to call the API.\n\n<Warning>\n**Scope alignment is critical.** The scopes passed to `EntraOBOToken` must be a subset of the scopes in `additional_authorize_scopes`. If you request a scope during OBO that wasn't included in the initial authorization, the exchange will fail.\n</Warning>\n\n<Tip>\nFor advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.\n</Tip>\n\n<Tip>\nFor 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).\n</Tip>\n"
  },
  {
    "path": "docs/integrations/chatgpt.mdx",
    "content": "---\ntitle: ChatGPT 🤝 FastMCP\nsidebarTitle: ChatGPT\ndescription: Connect FastMCP servers to ChatGPT in Chat and Deep Research modes\nicon: message-smile\n---\n\n[ChatGPT](https://chatgpt.com/) supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval.\n\n<Tip>\n**Developer Mode Required for Chat Mode**: To use MCP servers in regular ChatGPT conversations, you must first enable Developer Mode in your ChatGPT settings. This feature is available for ChatGPT Pro, Team, Enterprise, and Edu users.\n</Tip>\n\n<Note>\nOpenAI's official MCP documentation and examples are built with **FastMCP v2**! Learn more from their [MCP documentation](https://platform.openai.com/docs/mcp) and [Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode).\n</Note>\n\n## Build a Server\n\nFirst, let's create a simple FastMCP server:\n\n```python server.py\nfrom fastmcp import FastMCP\nimport random\n\nmcp = FastMCP(\"Demo Server\")\n\n@mcp.tool\ndef roll_dice(sides: int = 6) -> int:\n    \"\"\"Roll a dice with the specified number of sides.\"\"\"\n    return random.randint(1, sides)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n### Deploy Your Server\n\nYour server must be accessible from the internet. For development, use `ngrok`:\n\n<CodeGroup>\n```bash Terminal 1\npython server.py\n```\n\n```bash Terminal 2\nngrok http 8000\n```\n</CodeGroup>\n\nNote your public URL (e.g., `https://abc123.ngrok.io`) for the next steps.\n\n## Chat Mode\n\nChat mode lets you use MCP tools directly in ChatGPT conversations. See [OpenAI's Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode) for the latest requirements.\n\n### Add to ChatGPT\n\n#### 1. Enable Developer Mode\n\n1. Open ChatGPT and go to **Settings** → **Connectors**\n2. Under **Advanced**, toggle **Developer Mode** to enabled\n\n#### 2. Create Connector\n\n1. In **Settings** → **Connectors**, click **Create**\n2. Enter:\n   - **Name**: Your server name\n   - **Server URL**: `https://your-server.ngrok.io/mcp/`\n3. Check **I trust this provider**\n4. Add authentication if needed\n5. Click **Create**\n\n<Note>\n**Without Developer Mode**: If you don't have search/fetch tools, ChatGPT will reject the server. With Developer Mode enabled, you don't need search/fetch tools for Chat mode.\n</Note>\n\n#### 3. Use in Chat\n\n1. Start a new chat\n2. Click the **+** button → **More** → **Developer Mode**\n3. **Enable your MCP server connector** (required - the connector must be explicitly added to each chat)\n4. Now you can use your tools:\n\nExample usage:\n- \"Roll a 20-sided dice\"\n- \"Roll dice\" (uses default 6 sides)\n\n<Tip>\nThe connector must be explicitly enabled in each chat session through Developer Mode. Once added, it remains active for the entire conversation.\n</Tip>\n\n### Skip Confirmations\n\nUse `annotations={\"readOnlyHint\": True}` to skip confirmation prompts for read-only tools:\n\n```python\n@mcp.tool(annotations={\"readOnlyHint\": True})\ndef get_status() -> str:\n    \"\"\"Check system status.\"\"\"\n    return \"All systems operational\"\n\n@mcp.tool()  # No annotation - ChatGPT may ask for confirmation\ndef delete_item(id: str) -> str:\n    \"\"\"Delete an item.\"\"\"\n    return f\"Deleted {id}\"\n```\n\n## Deep Research Mode\n\nDeep Research mode provides systematic information retrieval with citations. See [OpenAI's MCP documentation](https://platform.openai.com/docs/mcp) for the latest Deep Research specifications.\n\n<Warning>\n**Search and Fetch Required**: Without Developer Mode, ChatGPT will reject any server that doesn't have both `search` and `fetch` tools. Even in Developer Mode, Deep Research only uses these two tools.\n</Warning>\n\n### Tool Implementation\n\nDeep Research tools must follow this pattern:\n\n```python\n@mcp.tool()\ndef search(query: str) -> dict:\n    \"\"\"\n    Search for records matching the query.\n    Must return {\"ids\": [list of string IDs]}\n    \"\"\"\n    # Your search logic\n    matching_ids = [\"id1\", \"id2\", \"id3\"]\n    return {\"ids\": matching_ids}\n\n@mcp.tool()\ndef fetch(id: str) -> dict:\n    \"\"\"\n    Fetch a complete record by ID.\n    Return the full record data for ChatGPT to analyze.\n    \"\"\"\n    # Your fetch logic\n    return {\n        \"id\": id,\n        \"title\": \"Record Title\",\n        \"content\": \"Full record content...\",\n        \"metadata\": {\"author\": \"Jane Doe\", \"date\": \"2024\"}\n    }\n```\n\n### Using Deep Research\n\n1. Ensure your server is added to ChatGPT's connectors (same as Chat mode)\n2. Start a new chat\n3. Click **+** → **Deep Research**\n4. Select your MCP server as a source\n5. Ask research questions\n\nChatGPT will use your `search` and `fetch` tools to find and cite relevant information.\n\n"
  },
  {
    "path": "docs/integrations/claude-code.mdx",
    "content": "---\ntitle: Claude Code 🤝 FastMCP\nsidebarTitle: Claude Code\ndescription: Install and use FastMCP servers in Claude Code\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\n[Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.\n\n## Requirements\n\nThis integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Claude Code's built-in MCP management commands.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.10.3\" />\n\nThe easiest way to install a FastMCP server in Claude Code is using the `fastmcp install claude-code` command. This automatically handles the configuration, dependency management, and calls Claude Code's built-in MCP management system.\n\n```bash\nfastmcp install claude-code server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install claude-code server.py\nfastmcp install claude-code server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install claude-code server.py:my_custom_server\n```\n\nThe command will automatically configure the server with Claude Code's `claude mcp add` command.\n\n#### Dependencies\n\nFastMCP provides flexible dependency management options for your Claude Code servers:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install claude-code server.py --with pandas --with requests\n```\n\n**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:\n\n```bash\nfastmcp install claude-code server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install claude-code server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Configuration\n\nControl the Python environment for your server with these options:\n\n**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:\n\n```bash\nfastmcp install claude-code server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:\n\n```bash\nfastmcp install claude-code server.py --project /path/to/my-project\n```\n\n#### Environment Variables\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install claude-code server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install claude-code server.py --server-name \"Weather Server\" --env-file .env\n```\n\n<Warning>\n**Claude Code must be installed**. The integration looks for the Claude Code CLI at the default installation location (`~/.claude/local/claude`) and uses the `claude mcp add` command to register servers.\n</Warning>\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually use Claude Code's built-in MCP management commands. This gives you direct control over how your server is launched:\n\n```bash\n# Add a server with custom configuration\nclaude mcp add dice-roller -- uv run --with fastmcp fastmcp run server.py\n\n# Add with environment variables\nclaude mcp add weather-server -e API_KEY=secret -e DEBUG=true -- uv run --with fastmcp fastmcp run server.py\n\n# Add with specific scope (local, user, or project)\nclaude mcp add my-server --scope user -- uv run --with fastmcp fastmcp run server.py\n```\n\nYou can also manually specify Python versions and project directories in your Claude Code commands:\n\n```bash\n# With specific Python version\nclaude mcp add ml-server -- uv run --python 3.11 --with fastmcp fastmcp run server.py\n\n# Within a project directory\nclaude mcp add project-server -- uv run --project /path/to/project --with fastmcp fastmcp run server.py\n```\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Claude Code.\n\nTry asking Claude something like:\n\n> \"Roll some dice for me\"\n\nClaude will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:\n\n> I'll roll some dice for you! Here are your results: [4, 2, 6]\n> \n> You rolled three dice and got a 4, a 2, and a 6!\n\nClaude Code can now access all the tools, resources, and prompts you've defined in your FastMCP server. \n\nIf your server provides resources, you can reference them with `@` mentions using the format `@server:protocol://resource/path`. If your server provides prompts, you can use them as slash commands with `/mcp__servername__promptname`."
  },
  {
    "path": "docs/integrations/claude-desktop.mdx",
    "content": "---\ntitle: Claude Desktop 🤝 FastMCP\nsidebarTitle: Claude Desktop\ndescription: Connect FastMCP servers to Claude Desktop\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\n[Claude Desktop](https://www.claude.com/download) supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.\n\n<Note>\nRemote MCP server support is currently in beta and available for users on Claude Pro, Max, Team, and Enterprise plans (as of June 2025). Most users will still need to use local STDIO connections.\n</Note>\n\n<Note>\nThis guide focuses specifically on using FastMCP servers with Claude Desktop. For general Claude Desktop MCP setup and official examples, see the [official Claude Desktop quickstart guide](https://modelcontextprotocol.io/quickstart/user).\n</Note>\n\n\n## Requirements\n\nClaude Desktop traditionally requires MCP servers to run locally using STDIO transport, where your server communicates with Claude through standard input/output rather than HTTP. However, users on certain plans now have access to remote server support as well.\n\n<Tip>\nIf you don't have access to remote server support or need to connect to remote servers, you can create a **proxy server** that runs locally via STDIO and forwards requests to remote HTTP servers. See the [Proxy Servers](#proxy-servers) section below.\n</Tip>\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.10.3\" />\n\nThe easiest way to install a FastMCP server in Claude Desktop is using the `fastmcp install claude-desktop` command. This automatically handles the configuration and dependency management.\n\n<Tip>\nPrior to version 2.10.3, Claude Desktop could be managed by running `fastmcp install <path>` without specifying the client.\n</Tip>\n\n```bash\nfastmcp install claude-desktop server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install claude-desktop server.py\nfastmcp install claude-desktop server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install claude-desktop server.py:my_custom_server\n```\n\nAfter installation, restart Claude Desktop completely. You should see a hammer icon (🔨) in the bottom left of the input box, indicating that MCP tools are available.\n\n#### Dependencies\n\nFastMCP provides several ways to manage your server's dependencies when installing in Claude Desktop:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install claude-desktop server.py --with pandas --with requests\n```\n\n**Requirements file**: If you have a `requirements.txt` file listing all your dependencies, use `--with-requirements` to install them all at once:\n\n```bash\nfastmcp install claude-desktop server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: For local packages in development, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install claude-desktop server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Directory\n\nFastMCP allows you to control the Python environment for your server:\n\n**Python version**: Use `--python` to specify which Python version your server should run with. This is particularly useful when your server requires a specific Python version:\n\n```bash\nfastmcp install claude-desktop server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project directory. This ensures that `uv` will discover all `pyproject.toml`, `uv.toml`, and `.python-version` files from that project:\n\n```bash\nfastmcp install claude-desktop server.py --project /path/to/my-project\n```\n\nWhen you specify a project directory, all relative paths in your server will be resolved from that directory, and the project's virtual environment will be used.\n\n#### Environment Variables\n\n<Warning>\nClaude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install claude-desktop server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install claude-desktop server.py --server-name \"Weather Server\" --env-file .env\n```\n<Warning>\n- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.\n- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.\n</Warning>\n\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually edit Claude Desktop's configuration file. You can open the configuration file from Claude's developer settings, or find it in the following locations:\n- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`\n- **Windows**: `%APPDATA%\\Claude\\claude_desktop_config.json`\n\nThe configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/your/server.py\"]\n    }\n  }\n}\n```\n\nAfter updating the configuration file, restart Claude Desktop completely. Look for the hammer icon (🔨) to confirm your server is loaded.\n\n#### Dependencies\n\nIf your server has dependencies, you can use `uv` or another package manager to set up the environment.\n\n\nWhen manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration uses `uv run` to create an isolated environment with your specified packages:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--with\", \"fastmcp\",\n        \"--with\", \"pandas\",\n        \"--with\", \"requests\", \n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nYou can also manually specify Python versions and project directories in your configuration. Add `--python` to use a specific Python version, or `--project` to run within a project directory:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--python\", \"3.11\",\n        \"--project\", \"/path/to/project\",\n        \"--with\", \"fastmcp\",\n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nThe order of arguments matters: Python version and project settings come before package specifications, which come before the actual command to run.\n\n<Warning>\n- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.\n- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.\n</Warning>\n\n#### Environment Variables\n\nYou can also specify environment variables in the configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"weather-server\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/weather_server.py\"],\n      \"env\": {\n        \"API_KEY\": \"your-api-key\",\n        \"DEBUG\": \"true\"\n      }\n    }\n  }\n}\n```\n<Warning>\nClaude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\n\n## Remote Servers\n\n\nUsers on Claude Pro, Max, Team, and Enterprise plans have first-class remote server support via integrations. For other users, or as an alternative approach, FastMCP can create a proxy server that forwards requests to a remote HTTP server. You can install the proxy server in Claude Desktop.\n\nCreate a proxy server that connects to a remote HTTP server:\n\n```python proxy_server.py\nfrom fastmcp.server import create_proxy\n\n# Create a proxy to a remote server\nproxy = create_proxy(\n    \"https://example.com/mcp/sse\",\n    name=\"Remote Server Proxy\"\n)\n\nif __name__ == \"__main__\":\n    proxy.run()  # Runs via STDIO for Claude Desktop\n```\n\n### Authentication\n\nFor authenticated remote servers, create an authenticated client following the guidance in the [client auth documentation](/clients/auth/bearer) and pass it to the proxy:\n\n```python auth_proxy_server.py {7}\nfrom fastmcp import Client\nfrom fastmcp.client.auth import BearerAuth\nfrom fastmcp.server import create_proxy\n\n# Create authenticated client\nclient = Client(\n    \"https://api.example.com/mcp/sse\",\n    auth=BearerAuth(token=\"your-access-token\")\n)\n\n# Create proxy using the authenticated client\nproxy = create_proxy(client, name=\"Authenticated Proxy\")\n\nif __name__ == \"__main__\":\n    proxy.run()\n```\n\n"
  },
  {
    "path": "docs/integrations/cursor.mdx",
    "content": "---\ntitle: Cursor 🤝 FastMCP\nsidebarTitle: Cursor\ndescription: Install and use FastMCP servers in Cursor\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\n[Cursor](https://www.cursor.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers.\n\n## Requirements\n\nThis integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly in Cursor's settings.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.10.3\" />\n\nThe easiest way to install a FastMCP server in Cursor is using the `fastmcp install cursor` command. This automatically handles the configuration, dependency management, and opens Cursor with a deeplink to install the server.\n\n```bash\nfastmcp install cursor server.py\n```\n\n#### Workspace Installation\n<VersionBadge version=\"2.12.0\" />\n\nBy default, FastMCP installs servers globally for Cursor. You can also install servers to project-specific workspaces using the `--workspace` flag:\n\n```bash\n# Install to current directory's .cursor/ folder\nfastmcp install cursor server.py --workspace .\n\n# Install to specific workspace\nfastmcp install cursor server.py --workspace /path/to/project\n```\n\nThis creates a `.cursor/mcp.json` configuration file in the specified workspace directory, allowing different projects to have their own MCP server configurations.\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install cursor server.py\nfastmcp install cursor server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install cursor server.py:my_custom_server\n```\n\nAfter running the command, Cursor will open automatically and prompt you to install the server. The command will be `uv`, which is expected as this is a Python STDIO server. Click \"Install\" to confirm:\n\n![Cursor install prompt](./cursor-install-mcp.png)\n\n#### Dependencies\n\nFastMCP offers multiple ways to manage dependencies for your Cursor servers:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install cursor server.py --with pandas --with requests\n```\n\n**Requirements file**: For projects with a `requirements.txt` file, use `--with-requirements` to install all dependencies at once:\n\n```bash\nfastmcp install cursor server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: When developing local packages, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install cursor server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Configuration\n\nControl your server's Python environment with these options:\n\n**Python version**: Use `--python` to specify which Python version your server should use. This is essential when your server requires specific Python features:\n\n```bash\nfastmcp install cursor server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project context. This ensures `uv` discovers all project configuration files and uses the correct virtual environment:\n\n```bash\nfastmcp install cursor server.py --project /path/to/my-project\n```\n\n#### Environment Variables\n\n<Warning>\nCursor runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install cursor server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install cursor server.py --server-name \"Weather Server\" --env-file .env\n```\n\n<Warning>\n**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.\n</Warning>\n\n### Generate MCP JSON\n\n<Note>\n**Use the first-class integration above for the best experience.** The MCP JSON generation is useful for advanced use cases, manual configuration, or integration with other tools.\n</Note>\n\nYou can generate MCP JSON configuration for manual use:\n\n```bash\n# Generate configuration and output to stdout\nfastmcp install mcp-json server.py --server-name \"Dice Roller\" --with pandas\n\n# Copy configuration to clipboard for easy pasting\nfastmcp install mcp-json server.py --server-name \"Dice Roller\" --copy\n```\n\nThis generates the standard `mcpServers` configuration format that can be used with any MCP-compatible client.\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually edit Cursor's configuration file. The configuration file is located at:\n- **All platforms**: `~/.cursor/mcp.json`\n\nThe configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/your/server.py\"]\n    }\n  }\n}\n```\n\nAfter updating the configuration file, your server should be available in Cursor.\n\n#### Dependencies\n\nIf your server has dependencies, you can use `uv` or another package manager to set up the environment.\n\nWhen manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration should use `uv run` to create an isolated environment with your specified packages:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--with\", \"fastmcp\",\n        \"--with\", \"pandas\",\n        \"--with\", \"requests\", \n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nYou can also manually specify Python versions and project directories in your configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--python\", \"3.11\",\n        \"--project\", \"/path/to/project\",\n        \"--with\", \"fastmcp\",\n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nNote that the order of arguments is important: Python version and project settings should come before package specifications.\n\n<Warning>\n**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.\n</Warning>\n\n#### Environment Variables\n\nYou can also specify environment variables in the configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"weather-server\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/weather_server.py\"],\n      \"env\": {\n        \"API_KEY\": \"your-api-key\",\n        \"DEBUG\": \"true\"\n      }\n    }\n  }\n}\n```\n\n<Warning>\nCursor runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Cursor's AI assistant.\n\nTry asking Cursor something like:\n\n> \"Roll some dice for me\"\n\nCursor will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:\n\n> 🎲 Here are your dice rolls: 4, 6, 4\n> \n> You rolled 3 dice with a total of 14! The 6 was a nice high roll there!\n\nThe AI assistant can now access all the tools, resources, and prompts you've defined in your FastMCP server.\n"
  },
  {
    "path": "docs/integrations/descope.mdx",
    "content": "---\ntitle: Descope 🤝 FastMCP\nsidebarTitle: Descope\ndescription: Secure your FastMCP server with Descope\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.12.4\" />\n\nThis guide shows you how to secure your FastMCP server using [**Descope**](https://www.descope.com), a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where Descope handles user login and your FastMCP server validates the tokens.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n\n1. To [sign up](https://www.descope.com/sign-up) for a Free Forever Descope account\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)\n\n### Step 1: Configure Descope\n\n<Steps>\n<Step title=\"Create an MCP Server\">\n    1. Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, and create a new MCP Server.\n    2. Give the MCP server a name and description.\n    3. Ensure that **Dynamic Client Registration (DCR)** is enabled. Then click **Create**.\n    4. Once you've created the MCP Server, note your Well-Known URL.\n    \n    \n    <Warning>\n    DCR is required for FastMCP clients to automatically register with your authentication server.\n    </Warning>\n</Step>\n\n<Step title=\"Note Your Well-Known URL\">\n    Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers):\n    ```\n    Well-Known URL: https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration\n    ```\n</Step>\n</Steps>\n\n### Step 2: Environment Setup\n\nCreate a `.env` file with your Descope configuration:\n\n```bash\nDESCOPE_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration     # Your Descope Well-Known URL\nSERVER_URL=http://localhost:3000     # Your server's base URL\n```\n\n### Step 3: FastMCP Configuration\n\nCreate your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.descope import DescopeProvider\n\n# The DescopeProvider automatically discovers Descope endpoints\n# and configures JWT token validation\nauth_provider = DescopeProvider(\n    config_url=https://.../.well-known/openid-configuration,        # Your MCP Server .well-known URL\n    base_url=SERVER_URL,                  # Your server's public URL\n)\n\n# Create FastMCP server with auth\nmcp = FastMCP(name=\"My Descope Protected Server\", auth=auth_provider)\n\n```\n\n## Testing\n\nTo test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the environment variables with your actual values!), you can run the following command:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nNow, you can use a FastMCP client to test that you can reach your server after authenticating:\n\n```python\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        assert await client.ping()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Production Configuration\n\nFor production deployments, load configuration from environment variables:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.descope import DescopeProvider\n\n# Load configuration from environment variables\nauth = DescopeProvider(\n    config_url=os.environ.get(\"DESCOPE_CONFIG_URL\"),\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-server.com\")\n)\n\nmcp = FastMCP(name=\"My Descope Protected Server\", auth=auth)\n```\n"
  },
  {
    "path": "docs/integrations/discord.mdx",
    "content": "---\ntitle: Discord OAuth 🤝 FastMCP\nsidebarTitle: Discord\ndescription: Secure your FastMCP server with Discord OAuth\nicon: discord\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.2\" />\n\nThis guide shows you how to secure your FastMCP server using **Discord OAuth**. Since Discord doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Discord's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[Discord Account](https://discord.com/)** with access to create applications\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a Discord Application\n\nCreate an application in the Discord Developer Portal to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to Discord Developer Portal\">\n    Go to the [Discord Developer Portal](https://discord.com/developers/applications).\n\n    Click **\"New Application\"** and give it a name users will recognize (e.g., \"My FastMCP Server\").\n</Step>\n\n<Step title=\"Configure OAuth2 Settings\">\n    In the left sidebar, click **\"OAuth2\"**.\n\n    In the **Redirects** section, click **\"Add Redirect\"** and enter your callback URL:\n    - For development: `http://localhost:8000/auth/callback`\n    - For production: `https://your-domain.com/auth/callback`\n\n    <Warning>\n    The redirect URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. Discord allows `http://localhost` URLs for development. For production, use HTTPS.\n    </Warning>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    On the same OAuth2 page, you'll find:\n\n    - **Client ID**: A numeric string like `12345`\n    - **Client Secret**: Click \"Reset Secret\" to generate one\n\n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `DiscordProvider`, which handles Discord's OAuth flow automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.discord import DiscordProvider\n\nauth_provider = DiscordProvider(\n    client_id=\"12345\",      # Your Discord Application Client ID\n    client_secret=\"your-client-secret\",    # Your Discord OAuth Client Secret\n    base_url=\"http://localhost:8000\",      # Must match your OAuth configuration\n)\n\nmcp = FastMCP(name=\"Discord Secured App\", auth=auth_provider)\n\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated Discord user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n\n    token = get_access_token()\n    return {\n        \"discord_id\": token.claims.get(\"sub\"),\n        \"username\": token.claims.get(\"username\"),\n        \"avatar\": token.claims.get(\"avatar\"),\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Discord OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Discord-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        print(\"✓ Authenticated with Discord!\")\n\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"Discord user: {result['username']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Discord's authorization page\n2. Sign in with your Discord account and authorize the app\n3. After authorization, you'll be redirected back\n4. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Discord Scopes\n\nDiscord OAuth supports several scopes for accessing different types of user data:\n\n| Scope | Description |\n|-------|-------------|\n| `identify` | Access username, avatar, and discriminator (default) |\n| `email` | Access the user's email address |\n| `guilds` | Access the user's list of servers |\n| `guilds.join` | Ability to add the user to a server |\n\nTo request additional scopes:\n\n```python\nauth_provider = DiscordProvider(\n    client_id=\"...\",\n    client_secret=\"...\",\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"identify\", \"email\"],\n)\n```\n\n## Production Configuration\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.discord import DiscordProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\nauth_provider = DiscordProvider(\n    client_id=\"12345\",\n    client_secret=os.environ[\"DISCORD_CLIENT_SECRET\"],\n    base_url=\"https://your-production-domain.com\",\n\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Discord App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n"
  },
  {
    "path": "docs/integrations/eunomia-authorization.mdx",
    "content": "---\ntitle: Eunomia Authorization 🤝 FastMCP\nsidebarTitle: Eunomia Auth\ndescription: Add policy-based authorization to your FastMCP servers with Eunomia\nicon: shield-check\n---\n\nAdd **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Eunomia][eunomia-github] authorization middleware**.\n\nControl which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic JSON-based policies and obtain a comprehensive audit log of all access attempts and violations.\n\n## How it Works\n\nExploiting FastMCP's [Middleware][fastmcp-middleware], the Eunomia middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks.\n\n### Listing Operations\n\nThe middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant EunomiaMiddleware as Eunomia Middleware\n    participant MCPServer as FastMCP Server\n    participant EunomiaServer as Eunomia Server\n\n    MCPClient->>EunomiaMiddleware: MCP Listing Request (e.g., tools/list)\n    EunomiaMiddleware->>MCPServer: MCP Listing Request\n    MCPServer-->>EunomiaMiddleware: MCP Listing Response\n    EunomiaMiddleware->>EunomiaServer: Authorization Checks\n    EunomiaServer->>EunomiaMiddleware: Authorization Decisions\n    EunomiaMiddleware-->>MCPClient: Filtered MCP Listing Response\n```\n\n### Execution Operations\n\nThe middleware behaves as a firewall for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant EunomiaMiddleware as Eunomia Middleware\n    participant MCPServer as FastMCP Server\n    participant EunomiaServer as Eunomia Server\n\n    MCPClient->>EunomiaMiddleware: MCP Execution Request (e.g., tools/call)\n    EunomiaMiddleware->>EunomiaServer: Authorization Check\n    EunomiaServer->>EunomiaMiddleware: Authorization Decision\n    EunomiaMiddleware-->>MCPClient: MCP Unauthorized Error (if denied)\n    EunomiaMiddleware->>MCPServer: MCP Execution Request (if allowed)\n    MCPServer-->>EunomiaMiddleware: MCP Execution Response (if allowed)\n    EunomiaMiddleware-->>MCPClient: MCP Execution Response (if allowed)\n```\n\n## Add Authorization to Your Server\n\n<Note>\nEunomia is an AI-specific authorization server that handles policy decisions. The server runs embedded within your MCP server by default for a zero-effort configuration, but can alternatively be run remotely for centralized policy decisions.\n\n</Note>\n\n### Create a Server with Authorization\n\nFirst, install the `eunomia-mcp` package:\n\n```bash\npip install eunomia-mcp\n```\n\nThen create a FastMCP server and add the Eunomia middleware in one line:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom eunomia_mcp import create_eunomia_middleware\n\n# Create your FastMCP server\nmcp = FastMCP(\"Secure MCP Server 🔒\")\n\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n# Add middleware to your server\nmiddleware = create_eunomia_middleware(policy_file=\"mcp_policies.json\")\nmcp.add_middleware(middleware)\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Configure Access Policies\n\nUse the `eunomia-mcp` CLI in your terminal to manage your authorization policies:\n\n```bash\n# Create a default policy file\neunomia-mcp init\n\n# Or create a policy file customized for your FastMCP server\neunomia-mcp init --custom-mcp \"app.server:mcp\"\n```\n\nThis creates `mcp_policies.json` file that you can further edit to your access control needs.\n\n```bash\n# Once edited, validate your policy file\neunomia-mcp validate mcp_policies.json\n```\n\n### Run the Server\n\nStart your FastMCP server normally:\n\n```bash\npython server.py\n```\n\nThe middleware will now intercept all MCP requests and check them against your policies. Requests include agent identification through headers like `X-Agent-ID`, `X-User-ID`, `User-Agent`, or `Authorization` and an automatic mapping of MCP methods to authorization resources and actions.\n\n<Tip>\n  For detailed policy configuration, custom authentication, and remote\n  deployments, visit the [Eunomia MCP Middleware\n  repository][eunomia-mcp-github].\n</Tip>\n\n[eunomia-github]: https://github.com/whataboutyou-ai/eunomia\n[eunomia-mcp-github]: https://github.com/whataboutyou-ai/eunomia/tree/main/pkgs/extensions/mcp\n[fastmcp-middleware]: /servers/middleware\n"
  },
  {
    "path": "docs/integrations/fastapi.mdx",
    "content": "---\ntitle: FastAPI 🤝 FastMCP\nsidebarTitle: FastAPI\ndescription: Integrate FastMCP with FastAPI applications\nicon: bolt\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nFastMCP provides two powerful ways to integrate with FastAPI applications:\n\n1. **[Generate an MCP server FROM your FastAPI app](#generating-an-mcp-server)** - Convert existing API endpoints into MCP tools\n2. **[Mount an MCP server INTO your FastAPI app](#mounting-an-mcp-server)** - Add MCP functionality to your web application\n\n<Note>\nWhen generating an MCP server from FastAPI, FastMCP uses OpenAPIProvider (v3.0.0+) under the hood to source tools from your FastAPI app's OpenAPI spec. See [Providers](/servers/providers/overview) to understand how FastMCP sources components.\n</Note>\n\n\n<Tip>\nGenerating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters.\n\nWe recommend using the FastAPI integration for bootstrapping and prototyping, not for mirroring your API to LLM clients. See the post [Stop Converting Your REST APIs to MCP](https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp) for more details.\n</Tip>\n\n\n<Note>\nFastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.\n</Note>\n\n## Example FastAPI Application\n\nThroughout this guide, we'll use this e-commerce API as our example (click the `Copy` button to copy it for use with other code blocks):\n\n```python [expandable]\n# Copy this FastAPI server into other code blocks in this guide\n\nfrom fastapi import FastAPI, HTTPException\nfrom pydantic import BaseModel\n\n# Models\nclass Product(BaseModel):\n    name: str\n    price: float\n    category: str\n    description: str | None = None\n\nclass ProductResponse(BaseModel):\n    id: int\n    name: str\n    price: float\n    category: str\n    description: str | None = None\n\n# Create FastAPI app\napp = FastAPI(title=\"E-commerce API\", version=\"1.0.0\")\n\n# In-memory database\nproducts_db = {\n    1: ProductResponse(\n        id=1, name=\"Laptop\", price=999.99, category=\"Electronics\"\n    ),\n    2: ProductResponse(\n        id=2, name=\"Mouse\", price=29.99, category=\"Electronics\"\n    ),\n    3: ProductResponse(\n        id=3, name=\"Desk Chair\", price=299.99, category=\"Furniture\"\n    ),\n}\nnext_id = 4\n\n@app.get(\"/products\", response_model=list[ProductResponse])\ndef list_products(\n    category: str | None = None,\n    max_price: float | None = None,\n) -> list[ProductResponse]:\n    \"\"\"List all products with optional filtering.\"\"\"\n    products = list(products_db.values())\n    if category:\n        products = [p for p in products if p.category == category]\n    if max_price:\n        products = [p for p in products if p.price <= max_price]\n    return products\n\n@app.get(\"/products/{product_id}\", response_model=ProductResponse)\ndef get_product(product_id: int):\n    \"\"\"Get a specific product by ID.\"\"\"\n    if product_id not in products_db:\n        raise HTTPException(status_code=404, detail=\"Product not found\")\n    return products_db[product_id]\n\n@app.post(\"/products\", response_model=ProductResponse)\ndef create_product(product: Product):\n    \"\"\"Create a new product.\"\"\"\n    global next_id\n    product_response = ProductResponse(id=next_id, **product.model_dump())\n    products_db[next_id] = product_response\n    next_id += 1\n    return product_response\n\n@app.put(\"/products/{product_id}\", response_model=ProductResponse)\ndef update_product(product_id: int, product: Product):\n    \"\"\"Update an existing product.\"\"\"\n    if product_id not in products_db:\n        raise HTTPException(status_code=404, detail=\"Product not found\")\n    products_db[product_id] = ProductResponse(\n        id=product_id,\n        **product.model_dump(),\n    )\n    return products_db[product_id]\n\n@app.delete(\"/products/{product_id}\")\ndef delete_product(product_id: int):\n    \"\"\"Delete a product.\"\"\"\n    if product_id not in products_db:\n        raise HTTPException(status_code=404, detail=\"Product not found\")\n    del products_db[product_id]\n    return {\"message\": \"Product deleted\"}\n```\n\n<Tip>\nAll subsequent code examples in this guide assume you have the above FastAPI application code already defined. Each example builds upon this base application, `app`.\n</Tip>\n\n## Generating an MCP Server\n\n<VersionBadge version=\"2.0.0\" />\n\nOne of the most common ways to bootstrap an MCP server is to generate it from an existing FastAPI application. FastMCP will expose your FastAPI endpoints as MCP components (tools, by default) in order to expose your API to LLM clients.\n\n\n\n### Basic Conversion\n\nConvert the FastAPI app to an MCP server with a single line:\n\n```python {5}\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\n\n# Convert to MCP server\nmcp = FastMCP.from_fastapi(app=app)\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Adding Components\n\nYour converted MCP server is a full FastMCP instance, meaning you can add new tools, resources, and other components to it just like you would with any other FastMCP instance.\n\n```python {8-11}\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\n\n# Convert to MCP server\nmcp = FastMCP.from_fastapi(app=app)\n\n# Add a new tool\n@mcp.tool\ndef get_product(product_id: int) -> ProductResponse:\n    \"\"\"Get a product by ID.\"\"\"\n    return products_db[product_id]\n\n# Run the MCP server\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n\n\n\n\n### Interacting with the MCP Server\n\nOnce you've converted your FastAPI app to an MCP server, you can interact with it using the FastMCP client to test functionality before deploying it to an LLM-based application.\n\n```python {3, }\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nimport asyncio\n\n# Convert to MCP server\nmcp = FastMCP.from_fastapi(app=app)\n\nasync def demo():\n    async with Client(mcp) as client:\n        # List available tools\n        tools = await client.list_tools()\n        print(f\"Available tools: {[t.name for t in tools]}\")\n        \n        # Create a product\n        result = await client.call_tool(\n            \"create_product_products_post\",\n            {\n                \"name\": \"Wireless Keyboard\",\n                \"price\": 79.99,\n                \"category\": \"Electronics\",\n                \"description\": \"Bluetooth mechanical keyboard\"\n            }\n        )\n        print(f\"Created product: {result.data}\")\n        \n        # List electronics under $100\n        result = await client.call_tool(\n            \"list_products_products_get\",\n            {\"category\": \"Electronics\", \"max_price\": 100}\n        )\n        print(f\"Affordable electronics: {result.data}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(demo())\n```\n\n### Custom Route Mapping\n\nBecause FastMCP's FastAPI integration is based on its [OpenAPI integration](/integrations/openapi), you can customize how endpoints are converted to MCP components in exactly the same way. For example, here we use a `RouteMap` to map all GET requests to MCP resources, and all POST/PUT/DELETE requests to MCP tools:\n\n```python\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\n# Custom mapping rules\nmcp = FastMCP.from_fastapi(\n    app=app,\n    route_maps=[\n        # GET with path params → ResourceTemplates\n        RouteMap(\n            methods=[\"GET\"], \n            pattern=r\".*\\{.*\\}.*\", \n            mcp_type=MCPType.RESOURCE_TEMPLATE\n        ),\n        # Other GETs → Resources\n        RouteMap(\n            methods=[\"GET\"], \n            pattern=r\".*\", \n            mcp_type=MCPType.RESOURCE\n        ),\n        # POST/PUT/DELETE → Tools (default)\n    ],\n)\n\n# Now:\n# - GET /products → Resource\n# - GET /products/{id} → ResourceTemplate\n# - POST/PUT/DELETE → Tools\n```\n\n<Tip>\nTo learn more about customizing the conversion process, see the [OpenAPI Integration guide](/integrations/openapi).\n</Tip>\n\n### Authentication and Headers\n\nYou can configure headers and other client options via the `httpx_client_kwargs` parameter. For example, to add authentication to your FastAPI app, you can pass a `headers` dictionary to the `httpx_client_kwargs` parameter:\n\n```python {27-31}\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\n\n# Add authentication to your FastAPI app\nfrom fastapi import Depends, Header\nfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials\n\nsecurity = HTTPBearer()\n\ndef verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):\n    if credentials.credentials != \"secret-token\":\n        raise HTTPException(status_code=401, detail=\"Invalid authentication\")\n    return credentials.credentials\n\n# Add a protected endpoint\n@app.get(\"/admin/stats\", dependencies=[Depends(verify_token)])\ndef get_admin_stats():\n    return {\n        \"total_products\": len(products_db),\n        \"categories\": list(set(p.category for p in products_db.values()))\n    }\n\n# Create MCP server with authentication headers\nmcp = FastMCP.from_fastapi(\n    app=app,\n    httpx_client_kwargs={\n        \"headers\": {\n            \"Authorization\": \"Bearer secret-token\",\n        }\n    }\n)\n```\n\n## Mounting an MCP Server\n\n<VersionBadge version=\"2.3.1\" />\n\nIn addition to generating servers, FastMCP can facilitate adding MCP servers to your existing FastAPI application. You can do this by mounting the MCP ASGI application.\n\n### Basic Mounting\n\nTo mount an MCP server, you can use the `http_app` method on your FastMCP instance. This will return an ASGI application that can be mounted to your FastAPI application.\n\n```python {23-30}\nfrom fastmcp import FastMCP\nfrom fastapi import FastAPI\n\n# Create MCP server\nmcp = FastMCP(\"Analytics Tools\")\n\n@mcp.tool\ndef analyze_pricing(category: str) -> dict:\n    \"\"\"Analyze pricing for a category.\"\"\"\n    products = [p for p in products_db.values() if p.category == category]\n    if not products:\n        return {\"error\": f\"No products in {category}\"}\n    \n    prices = [p.price for p in products]\n    return {\n        \"category\": category,\n        \"avg_price\": round(sum(prices) / len(prices), 2),\n        \"min\": min(prices),\n        \"max\": max(prices),\n    }\n\n# Create ASGI app from MCP server\nmcp_app = mcp.http_app(path='/mcp')\n\n# Key: Pass lifespan to FastAPI\napp = FastAPI(title=\"E-commerce API\", lifespan=mcp_app.lifespan)\n\n# Mount the MCP server\napp.mount(\"/analytics\", mcp_app)\n\n# Now: API at /products/*, MCP at /analytics/mcp/\n```\n\n## Offering an LLM-Friendly API\n\nA common pattern is to generate an MCP server from your FastAPI app and serve both interfaces from the same application. This provides an LLM-optimized interface alongside your regular API:\n\n```python\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\nfrom fastapi import FastAPI\n\n# 1. Generate MCP server from your API\nmcp = FastMCP.from_fastapi(app=app, name=\"E-commerce MCP\")\n\n# 2. Create the MCP's ASGI app\nmcp_app = mcp.http_app(path='/mcp')\n\n# 3. Create a new FastAPI app that combines both sets of routes\ncombined_app = FastAPI(\n    title=\"E-commerce API with MCP\",\n    routes=[\n        *mcp_app.routes,  # MCP routes\n        *app.routes,      # Original API routes\n    ],\n    lifespan=mcp_app.lifespan,\n)\n\n# Now you have:\n# - Regular API: http://localhost:8000/products\n# - LLM-friendly MCP: http://localhost:8000/mcp\n# Both served from the same FastAPI application!\n```\n\nThis approach lets you maintain a single codebase while offering both traditional REST endpoints and MCP-compatible endpoints for LLM clients.\n\n## Key Considerations\n\n### Operation IDs\n\nFastAPI operation IDs become MCP component names. Always specify meaningful operation IDs:\n\n```python\n# Good - explicit operation_id\n@app.get(\"/users/{user_id}\", operation_id=\"get_user_by_id\")\ndef get_user(user_id: int):\n    return {\"id\": user_id}\n\n# Less ideal - auto-generated name\n@app.get(\"/users/{user_id}\")\ndef get_user(user_id: int):\n    return {\"id\": user_id}\n```\n\n### Lifespan Management\n\nWhen mounting MCP servers, always pass the lifespan context:\n\n```python\n# Correct - lifespan passed, path=\"/\" since we mount at /mcp\nmcp_app = mcp.http_app(path=\"/\")\napp = FastAPI(lifespan=mcp_app.lifespan)\napp.mount(\"/mcp\", mcp_app)  # MCP endpoint at /mcp\n\n# Incorrect - missing lifespan\napp = FastAPI()\napp.mount(\"/mcp\", mcp.http_app(path=\"/\"))  # Session manager won't initialize\n```\n\nIf you're mounting an authenticated MCP server under a path prefix, see [Mounting Authenticated Servers](/deployment/http#mounting-authenticated-servers) for important OAuth routing considerations.\n\n### CORS Middleware\n\nIf your FastAPI app uses `CORSMiddleware` and you're mounting an OAuth-protected FastMCP server, avoid adding application-wide CORS middleware. FastMCP and the MCP SDK already handle CORS for OAuth routes, and layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).\n\nIf you need CORS on your own FastAPI routes, use the sub-app pattern: mount your API and FastMCP as separate apps, each with their own middleware, rather than adding top-level `CORSMiddleware` to the combined application.\n\n### Combining Lifespans\n\nIf your FastAPI app already has a lifespan (for database connections, startup tasks, etc.), you can't simply replace it with the MCP lifespan. Use `combine_lifespans` to run both:\n\n```python\nfrom fastapi import FastAPI\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.lifespan import combine_lifespans\nfrom contextlib import asynccontextmanager\n\n# Your existing lifespan\n@asynccontextmanager\nasync def app_lifespan(app: FastAPI):\n    print(\"Starting up the app...\")\n    yield\n    print(\"Shutting down the app...\")\n\n# Create MCP server\nmcp = FastMCP(\"Tools\")\nmcp_app = mcp.http_app(path=\"/\")\n\n# Combine both lifespans\napp = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))\napp.mount(\"/mcp\", mcp_app)  # MCP endpoint at /mcp\n```\n\n`combine_lifespans` enters lifespans in order and exits in reverse order.\n\n### Performance Tips\n\n1. **Use in-memory transport for testing** - Pass MCP servers directly to clients\n2. **Design purpose-built MCP tools** - Better than auto-converting complex APIs\n3. **Keep tool parameters simple** - LLMs perform better with focused interfaces\n\nFor more details on configuration options, see the [OpenAPI Integration guide](/integrations/openapi)."
  },
  {
    "path": "docs/integrations/gemini-cli.mdx",
    "content": "---\ntitle: Gemini CLI 🤝 FastMCP\nsidebarTitle: Gemini CLI\ndescription: Install and use FastMCP servers in Gemini CLI\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\n[Gemini CLI](https://geminicli.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers.\n\n## Requirements\n\nThis integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Gemini CLI's built-in MCP management commands.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.13.0\" />\n\nThe easiest way to install a FastMCP server in Gemini CLI is using the `fastmcp install gemini-cli` command. This automatically handles the configuration, dependency management, and calls Gemini CLI's built-in MCP management system.\n\n```bash\nfastmcp install gemini-cli server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install gemini-cli server.py\nfastmcp install gemini-cli server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install gemini-cli server.py:my_custom_server\n```\n\nThe command will automatically configure the server with Gemini CLI's `gemini mcp add` command.\n\n#### Dependencies\n\nFastMCP provides flexible dependency management options for your Gemini CLI servers:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install gemini-cli server.py --with pandas --with requests\n```\n\n**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:\n\n```bash\nfastmcp install gemini-cli server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install gemini-cli server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Configuration\n\nControl the Python environment for your server with these options:\n\n**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:\n\n```bash\nfastmcp install gemini-cli server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:\n\n```bash\nfastmcp install gemini-cli server.py --project /path/to/my-project\n```\n\n#### Environment Variables\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install gemini-cli server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install gemini-cli server.py --server-name \"Weather Server\" --env-file .env\n```\n\n<Warning>\n**Gemini CLI must be installed**. The integration looks for the Gemini CLI and uses the `gemini mcp add` command to register servers.\n</Warning>\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually use Gemini CLI's built-in MCP management commands. This gives you direct control over how your server is launched:\n\n```bash\n# Add a server with custom configuration\ngemini mcp add dice-roller uv -- run --with fastmcp fastmcp run server.py\n\n# Add with environment variables\ngemini mcp add weather-server -e API_KEY=secret -e DEBUG=true uv -- run --with fastmcp fastmcp run server.py\n\n# Add with specific scope (user, or project)\ngemini mcp add my-server --scope user uv -- run --with fastmcp fastmcp run server.py\n```\n\nYou can also manually specify Python versions and project directories in your Gemini CLI commands:\n\n```bash\n# With specific Python version\ngemini mcp add ml-server uv -- run --python 3.11 --with fastmcp fastmcp run server.py\n\n# Within a project directory\ngemini mcp add project-server uv -- run --project /path/to/project --with fastmcp fastmcp run server.py\n```\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Gemini CLI.\n\nTry asking Gemini something like:\n\n> \"Roll some dice for me\"\n\nGemini will automatically detect your `roll_dice` tool and use it to fulfill your request.\n\nGemini CLI can now access all the tools and prompts you've defined in your FastMCP server. \n\nIf your server provides prompts, you can use them as slash commands with `/prompt_name`.\n"
  },
  {
    "path": "docs/integrations/gemini.mdx",
    "content": "---\ntitle: Gemini SDK 🤝 FastMCP\nsidebarTitle: Gemini SDK\ndescription: Connect FastMCP servers to the Google Gemini SDK\nicon: message-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nGoogle's Gemini API includes built-in support for MCP servers in their Python and JavaScript SDKs, allowing you to connect directly to MCP servers and use their tools seamlessly with Gemini models.\n\n## Gemini Python SDK\n\nGoogle's [Gemini Python SDK](https://ai.google.dev/gemini-api/docs) can use FastMCP clients directly.\n\n<Note>\nGoogle's MCP integration is currently experimental and available in the Python and JavaScript SDKs. The API automatically calls MCP tools when needed and can connect to both local and remote MCP servers.\n</Note>\n\n<Tip>\nCurrently, Gemini's MCP support only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to the AI. Other MCP features like resources and prompts are not currently supported.\n</Tip>\n\n### Create a Server\n\nFirst, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Call the Server\n\n\nTo use the Gemini API with MCP, you'll need to install the Google Generative AI SDK:\n\n```bash\npip install google-genai\n```\n\nYou'll also need to authenticate with Google. You can do this by setting the `GEMINI_API_KEY` environment variable. Consult the Gemini SDK documentation for more information.\n\n```bash\nexport GEMINI_API_KEY=\"your-api-key\"\n```\n\nGemini's SDK interacts directly with the MCP client session. To call the server, you'll need to instantiate a FastMCP client, enter its connection context, and pass the client session to the Gemini SDK.\n\n```python {5, 9, 15}\nfrom fastmcp import Client\nfrom google import genai\nimport asyncio\n\nmcp_client = Client(\"server.py\")\ngemini_client = genai.Client()\n\nasync def main():    \n    async with mcp_client:\n        response = await gemini_client.aio.models.generate_content(\n            model=\"gemini-2.0-flash\",\n            contents=\"Roll 3 dice!\",\n            config=genai.types.GenerateContentConfig(\n                temperature=0,\n                tools=[mcp_client.session],  # Pass the FastMCP client session\n            ),\n        )\n        print(response.text)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIf you run this code, you'll see output like:\n\n```text\nOkay, I rolled 3 dice and got a 5, 4, and 1.\n```\n\n### Remote & Authenticated Servers\n\nIn the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/clients/transports) or [auth](/clients/auth) method supported by FastMCP, simply by changing the client configuration.\n\nFor example, to connect to a remote, authenticated server, you can use the following client:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import BearerAuth\n\nmcp_client = Client(\n    \"https://my-server.com/mcp/\",\n    auth=BearerAuth(\"<your-token>\"),\n)\n```\n\nThe rest of the code remains the same.\n\n\n"
  },
  {
    "path": "docs/integrations/github.mdx",
    "content": "---\ntitle: GitHub OAuth 🤝 FastMCP\nsidebarTitle: GitHub\ndescription: Secure your FastMCP server with GitHub OAuth\nicon: github\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nThis guide shows you how to secure your FastMCP server using **GitHub OAuth**. Since GitHub doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge GitHub's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[GitHub Account](https://github.com/)** with access to create OAuth Apps\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a GitHub OAuth App\n\nCreate an OAuth App in your GitHub settings to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to OAuth Apps\">\n    Go to **Settings → Developer settings → OAuth Apps** in your GitHub account, or visit [github.com/settings/developers](https://github.com/settings/developers).\n    \n    Click **\"New OAuth App\"** to create a new application.\n</Step>\n\n<Step title=\"Configure Your OAuth App\">\n    Fill in the application details:\n    \n    - **Application name**: Choose a name users will recognize (e.g., \"My FastMCP Server\")\n    - **Homepage URL**: Your application's homepage or documentation URL\n    - **Authorization callback URL**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    \n    <Warning>\n    The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, GitHub allows `http://localhost` URLs. For production, you must use HTTPS.\n    </Warning>\n    \n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHubProvider.\n    </Tip>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After creating the app, you'll see:\n    \n    - **Client ID**: A public identifier like `Ov23liAbcDefGhiJkLmN`\n    - **Client Secret**: Click \"Generate a new client secret\" and save the value securely\n    \n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `GitHubProvider`, which handles GitHub's OAuth quirks automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\n# The GitHubProvider handles GitHub's token format and validation\nauth_provider = GitHubProvider(\n    client_id=\"Ov23liAbcDefGhiJkLmN\",  # Your GitHub OAuth App Client ID\n    client_secret=\"github_pat_...\",     # Your GitHub OAuth App Client Secret\n    base_url=\"http://localhost:8000\",   # Must match your OAuth App configuration\n    # redirect_path=\"/auth/callback\"   # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"GitHub Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated GitHub user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n    \n    token = get_access_token()\n    # The GitHubProvider stores user data in token claims\n    return {\n        \"github_user\": token.claims.get(\"login\"),\n        \"name\": token.claims.get(\"name\"),\n        \"email\": token.claims.get(\"email\")\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by GitHub OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your GitHub-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle GitHub OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open GitHub login in your browser\n        print(\"✓ Authenticated with GitHub!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"GitHub user: {result['github_user']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to GitHub's authorization page\n2. After you authorize the app, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = GitHubProvider(\n    client_id=\"Ov23liAbcDefGhiJkLmN\",\n    client_secret=\"github_pat_...\",\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production GitHub App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n"
  },
  {
    "path": "docs/integrations/google.mdx",
    "content": "---\ntitle: Google OAuth 🤝 FastMCP\nsidebarTitle: Google\ndescription: Secure your FastMCP server with Google OAuth\nicon: google\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nThis guide shows you how to secure your FastMCP server using **Google OAuth**. Since Google doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Google's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[Google Cloud Account](https://console.cloud.google.com/)** with access to create OAuth 2.0 Client IDs\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a Google OAuth 2.0 Client ID\n\nCreate an OAuth 2.0 Client ID in your Google Cloud Console to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to OAuth Consent Screen\">\n    Go to the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) and select your project (or create a new one).\n    \n    First, configure the OAuth consent screen by navigating to **APIs & Services → OAuth consent screen**. Choose \"External\" for testing or \"Internal\" for G Suite organizations.\n</Step>\n\n<Step title=\"Create OAuth 2.0 Client ID\">\n    Navigate to **APIs & Services → Credentials** and click **\"+ CREATE CREDENTIALS\"** → **\"OAuth client ID\"**.\n    \n    Configure your OAuth client:\n    \n    - **Application type**: Web application\n    - **Name**: Choose a descriptive name (e.g., \"FastMCP Server\")\n    - **Authorized JavaScript origins**: Add your server's base URL (e.g., `http://localhost:8000`)\n    - **Authorized redirect URIs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    \n    <Warning>\n    The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Google allows `http://localhost` URLs with various ports. For production, you must use HTTPS.\n    </Warning>\n    \n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the GoogleProvider.\n    </Tip>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After creating the client, you'll receive:\n    \n    - **Client ID**: A string ending in `.apps.googleusercontent.com`\n    - **Client Secret**: A string starting with `GOCSPX-`\n    \n    Download the JSON credentials or copy these values securely.\n    \n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `GoogleProvider`, which handles Google's OAuth flow automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.google import GoogleProvider\n\n# The GoogleProvider handles Google's token format and validation\nauth_provider = GoogleProvider(\n    client_id=\"123456789.apps.googleusercontent.com\",  # Your Google OAuth Client ID\n    client_secret=\"GOCSPX-abc123...\",                  # Your Google OAuth Client Secret\n    base_url=\"http://localhost:8000\",                  # Must match your OAuth configuration\n    required_scopes=[                                  # Request user information\n        \"openid\",\n        \"https://www.googleapis.com/auth/userinfo.email\",\n    ],\n    # redirect_path=\"/auth/callback\"                  # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"Google Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated Google user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n    \n    token = get_access_token()\n    # The GoogleProvider stores user data in token claims\n    return {\n        \"google_id\": token.claims.get(\"sub\"),\n        \"email\": token.claims.get(\"email\"),\n        \"name\": token.claims.get(\"name\"),\n        \"picture\": token.claims.get(\"picture\"),\n        \"locale\": token.claims.get(\"locale\")\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Google OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Google-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Google OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Google login in your browser\n        print(\"✓ Authenticated with Google!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"Google user: {result['email']}\")\n        print(f\"Name: {result['name']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Google's authorization page\n2. Sign in with your Google account and grant the requested permissions\n3. After authorization, you'll be redirected back\n4. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.google import GoogleProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = GoogleProvider(\n    client_id=\"123456789.apps.googleusercontent.com\",\n    client_secret=\"GOCSPX-abc123...\",\n    base_url=\"https://your-production-domain.com\",\n    required_scopes=[\"openid\", \"https://www.googleapis.com/auth/userinfo.email\"],\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Google App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>"
  },
  {
    "path": "docs/integrations/goose.mdx",
    "content": "---\ntitle: Goose 🤝 FastMCP\nsidebarTitle: Goose\ndescription: Install and use FastMCP servers in Goose\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\n[Goose](https://block.github.io/goose/) is an open-source AI agent from Block that supports MCP servers as extensions. FastMCP can install your server directly into Goose using its deeplink protocol — one command opens Goose with an install dialog ready to go.\n\n## Requirements\n\nThis integration uses Goose's deeplink protocol to register your server as a STDIO extension running via `uvx`. You must have Goose installed on your system for the deeplink to open automatically.\n\nFor remote deployments, configure your FastMCP server with HTTP transport and add it to Goose directly using `goose configure` or the config file.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"3.0.0\" />\n\nThe easiest way to install a FastMCP server in Goose is using the `fastmcp install goose` command. This generates a `goose://` deeplink and opens it, prompting Goose to install the server.\n\n```bash\nfastmcp install goose server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install goose server.py\nfastmcp install goose server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install goose server.py:my_custom_server\n```\n\nUnder the hood, the generated command uses `uvx` to run your server in an isolated environment. Goose requires `uvx` rather than `uv run`, so the install produces a command like:\n\n```bash\nuvx --with pandas fastmcp run /path/to/server.py\n```\n\n#### Dependencies\n\nUse the `--with` flag to specify additional packages your server needs:\n\n```bash\nfastmcp install goose server.py --with pandas --with requests\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n#### Python Version\n\nUse `--python` to specify which Python version your server should use:\n\n```bash\nfastmcp install goose server.py --python 3.11\n```\n\n<Note>\nThe Goose install uses `uvx`, which does not support `--project`, `--with-requirements`, or `--with-editable`. If you need these options, use `fastmcp install mcp-json` to generate a full configuration and add it to Goose manually.\n</Note>\n\n#### Environment Variables\n\nGoose's deeplink protocol does not support environment variables. If your server needs them (like API keys), you have two options:\n\n1. **Configure after install**: Run `goose configure` and add environment variables to the extension.\n2. **Manual config**: Use `fastmcp install mcp-json` to generate the full configuration, then add it to `~/.config/goose/config.yaml` with the `envs` field.\n\n### Manual Configuration\n\nFor more control, you can manually edit Goose's configuration file at `~/.config/goose/config.yaml`:\n\n```yaml\nextensions:\n  dice-roller:\n    name: Dice Roller\n    cmd: uvx\n    args: [fastmcp, run, /path/to/server.py]\n    enabled: true\n    type: stdio\n    timeout: 300\n```\n\n#### Dependencies\n\nWhen manually configuring, add packages using `--with` flags in the args:\n\n```yaml\nextensions:\n  dice-roller:\n    name: Dice Roller\n    cmd: uvx\n    args: [--with, pandas, --with, requests, fastmcp, run, /path/to/server.py]\n    enabled: true\n    type: stdio\n    timeout: 300\n```\n\n#### Environment Variables\n\nEnvironment variables can be specified in the `envs` field:\n\n```yaml\nextensions:\n  weather-server:\n    name: Weather Server\n    cmd: uvx\n    args: [fastmcp, run, /path/to/weather_server.py]\n    enabled: true\n    envs:\n      API_KEY: your-api-key\n      DEBUG: \"true\"\n    type: stdio\n    timeout: 300\n```\n\nYou can also use `goose configure` to add extensions interactively, which prompts for environment variables.\n\n<Warning>\n**`uvx` (from `uv`) must be installed and available in your system PATH**. Goose uses `uvx` to run Python-based extensions in isolated environments.\n</Warning>\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Goose.\n\nTry asking Goose something like:\n\n> \"Roll some dice for me\"\n\nGoose will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:\n\n> 🎲 Here are your dice rolls: 4, 6, 4\n>\n> You rolled 3 dice with a total of 14!\n\nGoose can now access all the tools, resources, and prompts you've defined in your FastMCP server.\n"
  },
  {
    "path": "docs/integrations/mcp-json-configuration.mdx",
    "content": "---\ntitle: MCP JSON Configuration 🤝 FastMCP\nsidebarTitle: MCP.json\ndescription: Generate standard MCP configuration files for any compatible client\nicon: brackets-curly\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.10.3\" />\n\nFastMCP can generate standard MCP JSON configuration files that work with any MCP-compatible client including Claude Desktop, VS Code, Cursor, and other applications that support the Model Context Protocol.\n\n## MCP JSON Configuration Standard\n\nThe MCP JSON configuration format is an **emergent standard** that has developed across the MCP ecosystem. This format defines how MCP clients should configure and launch MCP servers, providing a consistent way to specify server commands, arguments, and environment variables.\n\n### Configuration Structure\n\nThe standard uses a `mcpServers` object where each key represents a server name and the value contains the server's configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"server-name\": {\n      \"command\": \"executable\",\n      \"args\": [\"arg1\", \"arg2\"],\n      \"env\": {\n        \"VAR\": \"value\"\n      }\n    }\n  }\n}\n```\n\n### Server Configuration Fields\n\n#### `command` (required)\nThe executable command to run the MCP server. This should be an absolute path or a command available in the system PATH.\n\n```json\n{\n  \"command\": \"python\"\n}\n```\n\n#### `args` (optional)\nAn array of command-line arguments passed to the server executable. Arguments are passed in order.\n\n```json\n{\n  \"args\": [\"server.py\", \"--verbose\", \"--port\", \"8080\"]\n}\n```\n\n#### `env` (optional)\nAn object containing environment variables to set when launching the server. All values must be strings.\n\n```json\n{\n  \"env\": {\n    \"API_KEY\": \"secret-key\",\n    \"DEBUG\": \"true\",\n    \"PORT\": \"8080\"\n  }\n}\n```\n\n### Client Adoption\n\nThis format is widely adopted across the MCP ecosystem:\n\n- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`\n- **Cursor**: Uses `~/.cursor/mcp.json`\n- **VS Code**: Uses workspace `.vscode/mcp.json`\n- **Other clients**: Many MCP-compatible applications follow this standard\n\n## Overview\n\n<Note>\n**For the best experience, use FastMCP's first-class integrations:** [`fastmcp install claude-code`](/integrations/claude-code), [`fastmcp install claude-desktop`](/integrations/claude-desktop), or [`fastmcp install cursor`](/integrations/cursor). Use MCP JSON generation for advanced use cases and unsupported clients.\n</Note>\n\nThe `fastmcp install mcp-json` command generates configuration in the standard `mcpServers` format used across the MCP ecosystem. This is useful when:\n\n- **Working with unsupported clients** - Any MCP client not directly integrated with FastMCP\n- **CI/CD environments** - Automated configuration generation for deployments  \n- **Configuration sharing** - Easy distribution of server setups to team members\n- **Custom tooling** - Integration with your own MCP management tools\n- **Manual setup** - When you prefer to manually configure your MCP client\n\n## Basic Usage\n\nGenerate configuration and output to stdout (useful for piping):\n\n```bash\nfastmcp install mcp-json server.py\n```\n\nThis outputs the server configuration JSON with the server name as the root key:\n\n```json\n{\n  \"My Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\", \n      \"fastmcp\",\n      \"run\",\n      \"/absolute/path/to/server.py\"\n    ]\n  }\n}\n```\n\nTo use this in a client configuration file, add it to the `mcpServers` object in your client's configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"My Server\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--with\",\n        \"fastmcp\", \n        \"fastmcp\",\n        \"run\",\n        \"/absolute/path/to/server.py\"\n      ]\n    }\n  }\n}\n```\n\n<Note>\nWhen using `--python`, `--project`, or `--with-requirements`, the generated configuration will include these options in the `uv run` command, ensuring your server runs with the correct Python version and dependencies.\n</Note>\n\n<Note>\nDifferent MCP clients may have specific configuration requirements or formatting needs. Always consult your client's documentation to ensure proper integration.\n</Note>\n\n## Configuration Options\n\n### Server Naming\n\n```bash\n# Use server's built-in name (from FastMCP constructor)\nfastmcp install mcp-json server.py\n\n# Override with custom name\nfastmcp install mcp-json server.py --name \"Custom Server Name\"\n```\n\n### Dependencies\n\nAdd Python packages your server needs:\n\n```bash\n# Single package\nfastmcp install mcp-json server.py --with pandas\n\n# Multiple packages  \nfastmcp install mcp-json server.py --with pandas --with requests --with httpx\n\n# Editable local package\nfastmcp install mcp-json server.py --with-editable ./my-package\n\n# From requirements file\nfastmcp install mcp-json server.py --with-requirements requirements.txt\n```\n\nYou can also use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"matplotlib\", \"seaborn\"]\n  }\n}\n```\n\nThen simply install with:\n```bash\nfastmcp install mcp-json fastmcp.json\n```\n\n\n### Environment Variables\n\n```bash\n# Individual environment variables\nfastmcp install mcp-json server.py \\\n  --env API_KEY=your-secret-key \\\n  --env DEBUG=true\n\n# Load from .env file\nfastmcp install mcp-json server.py --env-file .env\n```\n\n### Python Version and Project Directory\n\nSpecify Python version or run within a specific project:\n\n```bash\n# Use specific Python version\nfastmcp install mcp-json server.py --python 3.11\n\n# Run within a project directory\nfastmcp install mcp-json server.py --project /path/to/project\n```\n\n### Server Object Selection\n\nUse the same `file.py:object` notation as other FastMCP commands:\n\n```bash\n# Auto-detects server object (looks for 'mcp', 'server', or 'app')\nfastmcp install mcp-json server.py\n\n# Explicit server object\nfastmcp install mcp-json server.py:my_custom_server\n```\n\n## Clipboard Integration\n\nCopy configuration directly to your clipboard for easy pasting:\n\n```bash\nfastmcp install mcp-json server.py --copy\n```\n\n<Note>\nThe `--copy` flag requires the `pyperclip` Python package. If not installed, you'll see an error message with installation instructions.\n</Note>\n\n## Usage Examples\n\n### Basic Server\n\n```bash\nfastmcp install mcp-json dice_server.py\n```\n\nOutput:\n```json\n{\n  \"Dice Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\",\n      \"fastmcp\", \n      \"run\",\n      \"/home/user/dice_server.py\"\n    ]\n  }\n}\n```\n\n### Production Server with Dependencies\n\n```bash\nfastmcp install mcp-json api_server.py \\\n  --name \"Production API Server\" \\\n  --with requests \\\n  --with python-dotenv \\\n  --env API_BASE_URL=https://api.example.com \\\n  --env TIMEOUT=30\n```\n\n### Advanced Configuration\n\n```bash\nfastmcp install mcp-json ml_server.py \\\n  --name \"ML Analysis Server\" \\\n  --python 3.11 \\\n  --with-requirements requirements.txt \\\n  --project /home/user/ml-project \\\n  --env GPU_DEVICE=0\n```\n\nOutput:\n```json\n{\n  \"Production API Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\",\n      \"--with\",\n      \"python-dotenv\", \n      \"--with\",\n      \"requests\",\n      \"fastmcp\",\n      \"run\", \n      \"/home/user/api_server.py\"\n    ],\n    \"env\": {\n      \"API_BASE_URL\": \"https://api.example.com\",\n      \"TIMEOUT\": \"30\"\n    }\n  }\n}\n```\n\nThe advanced configuration example generates:\n```json\n{\n  \"ML Analysis Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--python\",\n      \"3.11\",\n      \"--project\",\n      \"/home/user/ml-project\",\n      \"--with\",\n      \"fastmcp\",\n      \"--with-requirements\",\n      \"requirements.txt\",\n      \"fastmcp\",\n      \"run\",\n      \"/home/user/ml_server.py\"\n    ],\n    \"env\": {\n      \"GPU_DEVICE\": \"0\"\n    }\n  }\n}\n```\n\n### Pipeline Usage\n\nSave configuration to file:\n\n```bash\nfastmcp install mcp-json server.py > mcp-config.json\n```\n\nUse in shell scripts:\n\n```bash\n#!/bin/bash\nCONFIG=$(fastmcp install mcp-json server.py --name \"CI Server\")\necho \"$CONFIG\" | jq '.\"CI Server\".command'\n# Output: \"uv\"\n```\n\n## Integration with MCP Clients\n\nThe generated configuration works with any MCP-compatible application:\n\n### Claude Desktop\n<Note>\n**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.\n</Note>\nCopy the `mcpServers` object into `~/.claude/claude_desktop_config.json`\n\n### Cursor\n<Note>\n**Prefer [`fastmcp install cursor`](/integrations/cursor)** for automatic installation. Use MCP JSON for advanced configuration needs.\n</Note>\nAdd to `~/.cursor/mcp.json`\n\n### VS Code  \nAdd to your workspace's `.vscode/mcp.json` file\n\n### Custom Applications\nUse the JSON configuration with any application that supports the MCP protocol\n\n## Configuration Format\n\nThe generated configuration outputs a server object with the server name as the root key:\n\n```json\n{\n  \"<server-name>\": {\n    \"command\": \"<executable>\",\n    \"args\": [\"<arg1>\", \"<arg2>\", \"...\"],\n    \"env\": {\n      \"<ENV_VAR>\": \"<value>\"\n    }\n  }\n}\n```\n\nTo use this in an MCP client, add it to the client's `mcpServers` configuration object.\n\n**Fields:**\n- `command`: The executable to run (always `uv` for FastMCP servers)\n- `args`: Command-line arguments including dependencies and server path\n- `env`: Environment variables (only included if specified)\n\n<Warning>\n**All file paths in the generated configuration are absolute paths**. This ensures the configuration works regardless of the working directory when the MCP client starts the server.\n</Warning>\n\n## Requirements\n\n- **uv**: Must be installed and available in your system PATH\n- **pyperclip** (optional): Required only for `--copy` functionality\n\nInstall uv if not already available:\n\n```bash\n# macOS\nbrew install uv\n\n# Linux/Windows  \ncurl -LsSf https://astral.sh/uv/install.sh | sh\n```\n"
  },
  {
    "path": "docs/integrations/oci.mdx",
    "content": "---\ntitle: OCI IAM OAuth 🤝 FastMCP\nsidebarTitle: Oracle\ndescription: Secure your FastMCP server with OCI IAM OAuth\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nThis guide shows you how to secure your FastMCP server using **OCI IAM OAuth**. Since OCI IAM doesn't support Dynamic Client Registration, this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge OCI's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\n1. An OCI cloud Account with access to create an Integrated Application in an Identity Domain.\n2. Your FastMCP server's URL (For dev environments, it is http://localhost:8000. For PROD environments, it could be https://mcp.yourdomain.com)\n\n### Step 1: Make sure client access is enabled for JWK's URL\n\n<Steps>\n<Step title=\"Navigate to OCI IAM Domain Settings\">\n\n    Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).\n    From \"Identity & Security\" menu, open Domains page.\n    On the Domains list page, select the domain that you are using for MCP Authentication.\n    Open Settings tab. \n    Click on \"Edit Domain Settings\" button.\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ocieditdomainsettingsbutton.png\" alt=\"OCI console showing the Edit Domain Settings button in the IAM Domain settings page\" />\n    </Frame>\n</Step>\n\n<Step title=\"Update Domain Setting\">\n\n    Enable \"Configure client access\" checkbox as shown in the screenshot.\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ocieditdomainsettings.png\" alt=\"OCI IAM Domain Settings\" />\n    </Frame>\n</Step>\n</Steps>\n\n### Step 2: Create OAuth client for MCP server authentication\n\nFollow the Steps as mentioned below to create an OAuth client.\n\n<Steps>\n<Step title=\"Navigate to OCI IAM Integrated Applications\">\n\n    Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).\n    From \"Identity & Security\" menu, open Domains page.\n    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).\n    On the details page, select Integrated applications. A list of applications in the domain is displayed.\n</Step>\n\n<Step title=\"Add an Integrated Application\">\n\n    Select Add application.\n    In the Add application window, select Confidential Application.\n    Select Launch workflow.\n    In the Add application details page, Enter name and description as shown below.\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ociaddapplication.png\" alt=\"Adding a Confidential Integrated Application in OCI IAM Domain\" />\n    </Frame>\n</Step>\n\n<Step title=\"Update OAuth Configuration for an Integrated Application\">\n\n    Once the Integrated Application is created, Click on \"OAuth configuration\" tab.\n    Click on \"Edit OAuth configuration\" button.\n    Configure the application as OAuth client by selecting \"Configure this application as a client now\" radio button.\n    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.\n    For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by \"/oauth/callback\".\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ocioauthconfiguration.png\" alt=\"OAuth Configuration for an Integrated Application in OCI IAM Domain\" />\n    </Frame>\n</Step>\n\n<Step title=\"Activate the Integrated Application\">\n\n    Click on \"Submit\" button to update OAuth configuration for the client application. \n    **Note: You don't need to do any special configuration to support PKCE for the OAuth client.**\n    Make sure to Activate the client application.\n    Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in your code.\n</Step>\n</Steps>\n\nThis is all you need to implement MCP server authentication against OCI IAM. However, you may want to use an authenticated user token to invoke OCI control plane APIs and propagate identity to the OCI control plane instead of using a service user account. In that case, you need to implement token exchange.\n\n### Step 3: Token Exchange Setup (Only if MCP server needs to talk to OCI Control Plane)\n\nToken exchange helps you exchange a logged-in user's OCI IAM token for an OCI control plane session token, also known as UPST (User Principal Session Token). To learn more about token exchange, refer to my [Workload Identity Federation Blog](https://www.ateam-oracle.com/post/workload-identity-federation)\n\nFor token exchange, we need to configure Identity propagation trust. The blog above discusses setting up the trust using REST APIs. However, you can also use OCI CLI. Before using the CLI command below, ensure that you have created a token exchange OAuth client. In most cases, you can use the same OAuth client that you created above. Replace `<IAM_GUID>` and `<CLIENT_ID>` in the CLI command below with your actual values.\n\n```bash\noci identity-domains identity-propagation-trust create \\\n--schemas '[\"urn:ietf:params:scim:schemas:oracle:idcs:IdentityPropagationTrust\"]' \\\n--public-key-endpoint \"https://<IAM_GUID>.identity.oraclecloud.com/admin/v1/SigningCert/jwk\" \\\n--name \"For Token Exchange\" --type \"JWT\" \\\n--issuer \"https://identity.oraclecloud.com/\" --active true \\\n--endpoint \"https://<IAM_GUID>.identity.oraclecloud.com\" \\\n--subject-claim-name \"sub\" --allow-impersonation false \\\n--subject-mapping-attribute \"username\" \\\n--subject-type \"User\" --client-claim-name \"iss\" \\\n--client-claim-values '[\"https://identity.oraclecloud.com/\"]' \\\n--oauth-clients '[\"<CLIENT_ID>\"]'\n```\n\nTo exchange access token for OCI token and create a signer object, you need to add below code in MCP server. You can then use the signer object to create any OCI control plane client. \n\n```python\n\nfrom fastmcp.server.dependencies import get_access_token\nfrom fastmcp.utilities.logging import get_logger\nfrom oci.auth.signers import TokenExchangeSigner\nimport os\n\nlogger = get_logger(__name__)\n\n# Load configuration from environment\nOCI_IAM_GUID = os.environ.get(\"OCI_IAM_GUID\")\nOCI_CLIENT_ID = os.environ.get(\"OCI_CLIENT_ID\")\nOCI_CLIENT_SECRET = os.environ.get(\"OCI_CLIENT_SECRET\")\n\n_global_token_cache = {} #In memory cache for OCI session token signer\n    \ndef get_oci_signer() -> TokenExchangeSigner:\n\n    authntoken = get_access_token()\n    tokenID = authntoken.claims.get(\"jti\")\n    token = authntoken.token\n    \n    #Check if the signer exists for the token ID in memory cache\n    cached_signer = _global_token_cache.get(tokenID)\n    logger.debug(f\"Global cached signer: {cached_signer}\")\n    if cached_signer:\n        logger.debug(f\"Using globally cached signer for token ID: {tokenID}\")\n        return cached_signer\n\n    #If the signer is not yet created for the token then create new OCI signer object\n    logger.debug(f\"Creating new signer for token ID: {tokenID}\")\n    signer = TokenExchangeSigner(\n        jwt_or_func=token,\n        oci_domain_id=OCI_IAM_GUID.split(\".\")[0] if OCI_IAM_GUID else \"\",\n        client_id=OCI_CLIENT_ID,\n        client_secret=OCI_CLIENT_SECRET,\n    )\n    logger.debug(f\"Signer {signer} created for token ID: {tokenID}\")\n        \n    #Cache the signer object in memory cache\n    _global_token_cache[tokenID] = signer\n    logger.debug(f\"Signer cached for token ID: {tokenID}\")\n\n    return signer\n```\n\n## Running MCP server\n\nOnce the setup is complete, to run the MCP server, run the below command.\n```bash\nfastmcp run server.py:mcp --transport http --port 8000\n```\n\nTo run MCP client, run the below command.\n```bash\npython3 client.py\n```\n\nMCP Client sample is as below.\n```python client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle OCI OAuth flows\n    async with Client(\"http://localhost:8000/mcp/\", auth=\"oauth\") as client:\n        # First-time connection will open OCI login in your browser\n        print(\"✓ Authenticated with OCI IAM\")\n\n        tools = await client.list_tools()\n        print(f\"🔧 Available tools ({len(tools)}):\")\n        for tool in tools:\n            print(f\"   - {tool.name}: {tool.description}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to OCI IAM's login page\n2. Sign in with your OCI account and grant the requested consent\n3. After authorization, you'll be redirected back to the redirect path\n4. The client receives the token and can make authenticated requests\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\n\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.oci import OCIProvider\n\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Load configuration from environment\n# Production setup with encrypted persistent token storage\nauth_provider = OCIProvider(\n    config_url=os.environ.get(\"OCI_CONFIG_URL\"),\n    client_id=os.environ.get(\"OCI_CLIENT_ID\"),\n    client_secret=os.environ.get(\"OCI_CLIENT_SECRET\"),\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-production-domain.com\"),\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production OCI App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at Rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>"
  },
  {
    "path": "docs/integrations/openai.mdx",
    "content": "---\ntitle: OpenAI API 🤝 FastMCP\nsidebarTitle: OpenAI API\ndescription: Connect FastMCP servers to the OpenAI API\nicon: message-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n\n## Responses API\n\nOpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses) supports [MCP servers](https://platform.openai.com/docs/guides/tools-remote-mcp) as remote tool sources, allowing you to extend AI capabilities with custom functions.\n\n<Note>\nThe Responses API is a distinct API from OpenAI's Completions API or Assistants API. At this time, only the Responses API supports MCP.\n</Note>\n\n<Tip>\nCurrently, the Responses API only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to the AI agent. Other MCP features like resources and prompts are not currently supported.\n</Tip>\n\n\n### Create a Server\n\nFirst, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n### Deploy the Server\n\nYour server must be deployed to a public URL in order for OpenAI to access it.\n\nFor development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.\n\nAssuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:\n\n<CodeGroup>\n```bash FastMCP server\npython server.py\n```\n\n```bash ngrok\nngrok http 8000\n```\n</CodeGroup>\n\n<Warning>\nThis exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.\n</Warning>\n\n### Call the Server\n\nTo use the Responses API, you'll need to install the OpenAI Python SDK (not included with FastMCP):\n\n```bash\npip install openai\n```\n\nYou'll also need to authenticate with OpenAI. You can do this by setting the `OPENAI_API_KEY` environment variable. Consult the OpenAI SDK documentation for more information.\n\n```bash\nexport OPENAI_API_KEY=\"your-api-key\"\n```\n\nHere is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment.\n\n```python {4, 11-16}\nfrom openai import OpenAI\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\nclient = OpenAI()\n\nresp = client.responses.create(\n    model=\"gpt-4.1\",\n    tools=[\n        {\n            \"type\": \"mcp\",\n            \"server_label\": \"dice_server\",\n            \"server_url\": f\"{url}/mcp/\",\n            \"require_approval\": \"never\",\n        },\n    ],\n    input=\"Roll a few dice!\",\n)\n\nprint(resp.output_text)\n```\nIf you run this code, you'll see something like the following output:\n\n```text\nYou rolled 3 dice and got the following results: 6, 4, and 2!\n```\n\n### Authentication\n\n<VersionBadge version=\"2.6.0\" />\n\nThe Responses API can include headers to authenticate the request, which means you don't have to worry about your server being publicly accessible.\n\n#### Server Authentication\n\nThe simplest way to add authentication to the server is to use a bearer token scheme. \n\nFor this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/servers/auth/token-verification) documentation. \n\nWe'll start by creating an RSA key pair to sign and verify tokens.\n\n```python\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n```\n\n<Warning>\nFastMCP's `RSAKeyPair` utility is for development and testing only.\n</Warning> \n\nNext, we'll create a `JWTVerifier` to authenticate the server. \n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n```\n\nHere is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**\n\n```python server.py [expandable]\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\nimport random\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    print(f\"\\n---\\n\\n🔑 Dice Roller access token:\\n\\n{access_token}\\n\\n---\\n\")\n    mcp.run(transport=\"http\", port=8000)\n```\n\n#### Client Authentication\n\nIf you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:\n\n```python\npythonAPIStatusError: Error code: 424 - {\n    \"error\": {\n        \"message\": \"Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)\",\n        \"type\": \"external_connector_error\",\n        \"param\": \"tools\",\n        \"code\": \"http_error\"\n    }\n}\n```\n\nAs expected, the server is rejecting the request because it's not authenticated.\n\nTo authenticate the client, you can pass the token in the `Authorization` header with the `Bearer` scheme:\n\n\n```python {4, 7, 19-21} [expandable]\nfrom openai import OpenAI\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\n# Your access token (replace with your actual token)\naccess_token = 'your-access-token'\n\nclient = OpenAI()\n\nresp = client.responses.create(\n    model=\"gpt-4.1\",\n    tools=[\n        {\n            \"type\": \"mcp\",\n            \"server_label\": \"dice_server\",\n            \"server_url\": f\"{url}/mcp/\",\n            \"require_approval\": \"never\",\n            \"headers\": {\n                \"Authorization\": f\"Bearer {access_token}\"\n            }\n        },\n    ],\n    input=\"Roll a few dice!\",\n)\n\nprint(resp.output_text)\n```\n\nYou should now see the dice roll results in the output."
  },
  {
    "path": "docs/integrations/openapi.mdx",
    "content": "---\ntitle: OpenAPI 🤝 FastMCP\nsidebarTitle: OpenAPI\ndescription: Generate MCP servers from any OpenAPI specification\nicon: list-tree\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nFastMCP can automatically generate an MCP server from any OpenAPI specification, allowing AI models to interact with existing APIs through the MCP protocol. Instead of manually creating tools and resources, you provide an OpenAPI spec and FastMCP intelligently converts API endpoints into the appropriate MCP components.\n\n<Note>\nUnder the hood, OpenAPI integration uses OpenAPIProvider (v3.0.0+) to source tools from the specification. See [Providers](/servers/providers/overview) to understand how FastMCP sources components.\n</Note>\n\n<Tip>\nGenerating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters.\n\nWe recommend using the FastAPI integration for bootstrapping and prototyping, not for mirroring your API to LLM clients. See the post [Stop Converting Your REST APIs to MCP](https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp) for more details.\n</Tip>\n\n## Create a Server\n\nTo convert an OpenAPI specification to an MCP server, use the `FastMCP.from_openapi()` class method:\n\n```python server.py\nimport httpx\nfrom fastmcp import FastMCP\n\n# Create an HTTP client for your API\nclient = httpx.AsyncClient(base_url=\"https://api.example.com\")\n\n# Load your OpenAPI spec \nopenapi_spec = httpx.get(\"https://api.example.com/openapi.json\").json()\n\n# Create the MCP server\nmcp = FastMCP.from_openapi(\n    openapi_spec=openapi_spec,\n    client=client,\n    name=\"My API Server\"\n)\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Authentication\n\nIf your API requires authentication, configure it on the HTTP client:\n\n```python\nimport httpx\nfrom fastmcp import FastMCP\n\n# Bearer token authentication\napi_client = httpx.AsyncClient(\n    base_url=\"https://api.example.com\",\n    headers={\"Authorization\": \"Bearer YOUR_TOKEN\"}\n)\n\n# Create MCP server with authenticated client\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec, \n    client=api_client,\n    timeout=30.0  # 30 second timeout for all requests\n)\n```\n\n## Route Mapping\n\nBy default, FastMCP converts **every endpoint** in your OpenAPI specification into an MCP **Tool**. This provides a simple, predictable starting point that ensures all your API's functionality is immediately available to the vast majority of LLM clients which only support MCP tools.\n\nWhile this is a pragmatic default for maximum compatibility, you can easily customize this behavior. Internally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types.\n\nEach `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely.\n\n- **Methods**: HTTP methods to match (e.g. `[\"GET\", \"POST\"]` or `\"*\"` for all)\n- **Pattern**: Regex pattern to match the route path (e.g. `r\"^/users/.*\"` or `r\".*\"` for all)\n- **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.\n- **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)\n- **MCP tags**: A set of custom tags to add to components created from matching routes\n\nHere is FastMCP's default rule:\n\n```python\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nDEFAULT_ROUTE_MAPPINGS = [\n    # All routes become tools\n    RouteMap(mcp_type=MCPType.TOOL),\n]\n```\n\n### Custom Route Maps\n\nWhen creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map.\n\nFor example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `Resource` and `ResourceTemplate` components based on whether they had path parameters. (This was changed solely for client compatibility reasons.) You can restore this behavior by providing custom route maps:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\n# Restore pre-2.8.0 semantic mapping\nsemantic_maps = [\n    # GET requests with path parameters become ResourceTemplates\n    RouteMap(methods=[\"GET\"], pattern=r\".*\\{.*\\}.*\", mcp_type=MCPType.RESOURCE_TEMPLATE),\n    # All other GET requests become Resources\n    RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n]\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=semantic_maps,\n)\n```\n\nWith these maps, `GET` requests are handled semantically, and all other methods (`POST`, `PUT`, etc.) will fall through to the default rule and become `Tool`s.\n\nHere is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged \"internal\". All other routes will be handled by the default rules:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        # Analytics `GET` endpoints are tools\n        RouteMap(\n            methods=[\"GET\"], \n            pattern=r\"^/analytics/.*\", \n            mcp_type=MCPType.TOOL,\n        ),\n\n        # Exclude all admin endpoints\n        RouteMap(\n            pattern=r\"^/admin/.*\", \n            mcp_type=MCPType.EXCLUDE,\n        ),\n\n        # Exclude all routes tagged \"internal\"\n        RouteMap(\n            tags={\"internal\"},\n            mcp_type=MCPType.EXCLUDE,\n        ),\n    ],\n)\n```\n\n<Tip>\nThe default route maps are always applied after your custom maps, so you do not have to create route maps for every possible route.\n</Tip>\n\n### Excluding Routes\n\nTo exclude routes from the MCP server, use a route map to assign them to `MCPType.EXCLUDE`. \n\nYou can use this to remove sensitive or internal routes by targeting them specifically:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        RouteMap(pattern=r\"^/admin/.*\", mcp_type=MCPType.EXCLUDE),\n        RouteMap(tags={\"internal\"}, mcp_type=MCPType.EXCLUDE),\n    ],\n)\n```\n\nOr you can use a catch-all rule to exclude everything that your maps don't handle explicitly:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        # custom mapping logic goes here\n        # ... your specific route maps ...\n        # exclude all remaining routes\n        RouteMap(mcp_type=MCPType.EXCLUDE),\n    ],\n)\n```\n\n<Tip>\nUsing a catch-all exclusion rule will prevent the default route mappings from being applied, since it will match every remaining route. This is useful if you want to explicitly allow-list certain routes.\n</Tip>\n\n### Advanced Route Mapping\n\n<VersionBadge version=\"2.5.0\" />\n\nFor advanced use cases that require more complex logic, you can provide a `route_map_fn` callable. After the route map logic is applied, this function is called on each matched route and its assigned MCP component type. It can optionally return a different component type to override the mapped assignment. If it returns `None`, the assigned type is used.\n\nIn addition to more precise targeting of methods, patterns, and tags, this function can access any additional OpenAPI metadata about the route.\n\n<Tip>\nThe `route_map_fn` is called on all routes, even those that matched `MCPType.EXCLUDE` in your custom maps. This gives you an opportunity to customize the mapping or even override an exclusion.\n</Tip>\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute\n\ndef custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:\n    \"\"\"Advanced route type mapping.\"\"\"\n    # Convert all admin routes to tools regardless of HTTP method\n    if \"/admin/\" in route.path:\n        return MCPType.TOOL\n\n    elif \"internal\" in route.tags:\n        return MCPType.EXCLUDE\n    \n    # Convert user detail routes to templates even if they're POST\n    elif route.path.startswith(\"/users/\") and route.method == \"POST\":\n        return MCPType.RESOURCE_TEMPLATE\n    \n    # Use defaults for all other routes\n    return None\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_map_fn=custom_route_mapper,\n)\n```\n\n## Customization\n\n### Component Names\n\n<VersionBadge version=\"2.5.0\" />\n\nFastMCP automatically generates names for MCP components based on the OpenAPI specification. By default, it uses the `operationId` from your OpenAPI spec, up to the first double underscore (`__`).\n\nAll component names are automatically:\n- **Slugified**: Spaces and special characters are converted to underscores or removed\n- **Truncated**: Limited to 56 characters maximum to ensure compatibility\n- **Unique**: If multiple components have the same name, a number is automatically appended to make them unique\n\nFor more control over component names, you can provide an `mcp_names` dictionary that maps `operationId` values to your desired names. The `operationId` must be exactly as it appears in the OpenAPI spec. The provided name will always be slugified and truncated.\n\n```python\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    mcp_names={\n        \"list_users__with_pagination\": \"user_list\",\n        \"create_user__admin_required\": \"create_user\", \n        \"get_user_details__admin_required\": \"user_detail\",\n    }\n)\n```\n\nAny `operationId` not found in `mcp_names` will use the default strategy (operationId up to the first `__`).\n\n### Tags\n\n<VersionBadge version=\"2.8.0\" />\n\nFastMCP provides several ways to add tags to your MCP components, allowing you to categorize and organize them for better discoverability and filtering. Tags are combined from multiple sources to create the final set of tags on each component.\n\n#### RouteMap Tags\n\nYou 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.\n\n```python\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        # Add custom tags to all POST endpoints\n        RouteMap(\n            methods=[\"POST\"],\n            pattern=r\".*\",\n            mcp_type=MCPType.TOOL,\n            mcp_tags={\"write-operation\", \"api-mutation\"}\n        ),\n        \n        # Add different tags to detail view endpoints\n        RouteMap(\n            methods=[\"GET\"],\n            pattern=r\".*\\{.*\\}.*\",\n            mcp_type=MCPType.RESOURCE_TEMPLATE,\n            mcp_tags={\"detail-view\", \"parameterized\"}\n        ),\n        \n        # Add tags to list endpoints\n        RouteMap(\n            methods=[\"GET\"],\n            pattern=r\".*\",\n            mcp_type=MCPType.RESOURCE,\n            mcp_tags={\"list-data\", \"collection\"}\n        ),\n    ],\n)\n```\n\n#### Global Tags\n\nYou can add tags to **all** components by providing a `tags` parameter when creating your MCP server. These global tags will be applied to every component created from your OpenAPI specification.\n\n```python\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    tags={\"api-v2\", \"production\", \"external\"}\n)\n```\n\n#### OpenAPI Tags in Client Meta\n\nFastMCP automatically includes OpenAPI tags from your specification in the component's metadata. These tags are available to MCP clients through the `meta.fastmcp.tags` field, allowing clients to filter and organize components based on the original OpenAPI tagging:\n\n<CodeGroup>\n```json {5} OpenAPI spec with tags\n{\n  \"paths\": {\n    \"/users\": {\n      \"get\": {\n        \"tags\": [\"users\", \"public\"],\n        \"operationId\": \"list_users\",\n        \"summary\": \"List all users\"\n      }\n    }\n  }\n}\n```\n```python {6-9} Access OpenAPI tags in MCP client\nasync with client:\n    tools = await client.list_tools()\n    for tool in tools:\n        if tool.meta:\n            # OpenAPI tags are now available in fastmcp namespace!\n            fastmcp_meta = tool.meta.get('fastmcp', {})\n            openapi_tags = fastmcp_meta.get('tags', [])\n            if 'users' in openapi_tags:\n                print(f\"Found user-related tool: {tool.name}\")\n```\n</CodeGroup>\n\nThis makes it easy for clients to understand and organize API endpoints based on their original OpenAPI categorization.\n\n### Advanced Customization\n\n<VersionBadge version=\"2.5.0\" />\n\nBy default, FastMCP creates MCP components using a variety of metadata from the OpenAPI spec, such as incorporating the OpenAPI description into the MCP component description.\n\nAt times you may want to modify those MCP components in a variety of ways, such as adding LLM-specific instructions or tags. For fine-grained customization, you can provide a `mcp_component_fn` when creating the MCP server. After each MCP component has been created, this function is called on it and has the opportunity to modify it in-place.\n\n<Tip>\nYour `mcp_component_fn` is expected to modify the component in-place, not to return a new component. The result of the function is ignored.\n</Tip>\n\n```python\nfrom fastmcp.server.openapi import (\n    HTTPRoute,\n    OpenAPITool,\n    OpenAPIResource,\n    OpenAPIResourceTemplate,\n)\n\ndef customize_components(\n    route: HTTPRoute, \n    component: OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate,\n) -> None:\n    # Add custom tags to all components\n    component.tags.add(\"openapi\")\n    \n    # Customize based on component type\n    if isinstance(component, OpenAPITool):\n        component.description = f\"🔧 {component.description} (via API)\"\n    \n    if isinstance(component, OpenAPIResource):\n        component.description = f\"📊 {component.description}\"\n        component.tags.add(\"data\")\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    mcp_component_fn=customize_components,\n)\n```\n\n## Request Parameter Handling\n\nFastMCP intelligently handles different types of parameters in OpenAPI requests:\n\n### Query Parameters\n\nBy default, FastMCP only includes query parameters that have non-empty values. Parameters with `None` values or empty strings are automatically filtered out.\n\n```python\n# When calling this tool...\nawait client.call_tool(\"search_products\", {\n    \"category\": \"electronics\",  # ✅ Included\n    \"min_price\": 100,           # ✅ Included  \n    \"max_price\": None,          # ❌ Excluded\n    \"brand\": \"\",                # ❌ Excluded\n})\n\n# The HTTP request will be: GET /products?category=electronics&min_price=100\n```\n\n### Path Parameters\n\nPath parameters are typically required by REST APIs. FastMCP:\n- Filters out `None` values\n- Validates that all required path parameters are provided\n- Raises clear errors for missing required parameters\n\n```python\n# ✅ This works\nawait client.call_tool(\"get_user\", {\"user_id\": 123})\n\n# ❌ This raises: \"Missing required path parameters: {'user_id'}\"\nawait client.call_tool(\"get_user\", {\"user_id\": None})\n```\n\n### Array Parameters\n\nFastMCP handles array parameters according to OpenAPI specifications:\n\n- **Query arrays**: Serialized based on the `explode` parameter (default: `True`)\n- **Path arrays**: Serialized as comma-separated values (OpenAPI 'simple' style)\n\n```python\n# Query array with explode=true (default)\n# ?tags=red&tags=blue&tags=green\n\n# Query array with explode=false  \n# ?tags=red,blue,green\n\n# Path array (always comma-separated)\n# /items/red,blue,green\n```\n\n### Headers\n\nHeader parameters are automatically converted to strings and included in the HTTP request."
  },
  {
    "path": "docs/integrations/permit.mdx",
    "content": "---\ntitle: Permit.io Authorization 🤝 FastMCP\nsidebarTitle: Permit.io\ndescription: Add fine-grained authorization to your FastMCP servers with Permit.io\nicon: shield-check\n---\n\nAdd **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Permit.io][permit-github] authorization middleware**.\n\nControl which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic policies using Permit.io's powerful RBAC, ABAC, and REBAC capabilities, and obtain comprehensive audit logs of all access attempts and violations.\n\n## How it Works\n\nLeveraging FastMCP's [Middleware][fastmcp-middleware], the Permit.io middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks against your Permit.io policies; covering both server methods and tool execution.\n\n### Policy Mapping\n\nThe middleware automatically maps MCP methods to Permit.io resources and actions:\n\n- **MCP server methods** (e.g., `tools/list`, `resources/read`):\n  - **Resource**: `{server_name}_{component}` (e.g., `myserver_tools`)\n  - **Action**: The method verb (e.g., `list`, `read`)\n- **Tool execution** (method `tools/call`):\n  - **Resource**: `{server_name}` (e.g., `myserver`)\n  - **Action**: The tool name (e.g., `greet`)\n\n![Permit.io Policy Mapping Example](./images/permit/policy_mapping.png)\n\n*Example: In Permit.io, the 'Admin' role is granted permissions on resources and actions as mapped by the middleware. For example, 'greet', 'greet-jwt', and 'login' are actions on the 'mcp_server' resource, and 'list' is an action on the 'mcp_server_tools' resource.*\n\n> **Note:**\n> Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies.\n>\n> ![Permit.io Directory Role Assignment Example](./images/permit/role_assignement.png)\n>\n> *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.*\n\nFor detailed policy mapping examples and configuration, see [Detailed Policy Mapping](https://github.com/permitio/permit-fastmcp/blob/main/docs/policy-mapping.md).\n\n### Listing Operations\n\nThe middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant PermitMiddleware as Permit.io Middleware\n    participant MCPServer as FastMCP Server\n    participant PermitPDP as Permit.io PDP\n\n    MCPClient->>PermitMiddleware: MCP Listing Request (e.g., tools/list)\n    PermitMiddleware->>MCPServer: MCP Listing Request\n    MCPServer-->>PermitMiddleware: MCP Listing Response\n    PermitMiddleware->>PermitPDP: Authorization Checks\n    PermitPDP->>PermitMiddleware: Authorization Decisions\n    PermitMiddleware-->>MCPClient: Filtered MCP Listing Response\n```\n\n### Execution Operations\n\nThe middleware behaves as an enforcement point for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant PermitMiddleware as Permit.io Middleware\n    participant MCPServer as FastMCP Server\n    participant PermitPDP as Permit.io PDP\n\n    MCPClient->>PermitMiddleware: MCP Execution Request (e.g., tools/call)\n    PermitMiddleware->>PermitPDP: Authorization Check\n    PermitPDP->>PermitMiddleware: Authorization Decision\n    PermitMiddleware-->>MCPClient: MCP Unauthorized Error (if denied)\n    PermitMiddleware->>MCPServer: MCP Execution Request (if allowed)\n    MCPServer-->>PermitMiddleware: MCP Execution Response (if allowed)\n    PermitMiddleware-->>MCPClient: MCP Execution Response (if allowed)\n```\n\n## Add Authorization to Your Server\n\n<Note>\nPermit.io is a cloud-native authorization service. You need a Permit.io account and a running Policy Decision Point (PDP) for the middleware to function. You can run the PDP locally with Docker or use Permit.io's cloud PDP.\n</Note>\n\n### Prerequisites\n\n1. **Permit.io Account**: Sign up at [permit.io](https://permit.io)\n2. **PDP Setup**: Run the Permit.io PDP locally or use the cloud PDP (RBAC only)\n3. **API Key**: Get your Permit.io API key from the dashboard\n\n### Run the Permit.io PDP\n\nRun the PDP locally with Docker:\n\n```bash\ndocker run -p 7766:7766 permitio/pdp:latest\n```\n\nOr use the cloud PDP URL: `https://cloudpdp.api.permit.io`\n\n### Create a Server with Authorization\n\nFirst, install the `permit-fastmcp` package:\n\n```bash\n# Using UV (recommended)\nuv add permit-fastmcp\n\n# Using pip\npip install permit-fastmcp\n```\n\nThen create a FastMCP server and add the Permit.io middleware:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom permit_fastmcp.middleware.middleware import PermitMcpMiddleware\n\nmcp = FastMCP(\"Secure FastMCP Server 🔒\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet a user by name\"\"\"\n    return f\"Hello, {name}!\"\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n# Add Permit.io authorization middleware\nmcp.add_middleware(PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-permit-api-key\"\n))\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")\n```\n\n### Configure Access Policies\n\nCreate your authorization policies in the Permit.io dashboard:\n\n1. **Create Resources**: Define resources like `mcp_server` and `mcp_server_tools`\n2. **Define Actions**: Add actions like `greet`, `add`, `list`, `read`\n3. **Create Roles**: Define roles like `Admin`, `User`, `Guest`\n4. **Assign Permissions**: Grant roles access to specific resources and actions\n5. **Assign Users**: Assign roles to users in the Permit.io Directory\n\nFor step-by-step setup instructions and troubleshooting, see [Getting Started & FAQ](https://github.com/permitio/permit-fastmcp/blob/main/docs/getting-started.md).\n\n#### Example Policy Configuration\n\nPolicies are defined in the Permit.io dashboard, but you can also use the [Permit.io Terraform provider](https://github.com/permitio/terraform-provider-permitio) to define policies in code.\n\n\n```terraform\n# Resources\nresource \"permitio_resource\" \"mcp_server\" {\n  name = \"mcp_server\"\n  key  = \"mcp_server\"\n  \n  actions = {\n    \"greet\" = { name = \"greet\" }\n    \"add\"   = { name = \"add\" }\n  }\n}\n\nresource \"permitio_resource\" \"mcp_server_tools\" {\n  name = \"mcp_server_tools\"\n  key  = \"mcp_server_tools\"\n  \n  actions = {\n    \"list\" = { name = \"list\" }\n  }\n}\n\n# Roles\nresource \"permitio_role\" \"Admin\" {\n  key         = \"Admin\"\n  name        = \"Admin\"\n  permissions = [\n    \"mcp_server:greet\",\n    \"mcp_server:add\", \n    \"mcp_server_tools:list\"\n  ]\n}\n```\n\nYou can also use the [Permit.io CLI](https://github.com/permitio/permit-cli), [API](https://api.permit.io/scalar) or [SDKs](https://github.com/permitio/permit-python) to manage policies, as well as writing policies directly in REGO (Open Policy Agent's policy language).\n\nFor complete policy examples including ABAC and RBAC configurations, see [Example Policies](https://github.com/permitio/permit-fastmcp/tree/main/docs/example_policies).\n\n### Identity Management\n\nThe middleware supports multiple identity extraction modes:\n\n- **Fixed Identity**: Use a fixed identity for all requests\n- **Header-based**: Extract identity from HTTP headers\n- **JWT-based**: Extract and verify JWT tokens\n- **Source-based**: Use the MCP context source field\n\nFor detailed identity mode configuration and environment variables, see [Identity Modes & Environment Variables](https://github.com/permitio/permit-fastmcp/blob/main/docs/identity-modes.md).\n\n#### JWT Authentication Example\n\n```python\nimport os\n\n# Configure JWT identity extraction\nos.environ[\"PERMIT_MCP_IDENTITY_MODE\"] = \"jwt\"\nos.environ[\"PERMIT_MCP_IDENTITY_JWT_SECRET\"] = \"your-jwt-secret\"\n\nmcp.add_middleware(PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-permit-api-key\"\n))\n```\n\n### ABAC Policies with Tool Arguments\n\nThe middleware supports Attribute-Based Access Control (ABAC) policies that can evaluate tool arguments as attributes. Tool arguments are automatically flattened as individual attributes (e.g., `arg_name`, `arg_number`) for granular policy conditions.\n\n![ABAC Condition Example](./images/permit/abac_condition_example.png)\n\n*Example: Create dynamic resources with conditions like `resource.arg_number greater-than 10` to allow the `conditional-greet` tool only when the number argument exceeds 10.*\n\n#### Example: Conditional Access\n\nCreate a dynamic resource with conditions like `resource.arg_number greater-than 10` to allow the `conditional-greet` tool only when the number argument exceeds 10.\n\n```python\n@mcp.tool\ndef conditional_greet(name: str, number: int) -> str:\n    \"\"\"Greet a user only if number > 10\"\"\"\n    return f\"Hello, {name}! Your number is {number}\"\n```\n\n![ABAC Policy Example](./images/permit/abac_policy_example.png)\n\n*Example: The Admin role is granted access to the \"conditional-greet\" action on the \"Big-greets\" dynamic resource, while other tools like \"greet\", \"greet-jwt\", and \"login\" are granted on the base \"mcp_server\" resource.*\n\nFor comprehensive ABAC configuration and advanced policy examples, see [ABAC Policies with Tool Arguments](https://github.com/permitio/permit-fastmcp/blob/main/docs/policy-mapping.md#abac-policies-with-tool-arguments).\n\n### Run the Server\n\nStart your FastMCP server normally:\n\n```bash\npython server.py\n```\n\nThe middleware will now intercept all MCP requests and check them against your Permit.io policies. Requests include user identification through the configured identity mode and automatic mapping of MCP methods to authorization resources and actions.\n\n## Advanced Configuration\n\n### Environment Variables\n\nConfigure the middleware using environment variables:\n\n```bash\n# Permit.io configuration\nexport PERMIT_MCP_PERMIT_PDP_URL=\"http://localhost:7766\"\nexport PERMIT_MCP_PERMIT_API_KEY=\"your-api-key\"\n\n# Identity configuration\nexport PERMIT_MCP_IDENTITY_MODE=\"jwt\"\nexport PERMIT_MCP_IDENTITY_JWT_SECRET=\"your-jwt-secret\"\n\n# Method configuration\nexport PERMIT_MCP_KNOWN_METHODS='[\"tools/list\",\"tools/call\"]'\nexport PERMIT_MCP_BYPASSED_METHODS='[\"initialize\",\"ping\"]'\n\n# Logging configuration\nexport PERMIT_MCP_ENABLE_AUDIT_LOGGING=\"true\"\n```\n\nFor a complete list of all configuration options and environment variables, see [Configuration Reference](https://github.com/permitio/permit-fastmcp/blob/main/docs/configuration-reference.md).\n\n### Custom Middleware Configuration\n\n```python\nfrom permit_fastmcp.middleware.middleware import PermitMcpMiddleware\n\nmiddleware = PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-api-key\",\n    enable_audit_logging=True,\n    bypass_methods=[\"initialize\", \"ping\", \"health/*\"]\n)\n\nmcp.add_middleware(middleware)\n```\n\nFor advanced configuration options and custom middleware extensions, see [Advanced Configuration](https://github.com/permitio/permit-fastmcp/blob/main/docs/advanced-configuration.md).\n\n## Example: Complete JWT Authentication Server\n\nSee the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/):\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom permit_fastmcp.middleware.middleware import PermitMcpMiddleware\nimport jwt\nimport datetime\n\n# Configure JWT identity extraction\nos.environ[\"PERMIT_MCP_IDENTITY_MODE\"] = \"jwt\"\nos.environ[\"PERMIT_MCP_IDENTITY_JWT_SECRET\"] = \"mysecretkey\"\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef login(username: str, password: str) -> str:\n    \"\"\"Login to get a JWT token\"\"\"\n    if username == \"admin\" and password == \"password\":\n        token = jwt.encode(\n            {\"sub\": username, \"exp\": datetime.datetime.utcnow() + datetime.timedelta(hours=1)},\n            \"mysecretkey\",\n            algorithm=\"HS256\"\n        )\n        return f\"Bearer {token}\"\n    raise Exception(\"Invalid credentials\")\n\n@mcp.tool\ndef greet_jwt(ctx: Context) -> str:\n    \"\"\"Greet a user by extracting their name from JWT\"\"\"\n    # JWT extraction handled by middleware\n    return \"Hello, authenticated user!\"\n\nmcp.add_middleware(PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-permit-api-key\"\n))\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")\n```\n\n<Tip>\n  For detailed policy configuration, custom authentication, and advanced\n  deployment patterns, visit the [Permit.io FastMCP Middleware\n  repository][permit-fastmcp-github]. For troubleshooting common issues, see [Troubleshooting](https://github.com/permitio/permit-fastmcp/blob/main/docs/troubleshooting.md).\n</Tip>\n\n\n[permit.io]: https://www.permit.io\n[permit-github]: https://github.com/permitio\n[permit-fastmcp-github]: https://github.com/permitio/permit-fastmcp\n[Agent.Security]: https://agent.security\n[fastmcp-middleware]: /servers/middleware\n"
  },
  {
    "path": "docs/integrations/propelauth.mdx",
    "content": "---\ntitle: PropelAuth 🤝 FastMCP\nsidebarTitle: PropelAuth\ndescription: Secure your FastMCP server with PropelAuth\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"3.1.0\" />\n\nThis guide shows you how to secure your FastMCP server using [**PropelAuth**](https://www.propelauth.com), a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where PropelAuth handles user login, consent management, and your FastMCP server validates the tokens.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n\n1. A [PropelAuth](https://www.propelauth.com) account\n2. Your FastMCP server's base URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Configure PropelAuth\n\n<Steps>\n<Step title=\"Enable MCP Authentication\">\n    Navigate to the **MCP** section in your PropelAuth dashboard, click **Enable MCP**, and choose which environments to enable it for (Test, Staging, Prod).\n</Step>\n\n<Step title=\"Configure Allowed MCP Clients\">\n    Under **MCP > Allowed MCP Clients**, add redirect URIs for each MCP client you want to allow. PropelAuth provides templates for popular clients like Claude, Cursor, and ChatGPT.\n</Step>\n\n<Step title=\"Configure Scopes\">\n    Under **MCP > Scopes**, define the permissions available to MCP clients (e.g., `read:user_data`).\n</Step>\n\n<Step title=\"Choose How Users Create OAuth Clients\">\n    Under **MCP > Settings > How Do Users Create OAuth Clients?**, you can optionally enable:\n    - **Dynamic Client Registration** — clients self-register automatically via the DCR protocol\n    - **Manually via Hosted Pages** — PropelAuth creates a UI for your users to register OAuth clients\n\n    You can enable neither, one, or both. If you enable neither, you'll manage OAuth client creation yourself.\n</Step>\n\n<Step title=\"Generate Introspection Credentials\">\n    Go to **MCP > Request Validation** and click **Create Credentials**. Note the **Client ID** and **Client Secret** - you'll need these to validate tokens.\n</Step>\n\n<Step title=\"Note Your Auth URL\">\n    Find your Auth URL in the **Backend Integration** section of the dashboard (e.g., `https://auth.yourdomain.com`).\n</Step>\n</Steps>\n\nFor more details, see the [PropelAuth MCP documentation](https://docs.propelauth.com/mcp-authentication/overview).\n\n### Step 2: Environment Setup\n\nCreate a `.env` file with your PropelAuth configuration:\n\n```bash\nPROPELAUTH_AUTH_URL=https://auth.yourdomain.com          # From Backend Integration page\nPROPELAUTH_INTROSPECTION_CLIENT_ID=your-client-id        # From MCP > Request Validation\nPROPELAUTH_INTROSPECTION_CLIENT_SECRET=your-client-secret # From MCP > Request Validation\nSERVER_URL=http://localhost:8000                          # Your server's base URL\n```\n\n### Step 3: FastMCP Configuration\n\nCreate your FastMCP server file and use the PropelAuthProvider to handle all the OAuth integration automatically:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.propelauth import PropelAuthProvider\n\nauth_provider = PropelAuthProvider(\n    auth_url=os.environ[\"PROPELAUTH_AUTH_URL\"],\n    introspection_client_id=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_ID\"],\n    introspection_client_secret=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_SECRET\"],\n    base_url=os.environ[\"SERVER_URL\"],\n    required_scopes=[\"read:user_data\"],                          # Optional scope enforcement\n)\n\nmcp = FastMCP(name=\"My PropelAuth Protected Server\", auth=auth_provider)\n```\n\n## Testing\n\nWith your `.env` loaded, start the server:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nThen use a FastMCP client to verify authentication works:\n\n```python\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        assert await client.ping()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Accessing User Information\n\nYou can use `get_access_token()` inside your tools to identify the authenticated user:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.propelauth import PropelAuthProvider\nfrom fastmcp.server.dependencies import get_access_token\n\nauth = PropelAuthProvider(\n    auth_url=os.environ[\"PROPELAUTH_AUTH_URL\"],\n    introspection_client_id=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_ID\"],\n    introspection_client_secret=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_SECRET\"],\n    base_url=os.environ[\"SERVER_URL\"],\n    required_scopes=[\"read:user_data\"],\n)\n\nmcp = FastMCP(name=\"My PropelAuth Protected Server\", auth=auth)\n\n@mcp.tool\ndef whoami() -> dict:\n    \"\"\"Return the authenticated user's ID.\"\"\"\n    token = get_access_token()\n    if token is None:\n        return {\"error\": \"Not authenticated\"}\n    user_id = token.claims.get(\"sub\")\n    return {\"user_id\": user_id}\n```\n\n## Advanced Configuration\n\nThe `PropelAuthProvider` supports optional overrides for token introspection behavior, including caching and request timeouts:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.propelauth import PropelAuthProvider\n\nauth = PropelAuthProvider(\n    auth_url=os.environ[\"PROPELAUTH_AUTH_URL\"],\n    introspection_client_id=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_ID\"],\n    introspection_client_secret=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_SECRET\"],\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-server.com\"),\n    required_scopes=[\"read:user_data\"],\n    resource=\"https://your-server.com/mcp\",              # Restrict to tokens intended for this server (RFC 8707)\n    token_introspection_overrides={\n        \"cache_ttl_seconds\": 300,       # Cache introspection results for 5 minutes\n        \"max_cache_size\": 1000,         # Maximum cached tokens\n        \"timeout_seconds\": 15,          # HTTP request timeout\n    },\n)\n\nmcp = FastMCP(name=\"My PropelAuth Protected Server\", auth=auth)\n```\n"
  },
  {
    "path": "docs/integrations/scalekit.mdx",
    "content": "---\ntitle: Scalekit 🤝 FastMCP\nsidebarTitle: Scalekit\ndescription: Secure your FastMCP server with Scalekit\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nInstall auth stack to your FastMCP server with [Scalekit](https://scalekit.com) using the [Remote OAuth](/servers/auth/remote-oauth) pattern: Scalekit handles user authentication, and the MCP server validates issued tokens.\n\n### Prerequisites\n\nBefore you begin\n\n1. Get a [Scalekit account](https://app.scalekit.com/) and grab your **Environment URL** from _Dashboard > Settings_ .\n2. Have your FastMCP server's base URL ready (can be localhost for development, e.g., `http://localhost:8000/`)\n\n### Step 1: Configure MCP server in Scalekit environment\n\n<Steps>\n<Step title=\"Register MCP server and set environment\">\n\nIn your Scalekit dashboard:\n    1. Open the **MCP Servers** section, then select **Create new server**\n    2. Enter server details: a name, a resource identifier, and the desired MCP client authentication settings\n    3. Save, then copy the **Resource ID** (for example, res_92015146095)\n\nIn your FastMCP project's `.env`:\n\n```sh\nSCALEKIT_ENVIRONMENT_URL=<YOUR_APP_ENVIRONMENT_URL>\nSCALEKIT_RESOURCE_ID=<YOUR_APP_RESOURCE_ID> # res_926EXAMPLE5878\nBASE_URL=http://localhost:8000/\n# Optional: additional scopes tokens must have\n# SCALEKIT_REQUIRED_SCOPES=read,write\n```\n\n</Step>\n</Steps>\n\n### Step 2: Add auth to FastMCP server\n\nCreate your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:\n\n> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.scalekit import ScalekitProvider\n\n# Discovers Scalekit endpoints and set up JWT token validation\nauth_provider = ScalekitProvider(\n    environment_url=SCALEKIT_ENVIRONMENT_URL,    # Scalekit environment URL\n    resource_id=SCALEKIT_RESOURCE_ID,            # Resource server ID\n    base_url=SERVER_URL,                         # Public MCP endpoint\n    required_scopes=[\"read\"],                    # Optional scope enforcement\n)\n\n# Create FastMCP server with auth\nmcp = FastMCP(name=\"My Scalekit Protected Server\", auth=auth_provider)\n\n@mcp.tool\ndef auth_status() -> dict:\n    \"\"\"Show Scalekit authentication status.\"\"\"\n    # Extract user claims from the JWT\n    return {\n        \"message\": \"This tool requires authentication via Scalekit\",\n        \"authenticated\": True,\n        \"provider\": \"Scalekit\"\n    }\n\n```\n\n<Tip>\nSet `required_scopes` when you need tokens to carry specific permissions. Leave it unset to allow any token issued for the resource.\n</Tip>\n\n## Testing\n\n### Start the MCP server\n\n```sh\nuv run python server.py\n```\n\nUse any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running serve. Verify that authentication succeeds and requests are authorized as expected.\n\n## Production Configuration\n\nFor production deployments, load configuration from environment variables:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.scalekit import ScalekitProvider\n\n# Load configuration from environment variables\nauth = ScalekitProvider(\n    environment_url=os.environ.get(\"SCALEKIT_ENVIRONMENT_URL\"),\n    resource_id=os.environ.get(\"SCALEKIT_RESOURCE_ID\"),\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-server.com\")\n)\n\nmcp = FastMCP(name=\"My Scalekit Protected Server\", auth=auth)\n\n@mcp.tool\ndef protected_action() -> str:\n    \"\"\"A tool that requires authentication.\"\"\"\n    return \"Access granted via Scalekit!\"\n```\n\n## Capabilities\n\nScalekit supports OAuth 2.1 with Dynamic Client Registration for MCP clients and enterprise SSO, and provides built‑in JWT validation and security controls.\n\n**OAuth 2.1/DCR**: clients self‑register, use PKCE, and work with the Remote OAuth pattern without pre‑provisioned credentials.\n\n**Validation and SSO**: tokens are verified (keys, RS256, issuer, audience, expiry), and SAML, OIDC, OAuth 2.0, ADFS, Azure AD, and Google Workspace are supported; use HTTPS in production and review auth logs as needed.\n\n## Debugging\n\nEnable detailed logging to troubleshoot authentication issues:\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n```\n\n### Token inspection\n\nYou can inspect JWT tokens in your tools to understand the user context:\n\n```python\nfrom fastmcp.server.context import request_ctx\nimport jwt\n\n@mcp.tool\ndef inspect_token() -> dict:\n    \"\"\"Inspect the current JWT token claims.\"\"\"\n    context = request_ctx.get()\n\n    # Extract token from Authorization header\n    if hasattr(context, 'request') and hasattr(context.request, 'headers'):\n        auth_header = context.request.headers.get('authorization', '')\n        if auth_header.startswith('Bearer '):\n            token = auth_header[7:]\n            # Decode without verification (already verified by provider)\n            claims = jwt.decode(token, options={\"verify_signature\": False})\n            return claims\n\n    return {\"error\": \"No token found\"}\n```\n"
  },
  {
    "path": "docs/integrations/supabase.mdx",
    "content": "---\ntitle: Supabase 🤝 FastMCP\nsidebarTitle: Supabase\ndescription: Secure your FastMCP server with Supabase Auth\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nThis guide shows you how to secure your FastMCP server using **Supabase Auth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where Supabase handles user authentication and your FastMCP server validates the tokens.\n\n<Warning>\nSupabase Auth does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server.\n</Warning>\n\n## Consent UI Requirement\n\nSupabase's OAuth Server delegates the user consent screen to your application. When an MCP client initiates authorization, Supabase authenticates the user and then redirects to your application at a configured callback URL (e.g., `https://your-app.com/oauth/callback?authorization_id=...`). Your application must host a page that calls Supabase's `approveAuthorization()` or `denyAuthorization()` APIs to complete the flow.\n\n`SupabaseProvider` handles the resource server side (token verification and metadata), but you are responsible for building and hosting the consent UI separately. See [Supabase's OAuth Server documentation](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for details on implementing the authorization page.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[Supabase Account](https://supabase.com/)** with a project or a self-hosted **Supabase Auth** instance\n2. **OAuth Server enabled** in your Supabase Dashboard (Authentication → OAuth Server)\n3. **Dynamic Client Registration enabled** in the same settings\n4. A **consent UI** hosted at your configured authorization path (see above)\n5. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Enable Supabase OAuth Server\n\nIn your Supabase Dashboard:\n1. Go to **Authentication → OAuth Server**\n2. Enable the **OAuth Server**\n3. Set your **Site URL** to where your consent UI is hosted\n4. Set the **Authorization Path** (e.g., `/oauth/callback`)\n5. Enable **Allow Dynamic OAuth Apps** for MCP client registration\n\n### Step 2: Get Supabase Project URL\n\nIn your Supabase Dashboard:\n1. Go to **Project Settings**\n2. Copy your **Project URL** (e.g., `https://abc123.supabase.co`)\n\n### Step 3: FastMCP Configuration\n\nCreate your FastMCP server using the `SupabaseProvider`:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.supabase import SupabaseProvider\n\nauth = SupabaseProvider(\n    project_url=\"https://abc123.supabase.co\",\n    base_url=\"http://localhost:8000\",\n)\n\nmcp = FastMCP(\"Supabase Protected Server\", auth=auth)\n\n@mcp.tool\ndef protected_tool(message: str) -> str:\n    \"\"\"This tool requires authentication.\"\"\"\n    return f\"Authenticated user says: {message}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Supabase-protected server:\n\n```python client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        print(\"Authenticated with Supabase!\")\n\n        result = await client.call_tool(\"protected_tool\", {\"message\": \"Hello!\"})\n        print(result)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Supabase's authorization endpoint\n2. After authenticating, Supabase redirects to your consent UI\n3. After you approve, the client receives the token and can make authenticated requests\n\n## Production Configuration\n\nFor production deployments, load configuration from environment variables:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.supabase import SupabaseProvider\n\nauth = SupabaseProvider(\n    project_url=os.environ[\"SUPABASE_PROJECT_URL\"],\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-server.com\"),\n)\n\nmcp = FastMCP(name=\"Supabase Secured App\", auth=auth)\n```\n"
  },
  {
    "path": "docs/integrations/workos.mdx",
    "content": "---\ntitle: WorkOS 🤝 FastMCP\nsidebarTitle: WorkOS\ndescription: Authenticate FastMCP servers with WorkOS Connect\nicon: shield-check\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nSecure your FastMCP server with WorkOS Connect authentication. This integration uses the OAuth Proxy pattern to handle authentication through WorkOS Connect while maintaining compatibility with MCP clients.\n\n<Note>\nThis guide covers WorkOS Connect applications. For Dynamic Client Registration (DCR) with AuthKit, see the [AuthKit integration](/integrations/authkit) instead.\n</Note>\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[WorkOS Account](https://workos.com/)** with access to create OAuth Apps\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a WorkOS OAuth App\n\nCreate an OAuth App in your WorkOS dashboard to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Create OAuth Application\">\nIn your WorkOS dashboard:\n1. Navigate to **Applications**\n2. Click **Create Application** \n3. Select **OAuth Application**\n4. Name your application\n</Step>\n\n<Step title=\"Get Credentials\">\nIn your OAuth application settings:\n1. Copy your **Client ID** (starts with `client_`)\n2. Click **Generate Client Secret** and save it securely\n3. Copy your **AuthKit Domain** (e.g., `https://your-app.authkit.app`)\n</Step>\n\n<Step title=\"Configure Redirect URI\">\nIn the **Redirect URIs** section:\n- Add: `http://localhost:8000/auth/callback` (for development)\n- For production, add your server's public URL + `/auth/callback`\n\n<Warning>\nThe callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.\n</Warning>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `WorkOSProvider`:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import WorkOSProvider\n\n# Configure WorkOS OAuth\nauth = WorkOSProvider(\n    client_id=\"client_YOUR_CLIENT_ID\",\n    client_secret=\"YOUR_CLIENT_SECRET\",\n    authkit_domain=\"https://your-app.authkit.app\",\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"openid\", \"profile\", \"email\"]\n)\n\nmcp = FastMCP(\"WorkOS Protected Server\", auth=auth)\n\n@mcp.tool\ndef protected_tool(message: str) -> str:\n    \"\"\"This tool requires authentication.\"\"\"\n    return f\"Authenticated user says: {message}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by WorkOS OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your WorkOS-protected server:\n\n```python client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():    \n    # The client will automatically handle WorkOS OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open WorkOS login in your browser\n        print(\"✓ Authenticated with WorkOS!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"protected_tool\", {\"message\": \"Hello!\"})\n        print(result)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to WorkOS's authorization page\n2. After you authorize the app, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import WorkOSProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth = WorkOSProvider(\n    client_id=\"client_YOUR_CLIENT_ID\",\n    client_secret=\"YOUR_CLIENT_SECRET\",\n    authkit_domain=\"https://your-app.authkit.app\",\n    base_url=\"https://your-production-domain.com\",\n    required_scopes=[\"openid\", \"profile\", \"email\"],\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production WorkOS App\", auth=auth)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Configuration Options\n\n<Card>\n<ParamField path=\"client_id\" required>\nWorkOS OAuth application client ID\n</ParamField>\n\n<ParamField path=\"client_secret\" required>\nWorkOS OAuth application client secret\n</ParamField>\n\n<ParamField path=\"authkit_domain\" required>\nYour WorkOS AuthKit domain URL (e.g., `https://your-app.authkit.app`)\n</ParamField>\n\n<ParamField path=\"base_url\" required>\nYour FastMCP server's public URL\n</ParamField>\n\n<ParamField path=\"required_scopes\" default=\"[]\">\nOAuth scopes to request\n</ParamField>\n\n<ParamField path=\"redirect_path\" default=\"/auth/callback\">\nOAuth callback path\n</ParamField>\n\n<ParamField path=\"timeout_seconds\" default=\"10\">\nAPI request timeout\n</ParamField>\n</Card>"
  },
  {
    "path": "docs/more/settings.mdx",
    "content": "---\ntitle: Settings\ndescription: Configure FastMCP behavior through environment variables or a .env file.\nicon: gear\n---\n\nFastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file (see the [Tasks (Docket)](#tasks-docket) section for a caveat about nested settings in `.env` files).\n\n```bash\n# Set via environment\nexport FASTMCP_LOG_LEVEL=DEBUG\nexport FASTMCP_PORT=3000\n\n# Or use a .env file (loaded automatically)\necho \"FASTMCP_LOG_LEVEL=DEBUG\" >> .env\n```\n\nYou can change which `.env` file is loaded by setting the `FASTMCP_ENV_FILE` environment variable (defaults to `.env`). Because this controls which file is loaded, it must be set as an environment variable — it cannot be set inside a `.env` file itself.\n\n## Logging\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_LOG_LEVEL` | `Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]` | `INFO` | Log level for FastMCP's own logging output. Case-insensitive. |\n| `FASTMCP_LOG_ENABLED` | `bool` | `true` | Enable or disable FastMCP logging entirely. |\n| `FASTMCP_CLIENT_LOG_LEVEL` | `Literal[\"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\", \"alert\", \"emergency\"]` | None | Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Individual clients can override this per-session using the MCP `logging/setLevel` request. |\n| `FASTMCP_ENABLE_RICH_LOGGING` | `bool` | `true` | Use rich formatting for log output. Set to `false` for plain Python logging. |\n| `FASTMCP_ENABLE_RICH_TRACEBACKS` | `bool` | `true` | Use rich tracebacks for errors. |\n| `FASTMCP_DEPRECATION_WARNINGS` | `bool` | `true` | Show deprecation warnings. |\n\n## Transport & HTTP\n\nThese control how the server listens when running with an HTTP transport.\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_TRANSPORT` | `Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"]` | `stdio` | Default transport. |\n| `FASTMCP_HOST` | `str` | `127.0.0.1` | Host to bind to. |\n| `FASTMCP_PORT` | `int` | `8000` | Port to bind to. |\n| `FASTMCP_SSE_PATH` | `str` | `/sse` | Path for SSE endpoint. |\n| `FASTMCP_MESSAGE_PATH` | `str` | `/messages/` | Path for SSE message endpoint. |\n| `FASTMCP_STREAMABLE_HTTP_PATH` | `str` | `/mcp` | Path for Streamable HTTP endpoint. |\n| `FASTMCP_STATELESS_HTTP` | `bool` | `false` | Enable stateless HTTP mode (new transport per request). Useful for multi-worker deployments. |\n| `FASTMCP_JSON_RESPONSE` | `bool` | `false` | Use JSON responses instead of SSE for Streamable HTTP. |\n| `FASTMCP_DEBUG` | `bool` | `false` | Enable debug mode. |\n\n## Error Handling\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_MASK_ERROR_DETAILS` | `bool` | `false` | Mask error details before sending to clients. When enabled, only messages from explicitly raised `ToolError`, `ResourceError`, or `PromptError` are included in responses. |\n| `FASTMCP_STRICT_INPUT_VALIDATION` | `bool` | `false` | Strictly validate tool inputs against the JSON schema. When disabled, compatible inputs are coerced (e.g., the string `\"10\"` becomes the integer `10`). |\n| `FASTMCP_MOUNTED_COMPONENTS_RAISE_ON_LOAD_ERROR` | `bool` | `false` | Raise errors when loading mounted components instead of logging warnings. |\n\n## Client\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_CLIENT_INIT_TIMEOUT` | `float \\| None` | None | Timeout in seconds for the client initialization handshake. Set to `0` or leave unset to disable. |\n| `FASTMCP_CLIENT_DISCONNECT_TIMEOUT` | `float` | `5` | Maximum time in seconds to wait for a clean disconnect before giving up. |\n| `FASTMCP_CLIENT_RAISE_FIRST_EXCEPTIONGROUP_ERROR` | `bool` | `true` | When an `ExceptionGroup` is raised, re-raise the first error directly instead of the group. Simplifies debugging but may mask secondary errors. |\n\n## CLI & Display\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_SHOW_SERVER_BANNER` | `bool` | `true` | Show the server banner on startup. Also controllable via `--no-banner` or `server.run(show_banner=False)`. |\n| `FASTMCP_CHECK_FOR_UPDATES` | `Literal[\"stable\", \"prerelease\", \"off\"]` | `stable` | Update checking on CLI startup. `stable` checks stable releases only, `prerelease` includes pre-releases, `off` disables checking. |\n\n## Tasks (Docket)\n\nThese configure the [Docket](https://github.com/prefecthq/docket) task queue used by [server tasks](/servers/tasks). All use the `FASTMCP_DOCKET_` prefix.\n\n<Warning>\nWhen setting Docket values in a `.env` file, use a **double** underscore: `FASTMCP_DOCKET__URL` (not `FASTMCP_DOCKET_URL`). This is because `.env` values are resolved through the parent `Settings` class, which uses `__` as its nested delimiter. As regular environment variables (e.g., `export`), the single-underscore form `FASTMCP_DOCKET_URL` works fine.\n</Warning>\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_DOCKET_NAME` | `str` | `fastmcp` | Queue name. Servers and workers sharing the same name and backend URL share a task queue. |\n| `FASTMCP_DOCKET_URL` | `str` | `memory://` | Backend URL. Use `memory://` for single-process or `redis://host:port/db` for distributed workers. |\n| `FASTMCP_DOCKET_WORKER_NAME` | `str \\| None` | None | Worker name. Auto-generated if unset. |\n| `FASTMCP_DOCKET_CONCURRENCY` | `int` | `10` | Maximum concurrent tasks per worker. |\n| `FASTMCP_DOCKET_REDELIVERY_TIMEOUT` | `timedelta` | `300s` | If a worker doesn't complete a task within this time, it's redelivered to another worker. |\n| `FASTMCP_DOCKET_RECONNECTION_DELAY` | `timedelta` | `5s` | Delay between reconnection attempts when the worker loses its backend connection. |\n| `FASTMCP_DOCKET_MINIMUM_CHECK_INTERVAL` | `timedelta` | `50ms` | How frequently the worker polls for new tasks. Lower values reduce latency at the cost of more CPU usage. |\n\n## Advanced\n\n| Environment Variable | Type | Default | Description |\n|---|---|---|---|\n| `FASTMCP_HOME` | `Path` | Platform default | Data directory for FastMCP. Defaults to the platform-specific user data directory. |\n| `FASTMCP_ENV_FILE` | `str` | `.env` | Path to the `.env` file to load settings from. Must be set as an environment variable (see above). |\n| `FASTMCP_SERVER_DEPENDENCIES` | `list[str]` | `[]` | Additional dependencies to install in the server environment. |\n| `FASTMCP_DECORATOR_MODE` | `Literal[\"function\", \"object\"]` | `function` | Controls what `@tool`, `@resource`, and `@prompt` decorators return. `function` returns the original function (default); `object` returns component objects (deprecated, will be removed). |\n| `FASTMCP_TEST_MODE` | `bool` | `false` | Enable test mode. |\n"
  },
  {
    "path": "docs/patterns/cli.mdx",
    "content": "---\ntitle: FastMCP CLI\nsidebarTitle: CLI\ndescription: Learn how to use the FastMCP command-line interface\nicon: terminal\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n\nFastMCP provides a command-line interface (CLI) that makes it easy to run, develop, and install your MCP servers. The CLI is automatically installed when you install FastMCP.\n\n```bash\nfastmcp --help\n```\n\n## Commands Overview\n\n| Command | Purpose | Dependency Management |\n| ------- | ------- | --------------------- |\n| `list` | List tools on any MCP server | **Supports:** URLs, local files, MCPConfig JSON, stdio commands. **Deps:** N/A (connects to existing servers) |\n| `call` | Call a tool on any MCP server | **Supports:** URLs, local files, MCPConfig JSON, stdio commands. **Deps:** N/A (connects to existing servers) |\n| `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, fastmcp.json configs, MCP configs. **Deps:** Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess. With fastmcp.json: Automatically manages dependencies based on configuration |\n| `dev` | Run a server with the MCP Inspector for testing | **Supports:** Local files and fastmcp.json configs. **Deps:** Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project. With fastmcp.json: Uses configured dependencies |\n| `install` | Install a server in MCP client applications | **Supports:** Local files and fastmcp.json configs. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable`. With fastmcp.json: Uses configured dependencies |\n| `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files and fastmcp.json configs. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available |\n| `project prepare` | Create a persistent uv project from fastmcp.json environment config | **Supports:** fastmcp.json configs only. **Deps:** Creates a uv project directory with all dependencies pre-installed for reuse with `--project` flag |\n| `auth cimd` | Create and validate CIMD documents for OAuth authentication | N/A |\n| `version` | Display version information | N/A |\n\n## `fastmcp list`\n\nList tools available on any MCP server. This works with remote URLs, local Python files, MCPConfig JSON files, and arbitrary stdio commands. Together with `fastmcp call`, these commands are especially useful for giving LLMs that don't have built-in MCP support access to MCP tools via shell commands.\n\n```bash\nfastmcp list http://localhost:8000/mcp\nfastmcp list server.py\nfastmcp list mcp.json\nfastmcp list --command 'npx -y @modelcontextprotocol/server-github'\n```\n\nBy default, the output shows each tool's signature and description. Use `--input-schema` or `--output-schema` to include full JSON schemas, or `--json` for machine-readable output.\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Command | `--command` | Connect to a stdio server command (e.g. `'npx -y @mcp/server'`) |\n| Transport | `--transport`, `-t` | Force transport type for URL targets (`http` or `sse`) |\n| Resources | `--resources` | Also list resources |\n| Prompts | `--prompts` | Also list prompts |\n| Input Schema | `--input-schema` | Show full input schemas |\n| Output Schema | `--output-schema` | Show full output schemas |\n| JSON | `--json` | Output as JSON |\n| Timeout | `--timeout` | Connection timeout in seconds |\n| Auth | `--auth` | Auth method: `oauth` (default for HTTP), a bearer token, or `none` to disable |\n\n### Server Targets\n\nThe `<server>` argument accepts:\n\n1. **URLs** — `http://` or `https://` endpoints. Uses Streamable HTTP by default; pass `--transport sse` for SSE servers.\n2. **Python files** — `.py` files are run via `fastmcp run` automatically.\n3. **MCPConfig JSON** — `.json` files with an `mcpServers` key are treated as multi-server configs.\n4. **Stdio commands** — Use `--command` to connect to any MCP server via stdio (e.g. `npx`, `uvx`).\n\n### Examples\n\n```bash\n# List tools on a remote server\nfastmcp list http://localhost:8000/mcp\n\n# List tools from a local Python file\nfastmcp list server.py\n\n# Include full input schemas\nfastmcp list server.py --input-schema\n\n# Machine-readable JSON\nfastmcp list server.py --json\n\n# SSE server\nfastmcp list http://localhost:8000/mcp --transport sse\n\n# Stdio command\nfastmcp list --command 'npx -y @modelcontextprotocol/server-github'\n\n# Include resources and prompts\nfastmcp list server.py --resources --prompts\n```\n\n## `fastmcp call`\n\nCall a tool on any MCP server. Arguments can be passed as `key=value` pairs, a single JSON object, or via `--input-json`.\n\n```bash\nfastmcp call server.py greet name=World\nfastmcp call http://localhost:8000/mcp search query=hello limit=5\nfastmcp call server.py create_item '{\"name\": \"x\", \"tags\": [\"a\", \"b\"]}'\n```\n\nTool arguments are automatically coerced to the correct type based on the tool's input schema — string values like `limit=5` become integers when the schema expects one.\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Command | `--command` | Connect to a stdio server command (e.g. `'npx -y @mcp/server'`) |\n| Transport | `--transport`, `-t` | Force transport type for URL targets (`http` or `sse`) |\n| Input JSON | `--input-json` | JSON string of tool arguments (merged with key=value args) |\n| JSON | `--json` | Output raw JSON result |\n| Timeout | `--timeout` | Connection timeout in seconds |\n| Auth | `--auth` | Auth method: `oauth` (default for HTTP), a bearer token, or `none` to disable |\n\n### Argument Passing\n\nThere are three ways to pass arguments:\n\n**Key=value pairs** are the simplest for flat arguments. Values are coerced using the tool's JSON schema (strings become ints, bools, etc.):\n\n```bash\nfastmcp call server.py search query=hello limit=5 verbose=true\n```\n\n**A single JSON object** works when you have structured or nested arguments:\n\n```bash\nfastmcp call server.py create_item '{\"name\": \"Widget\", \"tags\": [\"new\", \"sale\"]}'\n```\n\n**`--input-json`** provides a base dict that key=value pairs can override:\n\n```bash\nfastmcp call server.py search --input-json '{\"query\": \"hello\", \"limit\": 5}' limit=10\n```\n\n### Examples\n\n```bash\n# Call a tool with simple args\nfastmcp call server.py greet name=World\n\n# Call with JSON object\nfastmcp call server.py create '{\"name\": \"x\", \"tags\": [\"a\"]}'\n\n# Get JSON output for scripting\nfastmcp call server.py add a=3 b=4 --json\n\n# Call a tool on a remote server\nfastmcp call http://localhost:8000/mcp search query=hello\n\n# Call via stdio command\nfastmcp call --command 'npx -y @mcp/server' tool_name arg=value\n\n# Disable OAuth for HTTP targets\nfastmcp call http://localhost:8000/mcp search query=hello --auth none\n```\n\n<Tip>\nIf you call a tool that doesn't exist, FastMCP will suggest similar tool names. Use `fastmcp list` to see all available tools on a server.\n</Tip>\n\n## `fastmcp run`\n\nRun a FastMCP server directly or proxy a remote server.\n\n```bash\nfastmcp run server.py\n```\n\n<Tip>\nBy default, this command runs the server directly in your current Python environment. You are responsible for ensuring all dependencies are available. When using `--python`, `--with`, `--project`, or `--with-requirements` options, it runs the server via `uv run` subprocess instead.\n</Tip>\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Transport | `--transport`, `-t` | Transport protocol to use (`stdio`, `http`, or `sse`) |\n| Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) |\n| Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) |\n| Path | `--path` | Path to bind to when using http transport (default: `/mcp/` or `/sse/` for SSE) |\n| Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |\n| No Banner | `--no-banner` | Disable the startup banner display |\n| Auto-Reload | `--reload` / `--no-reload` | Enable auto-reload on file changes (development mode) |\n| Reload Directories | `--reload-dir` | Directories to watch for changes (can be used multiple times) |\n| No Environment | `--skip-env` | Skip environment setup with uv (use when already in a uv environment) |\n| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |\n| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |\n| Project Directory | `--project` | Run the command within the given project directory |\n| Requirements File | `--with-requirements` | Requirements file to install dependencies from |\n\n\n### Entrypoints\n<VersionBadge version=\"2.3.5\" />\n\nThe `fastmcp run` command supports the following entrypoints:\n\n1. **[Inferred server instance](#inferred-server-instance)**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **[Explicit server entrypoint](#explicit-server-entrypoint)**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server**\n5. **[FastMCP configuration file](#fastmcp-configuration)**: `fastmcp.json` - runs servers using FastMCP's declarative configuration format (auto-detects files in current directory)\n6. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file\n\n<Warning>\nNote: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == \"__main__\"` block. This means:\n- Any setup code in `__main__` will NOT run\n- Server configuration in `__main__` is bypassed  \n- `fastmcp run` finds your server entrypoint/factory and runs it with its own transport settings\n\nIf you need setup code to run, use the **factory pattern** instead.\n</Warning>\n\n#### Inferred Server Instance\n\nIf you provide a path to a file, `fastmcp run` will load the file and look for a FastMCP server instance stored as a variable named `mcp`, `server`, or `app`. If no such object is found, it will raise an error.\n\nFor example, if you have a file called `server.py` with the following content:\n\n```python server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n```\n\nYou can run it with:\n\n```bash\nfastmcp run server.py\n```\n\n#### Explicit Server Entrypoint\n\nIf your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server entrypoint:\n\n```bash\nfastmcp run server.py:custom_name\n```\n\nFor example, if you have a file called `server.py` with the following content:\n\n```python\nfrom fastmcp import FastMCP\n\nmy_server = FastMCP(\"CustomServer\")\n\n@my_server.tool\ndef hello() -> str:\n    return \"Hello from custom server!\"\n```\n\nYou can run it with:\n\n```bash\nfastmcp run server.py:my_server\n```\n\n#### Factory Function\n<VersionBadge version=\"2.11.2\" />\n\nSince `fastmcp run` ignores the `if __name__ == \"__main__\"` block, you can use a factory function to run setup code before your server starts. Factory functions are called without any arguments and must return a FastMCP server instance. Both sync and async factory functions are supported.\n\nThe syntax for using a factory function is the same as for an explicit server entrypoint: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance\n\nFor example, if you have a file called `server.py` with the following content:\n\n```python\nfrom fastmcp import FastMCP\n\nasync def create_server() -> FastMCP:\n    mcp = FastMCP(\"MyServer\")\n    \n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        return x + y\n    \n    # Setup that runs with fastmcp run\n    tool = await mcp.get_tool(\"add\")\n    tool.disable()\n    \n    return mcp\n```\n\nYou can run it with:\n\n```bash\nfastmcp run server.py:create_server\n```\n\n#### Remote Server Proxy\n\nFastMCP run can also start a local proxy server that connects to a remote server. This is useful when you want to run a remote server locally for testing or development purposes, or to use with a client that doesn't support direct connections to remote servers.\n\nTo start a local proxy, you can use the following syntax:\n\n```bash\nfastmcp run https://example.com/mcp\n```\n\n#### FastMCP Configuration\n<VersionBadge version=\"2.11.4\" />\n\nFastMCP supports declarative configuration through `fastmcp.json` files. When you run `fastmcp run` without arguments, it automatically looks for a `fastmcp.json` file in the current directory:\n\n```bash\n# Auto-detect fastmcp.json in current directory\nfastmcp run\n\n# Or explicitly specify a configuration file\nfastmcp run my-config.fastmcp.json\n```\n\nThe configuration file handles dependencies, environment variables, and transport settings. Command-line arguments override configuration file values:\n\n```bash\n# Override port from config file\nfastmcp run fastmcp.json --port 8080\n\n# Skip environment setup when already in a uv environment\nfastmcp run fastmcp.json --skip-env\n```\n\n<Note>\nThe `--skip-env` flag is useful when:\n- You're already in an activated virtual environment\n- You're inside a Docker container with pre-installed dependencies  \n- You're in a uv-managed environment (prevents infinite recursion)\n- You want to test the server without environment setup\n</Note>\n\nSee [Server Configuration](/deployment/server-configuration) for detailed documentation on fastmcp.json.\n\n#### MCP Configuration\n\nFastMCP can also run servers defined in a standard MCP configuration file. This is useful when you want to run multiple servers from a single file, or when you want to use a client that doesn't support direct connections to remote servers.\n\nTo run a MCP configuration file, you can use the following syntax:\n\n```bash\nfastmcp run mcp.json\n```\n\nThis will run all the servers defined in the file.\n\n## `fastmcp dev`\n\nThe `dev` command group contains development tools for MCP servers.\n\n### `fastmcp dev inspector`\n\nRun a MCP server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for testing. Auto-reload is enabled by default, so your server automatically restarts when you save changes to source files.\n\n```bash\nfastmcp dev inspector server.py\n```\n\n<Tip>\nThis command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. Dependencies can be:\n- Specified using `--with` and/or `--with-editable` options\n- Defined in a `fastmcp.json` configuration file\n- Available in a uv-managed project\n\nWhen using `fastmcp.json`, the dev command automatically uses the configured dependencies.\n</Tip>\n\n<Warning>\nThe `dev inspector` command is a shortcut for testing a server over STDIO only. When the Inspector launches, you may need to:\n1. Select \"STDIO\" from the transport dropdown\n2. Connect manually\n\nThis command does not support HTTP testing. To test a server over Streamable HTTP or SSE:\n1. Start your server manually with the appropriate transport using either the command line:\n   ```bash\n   fastmcp run server.py --transport http\n   ```\n   or by setting the transport in your code:\n   ```bash\n   python server.py  # Assuming your __main__ block sets Streamable HTTP transport\n   ```\n2. Open the MCP Inspector separately and connect to your running server\n</Warning>\n\n#### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Editable Package | `--with-editable`, `-e` | Directory containing pyproject.toml to install in editable mode |\n| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |\n| Inspector Version | `--inspector-version` | Version of the MCP Inspector to use |\n| UI Port | `--ui-port` | Port for the MCP Inspector UI |\n| Server Port | `--server-port` | Port for the MCP Inspector Proxy server |\n| Auto-Reload | `--reload` / `--no-reload` | Enable/disable auto-reload on file changes (enabled by default) |\n| Reload Directories | `--reload-dir` | Directories to watch for changes (can be used multiple times) |\n| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |\n| Project Directory | `--project` | Run the command within the given project directory |\n| Requirements File | `--with-requirements` | Requirements file to install dependencies from |\n\n#### Entrypoints\n\nThe `dev inspector` command supports local FastMCP server files and configuration:\n\n1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint\n3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration (auto-detects in current directory)\n\n<Warning>\nThe `dev inspector` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.\n</Warning>\n\n**Examples**\n\n```bash\n# Run dev server with editable mode and additional packages\nfastmcp dev inspector server.py -e . --with pandas --with matplotlib\n\n# Run dev server with fastmcp.json configuration (auto-detects)\nfastmcp dev inspector\n\n# Run dev server with explicit fastmcp.json file\nfastmcp dev inspector dev.fastmcp.json\n\n# Run dev server with specific Python version\nfastmcp dev inspector server.py --python 3.11\n\n# Run dev server with requirements file\nfastmcp dev inspector server.py --with-requirements requirements.txt\n\n# Run dev server within a specific project directory\nfastmcp dev inspector server.py --project /path/to/project\n```\n\n## `fastmcp install`\n<VersionBadge version=\"2.10.3\" />\n\nInstall a MCP server in MCP client applications. FastMCP currently supports the following clients:\n\n- **Claude Code** - Installs via Claude Code's built-in MCP management system\n- **Claude Desktop** - Installs via direct configuration file modification\n- **Cursor** - Installs via deeplink that opens Cursor for user confirmation\n- **Gemini CLI** - Installs via Gemini CLI's built-in MCP management system\n- **Goose** - Installs via deeplink that opens Goose for user confirmation (uses `uvx`)\n- **MCP JSON** - Generates standard MCP JSON configuration for manual use\n- **Stdio** - Outputs the shell command to run a server over stdio transport\n\n```bash\nfastmcp install claude-code server.py\nfastmcp install claude-desktop server.py\nfastmcp install cursor server.py\nfastmcp install gemini-cli server.py\nfastmcp install goose server.py\nfastmcp install mcp-json server.py\nfastmcp install stdio server.py\n```\n\nNote that for security reasons, MCP clients usually run every server in a completely isolated environment. Therefore, all dependencies must be explicitly specified using the `--with` and/or `--with-editable` options (following `uv` conventions) or by attaching them to your server in code via the `dependencies` parameter. You should not assume that the MCP server will have access to your local environment.\n\n<Warning>\n**`uv` must be installed and available in your system PATH**. Both Claude Desktop and Cursor run in isolated environments and need `uv` to manage dependencies. On macOS, install `uv` globally with Homebrew for Claude Desktop compatibility: `brew install uv`.\n</Warning>\n\n<Note>\n**Python Version Considerations**: The install commands now support the `--python` option to specify a Python version directly. You can also use `--project` to run within a specific project directory or `--with-requirements` to install dependencies from a requirements file.\n</Note>\n\n<Tip>\n**FastMCP `install` commands focus on local server files with STDIO transport.** For remote servers running with HTTP or SSE transport, use your client's native configuration - FastMCP's value is simplifying the complex local setup with dependencies and `uv` commands.\n</Tip>\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Server Name | `--server-name`, `-n` | Custom name for the server (defaults to server's name attribute or file name) |\n| Editable Package | `--with-editable`, `-e` | Directory containing pyproject.toml to install in editable mode |\n| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |\n| Environment Variables | `--env` | Environment variables in KEY=VALUE format (can be used multiple times) |\n| Environment File | `--env-file`, `-f` | Load environment variables from a .env file |\n| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |\n| Project Directory | `--project` | Run the command within the given project directory |\n| Requirements File | `--with-requirements` | Requirements file to install dependencies from |\n\n### Entrypoints\n\nThe `install` command supports local FastMCP server files and configuration:\n\n1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration with dependencies and settings\n\n<Note>\nFactory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server. When using fastmcp.json, dependencies are automatically handled.\n</Note>\n\n<Warning>\nThe `install` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files. For remote servers, use your MCP client's native configuration.\n</Warning>\n\n**Examples**\n\n```bash\n# Auto-detects server entrypoint (looks for 'mcp', 'server', or 'app')\nfastmcp install claude-desktop server.py\n\n# Install with fastmcp.json configuration (auto-detects)\nfastmcp install claude-desktop\n\n# Install with explicit fastmcp.json file\nfastmcp install claude-desktop my-config.fastmcp.json\n\n# Uses specific server entrypoint\nfastmcp install claude-desktop server.py:my_server\n\n# With custom name and dependencies\nfastmcp install claude-desktop server.py:my_server --server-name \"My Analysis Server\" --with pandas\n\n# Install in Claude Code with environment variables\nfastmcp install claude-code server.py --env API_KEY=secret --env DEBUG=true\n\n# Install in Cursor with environment variables\nfastmcp install cursor server.py --env API_KEY=secret --env DEBUG=true\n\n# Install with environment file\nfastmcp install cursor server.py --env-file .env\n\n# Install in Goose (uses uvx deeplink)\nfastmcp install goose server.py --with pandas\n\n# Install with specific Python version\nfastmcp install claude-desktop server.py --python 3.11\n\n# Install with requirements file\nfastmcp install claude-code server.py --with-requirements requirements.txt\n\n# Install within a project directory\nfastmcp install cursor server.py --project /path/to/project\n\n# Generate MCP JSON configuration\nfastmcp install mcp-json server.py --name \"My Server\" --with pandas\n\n# Copy JSON configuration to clipboard\nfastmcp install mcp-json server.py --copy\n\n# Output the stdio command for running a server\nfastmcp install stdio server.py\n\n# Output the stdio command from a fastmcp.json (includes configured dependencies)\nfastmcp install stdio fastmcp.json\n\n# Copy the stdio command to clipboard\nfastmcp install stdio server.py --copy\n```\n\n### MCP JSON Generation\n\nThe `mcp-json` subcommand generates standard MCP JSON configuration that can be used with any MCP-compatible client. This is useful when:\n\n- Working with MCP clients not directly supported by FastMCP\n- Creating configuration for CI/CD environments  \n- Sharing server configurations with others\n- Integration with custom tooling\n\nThe generated JSON follows the standard MCP server configuration format used by Claude Desktop, VS Code, Cursor, and other MCP clients, with the server name as the root key:\n\n```json\n{\n  \"server-name\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\",\n      \"fastmcp\",\n      \"run\",\n      \"/path/to/server.py\"\n    ],\n    \"env\": {\n      \"API_KEY\": \"value\"\n    }\n  }\n}\n```\n\n<Note>\nTo use this configuration with your MCP client, you'll typically need to add it to the client's `mcpServers` object. Consult your client's documentation for any specific configuration requirements or formatting needs.\n</Note>\n\n**Options specific to mcp-json:**\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Copy to Clipboard | `--copy` | Copy configuration to clipboard instead of printing to stdout |\n\n### Stdio Command\n\nThe `stdio` subcommand outputs the shell command an MCP host uses to start your server over stdio transport. Use it when you need a ready-to-paste `uv run --with fastmcp fastmcp run ...` command for a tool or script without a dedicated install target.\n\n```bash\n# Print the command to stdout\nfastmcp install stdio server.py\n\n# Output: uv run --with fastmcp fastmcp run /absolute/path/to/server.py\n```\n\nWhen you pass a `fastmcp.json`, FastMCP automatically includes dependencies from the configuration:\n\n```bash\nfastmcp install stdio fastmcp.json\n\n# Output: uv run --with fastmcp --with pillow --with 'qrcode[pil]>=8.0' fastmcp run /absolute/path/to/qr_server.py\n```\n\nUse `--copy` to send the command directly to your clipboard:\n\n```bash\nfastmcp install stdio server.py --copy\n# ✓ Command copied to clipboard\n```\n\n**Options specific to stdio:**\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Copy to Clipboard | `--copy` | Copy command to clipboard instead of printing to stdout |\n\n## `fastmcp inspect`\n\n<VersionBadge version=\"2.9.0\" />\n\nInspect a FastMCP server to view summary information or generate a detailed JSON report.\n\n```bash\n# Show text summary\nfastmcp inspect server.py\n\n# Output FastMCP JSON to stdout\nfastmcp inspect server.py --format fastmcp\n\n# Save MCP JSON to file (format required with -o)\nfastmcp inspect server.py --format mcp -o manifest.json\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Format | `--format`, `-f` | Output format: `fastmcp` (FastMCP-specific) or `mcp` (MCP protocol). Required when using `-o` |\n| Output File | `--output`, `-o` | Save JSON report to file instead of stdout. Requires `--format` |\n\n### Output Formats\n\n#### FastMCP Format (`--format fastmcp`)\nThe default and most comprehensive format, includes all FastMCP-specific metadata:\n- Server name, instructions, and version\n- FastMCP version and MCP version\n- Tool tags and enabled status\n- Output schemas for tools\n- Annotations and custom metadata\n- Uses snake_case field names\n- **Use this for**: Complete server introspection and debugging FastMCP servers\n\n#### MCP Protocol Format (`--format mcp`)\nShows exactly what MCP clients will see via the protocol:\n- Only includes standard MCP protocol fields\n- Matches output from `client.list_tools()`, `client.list_prompts()`, etc.\n- Uses camelCase field names (e.g., `inputSchema`)\n- Excludes FastMCP-specific fields like tags and enabled status\n- **Use this for**: Debugging client visibility and ensuring MCP compatibility\n\n### Entrypoints\n\nThe `inspect` command supports local FastMCP server files and configuration:\n\n1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **FastMCP configuration**: `fastmcp.json` - inspects servers defined with FastMCP's declarative configuration\n\n<Warning>\nThe `inspect` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.\n</Warning>\n\n### Examples\n\n```bash\n# Show text summary (no JSON output)\nfastmcp inspect server.py\n# Output: \n# Server: MyServer\n# Instructions: A helpful MCP server\n# Version: 1.0.0\n#\n# Components:\n#   Tools: 5\n#   Prompts: 2\n#   Resources: 3\n#   Templates: 1\n#\n# Environment:\n#   FastMCP: 2.0.0\n#   MCP: 1.0.0\n#\n# Use --format [fastmcp|mcp] for complete JSON output\n\n# Output FastMCP format to stdout\nfastmcp inspect server.py --format fastmcp\n\n# Specify server entrypoint\nfastmcp inspect server.py:my_server\n\n# Output MCP protocol format to stdout\nfastmcp inspect server.py --format mcp\n\n# Save to file (format required)\nfastmcp inspect server.py --format fastmcp -o server-manifest.json\n\n# Save MCP format with custom server object\nfastmcp inspect server.py:my_server --format mcp -o mcp-manifest.json\n\n# Error: format required with output file\nfastmcp inspect server.py -o output.json\n# Error: --format is required when using -o/--output\n```\n\n## `fastmcp project prepare`\n\nCreate a persistent uv project directory from a fastmcp.json file's environment configuration. This allows you to pre-install all dependencies once and reuse them with the `--project` flag.\n\n```bash\nfastmcp project prepare fastmcp.json --output-dir ./env\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Output Directory | `--output-dir` | **Required.** Directory where the persistent uv project will be created |\n\n### Usage Pattern\n\n```bash\n# Step 1: Prepare the environment (installs dependencies)\nfastmcp project prepare fastmcp.json --output-dir ./my-env\n\n# Step 2: Run using the prepared environment (fast, no dependency installation)\nfastmcp run fastmcp.json --project ./my-env\n```\n\nThe prepare command creates a uv project with:\n- A `pyproject.toml` containing all dependencies from the fastmcp.json\n- A `.venv` with all packages pre-installed\n- A `uv.lock` file for reproducible environments\n\nThis is useful when you want to separate environment setup from server execution, such as in deployment scenarios where dependencies are installed once and the server is run multiple times.\n\n## `fastmcp auth`\n\n<VersionBadge version=\"3.0.0\" />\n\nAuthentication-related utilities and configuration commands.\n\n### `fastmcp auth cimd create`\n\nGenerate a CIMD (Client ID Metadata Document) for hosting. This creates a JSON document that you can host at an HTTPS URL to use as your OAuth client identity.\n\n```bash\nfastmcp auth cimd create --name \"My App\" --redirect-uri \"http://localhost:*/callback\"\n```\n\n#### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Name | `--name` | **Required.** Human-readable name of the client application |\n| Redirect URI | `--redirect-uri` | **Required.** Allowed redirect URIs (can specify multiple) |\n| Client URI | `--client-uri` | URL of the client's home page |\n| Logo URI | `--logo-uri` | URL of the client's logo image |\n| Scope | `--scope` | Space-separated list of scopes the client may request |\n| Output | `--output`, `-o` | Output file path (default: stdout) |\n| Pretty | `--pretty` | Pretty-print JSON output (default: true) |\n\n#### Example\n\n```bash\n# Generate document to stdout\nfastmcp auth cimd create \\\n    --name \"My Production App\" \\\n    --redirect-uri \"http://localhost:*/callback\" \\\n    --redirect-uri \"https://myapp.example.com/callback\" \\\n    --client-uri \"https://myapp.example.com\" \\\n    --scope \"read write\"\n\n# Save to file\nfastmcp auth cimd create \\\n    --name \"My App\" \\\n    --redirect-uri \"http://localhost:*/callback\" \\\n    --output client.json\n```\n\nThe generated document includes a placeholder `client_id` that you must update to match the URL where you'll host the document before deploying.\n\n### `fastmcp auth cimd validate`\n\nValidate a hosted CIMD document by fetching it from its URL and checking that it conforms to the CIMD specification.\n\n```bash\nfastmcp auth cimd validate https://myapp.example.com/oauth/client.json\n```\n\n#### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Timeout | `--timeout`, `-t` | HTTP request timeout in seconds (default: 10) |\n\nThe validator checks:\n\n- The URL is a valid CIMD URL (HTTPS with non-root path)\n- The document is valid JSON and conforms to the CIMD schema\n- The `client_id` field in the document matches the URL\n- No shared-secret authentication methods are used\n\nOn success, it displays the document details:\n\n```\n→ Fetching https://myapp.example.com/oauth/client.json...\n✓ Valid CIMD document\n\nDocument details:\n  client_id: https://myapp.example.com/oauth/client.json\n  client_name: My App\n  token_endpoint_auth_method: none\n  redirect_uris:\n    • http://localhost:*/callback\n```\n\n## `fastmcp version`\n\nDisplay version information about FastMCP and related components.\n\n```bash\nfastmcp version\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Copy to Clipboard | `--copy` | Copy version information to clipboard |\n"
  },
  {
    "path": "docs/patterns/contrib.mdx",
    "content": "---\ntitle: \"Contrib Modules\"\ndescription: \"Community-contributed modules extending FastMCP\"\nicon: \"cubes\"\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.2.1\" />\n\nFastMCP includes a `contrib` package that holds community-contributed modules. These modules extend FastMCP's functionality but aren't officially maintained by the core team.\n\nContrib 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.\n\nThe available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/contrib).\n\n## Usage\n\nTo use a contrib module, import it from the `fastmcp.contrib` package:\n\n```python\nfrom fastmcp.contrib import my_module\n```\n\n## Important Considerations\n\n- **Stability**: Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.\n- **Compatibility**: Changes to core FastMCP might break modules in `contrib` without explicit warnings in the main changelog.\n- **Dependencies**: Contrib modules may have additional dependencies not required by the core library. These dependencies are typically documented in the module's README or separate requirements files.\n\n## Contributing\n\nWe welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:\n\n1. Create a new directory in `src/fastmcp/contrib/` for your module\n3. Add proper tests for your module in `tests/contrib/`\n2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions\n5. Submit a pull request\n\nThe ideal contrib module:\n- Solves a specific use case or integration need\n- Follows FastMCP coding standards\n- Includes thorough documentation and examples\n- Has comprehensive tests\n- Specifies any additional dependencies\n"
  },
  {
    "path": "docs/patterns/testing.mdx",
    "content": "---\ntitle: Testing your FastMCP Server\nsidebarTitle: Testing\ndescription: How to test your FastMCP server.\nicon: vial\n---\n\nThe best way to ensure a reliable and maintainable FastMCP Server is to test it! The FastMCP Client combined with Pytest provides a simple and powerful way to test your FastMCP servers.\n\n## Prerequisites\n\nTesting FastMCP servers requires `pytest-asyncio` to handle async test functions and fixtures. Install it as a development dependency:\n\n```bash\npip install pytest-asyncio\n```\n\nWe recommend configuring pytest to automatically handle async tests by setting the asyncio mode to `auto` in your `pyproject.toml`:\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\n```\n\nThis eliminates the need to decorate every async test with `@pytest.mark.asyncio`.\n\n## Testing with Pytest Fixtures\n\nUsing Pytest Fixtures, you can wrap your FastMCP Server in a Client instance that makes interacting with your server fast and easy. This is especially useful when building your own MCP Servers and enables a tight development loop by allowing you to avoid using a separate tool like MCP Inspector during development:\n\n```python\nimport pytest\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\n\nfrom my_project.main import mcp\n\n@pytest.fixture\nasync def main_mcp_client():\n    async with Client(transport=mcp) as mcp_client:\n        yield mcp_client\n\nasync def test_list_tools(main_mcp_client: Client[FastMCPTransport]):\n    list_tools = await main_mcp_client.list_tools()\n\n    assert len(list_tools) == 5\n```\n\nWe recommend the [inline-snapshot library](https://github.com/15r10nk/inline-snapshot) for asserting complex data structures coming from your MCP Server. This library allows you to write tests that are easy to read and understand, and are also easy to update when the data structure changes. \n\n```python\nfrom inline_snapshot import snapshot\n\nasync def test_list_tools(main_mcp_client: Client[FastMCPTransport]):\n    list_tools = await main_mcp_client.list_tools()\n\n    assert list_tools == snapshot()\n```\n\nSimply run `pytest --inline-snapshot=fix,create` to fill in the `snapshot()` with actual data.\n\n<Tip>\nFor values that change you can leverage the [dirty-equals](https://github.com/samuelcolvin/dirty-equals) library to perform flexible equality assertions on dynamic or non-deterministic values.\n</Tip>\n\nUsing the pytest `parametrize` decorator, you can easily test your tools with a wide variety of inputs.\n\n```python\nimport pytest\nfrom my_project.main import mcp\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\n@pytest.fixture\nasync def main_mcp_client():\n    async with Client(mcp) as client:\n        yield client\n\n\n@pytest.mark.parametrize(\n    \"first_number, second_number, expected\",\n    [\n        (1, 2, 3),\n        (2, 3, 5),\n        (3, 4, 7),\n    ],\n)\nasync def test_add(\n    first_number: int,\n    second_number: int,\n    expected: int,\n    main_mcp_client: Client[FastMCPTransport],\n):\n    result = await main_mcp_client.call_tool(\n        name=\"add\", arguments={\"x\": first_number, \"y\": second_number}\n    )\n    assert result.data is not None\n    assert isinstance(result.data, int)\n    assert result.data == expected\n```\n\n<Tip>\nThe [FastMCP Repository contains thousands of tests](https://github.com/PrefectHQ/fastmcp/tree/main/tests) for the FastMCP Client and Server. Everything from connecting to remote MCP servers, to testing tools, resources, and prompts is covered, take a look for inspiration!\n</Tip>"
  },
  {
    "path": "docs/public/schemas/fastmcp.json/latest.json",
    "content": "{\n  \"$defs\": {\n    \"Deployment\": {\n      \"description\": \"Configuration for server deployment and runtime settings.\",\n      \"properties\": {\n        \"transport\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"stdio\",\n                \"http\",\n                \"sse\",\n                \"streamable-http\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Transport protocol to use\",\n          \"title\": \"Transport\"\n        },\n        \"host\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Host to bind to when using HTTP transport\",\n          \"examples\": [\n            \"127.0.0.1\",\n            \"0.0.0.0\",\n            \"localhost\"\n          ],\n          \"title\": \"Host\"\n        },\n        \"port\": {\n          \"anyOf\": [\n            {\n              \"type\": \"integer\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Port to bind to when using HTTP transport\",\n          \"examples\": [\n            8000,\n            3000,\n            5000\n          ],\n          \"title\": \"Port\"\n        },\n        \"path\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"URL path for the server endpoint\",\n          \"examples\": [\n            \"/mcp/\",\n            \"/api/mcp/\",\n            \"/sse/\"\n          ],\n          \"title\": \"Path\"\n        },\n        \"log_level\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"DEBUG\",\n                \"INFO\",\n                \"WARNING\",\n                \"ERROR\",\n                \"CRITICAL\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Log level for the server\",\n          \"title\": \"Log Level\"\n        },\n        \"cwd\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Working directory for the server process\",\n          \"examples\": [\n            \".\",\n            \"./src\",\n            \"/app\"\n          ],\n          \"title\": \"Cwd\"\n        },\n        \"env\": {\n          \"anyOf\": [\n            {\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"object\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Environment variables to set when running the server\",\n          \"examples\": [\n            {\n              \"API_KEY\": \"secret\",\n              \"DEBUG\": \"true\"\n            }\n          ],\n          \"title\": \"Env\"\n        },\n        \"args\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Arguments to pass to the server (after --)\",\n          \"examples\": [\n            [\n              \"--config\",\n              \"config.json\",\n              \"--debug\"\n            ]\n          ],\n          \"title\": \"Args\"\n        }\n      },\n      \"title\": \"Deployment\",\n      \"type\": \"object\"\n    },\n    \"FileSystemSource\": {\n      \"description\": \"Source for local Python files.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"filesystem\",\n          \"default\": \"filesystem\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"path\": {\n          \"description\": \"Path to Python file containing the server\",\n          \"title\": \"Path\",\n          \"type\": \"string\"\n        },\n        \"entrypoint\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Name of server instance or factory function (a no-arg function that returns a FastMCP server)\",\n          \"title\": \"Entrypoint\"\n        }\n      },\n      \"required\": [\n        \"path\"\n      ],\n      \"title\": \"FileSystemSource\",\n      \"type\": \"object\"\n    },\n    \"UVEnvironment\": {\n      \"description\": \"Configuration for Python environment setup.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"uv\",\n          \"default\": \"uv\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"python\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python version constraint\",\n          \"examples\": [\n            \"3.10\",\n            \"3.11\",\n            \"3.12\"\n          ],\n          \"title\": \"Python\"\n        },\n        \"dependencies\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python packages to install with PEP 508 specifiers\",\n          \"examples\": [\n            [\n              \"fastmcp>=2.0,<3\",\n              \"httpx\",\n              \"pandas>=2.0\"\n            ]\n          ],\n          \"title\": \"Dependencies\"\n        },\n        \"requirements\": {\n          \"anyOf\": [\n            {\n              \"format\": \"path\",\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to requirements.txt file\",\n          \"examples\": [\n            \"requirements.txt\",\n            \"../requirements/prod.txt\"\n          ],\n          \"title\": \"Requirements\"\n        },\n        \"project\": {\n          \"anyOf\": [\n            {\n              \"format\": \"path\",\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to project directory containing pyproject.toml\",\n          \"examples\": [\n            \".\",\n            \"../my-project\"\n          ],\n          \"title\": \"Project\"\n        },\n        \"editable\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"format\": \"path\",\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Directories to install in editable mode\",\n          \"examples\": [\n            [\n              \".\",\n              \"../my-package\"\n            ],\n            [\n              \"/path/to/package\"\n            ]\n          ],\n          \"title\": \"Editable\"\n        }\n      },\n      \"title\": \"UVEnvironment\",\n      \"type\": \"object\"\n    }\n  },\n  \"description\": \"Configuration file for FastMCP servers\",\n  \"properties\": {\n    \"$schema\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ],\n      \"default\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n      \"description\": \"JSON schema for IDE support and validation\",\n      \"title\": \"$Schema\"\n    },\n    \"source\": {\n      \"$ref\": \"#/$defs/FileSystemSource\",\n      \"description\": \"Source configuration for the server\",\n      \"examples\": [\n        {\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"app\",\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"mcp\",\n          \"path\": \"src/server.py\",\n          \"type\": \"filesystem\"\n        }\n      ]\n    },\n    \"environment\": {\n      \"$ref\": \"#/$defs/UVEnvironment\",\n      \"description\": \"Python environment setup configuration\"\n    },\n    \"deployment\": {\n      \"$ref\": \"#/$defs/Deployment\",\n      \"description\": \"Server deployment and runtime settings\"\n    }\n  },\n  \"required\": [\n    \"source\"\n  ],\n  \"title\": \"FastMCP Configuration\",\n  \"type\": \"object\",\n  \"$id\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n}\n"
  },
  {
    "path": "docs/public/schemas/fastmcp.json/v1.json",
    "content": "{\n  \"$defs\": {\n    \"Deployment\": {\n      \"description\": \"Configuration for server deployment and runtime settings.\",\n      \"properties\": {\n        \"transport\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"stdio\",\n                \"http\",\n                \"sse\",\n                \"streamable-http\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Transport protocol to use\",\n          \"title\": \"Transport\"\n        },\n        \"host\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Host to bind to when using HTTP transport\",\n          \"examples\": [\n            \"127.0.0.1\",\n            \"0.0.0.0\",\n            \"localhost\"\n          ],\n          \"title\": \"Host\"\n        },\n        \"port\": {\n          \"anyOf\": [\n            {\n              \"type\": \"integer\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Port to bind to when using HTTP transport\",\n          \"examples\": [\n            8000,\n            3000,\n            5000\n          ],\n          \"title\": \"Port\"\n        },\n        \"path\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"URL path for the server endpoint\",\n          \"examples\": [\n            \"/mcp/\",\n            \"/api/mcp/\",\n            \"/sse/\"\n          ],\n          \"title\": \"Path\"\n        },\n        \"log_level\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"DEBUG\",\n                \"INFO\",\n                \"WARNING\",\n                \"ERROR\",\n                \"CRITICAL\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Log level for the server\",\n          \"title\": \"Log Level\"\n        },\n        \"cwd\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Working directory for the server process\",\n          \"examples\": [\n            \".\",\n            \"./src\",\n            \"/app\"\n          ],\n          \"title\": \"Cwd\"\n        },\n        \"env\": {\n          \"anyOf\": [\n            {\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"object\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Environment variables to set when running the server\",\n          \"examples\": [\n            {\n              \"API_KEY\": \"secret\",\n              \"DEBUG\": \"true\"\n            }\n          ],\n          \"title\": \"Env\"\n        },\n        \"args\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Arguments to pass to the server (after --)\",\n          \"examples\": [\n            [\n              \"--config\",\n              \"config.json\",\n              \"--debug\"\n            ]\n          ],\n          \"title\": \"Args\"\n        }\n      },\n      \"title\": \"Deployment\",\n      \"type\": \"object\"\n    },\n    \"FileSystemSource\": {\n      \"description\": \"Source for local Python files.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"filesystem\",\n          \"default\": \"filesystem\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"path\": {\n          \"description\": \"Path to Python file containing the server\",\n          \"title\": \"Path\",\n          \"type\": \"string\"\n        },\n        \"entrypoint\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Name of server instance or factory function (a no-arg function that returns a FastMCP server)\",\n          \"title\": \"Entrypoint\"\n        }\n      },\n      \"required\": [\n        \"path\"\n      ],\n      \"title\": \"FileSystemSource\",\n      \"type\": \"object\"\n    },\n    \"UVEnvironment\": {\n      \"description\": \"Configuration for Python environment setup.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"uv\",\n          \"default\": \"uv\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"python\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python version constraint\",\n          \"examples\": [\n            \"3.10\",\n            \"3.11\",\n            \"3.12\"\n          ],\n          \"title\": \"Python\"\n        },\n        \"dependencies\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python packages to install with PEP 508 specifiers\",\n          \"examples\": [\n            [\n              \"fastmcp>=2.0,<3\",\n              \"httpx\",\n              \"pandas>=2.0\"\n            ]\n          ],\n          \"title\": \"Dependencies\"\n        },\n        \"requirements\": {\n          \"anyOf\": [\n            {\n              \"format\": \"path\",\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to requirements.txt file\",\n          \"examples\": [\n            \"requirements.txt\",\n            \"../requirements/prod.txt\"\n          ],\n          \"title\": \"Requirements\"\n        },\n        \"project\": {\n          \"anyOf\": [\n            {\n              \"format\": \"path\",\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to project directory containing pyproject.toml\",\n          \"examples\": [\n            \".\",\n            \"../my-project\"\n          ],\n          \"title\": \"Project\"\n        },\n        \"editable\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"format\": \"path\",\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Directories to install in editable mode\",\n          \"examples\": [\n            [\n              \".\",\n              \"../my-package\"\n            ],\n            [\n              \"/path/to/package\"\n            ]\n          ],\n          \"title\": \"Editable\"\n        }\n      },\n      \"title\": \"UVEnvironment\",\n      \"type\": \"object\"\n    }\n  },\n  \"description\": \"Configuration file for FastMCP servers\",\n  \"properties\": {\n    \"$schema\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ],\n      \"default\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n      \"description\": \"JSON schema for IDE support and validation\",\n      \"title\": \"$Schema\"\n    },\n    \"source\": {\n      \"$ref\": \"#/$defs/FileSystemSource\",\n      \"description\": \"Source configuration for the server\",\n      \"examples\": [\n        {\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"app\",\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"mcp\",\n          \"path\": \"src/server.py\",\n          \"type\": \"filesystem\"\n        }\n      ]\n    },\n    \"environment\": {\n      \"$ref\": \"#/$defs/UVEnvironment\",\n      \"description\": \"Python environment setup configuration\"\n    },\n    \"deployment\": {\n      \"$ref\": \"#/$defs/Deployment\",\n      \"description\": \"Server deployment and runtime settings\"\n    }\n  },\n  \"required\": [\n    \"source\"\n  ],\n  \"title\": \"FastMCP Configuration\",\n  \"type\": \"object\",\n  \"$id\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n}\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.cli`\n\n\nFastMCP CLI package.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-apps_dev.mdx",
    "content": "---\ntitle: apps_dev\nsidebarTitle: apps_dev\n---\n\n# `fastmcp.cli.apps_dev`\n\n\nDev server for previewing FastMCPApp UIs locally.\n\nStarts the user's MCP server on a configurable port, then starts a lightweight\nStarlette dev server that:\n\n  - Serves a Prefab-based tool picker at GET /\n  - Proxies /mcp to the user's server (avoids browser CORS restrictions)\n  - Serves the AppBridge host page at GET /launch\n\nThe host page uses @modelcontextprotocol/ext-apps to connect to the MCP server\nand render the selected UI tool inside an iframe.\n\nStartup sequence\n----------------\n1. Download ext-apps app-bridge.js from npm and patch its bare\n   ``@modelcontextprotocol/sdk/…`` imports to use concrete esm.sh URLs.\n2. Detect the exact Zod v4 module URL that esm.sh serves for that SDK version\n   and build an import-map entry that redirects the broken ``v4.mjs`` (which\n   only re-exports ``{z, default}``) to ``v4/classic/index.mjs`` (which\n   correctly exports every named Zod v4 function).  Import maps apply to the\n   full module graph in the document, including cross-origin esm.sh modules.\n3. Serve both the patched JS and the import-map JSON from the dev server.\n\n\n## Functions\n\n### `run_dev_apps` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/apps_dev.py#L825\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_dev_apps(server_spec: str) -> None\n```\n\n\nStart the full dev environment for a FastMCPApp server.\n\nStarts the user's MCP server on *mcp_port*, starts the Prefab dev UI\non *dev_port* (with an /mcp proxy to the user's server), then opens\nthe browser.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-auth.mdx",
    "content": "---\ntitle: auth\nsidebarTitle: auth\n---\n\n# `fastmcp.cli.auth`\n\n\nAuthentication-related CLI commands.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-cimd.mdx",
    "content": "---\ntitle: cimd\nsidebarTitle: cimd\n---\n\n# `fastmcp.cli.cimd`\n\n\nCIMD (Client ID Metadata Document) CLI commands.\n\n## Functions\n\n### `create_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cimd.py#L32\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_command() -> None\n```\n\n\nGenerate a CIMD document for hosting.\n\nCreate a Client ID Metadata Document that you can host at an HTTPS URL.\nThe URL where you host this document becomes your client_id.\n\nAfter creating the document, host it at an HTTPS URL with a non-root path,\nfor example: https://myapp.example.com/oauth/client.json\n\n\n### `validate_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cimd.py#L144\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_command(url: Annotated[str, cyclopts.Parameter(help='URL of the CIMD document to validate')]) -> None\n```\n\n\nValidate a hosted CIMD document.\n\nFetches the document from the given URL and validates:\n- URL is valid CIMD URL (HTTPS, non-root path)\n- Document is valid JSON\n- Document conforms to CIMD schema\n- client_id in document matches the URL\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-cli.mdx",
    "content": "---\ntitle: cli\nsidebarTitle: cli\n---\n\n# `fastmcp.cli.cli`\n\n\nFastMCP CLI tools using Cyclopts.\n\n## Functions\n\n### `with_argv` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L73\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwith_argv(args: list[str] | None)\n```\n\n\nTemporarily replace sys.argv if args provided.\n\nThis context manager is used at the CLI boundary to inject\nserver arguments when needed, without mutating sys.argv deep\nin the source loading logic.\n\nArgs are provided without the script name, so we preserve sys.argv[0]\nand replace the rest.\n\n\n### `version` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L96\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nversion()\n```\n\n\nDisplay version information and platform details.\n\n\n### `inspector` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L142\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninspector(server_spec: str | None = None) -> None\n```\n\n\nRun an MCP server with the MCP Inspector for development.\n\n**Args:**\n- `server_spec`: Python file to run, optionally with \\:object suffix, or None to auto-detect fastmcp.json\n\n\n### `apps` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L337\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\napps(server_spec: str) -> None\n```\n\n\nPreview a FastMCPApp UI in the browser.\n\nStarts the MCP server from SERVER_SPEC on --mcp-port, launches a local\ndev UI on --dev-port with a tool picker and AppBridge host, then opens\nthe browser automatically.\n\nRequires fastmcp[apps] to be installed (prefab-ui).\n\n\n### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L385\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(server_spec: str | None = None, *server_args: str) -> None\n```\n\n\nRun an MCP server or connect to a remote one.\n\nThe server can be specified in several ways:\n1. Module approach: \"server.py\" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'\n2. Import approach: \"server.py:app\" - imports and runs the specified server object\n3. URL approach: \"http://server-url\" - connects to a remote server and creates a proxy\n4. MCPConfig file: \"mcp.json\" - runs as a proxy server for the MCP Servers in the MCPConfig file\n5. FastMCP config: \"fastmcp.json\" - runs server using FastMCP configuration\n6. No argument: looks for fastmcp.json in current directory\n7. Module mode: \"-m my_module\" - runs the module directly via python -m\n\nServer arguments can be passed after -- :\nfastmcp run server.py -- --config config.json --debug\n\n**Args:**\n- `server_spec`: Python file, object specification (file\\:obj), config file, URL, or None to auto-detect\n\n\n### `inspect` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L760\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninspect(server_spec: str | None = None) -> None\n```\n\n\nInspect an MCP server and display information or generate a JSON report.\n\nThis command analyzes an MCP server. Without flags, it displays a text summary.\nUse --format to output complete JSON data.\n\n**Examples:**\n\n# Show text summary\nfastmcp inspect server.py\n\n# Output FastMCP format JSON to stdout\nfastmcp inspect server.py --format fastmcp\n\n# Save MCP protocol format to file (format required with -o)\nfastmcp inspect server.py --format mcp -o manifest.json\n\n# Inspect from fastmcp.json configuration\nfastmcp inspect fastmcp.json\nfastmcp inspect  # auto-detect fastmcp.json\n\n**Args:**\n- `server_spec`: Python file to inspect, optionally with \\:object suffix, or fastmcp.json\n\n\n### `prepare` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/cli.py#L1002\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare(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\n```\n\n\nPrepare a FastMCP project by creating a persistent uv environment.\n\nThis command creates a persistent uv project with all dependencies installed:\n- Creates a pyproject.toml with dependencies from the config\n- Installs all Python packages into a .venv\n- Prepares the source (git clone, download, etc.) unless --skip-source\n\nAfter running this command, you can use:\nfastmcp run &lt;config&gt; --project &lt;output-dir&gt;\n\nThis is useful for:\n- CI/CD pipelines with separate build and run stages\n- Docker images where you prepare during build\n- Production deployments where you want fast startup times\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-client.mdx",
    "content": "---\ntitle: client\nsidebarTitle: client\n---\n\n# `fastmcp.cli.client`\n\n\nClient-side CLI commands for querying and invoking MCP servers.\n\n## Functions\n\n### `resolve_server_spec` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_server_spec(server_spec: str | None) -> str | dict[str, Any] | ClientTransport\n```\n\n\nTurn CLI inputs into something ``Client()`` accepts.\n\nExactly one of ``server_spec`` or ``command`` should be provided.\n\nResolution order for ``server_spec``:\n1. URLs (``http://``, ``https://``) — passed through as-is.\n   If ``--transport`` is ``sse``, the URL is rewritten to end with ``/sse``\n   so ``infer_transport`` picks the right transport.\n2. Existing file paths, or strings ending in ``.py``/``.js``/``.json``.\n3. Anything else — name-based resolution via ``resolve_name``.\n\nWhen ``command`` is provided, the string is shell-split into a\n``StdioTransport(command, args)``.\n\n\n### `coerce_value` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L264\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncoerce_value(raw: str, schema: dict[str, Any]) -> Any\n```\n\n\nCoerce a string CLI value according to a JSON-Schema type hint.\n\n\n### `parse_tool_arguments` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L298\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_tool_arguments(raw_args: tuple[str, ...], input_json: str | None, input_schema: dict[str, Any]) -> dict[str, Any]\n```\n\n\nBuild a tool-call argument dict from CLI inputs.\n\nA single JSON object argument is treated as the full argument dict.\n``--input-json`` provides the base dict; ``key=value`` pairs override.\nValues are coerced using the tool's ``inputSchema``.\n\n\n### `format_tool_signature` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L370\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_tool_signature(tool: mcp.types.Tool) -> str\n```\n\n\nBuild ``name(param: type, ...) -> return_type`` from a tool's JSON schemas.\n\n\n### `list_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L641\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_command(server_spec: Annotated[str | None, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, or .js file')] = None) -> None\n```\n\n\nList tools available on an MCP server.\n\n**Examples:**\n\nfastmcp list http://localhost:8000/mcp\nfastmcp list server.py\nfastmcp list mcp.json --json\nfastmcp list --command 'npx -y @mcp/server' --resources\nfastmcp list http://server/mcp --transport sse\n\n\n### `call_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L796\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_command(server_spec: Annotated[str | None, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, or .js file')] = None, target: Annotated[str, cyclopts.Parameter(help='Tool name, resource URI, or prompt name (with --prompt)')] = '', *arguments: str) -> None\n```\n\n\nCall a tool, read a resource, or get a prompt on an MCP server.\n\nBy default the target is treated as a tool name. If the target\ncontains ``://`` it is treated as a resource URI. Pass ``--prompt``\nto treat it as a prompt name.\n\nArguments are passed as key=value pairs. Use --input-json for complex\nor nested arguments.\n\n**Examples:**\n\n```\nfastmcp call server.py greet name=World\nfastmcp call server.py resource://docs/readme\nfastmcp call server.py analyze --prompt data='[1,2,3]'\nfastmcp call http://server/mcp create --input-json '{\"tags\": [\"a\",\"b\"]}'\n```\n\n\n### `discover_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/client.py#L897\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndiscover_command() -> None\n```\n\n\nDiscover MCP servers configured in editor and project configs.\n\nScans Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose, and\nproject-level mcp.json files for MCP server definitions.\n\nDiscovered server names can be used directly with ``fastmcp list``\nand ``fastmcp call`` instead of specifying a URL or file path.\n\n**Examples:**\n\nfastmcp discover\nfastmcp discover --source claude-code\nfastmcp discover --source cursor --source gemini --json\nfastmcp list weather\nfastmcp call cursor:weather get_forecast city=London\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-discovery.mdx",
    "content": "---\ntitle: discovery\nsidebarTitle: discovery\n---\n\n# `fastmcp.cli.discovery`\n\n\nDiscover MCP servers configured in editor config files.\n\nScans filesystem-readable config files from editors like Claude Desktop,\nClaude Code, Cursor, Gemini CLI, and Goose, as well as project-level\n``mcp.json`` files. Each discovered server can be resolved by name\n(or ``source:name``) so the CLI can connect without requiring a URL\nor file path.\n\n\n## Functions\n\n### `discover_servers` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/discovery.py#L314\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndiscover_servers(start_dir: Path | None = None) -> list[DiscoveredServer]\n```\n\n\nRun all scanners and return the combined results.\n\nDuplicate names across sources are preserved — callers can\nuse :pyattr:`DiscoveredServer.qualified_name` to disambiguate.\n\n\n### `resolve_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/discovery.py#L331\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_name(name: str, start_dir: Path | None = None) -> ClientTransport\n```\n\n\nResolve a server name (or ``source:name``) to a transport.\n\nRaises :class:`ValueError` when the name is not found or is ambiguous.\n\n\n## Classes\n\n### `DiscoveredServer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/discovery.py#L37\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA single MCP server found in an editor or project config.\n\n\n**Methods:**\n\n#### `qualified_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/discovery.py#L46\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nqualified_name(self) -> str\n```\n\nFully qualified ``source:name`` identifier.\n\n\n#### `transport_summary` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/discovery.py#L51\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransport_summary(self) -> str\n```\n\nHuman-readable one-liner describing the transport.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-generate.mdx",
    "content": "---\ntitle: generate\nsidebarTitle: generate\n---\n\n# `fastmcp.cli.generate`\n\n\nGenerate a standalone CLI script and agent skill from an MCP server.\n\n## Functions\n\n### `serialize_transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/generate.py#L122\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nserialize_transport(resolved: str | dict[str, Any] | ClientTransport) -> tuple[str, set[str]]\n```\n\n\nSerialize a resolved transport to a Python expression string.\n\nReturns ``(expression, extra_imports)`` where *extra_imports* is a set of\nimport lines needed by the expression.\n\n\n### `generate_cli_script` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/generate.py#L283\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_cli_script(server_name: str, server_spec: str, transport_code: str, extra_imports: set[str], tools: list[mcp.types.Tool]) -> str\n```\n\n\nGenerate the full CLI script source code.\n\n\n### `generate_skill_content` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/generate.py#L621\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_skill_content(server_name: str, cli_filename: str, tools: list[mcp.types.Tool]) -> str\n```\n\n\nGenerate a SKILL.md file for a generated CLI script.\n\n\n### `generate_cli_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/generate.py#L672\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_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\n```\n\n\nGenerate a standalone CLI script from an MCP server.\n\nConnects to the server, reads its tools/resources/prompts, and writes\na Python script that can invoke them directly. Also generates a SKILL.md\nagent skill file unless --no-skill is passed.\n\n**Examples:**\n\nfastmcp generate-cli weather\nfastmcp generate-cli weather my_cli.py\nfastmcp generate-cli http://localhost:8000/mcp\nfastmcp generate-cli server.py output.py -f\nfastmcp generate-cli weather --no-skill\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.cli.install`\n\n\nInstall subcommands for FastMCP CLI using Cyclopts.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-claude_code.mdx",
    "content": "---\ntitle: claude_code\nsidebarTitle: claude_code\n---\n\n# `fastmcp.cli.install.claude_code`\n\n\nClaude Code integration for FastMCP install using Cyclopts.\n\n## Functions\n\n### `find_claude_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfind_claude_command() -> str | None\n```\n\n\nFind the Claude Code CLI command.\n\nChecks common installation locations since 'claude' is often a shell alias\nthat doesn't work with subprocess calls.\n\n\n### `check_claude_code_available` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncheck_claude_code_available() -> bool\n```\n\n\nCheck if Claude Code CLI is available.\n\n\n### `install_claude_code` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L73\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_claude_code(file: Path, server_object: str | None, name: str) -> bool\n```\n\n\nInstall FastMCP server in Claude Code.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `name`: Name for the server in Claude Code\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `env_vars`: Optional dictionary of environment variables\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n\n**Returns:**\n- True if installation was successful, False otherwise\n\n\n### `claude_code_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclaude_code_command(server_spec: str) -> None\n```\n\n\nInstall an MCP server in Claude Code.\n\n**Args:**\n- `server_spec`: Python file to install, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx",
    "content": "---\ntitle: claude_desktop\nsidebarTitle: claude_desktop\n---\n\n# `fastmcp.cli.install.claude_desktop`\n\n\nClaude Desktop integration for FastMCP install using Cyclopts.\n\n## Functions\n\n### `get_claude_config_path` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_claude_config_path(config_path: Path | None = None) -> Path | None\n```\n\n\nGet the Claude config directory based on platform.\n\n**Args:**\n- `config_path`: Optional custom path to the Claude Desktop config directory\n\n\n### `install_claude_desktop` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L49\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_claude_desktop(file: Path, server_object: str | None, name: str) -> bool\n```\n\n\nInstall FastMCP server in Claude Desktop.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `name`: Name for the server in Claude's config\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `env_vars`: Optional dictionary of environment variables\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n- `config_path`: Optional custom path to Claude Desktop config directory\n\n**Returns:**\n- True if installation was successful, False otherwise\n\n\n### `claude_desktop_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclaude_desktop_command(server_spec: str) -> None\n```\n\n\nInstall an MCP server in Claude Desktop.\n\n**Args:**\n- `server_spec`: Python file to install, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-cursor.mdx",
    "content": "---\ntitle: cursor\nsidebarTitle: cursor\n---\n\n# `fastmcp.cli.install.cursor`\n\n\nCursor integration for FastMCP install using Cyclopts.\n\n## Functions\n\n### `generate_cursor_deeplink` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_cursor_deeplink(server_name: str, server_config: StdioMCPServer) -> str\n```\n\n\nGenerate a Cursor deeplink for installing the MCP server.\n\n**Args:**\n- `server_name`: Name of the server\n- `server_config`: Server configuration\n\n**Returns:**\n- Deeplink URL that can be clicked to install the server\n\n\n### `open_deeplink` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nopen_deeplink(deeplink: str) -> bool\n```\n\n\nAttempt to open a Cursor deeplink URL using the system's default handler.\n\n**Args:**\n- `deeplink`: The deeplink URL to open\n\n**Returns:**\n- True if the command succeeded, False otherwise\n\n\n### `install_cursor_workspace` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L59\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_cursor_workspace(file: Path, server_object: str | None, name: str, workspace_path: Path) -> bool\n```\n\n\nInstall FastMCP server to workspace-specific Cursor configuration.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `name`: Name for the server in Cursor\n- `workspace_path`: Path to the workspace directory\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `env_vars`: Optional dictionary of environment variables\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n\n**Returns:**\n- True if installation was successful, False otherwise\n\n\n### `install_cursor` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L143\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_cursor(file: Path, server_object: str | None, name: str) -> bool\n```\n\n\nInstall FastMCP server in Cursor.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `name`: Name for the server in Cursor\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `env_vars`: Optional dictionary of environment variables\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n- `workspace`: Optional workspace directory for project-specific installation\n\n**Returns:**\n- True if installation was successful, False otherwise\n\n\n### `cursor_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L228\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncursor_command(server_spec: str) -> None\n```\n\n\nInstall an MCP server in Cursor.\n\n**Args:**\n- `server_spec`: Python file to install, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx",
    "content": "---\ntitle: gemini_cli\nsidebarTitle: gemini_cli\n---\n\n# `fastmcp.cli.install.gemini_cli`\n\n\nGemini CLI integration for FastMCP install using Cyclopts.\n\n## Functions\n\n### `find_gemini_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/gemini_cli.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfind_gemini_command() -> str | None\n```\n\n\nFind the Gemini CLI command.\n\n\n### `check_gemini_cli_available` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/gemini_cli.py#L64\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncheck_gemini_cli_available() -> bool\n```\n\n\nCheck if Gemini CLI is available.\n\n\n### `install_gemini_cli` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/gemini_cli.py#L69\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_gemini_cli(file: Path, server_object: str | None, name: str) -> bool\n```\n\n\nInstall FastMCP server in Gemini CLI.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `name`: Name for the server in Gemini CLI\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `env_vars`: Optional dictionary of environment variables\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n\n**Returns:**\n- True if installation was successful, False otherwise\n\n\n### `gemini_cli_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/gemini_cli.py#L152\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngemini_cli_command(server_spec: str) -> None\n```\n\n\nInstall an MCP server in Gemini CLI.\n\n**Args:**\n- `server_spec`: Python file to install, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-goose.mdx",
    "content": "---\ntitle: goose\nsidebarTitle: goose\n---\n\n# `fastmcp.cli.install.goose`\n\n\nGoose integration for FastMCP install using Cyclopts.\n\n## Functions\n\n### `generate_goose_deeplink` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/goose.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_goose_deeplink(name: str, command: str, args: list[str]) -> str\n```\n\n\nGenerate a Goose deeplink for installing an MCP extension.\n\n**Args:**\n- `name`: Human-readable display name for the extension.\n- `command`: The executable command (e.g. \"uv\").\n- `args`: Arguments to the command.\n- `description`: Short description shown in Goose.\n\n**Returns:**\n- A goose://extension?... deeplink URL.\n\n\n### `install_goose` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/goose.py#L86\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_goose(file: Path, server_object: str | None, name: str) -> bool\n```\n\n\nInstall FastMCP server in Goose via deeplink.\n\n**Args:**\n- `file`: Path to the server file.\n- `server_object`: Optional server object name (for \\:object suffix).\n- `name`: Name for the extension in Goose.\n- `with_packages`: Optional list of additional packages to install.\n- `python_version`: Optional Python version to use.\n\n**Returns:**\n- True if installation was successful, False otherwise.\n\n\n### `goose_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/goose.py#L136\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngoose_command(server_spec: str) -> None\n```\n\n\nInstall an MCP server in Goose.\n\nUses uvx to run the server. Environment variables are not included\nin the deeplink; use `fastmcp install mcp-json` to generate a full\nconfig for manual installation.\n\n**Args:**\n- `server_spec`: Python file to install, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-mcp_json.mdx",
    "content": "---\ntitle: mcp_json\nsidebarTitle: mcp_json\n---\n\n# `fastmcp.cli.install.mcp_json`\n\n\nMCP configuration JSON generation for FastMCP install using Cyclopts.\n\n## Functions\n\n### `install_mcp_json` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_mcp_json(file: Path, server_object: str | None, name: str) -> bool\n```\n\n\nGenerate MCP configuration JSON for manual installation.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `name`: Name for the server in MCP config\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `env_vars`: Optional dictionary of environment variables\n- `copy`: If True, copy to clipboard instead of printing to stdout\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n\n**Returns:**\n- True if generation was successful, False otherwise\n\n\n### `mcp_json_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L98\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmcp_json_command(server_spec: str) -> None\n```\n\n\nGenerate MCP configuration JSON for manual installation.\n\n**Args:**\n- `server_spec`: Python file to install, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-shared.mdx",
    "content": "---\ntitle: shared\nsidebarTitle: shared\n---\n\n# `fastmcp.cli.install.shared`\n\n\nShared utilities for install commands.\n\n## Functions\n\n### `validate_server_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L28\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_server_name(name: str) -> str\n```\n\n\nValidate that a server name is safe for use as a subprocess argument.\n\nRaises SystemExit if the name contains shell metacharacters.\n\n\n### `parse_env_var` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_env_var(env_var: str) -> tuple[str, str]\n```\n\n\nParse environment variable string in format KEY=VALUE.\n\n\n### `process_common_args` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L53\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprocess_common_args(server_spec: str, server_name: str | None, with_packages: list[str] | None, env_vars: list[str] | None, env_file: Path | None) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]\n```\n\n\nProcess common arguments shared by all install commands.\n\nHandles both fastmcp.json config files and traditional file.py:object syntax.\n\n\n### `open_deeplink` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L169\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nopen_deeplink(url: str) -> bool\n```\n\n\nAttempt to open a deeplink URL using the system's default handler.\n\n**Args:**\n- `url`: The deeplink URL to open.\n- `expected_scheme`: The URL scheme to validate (e.g. \"cursor\", \"goose\").\n\n**Returns:**\n- True if the command succeeded, False otherwise.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-install-stdio.mdx",
    "content": "---\ntitle: stdio\nsidebarTitle: stdio\n---\n\n# `fastmcp.cli.install.stdio`\n\n\nStdio command generation for FastMCP install using Cyclopts.\n\n## Functions\n\n### `install_stdio` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/stdio.py#L21\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstall_stdio(file: Path, server_object: str | None) -> bool\n```\n\n\nGenerate the stdio command for running a FastMCP server.\n\n**Args:**\n- `file`: Path to the server file\n- `server_object`: Optional server object name (for \\:object suffix)\n- `with_editable`: Optional list of directories to install in editable mode\n- `with_packages`: Optional list of additional packages to install\n- `copy`: If True, copy to clipboard instead of printing to stdout\n- `python_version`: Optional Python version to use\n- `with_requirements`: Optional requirements file to install from\n- `project`: Optional project directory to run within\n\n**Returns:**\n- True if generation was successful, False otherwise\n\n\n### `stdio_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/install/stdio.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nstdio_command(server_spec: str) -> None\n```\n\n\nGenerate the stdio command for running a FastMCP server.\n\nOutputs the shell command that an MCP host would use to start this server\nover stdio transport. Useful for manual configuration or debugging.\n\n**Args:**\n- `server_spec`: Python file to run, optionally with \\:object suffix\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-run.mdx",
    "content": "---\ntitle: run\nsidebarTitle: run\n---\n\n# `fastmcp.cli.run`\n\n\nFastMCP run command implementation with enhanced type hints.\n\n## Functions\n\n### `is_url` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L83\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_url(path: str) -> bool\n```\n\n\nCheck if a string is a URL.\n\n\n### `create_client_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L89\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_client_server(url: str) -> Any\n```\n\n\nCreate a FastMCP server from a client URL.\n\n**Args:**\n- `url`: The URL to connect to\n\n**Returns:**\n- A FastMCP server instance\n\n\n### `create_mcp_config_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L109\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]\n```\n\n\nCreate a FastMCP server from a MCPConfig.\n\n\n### `load_mcp_server_config` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L118\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_mcp_server_config(config_path: Path) -> MCPServerConfig\n```\n\n\nLoad a FastMCP configuration from a fastmcp.json file.\n\n**Args:**\n- `config_path`: Path to fastmcp.json file\n\n**Returns:**\n- MCPServerConfig object\n\n\n### `run_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L135\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_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\n```\n\n\nRun a MCP server or connect to a remote one.\n\n**Args:**\n- `server_spec`: Python file, object specification (file\\:obj), config file, or URL\n- `transport`: Transport protocol to use\n- `host`: Host to bind to when using http transport\n- `port`: Port to bind to when using http transport\n- `path`: Path to bind to when using http transport\n- `log_level`: Log level\n- `server_args`: Additional arguments to pass to the server\n- `show_banner`: Whether to show the server banner\n- `use_direct_import`: Whether to use direct import instead of subprocess\n- `skip_source`: Whether to skip source preparation step\n- `stateless`: Whether to run in stateless mode (no session)\n\n\n### `run_module_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L260\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_module_command(module_name: str) -> None\n```\n\n\nRun a Python module directly using ``python -m <module>``.\n\nWhen ``-m`` is used, the module manages its own server startup.\nNo server-object discovery or transport overrides are applied.\n\n**Args:**\n- `module_name`: Dotted module name (e.g. ``my_package``).\n- `env_command_builder`: An optional callable that wraps a command list\nwith environment setup (e.g. ``UVEnvironment.build_command``).\n- `extra_args`: Extra arguments forwarded after the module name.\n\n\n### `run_v1_server_async` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L299\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_v1_server_async(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None\n```\n\n\nRun a FastMCP 1.x server using async methods.\n\n**Args:**\n- `server`: FastMCP 1.x server instance\n- `host`: Host to bind to\n- `port`: Port to bind to\n- `transport`: Transport protocol to use\n\n\n### `run_with_reload` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/run.py#L364\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_with_reload(cmd: list[str], reload_dirs: list[Path] | None = None, is_stdio: bool = False) -> None\n```\n\n\nRun a command with file watching and auto-reload.\n\n**Args:**\n- `cmd`: Command to run as subprocess (should include --no-reload)\n- `reload_dirs`: Directories to watch for changes (default\\: cwd)\n- `is_stdio`: Whether this is stdio transport\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-cli-tasks.mdx",
    "content": "---\ntitle: tasks\nsidebarTitle: tasks\n---\n\n# `fastmcp.cli.tasks`\n\n\nFastMCP tasks CLI for Docket task management.\n\n## Functions\n\n### `check_distributed_backend` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/tasks.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncheck_distributed_backend() -> None\n```\n\n\nCheck if Docket is configured with a distributed backend.\n\nThe CLI worker runs as a separate process, so it needs Redis/Valkey\nto coordinate with the main server process.\n\n**Raises:**\n- `SystemExit`: If using memory\\:// URL\n\n\n### `worker` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/cli/tasks.py#L61\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nworker(server_spec: Annotated[str | None, cyclopts.Parameter(help='Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json')] = None) -> None\n```\n\n\nStart an additional worker to process background tasks.\n\nConnects to your Docket backend and processes tasks in parallel with\nany other running workers. Configure via environment variables\n(FASTMCP_DOCKET_*).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.client`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-auth-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.client.auth`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-auth-bearer.mdx",
    "content": "---\ntitle: bearer\nsidebarTitle: bearer\n---\n\n# `fastmcp.client.auth.bearer`\n\n## Classes\n\n### `BearerAuth` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/bearer.py#L11\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `auth_flow` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/bearer.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nauth_flow(self, request)\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-auth-oauth.mdx",
    "content": "---\ntitle: oauth\nsidebarTitle: oauth\n---\n\n# `fastmcp.client.auth.oauth`\n\n## Functions\n\n### `check_if_auth_required` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L41\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncheck_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool\n```\n\n\nCheck if the MCP endpoint requires authentication by making a test request.\n\n**Returns:**\n- True if auth appears to be required, False otherwise\n\n\n## Classes\n\n### `ClientNotFoundError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L37\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRaised when OAuth client credentials are not found on the server.\n\n\n### `TokenStorageAdapter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L71\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `clear` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L99\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclear(self) -> None\n```\n\n#### `get_tokens` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L104\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tokens(self) -> OAuthToken | None\n```\n\n#### `set_tokens` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L108\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_tokens(self, tokens: OAuthToken) -> None\n```\n\n#### `get_client_info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L119\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_client_info(self) -> OAuthClientInformationFull | None\n```\n\n#### `set_client_info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L125\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_client_info(self, client_info: OAuthClientInformationFull) -> None\n```\n\n### `OAuth` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L138\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth client provider for MCP servers with browser-based authentication.\n\nThis class provides OAuth authentication for FastMCP clients by opening\na browser for user authorization and running a local callback server.\n\n\n**Methods:**\n\n#### `redirect_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L290\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nredirect_handler(self, authorization_url: str) -> None\n```\n\nOpen browser for authorization, with pre-flight check for invalid client.\n\n\n#### `callback_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L311\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncallback_handler(self) -> tuple[str, str | None]\n```\n\nHandle OAuth callback and return (auth_code, state).\n\n\n#### `async_auth_flow` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L350\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nasync_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]\n```\n\nHTTPX auth flow with automatic retry on stale cached credentials.\n\nIf the OAuth flow fails due to invalid/stale client credentials,\nclears the cache and retries once with fresh registration.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-client.mdx",
    "content": "---\ntitle: client\nsidebarTitle: client\n---\n\n# `fastmcp.client.client`\n\n## Classes\n\n### `ClientSessionState` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L97\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nHolds all session-related state for a Client instance.\n\nThis allows clean separation of configuration (which is copied) from\nsession state (which should be fresh for each new client instance).\n\n\n### `CallToolResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L114\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nParsed result from a tool call.\n\n\n### `Client` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L124\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMCP client that delegates connection management to a Transport instance.\n\nThe Client class is responsible for MCP protocol logic, while the Transport\nhandles connection establishment and management. Client provides methods for\nworking with resources, prompts, tools and other MCP capabilities.\n\nThis client supports reentrant context managers (multiple concurrent\n`async with client:` blocks) using reference counting and background session\nmanagement. This allows efficient session reuse in any scenario with\nnested or concurrent client usage.\n\nMCP SDK 1.10 introduced automatic list_tools() calls during call_tool()\nexecution. This created a race condition where events could be reset while\nother tasks were waiting on them, causing deadlocks. The issue was exposed\nin proxy scenarios but affects any reentrant usage.\n\nThe solution uses reference counting to track active context managers,\na background task to manage the session lifecycle, events to coordinate\nbetween tasks, and ensures all session state changes happen within a lock.\nEvents are only created when needed, never reset outside locks.\n\nThis design prevents race conditions where tasks wait on events that get\nreplaced by other tasks, ensuring reliable coordination in concurrent scenarios.\n\n**Args:**\n- `transport`: \nConnection source specification, which can be\\:\n\n    - ClientTransport\\: Direct transport instance\n    - FastMCP\\: In-process FastMCP server\n    - AnyUrl or str\\: URL to connect to\n    - Path\\: File path for local socket\n    - MCPConfig\\: MCP server configuration\n    - dict\\: Transport configuration\n- `roots`: Optional RootsList or RootsHandler for filesystem access\n- `sampling_handler`: Optional handler for sampling requests\n- `log_handler`: Optional handler for log messages\n- `message_handler`: Optional handler for protocol messages\n- `progress_handler`: Optional handler for progress notifications\n- `timeout`: Optional timeout for requests (seconds or timedelta)\n- `init_timeout`: Optional timeout for initial connection (seconds or timedelta).\nSet to 0 to disable. If None, uses the value in the FastMCP global settings.\n\n**Examples:**\n\n```python\n# Connect to FastMCP server\nclient = Client(\"http://localhost:8080\")\n\nasync with client:\n    # List available resources\n    resources = await client.list_resources()\n\n    # Call a tool\n    result = await client.call_tool(\"my_tool\", {\"param\": \"value\"})\n```\n\n\n**Methods:**\n\n#### `session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L371\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsession(self) -> ClientSession\n```\n\nGet the current active session. Raises RuntimeError if not connected.\n\n\n#### `initialize_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L381\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninitialize_result(self) -> mcp.types.InitializeResult | None\n```\n\nGet the result of the initialization request.\n\n\n#### `set_roots` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L385\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_roots(self, roots: RootsList | RootsHandler) -> None\n```\n\nSet the roots for the client. This does not automatically call `send_roots_list_changed`.\n\n\n#### `set_sampling_callback` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L389\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_sampling_callback(self, sampling_callback: SamplingHandler, sampling_capabilities: mcp.types.SamplingCapability | None = None) -> None\n```\n\nSet the sampling callback for the client.\n\n\n#### `set_elicitation_callback` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L404\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None\n```\n\nSet the elicitation callback for the client.\n\n\n#### `is_connected` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L412\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_connected(self) -> bool\n```\n\nCheck if the client is currently connected.\n\n\n#### `new` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L416\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nnew(self) -> Client[ClientTransportT]\n```\n\nCreate a new client instance with the same configuration but fresh session state.\n\nThis creates a new client with the same transport, handlers, and configuration,\nbut with no active session. Useful for creating independent sessions that don't\nshare state with the original client.\n\n**Returns:**\n- A new Client instance with the same configuration but disconnected state.\n\n\n#### `initialize` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L461\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninitialize(self, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.InitializeResult\n```\n\nSend an initialize request to the server.\n\nThis method performs the MCP initialization handshake with the server,\nexchanging capabilities and server information. It is idempotent - calling\nit multiple times returns the cached result from the first call.\n\nThe initialization happens automatically when entering the client context\nmanager unless `auto_initialize=False` was set during client construction.\nManual calls to this method are only needed when auto-initialization is disabled.\n\n**Args:**\n- `timeout`: Optional timeout for the initialization request (seconds or timedelta).\nIf None, uses the client's init_timeout setting.\n\n**Returns:**\n- The server's initialization response containing server info,\ncapabilities, protocol version, and optional instructions.\n\n**Raises:**\n- `RuntimeError`: If the client is not connected or initialization times out.\n\n\n#### `close` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L762\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose(self)\n```\n\n#### `ping` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L768\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nping(self) -> bool\n```\n\nSend a ping request.\n\n\n#### `cancel` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L773\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncancel(self, request_id: str | int, reason: str | None = None) -> None\n```\n\nSend a cancellation notification for an in-progress request.\n\n\n#### `progress` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L790\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprogress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None\n```\n\nSend a progress notification.\n\n\n#### `set_logging_level` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L802\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_logging_level(self, level: mcp.types.LoggingLevel) -> None\n```\n\nSend a logging/setLevel request.\n\n\n#### `send_roots_list_changed` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L806\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsend_roots_list_changed(self) -> None\n```\n\nSend a roots/list_changed notification.\n\n\n#### `complete_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L812\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncomplete_mcp(self, ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference, argument: dict[str, str], context_arguments: dict[str, Any] | None = None) -> mcp.types.CompleteResult\n```\n\nSend a completion request and return the complete MCP protocol result.\n\n**Args:**\n- `ref`: The reference to complete.\n- `argument`: Arguments to pass to the completion request.\n- `context_arguments`: Optional context arguments to\ninclude with the completion request. Defaults to None.\n\n**Returns:**\n- mcp.types.CompleteResult: The complete response object from the protocol,\ncontaining the completion and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `complete` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L843\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncomplete(self, ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference, argument: dict[str, str], context_arguments: dict[str, Any] | None = None) -> mcp.types.Completion\n```\n\nSend a completion request to the server.\n\n**Args:**\n- `ref`: The reference to complete.\n- `argument`: Arguments to pass to the completion request.\n- `context_arguments`: Optional context arguments to\ninclude with the completion request. Defaults to None.\n\n**Returns:**\n- mcp.types.Completion: The completion object.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `generate_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/client.py#L870\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_name(cls, name: str | None = None) -> str\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-elicitation.mdx",
    "content": "---\ntitle: elicitation\nsidebarTitle: elicitation\n---\n\n# `fastmcp.client.elicitation`\n\n## Functions\n\n### `create_elicitation_callback` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/elicitation.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_elicitation_callback(elicitation_handler: ElicitationHandler) -> ElicitationFnT\n```\n\n## Classes\n\n### `ElicitResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/elicitation.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-logging.mdx",
    "content": "---\ntitle: logging\nsidebarTitle: logging\n---\n\n# `fastmcp.client.logging`\n\n## Functions\n\n### `default_log_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/logging.py#L17\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_log_handler(message: LogMessage) -> None\n```\n\n\nDefault handler that properly routes server log messages to appropriate log levels.\n\n\n### `create_log_callback` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/logging.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_log_callback(handler: LogHandler | None = None) -> LoggingFnT\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-messages.mdx",
    "content": "---\ntitle: messages\nsidebarTitle: messages\n---\n\n# `fastmcp.client.messages`\n\n## Classes\n\n### `MessageHandler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L16\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nThis class is used to handle MCP messages sent to the client. It is used to handle all messages,\nrequests, notifications, and exceptions. Users can override any of the hooks\n\n\n**Methods:**\n\n#### `dispatch` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L30\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndispatch(self, message: Message) -> None\n```\n\n#### `on_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_message(self, message: Message) -> None\n```\n\n#### `on_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L79\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_request(self, message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]) -> None\n```\n\n#### `on_ping` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L84\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_ping(self, message: mcp.types.PingRequest) -> None\n```\n\n#### `on_list_roots` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L87\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_roots(self, message: mcp.types.ListRootsRequest) -> None\n```\n\n#### `on_create_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L90\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_create_message(self, message: mcp.types.CreateMessageRequest) -> None\n```\n\n#### `on_notification` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L93\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_notification(self, message: mcp.types.ServerNotification) -> None\n```\n\n#### `on_exception` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L96\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_exception(self, message: Exception) -> None\n```\n\n#### `on_progress` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L99\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_progress(self, message: mcp.types.ProgressNotification) -> None\n```\n\n#### `on_logging_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L102\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_logging_message(self, message: mcp.types.LoggingMessageNotification) -> None\n```\n\n#### `on_tool_list_changed` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L107\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_tool_list_changed(self, message: mcp.types.ToolListChangedNotification) -> None\n```\n\n#### `on_resource_list_changed` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L112\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_resource_list_changed(self, message: mcp.types.ResourceListChangedNotification) -> None\n```\n\n#### `on_prompt_list_changed` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L117\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_prompt_list_changed(self, message: mcp.types.PromptListChangedNotification) -> None\n```\n\n#### `on_resource_updated` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L122\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_resource_updated(self, message: mcp.types.ResourceUpdatedNotification) -> None\n```\n\n#### `on_cancelled` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/messages.py#L127\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_cancelled(self, message: mcp.types.CancelledNotification) -> None\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-mixins-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.client.mixins`\n\n\nClient mixins for FastMCP.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-mixins-prompts.mdx",
    "content": "---\ntitle: prompts\nsidebarTitle: prompts\n---\n\n# `fastmcp.client.mixins.prompts`\n\n\nPrompt-related methods for FastMCP Client.\n\n## Classes\n\n### `ClientPromptsMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L31\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing prompt-related methods for Client.\n\n\n**Methods:**\n\n#### `list_prompts_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts_mcp(self: Client) -> mcp.types.ListPromptsResult\n```\n\nSend a prompts/list request and return the complete MCP protocol result.\n\n**Args:**\n- `cursor`: Optional pagination cursor from a previous request's nextCursor.\n\n**Returns:**\n- mcp.types.ListPromptsResult: The complete response object from the protocol,\ncontaining the list of prompts and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L59\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Prompt]\n```\n\nRetrieve all prompts available on the server.\n\nThis method automatically fetches all pages if the server paginates results,\nreturning the complete list. For manual pagination control (e.g., to handle\nlarge result sets incrementally), use list_prompts_mcp() with the cursor parameter.\n\n**Args:**\n- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.\n\n**Returns:**\n- list\\[mcp.types.Prompt]: A list of all Prompt objects.\n\n**Raises:**\n- `RuntimeError`: If the page limit is reached before pagination completes.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `get_prompt_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L107\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt_mcp(self: Client, name: str, arguments: dict[str, Any] | None = None, meta: dict[str, Any] | None = None) -> mcp.types.GetPromptResult\n```\n\nSend a prompts/get request and return the complete MCP protocol result.\n\n**Args:**\n- `name`: The name of the prompt to retrieve.\n- `arguments`: Arguments to pass to the prompt. Defaults to None.\n- `meta`: Request metadata (e.g., for SEP-1686 tasks). Defaults to None.\n\n**Returns:**\n- mcp.types.GetPromptResult: The complete response object from the protocol,\ncontaining the prompt messages and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L176\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult\n```\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L187\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> PromptTask\n```\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L199\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult | PromptTask\n```\n\nRetrieve a rendered prompt message list from the server.\n\n**Args:**\n- `name`: The name of the prompt to retrieve.\n- `arguments`: Arguments to pass to the prompt. Defaults to None.\n- `version`: Specific prompt version to get. If None, gets highest version.\n- `meta`: Optional request-level metadata.\n- `task`: If True, execute as background task (SEP-1686). Defaults to False.\n- `task_id`: Optional client-provided task ID (auto-generated if not provided).\n- `ttl`: Time to keep results available in milliseconds (default 60s).\n\n**Returns:**\n- mcp.types.GetPromptResult | PromptTask: The complete response object if task=False,\nor a PromptTask object if task=True.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-mixins-resources.mdx",
    "content": "---\ntitle: resources\nsidebarTitle: resources\n---\n\n# `fastmcp.client.mixins.resources`\n\n\nResource-related methods for FastMCP Client.\n\n## Classes\n\n### `ClientResourcesMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L30\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing resource-related methods for Client.\n\n\n**Methods:**\n\n#### `list_resources_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L35\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources_mcp(self: Client) -> mcp.types.ListResourcesResult\n```\n\nSend a resources/list request and return the complete MCP protocol result.\n\n**Args:**\n- `cursor`: Optional pagination cursor from a previous request's nextCursor.\n\n**Returns:**\n- mcp.types.ListResourcesResult: The complete response object from the protocol,\ncontaining the list of resources and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L58\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Resource]\n```\n\nRetrieve all resources available on the server.\n\nThis method automatically fetches all pages if the server paginates results,\nreturning the complete list. For manual pagination control (e.g., to handle\nlarge result sets incrementally), use list_resources_mcp() with the cursor parameter.\n\n**Args:**\n- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.\n\n**Returns:**\n- list\\[mcp.types.Resource]: A list of all Resource objects.\n\n**Raises:**\n- `RuntimeError`: If the page limit is reached before pagination completes.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `list_resource_templates_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L105\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates_mcp(self: Client) -> mcp.types.ListResourceTemplatesResult\n```\n\nSend a resources/listResourceTemplates request and return the complete MCP protocol result.\n\n**Args:**\n- `cursor`: Optional pagination cursor from a previous request's nextCursor.\n\n**Returns:**\n- mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,\ncontaining the list of resource templates and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L128\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.ResourceTemplate]\n```\n\nRetrieve all resource templates available on the server.\n\nThis method automatically fetches all pages if the server paginates results,\nreturning the complete list. For manual pagination control (e.g., to handle\nlarge result sets incrementally), use list_resource_templates_mcp() with the\ncursor parameter.\n\n**Args:**\n- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.\n\n**Returns:**\n- list\\[mcp.types.ResourceTemplate]: A list of all ResourceTemplate objects.\n\n**Raises:**\n- `RuntimeError`: If the page limit is reached before pagination completes.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `read_resource_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L177\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource_mcp(self: Client, uri: AnyUrl | str, meta: dict[str, Any] | None = None) -> mcp.types.ReadResourceResult\n```\n\nSend a resources/read request and return the complete MCP protocol result.\n\n**Args:**\n- `uri`: The URI of the resource to read. Can be a string or an AnyUrl object.\n- `meta`: Request metadata (e.g., for SEP-1686 tasks). Defaults to None.\n\n**Returns:**\n- mcp.types.ReadResourceResult: The complete response object from the protocol,\ncontaining the resource contents and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L233\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]\n```\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L243\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self: Client, uri: AnyUrl | str) -> ResourceTask\n```\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L254\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask\n```\n\nRead the contents of a resource or resolved template.\n\n**Args:**\n- `uri`: The URI of the resource to read. Can be a string or an AnyUrl object.\n- `version`: Specific version to read. If None, reads highest version.\n- `meta`: Optional request-level metadata.\n- `task`: If True, execute as background task (SEP-1686). Defaults to False.\n- `task_id`: Optional client-provided task ID (auto-generated if not provided).\n- `ttl`: Time to keep results available in milliseconds (default 60s).\n\n**Returns:**\n- list\\[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask:\nA list of content objects if task=False, or a ResourceTask object if task=True.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-mixins-task_management.mdx",
    "content": "---\ntitle: task_management\nsidebarTitle: task_management\n---\n\n# `fastmcp.client.mixins.task_management`\n\n\nTask management methods for FastMCP Client.\n\n## Classes\n\n### `ClientTaskManagementMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/task_management.py#L30\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing task management methods for Client.\n\n\n**Methods:**\n\n#### `get_task_status` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/task_management.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_task_status(self: Client, task_id: str) -> GetTaskResult\n```\n\nQuery the status of a background task.\n\nSends a 'tasks/get' MCP protocol request over the existing transport.\n\n**Args:**\n- `task_id`: The task ID returned from call_tool_as_task\n\n**Returns:**\n- Status information including taskId, status, pollInterval, etc.\n\n**Raises:**\n- `RuntimeError`: If client not connected\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `get_task_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/task_management.py#L56\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_task_result(self: Client, task_id: str) -> Any\n```\n\nRetrieve the raw result of a completed background task.\n\nSends a 'tasks/result' MCP protocol request over the existing transport.\nReturns the raw result - callers should parse it appropriately.\n\n**Args:**\n- `task_id`: The task ID returned from call_tool_as_task\n\n**Returns:**\n- The raw result (could be tool, prompt, or resource result)\n\n**Raises:**\n- `RuntimeError`: If client not connected, task not found, or task failed\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `list_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/task_management.py#L85\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tasks(self: Client, cursor: str | None = None, limit: int = 50) -> dict[str, Any]\n```\n\nList background tasks.\n\nSends a 'tasks/list' MCP protocol request to the server. If the server\nreturns an empty list (indicating client-side tracking), falls back to\nquerying status for locally tracked task IDs.\n\n**Args:**\n- `cursor`: Optional pagination cursor\n- `limit`: Maximum number of tasks to return (default 50)\n\n**Returns:**\n- Response with structure:\n- tasks: List of task status dicts with taskId, status, etc.\n- nextCursor: Optional cursor for next page\n\n**Raises:**\n- `RuntimeError`: If client not connected\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `cancel_task` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/task_management.py#L135\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncancel_task(self: Client, task_id: str) -> mcp.types.CancelTaskResult\n```\n\nCancel a task, transitioning it to cancelled state.\n\nSends a 'tasks/cancel' MCP protocol request. Task will halt execution\nand transition to cancelled state.\n\n**Args:**\n- `task_id`: The task ID to cancel\n\n**Returns:**\n- The task status showing cancelled state\n\n**Raises:**\n- `RuntimeError`: If task doesn't exist\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-mixins-tools.mdx",
    "content": "---\ntitle: tools\nsidebarTitle: tools\n---\n\n# `fastmcp.client.mixins.tools`\n\n\nTool-related methods for FastMCP Client.\n\n## Classes\n\n### `ClientToolsMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing tool-related methods for Client.\n\n\n**Methods:**\n\n#### `list_tools_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools_mcp(self: Client) -> mcp.types.ListToolsResult\n```\n\nSend a tools/list request and return the complete MCP protocol result.\n\n**Args:**\n- `cursor`: Optional pagination cursor from a previous request's nextCursor.\n\n**Returns:**\n- mcp.types.ListToolsResult: The complete response object from the protocol,\ncontaining the list of tools and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L62\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Tool]\n```\n\nRetrieve all tools available on the server.\n\nThis method automatically fetches all pages if the server paginates results,\nreturning the complete list. For manual pagination control (e.g., to handle\nlarge result sets incrementally), use list_tools_mcp() with the cursor parameter.\n\n**Args:**\n- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.\n\n**Returns:**\n- list\\[mcp.types.Tool]: A list of all Tool objects.\n\n**Raises:**\n- `RuntimeError`: If the page limit is reached before pagination completes.\n- `McpError`: If the request results in a TimeoutError | JSONRPCError\n\n\n#### `call_tool_mcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L111\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_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\n```\n\nSend a tools/call request and return the complete MCP protocol result.\n\nThis method returns the raw CallToolResult object, which includes an isError flag\nand other metadata. It does not raise an exception if the tool call results in an error.\n\n**Args:**\n- `name`: The name of the tool to call.\n- `arguments`: Arguments to pass to the tool.\n- `timeout`: The timeout for the tool call. Defaults to None.\n- `progress_handler`: The progress handler to use for the tool call. Defaults to None.\n- `meta`: Additional metadata to include with the request.\nThis is useful for passing contextual information (like user IDs, trace IDs, or preferences)\nthat shouldn't be tool arguments but may influence server-side processing. The server\ncan access this via `context.request_context.meta`. Defaults to None.\n\n**Returns:**\n- mcp.types.CallToolResult: The complete response object from the protocol,\ncontaining the tool result and any additional metadata.\n\n**Raises:**\n- `RuntimeError`: If called while the client is not connected.\n- `McpError`: If the tool call requests results in a TimeoutError | JSONRPCError\n\n\n#### `call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L191\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult\n```\n\n#### `call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L205\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> ToolTask\n```\n\n#### `call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L220\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult | ToolTask\n```\n\nCall a tool on the server.\n\nUnlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.\n\n**Args:**\n- `name`: The name of the tool to call.\n- `arguments`: Arguments to pass to the tool. Defaults to None.\n- `version`: Specific tool version to call. If None, calls highest version.\n- `timeout`: The timeout for the tool call. Defaults to None.\n- `progress_handler`: The progress handler to use for the tool call. Defaults to None.\n- `raise_on_error`: Whether to raise an exception if the tool call results in an error. Defaults to True.\n- `meta`: Additional metadata to include with the request.\nThis is useful for passing contextual information (like user IDs, trace IDs, or preferences)\nthat shouldn't be tool arguments but may influence server-side processing. The server\ncan access this via `context.request_context.meta`. Defaults to None.\n- `task`: If True, execute as background task (SEP-1686). Defaults to False.\n- `task_id`: Optional client-provided task ID (auto-generated if not provided).\n- `ttl`: Time to keep results available in milliseconds (default 60s).\n\n**Returns:**\n- CallToolResult | ToolTask: The content returned by the tool if task=False,\nor a ToolTask object if task=True. If the tool returns structured\noutputs, they are returned as a dataclass (if an output schema\nis available) or a dictionary; otherwise, a list of content\nblocks is returned. Note: to receive both structured and\nunstructured outputs, use call_tool_mcp instead and access the\nraw result object.\n\n**Raises:**\n- `ToolError`: If the tool call results in an error.\n- `McpError`: If the tool call request results in a TimeoutError | JSONRPCError\n- `RuntimeError`: If called while the client is not connected.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-oauth_callback.mdx",
    "content": "---\ntitle: oauth_callback\nsidebarTitle: oauth_callback\n---\n\n# `fastmcp.client.oauth_callback`\n\n\n\nOAuth callback server for handling authorization code flows.\n\nThis module provides a reusable callback server that can handle OAuth redirects\nand display styled responses to users.\n\n\n## Functions\n\n### `create_callback_html` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/oauth_callback.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_callback_html(message: str, is_success: bool = True, title: str = 'FastMCP OAuth', server_url: str | None = None) -> str\n```\n\n\nCreate a styled HTML response for OAuth callbacks.\n\n\n### `create_oauth_callback_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/oauth_callback.py#L103\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_oauth_callback_server(port: int, callback_path: str = '/callback', server_url: str | None = None, result_container: OAuthCallbackResult | None = None, result_ready: anyio.Event | None = None) -> Server\n```\n\n\nCreate an OAuth callback server.\n\n**Args:**\n- `port`: The port to run the server on\n- `callback_path`: The path to listen for OAuth redirects on\n- `server_url`: Optional server URL to display in success messages\n- `result_container`: Optional container to store callback results\n- `result_ready`: Optional event to signal when callback is received\n\n**Returns:**\n- Configured uvicorn Server instance (not yet running)\n\n\n## Classes\n\n### `CallbackResponse` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/oauth_callback.py#L80\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `from_dict` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/oauth_callback.py#L87\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_dict(cls, data: dict[str, str]) -> CallbackResponse\n```\n\n#### `to_dict` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/oauth_callback.py#L90\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_dict(self) -> dict[str, str]\n```\n\n### `OAuthCallbackResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/oauth_callback.py#L95\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nContainer for OAuth callback results, used with anyio.Event for async coordination.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-progress.mdx",
    "content": "---\ntitle: progress\nsidebarTitle: progress\n---\n\n# `fastmcp.client.progress`\n\n## Functions\n\n### `default_progress_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/progress.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_progress_handler(progress: float, total: float | None, message: str | None) -> None\n```\n\n\nDefault handler for progress notifications.\n\nLogs progress updates at debug level, properly handling missing total or message values.\n\n**Args:**\n- `progress`: Current progress value\n- `total`: Optional total expected value\n- `message`: Optional status message\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-roots.mdx",
    "content": "---\ntitle: roots\nsidebarTitle: roots\n---\n\n# `fastmcp.client.roots`\n\n## Functions\n\n### `convert_roots_list` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/roots.py#L19\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_roots_list(roots: RootsList) -> list[mcp.types.Root]\n```\n\n### `create_roots_callback` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/roots.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_roots_callback(handler: RootsList | RootsHandler) -> ListRootsFnT\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-sampling-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.client.sampling`\n\n## Functions\n\n### `create_sampling_callback` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/sampling/__init__.py#L44\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-sampling-handlers-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.client.sampling.handlers`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx",
    "content": "---\ntitle: anthropic\nsidebarTitle: anthropic\n---\n\n# `fastmcp.client.sampling.handlers.anthropic`\n\n\nAnthropic sampling handler for FastMCP.\n\n## Classes\n\n### `AnthropicSamplingHandler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/sampling/handlers/anthropic.py#L72\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSampling handler that uses the Anthropic API.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-sampling-handlers-google_genai.mdx",
    "content": "---\ntitle: google_genai\nsidebarTitle: google_genai\n---\n\n# `fastmcp.client.sampling.handlers.google_genai`\n\n\nGoogle GenAI sampling handler with tool support for FastMCP 3.0.\n\n## Classes\n\n### `GoogleGenaiSamplingHandler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/sampling/handlers/google_genai.py#L56\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSampling handler that uses the Google GenAI API with tool support.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-sampling-handlers-openai.mdx",
    "content": "---\ntitle: openai\nsidebarTitle: openai\n---\n\n# `fastmcp.client.sampling.handlers.openai`\n\n\nOpenAI sampling handler for FastMCP.\n\n## Classes\n\n### `OpenAISamplingHandler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/sampling/handlers/openai.py#L95\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSampling handler that uses the OpenAI API.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-tasks.mdx",
    "content": "---\ntitle: tasks\nsidebarTitle: tasks\n---\n\n# `fastmcp.client.tasks`\n\n\nSEP-1686 client Task classes.\n\n## Classes\n\n### `TaskNotificationHandler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMessageHandler that routes task status notifications to Task objects.\n\n\n**Methods:**\n\n#### `dispatch` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndispatch(self, message: Message) -> None\n```\n\nDispatch messages, including task status notifications.\n\n\n### `Task` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAbstract base class for MCP background tasks (SEP-1686).\n\nProvides a uniform API whether the server accepts background execution\nor executes synchronously (graceful degradation per SEP-1686).\n\n\n**Methods:**\n\n#### `task_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L105\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntask_id(self) -> str\n```\n\nGet the task ID.\n\n\n#### `returned_immediately` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L110\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreturned_immediately(self) -> bool\n```\n\nCheck if server executed the task immediately.\n\n**Returns:**\n- True if server executed synchronously (graceful degradation or no task support)\n- False if server accepted background execution\n\n\n#### `on_status_change` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L145\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_status_change(self, callback: Callable[[GetTaskResult], None | Awaitable[None]]) -> None\n```\n\nRegister callback for status change notifications.\n\nThe callback will be invoked when a notifications/tasks/status is received\nfor this task (optional server feature per SEP-1686 lines 436-444).\n\nSupports both sync and async callbacks (auto-detected).\n\n**Args:**\n- `callback`: Function to call with GetTaskResult when status changes.\n     Can return None (sync) or Awaitable[None] (async).\n\n\n#### `status` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L171\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nstatus(self) -> GetTaskResult\n```\n\nGet current task status.\n\nIf server executed immediately, returns synthetic completed status.\nOtherwise queries the server for current status.\n\n\n#### `result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L202\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresult(self) -> TaskResultT\n```\n\nWait for and return the task result.\n\nMust be implemented by subclasses to return the appropriate result type.\n\n\n#### `wait` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L209\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwait(self) -> GetTaskResult\n```\n\nWait for task to reach a specific state or complete.\n\nUses event-based waiting when notifications are available (fast),\nwith fallback to polling (reliable). Optimally wakes up immediately\non status changes when server sends notifications/tasks/status.\n\n**Args:**\n- `state`: Desired state ('submitted', 'working', 'completed', 'failed').\n   If None, waits for any terminal state (completed/failed)\n- `timeout`: Maximum time to wait in seconds\n\n**Returns:**\n- Final task status\n\n**Raises:**\n- `TimeoutError`: If desired state not reached within timeout\n\n\n#### `cancel` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L272\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncancel(self) -> None\n```\n\nCancel this task, transitioning it to cancelled state.\n\nSends a tasks/cancel protocol request. The server will attempt to halt\nexecution and move the task to cancelled state.\n\nNote: If server executed immediately (graceful degradation), this is a no-op\nas there's no server-side task to cancel.\n\n\n### `ToolTask` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L294\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRepresents a tool call that may execute in background or immediately.\n\nProvides a uniform API whether the server accepts background execution\nor executes synchronously (graceful degradation per SEP-1686).\n\n\n**Methods:**\n\n#### `result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L336\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresult(self) -> CallToolResult\n```\n\nWait for and return the tool result.\n\nIf server executed immediately, returns the immediate result.\nOtherwise waits for background task to complete and retrieves result.\n\n**Returns:**\n- The parsed tool result (same as call_tool returns)\n\n\n### `PromptTask` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L396\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRepresents a prompt call that may execute in background or immediately.\n\nProvides a uniform API whether the server accepts background execution\nor executes synchronously (graceful degradation per SEP-1686).\n\n\n**Methods:**\n\n#### `result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L427\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresult(self) -> mcp.types.GetPromptResult\n```\n\nWait for and return the prompt result.\n\nIf server executed immediately, returns the immediate result.\nOtherwise waits for background task to complete and retrieves result.\n\n**Returns:**\n- The prompt result with messages and description\n\n\n### `ResourceTask` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L461\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRepresents a resource read that may execute in background or immediately.\n\nProvides a uniform API whether the server accepts background execution\nor executes synchronously (graceful degradation per SEP-1686).\n\n\n**Methods:**\n\n#### `result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/tasks.py#L497\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresult(self) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]\n```\n\nWait for and return the resource contents.\n\nIf server executed immediately, returns the immediate result.\nOtherwise waits for background task to complete and retrieves result.\n\n**Returns:**\n- list\\[ReadResourceContents]: The resource contents\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-telemetry.mdx",
    "content": "---\ntitle: telemetry\nsidebarTitle: telemetry\n---\n\n# `fastmcp.client.telemetry`\n\n\nClient-side telemetry helpers.\n\n## Functions\n\n### `client_span` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/telemetry.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclient_span(name: str, method: str, component_key: str, session_id: str | None = None, resource_uri: str | None = None) -> Generator[Span, None, None]\n```\n\n\nCreate a CLIENT span with standard MCP attributes.\n\nAutomatically records any exception on the span and sets error status.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.client.transports`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.client.transports.base`\n\n## Classes\n\n### `SessionKwargs` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/base.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nKeyword arguments for the MCP ClientSession constructor.\n\n\n### `ClientTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/base.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAbstract base class for different MCP client transport mechanisms.\n\nA Transport is responsible for establishing and managing connections\nto an MCP server, and providing a ClientSession within an async context.\n\n\n**Methods:**\n\n#### `connect_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/base.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]\n```\n\nEstablishes a connection and yields an active ClientSession.\n\nThe ClientSession is *not* expected to be initialized in this context manager.\n\nThe session is guaranteed to be valid only within the scope of the\nasync context manager. Connection setup and teardown are handled\nwithin this context.\n\n**Args:**\n- `**session_kwargs`: Keyword arguments to pass to the ClientSession\n              constructor (e.g., callbacks, timeouts).\n\n\n#### `close` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/base.py#L73\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose(self)\n```\n\nClose the transport.\n\n\n#### `get_session_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/base.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_session_id(self) -> str | None\n```\n\nGet the session ID for this transport, if available.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-config.mdx",
    "content": "---\ntitle: config\nsidebarTitle: config\n---\n\n# `fastmcp.client.transports.config`\n\n## Classes\n\n### `MCPConfigTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/config.py#L25\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for connecting to one or more MCP servers defined in an MCPConfig.\n\nThis transport provides a unified interface to multiple MCP servers defined in an MCPConfig\nobject or dictionary matching the MCPConfig schema. It supports two key scenarios:\n\n1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.\n2. If the MCPConfig contains multiple servers, it creates a composite client by mounting\n   all servers on a single FastMCP instance, with each server's name, by default, used as its mounting prefix.\n\nIn the multiserver case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`\nand resources with the pattern `protocol://{server_name}/path/to/resource`.\n\nThis is particularly useful for creating clients that need to interact with multiple specialized\nMCP servers through a single interface, simplifying client code.\n\n**Examples:**\n\n```python\nfrom fastmcp import Client\n\n# Create a config with multiple servers\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather-api.example.com/mcp\",\n            \"transport\": \"http\"\n        },\n        \"calendar\": {\n            \"url\": \"https://calendar-api.example.com/mcp\",\n            \"transport\": \"http\"\n        }\n    }\n}\n\n# Create a client with the config\nclient = Client(config)\n\nasync with client:\n    # Access tools with prefixes\n    weather = await client.call_tool(\"weather_get_forecast\", {\"city\": \"London\"})\n    events = await client.call_tool(\"calendar_list_events\", {\"date\": \"2023-06-01\"})\n\n    # Access resources with prefixed URIs\n    icons = await client.read_resource(\"weather://weather/icons/sunny\")\n```\n\n\n**Methods:**\n\n#### `connect_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/config.py#L88\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]\n```\n\n#### `close` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/config.py#L205\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose(self)\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-http.mdx",
    "content": "---\ntitle: http\nsidebarTitle: http\n---\n\n# `fastmcp.client.transports.http`\n\n\nStreamable HTTP transport for FastMCP Client.\n\n## Classes\n\n### `StreamableHttpTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/http.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport implementation that connects to an MCP server via Streamable HTTP Requests.\n\n\n**Methods:**\n\n#### `connect_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/http.py#L147\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]\n```\n\n#### `get_session_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/http.py#L200\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_session_id(self) -> str | None\n```\n\n#### `close` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/http.py#L208\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose(self)\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-inference.mdx",
    "content": "---\ntitle: inference\nsidebarTitle: inference\n---\n\n# `fastmcp.client.transports.inference`\n\n## Functions\n\n### `infer_transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/inference.py#L61\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninfer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport\n```\n\n\nInfer the appropriate transport type from the given transport argument.\n\nThis function attempts to infer the correct transport type from the provided\nargument, handling various input types and converting them to the appropriate\nClientTransport subclass.\n\nThe function supports these input types:\n- ClientTransport: Used directly without modification\n- FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport\n- Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)\n- AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)\n- MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers\n\nFor HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.\n\nFor MCPConfig with multiple servers, a composite client is created where each server\nis mounted with its name as prefix. This allows accessing tools and resources from multiple\nservers through a single unified client interface, using naming patterns like\n`servername_toolname` for tools and `protocol://servername/path` for resources.\nIf the MCPConfig contains only one server, a direct connection is established without prefixing.\n\n**Examples:**\n\n```python\n# Connect to a local Python script\ntransport = infer_transport(\"my_script.py\")\n\n# Connect to a remote server via HTTP\ntransport = infer_transport(\"http://example.com/mcp\")\n\n# Connect to multiple servers using MCPConfig\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\"url\": \"http://weather.example.com/mcp\"},\n        \"calendar\": {\"url\": \"http://calendar.example.com/mcp\"}\n    }\n}\ntransport = infer_transport(config)\n```\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-memory.mdx",
    "content": "---\ntitle: memory\nsidebarTitle: memory\n---\n\n# `fastmcp.client.transports.memory`\n\n## Classes\n\n### `FastMCPTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/memory.py#L14\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nIn-memory transport for FastMCP servers.\n\nThis transport connects directly to a FastMCP server instance in the same\nPython process. It works with both FastMCP 2.x servers and FastMCP 1.0\nservers from the low-level MCP SDK. This is particularly useful for unit\ntests or scenarios where client and server run in the same runtime.\n\n\n**Methods:**\n\n#### `connect_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/memory.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-sse.mdx",
    "content": "---\ntitle: sse\nsidebarTitle: sse\n---\n\n# `fastmcp.client.transports.sse`\n\n\nServer-Sent Events (SSE) transport for FastMCP Client.\n\n## Classes\n\n### `SSETransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/sse.py#L25\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport implementation that connects to an MCP server via Server-Sent Events.\n\n\n**Methods:**\n\n#### `connect_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/sse.py#L115\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-client-transports-stdio.mdx",
    "content": "---\ntitle: stdio\nsidebarTitle: stdio\n---\n\n# `fastmcp.client.transports.stdio`\n\n## Classes\n\n### `StdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase transport for connecting to an MCP server via subprocess with stdio.\n\nThis is a base class that can be subclassed for specific command-based\ntransports like Python, Node, Uvx, etc.\n\n\n**Methods:**\n\n#### `connect_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L72\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]\n```\n\n#### `connect` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L84\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconnect(self, **session_kwargs: Unpack[SessionKwargs]) -> ClientSession | None\n```\n\n#### `disconnect` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L120\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndisconnect(self)\n```\n\n#### `close` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L135\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose(self)\n```\n\n### `PythonStdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L205\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for running Python scripts.\n\n\n### `FastMCPStdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L258\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for running FastMCP servers using the FastMCP CLI.\n\n\n### `NodeStdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L287\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for running Node.js scripts.\n\n\n### `UvStdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L340\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for running commands via the uv tool.\n\n\n### `UvxStdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L419\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for running commands via the uvx tool.\n\n\n### `NpxStdioTransport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/client/transports/stdio.py#L484\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransport for running commands via the npx tool.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-decorators.mdx",
    "content": "---\ntitle: decorators\nsidebarTitle: decorators\n---\n\n# `fastmcp.decorators`\n\n\nShared decorator utilities for FastMCP.\n\n## Functions\n\n### `resolve_task_config` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/decorators.py#L17\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_task_config(task: bool | TaskConfig | None) -> bool | TaskConfig\n```\n\n\nResolve task config, defaulting None to False.\n\n\n### `get_fastmcp_meta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/decorators.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_fastmcp_meta(fn: Any) -> Any | None\n```\n\n\nExtract FastMCP metadata from a function, handling bound methods and wrappers.\n\n\n## Classes\n\n### `HasFastMCPMeta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/decorators.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for callables decorated with FastMCP metadata.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-dependencies.mdx",
    "content": "---\ntitle: dependencies\nsidebarTitle: dependencies\n---\n\n# `fastmcp.dependencies`\n\n\nDependency injection exports for FastMCP.\n\nThis module re-exports dependency injection symbols to provide a clean,\ncentralized import location for all dependency-related functionality.\n\nDI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket\nusing the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,\nCurrentWorker) and background task execution require fastmcp[tasks].\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-exceptions.mdx",
    "content": "---\ntitle: exceptions\nsidebarTitle: exceptions\n---\n\n# `fastmcp.exceptions`\n\n\nCustom exceptions for FastMCP.\n\n## Classes\n\n### `FastMCPError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L6\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase error for FastMCP.\n\n\n### `ValidationError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L10\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError in validating parameters or return values.\n\n\n### `ResourceError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L14\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError in resource operations.\n\n\n### `ToolError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L18\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError in tool operations.\n\n\n### `PromptError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError in prompt operations.\n\n\n### `InvalidSignature` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInvalid signature for use with FastMCP.\n\n\n### `ClientError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L30\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError in client operations.\n\n\n### `NotFoundError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nObject not found.\n\n\n### `DisabledError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nObject is disabled.\n\n\n### `AuthorizationError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/exceptions.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError when authorization check fails.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-experimental-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.experimental`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-experimental-sampling-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.experimental.sampling`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-experimental-sampling-handlers.mdx",
    "content": "---\ntitle: handlers\nsidebarTitle: handlers\n---\n\n# `fastmcp.experimental.sampling.handlers`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-experimental-transforms-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.experimental.transforms`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx",
    "content": "---\ntitle: code_mode\nsidebarTitle: code_mode\n---\n\n# `fastmcp.experimental.transforms.code_mode`\n\n## Classes\n\n### `SandboxProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L73\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInterface for executing LLM-generated Python code in a sandbox.\n\nWARNING: The ``code`` parameter passed to ``run`` contains untrusted,\nLLM-generated Python.  Implementations MUST execute it in an isolated\nsandbox — never with plain ``exec()``.  Use ``MontySandboxProvider``\n(backed by ``pydantic-monty``) for production workloads.\n\n\n**Methods:**\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L82\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, code: str) -> Any\n```\n\n### `MontySandboxProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L91\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSandbox provider backed by `pydantic-monty`.\n\n**Args:**\n- `limits`: Resource limits for sandbox execution. Supported keys\\:\n``max_duration_secs`` (float), ``max_allocations`` (int),\n``max_memory`` (int), ``max_recursion_depth`` (int),\n``gc_interval`` (int).  All are optional; omit a key to\nleave that limit uncapped.\n\n\n**Methods:**\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L109\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, code: str) -> Any\n```\n\n### `Search` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L179\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDiscovery tool factory that searches the catalog by query.\n\n**Args:**\n- `search_fn`: Async callable ``(tools, query) -> matching_tools``.\nDefaults to BM25 ranking.\n- `name`: Name of the synthetic tool exposed to the LLM.\n- `default_detail`: Default detail level for search results.\n``\"brief\"`` returns tool names and descriptions only.\n``\"detailed\"`` returns compact markdown with parameter schemas.\n``\"full\"`` returns complete JSON tool definitions.\n- `default_limit`: Maximum number of results to return.\nThe LLM can override this per call.  ``None`` means no limit.\n\n\n### `GetSchemas` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L261\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDiscovery tool factory that returns schemas for tools by name.\n\n**Args:**\n- `name`: Name of the synthetic tool exposed to the LLM.\n- `default_detail`: Default detail level for schema results.\n``\"brief\"`` returns tool names and descriptions only.\n``\"detailed\"`` renders compact markdown with parameter names,\ntypes, and required markers.\n``\"full\"`` returns the complete JSON schema.\n\n\n### `GetTags` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L322\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDiscovery tool factory that lists tool tags from the catalog.\n\nReads ``tool.tags`` from the catalog and groups tools by tag. Tools\nwithout tags appear under ``\"untagged\"``.\n\n**Args:**\n- `name`: Name of the synthetic tool exposed to the LLM.\n- `default_detail`: Default detail level.\n``\"brief\"`` returns tag names with tool counts.\n``\"full\"`` lists all tools under each tag.\n\n\n### `ListTools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L389\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDiscovery tool factory that lists all tools in the catalog.\n\n**Args:**\n- `name`: Name of the synthetic tool exposed to the LLM.\n- `default_detail`: Default detail level.\n``\"brief\"`` returns tool names and one-line descriptions.\n``\"detailed\"`` returns compact markdown with parameter schemas.\n``\"full\"`` returns the complete JSON schema.\n\n\n### `CodeMode` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L438\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransform that collapses all tools into discovery + execute meta-tools.\n\nDiscovery tools are composable via the ``discovery_tools`` parameter.\nEach is a callable that receives catalog access and returns a ``Tool``.\nBy default, ``Search`` and ``GetSchemas`` are included for\nprogressive disclosure: search finds candidates, get_schema retrieves\nparameter details, and execute runs code.\n\nThe ``execute`` tool is always present and provides a sandboxed Python\nenvironment with ``call_tool(name, params)`` in scope.\n\n\n**Methods:**\n\n#### `transform_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L488\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/experimental/transforms/code_mode.py#L491\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-mcp_config.mdx",
    "content": "---\ntitle: mcp_config\nsidebarTitle: mcp_config\n---\n\n# `fastmcp.mcp_config`\n\n\nCanonical MCP Configuration Format.\n\nThis module defines the standard configuration format for Model Context Protocol (MCP) servers.\nIt provides a client-agnostic, extensible format that can be used across all MCP implementations.\n\nThe configuration format supports both stdio and remote (HTTP/SSE) transports, with comprehensive\nfield definitions for server metadata, authentication, and execution parameters.\n\nExample configuration:\n```json\n{\n    \"mcpServers\": {\n        \"my-server\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@my/mcp-server\"],\n            \"env\": {\"API_KEY\": \"secret\"},\n            \"timeout\": 30000,\n            \"description\": \"My MCP server\"\n        }\n    }\n}\n```\n\n\n## Functions\n\n### `infer_transport_type_from_url` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L56\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninfer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']\n```\n\n\nInfer the appropriate transport type from the given URL.\n\n\n### `update_config_file` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L345\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nupdate_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None\n```\n\n\nUpdate an MCP configuration file from a server object, preserving existing fields.\n\nThis is used for updating the mcpServer configurations of third-party tools so we do not\nworry about transforming server objects here.\n\n\n## Classes\n\n### `StdioMCPServer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMCP server configuration for stdio transport.\n\nThis is the canonical configuration format for MCP servers using stdio transport.\n\n\n**Methods:**\n\n#### `to_transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L188\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_transport(self) -> StdioTransport\n```\n\n### `TransformingStdioMCPServer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L200\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA Stdio server with tool transforms.\n\n\n### `RemoteMCPServer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L204\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMCP server configuration for HTTP/SSE transport.\n\nThis is the canonical configuration format for MCP servers using remote transports.\n\n\n**Methods:**\n\n#### `to_transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L240\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_transport(self) -> StreamableHttpTransport | SSETransport\n```\n\n### `TransformingRemoteMCPServer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L265\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA Remote server with tool transforms.\n\n\n### `MCPConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L276\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA configuration object for MCP Servers that conforms to the canonical MCP configuration format\nwhile adding additional fields for enabling FastMCP-specific features like tool transformations\nand filtering by tags.\n\nFor an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.\n\n\n**Methods:**\n\n#### `wrap_servers_at_root` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L290\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]\n```\n\nIf there's no mcpServers key but there are server configs at root, wrap them.\n\n\n#### `add_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L303\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_server(self, name: str, server: MCPServerTypes) -> None\n```\n\nAdd or update a server in the configuration.\n\n\n#### `from_dict` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L308\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_dict(cls, config: dict[str, Any]) -> Self\n```\n\nParse MCP configuration from dictionary format.\n\n\n#### `to_dict` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L312\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_dict(self) -> dict[str, Any]\n```\n\nConvert MCPConfig to dictionary format, preserving all fields.\n\n\n#### `write_to_file` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L316\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrite_to_file(self, file_path: Path) -> None\n```\n\nWrite configuration to JSON file.\n\n\n#### `from_file` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L322\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_file(cls, file_path: Path) -> Self\n```\n\nLoad configuration from JSON file.\n\n\n### `CanonicalMCPConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L330\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCanonical MCP configuration format.\n\nThis defines the standard configuration format for Model Context Protocol servers.\nThe format is designed to be client-agnostic and extensible for future use cases.\n\n\n**Methods:**\n\n#### `add_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/mcp_config.py#L340\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_server(self, name: str, server: CanonicalMCPServerTypes) -> None\n```\n\nAdd or update a server in the configuration.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-prompts-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.prompts`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-prompts-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.prompts.base`\n\n\nBase classes for FastMCP prompts.\n\n## Classes\n\n### `Message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nWrapper for prompt message with auto-serialization.\n\nAccepts any content - strings pass through, other types\n(dict, list, BaseModel) are JSON-serialized to text.\n\n\n**Methods:**\n\n#### `to_mcp_prompt_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L98\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_prompt_message(self) -> PromptMessage\n```\n\nConvert to MCP PromptMessage.\n\n\n### `PromptArgument` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L103\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAn argument that can be passed to a prompt.\n\n\n### `PromptResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L115\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCanonical result type for prompt rendering.\n\nProvides explicit control over prompt responses: multiple messages,\nroles, and metadata at both the message and result level.\n\n\n**Methods:**\n\n#### `to_mcp_prompt_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L187\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_prompt_result(self) -> GetPromptResult\n```\n\nConvert to MCP GetPromptResult.\n\n\n### `Prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L197\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA prompt template that can be rendered with parameters.\n\n\n**Methods:**\n\n#### `to_mcp_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L209\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_prompt(self, **overrides: Any) -> SDKPrompt\n```\n\nConvert the prompt to an MCP prompt.\n\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L235\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any]) -> FunctionPrompt\n```\n\nCreate a Prompt from a function.\n\nThe function can return:\n- str: wrapped as single user Message\n- list\\[Message | str]: converted to list\\[Message]\n- PromptResult: used directly\n\n\n#### `render` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L271\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender(self, arguments: dict[str, Any] | None = None) -> str | list[Message | str] | PromptResult\n```\n\nRender the prompt with arguments.\n\nSubclasses must implement this method. Return one of:\n- str: Wrapped as single user Message\n- list\\[Message | str]: Converted to list\\[Message]\n- PromptResult: Used directly\n\n\n#### `convert_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L284\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_result(self, raw_value: Any) -> PromptResult\n```\n\nConvert a raw return value to PromptResult.\n\n**Raises:**\n- `TypeError`: for unsupported types\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L374\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this prompt with docket for background execution.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L380\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, arguments: dict[str, Any] | None, **kwargs: Any) -> Execution\n```\n\nSchedule this prompt for background execution via docket.\n\n**Args:**\n- `docket`: The Docket instance\n- `arguments`: Prompt arguments\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/base.py#L403\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-prompts-function_prompt.mdx",
    "content": "---\ntitle: function_prompt\nsidebarTitle: function_prompt\n---\n\n# `fastmcp.prompts.function_prompt`\n\n\nStandalone @prompt decorator for FastMCP.\n\n## Functions\n\n### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L402\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(name_or_fn: str | Callable[..., Any] | None = None) -> Any\n```\n\n\nStandalone decorator to mark a function as an MCP prompt.\n\nReturns the original function with metadata attached. Register with a server\nusing mcp.add_prompt().\n\n\n## Classes\n\n### `DecoratedPrompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L53\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for functions decorated with @prompt.\n\n\n### `PromptMeta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L62\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMetadata attached to functions by the @prompt decorator.\n\n\n### `FunctionPrompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA prompt that is a function.\n\n\n**Methods:**\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L84\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any]) -> FunctionPrompt\n```\n\nCreate a Prompt from a function.\n\n**Args:**\n- `fn`: The function to wrap\n- `metadata`: PromptMeta object with all configuration. If provided,\nindividual parameters must not be passed.\n- `name, title, etc.`: Individual parameters for backwards compatibility.\nCannot be used together with metadata parameter.\n\nThe function can return:\n- str: wrapped as single user Message\n- list\\[Message | str]: converted to list\\[Message]\n- PromptResult: used directly\n\n\n#### `render` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L285\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender(self, arguments: dict[str, Any] | None = None) -> PromptResult\n```\n\nRender the prompt with arguments.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L335\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this prompt with docket for background execution.\n\nFunctionPrompt registers the underlying function, which has the user's\nDepends parameters for docket to resolve.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/prompts/function_prompt.py#L345\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, arguments: dict[str, Any] | None, **kwargs: Any) -> Execution\n```\n\nSchedule this prompt for background execution via docket.\n\nFunctionPrompt splats the arguments dict since .fn expects **kwargs.\n\n**Args:**\n- `docket`: The Docket instance\n- `arguments`: Prompt arguments\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-resources-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.resources`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-resources-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.resources.base`\n\n\nBase classes and interfaces for FastMCP resources.\n\n## Classes\n\n### `ResourceContent` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L37\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nWrapper for resource content with optional MIME type and metadata.\n\nAccepts any value for content - strings and bytes pass through directly,\nother types (dict, list, BaseModel, etc.) are automatically JSON-serialized.\n\n\n**Methods:**\n\n#### `to_mcp_resource_contents` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L92\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_resource_contents(self, uri: AnyUrl | str) -> mcp.types.TextResourceContents | mcp.types.BlobResourceContents\n```\n\nConvert to MCP resource contents type.\n\n**Args:**\n- `uri`: The URI of the resource (required by MCP types)\n\n**Returns:**\n- TextResourceContents for str content, BlobResourceContents for bytes\n\n\n### `ResourceResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L119\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCanonical result type for resource reads.\n\nProvides explicit control over resource responses: multiple content items,\nper-item MIME types, and metadata at both the item and result level.\n\n\n**Methods:**\n\n#### `to_mcp_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L194\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_result(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult\n```\n\nConvert to MCP ReadResourceResult.\n\n**Args:**\n- `uri`: The URI of the resource (required by MCP types)\n\n**Returns:**\n- MCP ReadResourceResult with converted contents\n\n\n### `Resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L210\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for all resources.\n\n\n**Methods:**\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L235\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any], uri: str | AnyUrl) -> FunctionResource\n```\n\n#### `set_default_mime_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L274\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_default_mime_type(cls, mime_type: str | None) -> str\n```\n\nSet default MIME type if not provided.\n\n\n#### `set_default_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L281\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_default_name(self) -> Self\n```\n\nSet default name from URI if not provided.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L291\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> str | bytes | ResourceResult\n```\n\nRead the resource content.\n\nSubclasses implement this to return resource data. Supported return types:\n    - str: Text content\n    - bytes: Binary content\n    - ResourceResult: Full control over contents and result-level meta\n\n\n#### `convert_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L303\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_result(self, raw_value: Any) -> ResourceResult\n```\n\nConvert a raw result to ResourceResult.\n\nThis is used in two contexts:\n1. In _read() to convert user function return values to ResourceResult\n2. In tasks_result_handler() to convert Docket task results to ResourceResult\n\nHandles ResourceResult passthrough and converts raw values using\nResourceResult's normalization.  When the raw value is a plain\nstring or bytes, the resource's own ``mime_type`` is forwarded so\nthat ``ui://`` resources (and others with non-default MIME types)\ndon't fall back to ``text/plain``.\n\nThe resource's component-level ``meta`` (e.g. ``ui`` metadata for\nMCP Apps CSP/permissions) is propagated to each content item so\nthat hosts can read it from the ``resources/read`` response.\n\n\n#### `to_mcp_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L374\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_resource(self, **overrides: Any) -> SDKResource\n```\n\nConvert the resource to an SDKResource.\n\n\n#### `key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L397\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nkey(self) -> str\n```\n\nThe globally unique lookup key for this resource.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L402\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this resource with docket for background execution.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L408\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, **kwargs: Any) -> Execution\n```\n\nSchedule this resource for background execution via docket.\n\n**Args:**\n- `docket`: The Docket instance\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/base.py#L429\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-resources-function_resource.mdx",
    "content": "---\ntitle: function_resource\nsidebarTitle: function_resource\n---\n\n# `fastmcp.resources.function_resource`\n\n\nStandalone @resource decorator for FastMCP.\n\n## Functions\n\n### `resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L240\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresource(uri: str) -> Callable[[F], F]\n```\n\n\nStandalone decorator to mark a function as an MCP resource.\n\nReturns the original function with metadata attached. Register with a server\nusing mcp.add_resource().\n\n\n## Classes\n\n### `DecoratedResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L40\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for functions decorated with @resource.\n\n\n### `ResourceMeta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L49\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMetadata attached to functions by the @resource decorator.\n\n\n### `FunctionResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource that defers data loading by wrapping a function.\n\nThe function is only called when the resource is read, allowing for lazy loading\nof potentially expensive data. This is particularly useful when listing resources,\nas the function won't be called until the resource is actually accessed.\n\nThe function can return:\n- str for text content (default)\n- bytes for binary content\n- other types will be converted to JSON\n\n\n**Methods:**\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L84\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any], uri: str | AnyUrl | None = None) -> FunctionResource\n```\n\nCreate a FunctionResource from a function.\n\n**Args:**\n- `fn`: The function to wrap\n- `uri`: The URI for the resource (required if metadata not provided)\n- `metadata`: ResourceMeta object with all configuration. If provided,\nindividual parameters must not be passed.\n- `name, title, etc.`: Individual parameters for backwards compatibility.\nCannot be used together with metadata parameter.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L208\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> str | bytes | ResourceResult\n```\n\nRead the resource by calling the wrapped function.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/function_resource.py#L229\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this resource with docket for background execution.\n\nFunctionResource registers the underlying function, which has the user's\nDepends parameters for docket to resolve.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-resources-template.mdx",
    "content": "---\ntitle: template\nsidebarTitle: template\n---\n\n# `fastmcp.resources.template`\n\n\nResource template functionality.\n\n## Functions\n\n### `extract_query_params` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nextract_query_params(uri_template: str) -> set[str]\n```\n\n\nExtract query parameter names from RFC 6570 `{?param1,param2}` syntax.\n\n\n### `build_regex` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nbuild_regex(template: str) -> re.Pattern[str] | None\n```\n\n\nBuild regex pattern for URI template, handling RFC 6570 syntax.\n\nSupports:\n- `{var}` - simple path parameter\n- `{var*}` - wildcard path parameter (captures multiple segments)\n- `{?var1,var2}` - query parameters (ignored in path matching)\n\nReturns None if the template produces an invalid regex (e.g. parameter\nnames with hyphens, leading digits, or duplicates from a remote server).\n\n\n### `match_uri_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L79\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmatch_uri_template(uri: str, uri_template: str) -> dict[str, str] | None\n```\n\n\nMatch URI against template and extract both path and query parameters.\n\nSupports RFC 6570 URI templates:\n- Path params: `{var}`, `{var*}`\n- Query params: `{?var1,var2}`\n\n\n## Classes\n\n### `ResourceTemplate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L112\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA template for dynamically creating resources.\n\n\n**Methods:**\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_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\n```\n\n#### `set_default_mime_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L172\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_default_mime_type(cls, mime_type: str | None) -> str\n```\n\nSet default MIME type if not provided.\n\n\n#### `matches` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L178\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmatches(self, uri: str) -> dict[str, Any] | None\n```\n\nCheck if URI matches template and extract parameters.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L182\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult\n```\n\nRead the resource content.\n\n\n#### `convert_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L188\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_result(self, raw_value: Any) -> ResourceResult\n```\n\nConvert a raw result to ResourceResult.\n\nThis is used in two contexts:\n1. In _read() to convert user function return values to ResourceResult\n2. In tasks_result_handler() to convert Docket task results to ResourceResult\n\nHandles ResourceResult passthrough and converts raw values using\nResourceResult's normalization.\n\n\n#### `create_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L252\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_resource(self, uri: str, params: dict[str, Any]) -> Resource\n```\n\nCreate a resource from the template with the given parameters.\n\nThe base implementation does not support background tasks.\nUse FunctionResourceTemplate for task support.\n\n\n#### `to_mcp_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L263\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_template(self, **overrides: Any) -> SDKResourceTemplate\n```\n\nConvert the resource template to an SDKResourceTemplate.\n\n\n#### `from_mcp_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L283\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate\n```\n\nCreates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object.\n\n\n#### `key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L296\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nkey(self) -> str\n```\n\nThe globally unique lookup key for this template.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L301\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this template with docket for background execution.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L307\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution\n```\n\nSchedule this template for background execution via docket.\n\n**Args:**\n- `docket`: The Docket instance\n- `params`: Template parameters\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L330\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `FunctionResourceTemplate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L337\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA template for dynamically creating resources.\n\n\n**Methods:**\n\n#### `create_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L383\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_resource(self, uri: str, params: dict[str, Any]) -> Resource\n```\n\nCreate a resource from the template with the given parameters.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L402\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult\n```\n\nRead the resource content.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L441\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this template with docket for background execution.\n\nFunctionResourceTemplate registers the underlying function, which has the\nuser's Depends parameters for docket to resolve.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L451\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution\n```\n\nSchedule this template for background execution via docket.\n\nFunctionResourceTemplate splats the params dict since .fn expects **kwargs.\n\n**Args:**\n- `docket`: The Docket instance\n- `params`: Template parameters\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/template.py#L477\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_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\n```\n\nCreate a template from a function.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-resources-types.mdx",
    "content": "---\ntitle: types\nsidebarTitle: types\n---\n\n# `fastmcp.resources.types`\n\n\nConcrete resource implementations.\n\n## Classes\n\n### `TextResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L21\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource that reads from a string.\n\n\n**Methods:**\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nRead the text content.\n\n\n### `BinaryResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L37\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource that reads from bytes.\n\n\n**Methods:**\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nRead the binary content.\n\n\n### `FileResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L53\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource that reads from a file.\n\nSet is_binary=True to read file as binary data instead of text.\n\n\n**Methods:**\n\n#### `validate_absolute_path` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L75\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_absolute_path(cls, path: Path) -> Path\n```\n\nEnsure path is absolute.\n\n\n#### `set_binary_from_mime_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L83\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool\n```\n\nSet is_binary based on mime_type if not explicitly set.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L91\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nRead the file content.\n\n\n### `HttpResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L105\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource that reads from an HTTP endpoint.\n\n\n**Methods:**\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L114\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nRead the HTTP content.\n\n\n### `DirectoryResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L126\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource that lists files in a directory.\n\n\n**Methods:**\n\n#### `validate_absolute_path` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L146\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_absolute_path(cls, path: Path) -> Path\n```\n\nEnsure path is absolute.\n\n\n#### `list_files` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L152\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_files(self) -> list[Path]\n```\n\nList files in the directory.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/resources/types.py#L168\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nRead the directory listing.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-app.mdx",
    "content": "---\ntitle: app\nsidebarTitle: app\n---\n\n# `fastmcp.server.app`\n\n\nFastMCPApp — a Provider that represents a composable MCP application.\n\nFastMCPApp binds entry-point tools (model calls these) together with backend\ntools (the UI calls these via CallTool). Backend tools get global keys —\nUUID-suffixed stable identifiers that survive namespace transforms when\nservers are composed — so ``CallTool(save_contact)`` keeps working even when\nthe app is mounted under a namespace.\n\nUsage::\n\n    from fastmcp import FastMCP, FastMCPApp\n\n    app = FastMCPApp(\"Dashboard\")\n\n    @app.ui()\n    def show_dashboard() -> Component:\n        return Column(...)\n\n    @app.tool()\n    def save_contact(name: str, email: str) -> dict:\n        return {\"name\": name, \"email\": email}\n\n    server = FastMCP(\"Platform\")\n    server.add_provider(app)\n\n\n## Functions\n\n### `get_global_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L61\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_global_tool(name: str) -> Tool | None\n```\n\n\nLook up a tool by its global key, or return None.\n\n\n## Classes\n\n### `FastMCPApp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L167\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA Provider that represents an MCP application.\n\nBinds together entry-point tools (``@app.ui``), backend tools\n(``@app.tool``), the Prefab renderer resource, and global-key\ninfrastructure so that composed/namespaced servers can still reach\nbackend tools by stable identifiers.\n\n\n**Methods:**\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L189\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self, name_or_fn: F) -> F\n```\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L201\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self, name_or_fn: str | None = None) -> Callable[[F], F]\n```\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L212\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self, name_or_fn: str | AnyFunction | None = None) -> Any\n```\n\nRegister a backend tool that the UI calls via CallTool.\n\nBackend tools get a global key for composition safety and default\nto ``visibility=[\"app\"]``.  Pass ``model=True`` to also expose the\ntool to the model (``visibility=[\"app\", \"model\"]``).\n\nSupports multiple calling patterns::\n\n    @app.tool\n    def save(name: str): ...\n\n    @app.tool()\n    def save(name: str): ...\n\n    @app.tool(\"custom_name\")\n    def save(name: str): ...\n\n\n#### `ui` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L274\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nui(self, name_or_fn: F) -> F\n```\n\n#### `ui` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L289\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nui(self, name_or_fn: str | None = None) -> Callable[[F], F]\n```\n\n#### `ui` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L303\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nui(self, name_or_fn: str | AnyFunction | None = None) -> Any\n```\n\nRegister a UI entry-point tool that the model calls.\n\nEntry-point tools default to ``visibility=[\"model\"]`` and auto-wire\nthe Prefab renderer resource and CSP. They do NOT get a global key —\nthe model resolves them through the normal transform chain.\n\nSupports multiple calling patterns::\n\n    @app.ui\n    def dashboard() -> Component: ...\n\n    @app.ui()\n    def dashboard() -> Component: ...\n\n    @app.ui(\"my_dashboard\")\n    def dashboard() -> Component: ...\n\n\n#### `add_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L389\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_tool(self, tool: Tool | Callable[..., Any]) -> Tool\n```\n\nAdd a tool to this app programmatically.\n\nIf the tool has ``meta[\"ui\"][\"globalKey\"]``, it is assumed to already\nbe configured (but still registered for lookup). Otherwise it is\ntreated as a backend tool and gets a global key assigned automatically.\n\nPass ``fn`` to register the original callable in the resolver so that\n``CallTool(fn)`` can resolve to the global key.\n\n\n#### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L453\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(self) -> AsyncIterator[None]\n```\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/app.py#L461\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None\n```\n\nCreate a temporary FastMCP server and run this app standalone.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-apps.mdx",
    "content": "---\ntitle: apps\nsidebarTitle: apps\n---\n\n# `fastmcp.server.apps`\n\n\nMCP Apps support — extension negotiation and typed UI metadata models.\n\nProvides constants and Pydantic models for the MCP Apps extension\n(io.modelcontextprotocol/ui), enabling tools and resources to carry\nUI metadata for clients that support interactive app rendering.\n\n\n## Functions\n\n### `app_config_to_meta_dict` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/apps.py#L115\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\napp_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]\n```\n\n\nConvert an AppConfig or dict to the wire-format dict for ``meta[\"ui\"]``.\n\n\n### `resolve_ui_mime_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/apps.py#L122\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None\n```\n\n\nReturn the appropriate MIME type for a resource URI.\n\nFor ``ui://`` scheme resources, defaults to ``UI_MIME_TYPE`` when no\nexplicit MIME type is provided. This ensures UI resources are correctly\nidentified regardless of how they're registered (via FastMCP.resource,\nthe standalone @resource decorator, or resource templates).\n\n**Args:**\n- `uri`: The resource URI string\n- `explicit_mime_type`: The MIME type explicitly provided by the user\n\n**Returns:**\n- The resolved MIME type (explicit value, UI default, or None)\n\n\n## Classes\n\n### `ResourceCSP` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/apps.py#L18\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nContent Security Policy for MCP App resources.\n\nDeclares which external origins the app is allowed to connect to or\nload resources from.  Hosts use these declarations to build the\n``Content-Security-Policy`` header for the sandboxed iframe.\n\n\n### `ResourcePermissions` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/apps.py#L50\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nIframe sandbox permissions for MCP App resources.\n\nEach field, when set (typically to ``{}``), requests that the host\ngrant the corresponding Permission Policy feature to the sandboxed\niframe.  Hosts MAY honour these; apps should use JS feature detection\nas a fallback.\n\n\n### `AppConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/apps.py#L77\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for MCP App tools and resources.\n\nControls how a tool or resource participates in the MCP Apps extension.\nOn tools, ``resource_uri`` and ``visibility`` specify which UI resource\nto render and where the tool appears.  On resources, those fields must\nbe left unset (the resource itself is the UI).\n\nAll fields use ``exclude_none`` serialization so only explicitly-set\nvalues appear on the wire.  Aliases match the MCP Apps wire format\n(camelCase).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.auth`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-auth.mdx",
    "content": "---\ntitle: auth\nsidebarTitle: auth\n---\n\n# `fastmcp.server.auth.auth`\n\n## Classes\n\n### `AccessToken` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L54\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAccessToken that includes all JWT claims.\n\n\n### `TokenHandler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L60\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTokenHandler that returns MCP-compliant error responses.\n\nThis handler addresses two SDK issues:\n\n1. Error code: The SDK returns `unauthorized_client` for client authentication\n   failures, but RFC 6749 Section 5.2 requires `invalid_client` with HTTP 401.\n   This distinction matters for client re-registration behavior.\n\n2. Status code: The SDK returns HTTP 400 for all token errors including\n   `invalid_grant` (expired/invalid tokens). However, the MCP spec requires:\n   \"Invalid or expired tokens MUST receive a HTTP 401 response.\"\n\nThis handler transforms responses to be compliant with both OAuth 2.1 and MCP specs.\n\n\n**Methods:**\n\n#### `handle` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nhandle(self, request: Any)\n```\n\nWrap SDK handle() and transform auth error responses.\n\n\n### `PrivateKeyJWTClientAuthenticator` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L126\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nClient authenticator with private_key_jwt support for CIMD clients.\n\nExtends the SDK's ClientAuthenticator to add support for the `private_key_jwt`\nauthentication method per RFC 7523. This is required for CIMD (Client ID Metadata\nDocument) clients that use asymmetric keys for authentication.\n\nThe authenticator:\n1. Delegates to SDK for standard methods (client_secret_basic, client_secret_post, none)\n2. Adds private_key_jwt handling for CIMD clients\n3. Validates JWT assertions against client's JWKS\n\n\n**Methods:**\n\n#### `authenticate_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L156\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nauthenticate_request(self, request: Request) -> OAuthClientInformationFull\n```\n\nAuthenticate a client from an HTTP request.\n\nExtends SDK authentication to support private_key_jwt for CIMD clients.\nDelegates to SDK for client_secret_basic (Authorization header) and\nclient_secret_post (form body) authentication.\n\n\n### `AuthProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L207\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for all FastMCP authentication providers.\n\nThis class provides a unified interface for all authentication providers,\nwhether they are simple token verifiers or full OAuth authorization servers.\nAll providers must be able to verify tokens and can optionally provide\ncustom authentication routes.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L236\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a bearer token and return access info if valid.\n\nAll auth providers must implement token verification.\n\n**Args:**\n- `token`: The token string to validate\n\n**Returns:**\n- AccessToken object if valid, None if invalid or expired\n\n\n#### `set_mcp_path` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L249\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_mcp_path(self, mcp_path: str | None) -> None\n```\n\nSet the MCP endpoint path and compute resource URL.\n\nThis method is called by get_routes() to configure the expected\nresource URL before route creation. Subclasses can override to\nperform additional initialization that depends on knowing the\nMCP endpoint path.\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L263\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet all routes for this authentication provider.\n\nThis includes both well-known discovery routes and operational routes.\nEach provider is responsible for creating whatever routes it needs:\n- TokenVerifier: typically no routes (default implementation)\n- RemoteAuthProvider: protected resource metadata routes\n- OAuthProvider: full OAuth authorization server routes\n- Custom providers: whatever routes they need\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata, but the\nprovider does not create the actual MCP endpoint route.\n\n**Returns:**\n- List of all routes for this provider (excluding the MCP endpoint itself)\n\n\n#### `get_well_known_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L286\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_well_known_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet well-known discovery routes for this authentication provider.\n\nThis is a utility method that filters get_routes() to return only\nwell-known discovery routes (those starting with /.well-known/).\n\nWell-known routes provide OAuth metadata and discovery endpoints that\nclients use to discover authentication capabilities. These routes should\nbe mounted at the root level of the application to comply with RFC 8414\nand RFC 9728.\n\nCommon well-known routes:\n- /.well-known/oauth-authorization-server (authorization server metadata)\n- /.well-known/oauth-protected-resource/* (protected resource metadata)\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to construct path-scoped well-known URLs.\n\n**Returns:**\n- List of well-known discovery routes (typically mounted at root level)\n\n\n#### `get_middleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L318\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_middleware(self) -> list\n```\n\nGet HTTP application-level middleware for this auth provider.\n\n**Returns:**\n- List of Starlette Middleware instances to apply to the HTTP app\n\n\n### `TokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L352\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for token verifiers (Resource Servers).\n\nThis class provides token verification capability without OAuth server functionality.\nToken verifiers typically don't provide authentication routes by default.\n\n\n**Methods:**\n\n#### `scopes_supported` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L374\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nscopes_supported(self) -> list[str]\n```\n\nScopes to advertise in OAuth metadata.\n\nDefaults to required_scopes. Override in subclasses when the\nadvertised scopes differ from the validation scopes (e.g., Azure AD\nwhere tokens contain short-form scopes but clients request full URI\nscopes).\n\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L384\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a bearer token and return access info if valid.\n\n\n### `RemoteAuthProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L389\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAuthentication provider for resource servers that verify tokens from known authorization servers.\n\nThis provider composes a TokenVerifier with authorization server metadata to create\nstandardized OAuth 2.0 Protected Resource endpoints (RFC 9728). Perfect for:\n- JWT verification with known issuers\n- Remote token introspection services\n- Any resource server that knows where its tokens come from\n\nUse this when you have token verification logic and want to advertise\nthe authorization servers that issue valid tokens.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L436\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify token using the configured token verifier.\n\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L440\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet routes for this provider.\n\nCreates protected resource metadata routes (RFC 9728).\n\n\n### `MultiAuth` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L472\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComposes an optional auth server with additional token verifiers.\n\nUse this when a single server needs to accept tokens from multiple sources.\nFor example, an OAuth proxy for interactive clients combined with a JWT\nverifier for machine-to-machine tokens.\n\nToken verification tries the server first (if present), then each verifier\nin order, returning the first successful result. Routes and OAuth metadata\ncome from the server; verifiers contribute only token verification.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L537\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a token by trying the server, then each verifier in order.\n\nEach source is tried independently. If a source raises an exception,\nit is logged and treated as a non-match so that remaining sources\nstill get a chance to verify the token.\n\n\n#### `set_mcp_path` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L558\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_mcp_path(self, mcp_path: str | None) -> None\n```\n\nPropagate MCP path to the server and all verifiers.\n\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L566\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nDelegate route creation to the server.\n\n\n#### `get_well_known_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L572\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_well_known_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nDelegate well-known route creation to the server.\n\nThis ensures that server-specific well-known route logic (e.g.,\nOAuthProvider's RFC 8414 path-aware discovery) is preserved.\n\n\n### `OAuthProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L583\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth Authorization Server provider.\n\nThis class provides full OAuth server functionality including client registration,\nauthorization flows, token issuance, and token verification.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L646\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a bearer token and return access info if valid.\n\nThis method implements the TokenVerifier protocol by delegating\nto our existing load_access_token method.\n\n**Args:**\n- `token`: The token string to validate\n\n**Returns:**\n- AccessToken object if valid, None if invalid or expired\n\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L661\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet OAuth authorization server routes and optional protected resource routes.\n\nThis method creates the full set of OAuth routes including:\n- Standard OAuth authorization server routes (/.well-known/oauth-authorization-server, /authorize, /token, etc.)\n- Optional protected resource routes\n\n**Returns:**\n- List of OAuth routes\n\n\n#### `get_well_known_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/auth.py#L740\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_well_known_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet well-known discovery routes with RFC 8414 path-aware support.\n\nOverrides the base implementation to support path-aware authorization\nserver metadata discovery per RFC 8414. If issuer_url has a path component,\nthe authorization server metadata route is adjusted to include that path.\n\nFor example, if issuer_url is \"http://example.com/api\", the discovery\nendpoint will be at \"/.well-known/oauth-authorization-server/api\" instead\nof just \"/.well-known/oauth-authorization-server\".\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n\n**Returns:**\n- List of well-known discovery routes\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-authorization.mdx",
    "content": "---\ntitle: authorization\nsidebarTitle: authorization\n---\n\n# `fastmcp.server.auth.authorization`\n\n\nAuthorization checks for FastMCP components.\n\nThis module provides callable-based authorization for tools, resources, and prompts.\nAuth checks are functions that receive an AuthContext and return True to allow access\nor False to deny.\n\nAuth checks can also raise exceptions:\n- AuthorizationError: Propagates with the custom message for explicit denial\n- Other exceptions: Masked for security (logged, treated as auth failure)\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth import require_scopes\n\n    mcp = FastMCP()\n\n    @mcp.tool(auth=require_scopes(\"write\"))\n    def protected_tool(): ...\n\n    @mcp.resource(\"data://secret\", auth=require_scopes(\"read\"))\n    def secret_data(): ...\n\n    @mcp.prompt(auth=require_scopes(\"admin\"))\n    def admin_prompt(): ...\n    ```\n\n\n## Functions\n\n### `require_scopes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrequire_scopes(*scopes: str) -> AuthCheck\n```\n\n\nRequire specific OAuth scopes.\n\nReturns an auth check that requires ALL specified scopes to be present\nin the token (AND logic).\n\n**Args:**\n- `*scopes`: One or more scope strings that must all be present.\n\n\n### `restrict_tag` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L106\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrestrict_tag(tag: str) -> AuthCheck\n```\n\n\nRestrict components with a specific tag to require certain scopes.\n\nIf the component has the specified tag, the token must have ALL the\nrequired scopes. If the component doesn't have the tag, access is allowed.\n\n**Args:**\n- `tag`: The tag that triggers the scope requirement.\n- `scopes`: List of scopes required when the tag is present.\n\n\n### `run_auth_checks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L134\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool\n```\n\n\nRun auth checks with AND logic.\n\nAll checks must pass for authorization to succeed. Checks can be\nsynchronous or asynchronous functions.\n\nAuth checks can:\n- Return True to allow access\n- Return False to deny access\n- Raise AuthorizationError to deny with a custom message (propagates)\n- Raise other exceptions (masked for security, treated as denial)\n\n**Args:**\n- `checks`: A single check function or list of check functions.\nEach check can be sync (returns bool) or async (returns Awaitable[bool]).\n- `ctx`: The auth context to pass to each check.\n\n**Returns:**\n- True if all checks pass, False if any check fails.\n\n**Raises:**\n- `AuthorizationError`: If an auth check explicitly raises it.\n\n\n## Classes\n\n### `AuthContext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L48\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nContext passed to auth check callables.\n\nThis object is passed to each auth check function and provides\naccess to the current authentication token and the component being accessed.\n\n**Attributes:**\n- `token`: The current access token, or None if unauthenticated.\n- `component`: The component (tool, resource, or prompt) being accessed.\n- `tool`: Backwards-compatible alias for component when it's a Tool.\n\n\n**Methods:**\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L64\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self) -> Tool | None\n```\n\nBackwards-compatible access to the component as a Tool.\n\nReturns the component if it's a Tool, None otherwise.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-cimd.mdx",
    "content": "---\ntitle: cimd\nsidebarTitle: cimd\n---\n\n# `fastmcp.server.auth.cimd`\n\n\nCIMD (Client ID Metadata Document) support for FastMCP.\n\n.. warning::\n    **Beta Feature**: CIMD support is currently in beta. The API may change\n    in future releases. Please report any issues you encounter.\n\nCIMD is a simpler alternative to Dynamic Client Registration where clients\nhost a static JSON document at an HTTPS URL, and that URL becomes their\nclient_id. See the IETF draft: draft-parecki-oauth-client-id-metadata-document\n\nThis module provides:\n- CIMDDocument: Pydantic model for CIMD document validation\n- CIMDFetcher: Fetch and validate CIMD documents with SSRF protection\n- CIMDClientManager: Manages CIMD client operations\n\n\n## Classes\n\n### `CIMDDocument` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L45\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCIMD document per draft-parecki-oauth-client-id-metadata-document.\n\nThe client metadata document is a JSON document containing OAuth client\nmetadata. The client_id property MUST match the URL where this document\nis hosted.\n\nKey constraint: token_endpoint_auth_method MUST NOT use shared secrets\n(client_secret_post, client_secret_basic, client_secret_jwt).\n\nredirect_uris is required and must contain at least one entry.\n\n\n**Methods:**\n\n#### `validate_auth_method` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L125\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_auth_method(cls, v: str) -> str\n```\n\nEnsure no shared-secret auth methods are used.\n\n\n#### `validate_redirect_uris` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L137\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_redirect_uris(cls, v: list[str]) -> list[str]\n```\n\nEnsure redirect_uris is non-empty and each entry is a valid URI.\n\n\n### `CIMDValidationError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L154\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRaised when CIMD document validation fails.\n\n\n### `CIMDFetchError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L158\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRaised when CIMD document fetching fails.\n\n\n### `CIMDFetcher` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L186\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nFetch and validate CIMD documents with SSRF protection.\n\nDelegates HTTP fetching to ssrf_safe_fetch_response, which provides DNS\npinning, IP validation, size limits, and timeout enforcement. Documents are\ncached using HTTP caching semantics (Cache-Control/ETag/Last-Modified), with\na TTL fallback when response headers do not define caching behavior.\n\n\n**Methods:**\n\n#### `is_cimd_client_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L270\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_cimd_client_id(self, client_id: str) -> bool\n```\n\nCheck if a client_id looks like a CIMD URL.\n\nCIMD URLs must be HTTPS with a host and non-root path.\n\n\n#### `fetch` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L287\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfetch(self, client_id_url: str) -> CIMDDocument\n```\n\nFetch and validate a CIMD document with SSRF protection.\n\nUses ssrf_safe_fetch_response for the HTTP layer, which provides:\n- HTTPS only, DNS resolution with IP validation\n- DNS pinning (connects to validated IP directly)\n- Blocks private/loopback/link-local/multicast IPs\n- Response size limit and timeout enforcement\n- Redirects disabled\n\n**Args:**\n- `client_id_url`: The URL to fetch (also the expected client_id)\n\n**Returns:**\n- Validated CIMDDocument\n\n**Raises:**\n- `CIMDValidationError`: If document is invalid or URL blocked\n- `CIMDFetchError`: If document cannot be fetched\n\n\n#### `validate_redirect_uri` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L422\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_redirect_uri(self, doc: CIMDDocument, redirect_uri: str) -> bool\n```\n\nValidate that a redirect_uri is allowed by the CIMD document.\n\n**Args:**\n- `doc`: The CIMD document\n- `redirect_uri`: The redirect URI to validate\n\n**Returns:**\n- True if valid, False otherwise\n\n\n### `CIMDAssertionValidator` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L452\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nValidates JWT assertions for private_key_jwt CIMD clients.\n\nImplements RFC 7523 (JSON Web Token (JWT) Profile for OAuth 2.0 Client\nAuthentication and Authorization Grants) for CIMD client authentication.\n\nJTI replay protection uses TTL-based caching to ensure proper security:\n- JTIs are cached with expiration matching the JWT's exp claim\n- Expired JTIs are automatically cleaned up\n- Maximum assertion lifetime is enforced (5 minutes)\n\n\n**Methods:**\n\n#### `validate_assertion` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L495\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_assertion(self, assertion: str, client_id: str, token_endpoint: str, cimd_doc: CIMDDocument) -> bool\n```\n\nValidate JWT assertion from client.\n\n**Args:**\n- `assertion`: The JWT assertion string\n- `client_id`: Expected client_id (must match iss and sub claims)\n- `token_endpoint`: Token endpoint URL (must match aud claim)\n- `cimd_doc`: CIMD document containing JWKS for key verification\n\n**Returns:**\n- True if valid\n\n**Raises:**\n- `ValueError`: If validation fails\n\n\n### `CIMDClientManager` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L677\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nManages all CIMD client operations for OAuth proxy.\n\nThis class encapsulates:\n- CIMD client detection\n- Document fetching and validation\n- Synthetic OAuth client creation\n- Private key JWT assertion validation\n\nThis allows the OAuth proxy to delegate all CIMD-specific logic to a\nsingle, focused manager class.\n\n\n**Methods:**\n\n#### `is_cimd_client_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L711\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_cimd_client_id(self, client_id: str) -> bool\n```\n\nCheck if client_id is a CIMD URL.\n\n**Args:**\n- `client_id`: Client ID to check\n\n**Returns:**\n- True if client_id is an HTTPS URL (CIMD format)\n\n\n#### `get_client` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L722\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_client(self, client_id_url: str)\n```\n\nFetch CIMD document and create synthetic OAuth client.\n\n**Args:**\n- `client_id_url`: HTTPS URL pointing to CIMD document\n\n**Returns:**\n- OAuthProxyClient with CIMD document attached, or None if fetch fails\n\n\n#### `validate_private_key_jwt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/cimd.py#L771\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_private_key_jwt(self, assertion: str, client, token_endpoint: str) -> bool\n```\n\nValidate JWT assertion for private_key_jwt auth.\n\n**Args:**\n- `assertion`: JWT assertion string from client\n- `client`: OAuth proxy client (must have cimd_document)\n- `token_endpoint`: Token endpoint URL for aud validation\n\n**Returns:**\n- True if assertion is valid\n\n**Raises:**\n- `ValueError`: If client doesn't have CIMD document or validation fails\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx",
    "content": "---\ntitle: jwt_issuer\nsidebarTitle: jwt_issuer\n---\n\n# `fastmcp.server.auth.jwt_issuer`\n\n\nJWT token issuance and verification for FastMCP OAuth Proxy.\n\nThis module implements the token factory pattern for OAuth proxies, where the proxy\nissues its own JWT tokens to clients instead of forwarding upstream provider tokens.\nThis maintains proper OAuth 2.0 token audience boundaries.\n\n\n## Functions\n\n### `derive_jwt_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nderive_jwt_key() -> bytes\n```\n\n\nDerive JWT signing key from a high-entropy or low-entropy key material and server salt.\n\n\n## Classes\n\n### `JWTIssuer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L79\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nIssues and validates FastMCP-signed JWT tokens using HS256.\n\nThis issuer creates JWT tokens for MCP clients with proper audience claims,\nmaintaining OAuth 2.0 token boundaries. Tokens are signed with HS256 using\na key derived from the upstream client secret.\n\n\n**Methods:**\n\n#### `issue_access_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L105\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nissue_access_token(self, client_id: str, scopes: list[str], jti: str, expires_in: int = 3600, upstream_claims: dict[str, Any] | None = None) -> str\n```\n\nIssue a minimal FastMCP access token.\n\nFastMCP tokens are reference tokens containing only the minimal claims\nneeded for validation and lookup. The JTI maps to the upstream token\nwhich contains actual user identity and authorization data.\n\n**Args:**\n- `client_id`: MCP client ID\n- `scopes`: Token scopes\n- `jti`: Unique token identifier (maps to upstream token)\n- `expires_in`: Token lifetime in seconds\n- `upstream_claims`: Optional claims from upstream IdP token to include\n\n**Returns:**\n- Signed JWT token\n\n\n#### `issue_refresh_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L157\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nissue_refresh_token(self, client_id: str, scopes: list[str], jti: str, expires_in: int, upstream_claims: dict[str, Any] | None = None) -> str\n```\n\nIssue a minimal FastMCP refresh token.\n\nFastMCP refresh tokens are reference tokens containing only the minimal\nclaims needed for validation and lookup. The JTI maps to the upstream\ntoken which contains actual user identity and authorization data.\n\n**Args:**\n- `client_id`: MCP client ID\n- `scopes`: Token scopes\n- `jti`: Unique token identifier (maps to upstream token)\n- `expires_in`: Token lifetime in seconds (should match upstream refresh expiry)\n- `upstream_claims`: Optional claims from upstream IdP token to include\n\n**Returns:**\n- Signed JWT token\n\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/jwt_issuer.py#L210\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str, expected_token_use: str = 'access') -> dict[str, Any]\n```\n\nVerify and decode a FastMCP token.\n\nValidates JWT signature, expiration, issuer, audience, and token type.\n\n**Args:**\n- `token`: JWT token to verify\n- `expected_token_use`: Expected token type (\"access\" or \"refresh\").\nDefaults to \"access\", which rejects refresh tokens.\n\n**Returns:**\n- Decoded token payload\n\n**Raises:**\n- `JoseError`: If token is invalid, expired, or has wrong claims\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-middleware.mdx",
    "content": "---\ntitle: middleware\nsidebarTitle: middleware\n---\n\n# `fastmcp.server.auth.middleware`\n\n\nEnhanced authentication middleware with better error messages.\n\nThis module provides enhanced versions of MCP SDK authentication middleware\nthat return more helpful error messages for developers troubleshooting\nauthentication issues.\n\n\n## Classes\n\n### `RequireAuthMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/middleware.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nEnhanced authentication middleware with detailed error messages.\n\nExtends the SDK's RequireAuthMiddleware to provide more actionable\nerror messages when authentication fails. This helps developers\nunderstand what went wrong and how to fix it.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-oauth_proxy-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.auth.oauth_proxy`\n\n\nOAuth Proxy Provider for FastMCP.\n\nThis package provides OAuth proxy functionality split across multiple modules:\n- models: Pydantic models and constants\n- ui: HTML generation functions\n- consent: Consent management mixin\n- proxy: Main OAuthProxy class\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx",
    "content": "---\ntitle: consent\nsidebarTitle: consent\n---\n\n# `fastmcp.server.auth.oauth_proxy.consent`\n\n\nOAuth Proxy Consent Management.\n\nThis module contains consent management functionality for the OAuth proxy.\nThe ConsentMixin class provides methods for handling user consent flows,\ncookie management, and consent page rendering.\n\n\n## Classes\n\n### `ConsentMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/consent.py#L35\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin class providing consent management functionality for OAuthProxy.\n\nThis mixin contains all methods related to:\n- Cookie signing and verification\n- Consent page rendering\n- Consent approval/denial handling\n- URI normalization for consent tracking\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx",
    "content": "---\ntitle: models\nsidebarTitle: models\n---\n\n# `fastmcp.server.auth.oauth_proxy.models`\n\n\nOAuth Proxy Models and Constants.\n\nThis module contains all Pydantic models and constants used by the OAuth proxy.\n\n\n## Classes\n\n### `OAuthTransaction` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L40\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth transaction state for consent flow.\n\nStored server-side to track active authorization flows with client context.\nIncludes CSRF tokens for consent protection per MCP security best practices.\n\n\n### `ClientCode` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L62\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nClient authorization code with PKCE and upstream tokens.\n\nStored server-side after upstream IdP callback. Contains the upstream\ntokens bound to the client's PKCE challenge for secure token exchange.\n\n\n### `UpstreamTokenSet` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L80\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nStored upstream OAuth tokens from identity provider.\n\nThese tokens are obtained from the upstream provider (Google, GitHub, etc.)\nand stored in plaintext within this model. Encryption is handled transparently\nat the storage layer via FernetEncryptionWrapper. Tokens are never exposed to MCP clients.\n\n\n### `JTIMapping` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L102\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMaps FastMCP token JTI to upstream token ID.\n\nThis allows stateless JWT validation while still being able to look up\nthe corresponding upstream token when tools need to access upstream APIs.\n\n\n### `RefreshTokenMetadata` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L114\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMetadata for a refresh token, stored keyed by token hash.\n\nWe store only metadata (not the token itself) for security - if storage\nis compromised, attackers get hashes they can't reverse into usable tokens.\n\n\n### `ProxyDCRClient` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L136\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nClient for DCR proxy with configurable redirect URI validation.\n\nThis special client class is critical for the OAuth proxy to work correctly\nwith Dynamic Client Registration (DCR). Here's why it exists:\n\nProblem:\n--------\nWhen MCP clients use OAuth, they dynamically register with random localhost\nports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:\n1. Accept these dynamic redirect URIs from clients based on configured patterns\n2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)\n3. Forward the authorization code back to the client's dynamic URI\n\nSolution:\n---------\nThis class validates redirect URIs against configurable patterns,\nwhile the proxy internally uses its own fixed redirect URI with the upstream\nprovider. This allows the flow to work even when clients reconnect with\ndifferent ports or when tokens are cached.\n\nWithout proper validation, clients could get \"Redirect URI not registered\" errors\nwhen trying to authenticate with cached tokens, or security vulnerabilities could\narise from accepting arbitrary redirect URIs.\n\n\n**Methods:**\n\n#### `validate_redirect_uri` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/models.py#L167\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl\n```\n\nValidate redirect URI against proxy patterns and optionally CIMD redirect_uris.\n\nFor CIMD clients: validates against BOTH the CIMD document's redirect_uris\nAND the proxy's allowed patterns (if configured). Both must pass.\n\nFor DCR clients: validates against proxy patterns first, falling back to\nbase validation (registered redirect_uris) if patterns don't match.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx",
    "content": "---\ntitle: proxy\nsidebarTitle: proxy\n---\n\n# `fastmcp.server.auth.oauth_proxy.proxy`\n\n\nOAuth Proxy Provider for FastMCP.\n\nThis provider acts as a transparent proxy to an upstream OAuth Authorization Server,\nhandling Dynamic Client Registration locally while forwarding all other OAuth flows.\nThis enables authentication with upstream providers that don't support DCR or have\nrestricted client registration policies.\n\nKey features:\n- Proxies authorization and token endpoints to upstream server\n- Implements local Dynamic Client Registration with fixed upstream credentials\n- Validates tokens using upstream JWKS\n- Maintains minimal local state for bookkeeping\n- Enhanced logging with request correlation\n\nThis implementation is based on the OAuth 2.1 specification and is designed for\nproduction use with enterprise identity providers.\n\n\n## Classes\n\n### `OAuthProxy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L119\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.\n\nPurpose\n-------\nMCP clients expect OAuth providers to support Dynamic Client Registration (DCR),\nwhere clients can register themselves dynamically and receive unique credentials.\nMost enterprise IDPs (Google, GitHub, Azure AD, etc.) don't support DCR and require\npre-registered OAuth applications with fixed credentials.\n\nThis proxy bridges that gap by:\n- Presenting a full DCR-compliant OAuth interface to MCP clients\n- Translating DCR registration requests to use pre-configured upstream credentials\n- Proxying all OAuth flows to the upstream IDP with appropriate translations\n- Managing the state and security requirements of both protocols\n\nArchitecture Overview\n--------------------\nThe proxy maintains a single OAuth app registration with the upstream provider\nwhile allowing unlimited MCP clients to register and authenticate dynamically.\nIt implements the complete OAuth 2.1 + DCR specification for clients while\ntranslating to whatever OAuth variant the upstream provider requires.\n\nKey Translation Challenges Solved\n---------------------------------\n1. Dynamic Client Registration:\n   - MCP clients expect to register dynamically and get unique credentials\n   - Upstream IDPs require pre-registered apps with fixed credentials\n   - Solution: Accept DCR requests, return shared upstream credentials\n\n2. Dynamic Redirect URIs:\n   - MCP clients use random localhost ports that change between sessions\n   - Upstream IDPs require fixed, pre-registered redirect URIs\n   - Solution: Use proxy's fixed callback URL with upstream, forward to client's dynamic URI\n\n3. Authorization Code Mapping:\n   - Upstream returns codes for the proxy's redirect URI\n   - Clients expect codes for their own redirect URIs\n   - Solution: Exchange upstream code server-side, issue new code to client\n\n4. State Parameter Collision:\n   - Both client and proxy need to maintain state through the flow\n   - Only one state parameter available in OAuth\n   - Solution: Use transaction ID as state with upstream, preserve client's state\n\n5. Token Management:\n   - Clients may expect different token formats/claims than upstream provides\n   - Need to track tokens for revocation and refresh\n   - Solution: Store token relationships, forward upstream tokens transparently\n\nOAuth Flow Implementation\n------------------------\n1. Client Registration (DCR):\n   - Accept any client registration request\n   - Store ProxyDCRClient that accepts dynamic redirect URIs\n\n2. Authorization:\n   - Store transaction mapping client details to proxy flow\n   - Redirect to upstream with proxy's fixed redirect URI\n   - Use transaction ID as state parameter with upstream\n\n3. Upstream Callback:\n   - Exchange upstream authorization code for tokens (server-side)\n   - Generate new authorization code bound to client's PKCE challenge\n   - Redirect to client's original dynamic redirect URI\n\n4. Token Exchange:\n   - Validate client's code and PKCE verifier\n   - Return previously obtained upstream tokens\n   - Clean up one-time use authorization code\n\n5. Token Refresh:\n   - Forward refresh requests to upstream using authlib\n   - Handle token rotation if upstream issues new refresh token\n   - Update local token mappings\n\nState Management\n---------------\nThe proxy maintains minimal but crucial state via pluggable storage (client_storage):\n- _oauth_transactions: Active authorization flows with client context\n- _client_codes: Authorization codes with PKCE challenges and upstream tokens\n- _jti_mapping_store: Maps FastMCP token JTIs to upstream token IDs\n- _refresh_token_store: Refresh token metadata (keyed by token hash)\n\nAll state is stored in the configured client_storage backend (Redis, disk, etc.)\nenabling horizontal scaling across multiple instances.\n\nSecurity Considerations\n----------------------\n- Refresh tokens stored by hash only (defense in depth if storage compromised)\n- PKCE enforced end-to-end (client to proxy, proxy to upstream)\n- Authorization codes are single-use with short expiry\n- Transaction IDs are cryptographically random\n- All state is cleaned up after use to prevent replay\n- Token validation delegates to upstream provider\n\nProvider Compatibility\n---------------------\nWorks with any OAuth 2.0 provider that supports:\n- Authorization code flow\n- Fixed redirect URI (configured in provider's app settings)\n- Standard token endpoint\n\nHandles provider-specific requirements:\n- Google: Ensures minimum scope requirements\n- GitHub: Compatible with OAuth Apps and GitHub Apps\n- Azure AD: Handles tenant-specific endpoints\n- Generic: Works with any spec-compliant provider\n\n\n**Methods:**\n\n#### `set_mcp_path` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L558\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_mcp_path(self, mcp_path: str | None) -> None\n```\n\nSet the MCP endpoint path and create JWTIssuer with correct audience.\n\nThis method is called by get_routes() to configure the resource URL\nand create the JWTIssuer. The JWT audience is set to the full resource\nURL (e.g., http://localhost:8000/mcp) to ensure tokens are bound to\nthis specific MCP endpoint.\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n\n\n#### `jwt_issuer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L582\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\njwt_issuer(self) -> JWTIssuer\n```\n\nGet the JWT issuer, ensuring it has been initialized.\n\nThe JWT issuer is created when set_mcp_path() is called (via get_routes()).\nThis property ensures a clear error if used before initialization.\n\n\n#### `get_client` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L642\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_client(self, client_id: str) -> OAuthClientInformationFull | None\n```\n\nGet client information by ID. This is generally the random ID\nprovided to the DCR client during registration, not the upstream client ID.\n\nFor unregistered clients, returns None (which will raise an error in the SDK).\nCIMD clients (URL-based client IDs) are looked up and cached automatically.\n\n\n#### `register_client` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L686\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_client(self, client_info: OAuthClientInformationFull) -> None\n```\n\nRegister a client locally\n\nWhen a client registers, we create a ProxyDCRClient that is more\nforgiving about validating redirect URIs, since the DCR client's\nredirect URI will likely be localhost or unknown to the proxied IDP. The\nproxied IDP only knows about this server's fixed redirect URI.\n\n\n#### `authorize` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L739\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nauthorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str\n```\n\nStart OAuth transaction and route through consent interstitial.\n\nFlow:\n1. Validate client's resource matches server's resource URL (security check)\n2. Store transaction with client details and PKCE (if forwarding)\n3. Return local /consent URL; browser visits consent first\n4. Consent handler redirects to upstream IdP if approved/already approved\n\nIf consent is disabled (require_authorization_consent=False), skip the consent screen\nand redirect directly to the upstream IdP.\n\n\n#### `load_authorization_code` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L858\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None\n```\n\nLoad authorization code for validation.\n\nLook up our client code and return authorization code object\nwith PKCE challenge for validation.\n\n\n#### `exchange_authorization_code` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L906\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nexchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken\n```\n\nExchange authorization code for FastMCP-issued tokens.\n\nImplements the token factory pattern:\n1. Retrieves upstream tokens from stored authorization code\n2. Extracts user identity from upstream token\n3. Encrypts and stores upstream tokens\n4. Issues FastMCP-signed JWT tokens\n5. Returns FastMCP tokens (NOT upstream tokens)\n\nPKCE validation is handled by the MCP framework before this method is called.\n\n\n#### `load_refresh_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1164\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None\n```\n\nLoad refresh token metadata from distributed storage.\n\nLooks up by token hash and reconstructs the RefreshToken object.\nValidates that the token belongs to the requesting client.\n\n\n#### `exchange_refresh_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1193\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nexchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken\n```\n\nExchange FastMCP refresh token for new FastMCP access token.\n\nImplements two-tier refresh:\n1. Verify FastMCP refresh token\n2. Look up upstream token via JTI mapping\n3. Refresh upstream token with upstream provider\n4. Update stored upstream token\n5. Issue new FastMCP access token\n6. Keep same FastMCP refresh token (unless upstream rotates)\n\n\n#### `load_access_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1460\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_access_token(self, token: str) -> AccessToken | None\n```\n\nValidate FastMCP JWT by swapping for upstream token.\n\nThis implements the token swap pattern:\n1. Verify FastMCP JWT signature (proves it's our token)\n2. Look up upstream token via JTI mapping\n3. Decrypt upstream token\n4. Validate upstream token with provider (GitHub API, JWT validation, etc.)\n5. Return upstream validation result\n\nThe FastMCP JWT is a reference token - all authorization data comes\nfrom validating the upstream token via the TokenVerifier.\n\n\n#### `revoke_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1539\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrevoke_token(self, token: AccessToken | RefreshToken) -> None\n```\n\nRevoke token locally and with upstream server if supported.\n\nFor refresh tokens, removes from local storage by hash.\nFor all tokens, attempts upstream revocation if endpoint is configured.\nAccess token JTI mappings expire via TTL.\n\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1585\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet OAuth routes with custom handlers for better error UX.\n\nThis method creates standard OAuth routes and replaces:\n- /authorize endpoint: Enhanced error responses for unregistered clients\n- /token endpoint: OAuth 2.1 compliant error codes\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-oauth_proxy-ui.mdx",
    "content": "---\ntitle: ui\nsidebarTitle: ui\n---\n\n# `fastmcp.server.auth.oauth_proxy.ui`\n\n\nOAuth Proxy UI Generation Functions.\n\nThis module contains HTML generation functions for consent and error pages.\n\n\n## Functions\n\n### `create_consent_html` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/ui.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None, csp_policy: str | None = None, is_cimd_client: bool = False, cimd_domain: str | None = None) -> str\n```\n\n\nCreate a styled HTML consent page for OAuth authorization requests.\n\n**Args:**\n- `csp_policy`: Content Security Policy override.\nIf None, uses the built-in CSP policy with appropriate directives.\nIf empty string \"\", disables CSP entirely (no meta tag is rendered).\nIf a non-empty string, uses that as the CSP policy value.\n\n\n### `create_error_html` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy/ui.py#L215\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_error_html(error_title: str, error_message: str, error_details: dict[str, str] | None = None, server_name: str | None = None, server_icon_url: str | None = None) -> str\n```\n\n\nCreate a styled HTML error page for OAuth errors.\n\n**Args:**\n- `error_title`: The error title (e.g., \"OAuth Error\", \"Authorization Failed\")\n- `error_message`: The main error message to display\n- `error_details`: Optional dictionary of error details to show (e.g., `{\"Error Code\"\\: \"invalid_client\"}`)\n- `server_name`: Optional server name to display\n- `server_icon_url`: Optional URL to server icon/logo\n\n**Returns:**\n- Complete HTML page as a string\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx",
    "content": "---\ntitle: oidc_proxy\nsidebarTitle: oidc_proxy\n---\n\n# `fastmcp.server.auth.oidc_proxy`\n\n\nOIDC Proxy Provider for FastMCP.\n\nThis provider acts as a transparent proxy to an upstream OIDC compliant Authorization\nServer. It leverages the OAuthProxy class to handle Dynamic Client Registration and\nforwarding of all OAuth flows.\n\nThis implementation is based on:\n    OpenID Connect Discovery 1.0 - https://openid.net/specs/openid-connect-discovery-1_0.html\n    OAuth 2.0 Authorization Server Metadata - https://datatracker.ietf.org/doc/html/rfc8414\n\n\n## Classes\n\n### `OIDCConfiguration` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOIDC Configuration.\n\n\n**Methods:**\n\n#### `get_oidc_configuration` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L144\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_oidc_configuration(cls, config_url: AnyHttpUrl) -> Self\n```\n\nGet the OIDC configuration for the specified config URL.\n\n**Args:**\n- `config_url`: The OIDC config URL\n- `strict`: The strict flag for the configuration\n- `timeout_seconds`: HTTP request timeout in seconds\n\n\n### `OIDCProxy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L174\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth provider that wraps OAuthProxy to provide configuration via an OIDC configuration URL.\n\nThis provider makes it easier to add OAuth protection for any upstream provider\nthat is OIDC compliant.\n\n\n**Methods:**\n\n#### `get_oidc_configuration` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L450\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_oidc_configuration(self, config_url: AnyHttpUrl, strict: bool | None, timeout_seconds: int | None) -> OIDCConfiguration\n```\n\nGets the OIDC configuration for the specified configuration URL.\n\n**Args:**\n- `config_url`: The OIDC configuration URL\n- `strict`: The strict flag for the configuration\n- `timeout_seconds`: HTTP request timeout in seconds\n\n\n#### `get_token_verifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/oidc_proxy.py#L467\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_token_verifier(self) -> TokenVerifier\n```\n\nCreates the token verifier for the specified OIDC configuration and arguments.\n\n**Args:**\n- `algorithm`: Optional token verifier algorithm\n- `audience`: Optional token verifier audience\n- `required_scopes`: Optional token verifier required_scopes\n- `timeout_seconds`: HTTP request timeout in seconds\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.auth.providers`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx",
    "content": "---\ntitle: auth0\nsidebarTitle: auth0\n---\n\n# `fastmcp.server.auth.providers.auth0`\n\n\nAuth0 OAuth provider for FastMCP.\n\nThis module provides a complete Auth0 integration that's ready to use with\njust the configuration URL, client ID, client secret, audience, and base URL.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.auth0 import Auth0Provider\n\n    # Simple Auth0 OAuth protection\n    auth = Auth0Provider(\n        config_url=\"https://auth0.config.url\",\n        client_id=\"your-auth0-client-id\",\n        client_secret=\"your-auth0-client-secret\",\n        audience=\"your-auth0-api-audience\",\n        base_url=\"http://localhost:8000\",\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `Auth0Provider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/auth0.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAn Auth0 provider implementation for FastMCP.\n\nThis provider is a complete Auth0 integration that's ready to use with\njust the configuration URL, client ID, client secret, audience, and base URL.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-aws.mdx",
    "content": "---\ntitle: aws\nsidebarTitle: aws\n---\n\n# `fastmcp.server.auth.providers.aws`\n\n\nAWS Cognito OAuth provider for FastMCP.\n\nThis module provides a complete AWS Cognito OAuth integration that's ready to use\nwith a user pool ID, domain prefix, client ID and client secret. It handles all\nthe complexity of AWS Cognito's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider\n\n    # Simple AWS Cognito OAuth protection\n    auth = AWSCognitoProvider(\n        user_pool_id=\"your-user-pool-id\",\n        aws_region=\"eu-central-1\",\n        client_id=\"your-cognito-client-id\",\n        client_secret=\"your-cognito-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `AWSCognitoTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L40\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken verifier that filters claims to Cognito-specific subset.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify token and filter claims to Cognito-specific subset.\n\n\n### `AWSCognitoProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L67\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComplete AWS Cognito OAuth provider for FastMCP.\n\nThis provider makes it trivial to add AWS Cognito OAuth protection to any\nFastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,\nclient credentials, and a base URL, and you're ready to go.\n\nFeatures:\n- Automatic OIDC Discovery from AWS Cognito User Pool\n- Automatic JWT token validation via Cognito's public keys\n- Cognito-specific claim filtering (sub, username, cognito:groups)\n- Support for Cognito User Pools\n\n\n**Methods:**\n\n#### `get_token_verifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/aws.py#L178\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_token_verifier(self) -> AWSCognitoTokenVerifier\n```\n\nCreates a Cognito-specific token verifier with claim filtering.\n\n**Args:**\n- `algorithm`: Optional token verifier algorithm\n- `audience`: Optional token verifier audience\n- `required_scopes`: Optional token verifier required_scopes\n- `timeout_seconds`: HTTP request timeout in seconds\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-azure.mdx",
    "content": "---\ntitle: azure\nsidebarTitle: azure\n---\n\n# `fastmcp.server.auth.providers.azure`\n\n\nAzure (Microsoft Entra) OAuth provider for FastMCP.\n\nThis provider implements Azure/Microsoft Entra ID OAuth authentication\nusing the OAuth Proxy pattern for non-DCR OAuth flows.\n\n\n## Functions\n\n### `EntraOBOToken` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L700\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nEntraOBOToken(scopes: list[str]) -> str\n```\n\n\nExchange the user's Entra token for a downstream API token via OBO.\n\nThis dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,\nallowing your MCP server to call downstream APIs (like Microsoft Graph) on\nbehalf of the authenticated user.\n\n**Args:**\n- `scopes`: The scopes to request for the downstream API. For Microsoft Graph,\nuse scopes like [\"https\\://graph.microsoft.com/Mail.Read\"] or\n[\"https\\://graph.microsoft.com/.default\"].\n\n**Returns:**\n- A dependency that resolves to the downstream API access token string\n\n**Raises:**\n- `ImportError`: If fastmcp[azure] is not installed\n- `RuntimeError`: If no access token is available, provider is not Azure,\nor OBO exchange fails\n\n\n## Classes\n\n### `AzureProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L35\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAzure (Microsoft Entra) OAuth provider for FastMCP.\n\nThis provider implements Azure/Microsoft Entra ID authentication using the\nOAuth Proxy pattern. It supports both organizational accounts and personal\nMicrosoft accounts depending on the tenant configuration.\n\nScope Handling:\n- required_scopes: Provide unprefixed scope names (e.g., [\"read\", \"write\"])\n  → Automatically prefixed with identifier_uri during initialization\n  → Validated on all tokens and advertised to MCP clients\n- additional_authorize_scopes: Provide full format (e.g., [\"User.Read\"])\n  → NOT prefixed, NOT validated, NOT advertised to clients\n  → Used to request Microsoft Graph or other upstream API permissions\n\nFeatures:\n- OAuth proxy to Azure/Microsoft identity platform\n- JWT validation using tenant issuer and JWKS\n- Supports tenant configurations: specific tenant ID, \"organizations\", or \"consumers\"\n- Custom API scopes and Microsoft Graph scopes in a single provider\n\nSetup:\n1. Create an App registration in Azure Portal\n2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)\n3. Add an Application ID URI under \"Expose an API\" (defaults to api://{client_id})\n4. Add custom scopes (e.g., \"read\", \"write\") under \"Expose an API\"\n5. Set access token version to 2 in the App manifest: \"requestedAccessTokenVersion\": 2\n6. Create a client secret\n7. Get Application (client) ID, Directory (tenant) ID, and client secret\n\n\n**Methods:**\n\n#### `authorize` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L259\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nauthorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str\n```\n\nStart OAuth transaction and redirect to Azure AD.\n\nOverride parent's authorize method to filter out the 'resource' parameter\nwhich is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use\nscopes to determine the resource/audience instead of a separate parameter.\n\n**Args:**\n- `client`: OAuth client information\n- `params`: Authorization parameters from the client\n\n**Returns:**\n- Authorization URL to redirect the user to Azure AD\n\n\n#### `get_obo_credential` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L485\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential\n```\n\nGet a cached or new OnBehalfOfCredential for OBO token exchange.\n\nCredentials are cached by user assertion so the Azure SDK's internal\ntoken cache can avoid redundant OBO exchanges when the same user\ncalls multiple tools with the same scopes.\n\n**Args:**\n- `user_assertion`: The user's access token to exchange via OBO.\n\n**Returns:**\n- A configured OnBehalfOfCredential ready for get_token() calls.\n\n**Raises:**\n- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).\n\n\n#### `close_obo_credentials` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L536\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose_obo_credentials(self) -> None\n```\n\nClose all cached OBO credentials.\n\n\n### `AzureJWTVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L547\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nJWT verifier pre-configured for Azure AD / Microsoft Entra ID.\n\nAuto-configures JWKS URI, issuer, audience, and scope handling from your\nAzure app registration details. Designed for Managed Identity and other\ntoken-verification-only scenarios where AzureProvider's full OAuth proxy\nisn't needed.\n\nHandles Azure's scope format automatically:\n- Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)\n- Advertises full-URI scopes in OAuth metadata (what clients need to request)\n\nExample::\n\n    from fastmcp.server.auth import RemoteAuthProvider\n    from fastmcp.server.auth.providers.azure import AzureJWTVerifier\n    from pydantic import AnyHttpUrl\n\n    verifier = AzureJWTVerifier(\n        client_id=\"your-client-id\",\n        tenant_id=\"your-tenant-id\",\n        required_scopes=[\"access_as_user\"],\n    )\n\n    auth = RemoteAuthProvider(\n        token_verifier=verifier,\n        authorization_servers=[\n            AnyHttpUrl(\"https://login.microsoftonline.com/your-tenant-id/v2.0\")\n        ],\n        base_url=\"https://my-server.com\",\n    )\n\n\n**Methods:**\n\n#### `scopes_supported` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L627\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nscopes_supported(self) -> list[str]\n```\n\nReturn scopes with Azure URI prefix for OAuth metadata.\n\nAzure tokens contain short-form scopes (e.g., ``read``) in the ``scp``\nclaim, but clients must request full URI scopes (e.g.,\n``api://client-id/read``) from the Azure authorization endpoint. This\nproperty returns the full-URI form for OAuth metadata while\n``required_scopes`` retains the short form for token validation.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-debug.mdx",
    "content": "---\ntitle: debug\nsidebarTitle: debug\n---\n\n# `fastmcp.server.auth.providers.debug`\n\n\nDebug token verifier for testing and special cases.\n\nThis module provides a flexible token verifier that delegates validation\nto a custom callable. Useful for testing, development, or scenarios where\nstandard verification isn't possible (like opaque tokens without introspection).\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n    # Accept all tokens (default - useful for testing)\n    auth = DebugTokenVerifier()\n\n    # Custom sync validation logic\n    auth = DebugTokenVerifier(validate=lambda token: token.startswith(\"valid-\"))\n\n    # Custom async validation logic\n    async def check_cache(token: str) -> bool:\n        return await redis.exists(f\"token:{token}\")\n\n    auth = DebugTokenVerifier(validate=check_cache)\n\n    mcp = FastMCP(\"My Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `DebugTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/debug.py#L40\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken verifier with custom validation logic.\n\nThis verifier delegates token validation to a user-provided callable.\nBy default, it accepts all non-empty tokens (useful for testing).\n\nUse cases:\n- Testing: Accept any token without real verification\n- Development: Custom validation logic for prototyping\n- Opaque tokens: When you have tokens with no introspection endpoint\n\nWARNING: This bypasses standard security checks. Only use in controlled\nenvironments or when you understand the security implications.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/debug.py#L77\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify token using custom validation logic.\n\n**Args:**\n- `token`: The token string to validate\n\n**Returns:**\n- AccessToken if validation succeeds, None otherwise\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-descope.mdx",
    "content": "---\ntitle: descope\nsidebarTitle: descope\n---\n\n# `fastmcp.server.auth.providers.descope`\n\n\nDescope authentication provider for FastMCP.\n\nThis module provides DescopeProvider - a complete authentication solution that integrates\nwith Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)\nfor seamless MCP client authentication.\n\n\n## Classes\n\n### `DescopeProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/descope.py#L25\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDescope metadata provider for DCR (Dynamic Client Registration).\n\nThis provider implements Descope integration using metadata forwarding.\nThis is the recommended approach for Descope DCR\nas it allows Descope to handle the OAuth flow directly while FastMCP acts\nas a resource server.\n\nIMPORTANT SETUP REQUIREMENTS:\n\n1. Create an MCP Server in Descope Console:\n   - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console\n   - Create a new MCP Server\n   - Ensure that **Dynamic Client Registration (DCR)** is enabled\n   - Note your Well-Known URL\n\n2. Note your Well-Known URL:\n   - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)\n   - Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``\n\nFor detailed setup instructions, see:\nhttps://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr\n\n\n**Methods:**\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/descope.py#L165\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet OAuth routes including Descope authorization server metadata forwarding.\n\nThis returns the standard protected resource routes plus an authorization server\nmetadata endpoint that forwards Descope's OAuth metadata to clients.\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-discord.mdx",
    "content": "---\ntitle: discord\nsidebarTitle: discord\n---\n\n# `fastmcp.server.auth.providers.discord`\n\n\nDiscord OAuth provider for FastMCP.\n\nThis module provides a complete Discord OAuth integration that's ready to use\nwith just a client ID and client secret. It handles all the complexity of\nDiscord's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.discord import DiscordProvider\n\n    # Simple Discord OAuth protection\n    auth = DiscordProvider(\n        client_id=\"your-discord-client-id\",\n        client_secret=\"your-discord-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `DiscordTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/discord.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken verifier for Discord OAuth tokens.\n\nDiscord OAuth tokens are opaque (not JWTs), so we verify them\nby calling Discord's tokeninfo API to check if they're valid and get user info.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/discord.py#L72\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify Discord OAuth token by calling Discord's tokeninfo API.\n\n\n### `DiscordProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/discord.py#L165\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComplete Discord OAuth provider for FastMCP.\n\nThis provider makes it trivial to add Discord OAuth protection to any\nFastMCP server. Just provide your Discord OAuth app credentials and\na base URL, and you're ready to go.\n\nFeatures:\n- Transparent OAuth proxy to Discord\n- Automatic token validation via Discord's API\n- User information extraction from Discord APIs\n- Minimal configuration required\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-github.mdx",
    "content": "---\ntitle: github\nsidebarTitle: github\n---\n\n# `fastmcp.server.auth.providers.github`\n\n\nGitHub OAuth provider for FastMCP.\n\nThis module provides a complete GitHub OAuth integration that's ready to use\nwith just a client ID and client secret. It handles all the complexity of\nGitHub's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.github import GitHubProvider\n\n    # Simple GitHub OAuth protection\n    auth = GitHubProvider(\n        client_id=\"your-github-client-id\",\n        client_secret=\"your-github-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `GitHubTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L41\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken verifier for GitHub OAuth tokens.\n\nGitHub OAuth tokens are opaque (not JWTs), so we verify them\nby calling GitHub's API to check if they're valid and get user info.\n\nCaching is disabled by default.  Set ``cache_ttl_seconds`` to a positive\ninteger to cache successful verification results and avoid repeated\nGitHub API calls for the same token.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L82\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify GitHub OAuth token by calling GitHub API.\n\n\n### `GitHubProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L178\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComplete GitHub OAuth provider for FastMCP.\n\nThis provider makes it trivial to add GitHub OAuth protection to any\nFastMCP server. Just provide your GitHub OAuth app credentials and\na base URL, and you're ready to go.\n\nFeatures:\n- Transparent OAuth proxy to GitHub\n- Automatic token validation via GitHub API\n- User information extraction\n- Minimal configuration required\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-google.mdx",
    "content": "---\ntitle: google\nsidebarTitle: google\n---\n\n# `fastmcp.server.auth.providers.google`\n\n\nGoogle OAuth provider for FastMCP.\n\nThis module provides a complete Google OAuth integration that's ready to use\nwith just a client ID and client secret. It handles all the complexity of\nGoogle's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.google import GoogleProvider\n\n    # Simple Google OAuth protection\n    auth = GoogleProvider(\n        client_id=\"your-google-client-id.apps.googleusercontent.com\",\n        client_secret=\"your-google-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `GoogleTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L57\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken verifier for Google OAuth tokens.\n\nGoogle OAuth tokens are opaque (not JWTs), so we verify them\nby calling Google's tokeninfo API to check if they're valid and get user info.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L89\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify Google OAuth token by calling Google's tokeninfo API.\n\n\n### `GoogleProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L190\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComplete Google OAuth provider for FastMCP.\n\nThis provider makes it trivial to add Google OAuth protection to any\nFastMCP server. Just provide your Google OAuth app credentials and\na base URL, and you're ready to go.\n\nFeatures:\n- Transparent OAuth proxy to Google\n- Automatic token validation via Google's tokeninfo API\n- User information extraction from Google APIs\n- Minimal configuration required\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx",
    "content": "---\ntitle: in_memory\nsidebarTitle: in_memory\n---\n\n# `fastmcp.server.auth.providers.in_memory`\n\n## Classes\n\n### `InMemoryOAuthProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L31\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAn in-memory OAuth provider for testing purposes.\nIt simulates the OAuth 2.1 flow locally without external calls.\n\n\n**Methods:**\n\n#### `get_client` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L65\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_client(self, client_id: str) -> OAuthClientInformationFull | None\n```\n\n#### `register_client` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_client(self, client_info: OAuthClientInformationFull) -> None\n```\n\n#### `authorize` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L92\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nauthorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str\n```\n\nSimulates user authorization and generates an authorization code.\nReturns a redirect URI with the code and state.\n\n\n#### `load_authorization_code` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L149\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None\n```\n\n#### `exchange_authorization_code` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L162\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nexchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken\n```\n\n#### `load_refresh_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L215\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None\n```\n\n#### `exchange_refresh_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L230\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nexchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken\n```\n\n#### `load_access_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L287\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_access_token(self, token: str) -> AccessToken | None\n```\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L298\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a bearer token and return access info if valid.\n\nThis method implements the TokenVerifier protocol by delegating\nto our existing load_access_token method.\n\n**Args:**\n- `token`: The token string to validate\n\n**Returns:**\n- AccessToken object if valid, None if invalid or expired\n\n\n#### `revoke_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/in_memory.py#L355\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrevoke_token(self, token: AccessToken | RefreshToken) -> None\n```\n\nRevokes an access or refresh token and its counterpart.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx",
    "content": "---\ntitle: introspection\nsidebarTitle: introspection\n---\n\n# `fastmcp.server.auth.providers.introspection`\n\n\nOAuth 2.0 Token Introspection (RFC 7662) provider for FastMCP.\n\nThis module provides token verification for opaque tokens using the OAuth 2.0\nToken Introspection protocol defined in RFC 7662. It allows FastMCP servers to\nvalidate tokens issued by authorization servers that don't use JWT format.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\n\n    # Verify opaque tokens via RFC 7662 introspection\n    verifier = IntrospectionTokenVerifier(\n        introspection_url=\"https://auth.example.com/oauth/introspect\",\n        client_id=\"your-client-id\",\n        client_secret=\"your-client-secret\",\n        required_scopes=[\"read\", \"write\"]\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=verifier)\n    ```\n\n\n## Classes\n\n### `IntrospectionTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/introspection.py#L45\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth 2.0 Token Introspection verifier (RFC 7662).\n\nThis verifier validates opaque tokens by calling an OAuth 2.0 token introspection\nendpoint. Unlike JWT verification which is stateless, token introspection requires\na network call to the authorization server for each token validation.\n\nThe verifier authenticates to the introspection endpoint using either:\n- HTTP Basic Auth (client_secret_basic, default): credentials in Authorization header\n- POST body authentication (client_secret_post): credentials in request body\n\nBoth methods are specified in RFC 6749 (OAuth 2.0) and RFC 7662 (Token Introspection).\n\nUse this when:\n- Your authorization server issues opaque (non-JWT) tokens\n- You need to validate tokens from Auth0, Okta, Keycloak, or other OAuth servers\n- Your tokens require real-time revocation checking\n- Your authorization server supports RFC 7662 introspection\n\nCaching is disabled by default to preserve real-time revocation semantics.\nSet ``cache_ttl_seconds`` to enable caching and reduce load on the\nintrospection endpoint (e.g., ``cache_ttl_seconds=300`` for 5 minutes).\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/introspection.py#L179\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a bearer token using OAuth 2.0 Token Introspection (RFC 7662).\n\nThis method makes a POST request to the introspection endpoint with the token,\nauthenticated using the configured client authentication method (client_secret_basic\nor client_secret_post).\n\nResults are cached in-memory to reduce load on the introspection endpoint.\nCache TTL and size are configurable via constructor parameters.\n\n**Args:**\n- `token`: The opaque token string to validate\n\n**Returns:**\n- AccessToken object if valid and active, None if invalid, inactive, or expired\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx",
    "content": "---\ntitle: jwt\nsidebarTitle: jwt\n---\n\n# `fastmcp.server.auth.providers.jwt`\n\n\nTokenVerifier implementations for FastMCP.\n\n## Classes\n\n### `JWKData` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L27\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nJSON Web Key data structure.\n\n\n### `JWKSData` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L40\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nJSON Web Key Set data structure.\n\n\n### `RSAKeyPair` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRSA key pair for JWT testing.\n\n\n**Methods:**\n\n#### `generate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L54\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate(cls) -> RSAKeyPair\n```\n\nGenerate an RSA key pair for testing.\n\n**Returns:**\n- Generated key pair\n\n\n#### `create_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L89\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str\n```\n\nGenerate a test JWT token for testing purposes.\n\n**Args:**\n- `subject`: Subject claim (usually user ID)\n- `issuer`: Issuer claim\n- `audience`: Audience claim - can be a string or list of strings (optional)\n- `scopes`: List of scopes to include\n- `expires_in_seconds`: Token expiration time in seconds\n- `additional_claims`: Any additional claims to include\n- `kid`: Key ID to include in header\n\n\n### `JWTVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L156\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nJWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.\n\nThis verifier validates JWT tokens using various signing algorithms:\n- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):\n  Uses public/private key pairs. Ideal for external clients and services where\n  only the authorization server has the private key.\n- **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both\n  signing and verification. Perfect for internal microservices and trusted\n  environments where the secret can be securely shared.\n\nUse this when:\n- You have JWT tokens issued by an external service (asymmetric)\n- You need JWKS support for automatic key rotation (asymmetric)\n- You have internal microservices sharing a secret key (symmetric)\n- Your tokens contain standard OAuth scopes and claims\n\n\n**Methods:**\n\n#### `load_access_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L397\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_access_token(self, token: str) -> AccessToken | None\n```\n\nValidate a JWT bearer token and return an AccessToken when the token is valid.\n\n**Args:**\n- `token`: The JWT bearer token string to validate.\n\n**Returns:**\n- 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.\n\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L515\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify a bearer token and return access info if valid.\n\nThis method implements the TokenVerifier protocol by delegating\nto our existing load_access_token method.\n\n**Args:**\n- `token`: The JWT token string to validate\n\n**Returns:**\n- AccessToken object if valid, None if invalid or expired\n\n\n### `StaticTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L531\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSimple static token verifier for testing and development.\n\nThis verifier validates tokens against a predefined dictionary of valid token\nstrings and their associated claims. When a token string matches a key in the\ndictionary, the verifier returns the corresponding claims as if the token was\nvalidated by a real authorization server.\n\nUse this when:\n- You're developing or testing locally without a real OAuth server\n- You need predictable tokens for automated testing\n- You want to simulate different users/scopes without complex setup\n- You're prototyping and need simple API key-style authentication\n\nWARNING: Never use this in production - tokens are stored in plain text!\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L565\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify token against static token dictionary.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-oci.mdx",
    "content": "---\ntitle: oci\nsidebarTitle: oci\n---\n\n# `fastmcp.server.auth.providers.oci`\n\n\nOCI OIDC provider for FastMCP.\n\nThe pull request for the provider is submitted to fastmcp.\n\nThis module provides OIDC Implementation to integrate MCP servers with OCI.\nYou only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.\n\nPost Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.\nYou need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.\nThe sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.\nYou can use the signer object to create OCI service object.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.oci import OCIProvider\n    from fastmcp.server.dependencies import get_access_token\n    from fastmcp.utilities.logging import get_logger\n\n    import os\n\n    import oci\n    from oci.auth.signers import TokenExchangeSigner\n\n    logger = get_logger(__name__)\n\n    # Load configuration from environment\n    config_url = os.environ.get(\"OCI_CONFIG_URL\")  # OCI IAM Domain OIDC discovery URL\n    client_id = os.environ.get(\"OCI_CLIENT_ID\")  # Client ID configured for the OCI IAM Domain Integrated Application\n    client_secret = os.environ.get(\"OCI_CLIENT_SECRET\")  # Client secret configured for the OCI IAM Domain Integrated Application\n    iam_guid = os.environ.get(\"OCI_IAM_GUID\")  # IAM GUID configured for the OCI IAM Domain\n\n    # Simple OCI OIDC protection\n    auth = OCIProvider(\n        config_url=config_url,  # config URL is the OCI IAM Domain OIDC discovery URL\n        client_id=client_id,  # This is same as the client ID configured for the OCI IAM Domain Integrated Application\n        client_secret=client_secret,  # This is same as the client secret configured for the OCI IAM Domain Integrated Application\n        required_scopes=[\"openid\", \"profile\", \"email\"],\n        redirect_path=\"/auth/callback\",\n        base_url=\"http://localhost:8000\",\n    )\n\n    # NOTE: For production use, replace this with a thread-safe cache implementation\n    # such as threading.Lock-protected dict or a proper caching library\n    _global_token_cache = {}  # In memory cache for OCI session token signer\n\n    def get_oci_signer() -> TokenExchangeSigner:\n\n        authntoken = get_access_token()\n        tokenID = authntoken.claims.get(\"jti\")\n        token = authntoken.token\n\n        # Check if the signer exists for the token ID in memory cache\n        cached_signer = _global_token_cache.get(tokenID)\n        logger.debug(f\"Global cached signer: {cached_signer}\")\n        if cached_signer:\n            logger.debug(f\"Using globally cached signer for token ID: {tokenID}\")\n            return cached_signer\n\n        # If the signer is not yet created for the token then create new OCI signer object\n        logger.debug(f\"Creating new signer for token ID: {tokenID}\")\n        signer = TokenExchangeSigner(\n            jwt_or_func=token,\n            oci_domain_id=iam_guid.split(\".\")[0] if iam_guid else None,  # This is same as IAM GUID configured for the OCI IAM Domain\n            client_id=client_id,  # This is same as the client ID configured for the OCI IAM Domain Integrated Application\n            client_secret=client_secret,  # This is same as the client secret configured for the OCI IAM Domain Integrated Application\n        )\n        logger.debug(f\"Signer {signer} created for token ID: {tokenID}\")\n\n        #Cache the signer object in memory cache\n        _global_token_cache[tokenID] = signer\n        logger.debug(f\"Signer cached for token ID: {tokenID}\")\n\n        return signer\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\n\n## Classes\n\n### `OCIProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/oci.py#L92\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAn OCI IAM Domain provider implementation for FastMCP.\n\nThis provider is a complete OCI integration that's ready to use with\njust the configuration URL, client ID, client secret, and base URL.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx",
    "content": "---\ntitle: propelauth\nsidebarTitle: propelauth\n---\n\n# `fastmcp.server.auth.providers.propelauth`\n\n\nPropelAuth authentication provider for FastMCP.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.propelauth import PropelAuthProvider\n\n    auth = PropelAuthProvider(\n        auth_url=\"https://auth.yourdomain.com\",\n        introspection_client_id=\"your-client-id\",\n        introspection_client_secret=\"your-client-secret\",\n        base_url=\"https://your-fastmcp-server.com\",\n        required_scopes=[\"read:user_data\"],\n    )\n\n    mcp = FastMCP(\"My App\", auth=auth)\n    ```\n\n\n## Classes\n\n### `PropelAuthTokenIntrospectionOverrides` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/propelauth.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n### `PropelAuthProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/propelauth.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nPropelAuth resource server provider using OAuth 2.1 token introspection.\n\nThis provider validates access tokens via PropelAuth's introspection endpoint\nand forwards authorization server metadata for OAuth discovery.\n\nFor detailed setup instructions, see:\nhttps://docs.propelauth.com/mcp-authentication/overview\n\n\n**Methods:**\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/propelauth.py#L141\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet routes for this provider.\n\nIncludes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),\nand creates an authorization server metadata route that forwards to PropelAuth's route\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata.\n\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/propelauth.py#L185\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify token and check the ``aud`` claim against the configured resource.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx",
    "content": "---\ntitle: scalekit\nsidebarTitle: scalekit\n---\n\n# `fastmcp.server.auth.providers.scalekit`\n\n\nScalekit authentication provider for FastMCP.\n\nThis module provides ScalekitProvider - a complete authentication solution that integrates\nwith Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server\nauthentication for seamless MCP client authentication.\n\n\n## Classes\n\n### `ScalekitProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nScalekit resource server provider for OAuth 2.1 authentication.\n\nThis provider implements Scalekit integration using resource server pattern.\nFastMCP acts as a protected resource server that validates access tokens issued\nby Scalekit's authorization server.\n\nIMPORTANT SETUP REQUIREMENTS:\n\n1. Create an MCP Server in Scalekit Dashboard:\n   - Go to your [Scalekit Dashboard](https://app.scalekit.com/)\n   - Navigate to MCP Servers section\n   - Register a new MCP Server with appropriate scopes\n   - Ensure the Resource Identifier matches exactly what you configure as MCP URL\n   - Note the Resource ID\n\n2. Environment Configuration:\n   - Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)\n   - Set SCALEKIT_RESOURCE_ID from your created resource\n   - Set BASE_URL to your FastMCP server's public URL\n\nFor detailed setup instructions, see:\nhttps://docs.scalekit.com/mcp/overview/\n\n\n**Methods:**\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L156\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet OAuth routes including Scalekit authorization server metadata forwarding.\n\nThis returns the standard protected resource routes plus an authorization server\nmetadata endpoint that forwards Scalekit's OAuth metadata to clients.\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx",
    "content": "---\ntitle: supabase\nsidebarTitle: supabase\n---\n\n# `fastmcp.server.auth.providers.supabase`\n\n\nSupabase authentication provider for FastMCP.\n\nThis module provides SupabaseProvider - a complete authentication solution that integrates\nwith Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)\nfor seamless MCP client authentication.\n\n\n## Classes\n\n### `SupabaseProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L25\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSupabase metadata provider for DCR (Dynamic Client Registration).\n\nThis provider implements Supabase Auth integration using metadata forwarding.\nThis approach allows Supabase to handle the OAuth flow directly while FastMCP acts\nas a resource server, verifying JWTs issued by Supabase Auth.\n\nIMPORTANT SETUP REQUIREMENTS:\n\n1. Supabase Project Setup:\n   - Create a Supabase project at https://supabase.com\n   - Note your project URL (e.g., \"https://abc123.supabase.co\")\n   - Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)\n   - Asymmetric keys (RS256/ES256) are recommended for production\n\n2. JWT Verification:\n   - FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json\n   - JWTs are issued by {project_url}{auth_route}\n   - Default auth_route is \"/auth/v1\" (can be customized for self-hosted setups)\n   - Tokens are cached for up to 10 minutes by Supabase's edge servers\n   - Algorithm must match your Supabase Auth configuration\n\n3. Authorization:\n   - Supabase uses Row Level Security (RLS) policies for database authorization\n   - OAuth-level scopes are an upcoming feature in Supabase Auth\n   - Both approaches will be supported once scope handling is available\n\nFor detailed setup instructions, see:\nhttps://supabase.com/docs/guides/auth/jwts\n\n\n**Methods:**\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L137\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet OAuth routes including Supabase authorization server metadata forwarding.\n\nThis returns the standard protected resource routes plus an authorization server\nmetadata endpoint that forwards Supabase's OAuth metadata to clients.\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-providers-workos.mdx",
    "content": "---\ntitle: workos\nsidebarTitle: workos\n---\n\n# `fastmcp.server.auth.providers.workos`\n\n\nWorkOS authentication providers for FastMCP.\n\nThis module provides two WorkOS authentication strategies:\n\n1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)\n2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit\n\nChoose based on your WorkOS setup and authentication requirements.\n\n\n## Classes\n\n### `WorkOSTokenVerifier` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L31\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken verifier for WorkOS OAuth tokens.\n\nWorkOS AuthKit tokens are opaque, so we verify them by calling\nthe /oauth2/userinfo endpoint to check validity and get user info.\n\n\n**Methods:**\n\n#### `verify_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L61\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nverify_token(self, token: str) -> AccessToken | None\n```\n\nVerify WorkOS OAuth token by calling userinfo endpoint.\n\n\n### `WorkOSProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L126\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComplete WorkOS OAuth provider for FastMCP.\n\nThis provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.\nIt provides OAuth2 authentication for users through WorkOS Connect applications.\n\nFeatures:\n- Transparent OAuth proxy to WorkOS AuthKit\n- Automatic token validation via userinfo endpoint\n- User information extraction from ID tokens\n- Support for standard OAuth scopes (openid, profile, email)\n\nSetup Requirements:\n1. Create a WorkOS Connect application in your dashboard\n2. Note your AuthKit domain (e.g., \"https://your-app.authkit.app\")\n3. Configure redirect URI as: http://localhost:8000/auth/callback\n4. Note your Client ID and Client Secret\n\n\n### `AuthKitProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L249\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAuthKit metadata provider for DCR (Dynamic Client Registration).\n\nThis provider implements AuthKit integration using metadata forwarding\ninstead of OAuth proxying. This is the recommended approach for WorkOS DCR\nas it allows WorkOS to handle the OAuth flow directly while FastMCP acts\nas a resource server.\n\nIMPORTANT SETUP REQUIREMENTS:\n\n1. Enable Dynamic Client Registration in WorkOS Dashboard:\n   - Go to Applications → Configuration\n   - Toggle \"Dynamic Client Registration\" to enabled\n\n2. Configure your FastMCP server URL as a callback:\n   - Add your server URL to the Redirects tab in WorkOS dashboard\n   - Example: https://your-fastmcp-server.com/oauth2/callback\n\nFor detailed setup instructions, see:\nhttps://workos.com/docs/authkit/mcp/integrating/token-verification\n\n\n**Methods:**\n\n#### `get_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L347\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_routes(self, mcp_path: str | None = None) -> list[Route]\n```\n\nGet OAuth routes including AuthKit authorization server metadata forwarding.\n\nThis returns the standard protected resource routes plus an authorization server\nmetadata endpoint that forwards AuthKit's OAuth metadata to clients.\n\n**Args:**\n- `mcp_path`: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\nThis is used to advertise the resource URL in metadata.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx",
    "content": "---\ntitle: redirect_validation\nsidebarTitle: redirect_validation\n---\n\n# `fastmcp.server.auth.redirect_validation`\n\n\nUtilities for validating client redirect URIs in OAuth flows.\n\nThis module provides secure redirect URI validation with wildcard support,\nprotecting against userinfo-based bypass attacks like http://localhost@evil.com.\n\n\n## Functions\n\n### `matches_allowed_pattern` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/redirect_validation.py#L121\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmatches_allowed_pattern(uri: str, pattern: str) -> bool\n```\n\n\nSecurely check if a URI matches an allowed pattern with wildcard support.\n\nThis function parses both the URI and pattern as URLs, comparing each\ncomponent separately to prevent bypass attacks like userinfo injection.\n\nPatterns support wildcards:\n- http://localhost:* matches any localhost port\n- http://127.0.0.1:* matches any 127.0.0.1 port\n- https://*.example.com/* matches any subdomain of example.com\n- https://app.example.com/auth/* matches any path under /auth/\n\nSecurity: Rejects URIs with userinfo (user:pass@host) which could bypass\nnaive string matching (e.g., http://localhost@evil.com).\n\n**Args:**\n- `uri`: The redirect URI to validate\n- `pattern`: The allowed pattern (may contain wildcards)\n\n**Returns:**\n- True if the URI matches the pattern\n\n\n### `validate_redirect_uri` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/redirect_validation.py#L175\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_redirect_uri(redirect_uri: str | AnyUrl | None, allowed_patterns: list[str] | None) -> bool\n```\n\n\nValidate a redirect URI against allowed patterns.\n\n**Args:**\n- `redirect_uri`: The redirect URI to validate\n- `allowed_patterns`: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility).\n             If empty list, no URIs are allowed.\n             To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS.\n\n**Returns:**\n- True if the redirect URI is allowed\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-auth-ssrf.mdx",
    "content": "---\ntitle: ssrf\nsidebarTitle: ssrf\n---\n\n# `fastmcp.server.auth.ssrf`\n\n\nSSRF-safe HTTP utilities for FastMCP.\n\nThis module provides SSRF-protected HTTP fetching with:\n- DNS resolution and IP validation before requests\n- DNS pinning to prevent rebinding TOCTOU attacks\n- Support for both CIMD and JWKS fetches\n\n\n## Functions\n\n### `format_ip_for_url` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_ip_for_url(ip_str: str) -> str\n```\n\n\nFormat IP address for use in URL (bracket IPv6 addresses).\n\nIPv6 addresses must be bracketed in URLs to distinguish the address from\nthe port separator. For example: https://[2001:db8::1]:443/path\n\n**Args:**\n- `ip_str`: IP address string\n\n**Returns:**\n- IP string suitable for URL (IPv6 addresses are bracketed)\n\n\n### `is_ip_allowed` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L55\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_ip_allowed(ip_str: str) -> bool\n```\n\n\nCheck if an IP address is allowed (must be globally routable unicast).\n\nUses ip.is_global which catches:\n- Private (10.x, 172.16-31.x, 192.168.x)\n- Loopback (127.x, ::1)\n- Link-local (169.254.x, fe80::) - includes AWS metadata!\n- Reserved, unspecified\n- RFC6598 Carrier-Grade NAT (100.64.0.0/10) - can point to internal networks\n\nAdditionally blocks multicast addresses (not caught by is_global).\n\n**Args:**\n- `ip_str`: IP address string to check\n\n**Returns:**\n- True if the IP is allowed (public unicast internet), False if blocked\n\n\n### `resolve_hostname` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L98\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_hostname(hostname: str, port: int = 443) -> list[str]\n```\n\n\nResolve hostname to IP addresses using DNS.\n\n**Args:**\n- `hostname`: Hostname to resolve\n- `port`: Port number (used for getaddrinfo)\n\n**Returns:**\n- List of resolved IP addresses\n\n**Raises:**\n- `SSRFError`: If resolution fails\n\n\n### `validate_url` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L147\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_url(url: str, require_path: bool = False) -> ValidatedURL\n```\n\n\nValidate URL for SSRF and resolve to IPs.\n\n**Args:**\n- `url`: URL to validate\n- `require_path`: If True, require non-root path (for CIMD)\n\n**Returns:**\n- ValidatedURL with resolved IPs\n\n**Raises:**\n- `SSRFError`: If URL is invalid or resolves to blocked IPs\n\n\n### `ssrf_safe_fetch` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L196\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nssrf_safe_fetch(url: str) -> bytes\n```\n\n\nFetch URL with comprehensive SSRF protection and DNS pinning.\n\nSecurity measures:\n1. HTTPS only\n2. DNS resolution with IP validation\n3. Connects to validated IP directly (DNS pinning prevents rebinding)\n4. Response size limit\n5. Redirects disabled\n6. Overall timeout\n\n**Args:**\n- `url`: URL to fetch\n- `require_path`: If True, require non-root path\n- `max_size`: Maximum response size in bytes (default 5KB)\n- `timeout`: Per-operation timeout in seconds\n- `overall_timeout`: Overall timeout for entire operation\n\n**Returns:**\n- Response body as bytes\n\n**Raises:**\n- `SSRFError`: If SSRF validation fails\n- `SSRFFetchError`: If fetch fails\n\n\n### `ssrf_safe_fetch_response` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L239\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nssrf_safe_fetch_response(url: str) -> SSRFFetchResponse\n```\n\n\nFetch URL with SSRF protection and return response metadata.\n\nThis is equivalent to :func:`ssrf_safe_fetch` but returns response headers\nand status code, and supports conditional request headers.\n\n\n## Classes\n\n### `SSRFError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRaised when an SSRF protection check fails.\n\n\n### `SSRFFetchError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L51\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRaised when SSRF-safe fetch fails.\n\n\n### `ValidatedURL` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L128\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA URL that has been validated for SSRF with resolved IPs.\n\n\n### `SSRFFetchResponse` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/auth/ssrf.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResponse payload from an SSRF-safe fetch.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-context.mdx",
    "content": "---\ntitle: context\nsidebarTitle: context\n---\n\n# `fastmcp.server.context`\n\n## Functions\n\n### `set_transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L88\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_transport(transport: TransportType) -> Token[TransportType | None]\n```\n\n\nSet the current transport type. Returns token for reset.\n\n\n### `reset_transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L95\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreset_transport(token: Token[TransportType | None]) -> None\n```\n\n\nReset transport to previous value.\n\n\n### `set_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L125\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_context(context: Context) -> Generator[Context, None, None]\n```\n\n## Classes\n\n### `LogData` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L101\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nData object for passing log arguments to client-side handlers.\n\nThis provides an interface to match the Python standard library logging,\nfor compatibility with structured logging.\n\n\n### `Context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L134\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nContext object providing access to MCP capabilities.\n\nThis provides a cleaner interface to MCP's RequestContext functionality.\nIt gets injected into tool and resource functions that request it via type hints.\n\nTo use context in a tool function, add a parameter with the Context type annotation:\n\n```python\n@server.tool\nasync def my_tool(x: int, ctx: Context) -> str:\n    # Log messages to the client\n    await ctx.info(f\"Processing {x}\")\n    await ctx.debug(\"Debug info\")\n    await ctx.warning(\"Warning message\")\n    await ctx.error(\"Error message\")\n\n    # Report progress\n    await ctx.report_progress(50, 100, \"Processing\")\n\n    # Access resources\n    data = await ctx.read_resource(\"resource://data\")\n\n    # Get request info\n    request_id = ctx.request_id\n    client_id = ctx.client_id\n\n    # Manage state across the session (persists across requests)\n    await ctx.set_state(\"key\", \"value\")\n    value = await ctx.get_state(\"key\")\n\n    # Store non-serializable values for the current request only\n    await ctx.set_state(\"client\", http_client, serializable=False)\n\n    return str(x)\n```\n\nState Management:\nContext provides session-scoped state that persists across requests within\nthe same MCP session. State is automatically keyed by session, ensuring\nisolation between different clients.\n\nState set during `on_initialize` middleware will persist to subsequent tool\ncalls when using the same session object (STDIO, SSE, single-server HTTP).\nFor distributed/serverless HTTP deployments where different machines handle\nthe init and tool calls, state is isolated by the mcp-session-id header.\n\nThe context parameter name can be anything as long as it's annotated with Context.\nThe context is optional - tools that don't need it can omit the parameter.\n\n\n**Methods:**\n\n#### `is_background_task` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L207\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_background_task(self) -> bool\n```\n\nTrue when this context is running in a background task (Docket worker).\n\nWhen True, certain operations like elicit() and sample() will use\ntask-aware implementations that can pause the task and wait for\nclient input.\n\n\n#### `task_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L226\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntask_id(self) -> str | None\n```\n\nGet the background task ID if running in a background task.\n\nReturns None if not running in a background task context.\n\n\n#### `origin_request_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L234\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\norigin_request_id(self) -> str | None\n```\n\nGet the request ID that originated this execution, if available.\n\nIn foreground request mode, this is the current request_id.\nIn background task mode, this is the request_id captured when the task\nwas submitted, if one was available.\n\n\n#### `fastmcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L246\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfastmcp(self) -> FastMCP\n```\n\nGet the FastMCP instance.\n\n\n#### `request_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L321\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrequest_context(self) -> RequestContext[ServerSession, Any, Request] | None\n```\n\nAccess to the underlying request context.\n\nReturns None when the MCP session has not been established yet.\nReturns the full RequestContext once the MCP session is available.\n\nFor HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,\nwhich works whether or not the MCP session is available.\n\nExample in middleware:\n```python\nasync def on_request(self, context, call_next):\n    ctx = context.fastmcp_context\n    if ctx.request_context:\n        # MCP session available - can access session_id, request_id, etc.\n        session_id = ctx.session_id\n    else:\n        # MCP session not available yet - use HTTP helpers\n        from fastmcp.server.dependencies import get_http_request\n        request = get_http_request()\n    return await call_next(context)\n```\n\n\n#### `lifespan_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L350\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan_context(self) -> dict[str, Any]\n```\n\nAccess the server's lifespan context.\n\nReturns the context dict yielded by the server's lifespan function.\nReturns an empty dict if no lifespan was configured or if the MCP\nsession is not yet established.\n\nIn background tasks (Docket workers), where request_context is not\navailable, falls back to reading from the FastMCP server's lifespan\nresult directly.\n\nExample:\n```python\n@server.tool\ndef my_tool(ctx: Context) -> str:\n    db = ctx.lifespan_context.get(\"db\")\n    if db:\n        return db.query(\"SELECT 1\")\n    return \"No database connection\"\n```\n\n\n#### `report_progress` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L381\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreport_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None\n```\n\nReport progress for the current operation.\n\nWorks in both foreground (MCP progress notifications) and background\n(Docket task execution) contexts.\n\n**Args:**\n- `progress`: Current progress value e.g. 24\n- `total`: Optional total value e.g. 100\n- `message`: Optional status message describing current progress\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L474\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self) -> list[SDKResource]\n```\n\nList all available resources from the server.\n\n**Returns:**\n- List of Resource objects available on the server\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L490\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self) -> list[SDKPrompt]\n```\n\nList all available prompts from the server.\n\n**Returns:**\n- List of Prompt objects available on the server\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L506\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult\n```\n\nGet a prompt by name with optional arguments.\n\n**Args:**\n- `name`: The name of the prompt to get\n- `arguments`: Optional arguments to pass to the prompt\n\n**Returns:**\n- The prompt result\n\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L525\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self, uri: str | AnyUrl) -> ResourceResult\n```\n\nRead a resource by URI.\n\n**Args:**\n- `uri`: Resource URI to read\n\n**Returns:**\n- ResourceResult with contents\n\n\n#### `log` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L541\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlog(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None\n```\n\nSend a log message to the client.\n\nMessages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\n\n**Args:**\n- `message`: Log message\n- `level`: Optional log level. One of \"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\",\n\"alert\", or \"emergency\". Default is \"info\".\n- `logger_name`: Optional logger name\n- `extra`: Optional mapping for additional arguments\n\n\n#### `transport` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L571\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransport(self) -> TransportType | None\n```\n\nGet the current transport type.\n\nReturns the transport type used to run this server: \"stdio\", \"sse\",\nor \"streamable-http\". Returns None if called outside of a server context.\n\n\n#### `client_supports_extension` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L579\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclient_supports_extension(self, extension_id: str) -> bool\n```\n\nCheck whether the connected client supports a given MCP extension.\n\nInspects the ``extensions`` extra field on ``ClientCapabilities``\nsent by the client during initialization.\n\nReturns ``False`` when no session is available (e.g., outside a\nrequest context) or when the client did not advertise the extension.\n\nExample::\n\n    from fastmcp.server.apps import UI_EXTENSION_ID\n\n    @mcp.tool\n    async def my_tool(ctx: Context) -> str:\n        if ctx.client_supports_extension(UI_EXTENSION_ID):\n            return \"UI-capable client\"\n        return \"text-only client\"\n\n\n#### `client_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L607\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclient_id(self) -> str | None\n```\n\nGet the client ID if available.\n\n\n#### `request_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L616\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrequest_id(self) -> str\n```\n\nGet the unique ID for this request.\n\nRaises RuntimeError if MCP request context is not available.\n\n\n#### `session_id` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L629\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsession_id(self) -> str\n```\n\nGet the MCP session ID for ALL transports.\n\nReturns the session ID that can be used as a key for session-based\ndata storage (e.g., Redis) to share data between tool calls within\nthe same client session.\n\n**Returns:**\n- The session ID for StreamableHTTP transports, or a generated ID\n- for other transports.\n\n\n#### `session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L686\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsession(self) -> ServerSession\n```\n\nAccess to the underlying session for advanced usage.\n\nIn request mode: Returns the session from the active request context.\nIn background task mode: Returns the session stored at Context creation.\n\nRaises RuntimeError if no session is available.\n\n\n#### `debug` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L712\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndebug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None\n```\n\nSend a `DEBUG`-level message to the connected MCP Client.\n\nMessages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\n\n\n#### `info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L728\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninfo(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None\n```\n\nSend a `INFO`-level message to the connected MCP Client.\n\nMessages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\n\n\n#### `warning` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L744\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwarning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None\n```\n\nSend a `WARNING`-level message to the connected MCP Client.\n\nMessages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\n\n\n#### `error` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L760\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nerror(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None\n```\n\nSend a `ERROR`-level message to the connected MCP Client.\n\nMessages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\n\n\n#### `list_roots` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L776\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_roots(self) -> list[Root]\n```\n\nList the roots available to the server, as indicated by the client.\n\n\n#### `send_notification` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L781\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsend_notification(self, notification: mcp.types.ServerNotificationType) -> None\n```\n\nSend a notification to the client immediately.\n\n**Args:**\n- `notification`: An MCP notification instance (e.g., ToolListChangedNotification())\n\n\n#### `close_sse_stream` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L791\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclose_sse_stream(self) -> None\n```\n\nClose the current response stream to trigger client reconnection.\n\nWhen using StreamableHTTP transport with an EventStore configured, this\nmethod gracefully closes the HTTP connection for the current request.\nThe client will automatically reconnect (after `retry_interval` milliseconds)\nand resume receiving events from where it left off via the EventStore.\n\nThis is useful for long-running operations to avoid load balancer timeouts.\nInstead of holding a connection open for minutes, you can periodically close\nand let the client reconnect.\n\n\n#### `sample_step` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L830\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsample_step(self, messages: str | Sequence[str | SamplingMessage]) -> SampleStep\n```\n\nMake a single LLM sampling call.\n\nThis is a stateless function that makes exactly one LLM call and optionally\nexecutes any requested tools. Use this for fine-grained control over the\nsampling loop.\n\n**Args:**\n- `messages`: The message(s) to send. Can be a string, list of strings,\nor list of SamplingMessage objects.\n- `system_prompt`: Optional system prompt for the LLM.\n- `temperature`: Optional sampling temperature.\n- `max_tokens`: Maximum tokens to generate. Defaults to 512.\n- `model_preferences`: Optional model preferences.\n- `tools`: Optional list of tools the LLM can use.\n- `tool_choice`: Tool choice mode (\"auto\", \"required\", or \"none\").\n- `execute_tools`: If True (default), execute tool calls and append results\nto history. If False, return immediately with tool_calls available\nin the step for manual execution.\n- `mask_error_details`: If True, mask detailed error messages from tool\nexecution. When None (default), uses the global settings value.\nTools can raise ToolError to bypass masking.\n- `tool_concurrency`: Controls parallel execution of tools\\:\n- None (default)\\: Sequential execution (one at a time)\n- 0\\: Unlimited parallel execution\n- N > 0\\: Execute at most N tools concurrently\nIf any tool has sequential=True, all tools execute sequentially\nregardless of this setting.\n\n**Returns:**\n- SampleStep containing:\n- - .response: The raw LLM response\n- - .history: Messages including input, assistant response, and tool results\n- - .is_tool_use: True if the LLM requested tool execution\n- - .tool_calls: List of tool calls (if any)\n- - .text: The text content (if any)\n\n\n#### `sample` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L909\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]\n```\n\nOverload: With result_type, returns SamplingResult[ResultT].\n\n\n#### `sample` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L925\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str]\n```\n\nOverload: Without result_type, returns SamplingResult[str].\n\n\n#### `sample` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L940\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str]\n```\n\nSend a sampling request to the client and await the response.\n\nThis method runs to completion automatically. When tools are provided,\nit executes a tool loop: if the LLM returns a tool use request, the tools\nare executed and the results are sent back to the LLM. This continues\nuntil the LLM provides a final text response.\n\nWhen result_type is specified, a synthetic `final_response` tool is\ncreated. The LLM calls this tool to provide the structured response,\nwhich is validated against the result_type and returned as `.result`.\n\nFor fine-grained control over the sampling loop, use sample_step() instead.\n\n**Args:**\n- `messages`: The message(s) to send. Can be a string, list of strings,\nor list of SamplingMessage objects.\n- `system_prompt`: Optional system prompt for the LLM.\n- `temperature`: Optional sampling temperature.\n- `max_tokens`: Maximum tokens to generate. Defaults to 512.\n- `model_preferences`: Optional model preferences.\n- `tools`: Optional list of tools the LLM can use. Accepts plain\nfunctions or SamplingTools.\n- `result_type`: Optional type for structured output. When specified,\na synthetic `final_response` tool is created and the LLM's\nresponse is validated against this type.\n- `mask_error_details`: If True, mask detailed error messages from tool\nexecution. When None (default), uses the global settings value.\nTools can raise ToolError to bypass masking.\n- `tool_concurrency`: Controls parallel execution of tools\\:\n- None (default)\\: Sequential execution (one at a time)\n- 0\\: Unlimited parallel execution\n- N > 0\\: Execute at most N tools concurrently\nIf any tool has sequential=True, all tools execute sequentially\nregardless of this setting.\n\n**Returns:**\n- SamplingResult[T] containing:\n- - .text: The text representation (raw text or JSON for structured)\n- - .result: The typed result (str for text, parsed object for structured)\n- - .history: All messages exchanged during sampling\n\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1015\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation\n```\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1027\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation\n```\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1037\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation\n```\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1047\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation\n```\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1057\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation\n```\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1069\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation\n```\n\n#### `elicit` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1081\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit(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\n```\n\nSend an elicitation request to the client and await the response.\n\nCall this method at any time to request additional information from\nthe user through the client. The client must support elicitation,\nor the request will error.\n\nNote that the MCP protocol only supports simple object schemas with\nprimitive types. You can provide a dataclass, TypedDict, or BaseModel to\ncomply. If you provide a primitive type, an object schema with a single\n\"value\" field will be generated for the MCP interaction and\nautomatically deconstructed into the primitive type upon response.\n\nIf the response_type is None, the generated schema will be that of an\nempty object in order to comply with the MCP protocol requirements.\nClients must send an empty object (\"{}\")in response.\n\n**Args:**\n- `message`: A human-readable message explaining what information is needed\n- `response_type`: The type of the response, which should be a primitive\ntype or dataclass or BaseModel. If it is a primitive type, an\nobject schema with a single \"value\" field will be generated.\n\n\n#### `set_state` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1195\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_state(self, key: str, value: Any) -> None\n```\n\nSet a value in the state store.\n\nBy default, values are stored in the session-scoped state store and\npersist across requests within the same MCP session. Values must be\nJSON-serializable (dicts, lists, strings, numbers, etc.).\n\nFor non-serializable values (e.g., HTTP clients, database connections),\npass ``serializable=False``. These values are stored in a request-scoped\ndict and only live for the current MCP request (tool call, resource\nread, or prompt render). They will not be available in subsequent\nrequests.\n\nThe key is automatically prefixed with the session identifier.\n\n\n#### `get_state` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1237\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_state(self, key: str) -> Any\n```\n\nGet a value from the state store.\n\nChecks request-scoped state first (set with ``serializable=False``),\nthen falls back to the session-scoped state store.\n\nReturns None if the key is not found.\n\n\n#### `delete_state` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1251\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndelete_state(self, key: str) -> None\n```\n\nDelete a value from the state store.\n\nRemoves from both request-scoped and session-scoped stores.\n\n\n#### `enable_components` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1272\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nenable_components(self) -> None\n```\n\nEnable components matching criteria for this session only.\n\nSession rules override global transforms. Rules accumulate - each call\nadds a new rule to the session. Later marks override earlier ones\n(Visibility transform semantics).\n\nSends notifications to this session only: ToolListChangedNotification,\nResourceListChangedNotification, and PromptListChangedNotification.\n\n**Args:**\n- `names`: Component names or URIs to match.\n- `keys`: Component keys to match (e.g., {\"tool\\:my_tool@v1\"}).\n- `version`: Component version spec to match.\n- `tags`: Tags to match (component must have at least one).\n- `components`: Component types to match (e.g., {\"tool\", \"prompt\"}).\n- `match_all`: If True, matches all components regardless of other criteria.\n\n\n#### `disable_components` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1310\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndisable_components(self) -> None\n```\n\nDisable components matching criteria for this session only.\n\nSession rules override global transforms. Rules accumulate - each call\nadds a new rule to the session. Later marks override earlier ones\n(Visibility transform semantics).\n\nSends notifications to this session only: ToolListChangedNotification,\nResourceListChangedNotification, and PromptListChangedNotification.\n\n**Args:**\n- `names`: Component names or URIs to match.\n- `keys`: Component keys to match (e.g., {\"tool\\:my_tool@v1\"}).\n- `version`: Component version spec to match.\n- `tags`: Tags to match (component must have at least one).\n- `components`: Component types to match (e.g., {\"tool\", \"prompt\"}).\n- `match_all`: If True, matches all components regardless of other criteria.\n\n\n#### `reset_visibility` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/context.py#L1348\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreset_visibility(self) -> None\n```\n\nClear all session visibility rules.\n\nUse this to reset session visibility back to global defaults.\n\nSends notifications to this session only: ToolListChangedNotification,\nResourceListChangedNotification, and PromptListChangedNotification.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-dependencies.mdx",
    "content": "---\ntitle: dependencies\nsidebarTitle: dependencies\n---\n\n# `fastmcp.server.dependencies`\n\n\nDependency injection for FastMCP.\n\nDI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket\nusing the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,\nCurrentWorker) and background task execution require fastmcp[tasks].\n\n\n## Functions\n\n### `get_task_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L101\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_task_context() -> TaskContextInfo | None\n```\n\n\nGet the current task context if running inside a background task worker.\n\nThis function extracts task information from the Docket execution context.\nReturns None if not running in a task context (e.g., foreground execution).\n\n**Returns:**\n- TaskContextInfo with task_id and session_id, or None if not in a task.\n\n\n### `register_task_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_task_session(session_id: str, session: ServerSession) -> None\n```\n\n\nRegister a session for Context access in background tasks.\n\nCalled automatically when a task is submitted to Docket. The session is\nstored as a weakref so it doesn't prevent garbage collection when the\nclient disconnects.\n\n**Args:**\n- `session_id`: The session identifier\n- `session`: The ServerSession instance\n\n\n### `get_task_session` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L153\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_task_session(session_id: str) -> ServerSession | None\n```\n\n\nGet a registered session by ID if still alive.\n\n**Args:**\n- `session_id`: The session identifier\n\n**Returns:**\n- The ServerSession if found and alive, None otherwise\n\n\n### `is_docket_available` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L189\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_docket_available() -> bool\n```\n\n\nCheck if pydocket is installed.\n\n\n### `require_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L202\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrequire_docket(feature: str) -> None\n```\n\n\nRaise ImportError with install instructions if docket not available.\n\n**Args:**\n- `feature`: Description of what requires docket (e.g., \"`task=True`\",\n     \"CurrentDocket()\"). Will be included in the error message.\n\n\n### `transform_context_annotations` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L227\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]\n```\n\n\nTransform ctx: Context into ctx: Context = CurrentContext().\n\nTransforms ALL params typed as Context to use Docket's DI system,\nunless they already have a Dependency-based default (like CurrentContext()).\n\nThis unifies the legacy type annotation DI with Docket's Depends() system,\nallowing both patterns to work through a single resolution path.\n\nNote: Only POSITIONAL_OR_KEYWORD parameters are reordered (params with defaults\nafter those without). KEYWORD_ONLY parameters keep their position since Python\nallows them to have defaults in any order.\n\n**Args:**\n- `fn`: Function to transform\n\n**Returns:**\n- Function with modified signature (same function object, updated __signature__)\n\n\n### `get_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L368\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_context() -> Context\n```\n\n\nGet the current FastMCP Context instance directly.\n\n\n### `get_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L378\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_server() -> FastMCP\n```\n\n\nGet the current FastMCP server instance directly.\n\n**Returns:**\n- The active FastMCP server\n\n**Raises:**\n- `RuntimeError`: If no server in context\n\n\n### `get_http_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L396\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_http_request() -> Request\n```\n\n\nGet the current HTTP request.\n\nTries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.\n\n\n### `get_http_headers` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L416\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_http_headers(include_all: bool = False, include: set[str] | None = None) -> dict[str, str]\n```\n\n\nExtract headers from the current HTTP request if available.\n\nNever raises an exception, even if there is no active HTTP request (in which case\nan empty dict is returned).\n\nBy default, strips problematic headers like `content-length` and `authorization`\nthat cause issues if forwarded to downstream services. If `include_all` is True,\nall headers are returned.\n\nThe `include` parameter allows specific headers to be included even if they would\nnormally be excluded. This is useful for proxy transports that need to forward\nauthorization headers to upstream MCP servers.\n\n\n### `get_access_token` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L473\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_access_token() -> AccessToken | None\n```\n\n\nGet the FastMCP access token from the current context.\n\nThis function first tries to get the token from the current HTTP request's scope,\nwhich is more reliable for long-lived connections where the SDK's auth_context_var\nmay become stale after token refresh. Falls back to the SDK's context var if no\nrequest is available. In background tasks (Docket workers), falls back to the\ntoken snapshot stored in Redis at task submission time.\n\n**Returns:**\n- The access token if an authenticated user is available, None otherwise.\n\n\n### `without_injected_parameters` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L545\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwithout_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]\n```\n\n\nCreate a wrapper function without injected parameters.\n\nReturns a wrapper that excludes Context and Docket dependency parameters,\nmaking it safe to use with Pydantic TypeAdapter for schema generation and\nvalidation. The wrapper internally handles all dependency resolution and\nContext injection when called.\n\nHandles:\n- Legacy Context injection (always works)\n- Depends() injection (always works - uses docket or vendored DI engine)\n\n**Args:**\n- `fn`: Original function with Context and/or dependencies\n\n**Returns:**\n- Async wrapper function without injected parameters\n\n\n### `resolve_dependencies` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L694\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]\n```\n\n\nResolve dependencies for a FastMCP function.\n\nThis function:\n1. Filters out any dependency parameter names from user arguments (security)\n2. Resolves Depends() parameters via the DI system\n\nThe filtering prevents external callers from overriding injected parameters by\nproviding values for dependency parameter names. This is a security feature.\n\nNote: Context injection is handled via transform_context_annotations() which\nconverts `ctx: Context` to `ctx: Context = Depends(get_context)` at registration\ntime, so all injection goes through the unified DI system.\n\n**Args:**\n- `fn`: The function to resolve dependencies for\n- `arguments`: User arguments (may contain keys that match dependency names,\n      which will be filtered out)\n\n\n### `CurrentContext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L907\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentContext() -> Context\n```\n\n\nGet the current FastMCP Context instance.\n\nThis dependency provides access to the active FastMCP Context for the\ncurrent MCP operation (tool/resource/prompt call).\n\n**Returns:**\n- A dependency that resolves to the active Context instance\n\n**Raises:**\n- `RuntimeError`: If no active context found (during resolution)\n\n\n### `OptionalCurrentContext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L932\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nOptionalCurrentContext() -> Context | None\n```\n\n\nGet the current FastMCP Context, or None when no context is active.\n\n\n### `CurrentDocket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L960\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentDocket() -> Docket\n```\n\n\nGet the current Docket instance managed by FastMCP.\n\nThis dependency provides access to the Docket instance that FastMCP\nautomatically creates for background task scheduling.\n\n**Returns:**\n- A dependency that resolves to the active Docket instance\n\n**Raises:**\n- `RuntimeError`: If not within a FastMCP server context\n- `ImportError`: If fastmcp[tasks] not installed\n\n\n### `CurrentWorker` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1010\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentWorker() -> Worker\n```\n\n\nGet the current Docket Worker instance managed by FastMCP.\n\nThis dependency provides access to the Worker instance that FastMCP\nautomatically creates for background task processing.\n\n**Returns:**\n- A dependency that resolves to the active Worker instance\n\n**Raises:**\n- `RuntimeError`: If not within a FastMCP server context\n- `ImportError`: If fastmcp[tasks] not installed\n\n\n### `CurrentFastMCP` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1057\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentFastMCP() -> FastMCP\n```\n\n\nGet the current FastMCP server instance.\n\nThis dependency provides access to the active FastMCP server.\n\n**Returns:**\n- A dependency that resolves to the active FastMCP server\n\n**Raises:**\n- `RuntimeError`: If no server in context (during resolution)\n\n\n### `CurrentRequest` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1097\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentRequest() -> Request\n```\n\n\nGet the current HTTP request.\n\nThis dependency provides access to the Starlette Request object for the\ncurrent HTTP request. Only available when running over HTTP transports\n(SSE or Streamable HTTP).\n\n**Returns:**\n- A dependency that resolves to the active Starlette Request\n\n**Raises:**\n- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)\n\n\n### `CurrentHeaders` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1138\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentHeaders() -> dict[str, str]\n```\n\n\nGet the current HTTP request headers.\n\nThis dependency provides access to the HTTP headers for the current request,\nincluding the authorization header. Returns an empty dictionary when no HTTP\nrequest is available, making it safe to use in code that might run over any\ntransport.\n\n**Returns:**\n- A dependency that resolves to a dictionary of header name -> value\n\n\n### `CurrentAccessToken` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1372\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nCurrentAccessToken() -> AccessToken\n```\n\n\nGet the current access token for the authenticated user.\n\nThis dependency provides access to the AccessToken for the current\nauthenticated request. Raises an error if no authentication is present.\n\n**Returns:**\n- A dependency that resolves to the active AccessToken\n\n**Raises:**\n- `RuntimeError`: If no authenticated user (use get_access_token() for optional)\n\n\n### `TokenClaim` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1429\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nTokenClaim(name: str) -> str\n```\n\n\nGet a specific claim from the access token.\n\nThis dependency extracts a single claim value from the current access token.\nIt's useful for getting user identifiers, roles, or other token claims\nwithout needing the full token object.\n\n**Args:**\n- `name`: The name of the claim to extract (e.g., \"oid\", \"sub\", \"email\")\n\n**Returns:**\n- A dependency that resolves to the claim value as a string\n\n**Raises:**\n- `RuntimeError`: If no access token is available or claim is missing\n\n\n## Classes\n\n### `TaskContextInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L87\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation about the current background task context.\n\nReturned by ``get_task_context()`` when running inside a Docket worker.\nContains identifiers needed to communicate with the MCP session.\n\n\n### `ProgressLike` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1166\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for progress tracking interface.\n\nDefines the common interface between InMemoryProgress (server context)\nand Docket's Progress (worker context).\n\n\n**Methods:**\n\n#### `current` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1174\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncurrent(self) -> int | None\n```\n\nCurrent progress value.\n\n\n#### `total` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1179\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntotal(self) -> int\n```\n\nTotal/target progress value.\n\n\n#### `message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1184\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmessage(self) -> str | None\n```\n\nCurrent progress message.\n\n\n#### `set_total` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1188\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_total(self, total: int) -> None\n```\n\nSet the total/target value for progress tracking.\n\n\n#### `increment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1192\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nincrement(self, amount: int = 1) -> None\n```\n\nAtomically increment the current progress value.\n\n\n#### `set_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1196\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_message(self, message: str | None) -> None\n```\n\nUpdate the progress status message.\n\n\n### `InMemoryProgress` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1201\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nIn-memory progress tracker for immediate tool execution.\n\nProvides the same interface as Docket's Progress but stores state in memory\ninstead of Redis. Useful for testing and immediate execution where\nprogress doesn't need to be observable across processes.\n\n\n**Methods:**\n\n#### `current` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1226\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncurrent(self) -> int | None\n```\n\n#### `total` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1230\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntotal(self) -> int\n```\n\n#### `message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1234\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmessage(self) -> str | None\n```\n\n#### `set_total` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1237\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_total(self, total: int) -> None\n```\n\nSet the total/target value for progress tracking.\n\n\n#### `increment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1243\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nincrement(self, amount: int = 1) -> None\n```\n\nAtomically increment the current progress value.\n\n\n#### `set_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1252\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_message(self, message: str | None) -> None\n```\n\nUpdate the progress status message.\n\n\n### `Progress` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1257\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nFastMCP Progress dependency that works in both server and worker contexts.\n\nHandles three execution modes:\n- In Docket worker: Uses the execution's progress (observable via Redis)\n- In FastMCP server with Docket: Falls back to in-memory progress\n- In FastMCP server without Docket: Uses in-memory progress\n\nThis allows tools to use Progress() regardless of whether they're called\nimmediately or as background tasks, and regardless of whether pydocket\nis installed.\n\n\n**Methods:**\n\n#### `current` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1299\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncurrent(self) -> int | None\n```\n\nCurrent progress value.\n\n\n#### `total` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1305\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntotal(self) -> int\n```\n\nTotal/target progress value.\n\n\n#### `message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1311\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmessage(self) -> str | None\n```\n\nCurrent progress message.\n\n\n#### `set_total` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1316\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_total(self, total: int) -> None\n```\n\nSet the total/target value for progress tracking.\n\n\n#### `increment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1321\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nincrement(self, amount: int = 1) -> None\n```\n\nAtomically increment the current progress value.\n\n\n#### `set_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1326\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_message(self, message: str | None) -> None\n```\n\nUpdate the progress status message.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-elicitation.mdx",
    "content": "---\ntitle: elicitation\nsidebarTitle: elicitation\n---\n\n# `fastmcp.server.elicitation`\n\n## Functions\n\n### `parse_elicit_response_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L132\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_elicit_response_type(response_type: Any) -> ElicitConfig\n```\n\n\nParse response_type into schema and handling configuration.\n\nSupports multiple syntaxes:\n- None: Empty object schema, expect empty response\n- dict: `{\"low\": {\"title\": \"...\"}}` -> single-select titled enum\n- list patterns:\n    - `[[\"a\", \"b\"]]` -> multi-select untitled\n    - `[{\"low\": {...}}]` -> multi-select titled\n    - `[\"a\", \"b\"]` -> single-select untitled\n- `list[X]` type annotation: multi-select with type\n- Scalar types (bool, int, float, str, Literal, Enum): single value\n- Other types (dataclass, BaseModel): use directly\n\n\n### `handle_elicit_accept` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L265\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nhandle_elicit_accept(config: ElicitConfig, content: Any) -> AcceptedElicitation[Any]\n```\n\n\nHandle an accepted elicitation response.\n\n**Args:**\n- `config`: The elicitation configuration from parse_elicit_response_type\n- `content`: The response content from the client\n\n**Returns:**\n- AcceptedElicitation with the extracted/validated data\n\n\n### `get_elicitation_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L324\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_elicitation_schema(response_type: type[T]) -> dict[str, Any]\n```\n\n\nGet the schema for an elicitation response.\n\n**Args:**\n- `response_type`: The type of the response\n\n\n### `validate_elicitation_json_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L343\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_elicitation_json_schema(schema: dict[str, Any]) -> None\n```\n\n\nValidate that a JSON schema follows MCP elicitation requirements.\n\nThis ensures the schema is compatible with MCP elicitation requirements:\n- Must be an object schema\n- Must only contain primitive field types (string, number, integer, boolean)\n- Must be flat (no nested objects or arrays of objects)\n- Allows const fields (for Literal types) and enum fields (for Enum types)\n- Only primitive types and their nullable variants are allowed\n\n**Args:**\n- `schema`: The JSON schema to validate\n\n**Raises:**\n- `TypeError`: If the schema doesn't meet MCP elicitation requirements\n\n\n## Classes\n\n### `ElicitationJsonSchema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCustom JSON schema generator for MCP elicitation that always inlines enums.\n\nMCP elicitation requires inline enum schemas without $ref/$defs references.\nThis generator ensures enums are always generated inline for compatibility.\nOptionally adds enumNames for better UI display when available.\n\n\n**Methods:**\n\n#### `generate_inner` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L44\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue\n```\n\nOverride to prevent ref generation for enums and handle list schemas.\n\n\n#### `list_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L57\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue\n```\n\nGenerate schema for list types, detecting enum items for multi-select.\n\n\n#### `enum_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L94\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nenum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue\n```\n\nGenerate inline enum schema.\n\nAlways generates enum pattern: `{\"enum\": [value, ...]}`\nTitled enums are handled separately via dict-based syntax in ctx.elicit().\n\n\n### `AcceptedElicitation` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L105\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResult when user accepts the elicitation.\n\n\n### `ScalarElicitationType` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L113\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n### `ElicitConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/elicitation.py#L118\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for an elicitation request.\n\n**Attributes:**\n- `schema`: The JSON schema to send to the client\n- `response_type`: The type to validate responses with (None for raw schemas)\n- `is_raw`: True if schema was built directly (extract \"value\" from response)\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-event_store.mdx",
    "content": "---\ntitle: event_store\nsidebarTitle: event_store\n---\n\n# `fastmcp.server.event_store`\n\n\nEventStore implementation backed by AsyncKeyValue.\n\nThis module provides an EventStore implementation that enables SSE polling/resumability\nfor Streamable HTTP transports. Events are stored using the key_value package's\nAsyncKeyValue protocol, allowing users to configure any compatible backend\n(in-memory, Redis, etc.) following the same pattern as ResponseCachingMiddleware.\n\n\n## Classes\n\n### `EventEntry` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/event_store.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nStored event entry.\n\n\n### `StreamEventList` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/event_store.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nList of event IDs for a stream.\n\n\n### `EventStore` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/event_store.py#L40\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nEventStore implementation backed by AsyncKeyValue.\n\nEnables SSE polling/resumability by storing events that can be replayed\nwhen clients reconnect. Works with any AsyncKeyValue backend (memory, Redis, etc.)\nfollowing the same pattern as ResponseCachingMiddleware and OAuthProxy.\n\n**Args:**\n- `storage`: AsyncKeyValue backend. Defaults to MemoryStore.\n- `max_events_per_stream`: Maximum events to retain per stream. Default 100.\n- `ttl`: Event TTL in seconds. Default 3600 (1 hour). Set to None for no expiration.\n\n\n**Methods:**\n\n#### `store_event` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/event_store.py#L94\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nstore_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId\n```\n\nStore an event and return its ID.\n\n**Args:**\n- `stream_id`: ID of the stream the event belongs to\n- `message`: The JSON-RPC message to store, or None for priming events\n\n**Returns:**\n- The generated event ID for the stored event\n\n\n#### `replay_events_after` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/event_store.py#L135\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreplay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None\n```\n\nReplay events that occurred after the specified event ID.\n\n**Args:**\n- `last_event_id`: The ID of the last event the client received\n- `send_callback`: A callback function to send events to the client\n\n**Returns:**\n- The stream ID of the replayed events, or None if the event ID was not found\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-http.mdx",
    "content": "---\ntitle: http\nsidebarTitle: http\n---\n\n# `fastmcp.server.http`\n\n## Functions\n\n### `set_http_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L77\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_http_request(request: Request) -> Generator[Request, None, None]\n```\n\n### `create_base_app` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L110\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan\n```\n\n\nCreate a base Starlette app with common middleware and routes.\n\n**Args:**\n- `routes`: List of routes to include in the app\n- `middleware`: List of middleware to include in the app\n- `debug`: Whether to enable debug mode\n- `lifespan`: Optional lifespan manager for the app\n\n**Returns:**\n- A Starlette application\n\n\n### `create_sse_app` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_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\n```\n\n\nReturn an instance of the SSE server app.\n\n**Args:**\n- `server`: The FastMCP server instance\n- `message_path`: Path for SSE messages\n- `sse_path`: Path for SSE connections\n- `auth`: Optional authentication provider (AuthProvider)\n- `debug`: Whether to enable debug mode\n- `routes`: Optional list of custom routes\n- `middleware`: Optional list of middleware\n\nReturns:\n    A Starlette application with RequestContextMiddleware\n\n\n### `create_streamable_http_app` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L266\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_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\n```\n\n\nReturn an instance of the StreamableHTTP server app.\n\n**Args:**\n- `server`: The FastMCP server instance\n- `streamable_http_path`: Path for StreamableHTTP connections\n- `event_store`: Optional event store for SSE polling/resumability\n- `retry_interval`: Optional retry interval in milliseconds for SSE polling.\nControls how quickly clients should reconnect after server-initiated\ndisconnections. Requires event_store to be set. Defaults to SDK default.\n- `auth`: Optional authentication provider (AuthProvider)\n- `json_response`: Whether to use JSON response format\n- `stateless_http`: Whether to use stateless mode (new transport per request)\n- `debug`: Whether to enable debug mode\n- `routes`: Optional list of custom routes\n- `middleware`: Optional list of middleware\n\n**Returns:**\n- A Starlette application with StreamableHTTP support\n\n\n## Classes\n\n### `StreamableHTTPASGIApp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L32\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nASGI application wrapper for Streamable HTTP server transport.\n\n\n### `StarletteWithLifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L70\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L72\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(self) -> Lifespan[Starlette]\n```\n\n### `RequestContextMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/http.py#L85\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that stores each request in a ContextVar and sets transport type.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-lifespan.mdx",
    "content": "---\ntitle: lifespan\nsidebarTitle: lifespan\n---\n\n# `fastmcp.server.lifespan`\n\n\nComposable lifespans for FastMCP servers.\n\nThis module provides a `@lifespan` decorator for creating composable server lifespans\nthat can be combined using the `|` operator.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.lifespan import lifespan\n\n    @lifespan\n    async def db_lifespan(server):\n        conn = await connect_db()\n        yield {\"db\": conn}\n        await conn.close()\n\n    @lifespan\n    async def cache_lifespan(server):\n        cache = await connect_cache()\n        yield {\"cache\": cache}\n        await cache.close()\n\n    mcp = FastMCP(\"server\", lifespan=db_lifespan | cache_lifespan)\n    ```\n\nTo compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:\n\n    ```python\n    from contextlib import asynccontextmanager\n    from fastmcp.server.lifespan import lifespan, ContextManagerLifespan\n\n    @asynccontextmanager\n    async def legacy_lifespan(server):\n        yield {\"legacy\": True}\n\n    @lifespan\n    async def new_lifespan(server):\n        yield {\"new\": True}\n\n    # Wrap the legacy lifespan explicitly\n    combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan\n    ```\n\n\n## Functions\n\n### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/lifespan.py#L172\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(fn: LifespanFn) -> Lifespan\n```\n\n\nDecorator to create a composable lifespan.\n\nUse this decorator on an async generator function to make it composable\nwith other lifespans using the `|` operator.\n\n**Args:**\n- `fn`: An async generator function that takes a FastMCP server and yields\na dict for the lifespan context.\n\n**Returns:**\n- A composable Lifespan wrapper.\n\n\n## Classes\n\n### `Lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/lifespan.py#L61\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nComposable lifespan wrapper.\n\nWraps an async generator function and enables composition via the `|` operator.\nThe wrapped function should yield a dict that becomes part of the lifespan context.\n\n\n### `ContextManagerLifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/lifespan.py#L110\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nLifespan wrapper for already-wrapped context manager functions.\n\nUse this for functions already decorated with @asynccontextmanager.\n\n\n### `ComposedLifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/lifespan.py#L137\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTwo lifespans composed together.\n\nEnters the left lifespan first, then the right. Exits in reverse order.\nResults are shallow-merged into a single dict.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-low_level.mdx",
    "content": "---\ntitle: low_level\nsidebarTitle: low_level\n---\n\n# `fastmcp.server.low_level`\n\n## Classes\n\n### `MiddlewareServerSession` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nServerSession that routes initialization requests through FastMCP middleware.\n\n\n**Methods:**\n\n#### `fastmcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L48\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfastmcp(self) -> FastMCP\n```\n\nGet the FastMCP instance.\n\n\n#### `client_supports_extension` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L55\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclient_supports_extension(self, extension_id: str) -> bool\n```\n\nCheck if the connected client supports a given MCP extension.\n\nInspects the ``extensions`` extra field on ``ClientCapabilities``\nsent by the client during initialization.\n\n\n### `LowLevelServer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `fastmcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L169\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfastmcp(self) -> FastMCP\n```\n\nGet the FastMCP instance.\n\n\n#### `create_initialization_options` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L176\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, **kwargs: Any) -> InitializationOptions\n```\n\n#### `get_capabilities` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L191\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_capabilities(self, notification_options: NotificationOptions, experimental_capabilities: dict[str, dict[str, Any]]) -> mcp.types.ServerCapabilities\n```\n\nOverride to set capabilities.tasks as a first-class field per SEP-1686.\n\nThis ensures task capabilities appear in capabilities.tasks instead of\ncapabilities.experimental.tasks, which is required by the MCP spec and\nenables proper task detection by clients like VS Code Copilot 1.107+.\n\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L225\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], write_stream: MemoryObjectSendStream[SessionMessage], initialization_options: InitializationOptions, raise_exceptions: bool = False, stateless: bool = False)\n```\n\nOverrides the run method to use the MiddlewareServerSession.\n\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L261\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self) -> Callable[[Callable[[AnyUrl], Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult]]], Callable[[AnyUrl], Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult]]]\n```\n\nDecorator for registering a read_resource handler with CreateTaskResult support.\n\nThe MCP SDK's read_resource decorator does not support returning CreateTaskResult\nfor background task execution. This decorator wraps the result in ServerResult.\n\nThis decorator can be removed once the MCP SDK adds native CreateTaskResult support\nfor resources.\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/low_level.py#L305\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_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]]]\n```\n\nDecorator for registering a get_prompt handler with CreateTaskResult support.\n\nThe MCP SDK's get_prompt decorator does not support returning CreateTaskResult\nfor background task execution. This decorator wraps the result in ServerResult.\n\nThis decorator can be removed once the MCP SDK adds native CreateTaskResult support\nfor prompts.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.middleware`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-authorization.mdx",
    "content": "---\ntitle: authorization\nsidebarTitle: authorization\n---\n\n# `fastmcp.server.middleware.authorization`\n\n\nAuthorization middleware for FastMCP.\n\nThis module provides middleware-based authorization using callable auth checks.\nAuthMiddleware applies auth checks globally to all components on the server.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth import require_scopes, restrict_tag\n    from fastmcp.server.middleware import AuthMiddleware\n\n    # Require specific scope for all components\n    mcp = FastMCP(middleware=[\n        AuthMiddleware(auth=require_scopes(\"api\"))\n    ])\n\n    # Tag-based: components tagged \"admin\" require \"admin\" scope\n    mcp = FastMCP(middleware=[\n        AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))\n    ])\n    ```\n\n\n## Classes\n\n### `AuthMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L51\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nGlobal authorization middleware using callable checks.\n\nThis middleware applies auth checks to all components (tools, resources,\nprompts) on the server. It uses the same callable API as component-level\nauth checks.\n\nThe middleware:\n- Filters tools/resources/prompts from list responses based on auth checks\n- Checks auth before tool execution, resource read, and prompt render\n- Skips all auth checks for STDIO transport (no OAuth concept)\n\n**Args:**\n- `auth`: A single auth check function or list of check functions.\nAll checks must pass for authorization to succeed (AND logic).\n\n\n**Methods:**\n\n#### `on_list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L85\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]\n```\n\nFilter tools/list response based on auth checks.\n\n\n#### `on_call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L113\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult\n```\n\nCheck auth before tool execution.\n\n\n#### `on_list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L156\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resources(self, context: MiddlewareContext[mt.ListResourcesRequest], call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]\n```\n\nFilter resources/list response based on auth checks.\n\n\n#### `on_read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L183\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_read_resource(self, context: MiddlewareContext[mt.ReadResourceRequestParams], call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult]) -> ResourceResult\n```\n\nCheck auth before resource read.\n\n\n#### `on_list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L226\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]\n```\n\nFilter resource templates/list response based on auth checks.\n\n\n#### `on_list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L255\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_prompts(self, context: MiddlewareContext[mt.ListPromptsRequest], call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]\n```\n\nFilter prompts/list response based on auth checks.\n\n\n#### `on_get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/authorization.py#L282\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_get_prompt(self, context: MiddlewareContext[mt.GetPromptRequestParams], call_next: CallNext[mt.GetPromptRequestParams, PromptResult]) -> PromptResult\n```\n\nCheck auth before prompt render.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-caching.mdx",
    "content": "---\ntitle: caching\nsidebarTitle: caching\n---\n\n# `fastmcp.server.middleware.caching`\n\n\nA middleware for response caching.\n\n## Classes\n\n### `CachableResourceContent` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA wrapper for ResourceContent that can be cached.\n\n\n### `CachableResourceResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA wrapper for ResourceResult that can be cached.\n\n\n**Methods:**\n\n#### `get_size` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L53\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_size(self) -> int\n```\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L57\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, value: ResourceResult) -> Self\n```\n\n#### `unwrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nunwrap(self) -> ResourceResult\n```\n\n### `CachableToolResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L80\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L86\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, value: ToolResult) -> Self\n```\n\n#### `unwrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L93\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nunwrap(self) -> ToolResult\n```\n\n### `CachableMessage` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L101\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA wrapper for Message that can be cached.\n\n\n### `CachablePromptResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L113\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA wrapper for PromptResult that can be cached.\n\n\n**Methods:**\n\n#### `get_size` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L120\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_size(self) -> int\n```\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L124\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, value: PromptResult) -> Self\n```\n\n#### `unwrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L133\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nunwrap(self) -> PromptResult\n```\n\n### `SharedMethodSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L144\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nShared config for a cache method.\n\n\n### `ListToolsSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L151\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration options for Tool-related caching.\n\n\n### `ListResourcesSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration options for Resource-related caching.\n\n\n### `ListPromptsSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L159\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration options for Prompt-related caching.\n\n\n### `CallToolSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L163\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration options for Tool-related caching.\n\n\n### `ReadResourceSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L170\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration options for Resource-related caching.\n\n\n### `GetPromptSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L174\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration options for Prompt-related caching.\n\n\n### `ResponseCachingStatistics` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L178\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n### `ResponseCachingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L187\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nThe response caching middleware offers a simple way to cache responses to mcp methods. The Middleware\nsupports cache invalidation via notifications from the server. The Middleware implements TTL-based caching\nbut cache implementations may offer additional features like LRU eviction, size limits, and more.\n\nWhen items are retrieved from the cache they will no longer be the original objects, but rather no-op objects\nthis means that response caching may not be compatible with other middleware that expects original subclasses.\n\nNotes:\n- Caches `tools/call`, `resources/read`, `prompts/get`, `tools/list`, `resources/list`, and `prompts/list` requests.\n- Cache keys are derived from method name and arguments.\n\n\n**Methods:**\n\n#### `on_list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L291\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]\n```\n\nList tools from the cache, if caching is enabled, and the result is in the cache. Otherwise,\notherwise call the next middleware and store the result in the cache if caching is enabled.\n\n\n#### `on_list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L330\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resources(self, context: MiddlewareContext[mcp.types.ListResourcesRequest], call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]\n```\n\nList resources from the cache, if caching is enabled, and the result is in the cache. Otherwise,\notherwise call the next middleware and store the result in the cache if caching is enabled.\n\n\n#### `on_list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L369\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_prompts(self, context: MiddlewareContext[mcp.types.ListPromptsRequest], call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]\n```\n\nList prompts from the cache, if caching is enabled, and the result is in the cache. Otherwise,\notherwise call the next middleware and store the result in the cache if caching is enabled.\n\n\n#### `on_call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L406\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult\n```\n\nCall a tool from the cache, if caching is enabled, and the result is in the cache. Otherwise,\notherwise call the next middleware and store the result in the cache if caching is enabled.\n\n\n#### `on_read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L439\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_read_resource(self, context: MiddlewareContext[mcp.types.ReadResourceRequestParams], call_next: CallNext[mcp.types.ReadResourceRequestParams, ResourceResult]) -> ResourceResult\n```\n\nRead a resource from the cache, if caching is enabled, and the result is in the cache. Otherwise,\notherwise call the next middleware and store the result in the cache if caching is enabled.\n\n\n#### `on_get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L467\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_get_prompt(self, context: MiddlewareContext[mcp.types.GetPromptRequestParams], call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult]) -> PromptResult\n```\n\nGet a prompt from the cache, if caching is enabled, and the result is in the cache. Otherwise,\notherwise call the next middleware and store the result in the cache if caching is enabled.\n\n\n#### `statistics` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L505\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nstatistics(self) -> ResponseCachingStatistics\n```\n\nGet the statistics for the cache.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-dereference.mdx",
    "content": "---\ntitle: dereference\nsidebarTitle: dereference\n---\n\n# `fastmcp.server.middleware.dereference`\n\n\nMiddleware that dereferences $ref in JSON schemas before sending to clients.\n\n## Classes\n\n### `DereferenceRefsMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDereferences $ref in component schemas before sending to clients.\n\nSome MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref\nproperly. This middleware inlines all $ref definitions so schemas are\nself-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.\n\n\n**Methods:**\n\n#### `on_list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L24\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]\n```\n\n#### `on_list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-error_handling.mdx",
    "content": "---\ntitle: error_handling\nsidebarTitle: error_handling\n---\n\n# `fastmcp.server.middleware.error_handling`\n\n\nError handling middleware for consistent error responses and tracking.\n\n## Classes\n\n### `ErrorHandlingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/error_handling.py#L18\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that provides consistent error handling and logging.\n\nCatches exceptions, logs them appropriately, and converts them to\nproper MCP error responses. Also tracks error patterns for monitoring.\n\n\n**Methods:**\n\n#### `on_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/error_handling.py#L120\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_message(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nHandle errors for all messages.\n\n\n#### `get_error_stats` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/error_handling.py#L131\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_error_stats(self) -> dict[str, int]\n```\n\nGet error statistics for monitoring.\n\n\n### `RetryMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/error_handling.py#L136\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that implements automatic retry logic for failed requests.\n\nRetries requests that fail with transient errors, using exponential\nbackoff to avoid overwhelming the server or external dependencies.\n\n\n**Methods:**\n\n#### `on_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/error_handling.py#L192\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_request(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nImplement retry logic for requests.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-logging.mdx",
    "content": "---\ntitle: logging\nsidebarTitle: logging\n---\n\n# `fastmcp.server.middleware.logging`\n\n\nComprehensive logging middleware for FastMCP servers.\n\n## Functions\n\n### `default_serializer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_serializer(data: Any) -> str\n```\n\n\nThe default serializer for Payloads in the logging middleware.\n\n\n## Classes\n\n### `BaseLoggingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for logging middleware.\n\n\n**Methods:**\n\n#### `on_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L124\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any\n```\n\nLog messages for configured methods.\n\n\n### `LoggingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L148\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that provides comprehensive request and response logging.\n\nLogs all MCP messages with configurable detail levels. Useful for debugging,\nmonitoring, and understanding server usage patterns.\n\n\n### `StructuredLoggingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L203\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that provides structured JSON logging for better log analysis.\n\nOutputs structured logs that are easier to parse and analyze with log\naggregation tools like ELK stack, Splunk, or cloud logging services.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-middleware.mdx",
    "content": "---\ntitle: middleware\nsidebarTitle: middleware\n---\n\n# `fastmcp.server.middleware.middleware`\n\n## Functions\n\n### `make_middleware_wrapper` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L66\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmake_middleware_wrapper(middleware: Middleware, call_next: CallNext[T, R]) -> CallNext[T, R]\n```\n\n\nCreate a wrapper that applies a single middleware to a context. The\nclosure bakes in the middleware and call_next function, so it can be\npassed to other functions that expect a call_next function.\n\n\n## Classes\n\n### `CallNext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n### `MiddlewareContext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nUnified context for all middleware operations.\n\n\n**Methods:**\n\n#### `copy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L62\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncopy(self, **kwargs: Any) -> MiddlewareContext[T]\n```\n\n### `Middleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L79\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for FastMCP middleware with dispatching hooks.\n\n\n**Methods:**\n\n#### `on_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L128\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any\n```\n\n#### `on_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L135\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_request(self, context: MiddlewareContext[mt.Request[Any, Any]], call_next: CallNext[mt.Request[Any, Any], Any]) -> Any\n```\n\n#### `on_notification` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L142\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_notification(self, context: MiddlewareContext[mt.Notification[Any, Any]], call_next: CallNext[mt.Notification[Any, Any], Any]) -> Any\n```\n\n#### `on_initialize` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L149\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_initialize(self, context: MiddlewareContext[mt.InitializeRequest], call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None]) -> mt.InitializeResult | None\n```\n\n#### `on_call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L156\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult\n```\n\n#### `on_read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L163\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_read_resource(self, context: MiddlewareContext[mt.ReadResourceRequestParams], call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult]) -> ResourceResult\n```\n\n#### `on_get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L170\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_get_prompt(self, context: MiddlewareContext[mt.GetPromptRequestParams], call_next: CallNext[mt.GetPromptRequestParams, PromptResult]) -> PromptResult\n```\n\n#### `on_list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L177\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]\n```\n\n#### `on_list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L184\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resources(self, context: MiddlewareContext[mt.ListResourcesRequest], call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]\n```\n\n#### `on_list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L191\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]\n```\n\n#### `on_list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/middleware.py#L200\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_prompts(self, context: MiddlewareContext[mt.ListPromptsRequest], call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-ping.mdx",
    "content": "---\ntitle: ping\nsidebarTitle: ping\n---\n\n# `fastmcp.server.middleware.ping`\n\n\nPing middleware for keeping client connections alive.\n\n## Classes\n\n### `PingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/ping.py#L10\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that sends periodic pings to keep client connections alive.\n\nStarts a background ping task on first message from each session. The task\nsends server-to-client pings at the configured interval until the session\nends.\n\n\n**Methods:**\n\n#### `on_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/ping.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_message(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nStart ping task on first message from a session.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx",
    "content": "---\ntitle: rate_limiting\nsidebarTitle: rate_limiting\n---\n\n# `fastmcp.server.middleware.rate_limiting`\n\n\nRate limiting middleware for protecting FastMCP servers from abuse.\n\n## Classes\n\n### `RateLimitError` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nError raised when rate limit is exceeded.\n\n\n### `TokenBucketRateLimiter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nToken bucket implementation for rate limiting.\n\n\n**Methods:**\n\n#### `consume` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconsume(self, tokens: int = 1) -> bool\n```\n\nTry to consume tokens from the bucket.\n\n**Args:**\n- `tokens`: Number of tokens to consume\n\n**Returns:**\n- True if tokens were available and consumed, False otherwise\n\n\n### `SlidingWindowRateLimiter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L61\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSliding window rate limiter implementation.\n\n\n**Methods:**\n\n#### `is_allowed` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_allowed(self) -> bool\n```\n\nCheck if a request is allowed.\n\n\n### `RateLimitingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L92\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that implements rate limiting to prevent server abuse.\n\nUses a token bucket algorithm by default, allowing for burst traffic\nwhile maintaining a sustainable long-term rate.\n\n\n**Methods:**\n\n#### `on_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L152\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_request(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nApply rate limiting to requests.\n\n\n### `SlidingWindowRateLimitingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L170\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that implements sliding window rate limiting.\n\nUses a sliding window approach which provides more precise rate limiting\nbut uses more memory to track individual request timestamps.\n\n\n**Methods:**\n\n#### `on_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/rate_limiting.py#L219\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_request(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nApply sliding window rate limiting to requests.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx",
    "content": "---\ntitle: response_limiting\nsidebarTitle: response_limiting\n---\n\n# `fastmcp.server.middleware.response_limiting`\n\n\nResponse limiting middleware for controlling tool response sizes.\n\n## Classes\n\n### `ResponseLimitingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/response_limiting.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that limits the response size of tool calls.\n\nIntercepts tool call responses and enforces size limits. If a response\nexceeds the limit, it extracts text content, truncates it, and returns\na single TextContent block.\n\n\n**Methods:**\n\n#### `on_call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/response_limiting.py#L93\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult\n```\n\nIntercept tool calls and limit response size.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-timing.mdx",
    "content": "---\ntitle: timing\nsidebarTitle: timing\n---\n\n# `fastmcp.server.middleware.timing`\n\n\nTiming middleware for measuring and logging request performance.\n\n## Classes\n\n### `TimingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L10\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMiddleware that logs the execution time of requests.\n\nOnly measures and logs timing for request messages (not notifications).\nProvides insights into performance characteristics of your MCP server.\n\n\n**Methods:**\n\n#### `on_request` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_request(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime request execution and log the results.\n\n\n### `DetailedTimingMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L60\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nEnhanced timing middleware with per-operation breakdowns.\n\nProvides detailed timing information for different types of MCP operations,\nallowing you to identify performance bottlenecks in specific operations.\n\n\n**Methods:**\n\n#### `on_call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L111\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_call_tool(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime tool execution.\n\n\n#### `on_read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L118\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_read_resource(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime resource reading.\n\n\n#### `on_get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L127\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_get_prompt(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime prompt retrieval.\n\n\n#### `on_list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L134\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_tools(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime tool listing.\n\n\n#### `on_list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L140\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resources(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime resource listing.\n\n\n#### `on_list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L146\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_resource_templates(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime resource template listing.\n\n\n#### `on_list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/timing.py#L152\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_prompts(self, context: MiddlewareContext, call_next: CallNext) -> Any\n```\n\nTime prompt listing.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx",
    "content": "---\ntitle: tool_injection\nsidebarTitle: tool_injection\n---\n\n# `fastmcp.server.middleware.tool_injection`\n\n\nA middleware for injecting tools into the MCP server context.\n\n## Functions\n\n### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L56\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(context: Context) -> list[Prompt]\n```\n\n\nList prompts available on the server.\n\n\n### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L66\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(context: Context, name: Annotated[str, 'The name of the prompt to render.'], arguments: Annotated[dict[str, Any] | None, 'The arguments to pass to the prompt.'] = None) -> mcp.types.GetPromptResult\n```\n\n\nRender a prompt available on the server.\n\n\n### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L101\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(context: Context) -> list[mcp.types.Resource]\n```\n\n\nList resources available on the server.\n\n\n### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L111\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(context: Context, uri: Annotated[AnyUrl | str, 'The URI of the resource to read.']) -> ResourceResult\n```\n\n\nRead a resource available on the server.\n\n\n## Classes\n\n### `ToolInjectionMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA middleware for injecting tools into the context.\n\n\n**Methods:**\n\n#### `on_list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]\n```\n\nInject tools into the response.\n\n\n#### `on_call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\non_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult\n```\n\nIntercept tool calls to injected tools.\n\n\n### `PromptToolMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L82\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA middleware for injecting prompts as tools into the context.\n\n.. deprecated::\n    Use ``fastmcp.server.transforms.PromptsAsTools`` instead.\n\n\n### `ResourceToolMiddleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/middleware/tool_injection.py#L124\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA middleware for injecting resources as tools into the context.\n\n.. deprecated::\n    Use ``fastmcp.server.transforms.ResourcesAsTools`` instead.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-mixins-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.mixins`\n\n\nServer mixins for FastMCP.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-mixins-lifespan.mdx",
    "content": "---\ntitle: lifespan\nsidebarTitle: lifespan\n---\n\n# `fastmcp.server.mixins.lifespan`\n\n\nLifespan and Docket task infrastructure for FastMCP Server.\n\n## Classes\n\n### `LifespanMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/lifespan.py#L25\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing lifespan and Docket task infrastructure for FastMCP.\n\n\n**Methods:**\n\n#### `docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/lifespan.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndocket(self: FastMCP) -> Docket | None\n```\n\nGet the Docket instance if Docket support is enabled.\n\nReturns None if Docket is not enabled or server hasn't been started yet.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx",
    "content": "---\ntitle: mcp_operations\nsidebarTitle: mcp_operations\n---\n\n# `fastmcp.server.mixins.mcp_operations`\n\n\nMCP protocol handler setup and wire-format handlers for FastMCP Server.\n\n## Classes\n\n### `MCPOperationsMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/mcp_operations.py#L44\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing MCP protocol handler setup and wire-format handlers.\n\nNote: Methods registered with SDK decorators (e.g., _list_tools_mcp, _call_tool_mcp)\ncannot use `self: FastMCP` type hints because the SDK's `get_type_hints()` fails\nto resolve FastMCP at runtime (it's only available under TYPE_CHECKING). When\ntype hints fail to resolve, the SDK falls back to calling handlers with no arguments.\nThese methods use untyped `self` to avoid this issue.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-mixins-transport.mdx",
    "content": "---\ntitle: transport\nsidebarTitle: transport\n---\n\n# `fastmcp.server.mixins.transport`\n\n\nTransport-related methods for FastMCP Server.\n\n## Classes\n\n### `TransportMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L37\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin providing transport-related methods for FastMCP.\n\nIncludes HTTP/stdio/SSE transport handling and custom HTTP routes.\n\n\n**Methods:**\n\n#### `run_async` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_async(self: FastMCP, transport: Transport | None = None, show_banner: bool | None = None, **transport_kwargs: Any) -> None\n```\n\nRun the FastMCP server asynchronously.\n\n**Args:**\n- `transport`: Transport protocol to use (\"stdio\", \"http\", \"sse\", or \"streamable-http\")\n- `show_banner`: Whether to display the server banner. If None, uses the\nFASTMCP_SHOW_SERVER_BANNER setting (default\\: True).\n\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L77\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self: FastMCP, transport: Transport | None = None, show_banner: bool | None = None, **transport_kwargs: Any) -> None\n```\n\nRun the FastMCP server. Note this is a synchronous function.\n\n**Args:**\n- `transport`: Transport protocol to use (\"http\", \"stdio\", \"sse\", or \"streamable-http\")\n- `show_banner`: Whether to display the server banner. If None, uses the\nFASTMCP_SHOW_SERVER_BANNER setting (default\\: True).\n\n\n#### `custom_route` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L100\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncustom_route(self: FastMCP, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) -> Callable[[Callable[[Request], Awaitable[Response]]], Callable[[Request], Awaitable[Response]]]\n```\n\nDecorator to register a custom HTTP route on the FastMCP server.\n\nAllows adding arbitrary HTTP endpoints outside the standard MCP protocol,\nwhich can be useful for OAuth callbacks, health checks, or admin APIs.\nThe handler function must be an async function that accepts a Starlette\nRequest and returns a Response.\n\n**Args:**\n- `path`: URL path for the route (e.g., \"/auth/callback\")\n- `methods`: List of HTTP methods to support (e.g., [\"GET\", \"POST\"])\n- `name`: Optional name for the route (to reference this route with\nStarlette's reverse URL lookup feature)\n- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True\n\n\n#### `run_stdio_async` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L184\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_stdio_async(self: FastMCP, show_banner: bool = True, log_level: str | None = None, stateless: bool = False) -> None\n```\n\nRun the server using stdio transport.\n\n**Args:**\n- `show_banner`: Whether to display the server banner\n- `log_level`: Log level for the server\n- `stateless`: Whether to run in stateless mode (no session initialization)\n\n\n#### `run_http_async` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L226\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_http_async(self: FastMCP, show_banner: bool = True, transport: Literal['http', 'streamable-http', 'sse'] = 'http', host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, stateless: bool | None = None) -> None\n```\n\nRun the server using HTTP transport.\n\n**Args:**\n- `transport`: Transport protocol to use - \"http\" (default), \"streamable-http\", or \"sse\"\n- `host`: Host address to bind to (defaults to settings.host)\n- `port`: Port to bind to (defaults to settings.port)\n- `log_level`: Log level for the server (defaults to settings.log_level)\n- `path`: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)\n- `uvicorn_config`: Additional configuration for the Uvicorn server\n- `middleware`: A list of middleware to apply to the app\n- `json_response`: Whether to use JSON response format (defaults to settings.json_response)\n- `stateless_http`: Whether to use stateless HTTP (defaults to settings.stateless_http)\n- `stateless`: Alias for stateless_http for CLI consistency\n\n\n#### `http_app` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L305\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nhttp_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\n```\n\nCreate a Starlette app using the specified HTTP transport.\n\n**Args:**\n- `path`: The path for the HTTP endpoint\n- `middleware`: A list of middleware to apply to the app\n- `json_response`: Whether to use JSON response format\n- `stateless_http`: Whether to use stateless mode (new transport per request)\n- `transport`: Transport protocol to use - \"http\", \"streamable-http\", or \"sse\"\n- `event_store`: Optional event store for SSE polling/resumability. When set,\nenables clients to reconnect and resume receiving events after\nserver-initiated disconnections. Only used with streamable-http transport.\n- `retry_interval`: Optional retry interval in milliseconds for SSE polling.\nControls how quickly clients should reconnect after server-initiated\ndisconnections. Requires event_store to be set. Only used with\nstreamable-http transport.\n\n**Returns:**\n- A Starlette application configured with the specified transport\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-openapi-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.openapi`\n\n\nOpenAPI server implementation for FastMCP.\n\n.. deprecated::\n    This module is deprecated. Import from fastmcp.server.providers.openapi instead.\n\nThe recommended approach is to use OpenAPIProvider with FastMCP:\n\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.openapi import OpenAPIProvider\n    import httpx\n\n    client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n    provider = OpenAPIProvider(openapi_spec=spec, client=client)\n\n    mcp = FastMCP(\"My API Server\")\n    mcp.add_provider(provider)\n\nFastMCPOpenAPI is still available but deprecated.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-openapi-components.mdx",
    "content": "---\ntitle: components\nsidebarTitle: components\n---\n\n# `fastmcp.server.openapi.components`\n\n\nOpenAPI component implementations - backwards compatibility stub.\n\nThis module is deprecated. Import from fastmcp.server.providers.openapi instead.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-openapi-routing.mdx",
    "content": "---\ntitle: routing\nsidebarTitle: routing\n---\n\n# `fastmcp.server.openapi.routing`\n\n\nRoute mapping logic for OpenAPI operations.\n\n.. deprecated::\n    This module is deprecated. Import from fastmcp.server.providers.openapi instead.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-openapi-server.mdx",
    "content": "---\ntitle: server\nsidebarTitle: server\n---\n\n# `fastmcp.server.openapi.server`\n\n\nFastMCPOpenAPI - backwards compatibility wrapper.\n\nThis class is deprecated. Use FastMCP with OpenAPIProvider instead:\n\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.openapi import OpenAPIProvider\n    import httpx\n\n    client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n    provider = OpenAPIProvider(openapi_spec=spec, client=client)\n    mcp = FastMCP(\"My API Server\", providers=[provider])\n\n\n## Classes\n\n### `FastMCPOpenAPI` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/openapi/server.py#L30\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nFastMCP server implementation that creates components from an OpenAPI schema.\n\n.. deprecated::\n    Use FastMCP with OpenAPIProvider instead. This class will be\n    removed in a future version.\n\nExample (deprecated):\n    ```python\n    from fastmcp.server.openapi import FastMCPOpenAPI\n    import httpx\n\n    server = FastMCPOpenAPI(\n        openapi_spec=spec,\n        client=httpx.AsyncClient(),\n    )\n    ```\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.providers`\n\n\nProviders for dynamic MCP components.\n\nThis module provides the `Provider` abstraction for providing tools,\nresources, and prompts dynamically at runtime.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.providers import Provider\n    from fastmcp.tools import Tool\n\n    class DatabaseProvider(Provider):\n        def __init__(self, db_url: str):\n            self.db = Database(db_url)\n\n        async def _list_tools(self) -> list[Tool]:\n            rows = await self.db.fetch(\"SELECT * FROM tools\")\n            return [self._make_tool(row) for row in rows]\n\n        async def _get_tool(self, name: str) -> Tool | None:\n            row = await self.db.fetchone(\"SELECT * FROM tools WHERE name = ?\", name)\n            return self._make_tool(row) if row else None\n\n    mcp = FastMCP(\"Server\", providers=[DatabaseProvider(db_url)])\n    ```\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-aggregate.mdx",
    "content": "---\ntitle: aggregate\nsidebarTitle: aggregate\n---\n\n# `fastmcp.server.providers.aggregate`\n\n\nAggregateProvider for combining multiple providers into one.\n\nThis module provides `AggregateProvider`, a utility class that presents\nmultiple providers as a single unified provider. Useful when you want to\ncombine custom providers without creating a full FastMCP server.\n\nExample:\n    ```python\n    from fastmcp.server.providers import AggregateProvider\n\n    # Combine multiple providers into one\n    combined = AggregateProvider()\n    combined.add_provider(provider1)\n    combined.add_provider(provider2, namespace=\"api\")  # Tools become \"api_foo\"\n\n    # Use like any other provider\n    tools = await combined.list_tools()\n    ```\n\n\n## Classes\n\n### `AggregateProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/aggregate.py#L46\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nUtility provider that combines multiple providers into one.\n\nComponents are aggregated from all providers. For get_* operations,\nproviders are queried in parallel and the highest version is returned.\n\nWhen adding providers with a namespace, wrap_transform() is used to apply\nthe Namespace transform. This means namespace transformation is handled\nby the wrapped provider, not by AggregateProvider.\n\nErrors from individual providers are logged and skipped (graceful degradation).\n\n\n**Methods:**\n\n#### `add_provider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/aggregate.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_provider(self, provider: Provider) -> None\n```\n\nAdd a provider with optional namespace.\n\nIf the provider is a FastMCP server, it's automatically wrapped in\nFastMCPProvider to ensure middleware is invoked correctly.\n\n**Args:**\n- `provider`: The provider to add.\n- `namespace`: Optional namespace prefix. When set\\:\n- Tools become \"namespace_toolname\"\n- Resources become \"protocol\\://namespace/path\"\n- Prompts become \"namespace_promptname\"\n\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/aggregate.py#L243\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nGet all task-eligible components from all providers.\n\n\n#### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/aggregate.py#L256\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(self) -> AsyncIterator[None]\n```\n\nCombine lifespans of all providers.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.server.providers.base`\n\n\nBase Provider class for dynamic MCP components.\n\nThis module provides the `Provider` abstraction for providing tools,\nresources, and prompts dynamically at runtime.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.providers import Provider\n    from fastmcp.tools import Tool\n\n    class DatabaseProvider(Provider):\n        def __init__(self, db_url: str):\n            super().__init__()\n            self.db = Database(db_url)\n\n        async def _list_tools(self) -> list[Tool]:\n            rows = await self.db.fetch(\"SELECT * FROM tools\")\n            return [self._make_tool(row) for row in rows]\n\n        async def _get_tool(self, name: str) -> Tool | None:\n            row = await self.db.fetchone(\"SELECT * FROM tools WHERE name = ?\", name)\n            return self._make_tool(row) if row else None\n\n    mcp = FastMCP(\"Server\", providers=[DatabaseProvider(db_url)])\n    ```\n\n\n## Classes\n\n### `Provider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L51\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for dynamic component providers.\n\nSubclass and override whichever methods you need. Default implementations\nreturn empty lists / None, so you only need to implement what your provider\nsupports.\n\n\n**Methods:**\n\n#### `transforms` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransforms(self) -> list[Transform]\n```\n\nAll transforms applied to components from this provider.\n\n\n#### `add_transform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L80\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_transform(self, transform: Transform) -> None\n```\n\nAdd a transform to this provider.\n\nTransforms modify components (tools, resources, prompts) as they flow\nthrough the provider. They're applied in order - first added is innermost.\n\n**Args:**\n- `transform`: The transform to add.\n\n\n#### `wrap_transform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L100\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap_transform(self, transform: Transform) -> Provider\n```\n\nReturn a new provider with this transform applied (immutable).\n\nUnlike add_transform() which mutates this provider, wrap_transform()\nreturns a new provider that wraps this one. The original provider\nis unchanged.\n\nThis is useful when you want to apply transforms without side effects,\nsuch as adding the same provider to multiple aggregators with different\nnamespaces.\n\n**Args:**\n- `transform`: The transform to apply.\n\n**Returns:**\n- A new provider that wraps this one with the transform applied.\n\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L136\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self) -> Sequence[Tool]\n```\n\nList tools with all transforms applied.\n\nApplies transforms sequentially: base → transforms (in order).\nEach transform receives the result from the previous transform.\nComponents may be marked as disabled but are NOT filtered here -\nfiltering happens at the server level to allow session transforms to override.\n\n**Returns:**\n- Transformed sequence of tools (including disabled ones).\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L152\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None\n```\n\nGet tool by transformed name with all transforms applied.\n\nNote: This method does NOT filter disabled components. The Server\n(FastMCP) performs enabled filtering after all transforms complete,\nallowing session-level transforms to override provider-level disables.\n\n**Args:**\n- `name`: The transformed tool name to look up.\n- `version`: Optional version filter. If None, returns highest version.\n\n**Returns:**\n- The tool if found (may be marked disabled), None if not found.\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L178\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self) -> Sequence[Resource]\n```\n\nList resources with all transforms applied.\n\nComponents may be marked as disabled but are NOT filtered here.\n\n\n#### `get_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L188\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None\n```\n\nGet resource by transformed URI with all transforms applied.\n\nNote: This method does NOT filter disabled components. The Server\n(FastMCP) performs enabled filtering after all transforms complete.\n\n**Args:**\n- `uri`: The transformed resource URI to look up.\n- `version`: Optional version filter. If None, returns highest version.\n\n**Returns:**\n- The resource if found (may be marked disabled), None if not found.\n\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L213\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self) -> Sequence[ResourceTemplate]\n```\n\nList resource templates with all transforms applied.\n\nComponents may be marked as disabled but are NOT filtered here.\n\n\n#### `get_resource_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L223\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None\n```\n\nGet resource template by transformed URI with all transforms applied.\n\nNote: This method does NOT filter disabled components. The Server\n(FastMCP) performs enabled filtering after all transforms complete.\n\n**Args:**\n- `uri`: The transformed template URI to look up.\n- `version`: Optional version filter. If None, returns highest version.\n\n**Returns:**\n- The template if found (may be marked disabled), None if not found.\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L250\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self) -> Sequence[Prompt]\n```\n\nList prompts with all transforms applied.\n\nComponents may be marked as disabled but are NOT filtered here.\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L260\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None\n```\n\nGet prompt by transformed name with all transforms applied.\n\nNote: This method does NOT filter disabled components. The Server\n(FastMCP) performs enabled filtering after all transforms complete.\n\n**Args:**\n- `name`: The transformed prompt name to look up.\n- `version`: Optional version filter. If None, returns highest version.\n\n**Returns:**\n- The prompt if found (may be marked disabled), None if not found.\n\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L418\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nReturn components that should be registered as background tasks.\n\nOverride to customize which components are task-eligible.\nDefault calls list_* methods, applies provider transforms, and filters\nfor components with task_config.mode != 'forbidden'.\n\nUsed by the server during startup to register functions with Docket.\n\n\n#### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L463\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(self) -> AsyncIterator[None]\n```\n\nUser-overridable lifespan for custom setup and teardown.\n\nOverride this method to perform provider-specific initialization\nlike opening database connections, setting up external resources,\nor other state management needed for the provider's lifetime.\n\nThe lifespan scope matches the server's lifespan - code before yield\nruns at startup, code after yield runs at shutdown.\n\n\n#### `enable` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L492\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nenable(self) -> Self\n```\n\nEnable components matching all specified criteria.\n\nAdds a visibility transform that marks matching components as enabled.\nLater transforms override earlier ones, so enable after disable makes\nthe component enabled.\n\nWith only=True, switches to allowlist mode - first disables everything,\nthen enables matching components.\n\n**Args:**\n- `names`: Component names or URIs to enable.\n- `keys`: Component keys to enable (e.g., {\"tool\\:my_tool@v1\"}).\n- `version`: Component version spec to enable (e.g., VersionSpec(eq=\"v1\") or\nVersionSpec(gte=\"v2\")). Unversioned components will not match.\n- `tags`: Enable components with these tags.\n- `components`: Component types to include (e.g., {\"tool\", \"prompt\"}).\n- `only`: If True, ONLY enable matching components (allowlist mode).\n\n**Returns:**\n- Self for method chaining.\n\n\n#### `disable` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/base.py#L541\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndisable(self) -> Self\n```\n\nDisable components matching all specified criteria.\n\nAdds a visibility transform that marks matching components as disabled.\nComponents can be re-enabled by calling enable() with matching criteria\n(the later transform wins).\n\n**Args:**\n- `names`: Component names or URIs to disable.\n- `keys`: Component keys to disable (e.g., {\"tool\\:my_tool@v1\"}).\n- `version`: Component version spec to disable (e.g., VersionSpec(eq=\"v1\") or\nVersionSpec(gte=\"v2\")). Unversioned components will not match.\n- `tags`: Disable components with these tags.\n- `components`: Component types to include (e.g., {\"tool\", \"prompt\"}).\n\n**Returns:**\n- Self for method chaining.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx",
    "content": "---\ntitle: fastmcp_provider\nsidebarTitle: fastmcp_provider\n---\n\n# `fastmcp.server.providers.fastmcp_provider`\n\n\nFastMCPProvider for wrapping FastMCP servers as providers.\n\nThis module provides the `FastMCPProvider` class that wraps a FastMCP server\nand exposes its components through the Provider interface.\n\nIt also provides FastMCPProvider* component classes that delegate execution to\nthe wrapped server's middleware, ensuring middleware runs when components are\nexecuted.\n\n\n## Classes\n\n### `FastMCPProviderTool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L72\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTool that delegates execution to a wrapped server's middleware.\n\nWhen `run()` is called, this tool invokes the wrapped server's\n`_call_tool_middleware()` method, ensuring the server's middleware\nchain is executed.\n\n\n**Methods:**\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L94\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool\n```\n\nWrap a Tool to delegate execution to the server's middleware.\n\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L147\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any]) -> ToolResult\n```\n\nDelegate to child server's call_tool() without task_meta.\n\nThis is called when the tool is used within a TransformedTool\nforwarding function or other contexts where task_meta is not available.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L166\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `FastMCPProviderResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L173\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResource that delegates reading to a wrapped server's read_resource().\n\nWhen `read()` is called, this resource invokes the wrapped server's\n`read_resource()` method, ensuring the server's middleware chain is executed.\n\n\n**Methods:**\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L194\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource\n```\n\nWrap a Resource to delegate reading to the server's middleware.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L237\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `FastMCPProviderPrompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L244\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nPrompt that delegates rendering to a wrapped server's render_prompt().\n\nWhen `render()` is called, this prompt invokes the wrapped server's\n`render_prompt()` method, ensuring the server's middleware chain is executed.\n\n\n**Methods:**\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L265\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt\n```\n\nWrap a Prompt to delegate rendering to the server's middleware.\n\n\n#### `render` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L316\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender(self, arguments: dict[str, Any] | None = None) -> PromptResult\n```\n\nDelegate to child server's render_prompt() without task_meta.\n\nThis is called when the prompt is used within a transformed context\nor other contexts where task_meta is not available.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L335\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `FastMCPProviderResourceTemplate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L342\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResource template that creates FastMCPProviderResources.\n\nWhen `create_resource()` is called, this template creates a\nFastMCPProviderResource that will invoke the wrapped server's middleware\nwhen read.\n\n\n**Methods:**\n\n#### `wrap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L364\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwrap(cls, server: Any, template: ResourceTemplate) -> FastMCPProviderResourceTemplate\n```\n\nWrap a ResourceTemplate to create FastMCPProviderResources.\n\n\n#### `create_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L385\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_resource(self, uri: str, params: dict[str, Any]) -> Resource\n```\n\nCreate a FastMCPProviderResource for the given URI.\n\nThe `uri` is the external/transformed URI (e.g., with namespace prefix).\nWe use `_original_uri_template` with `params` to construct the internal\nURI that the nested server understands.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L435\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult\n```\n\nRead the resource content for background task execution.\n\nReads the resource via the wrapped server and returns the ResourceResult.\nThis method is called by Docket during background task execution.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L456\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nNo-op: the child's actual template is registered via get_tasks().\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L459\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution\n```\n\nSchedule this template for background execution via docket.\n\nThe child's FunctionResourceTemplate.fn is registered (via get_tasks),\nand it expects splatted **kwargs, so we splat params here.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L478\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `FastMCPProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L490\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider that wraps a FastMCP server.\n\nThis provider enables mounting one FastMCP server onto another, exposing\nthe mounted server's tools, resources, and prompts through the parent\nserver.\n\nComponents returned by this provider are wrapped in FastMCPProvider*\nclasses that delegate execution to the wrapped server's middleware chain.\nThis ensures middleware runs when components are executed.\n\n\n**Methods:**\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L655\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nReturn task-eligible components from the mounted server.\n\nReturns the child's ACTUAL components (not wrapped) so their actual\nfunctions get registered with Docket. Gets components with child\nserver's transforms applied, then applies this provider's transforms\nfor correct registration keys.\n\n\n#### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/fastmcp_provider.py#L696\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(self) -> AsyncIterator[None]\n```\n\nStart the mounted server's user lifespan.\n\nThis starts only the wrapped server's user-defined lifespan, NOT its\nfull _lifespan_manager() (which includes Docket). The parent server's\nDocket handles all background tasks.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-filesystem.mdx",
    "content": "---\ntitle: filesystem\nsidebarTitle: filesystem\n---\n\n# `fastmcp.server.providers.filesystem`\n\n\nFileSystemProvider for filesystem-based component discovery.\n\nFileSystemProvider scans a directory for Python files, imports them, and\nregisters any Tool, Resource, ResourceTemplate, or Prompt objects found.\n\nComponents are created using the standalone decorators from fastmcp.tools,\nfastmcp.resources, and fastmcp.prompts:\n\nExample:\n    ```python\n    # In mcp/tools.py\n    from fastmcp.tools import tool\n\n    @tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    # In main.py\n    from pathlib import Path\n\n    from fastmcp import FastMCP\n    from fastmcp.server.providers import FileSystemProvider\n\n    mcp = FastMCP(\"MyServer\", providers=[FileSystemProvider(Path(__file__).parent / \"mcp\")])\n    ```\n\n\n## Classes\n\n### `FileSystemProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/filesystem.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider that discovers components from the filesystem.\n\nScans a directory for Python files and registers any Tool, Resource,\nResourceTemplate, or Prompt objects found. Components are created using\nthe standalone decorators:\n- @tool from fastmcp.tools\n- @resource from fastmcp.resources\n- @prompt from fastmcp.prompts\n\n**Args:**\n- `root`: Root directory to scan. Defaults to current directory.\n- `reload`: If True, re-scan files on every request (dev mode).\nDefaults to False (scan once at init, cache results).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx",
    "content": "---\ntitle: filesystem_discovery\nsidebarTitle: filesystem_discovery\n---\n\n# `fastmcp.server.providers.filesystem_discovery`\n\n\nFile discovery and module import utilities for filesystem-based routing.\n\nThis module provides functions to:\n1. Discover Python files in a directory tree\n2. Import modules (as packages if __init__.py exists, else directly)\n3. Extract decorated components (Tool, Resource, Prompt objects) from imported modules\n\n\n## Functions\n\n### `discover_files` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/filesystem_discovery.py#L32\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndiscover_files(root: Path) -> list[Path]\n```\n\n\nRecursively discover all Python files under a directory.\n\nExcludes __init__.py files (they're for package structure, not components).\n\n**Args:**\n- `root`: Root directory to scan.\n\n**Returns:**\n- List of .py file paths, sorted for deterministic order.\n\n\n### `import_module_from_file` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/filesystem_discovery.py#L109\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nimport_module_from_file(file_path: Path) -> ModuleType\n```\n\n\nImport a Python file as a module.\n\nIf the file is part of a package (directory has __init__.py), imports\nit as a proper package member (relative imports work). Otherwise,\nimports directly using spec_from_file_location.\n\n**Args:**\n- `file_path`: Path to the Python file.\n\n**Returns:**\n- The imported module.\n\n**Raises:**\n- `ImportError`: If the module cannot be imported.\n\n\n### `extract_components` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/filesystem_discovery.py#L175\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nextract_components(module: ModuleType) -> list[FastMCPComponent]\n```\n\n\nExtract all MCP components from a module.\n\nScans all module attributes for instances of Tool, Resource,\nResourceTemplate, or Prompt objects created by standalone decorators,\nor functions decorated with @tool/@resource/@prompt that have __fastmcp__ metadata.\n\n**Args:**\n- `module`: The imported module to scan.\n\n**Returns:**\n- List of component objects (Tool, Resource, ResourceTemplate, Prompt).\n\n\n### `discover_and_import` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/filesystem_discovery.py#L299\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndiscover_and_import(root: Path) -> DiscoveryResult\n```\n\n\nDiscover files, import modules, and extract components.\n\nThis is the main entry point for filesystem-based discovery.\n\n**Args:**\n- `root`: Root directory to scan.\n\n**Returns:**\n- DiscoveryResult with components and any failed files.\n\n\n## Classes\n\n### `DiscoveryResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/filesystem_discovery.py#L24\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResult of filesystem discovery.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-local_provider-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.providers.local_provider`\n\n\nLocalProvider for locally-defined MCP components.\n\nThis module provides the `LocalProvider` class that manages tools, resources,\ntemplates, and prompts registered via decorators or direct methods.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-local_provider-decorators-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.providers.local_provider.decorators`\n\n\nDecorator mixins for LocalProvider.\n\nThis module provides mixin classes that add decorator functionality\nto LocalProvider for tools, resources, templates, and prompts.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-local_provider-decorators-prompts.mdx",
    "content": "---\ntitle: prompts\nsidebarTitle: prompts\n---\n\n# `fastmcp.server.providers.local_provider.decorators.prompts`\n\n\nPrompt decorator mixin for LocalProvider.\n\nThis module provides the PromptDecoratorMixin class that adds prompt\nregistration functionality to LocalProvider.\n\n\n## Classes\n\n### `PromptDecoratorMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/prompts.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin class providing prompt decorator functionality for LocalProvider.\n\nThis mixin contains all methods related to:\n- Prompt registration via add_prompt()\n- Prompt decorator (@provider.prompt)\n\n\n**Methods:**\n\n#### `add_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/prompts.py#L37\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_prompt(self: LocalProvider, prompt: Prompt | Callable[..., Any]) -> Prompt\n```\n\nAdd a prompt to this provider's storage.\n\nAccepts either a Prompt object or a decorated function with __fastmcp__ metadata.\n\n\n#### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/prompts.py#L74\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(self: LocalProvider, name_or_fn: F) -> F\n```\n\n#### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/prompts.py#L91\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[F], F]\n```\n\n#### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/prompts.py#L107\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]\n```\n\nDecorator to register a prompt.\n\nThis decorator supports multiple calling patterns:\n- @provider.prompt (without parentheses)\n- @provider.prompt() (with empty parentheses)\n- @provider.prompt(\"custom_name\") (with name as first argument)\n- @provider.prompt(name=\"custom_name\") (with name as keyword argument)\n- provider.prompt(function, name=\"custom_name\") (direct function call)\n\n**Args:**\n- `name_or_fn`: Either a function (when used as @prompt), a string name, or None\n- `name`: Optional name for the prompt (keyword-only, alternative to name_or_fn)\n- `title`: Optional title for the prompt\n- `description`: Optional description of what the prompt does\n- `icons`: Optional icons for the prompt\n- `tags`: Optional set of tags for categorizing the prompt\n- `enabled`: Whether the prompt is enabled (default True). If False, adds to blocklist.\n- `meta`: Optional meta information about the prompt\n- `task`: Optional task configuration for background execution\n- `auth`: Optional authorization checks for the prompt\n\n**Returns:**\n- The registered FunctionPrompt or a decorator function.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-local_provider-decorators-resources.mdx",
    "content": "---\ntitle: resources\nsidebarTitle: resources\n---\n\n# `fastmcp.server.providers.local_provider.decorators.resources`\n\n\nResource decorator mixin for LocalProvider.\n\nThis module provides the ResourceDecoratorMixin class that adds resource\nand template registration functionality to LocalProvider.\n\n\n## Classes\n\n### `ResourceDecoratorMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/resources.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin class providing resource decorator functionality for LocalProvider.\n\nThis mixin contains all methods related to:\n- Resource registration via add_resource()\n- Resource template registration via add_template()\n- Resource decorator (@provider.resource)\n\n\n**Methods:**\n\n#### `add_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/resources.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_resource(self: LocalProvider, resource: Resource | ResourceTemplate | Callable[..., Any]) -> Resource | ResourceTemplate\n```\n\nAdd a resource to this provider's storage.\n\nAccepts either a Resource/ResourceTemplate object or a decorated function with __fastmcp__ metadata.\n\n\n#### `add_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/resources.py#L101\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_template(self: LocalProvider, template: ResourceTemplate) -> ResourceTemplate\n```\n\nAdd a resource template to this provider's storage.\n\n\n#### `resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/resources.py#L107\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresource(self: LocalProvider, uri: str) -> Callable[[F], F]\n```\n\nDecorator to register a function as a resource.\n\nIf the URI contains parameters (e.g. \"resource://{param}\") or the function\nhas parameters, it will be registered as a template resource.\n\n**Args:**\n- `uri`: URI for the resource (e.g. \"resource\\://my-resource\" or \"resource\\://{param}\")\n- `name`: Optional name for the resource\n- `title`: Optional title for the resource\n- `description`: Optional description of the resource\n- `icons`: Optional icons for the resource\n- `mime_type`: Optional MIME type for the resource\n- `tags`: Optional set of tags for categorizing the resource\n- `enabled`: Whether the resource is enabled (default True). If False, adds to blocklist.\n- `annotations`: Optional annotations about the resource's behavior\n- `meta`: Optional meta information about the resource\n- `task`: Optional task configuration for background execution\n- `auth`: Optional authorization checks for the resource\n\n**Returns:**\n- A decorator function.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx",
    "content": "---\ntitle: tools\nsidebarTitle: tools\n---\n\n# `fastmcp.server.providers.local_provider.decorators.tools`\n\n\nTool decorator mixin for LocalProvider.\n\nThis module provides the ToolDecoratorMixin class that adds tool\nregistration functionality to LocalProvider.\n\n\n## Classes\n\n### `ToolDecoratorMixin` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L146\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMixin class providing tool decorator functionality for LocalProvider.\n\nThis mixin contains all methods related to:\n- Tool registration via add_tool()\n- Tool decorator (@provider.tool)\n\n\n**Methods:**\n\n#### `add_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L154\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_tool(self: LocalProvider, tool: Tool | Callable[..., Any]) -> Tool\n```\n\nAdd a tool to this provider's storage.\n\nAccepts either a Tool object or a decorated function with __fastmcp__ metadata.\n\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L206\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self: LocalProvider, name_or_fn: F) -> F\n```\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L228\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[F], F]\n```\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L253\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]\n```\n\nDecorator to register a tool.\n\nThis decorator supports multiple calling patterns:\n- @provider.tool (without parentheses)\n- @provider.tool() (with empty parentheses)\n- @provider.tool(\"custom_name\") (with name as first argument)\n- @provider.tool(name=\"custom_name\") (with name as keyword argument)\n- provider.tool(function, name=\"custom_name\") (direct function call)\n\n**Args:**\n- `name_or_fn`: Either a function (when used as @tool), a string name, or None\n- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn)\n- `title`: Optional title for the tool\n- `description`: Optional description of what the tool does\n- `icons`: Optional icons for the tool\n- `tags`: Optional set of tags for categorizing the tool\n- `output_schema`: Optional JSON schema for the tool's output\n- `annotations`: Optional annotations about the tool's behavior\n- `exclude_args`: Optional list of argument names to exclude from the tool schema\n- `meta`: Optional meta information about the tool\n- `enabled`: Whether the tool is enabled (default True). If False, adds to blocklist.\n- `task`: Optional task configuration for background execution\n- `serializer`: Deprecated. Return ToolResult from your tools for full control over serialization.\n\n**Returns:**\n- The registered FunctionTool or a decorator function.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-local_provider-local_provider.mdx",
    "content": "---\ntitle: local_provider\nsidebarTitle: local_provider\n---\n\n# `fastmcp.server.providers.local_provider.local_provider`\n\n\nLocalProvider for locally-defined MCP components.\n\nThis module provides the `LocalProvider` class that manages tools, resources,\ntemplates, and prompts registered via decorators or direct methods.\n\nLocalProvider can be used standalone and attached to multiple servers:\n\n```python\nfrom fastmcp.server.providers import LocalProvider\n\n# Create a reusable provider with tools\nprovider = LocalProvider()\n\n@provider.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# Attach to any server\nfrom fastmcp import FastMCP\nserver1 = FastMCP(\"Server1\", providers=[provider])\nserver2 = FastMCP(\"Server2\", providers=[provider])\n```\n\n\n## Classes\n\n### `LocalProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/local_provider.py#L51\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider for locally-defined components.\n\nSupports decorator-based registration (`@provider.tool`, `@provider.resource`,\n`@provider.prompt`) and direct object registration methods.\n\nWhen used standalone, LocalProvider uses default settings. When attached\nto a FastMCP server via the server's decorators, server-level settings\nlike `_tool_serializer` and `_support_tasks_by_default` are injected.\n\n\n**Methods:**\n\n#### `remove_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/local_provider.py#L229\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nremove_tool(self, name: str, version: str | None = None) -> None\n```\n\nRemove tool(s) from this provider's storage.\n\n**Args:**\n- `name`: The tool name.\n- `version`: If None, removes ALL versions. If specified, removes only that version.\n\n**Raises:**\n- `KeyError`: If no matching tool is found.\n\n\n#### `remove_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/local_provider.py#L257\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nremove_resource(self, uri: str, version: str | None = None) -> None\n```\n\nRemove resource(s) from this provider's storage.\n\n**Args:**\n- `uri`: The resource URI.\n- `version`: If None, removes ALL versions. If specified, removes only that version.\n\n**Raises:**\n- `KeyError`: If no matching resource is found.\n\n\n#### `remove_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/local_provider.py#L285\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nremove_template(self, uri_template: str, version: str | None = None) -> None\n```\n\nRemove resource template(s) from this provider's storage.\n\n**Args:**\n- `uri_template`: The template URI pattern.\n- `version`: If None, removes ALL versions. If specified, removes only that version.\n\n**Raises:**\n- `KeyError`: If no matching template is found.\n\n\n#### `remove_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/local_provider.py#L315\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nremove_prompt(self, name: str, version: str | None = None) -> None\n```\n\nRemove prompt(s) from this provider's storage.\n\n**Args:**\n- `name`: The prompt name.\n- `version`: If None, removes ALL versions. If specified, removes only that version.\n\n**Raises:**\n- `KeyError`: If no matching prompt is found.\n\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/local_provider.py#L449\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nReturn components eligible for background task execution.\n\nReturns components that have task_config.mode != 'forbidden'.\nThis includes both FunctionTool/Resource/Prompt instances created via\ndecorators and custom Tool/Resource/Prompt subclasses.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-openapi-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.providers.openapi`\n\n\nOpenAPI provider for FastMCP.\n\nThis module provides OpenAPI integration for FastMCP through the Provider pattern.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.openapi import OpenAPIProvider\n    import httpx\n\n    client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n    provider = OpenAPIProvider(openapi_spec=spec, client=client)\n    mcp = FastMCP(\"API Server\", providers=[provider])\n    ```\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-openapi-components.mdx",
    "content": "---\ntitle: components\nsidebarTitle: components\n---\n\n# `fastmcp.server.providers.openapi.components`\n\n\nOpenAPI component classes: Tool, Resource, and ResourceTemplate.\n\n## Classes\n\n### `OpenAPITool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L137\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTool implementation for OpenAPI endpoints.\n\n\n**Methods:**\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L179\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any]) -> ToolResult\n```\n\nExecute the HTTP request using RequestDirector.\n\n\n### `OpenAPIResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L256\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResource implementation for OpenAPI endpoints.\n\n\n**Methods:**\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L286\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nFetch the resource data by making an HTTP request.\n\n\n### `OpenAPIResourceTemplate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L370\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResource template implementation for OpenAPI endpoints.\n\n\n**Methods:**\n\n#### `create_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L402\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource\n```\n\nCreate a resource with the given parameters.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx",
    "content": "---\ntitle: provider\nsidebarTitle: provider\n---\n\n# `fastmcp.server.providers.openapi.provider`\n\n\nOpenAPIProvider for creating MCP components from OpenAPI specifications.\n\n## Classes\n\n### `OpenAPIProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L51\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider that creates MCP components from an OpenAPI specification.\n\nComponents are created eagerly during initialization by parsing the OpenAPI\nspec. Each component makes HTTP calls to the described API endpoints.\n\n\n**Methods:**\n\n#### `lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L181\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlifespan(self) -> AsyncIterator[None]\n```\n\nManage the lifecycle of the auto-created httpx client.\n\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L431\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nReturn empty list - OpenAPI components don't support tasks.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-openapi-routing.mdx",
    "content": "---\ntitle: routing\nsidebarTitle: routing\n---\n\n# `fastmcp.server.providers.openapi.routing`\n\n\nRoute mapping logic for OpenAPI operations.\n\n## Classes\n\n### `MCPType` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/routing.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nType of FastMCP component to create from a route.\n\n\n### `RouteMap` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/openapi/routing.py#L59\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMapping configuration for HTTP routes to FastMCP component types.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-proxy.mdx",
    "content": "---\ntitle: proxy\nsidebarTitle: proxy\n---\n\n# `fastmcp.server.providers.proxy`\n\n\nProxyProvider for proxying to remote MCP servers.\n\nThis module provides the `ProxyProvider` class that proxies components from\na remote MCP server via a client factory. It also provides proxy component\nclasses that forward execution to remote servers.\n\n\n## Functions\n\n### `default_proxy_roots_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L842\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList\n```\n\n\nForward list roots request from remote server to proxy's connected clients.\n\n\n### `default_proxy_sampling_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L850\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_proxy_sampling_handler(messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult\n```\n\n\nForward sampling request from remote server to proxy's connected clients.\n\n\n### `default_proxy_elicitation_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L873\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_proxy_elicitation_handler(message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult\n```\n\n\nForward elicitation request from remote server to proxy's connected clients.\n\n\n### `default_proxy_log_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L895\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_proxy_log_handler(message: LogMessage) -> None\n```\n\n\nForward log notification from remote server to proxy's connected clients.\n\n\n### `default_proxy_progress_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L903\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_proxy_progress_handler(progress: float, total: float | None, message: str | None) -> None\n```\n\n\nForward progress notification from remote server to proxy's connected clients.\n\n\n## Classes\n\n### `ProxyTool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L69\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA Tool that represents and executes a tool on a remote server.\n\n\n**Methods:**\n\n#### `model_copy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L86\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmodel_copy(self, **kwargs: Any) -> ProxyTool\n```\n\nOverride to preserve _backend_name when name changes.\n\n\n#### `from_mcp_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L96\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_mcp_tool(cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool) -> ProxyTool\n```\n\nFactory method to create a ProxyTool from a raw MCP tool schema.\n\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L113\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult\n```\n\nExecutes the tool by making a call through the client.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L168\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `ProxyResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L175\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA Resource that represents and reads a resource from a remote server.\n\n\n**Methods:**\n\n#### `model_copy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L200\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmodel_copy(self, **kwargs: Any) -> ProxyResource\n```\n\nOverride to preserve _backend_uri when uri changes.\n\n\n#### `from_mcp_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L210\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_mcp_resource(cls, client_factory: ClientFactoryT, mcp_resource: mcp.types.Resource) -> ProxyResource\n```\n\nFactory method to create a ProxyResource from a raw MCP resource schema.\n\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L230\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> ResourceResult\n```\n\nRead the resource content from the remote server.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L275\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `ProxyTemplate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L282\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA ResourceTemplate that represents and creates resources from a remote server template.\n\n\n**Methods:**\n\n#### `model_copy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L299\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmodel_copy(self, **kwargs: Any) -> ProxyTemplate\n```\n\nOverride to preserve _backend_uri_template when uri_template changes.\n\n\n#### `from_mcp_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L309\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_mcp_template(cls, client_factory: ClientFactoryT, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate\n```\n\nFactory method to create a ProxyTemplate from a raw MCP template schema.\n\n\n#### `create_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L328\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource\n```\n\nCreate a resource from the template by calling the remote server.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L390\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `ProxyPrompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L397\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA Prompt that represents and renders a prompt from a remote server.\n\n\n**Methods:**\n\n#### `model_copy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L414\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmodel_copy(self, **kwargs: Any) -> ProxyPrompt\n```\n\nOverride to preserve _backend_name when name changes.\n\n\n#### `from_mcp_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L424\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_mcp_prompt(cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt\n```\n\nFactory method to create a ProxyPrompt from a raw MCP prompt schema.\n\n\n#### `render` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L448\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender(self, arguments: dict[str, Any]) -> PromptResult\n```\n\nRender the prompt by making a call through the client.\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L470\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\n### `ProxyProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L498\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider that proxies to a remote MCP server via a client factory.\n\nThis provider fetches components from a remote server and returns Proxy*\ncomponent instances that forward execution to the remote server.\n\nAll components returned by this provider have task_config.mode=\"forbidden\"\nbecause tasks cannot be executed through a proxy.\n\nComponent lists (tools, resources, templates, prompts) are cached so that\nindividual lookups (e.g. during ``call_tool``) can resolve from the cache\ninstead of opening a new backend connection.  The cache stores the\nbackend's raw component metadata and is shared across all sessions;\nper-session visibility and auth filtering are applied after cache lookup\nby the server layer.  The cache is refreshed whenever a ``list_*`` call\nis made, and entries expire after ``cache_ttl`` seconds (default 300).\nSet ``cache_ttl=0`` to disable caching.  Disabling is recommended for\nbackends whose component lists change dynamically.\n\n\n**Methods:**\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L714\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nReturn empty list since proxy components don't support tasks.\n\nOverride the base implementation to avoid calling list_tools() during\nserver lifespan initialization, which would open the client before any\ncontext is set. All Proxy* components have task_config.mode=\"forbidden\".\n\n\n### `FastMCPProxy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L795\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA FastMCP server that acts as a proxy to a remote MCP-compliant server.\n\nThis is a convenience wrapper that creates a FastMCP server with a\nProxyProvider. For more control, use FastMCP with add_provider(ProxyProvider(...)).\n\n\n### `ProxyClient` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L967\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.\n\nSupports forwarding roots, sampling, elicitation, logging, and progress.\n\n\n### `StatefulProxyClient` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L1000\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA proxy client that provides a stateful client factory for the proxy server.\n\nThe stateful proxy client bound its copy to the server session.\nAnd it will be disconnected when the session is exited.\n\nThis is useful to proxy a stateful mcp server such as the Playwright MCP server.\nNote that it is essential to ensure that the proxy server itself is also stateful.\n\nBecause session reuse means the receive-loop task inherits a stale\n``request_ctx`` ContextVar snapshot, the default proxy handlers are\nreplaced with versions that restore the ContextVar before forwarding.\n``ProxyTool.run`` stashes the current ``RequestContext`` in\n``_proxy_rc_ref`` before each backend call, and the handlers consult\nit to detect (and correct) staleness.\n\n\n**Methods:**\n\n#### `clear` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L1051\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclear(self)\n```\n\nClear all cached clients and force disconnect them.\n\n\n#### `new_stateful` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L1057\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nnew_stateful(self) -> Client[ClientTransportT]\n```\n\nCreate a new stateful proxy client instance with the same configuration.\n\nUse this method as the client factory for stateful proxy server.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-skills-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.providers.skills`\n\n\nSkills providers for exposing agent skills as MCP resources.\n\nThis module provides a two-layer architecture for skill discovery:\n\n- **SkillProvider**: Handles a single skill folder, exposing its files as resources.\n- **SkillsDirectoryProvider**: Scans a directory, creates a SkillProvider per folder.\n- **Vendor providers**: Platform-specific providers for Claude, Cursor, VS Code, Codex,\n  Gemini, Goose, Copilot, and OpenCode.\n\nExample:\n    ```python\n    from pathlib import Path\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.skills import ClaudeSkillsProvider, SkillProvider\n\n    mcp = FastMCP(\"Skills Server\")\n\n    # Load a single skill\n    mcp.add_provider(SkillProvider(Path.home() / \".claude/skills/pdf-processing\"))\n\n    # Or load all skills in a directory\n    mcp.add_provider(ClaudeSkillsProvider())  # Uses ~/.claude/skills/\n    ```\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-skills-claude_provider.mdx",
    "content": "---\ntitle: claude_provider\nsidebarTitle: claude_provider\n---\n\n# `fastmcp.server.providers.skills.claude_provider`\n\n\nClaude-specific skills provider for Claude Code skills.\n\n## Classes\n\n### `ClaudeSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/claude_provider.py#L11\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider for Claude Code skills from ~/.claude/skills/.\n\nA convenience subclass that sets the default root to Claude's skills location.\n\n**Args:**\n- `reload`: If True, re-scan on every request. Defaults to False.\n- `supporting_files`: How supporting files are exposed\\:\n- \"template\"\\: Accessed via ResourceTemplate, hidden from list_resources().\n- \"resources\"\\: Each file exposed as individual Resource in list_resources().\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-skills-directory_provider.mdx",
    "content": "---\ntitle: directory_provider\nsidebarTitle: directory_provider\n---\n\n# `fastmcp.server.providers.skills.directory_provider`\n\n\nDirectory scanning provider for discovering multiple skills.\n\n## Classes\n\n### `SkillsDirectoryProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/directory_provider.py#L19\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider that scans directories and creates a SkillProvider per skill folder.\n\nThis extends AggregateProvider to combine multiple SkillProviders into one.\nEach subdirectory containing a main file (default: SKILL.md) becomes a skill.\nCan scan multiple root directories - if a skill name appears in multiple roots,\nthe first one found wins.\n\n**Args:**\n- `roots`: Root directory(ies) containing skill folders. Can be a single path\nor a sequence of paths.\n- `reload`: If True, re-discover skills on each request. Defaults to False.\n- `main_file_name`: Name of the main skill file. Defaults to \"SKILL.md\".\n- `supporting_files`: How supporting files are exposed in child SkillProviders\\:\n- \"template\"\\: Accessed via ResourceTemplate, hidden from list_resources().\n- \"resources\"\\: Each file exposed as individual Resource in list_resources().\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx",
    "content": "---\ntitle: skill_provider\nsidebarTitle: skill_provider\n---\n\n# `fastmcp.server.providers.skills.skill_provider`\n\n\nBasic skill provider for handling a single skill folder.\n\n## Classes\n\n### `SkillResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L35\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource representing a skill's main file or manifest.\n\n\n**Methods:**\n\n#### `get_meta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L41\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_meta(self) -> dict[str, Any]\n```\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L50\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> str | bytes | ResourceResult\n```\n\nRead the resource content.\n\n\n### `SkillFileTemplate` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L70\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA template for accessing files within a skill.\n\n\n**Methods:**\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L75\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult\n```\n\nRead a file from the skill directory.\n\n\n#### `create_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L115\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_resource(self, uri: str, params: dict[str, Any]) -> Resource\n```\n\nCreate a resource for the given URI and parameters.\n\nNote: This is not typically used since _read() handles file reading directly.\nProvided for compatibility with the ResourceTemplate interface.\n\n\n### `SkillFileResource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L141\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA resource representing a specific file within a skill.\n\n\n**Methods:**\n\n#### `get_meta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L147\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_meta(self) -> dict[str, Any]\n```\n\n#### `read` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread(self) -> str | bytes | ResourceResult\n```\n\nRead the file content.\n\n\n### `SkillProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L179\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvider that exposes a single skill folder as MCP resources.\n\nEach skill folder must contain a main file (default: SKILL.md) and may\ncontain additional supporting files.\n\nExposes:\n- A Resource for the main file (skill://{name}/SKILL.md)\n- A Resource for the synthetic manifest (skill://{name}/_manifest)\n- Supporting files via ResourceTemplate or Resources (configurable)\n\n**Args:**\n- `skill_path`: Path to the skill directory.\n- `main_file_name`: Name of the main skill file. Defaults to \"SKILL.md\".\n- `supporting_files`: How supporting files (everything except main file and\nmanifest) are exposed to clients\\:\n- \"template\"\\: Accessed via ResourceTemplate, hidden from list_resources().\n  Clients discover files by reading the manifest first.\n- \"resources\"\\: Each file exposed as individual Resource in list_resources().\n  Full enumeration upfront.\n\n\n**Methods:**\n\n#### `skill_info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L271\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nskill_info(self) -> SkillInfo\n```\n\nGet the loaded skill info.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-skills-vendor_providers.mdx",
    "content": "---\ntitle: vendor_providers\nsidebarTitle: vendor_providers\n---\n\n# `fastmcp.server.providers.skills.vendor_providers`\n\n\nVendor-specific skills providers for various AI coding platforms.\n\n## Classes\n\n### `CursorSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L11\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCursor skills from ~/.cursor/skills/.\n\n\n### `VSCodeSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nVS Code skills from ~/.copilot/skills/.\n\n\n### `CodexSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nCodex skills from /etc/codex/skills/ and ~/.codex/skills/.\n\nScans both system-level and user-level directories. System skills take\nprecedence if duplicates exist.\n\n\n### `GeminiSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L73\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nGemini skills from ~/.gemini/skills/.\n\n\n### `GooseSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L91\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nGoose skills from ~/.config/agents/skills/.\n\n\n### `CopilotSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L109\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nGitHub Copilot skills from ~/.copilot/skills/.\n\n\n### `OpenCodeSkillsProvider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/vendor_providers.py#L127\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOpenCode skills from ~/.config/opencode/skills/.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-providers-wrapped_provider.mdx",
    "content": "---\ntitle: wrapped_provider\nsidebarTitle: wrapped_provider\n---\n\n# `fastmcp.server.providers.wrapped_provider`\n\n\nWrappedProvider for immutable transform composition.\n\nThis module provides `_WrappedProvider`, an internal class that wraps a provider\nwith an additional transform. Created by `Provider.wrap_transform()`.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-proxy.mdx",
    "content": "---\ntitle: proxy\nsidebarTitle: proxy\n---\n\n# `fastmcp.server.proxy`\n\n\nBackwards compatibility - import from fastmcp.server.providers.proxy instead.\n\nThis module re-exports all proxy-related classes from their new location\nat fastmcp.server.providers.proxy. Direct imports from this module are\ndeprecated and will be removed in a future version.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-sampling-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.sampling`\n\n\nSampling module for FastMCP servers.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-sampling-run.mdx",
    "content": "---\ntitle: run\nsidebarTitle: run\n---\n\n# `fastmcp.server.sampling.run`\n\n\nSampling types and helper functions for FastMCP servers.\n\n## Functions\n\n### `determine_handler_mode` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L132\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndetermine_handler_mode(context: Context, needs_tools: bool) -> bool\n```\n\n\nDetermine whether to use fallback handler or client for sampling.\n\n**Args:**\n- `context`: The MCP context.\n- `needs_tools`: Whether the sampling request requires tool support.\n\n**Returns:**\n- True if fallback handler should be used, False to use client.\n\n**Raises:**\n- `ValueError`: If client lacks required capability and no fallback configured.\n\n\n### `call_sampling_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L191\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_sampling_handler(context: Context, messages: list[SamplingMessage]) -> CreateMessageResult | CreateMessageResultWithTools\n```\n\n\nMake LLM call using the fallback handler.\n\nNote: This function expects the caller (sample_step) to have validated that\nsampling_handler is set via determine_handler_mode(). The checks below are\nsafeguards against internal misuse.\n\n\n### `execute_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L244\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nexecute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False, tool_concurrency: int | None = None) -> list[ToolResultContent]\n```\n\n\nExecute tool calls and return results.\n\n**Args:**\n- `tool_calls`: List of tool use requests from the LLM.\n- `tool_map`: Mapping from tool name to SamplingTool.\n- `mask_error_details`: If True, mask detailed error messages from tool execution.\nWhen masked, only generic error messages are returned to the LLM.\nTools can explicitly raise ToolError to bypass masking when they want\nto provide specific error messages to the LLM.\n- `tool_concurrency`: Controls parallel execution of tools\\:\n- None (default)\\: Sequential execution (one at a time)\n- 0\\: Unlimited parallel execution\n- N > 0\\: Execute at most N tools concurrently\nIf any tool has sequential=True, all tools execute sequentially\nregardless of this setting.\n\n**Returns:**\n- List of tool result content blocks in the same order as tool_calls.\n\n\n### `prepare_messages` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L354\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[SamplingMessage]\n```\n\n\nConvert various message formats to a list of SamplingMessage objects.\n\n\n### `prepare_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L373\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare_tools(tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]] | None) -> list[SamplingTool] | None\n```\n\n\nConvert tools to SamplingTool objects.\n\nAccepts SamplingTool instances, FunctionTool instances, TransformedTool instances,\nor plain callable functions. FunctionTool and TransformedTool are converted using\nfrom_callable_tool(), while plain functions use from_function().\n\n**Args:**\n- `tools`: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,\nTransformedTool, or plain callable functions.\n\n**Returns:**\n- List of SamplingTool instances, or None if tools is None.\n\n\n### `extract_tool_calls` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L409\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nextract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools) -> list[ToolUseContent]\n```\n\n\nExtract tool calls from a response.\n\n\n### `create_final_response_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L421\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_final_response_tool(result_type: type) -> SamplingTool\n```\n\n\nCreate a synthetic 'final_response' tool for structured output.\n\nThis tool is used to capture structured responses from the LLM.\nThe tool's schema is derived from the result_type.\n\n\n### `sample_step_impl` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L457\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsample_step_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SampleStep\n```\n\n\nImplementation of Context.sample_step().\n\nMake a single LLM sampling call. This is a stateless function that makes\nexactly one LLM call and optionally executes any requested tools.\n\n\n### `sample_impl` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L574\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsample_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]\n```\n\n\nImplementation of Context.sample().\n\nSend a sampling request to the client and await the response. This method\nruns to completion automatically, executing a tool loop until the LLM\nprovides a final text response.\n\n\n## Classes\n\n### `SamplingResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L54\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResult of a sampling operation.\n\n**Attributes:**\n- `text`: The text representation of the result (raw text or JSON for structured).\n- `result`: The typed result (str for text, parsed object for structured output).\n- `history`: All messages exchanged during sampling.\n\n\n### `SampleStep` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L69\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nResult of a single sampling call.\n\nRepresents what the LLM returned in this step plus the message history.\n\n\n**Methods:**\n\n#### `is_tool_use` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L79\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_tool_use(self) -> bool\n```\n\nTrue if the LLM is requesting tool execution.\n\n\n#### `text` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L86\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntext(self) -> str | None\n```\n\nExtract text from the response, if available.\n\n\n#### `tool_calls` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L99\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool_calls(self) -> list[ToolUseContent]\n```\n\nGet the list of tool calls from the response.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx",
    "content": "---\ntitle: sampling_tool\nsidebarTitle: sampling_tool\n---\n\n# `fastmcp.server.sampling.sampling_tool`\n\n\nSamplingTool for use during LLM sampling requests.\n\n## Classes\n\n### `SamplingTool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA tool that can be used during LLM sampling.\n\nSamplingTools bundle a tool's schema (name, description, parameters) with\nan executor function, enabling servers to execute agentic workflows where\nthe LLM can request tool calls during sampling.\n\nIn most cases, pass functions directly to ctx.sample():\n\n    def search(query: str) -> str:\n        '''Search the web.'''\n        return web_search(query)\n\n    result = await context.sample(\n        messages=\"Find info about Python\",\n        tools=[search],  # Plain functions work directly\n    )\n\nCreate a SamplingTool explicitly when you need custom name/description:\n\n    tool = SamplingTool.from_function(search, name=\"web_search\")\n\n\n**Methods:**\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L54\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any] | None = None) -> Any\n```\n\nExecute the tool with the given arguments.\n\n**Args:**\n- `arguments`: Dictionary of arguments to pass to the tool function.\n\n**Returns:**\n- The result of executing the tool function.\n\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L84\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any]) -> SamplingTool\n```\n\nCreate a SamplingTool from a function.\n\nThe function's signature is analyzed to generate a JSON schema for\nthe tool's parameters. Type hints are used to determine parameter types.\n\n**Args:**\n- `fn`: The function to create a tool from.\n- `name`: Optional name override. Defaults to the function's name.\n- `description`: Optional description override. Defaults to the function's docstring.\n- `sequential`: If True, this tool requires sequential execution and prevents\nparallel execution of all tools in the batch. Set to True for tools\nwith shared state, file writes, or other operations that cannot run\nconcurrently. Defaults to False.\n\n**Returns:**\n- A SamplingTool wrapping the function.\n\n**Raises:**\n- `ValueError`: If the function is a lambda without a name override.\n\n\n#### `from_callable_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L126\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_callable_tool(cls, tool: FunctionTool | TransformedTool) -> SamplingTool\n```\n\nCreate a SamplingTool from a FunctionTool or TransformedTool.\n\nReuses existing server tools in sampling contexts. For TransformedTool,\nthe tool's .run() method is used to ensure proper argument transformation,\nand the ToolResult is automatically unwrapped.\n\n**Args:**\n- `tool`: A FunctionTool or TransformedTool to convert.\n- `name`: Optional name override. Defaults to tool.name.\n- `description`: Optional description override. Defaults to tool.description.\n\n**Raises:**\n- `TypeError`: If the tool is not a FunctionTool or TransformedTool.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-server.mdx",
    "content": "---\ntitle: server\nsidebarTitle: server\n---\n\n# `fastmcp.server.server`\n\n\nFastMCP - A more ergonomic interface for MCP servers.\n\n## Functions\n\n### `default_lifespan` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L171\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]\n```\n\n\nDefault lifespan context manager that does nothing.\n\n**Args:**\n- `server`: The server instance this lifespan is managing\n\n**Returns:**\n- An empty dictionary as the lifespan result.\n\n\n### `create_proxy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L2219\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy\n```\n\n\nCreate a FastMCP proxy server for the given target.\n\nThis is the recommended way to create a proxy server. For lower-level control,\nuse `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`.\n\n**Args:**\n- `target`: The backend to proxy to. Can be\\:\n- A Client instance (connected or disconnected)\n- A ClientTransport\n- A FastMCP server instance\n- A URL string or AnyUrl\n- A Path to a server script\n- An MCPConfig or dict\n- `**settings`: Additional settings passed to FastMCPProxy (name, etc.)\n\n**Returns:**\n- A FastMCPProxy server that proxies to the target.\n\n\n## Classes\n\n### `StateValue` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L207\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nWrapper for stored context state values.\n\n\n### `FastMCP` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L213\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L363\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nname(self) -> str\n```\n\n#### `instructions` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L367\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstructions(self) -> str | None\n```\n\n#### `instructions` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L371\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninstructions(self, value: str | None) -> None\n```\n\n#### `version` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L375\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nversion(self) -> str | None\n```\n\n#### `website_url` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L379\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nwebsite_url(self) -> str | None\n```\n\n#### `icons` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L383\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nicons(self) -> list[mcp.types.Icon]\n```\n\n#### `local_provider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L390\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlocal_provider(self) -> LocalProvider\n```\n\nThe server's local provider, which stores directly-registered components.\n\nUse this to remove components:\n\n    mcp.local_provider.remove_tool(\"my_tool\")\n    mcp.local_provider.remove_resource(\"data://info\")\n    mcp.local_provider.remove_prompt(\"my_prompt\")\n\n\n#### `add_middleware` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L412\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_middleware(self, middleware: Middleware) -> None\n```\n\n#### `add_provider` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L415\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_provider(self, provider: Provider) -> None\n```\n\nAdd a provider for dynamic tools, resources, and prompts.\n\nProviders are queried in registration order. The first provider to return\na non-None result wins. Static components (registered via decorators)\nalways take precedence over providers.\n\n**Args:**\n- `provider`: A Provider instance that will provide components dynamically.\n- `namespace`: Optional namespace prefix. When set\\:\n- Tools become \"namespace_toolname\"\n- Resources become \"protocol\\://namespace/path\"\n- Prompts become \"namespace_promptname\"\n\n\n#### `get_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L437\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tasks(self) -> Sequence[FastMCPComponent]\n```\n\nGet task-eligible components with all transforms applied.\n\nOverrides AggregateProvider.get_tasks() to apply server-level transforms\nafter aggregation. AggregateProvider handles provider-level namespacing.\n\n\n#### `add_transform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L466\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_transform(self, transform: Transform) -> None\n```\n\nAdd a server-level transform.\n\nServer-level transforms are applied after all providers are aggregated.\nThey transform tools, resources, and prompts from ALL providers.\n\n**Args:**\n- `transform`: The transform to add.\n\n\n#### `add_tool_transformation` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L486\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None\n```\n\nAdd a tool transformation.\n\n.. deprecated::\n    Use ``add_transform(ToolTransform({...}))`` instead.\n\n\n#### `remove_tool_transformation` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L503\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nremove_tool_transformation(self, _tool_name: str) -> None\n```\n\nRemove a tool transformation.\n\n.. deprecated::\n    Tool transformations are now immutable. Use enable/disable controls instead.\n\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L518\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self) -> Sequence[Tool]\n```\n\nList all enabled tools from providers.\n\nOverrides Provider.list_tools() to add visibility filtering, auth filtering,\nand middleware execution. Returns all versions (no deduplication).\nProtocol handlers deduplicate for MCP wire format.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L588\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None\n```\n\nGet a tool by name, filtering disabled tools.\n\nOverrides Provider.get_tool() to add visibility filtering after all\ntransforms (including session-level) have been applied. This ensures\nsession transforms can override provider-level disables.\n\nWhen the highest version is disabled and no explicit version was\nrequested, falls back to the next-highest enabled version.\n\n**Args:**\n- `name`: The tool name.\n- `version`: Version filter (None returns highest version).\n\n**Returns:**\n- The tool if found and enabled, None otherwise.\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L641\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self) -> Sequence[Resource]\n```\n\nList all enabled resources from providers.\n\nOverrides Provider.list_resources() to add visibility filtering, auth filtering,\nand middleware execution. Returns all versions (no deduplication).\nProtocol handlers deduplicate for MCP wire format.\n\n\n#### `get_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L713\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None\n```\n\nGet a resource by URI, filtering disabled resources.\n\nOverrides Provider.get_resource() to add visibility filtering after all\ntransforms (including session-level) have been applied.\n\nWhen the highest version is disabled and no explicit version was\nrequested, falls back to the next-highest enabled version.\n\n**Args:**\n- `uri`: The resource URI.\n- `version`: Version filter (None returns highest version).\n\n**Returns:**\n- The resource if found and enabled, None otherwise.\n\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L763\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self) -> Sequence[ResourceTemplate]\n```\n\nList all enabled resource templates from providers.\n\nOverrides Provider.list_resource_templates() to add visibility filtering,\nauth filtering, and middleware execution. Returns all versions (no deduplication).\nProtocol handlers deduplicate for MCP wire format.\n\n\n#### `get_resource_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L837\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None\n```\n\nGet a resource template by URI, filtering disabled templates.\n\nOverrides Provider.get_resource_template() to add visibility filtering after\nall transforms (including session-level) have been applied.\n\nWhen the highest version is disabled and no explicit version was\nrequested, falls back to the next-highest enabled version.\n\n**Args:**\n- `uri`: The template URI.\n- `version`: Version filter (None returns highest version).\n\n**Returns:**\n- The template if found and enabled, None otherwise.\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L891\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self) -> Sequence[Prompt]\n```\n\nList all enabled prompts from providers.\n\nOverrides Provider.list_prompts() to add visibility filtering, auth filtering,\nand middleware execution. Returns all versions (no deduplication).\nProtocol handlers deduplicate for MCP wire format.\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L961\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None\n```\n\nGet a prompt by name, filtering disabled prompts.\n\nOverrides Provider.get_prompt() to add visibility filtering after all\ntransforms (including session-level) have been applied.\n\nWhen the highest version is disabled and no explicit version was\nrequested, falls back to the next-highest enabled version.\n\n**Args:**\n- `name`: The prompt name.\n- `version`: Version filter (None returns highest version).\n\n**Returns:**\n- The prompt if found and enabled, None otherwise.\n\n\n#### `call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1012\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult\n```\n\n#### `call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1023\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult\n```\n\n#### `call_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1033\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult\n```\n\nCall a tool by name.\n\nThis is the public API for executing tools. By default, middleware is applied.\n\n**Args:**\n- `name`: The tool name\n- `arguments`: Tool arguments (optional)\n- `version`: Specific version to call. If None, calls highest version.\n- `run_middleware`: If True (default), apply the middleware chain.\nSet to False when called from middleware to avoid re-applying.\n- `task_meta`: If provided, execute as a background task and return\nCreateTaskResult. If None (default), execute synchronously and\nreturn ToolResult.\n\n**Returns:**\n- ToolResult when task_meta is None.\n- CreateTaskResult when task_meta is provided.\n\n**Raises:**\n- `NotFoundError`: If tool not found or disabled\n- `ToolError`: If tool execution fails\n- `ValidationError`: If arguments fail validation\n\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1148\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self, uri: str) -> ResourceResult\n```\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1158\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self, uri: str) -> mcp.types.CreateTaskResult\n```\n\n#### `read_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1167\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nread_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult\n```\n\nRead a resource by URI.\n\nThis is the public API for reading resources. By default, middleware is applied.\nChecks concrete resources first, then templates.\n\n**Args:**\n- `uri`: The resource URI\n- `version`: Specific version to read. If None, reads highest version.\n- `run_middleware`: If True (default), apply the middleware chain.\nSet to False when called from middleware to avoid re-applying.\n- `task_meta`: If provided, execute as a background task and return\nCreateTaskResult. If None (default), execute synchronously and\nreturn ResourceResult.\n\n**Returns:**\n- ResourceResult when task_meta is None.\n- CreateTaskResult when task_meta is provided.\n\n**Raises:**\n- `NotFoundError`: If resource not found or disabled\n- `ResourceError`: If resource read fails\n\n\n#### `render_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1301\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult\n```\n\n#### `render_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1312\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult\n```\n\n#### `render_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1322\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrender_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult\n```\n\nRender a prompt by name.\n\nThis is the public API for rendering prompts. By default, middleware is applied.\nUse get_prompt() to retrieve the prompt definition without rendering.\n\n**Args:**\n- `name`: The prompt name\n- `arguments`: Prompt arguments (optional)\n- `version`: Specific version to render. If None, renders highest version.\n- `run_middleware`: If True (default), apply the middleware chain.\nSet to False when called from middleware to avoid re-applying.\n- `task_meta`: If provided, execute as a background task and return\nCreateTaskResult. If None (default), execute synchronously and\nreturn PromptResult.\n\n**Returns:**\n- PromptResult when task_meta is None.\n- CreateTaskResult when task_meta is provided.\n\n**Raises:**\n- `NotFoundError`: If prompt not found or disabled\n- `PromptError`: If prompt rendering fails\n\n\n#### `add_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1398\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_tool(self, tool: Tool | Callable[..., Any]) -> Tool\n```\n\nAdd a tool to the server.\n\nThe tool function can optionally request a Context object by adding a parameter\nwith the Context type annotation. See the @tool decorator for examples.\n\n**Args:**\n- `tool`: The Tool instance or @tool-decorated function to register\n\n**Returns:**\n- The tool instance that was added to the server.\n\n\n#### `remove_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1412\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nremove_tool(self, name: str, version: str | None = None) -> None\n```\n\nRemove tool(s) from the server.\n\n.. deprecated::\n    Use ``mcp.local_provider.remove_tool(name)`` instead.\n\n**Args:**\n- `name`: The name of the tool to remove.\n- `version`: If None, removes ALL versions. If specified, removes only that version.\n\n**Raises:**\n- `NotFoundError`: If no matching tool is found.\n\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1442\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self, name_or_fn: F) -> F\n```\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1463\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self, name_or_fn: str | None = None) -> Callable[[F], F]\n```\n\n#### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1483\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]\n```\n\nDecorator to register a tool.\n\nTools can optionally request a Context object by adding a parameter with the\nContext type annotation. The context provides access to MCP capabilities like\nlogging, progress reporting, and resource access.\n\nThis decorator supports multiple calling patterns:\n- @server.tool (without parentheses)\n- @server.tool (with empty parentheses)\n- @server.tool(\"custom_name\") (with name as first argument)\n- @server.tool(name=\"custom_name\") (with name as keyword argument)\n- server.tool(function, name=\"custom_name\") (direct function call)\n\n**Args:**\n- `name_or_fn`: Either a function (when used as @tool), a string name, or None\n- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn)\n- `description`: Optional description of what the tool does\n- `tags`: Optional set of tags for categorizing the tool\n- `output_schema`: Optional JSON schema for the tool's output\n- `annotations`: Optional annotations about the tool's behavior\n- `exclude_args`: Optional list of argument names to exclude from the tool schema.\nDeprecated\\: Use `Depends()` for dependency injection instead.\n- `meta`: Optional meta information about the tool\n\n**Examples:**\n\nRegister a tool with a custom name:\n```python\n@server.tool\ndef my_tool(x: int) -> str:\n    return str(x)\n\n# Register a tool with a custom name\n@server.tool\ndef my_tool(x: int) -> str:\n    return str(x)\n\n@server.tool(\"custom_name\")\ndef my_tool(x: int) -> str:\n    return str(x)\n\n@server.tool(name=\"custom_name\")\ndef my_tool(x: int) -> str:\n    return str(x)\n\n# Direct function call\nserver.tool(my_function, name=\"custom_name\")\n```\n\n\n#### `add_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1582\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate\n```\n\nAdd a resource to the server.\n\n**Args:**\n- `resource`: A Resource instance or @resource-decorated function to add\n\n**Returns:**\n- The resource instance that was added to the server.\n\n\n#### `add_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1595\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_template(self, template: ResourceTemplate) -> ResourceTemplate\n```\n\nAdd a resource template to the server.\n\n**Args:**\n- `template`: A ResourceTemplate instance to add\n\n**Returns:**\n- The template instance that was added to the server.\n\n\n#### `resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1606\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresource(self, uri: str) -> Callable[[F], F]\n```\n\nDecorator to register a function as a resource.\n\nThe function will be called when the resource is read to generate its content.\nThe function can return:\n- str for text content\n- bytes for binary content\n- other types will be converted to JSON\n\nResources can optionally request a Context object by adding a parameter with the\nContext type annotation. The context provides access to MCP capabilities like\nlogging, progress reporting, and session information.\n\nIf the URI contains parameters (e.g. \"resource://{param}\") or the function\nhas parameters, it will be registered as a template resource.\n\n**Args:**\n- `uri`: URI for the resource (e.g. \"resource\\://my-resource\" or \"resource\\://{param}\")\n- `name`: Optional name for the resource\n- `description`: Optional description of the resource\n- `mime_type`: Optional MIME type for the resource\n- `tags`: Optional set of tags for categorizing the resource\n- `annotations`: Optional annotations about the resource's behavior\n- `meta`: Optional meta information about the resource\n\n**Examples:**\n\nRegister a resource with a custom name:\n```python\n@server.resource(\"resource://my-resource\")\ndef get_data() -> str:\n    return \"Hello, world!\"\n\n@server.resource(\"resource://my-resource\")\nasync get_data() -> str:\n    data = await fetch_data()\n    return f\"Hello, world! {data}\"\n\n@server.resource(\"resource://{city}/weather\")\ndef get_weather(city: str) -> str:\n    return f\"Weather for {city}\"\n\n@server.resource(\"resource://{city}/weather\")\nasync def get_weather_with_context(city: str, ctx: Context) -> str:\n    await ctx.info(f\"Fetching weather for {city}\")\n    return f\"Weather for {city}\"\n\n@server.resource(\"resource://{city}/weather\")\nasync def get_weather(city: str) -> str:\n    data = await fetch_weather(city)\n    return f\"Weather for {city}: {data}\"\n```\n\n\n#### `add_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1725\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt\n```\n\nAdd a prompt to the server.\n\n**Args:**\n- `prompt`: A Prompt instance or @prompt-decorated function to add\n\n**Returns:**\n- The prompt instance that was added to the server.\n\n\n#### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1737\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(self, name_or_fn: F) -> F\n```\n\n#### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1753\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(self, name_or_fn: str | None = None) -> Callable[[F], F]\n```\n\n#### `prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1768\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]\n```\n\nDecorator to register a prompt.\n\n        Prompts can optionally request a Context object by adding a parameter with the\n        Context type annotation. The context provides access to MCP capabilities like\n        logging, progress reporting, and session information.\n\n        This decorator supports multiple calling patterns:\n        - @server.prompt (without parentheses)\n        - @server.prompt() (with empty parentheses)\n        - @server.prompt(\"custom_name\") (with name as first argument)\n        - @server.prompt(name=\"custom_name\") (with name as keyword argument)\n        - server.prompt(function, name=\"custom_name\") (direct function call)\n\n        Args:\n            name_or_fn: Either a function (when used as @prompt), a string name, or None\n            name: Optional name for the prompt (keyword-only, alternative to name_or_fn)\n            description: Optional description of what the prompt does\n            tags: Optional set of tags for categorizing the prompt\n            meta: Optional meta information about the prompt\n\n        Examples:\n\n            ```python\n            @server.prompt\n            def analyze_table(table_name: str) -> list[Message]:\n                schema = read_table_schema(table_name)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": f\"Analyze this schema:\n{schema}\"\n                    }\n                ]\n\n            @server.prompt()\n            async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:\n                await ctx.info(f\"Analyzing table {table_name}\")\n                schema = read_table_schema(table_name)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": f\"Analyze this schema:\n{schema}\"\n                    }\n                ]\n\n            @server.prompt(\"custom_name\")\n            async def analyze_file(path: str) -> list[Message]:\n                content = await read_file(path)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": {\n                            \"type\": \"resource\",\n                            \"resource\": {\n                                \"uri\": f\"file://{path}\",\n                                \"text\": content\n                            }\n                        }\n                    }\n                ]\n\n            @server.prompt(name=\"custom_name\")\n            def another_prompt(data: str) -> list[Message]:\n                return [{\"role\": \"user\", \"content\": data}]\n\n            # Direct function call\n            server.prompt(my_function, name=\"custom_name\")\n            ```\n\n\n#### `mount` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1868\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmount(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\n```\n\nMount another FastMCP server on this server with an optional namespace.\n\nUnlike importing (with import_server), mounting establishes a dynamic connection\nbetween servers. When a client interacts with a mounted server's objects through\nthe parent server, requests are forwarded to the mounted server in real-time.\nThis means changes to the mounted server are immediately reflected when accessed\nthrough the parent.\n\nWhen a server is mounted with a namespace:\n- Tools from the mounted server are accessible with namespaced names.\n  Example: If server has a tool named \"get_weather\", it will be available as \"namespace_get_weather\".\n- Resources are accessible with namespaced URIs.\n  Example: If server has a resource with URI \"weather://forecast\", it will be available as\n  \"weather://namespace/forecast\".\n- Templates are accessible with namespaced URI templates.\n  Example: If server has a template with URI \"weather://location/{id}\", it will be available\n  as \"weather://namespace/location/{id}\".\n- Prompts are accessible with namespaced names.\n  Example: If server has a prompt named \"weather_prompt\", it will be available as\n  \"namespace_weather_prompt\".\n\nWhen a server is mounted without a namespace (namespace=None), its tools, resources, templates,\nand prompts are accessible with their original names. Multiple servers can be mounted\nwithout namespaces, and they will be tried in order until a match is found.\n\nThe mounted server's lifespan is executed when the parent server starts, and its\nmiddleware chain is invoked for all operations (tool calls, resource reads, prompts).\n\n**Args:**\n- `server`: The FastMCP server to mount.\n- `namespace`: Optional namespace to use for the mounted server's objects. If None,\nthe server's objects are accessible with their original names.\n- `as_proxy`: Deprecated. Mounted servers now always have their lifespan and\nmiddleware invoked. To create a proxy server, use create_proxy()\nexplicitly before mounting.\n- `tool_names`: Optional mapping of original tool names to custom names. Use this\nto override namespaced names. Keys are the original tool names from the\nmounted server.\n- `prefix`: Deprecated. Use namespace instead.\n\n\n#### `import_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L1962\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nimport_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None\n```\n\nImport the MCP objects from another FastMCP server into this one,\noptionally with a given prefix.\n\n.. deprecated::\n    Use :meth:`mount` instead. ``import_server`` will be removed in a\n    future version.\n\nNote that when a server is *imported*, its objects are immediately\nregistered to the importing server. This is a one-time operation and\nfuture changes to the imported server will not be reflected in the\nimporting server. Server-level configurations and lifespans are not imported.\n\nWhen a server is imported with a prefix:\n- The tools are imported with prefixed names\n  Example: If server has a tool named \"get_weather\", it will be\n  available as \"prefix_get_weather\"\n- The resources are imported with prefixed URIs using the new format\n  Example: If server has a resource with URI \"weather://forecast\", it will\n  be available as \"weather://prefix/forecast\"\n- The templates are imported with prefixed URI templates using the new format\n  Example: If server has a template with URI \"weather://location/{id}\", it will\n  be available as \"weather://prefix/location/{id}\"\n- The prompts are imported with prefixed names\n  Example: If server has a prompt named \"weather_prompt\", it will be available as\n  \"prefix_weather_prompt\"\n\nWhen a server is imported without a prefix (prefix=None), its tools, resources,\ntemplates, and prompts are imported with their original names.\n\n**Args:**\n- `server`: The FastMCP server to import\n- `prefix`: Optional prefix to use for the imported server's objects. If None,\nobjects are imported with their original names.\n\n\n#### `from_openapi` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L2062\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_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\n```\n\nCreate a FastMCP server from an OpenAPI specification.\n\n**Args:**\n- `openapi_spec`: OpenAPI schema as a dictionary\n- `client`: Optional httpx AsyncClient for making HTTP requests.\nIf not provided, a default client is created using the first\nserver URL from the OpenAPI spec with a 30-second timeout.\n- `name`: Name for the MCP server\n- `route_maps`: Optional list of RouteMap objects defining route mappings\n- `route_map_fn`: Optional callable for advanced route type mapping\n- `mcp_component_fn`: Optional callable for component customization\n- `mcp_names`: Optional dictionary mapping operationId to component names\n- `tags`: Optional set of tags to add to all components\n- `validate_output`: If True (default), tools use the output schema\nextracted from the OpenAPI spec for response validation. If\nFalse, a permissive schema is used instead, allowing any\nresponse structure while still returning structured JSON.\n- `**settings`: Additional settings passed to FastMCP\n\n**Returns:**\n- A FastMCP server with an OpenAPIProvider attached.\n\n\n#### `from_fastapi` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L2113\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_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\n```\n\nCreate a FastMCP server from a FastAPI application.\n\n**Args:**\n- `app`: FastAPI application instance\n- `name`: Name for the MCP server (defaults to app.title)\n- `route_maps`: Optional list of RouteMap objects defining route mappings\n- `route_map_fn`: Optional callable for advanced route type mapping\n- `mcp_component_fn`: Optional callable for component customization\n- `mcp_names`: Optional dictionary mapping operationId to component names\n- `httpx_client_kwargs`: Optional kwargs passed to httpx.AsyncClient.\nUse this to configure timeout and other client settings.\n- `tags`: Optional set of tags to add to all components\n- `**settings`: Additional settings passed to FastMCP\n\n**Returns:**\n- A FastMCP server with an OpenAPIProvider attached.\n\n\n#### `as_proxy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L2168\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nas_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy\n```\n\nCreate a FastMCP proxy server for the given backend.\n\n.. deprecated::\n    Use :func:`fastmcp.server.create_proxy` instead.\n    This method will be removed in a future version.\n\nThe `backend` argument can be either an existing `fastmcp.client.Client`\ninstance or any value accepted as the `transport` argument of\n`fastmcp.client.Client`. This mirrors the convenience of the\n`fastmcp.client.Client` constructor.\n\n\n#### `generate_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/server.py#L2205\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_name(cls, name: str | None = None) -> str\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.tasks`\n\n\nMCP SEP-1686 background tasks support.\n\nThis module implements protocol-level background task execution for MCP servers.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-capabilities.mdx",
    "content": "---\ntitle: capabilities\nsidebarTitle: capabilities\n---\n\n# `fastmcp.server.tasks.capabilities`\n\n\nSEP-1686 task capabilities declaration.\n\n## Functions\n\n### `get_task_capabilities` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/capabilities.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_task_capabilities() -> ServerTasksCapability | None\n```\n\n\nReturn the SEP-1686 task capabilities.\n\nReturns task capabilities as a first-class ServerCapabilities field,\ndeclaring support for list, cancel, and request operations per SEP-1686.\n\nReturns None if pydocket is not installed (no task support).\n\nNote: prompts/resources are passed via extra_data since the SDK types\ndon't include them yet (FastMCP supports them ahead of the spec).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-config.mdx",
    "content": "---\ntitle: config\nsidebarTitle: config\n---\n\n# `fastmcp.server.tasks.config`\n\n\nTaskConfig for MCP SEP-1686 background task execution modes.\n\nThis module defines the configuration for how tools, resources, and prompts\nhandle task-augmented execution as specified in SEP-1686.\n\n\n## Classes\n\n### `TaskMeta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/config.py#L28\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMetadata for task-augmented execution requests.\n\nWhen passed to call_tool/read_resource/get_prompt, signals that\nthe operation should be submitted as a background task.\n\n**Attributes:**\n- `ttl`: Client-requested TTL in milliseconds. If None, uses server default.\n- `fn_key`: Docket routing key. Auto-derived from component name if None.\n\n\n### `TaskConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/config.py#L44\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for MCP background task execution (SEP-1686).\n\nControls how a component handles task-augmented requests:\n\n- \"forbidden\": Component does not support task execution. Clients must not\n  request task augmentation; server returns -32601 if they do.\n- \"optional\": Component supports both synchronous and task execution.\n  Client may request task augmentation or call normally.\n- \"required\": Component requires task execution. Clients must request task\n  augmentation; server returns -32601 if they don't.\n\n\n**Methods:**\n\n#### `from_bool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/config.py#L82\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_bool(cls, value: bool) -> TaskConfig\n```\n\nConvert boolean task flag to TaskConfig.\n\n**Args:**\n- `value`: True for \"optional\" mode, False for \"forbidden\" mode.\n\n**Returns:**\n- TaskConfig with appropriate mode.\n\n\n#### `supports_tasks` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/config.py#L93\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsupports_tasks(self) -> bool\n```\n\nCheck if this component supports task execution.\n\n**Returns:**\n- True if mode is \"optional\" or \"required\", False if \"forbidden\".\n\n\n#### `validate_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/config.py#L101\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_function(self, fn: Callable[..., Any], name: str) -> None\n```\n\nValidate that function is compatible with this task config.\n\nTask execution requires:\n1. fastmcp[tasks] to be installed (pydocket)\n2. Async functions\n\nRaises ImportError if mode is \"optional\" or \"required\" but pydocket\nis not installed. Raises ValueError if function is synchronous.\n\n**Args:**\n- `fn`: The function to validate (handles callable classes and staticmethods).\n- `name`: Name for error messages.\n\n**Raises:**\n- `ImportError`: If task execution is enabled but pydocket not installed.\n- `ValueError`: If task execution is enabled but function is sync.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-elicitation.mdx",
    "content": "---\ntitle: elicitation\nsidebarTitle: elicitation\n---\n\n# `fastmcp.server.tasks.elicitation`\n\n\nBackground task elicitation support (SEP-1686).\n\nThis module provides elicitation capabilities for background tasks running\nin Docket workers. Unlike regular MCP requests, background tasks don't have\nan active request context, so elicitation requires special handling:\n\n1. Set task status to \"input_required\" via Redis\n2. Send notifications/tasks/status with elicitation metadata\n3. Wait for client to send input via tasks/sendInput\n4. Resume task execution with the provided input\n\nThis uses the public MCP SDK APIs where possible, with minimal use of\ninternal APIs for background task coordination.\n\n\n## Functions\n\n### `elicit_for_task` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nelicit_for_task(task_id: str, session: ServerSession | None, message: str, schema: dict[str, Any], fastmcp: FastMCP) -> mcp.types.ElicitResult\n```\n\n\nSend an elicitation request from a background task.\n\nThis function handles the complexity of eliciting user input when running\nin a Docket worker context where there's no active MCP request.\n\n**Args:**\n- `task_id`: The background task ID\n- `session`: The MCP ServerSession for this task\n- `message`: The message to display to the user\n- `schema`: The JSON schema for the expected response\n- `fastmcp`: The FastMCP server instance\n\n**Returns:**\n- ElicitResult containing the user's response\n\n**Raises:**\n- `RuntimeError`: If Docket is not available\n- `McpError`: If the elicitation request fails\n\n\n### `relay_elicitation` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L234\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrelay_elicitation(session: ServerSession, session_id: str, task_id: str, elicitation: dict[str, Any], fastmcp: FastMCP) -> None\n```\n\n\nRelay elicitation from a background task worker to the client.\n\nCalled by the notification subscriber when it detects an input_required\nnotification with elicitation metadata. Sends a standard elicitation/create\nrequest to the client session, then uses handle_task_input() to push the\nresponse to Redis so the blocked worker can resume.\n\n**Args:**\n- `session`: MCP ServerSession\n- `session_id`: Session identifier\n- `task_id`: Background task ID\n- `elicitation`: Elicitation metadata (message, requestedSchema)\n- `fastmcp`: FastMCP server instance\n\n\n### `handle_task_input` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L290\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nhandle_task_input(task_id: str, session_id: str, action: str, content: dict[str, Any] | None, fastmcp: FastMCP) -> bool\n```\n\n\nHandle input sent to a background task via tasks/sendInput.\n\nThis is called when a client sends input in response to an elicitation\nrequest from a background task.\n\n**Args:**\n- `task_id`: The background task ID\n- `session_id`: The MCP session ID\n- `action`: The elicitation action (\"accept\", \"decline\", \"cancel\")\n- `content`: The response content (for \"accept\" action)\n- `fastmcp`: The FastMCP server instance\n\n**Returns:**\n- True if the input was successfully stored, False otherwise\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-handlers.mdx",
    "content": "---\ntitle: handlers\nsidebarTitle: handlers\n---\n\n# `fastmcp.server.tasks.handlers`\n\n\nSEP-1686 task execution handlers.\n\nHandles queuing tool/prompt/resource executions to Docket as background tasks.\n\n\n## Functions\n\n### `submit_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/handlers.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsubmit_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\n```\n\n\nSubmit any component to Docket for background execution (SEP-1686).\n\nUnified handler for all component types. Called by component's internal\nmethods (_run, _read, _render) when task metadata is present and mode allows.\n\nQueues the component's method to Docket, stores raw return values,\nand converts to MCP types on retrieval.\n\n**Args:**\n- `task_type`: Component type for task key construction\n- `key`: The component key as seen by MCP layer (with namespace prefix)\n- `component`: The component instance (Tool, Resource, ResourceTemplate, Prompt)\n- `arguments`: Arguments/params (None for Resource which has no args)\n- `task_meta`: Task execution metadata. If task_meta.ttl is provided, it\noverrides the server default (docket.execution_ttl).\n\n**Returns:**\n- Task stub with proper Task object\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-keys.mdx",
    "content": "---\ntitle: keys\nsidebarTitle: keys\n---\n\n# `fastmcp.server.tasks.keys`\n\n\nTask key management for SEP-1686 background tasks.\n\nTask keys encode security scoping and metadata in the Docket key format:\n    `{session_id}:{client_task_id}:{task_type}:{component_identifier}`\n\nThis format provides:\n- Session-based security scoping (prevents cross-session access)\n- Task type identification (tool/prompt/resource)\n- Component identification (name or URI for result conversion)\n\n\n## Functions\n\n### `build_task_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/keys.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nbuild_task_key(session_id: str, client_task_id: str, task_type: str, component_identifier: str) -> str\n```\n\n\nBuild Docket task key with embedded metadata.\n\nFormat: `{session_id}:{client_task_id}:{task_type}:{component_identifier}`\n\nThe component_identifier is URI-encoded to handle special characters (colons, slashes, etc.).\n\n**Args:**\n- `session_id`: Session ID for security scoping\n- `client_task_id`: Client-provided task ID\n- `task_type`: Type of task (\"tool\", \"prompt\", \"resource\")\n- `component_identifier`: Tool name, prompt name, or resource URI\n\n**Returns:**\n- Encoded task key for Docket\n\n**Examples:**\n\n>>> build_task_key(\"session123\", \"task456\", \"tool\", \"my_tool\")\n'session123:task456:tool:my_tool'\n>>> build_task_key(\"session123\", \"task456\", \"resource\", \"file://data.txt\")\n'session123:task456:resource:file%3A%2F%2Fdata.txt'\n\n\n### `parse_task_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/keys.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_task_key(task_key: str) -> dict[str, str]\n```\n\n\nParse Docket task key to extract metadata.\n\n**Args:**\n- `task_key`: Encoded task key from Docket\n\n**Returns:**\n- Dict with keys: session_id, client_task_id, task_type, component_identifier\n\n**Examples:**\n\n>>> parse_task_key(\"session123:task456:tool:my_tool\")\n`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`\n>>> parse_task_key(\"session123:task456:resource:file%3A%2F%2Fdata.txt\")\n`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'resource', 'component_identifier': 'file://data.txt'}`\n\n\n### `get_client_task_id_from_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/keys.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_client_task_id_from_key(task_key: str) -> str\n```\n\n\nExtract just the client task ID from a task key.\n\n**Args:**\n- `task_key`: Full encoded task key\n\n**Returns:**\n- Client-provided task ID (second segment)\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-notifications.mdx",
    "content": "---\ntitle: notifications\nsidebarTitle: notifications\n---\n\n# `fastmcp.server.tasks.notifications`\n\n\nDistributed notification queue for background task events (SEP-1686).\n\nEnables distributed Docket workers to send MCP notifications to clients\nwithout holding session references. Workers push to a Redis queue,\nthe MCP server process subscribes and forwards to the client's session.\n\nPattern: Fire-and-forward with retry\n- One queue per session_id\n- LPUSH/BRPOP for reliable ordered delivery\n- Retry up to 3 times on delivery failure, then discard\n- TTL-based expiration for stale messages\n\nNote: Docket's execution.subscribe() handles task state/progress events via\nRedis Pub/Sub. This module handles elicitation-specific notifications that\nrequire reliable delivery (input_required prompts, cancel signals).\n\n\n## Functions\n\n### `push_notification` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L48\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\npush_notification(session_id: str, notification: dict[str, Any], docket: Docket) -> None\n```\n\n\nPush notification to session's queue (called from Docket worker).\n\nUsed for elicitation-specific notifications (input_required, cancel)\nthat need reliable delivery across distributed processes.\n\n**Args:**\n- `session_id`: Target session's identifier\n- `notification`: MCP notification dict (method, params, _meta)\n- `docket`: Docket instance for Redis access\n\n\n### `notification_subscriber_loop` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nnotification_subscriber_loop(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None\n```\n\n\nSubscribe to notification queue and forward to session.\n\nRuns in the MCP server process. Bridges distributed workers to clients.\n\nThis loop:\n1. Maintains a heartbeat (active subscriber marker for debugging)\n2. Blocks on BRPOP waiting for notifications\n3. Forwards notifications to the client's session\n4. Retries failed deliveries, then discards (no dead-letter queue)\n\n**Args:**\n- `session_id`: Session identifier to subscribe to\n- `session`: MCP ServerSession for sending notifications\n- `docket`: Docket instance for Redis access\n- `fastmcp`: FastMCP server instance (for elicitation relay)\n\n\n### `ensure_subscriber_running` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L238\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nensure_subscriber_running(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None\n```\n\n\nStart notification subscriber if not already running (idempotent).\n\nSubscriber is created on first task submission and cleaned up on disconnect.\nSafe to call multiple times for the same session.\n\n**Args:**\n- `session_id`: Session identifier\n- `session`: MCP ServerSession\n- `docket`: Docket instance\n- `fastmcp`: FastMCP server instance (for elicitation relay)\n\n\n### `stop_subscriber` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L278\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nstop_subscriber(session_id: str) -> None\n```\n\n\nStop notification subscriber for a session.\n\nCalled when session disconnects. Pending messages remain in queue\nfor delivery if client reconnects (with TTL expiration).\n\n**Args:**\n- `session_id`: Session identifier\n\n\n### `get_subscriber_count` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L298\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_subscriber_count() -> int\n```\n\n\nGet number of active subscribers (for monitoring).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-requests.mdx",
    "content": "---\ntitle: requests\nsidebarTitle: requests\n---\n\n# `fastmcp.server.tasks.requests`\n\n\nSEP-1686 task request handlers.\n\nHandles MCP task protocol requests: tasks/get, tasks/result, tasks/list, tasks/cancel.\nThese handlers query and manage existing tasks (contrast with handlers.py which creates tasks).\n\nThis module requires fastmcp[tasks] (pydocket). It is only imported when docket is available.\n\n\n## Functions\n\n### `tasks_get_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/requests.py#L137\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskResult\n```\n\n\nHandle MCP 'tasks/get' request (SEP-1686).\n\n**Args:**\n- `server`: FastMCP server instance\n- `params`: Request params containing taskId\n\n**Returns:**\n- Task status response with spec-compliant fields\n\n\n### `tasks_result_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/requests.py#L222\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any\n```\n\n\nHandle MCP 'tasks/result' request (SEP-1686).\n\nConverts raw task return values to MCP types based on task type.\n\n**Args:**\n- `server`: FastMCP server instance\n- `params`: Request params containing taskId\n\n**Returns:**\n- MCP result (CallToolResult, GetPromptResult, or ReadResourceResult)\n\n\n### `tasks_list_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/requests.py#L403\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntasks_list_handler(server: FastMCP, params: dict[str, Any]) -> ListTasksResult\n```\n\n\nHandle MCP 'tasks/list' request (SEP-1686).\n\nNote: With client-side tracking, this returns minimal info.\n\n**Args:**\n- `server`: FastMCP server instance\n- `params`: Request params (cursor, limit)\n\n**Returns:**\n- Response with tasks list and pagination\n\n\n### `tasks_cancel_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/requests.py#L421\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntasks_cancel_handler(server: FastMCP, params: dict[str, Any]) -> CancelTaskResult\n```\n\n\nHandle MCP 'tasks/cancel' request (SEP-1686).\n\nCancels a running task, transitioning it to cancelled state.\n\n**Args:**\n- `server`: FastMCP server instance\n- `params`: Request params containing taskId\n\n**Returns:**\n- Task status response showing cancelled state\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-routing.mdx",
    "content": "---\ntitle: routing\nsidebarTitle: routing\n---\n\n# `fastmcp.server.tasks.routing`\n\n\nTask routing helper for MCP components.\n\nProvides unified task mode enforcement and docket routing logic.\n\n\n## Functions\n\n### `check_background_task` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/routing.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncheck_background_task(component: Tool | Resource | ResourceTemplate | Prompt, task_type: TaskType, arguments: dict[str, Any] | None = None, task_meta: TaskMeta | None = None) -> mcp.types.CreateTaskResult | None\n```\n\n\nCheck task mode and submit to background if requested.\n\n**Args:**\n- `component`: The MCP component\n- `task_type`: Type of task (\"tool\", \"resource\", \"template\", \"prompt\")\n- `arguments`: Arguments for tool/prompt/template execution\n- `task_meta`: Task execution metadata. If provided, execute as background task.\n\n**Returns:**\n- CreateTaskResult if submitted to docket, None for sync execution\n\n**Raises:**\n- `McpError`: If mode=\"required\" but no task metadata, or mode=\"forbidden\"\n      but task metadata is present\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-tasks-subscriptions.mdx",
    "content": "---\ntitle: subscriptions\nsidebarTitle: subscriptions\n---\n\n# `fastmcp.server.tasks.subscriptions`\n\n\nTask subscription helpers for sending MCP notifications (SEP-1686).\n\nSubscribes to Docket execution state changes and sends notifications/tasks/status\nto clients when their tasks change state.\n\nThis module requires fastmcp[tasks] (pydocket). It is only imported when docket is available.\n\n\n## Functions\n\n### `subscribe_to_task_updates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/tasks/subscriptions.py#L31\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsubscribe_to_task_updates(task_id: str, task_key: str, session: ServerSession, docket: Docket, poll_interval_ms: int = 5000) -> None\n```\n\n\nSubscribe to Docket execution events and send MCP notifications.\n\nPer SEP-1686 lines 436-444, servers MAY send notifications/tasks/status\nwhen task state changes. This is an optional optimization that reduces\nclient polling frequency.\n\n**Args:**\n- `task_id`: Client-visible task ID (server-generated UUID)\n- `task_key`: Internal Docket execution key (includes session, type, component)\n- `session`: MCP ServerSession for sending notifications\n- `docket`: Docket instance for subscribing to execution events\n- `poll_interval_ms`: Poll interval in milliseconds to include in notifications\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-telemetry.mdx",
    "content": "---\ntitle: telemetry\nsidebarTitle: telemetry\n---\n\n# `fastmcp.server.telemetry`\n\n\nServer-side telemetry helpers.\n\n## Functions\n\n### `get_auth_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/telemetry.py#L13\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_auth_span_attributes() -> dict[str, str]\n```\n\n\nGet auth attributes for the current request, if authenticated.\n\n\n### `get_session_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/telemetry.py#L30\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_session_span_attributes() -> dict[str, str]\n```\n\n\nGet session attributes for the current request.\n\n\n### `server_span` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/telemetry.py#L56\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nserver_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None) -> Generator[Span, None, None]\n```\n\n\nCreate a SERVER span with standard MCP attributes and auth context.\n\nAutomatically records any exception on the span and sets error status.\n\n\n### `delegate_span` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/telemetry.py#L100\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndelegate_span(name: str, provider_type: str, component_key: str) -> Generator[Span, None, None]\n```\n\n\nCreate an INTERNAL span for provider delegation.\n\nUsed by FastMCPProvider when delegating to mounted servers.\nAutomatically records any exception on the span and sets error status.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.transforms`\n\n\nTransform system for component transformations.\n\nTransforms modify components (tools, resources, prompts). List operations use a pure\nfunction pattern where transforms receive sequences and return transformed sequences.\nGet operations use a middleware pattern with `call_next` to chain lookups.\n\nUnlike middleware (which operates on requests), transforms are observable by the\nsystem for task registration, tag filtering, and component introspection.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms import Namespace\n\n    server = FastMCP(\"Server\")\n    mount = server.mount(other_server)\n    mount.add_transform(Namespace(\"api\"))  # Tools become api_toolname\n    ```\n\n\n## Classes\n\n### `GetToolNext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for get_tool call_next functions.\n\n\n### `GetResourceNext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L44\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for get_resource call_next functions.\n\n\n### `GetResourceTemplateNext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L52\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for get_resource_template call_next functions.\n\n\n### `GetPromptNext` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L60\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for get_prompt call_next functions.\n\n\n### `Transform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for component transformations.\n\nList operations use a pure function pattern: transforms receive sequences\nand return transformed sequences. Get operations use a middleware pattern\nwith `call_next` to chain lookups.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L95\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nList tools with transformation applied.\n\n**Args:**\n- `tools`: Sequence of tools to transform.\n\n**Returns:**\n- Transformed sequence of tools.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L106\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nGet a tool by name.\n\n**Args:**\n- `name`: The requested tool name (may be transformed).\n- `call_next`: Callable to get tool from downstream.\n- `version`: Optional version filter to apply.\n\n**Returns:**\n- The tool if found, None otherwise.\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L125\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]\n```\n\nList resources with transformation applied.\n\n**Args:**\n- `resources`: Sequence of resources to transform.\n\n**Returns:**\n- Transformed sequence of resources.\n\n\n#### `get_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L136\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None\n```\n\nGet a resource by URI.\n\n**Args:**\n- `uri`: The requested resource URI (may be transformed).\n- `call_next`: Callable to get resource from downstream.\n- `version`: Optional version filter to apply.\n\n**Returns:**\n- The resource if found, None otherwise.\n\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L159\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]\n```\n\nList resource templates with transformation applied.\n\n**Args:**\n- `templates`: Sequence of resource templates to transform.\n\n**Returns:**\n- Transformed sequence of resource templates.\n\n\n#### `get_resource_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L172\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None\n```\n\nGet a resource template by URI.\n\n**Args:**\n- `uri`: The requested template URI (may be transformed).\n- `call_next`: Callable to get template from downstream.\n- `version`: Optional version filter to apply.\n\n**Returns:**\n- The resource template if found, None otherwise.\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L195\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]\n```\n\nList prompts with transformation applied.\n\n**Args:**\n- `prompts`: Sequence of prompts to transform.\n\n**Returns:**\n- Transformed sequence of prompts.\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/__init__.py#L206\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None\n```\n\nGet a prompt by name.\n\n**Args:**\n- `name`: The requested prompt name (may be transformed).\n- `call_next`: Callable to get prompt from downstream.\n- `version`: Optional version filter to apply.\n\n**Returns:**\n- The prompt if found, None otherwise.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-catalog.mdx",
    "content": "---\ntitle: catalog\nsidebarTitle: catalog\n---\n\n# `fastmcp.server.transforms.catalog`\n\n\nBase class for transforms that need to read the real component catalog.\n\nSome transforms replace ``list_tools()`` output with synthetic components\n(e.g. a search interface) while still needing access to the *real*\n(auth-filtered) catalog at call time.  ``CatalogTransform`` provides the\nbypass machinery so subclasses can call ``get_tool_catalog()`` without\ntriggering their own replacement logic.\n\nRe-entrancy problem\n-------------------\n\nWhen a synthetic tool handler calls ``get_tool_catalog()``, that calls\n``ctx.fastmcp.list_tools()`` which re-enters the transform pipeline —\nincluding *this* transform's ``list_tools()``.  If the subclass overrides\n``list_tools()`` directly, the re-entrant call would hit the subclass's\nreplacement logic again (returning synthetic tools instead of the real\ncatalog).  A ``super()`` call can't prevent this because Python can't\nshort-circuit a method after ``super()`` returns.\n\nSolution: ``CatalogTransform`` owns ``list_tools()`` and uses a\nper-instance ``ContextVar`` to detect re-entrant calls.  During bypass,\nit passes through to the base ``Transform.list_tools()`` (a no-op).\nOtherwise, it delegates to ``transform_tools()`` — the subclass hook\nwhere replacement logic lives.  Same pattern for resources, prompts,\nand resource templates.\n\nThis is *not* the same as the ``Provider._list_tools()`` convention\n(which produces raw components with no arguments).  ``transform_tools()``\nreceives the current catalog and returns a transformed version.  The\ndistinct name avoids confusion between the two patterns.\n\nUsage::\n\n    class MyTransform(CatalogTransform):\n        async def transform_tools(self, tools):\n            return [self._make_search_tool()]\n\n        def _make_search_tool(self):\n            async def search(ctx: Context = None):\n                real_tools = await self.get_tool_catalog(ctx)\n                ...\n            return Tool.from_function(fn=search, name=\"search\")\n\n\n## Classes\n\n### `CatalogTransform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L65\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransform that needs access to the real component catalog.\n\nSubclasses override ``transform_tools()`` / ``transform_resources()``\n/ ``transform_prompts()`` / ``transform_resource_templates()``\ninstead of the ``list_*()`` methods.  The base class owns\n``list_*()`` and handles re-entrant bypass automatically — subclasses\nnever see re-entrant calls from ``get_*_catalog()``.\n\nThe ``get_*_catalog()`` methods fetch the real (auth-filtered) catalog\nby temporarily setting a bypass flag so that this transform's\n``list_*()`` passes through without calling the subclass hook.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L89\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L94\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]\n```\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L99\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]\n```\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L106\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]\n```\n\n#### `transform_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L115\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nTransform the tool catalog.\n\nOverride this method to replace, filter, or augment the tool listing.\nThe default implementation passes through unchanged.\n\nDo NOT override ``list_tools()`` directly — the base class uses it\nto handle re-entrant bypass when ``get_tool_catalog()`` reads the\nreal catalog.\n\n\n#### `transform_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L127\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]\n```\n\nTransform the resource catalog.\n\nOverride this method to replace, filter, or augment the resource listing.\nThe default implementation passes through unchanged.\n\nDo NOT override ``list_resources()`` directly — the base class uses it\nto handle re-entrant bypass when ``get_resource_catalog()`` reads the\nreal catalog.\n\n\n#### `transform_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L141\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]\n```\n\nTransform the resource template catalog.\n\nOverride this method to replace, filter, or augment the template listing.\nThe default implementation passes through unchanged.\n\nDo NOT override ``list_resource_templates()`` directly — the base class\nuses it to handle re-entrant bypass when\n``get_resource_template_catalog()`` reads the real catalog.\n\n\n#### `transform_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]\n```\n\nTransform the prompt catalog.\n\nOverride this method to replace, filter, or augment the prompt listing.\nThe default implementation passes through unchanged.\n\nDo NOT override ``list_prompts()`` directly — the base class uses it\nto handle re-entrant bypass when ``get_prompt_catalog()`` reads the\nreal catalog.\n\n\n#### `get_tool_catalog` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L171\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool_catalog(self, ctx: Context) -> Sequence[Tool]\n```\n\nFetch the real tool catalog, bypassing this transform.\n\nThe result is deduplicated by name so that only the highest version\nof each tool is returned — matching what protocol handlers expose\non the wire.\n\n**Args:**\n- `ctx`: The current request context.\n- `run_middleware`: Whether to run middleware on the inner call.\nDefaults to True because this is typically called from a\ntool handler where list_tools middleware has not yet run.\n\n\n#### `get_resource_catalog` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L193\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_catalog(self, ctx: Context) -> Sequence[Resource]\n```\n\nFetch the real resource catalog, bypassing this transform.\n\n**Args:**\n- `ctx`: The current request context.\n- `run_middleware`: Whether to run middleware on the inner call.\nDefaults to True because this is typically called from a\ntool handler where list_resources middleware has not yet run.\n\n\n#### `get_prompt_catalog` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L210\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt_catalog(self, ctx: Context) -> Sequence[Prompt]\n```\n\nFetch the real prompt catalog, bypassing this transform.\n\n**Args:**\n- `ctx`: The current request context.\n- `run_middleware`: Whether to run middleware on the inner call.\nDefaults to True because this is typically called from a\ntool handler where list_prompts middleware has not yet run.\n\n\n#### `get_resource_template_catalog` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/catalog.py#L227\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template_catalog(self, ctx: Context) -> Sequence[ResourceTemplate]\n```\n\nFetch the real resource template catalog, bypassing this transform.\n\n**Args:**\n- `ctx`: The current request context.\n- `run_middleware`: Whether to run middleware on the inner call.\nDefaults to True because this is typically called from a\ntool handler where list_resource_templates middleware has\nnot yet run.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-namespace.mdx",
    "content": "---\ntitle: namespace\nsidebarTitle: namespace\n---\n\n# `fastmcp.server.transforms.namespace`\n\n\nNamespace transform for prefixing component names.\n\n## Classes\n\n### `Namespace` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L28\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nPrefixes component names with a namespace.\n\n- Tools: name → namespace_name\n- Prompts: name → namespace_name\n- Resources: protocol://path → protocol://namespace/path\n- Resource Templates: same as resources\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L97\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nPrefix tool names with namespace.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L103\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nGet tool by namespaced name.\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L119\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]\n```\n\nAdd namespace path segment to resource URIs.\n\n\n#### `get_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L126\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None\n```\n\nGet resource by namespaced URI.\n\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L146\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]\n```\n\nAdd namespace path segment to template URIs.\n\n\n#### `get_resource_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None\n```\n\nGet resource template by namespaced URI.\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L177\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]\n```\n\nPrefix prompt names with namespace.\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/namespace.py#L183\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None\n```\n\nGet prompt by namespaced name.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx",
    "content": "---\ntitle: prompts_as_tools\nsidebarTitle: prompts_as_tools\n---\n\n# `fastmcp.server.transforms.prompts_as_tools`\n\n\nTransform that exposes prompts as tools.\n\nThis transform generates tools for listing and getting prompts, enabling\nclients that only support tools to access prompt functionality.\n\nThe generated tools route through `ctx.fastmcp` at runtime, so all server\nmiddleware (auth, visibility, rate limiting, etc.) applies to prompt\noperations exactly as it would for direct `prompts/get` calls.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms import PromptsAsTools\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_transform(PromptsAsTools(mcp))\n    # Now has list_prompts and get_prompt tools\n    ```\n\n\n## Classes\n\n### `PromptsAsTools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/prompts_as_tools.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransform that adds tools for listing and getting prompts.\n\nGenerates two tools:\n- `list_prompts`: Lists all prompts\n- `get_prompt`: Gets a specific prompt with optional arguments\n\nThe generated tools route through the server at runtime, so auth,\nmiddleware, and visibility apply automatically.\n\nThis transform should be applied to a FastMCP server instance, not\na raw Provider, because the generated tools need the server's\nmiddleware chain for auth and visibility filtering.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/prompts_as_tools.py#L75\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nAdd prompt tools to the tool list.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/prompts_as_tools.py#L83\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nGet a tool by name, including generated prompt tools.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx",
    "content": "---\ntitle: resources_as_tools\nsidebarTitle: resources_as_tools\n---\n\n# `fastmcp.server.transforms.resources_as_tools`\n\n\nTransform that exposes resources as tools.\n\nThis transform generates tools for listing and reading resources, enabling\nclients that only support tools to access resource functionality.\n\nThe generated tools route through `ctx.fastmcp` at runtime, so all server\nmiddleware (auth, visibility, rate limiting, etc.) applies to resource\noperations exactly as it would for direct `resources/read` calls.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms import ResourcesAsTools\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_transform(ResourcesAsTools(mcp))\n    # Now has list_resources and read_resource tools\n    ```\n\n\n## Classes\n\n### `ResourcesAsTools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/resources_as_tools.py#L41\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTransform that adds tools for listing and reading resources.\n\nGenerates two tools:\n- `list_resources`: Lists all resources and templates\n- `read_resource`: Reads a resource by URI\n\nThe generated tools route through the server at runtime, so auth,\nmiddleware, and visibility apply automatically.\n\nThis transform should be applied to a FastMCP server instance, not\na raw Provider, because the generated tools need the server's\nmiddleware chain for auth and visibility filtering.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/resources_as_tools.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nAdd resource tools to the tool list.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/resources_as_tools.py#L86\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nGet a tool by name, including generated resource tools.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-search-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.server.transforms.search`\n\n\nSearch transforms for tool discovery.\n\nSearch transforms collapse a large tool catalog into a search interface,\nletting LLMs discover tools on demand instead of seeing the full list.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms.search import RegexSearchTransform\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_transform(RegexSearchTransform())\n    # list_tools now returns only search_tools + call_tool\n    ```\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-search-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.server.transforms.search.base`\n\n\nBase class for search transforms.\n\nSearch transforms replace ``list_tools()`` output with a small set of\nsynthetic tools — a search tool and a call-tool proxy — so LLMs can\ndiscover tools on demand instead of receiving the full catalog.\n\nAll concrete search transforms (``RegexSearchTransform``,\n``BM25SearchTransform``, etc.) inherit from ``BaseSearchTransform`` and\nimplement ``_make_search_tool()`` and ``_search()`` to provide their\nspecific search strategy.\n\nExample::\n\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms.search import RegexSearchTransform\n\n    mcp = FastMCP(\"Server\")\n\n    @mcp.tool\n    def add(a: int, b: int) -> int: ...\n\n    @mcp.tool\n    def multiply(x: float, y: float) -> float: ...\n\n    # Clients now see only ``search_tools`` and ``call_tool``.\n    # The original tools are discoverable via search.\n    mcp.add_transform(RegexSearchTransform())\n\n\n## Functions\n\n### `serialize_tools_for_output_json` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/base.py#L60\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nserialize_tools_for_output_json(tools: Sequence[Tool]) -> list[dict[str, Any]]\n```\n\n\nSerialize tools to the same dict format as ``list_tools`` output.\n\n\n### `serialize_tools_for_output_markdown` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/base.py#L138\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nserialize_tools_for_output_markdown(tools: Sequence[Tool]) -> str\n```\n\n\nSerialize tools to compact markdown, using ~65-70% fewer tokens than JSON.\n\n\n## Classes\n\n### `BaseSearchTransform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/base.py#L154\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nReplace the tool listing with a search interface.\n\nWhen this transform is active, ``list_tools()`` returns only:\n\n* Any tools listed in ``always_visible`` (pinned).\n* A **search tool** that finds tools matching a query.\n* A **call_tool** proxy that executes tools discovered via search.\n\nHidden tools remain callable — ``get_tool()`` delegates unknown\nnames downstream, so direct calls and the call-tool proxy both work.\n\nSearch results respect the full auth pipeline: middleware, visibility\ntransforms, and component-level auth checks all apply.\n\n**Args:**\n- `max_results`: Maximum number of tools returned per search.\n- `always_visible`: Tool names that stay in the ``list_tools``\noutput alongside the synthetic search/call tools.\n- `search_tool_name`: Name of the generated search tool.\n- `call_tool_name`: Name of the generated call-tool proxy.\n\n\n**Methods:**\n\n#### `transform_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/base.py#L199\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntransform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nReplace the catalog with pinned + synthetic search/call tools.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/base.py#L204\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nIntercept synthetic tool names; delegate everything else.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-search-bm25.mdx",
    "content": "---\ntitle: bm25\nsidebarTitle: bm25\n---\n\n# `fastmcp.server.transforms.search.bm25`\n\n\nBM25-based search transform.\n\n## Classes\n\n### `BM25SearchTransform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/bm25.py#L86\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSearch transform using BM25 Okapi relevance ranking.\n\nMaintains an in-memory index that is lazily rebuilt when the tool\ncatalog changes (detected via a hash of tool names).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-search-regex.mdx",
    "content": "---\ntitle: regex\nsidebarTitle: regex\n---\n\n# `fastmcp.server.transforms.search.regex`\n\n\nRegex-based search transform.\n\n## Classes\n\n### `RegexSearchTransform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/search/regex.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSearch transform using regex pattern matching.\n\nTools are matched against their name, description, and parameter\ninformation using ``re.search`` with ``re.IGNORECASE``.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-tool_transform.mdx",
    "content": "---\ntitle: tool_transform\nsidebarTitle: tool_transform\n---\n\n# `fastmcp.server.transforms.tool_transform`\n\n\nTransform for applying tool transformations.\n\n## Classes\n\n### `ToolTransform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/tool_transform.py#L16\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nApplies tool transformations to modify tool schemas.\n\nWraps ToolTransformConfig to apply argument renames, schema changes,\nhidden arguments, and other transformations at the transform level.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/tool_transform.py#L64\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nApply transforms to matching tools.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/tool_transform.py#L75\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nGet tool by transformed name.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-version_filter.mdx",
    "content": "---\ntitle: version_filter\nsidebarTitle: version_filter\n---\n\n# `fastmcp.server.transforms.version_filter`\n\n\nVersion filter transform for filtering components by version range.\n\n## Classes\n\n### `VersionFilter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L24\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nFilters components by version range.\n\nWhen applied to a provider or server, components within the version range\nare visible, and unversioned components are included by default. Within\nthat filtered set, the highest version of each component is exposed to\nclients (standard deduplication behavior). Set\n``include_unversioned=False`` to exclude unversioned components.\n\nParameters mirror comparison operators for clarity:\n\n    # Versions < 3.0 (v1 and v2)\n    server.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n    # Versions >= 2.0 and < 3.0 (only v2.x)\n    server.add_transform(VersionFilter(version_gte=\"2.0\", version_lt=\"3.0\"))\n\nWorks with any version string - PEP 440 (1.0, 2.0) or dates (2025-01-01).\n\n**Args:**\n- `version_gte`: Versions >= this value pass through.\n- `version_lt`: Versions < this value pass through.\n- `include_unversioned`: Whether unversioned components (``version=None``)\nshould pass through the filter. Defaults to True.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L80\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L87\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L96\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]\n```\n\n#### `get_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L103\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None\n```\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L116\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]\n```\n\n#### `get_resource_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L125\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None\n```\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L138\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]\n```\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/version_filter.py#L145\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-server-transforms-visibility.mdx",
    "content": "---\ntitle: visibility\nsidebarTitle: visibility\n---\n\n# `fastmcp.server.transforms.visibility`\n\n\nVisibility transform for marking component visibility state.\n\nEach Visibility instance marks components via internal metadata. Multiple\nvisibility transforms can be stacked - later transforms override earlier ones.\nFinal filtering happens at the Provider level.\n\n\n## Functions\n\n### `is_enabled` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L271\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_enabled(component: FastMCPComponent) -> bool\n```\n\n\nCheck if component is enabled.\n\nReturns True if:\n- No visibility mark exists (default is enabled)\n- Visibility mark is True\n\nReturns False if visibility mark is False.\n\n**Args:**\n- `component`: Component to check.\n\n**Returns:**\n- True if component should be enabled/visible to clients.\n\n\n### `get_visibility_rules` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L300\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_visibility_rules(context: Context) -> list[dict[str, Any]]\n```\n\n\nLoad visibility rule dicts from session state.\n\n\n### `save_visibility_rules` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L305\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsave_visibility_rules(context: Context, rules: list[dict[str, Any]]) -> None\n```\n\n\nSave visibility rule dicts to session state and send notifications.\n\n**Args:**\n- `context`: The context to save rules for.\n- `rules`: The visibility rules to save.\n- `components`: Optional hint about which component types are affected.\nIf None, sends notifications for all types (safe default).\nIf provided, only sends notifications for specified types.\n\n\n### `create_visibility_transforms` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L332\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_visibility_transforms(rules: list[dict[str, Any]]) -> list[Visibility]\n```\n\n\nConvert rule dicts to Visibility transforms.\n\n\n### `get_session_transforms` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L360\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_session_transforms(context: Context) -> list[Visibility]\n```\n\n\nGet session-specific Visibility transforms from state store.\n\n\n### `enable_components` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L372\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nenable_components(context: Context) -> None\n```\n\n\nEnable components matching criteria for this session only.\n\nSession rules override global transforms. Rules accumulate - each call\nadds a new rule to the session. Later marks override earlier ones\n(Visibility transform semantics).\n\nSends notifications to this session only: ToolListChangedNotification,\nResourceListChangedNotification, and PromptListChangedNotification.\n\n**Args:**\n- `context`: The context for this session.\n- `names`: Component names or URIs to match.\n- `keys`: Component keys to match (e.g., {\"tool\\:my_tool@v1\"}).\n- `version`: Component version spec to match.\n- `tags`: Tags to match (component must have at least one).\n- `components`: Component types to match (e.g., {\"tool\", \"prompt\"}).\n- `match_all`: If True, matches all components regardless of other criteria.\n\n\n### `disable_components` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L426\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndisable_components(context: Context) -> None\n```\n\n\nDisable components matching criteria for this session only.\n\nSession rules override global transforms. Rules accumulate - each call\nadds a new rule to the session. Later marks override earlier ones\n(Visibility transform semantics).\n\nSends notifications to this session only: ToolListChangedNotification,\nResourceListChangedNotification, and PromptListChangedNotification.\n\n**Args:**\n- `context`: The context for this session.\n- `names`: Component names or URIs to match.\n- `keys`: Component keys to match (e.g., {\"tool\\:my_tool@v1\"}).\n- `version`: Component version spec to match.\n- `tags`: Tags to match (component must have at least one).\n- `components`: Component types to match (e.g., {\"tool\", \"prompt\"}).\n- `match_all`: If True, matches all components regardless of other criteria.\n\n\n### `reset_visibility` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L480\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreset_visibility(context: Context) -> None\n```\n\n\nClear all session visibility rules.\n\nUse this to reset session visibility back to global defaults.\n\nSends notifications to this session only: ToolListChangedNotification,\nResourceListChangedNotification, and PromptListChangedNotification.\n\n**Args:**\n- `context`: The context for this session.\n\n\n### `apply_session_transforms` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L497\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\napply_session_transforms(components: Sequence[ComponentT]) -> Sequence[ComponentT]\n```\n\n\nApply session-specific visibility transforms to components.\n\nThis helper applies session-level enable/disable rules by marking\ncomponents with their visibility state. Session transforms override\nglobal transforms due to mark-based semantics (later marks win).\n\n**Args:**\n- `components`: The components to apply session transforms to.\n\n**Returns:**\n- The components with session transforms applied.\n\n\n## Classes\n\n### `Visibility` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSets visibility state on matching components.\n\nDoes NOT filter inline - just marks components with visibility state.\nLater transforms in the chain can override earlier marks.\nFinal filtering happens at the Provider level after all transforms run.\n\n\n**Methods:**\n\n#### `list_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L196\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]\n```\n\nMark tools by visibility state.\n\n\n#### `get_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L200\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tool(self, name: str, call_next: GetToolNext) -> Tool | None\n```\n\nMark tool if found.\n\n\n#### `list_resources` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L213\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]\n```\n\nMark resources by visibility state.\n\n\n#### `get_resource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L217\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None\n```\n\nMark resource if found.\n\n\n#### `list_resource_templates` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L234\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]\n```\n\nMark resource templates by visibility state.\n\n\n#### `get_resource_template` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L240\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None\n```\n\nMark resource template if found.\n\n\n#### `list_prompts` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L257\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]\n```\n\nMark prompts by visibility state.\n\n\n#### `get_prompt` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/transforms/visibility.py#L261\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None\n```\n\nMark prompt if found.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-settings.mdx",
    "content": "---\ntitle: settings\nsidebarTitle: settings\n---\n\n# `fastmcp.settings`\n\n## Classes\n\n### `DocketSettings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/settings.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nDocket worker configuration.\n\n\n### `Settings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/settings.py#L136\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nFastMCP settings.\n\n\n**Methods:**\n\n#### `get_setting` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/settings.py#L148\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_setting(self, attr: str) -> Any\n```\n\nGet a setting. If the setting contains one or more `__`, it will be\ntreated as a nested setting.\n\n\n#### `set_setting` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/settings.py#L161\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset_setting(self, attr: str, value: Any) -> None\n```\n\nSet a setting. If the setting contains one or more `__`, it will be\ntreated as a nested setting.\n\n\n#### `normalize_log_level` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/settings.py#L183\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nnormalize_log_level(cls, v)\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-telemetry.mdx",
    "content": "---\ntitle: telemetry\nsidebarTitle: telemetry\n---\n\n# `fastmcp.telemetry`\n\n\nOpenTelemetry instrumentation for FastMCP.\n\nThis module provides native OpenTelemetry integration for FastMCP servers and clients.\nIt uses only the opentelemetry-api package, so telemetry is a no-op unless the user\ninstalls an OpenTelemetry SDK and configures exporters.\n\nExample usage with SDK:\n    ```python\n    from opentelemetry import trace\n    from opentelemetry.sdk.trace import TracerProvider\n    from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor\n\n    # Configure the SDK (user responsibility)\n    provider = TracerProvider()\n    provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))\n    trace.set_tracer_provider(provider)\n\n    # Now FastMCP will emit traces\n    from fastmcp import FastMCP\n    mcp = FastMCP(\"my-server\")\n    ```\n\n\n## Functions\n\n### `get_tracer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/telemetry.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_tracer(version: str | None = None) -> Tracer\n```\n\n\nGet the FastMCP tracer for creating spans.\n\n**Args:**\n- `version`: Optional version string for the instrumentation\n\n**Returns:**\n- A tracer instance. Returns a no-op tracer if no SDK is configured.\n\n\n### `inject_trace_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/telemetry.py#L50\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninject_trace_context(meta: dict[str, Any] | None = None) -> dict[str, Any] | None\n```\n\n\nInject current trace context into a meta dict for MCP request propagation.\n\n**Args:**\n- `meta`: Optional existing meta dict to merge with trace context\n\n**Returns:**\n- A new dict containing the original meta (if any) plus trace context keys,\n- or None if no trace context to inject and meta was None\n\n\n### `record_span_error` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/telemetry.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrecord_span_error(span: Span, exception: BaseException) -> None\n```\n\n\nRecord an exception on a span and set error status.\n\n\n### `extract_trace_context` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/telemetry.py#L82\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nextract_trace_context(meta: dict[str, Any] | None) -> Context\n```\n\n\nExtract trace context from an MCP request meta dict.\n\nIf already in a valid trace (e.g., from HTTP propagation), the existing\ntrace context is preserved and meta is not used.\n\n**Args:**\n- `meta`: The meta dict from an MCP request (ctx.request_context.meta)\n\n**Returns:**\n- An OpenTelemetry Context with the extracted trace context,\n- or the current context if no trace context found or already in a trace\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-tools-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.tools`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-tools-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.tools.base`\n\n## Functions\n\n### `default_serializer` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L64\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndefault_serializer(data: Any) -> str\n```\n\n## Classes\n\n### `ToolResult` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `to_mcp_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L123\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult\n```\n\n### `Tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInternal tool registration info.\n\n\n**Methods:**\n\n#### `to_mcp_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L181\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_tool(self, **overrides: Any) -> MCPTool\n```\n\nConvert the FastMCP tool to an MCP tool.\n\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L208\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any]) -> FunctionTool\n```\n\nCreate a Tool from a function.\n\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L248\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any]) -> ToolResult\n```\n\nRun the tool with arguments.\n\nThis method is not implemented in the base Tool class and must be\nimplemented by subclasses.\n\n`run()` can EITHER return a list of ContentBlocks, or a tuple of\n(list of ContentBlocks, dict of structured output).\n\n\n#### `convert_result` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L260\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_result(self, raw_value: Any) -> ToolResult\n```\n\nConvert a raw result to ToolResult.\n\nHandles ToolResult passthrough and converts raw values using the tool's\nattributes (serializer, output_schema) for proper conversion.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L359\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this tool with docket for background execution.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L365\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution\n```\n\nSchedule this tool for background execution via docket.\n\n**Args:**\n- `docket`: The Docket instance\n- `arguments`: Tool arguments\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n\n#### `from_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L389\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_tool(cls, tool: Tool | Callable[..., Any]) -> TransformedTool\n```\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/base.py#L437\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-tools-function_parsing.mdx",
    "content": "---\ntitle: function_parsing\nsidebarTitle: function_parsing\n---\n\n# `fastmcp.tools.function_parsing`\n\n\nFunction introspection and schema generation for FastMCP tools.\n\n## Classes\n\n### `ParsedFunction` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_parsing.py#L117\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_parsing.py#L126\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True, wrap_non_object_output_schema: bool = True) -> ParsedFunction\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-tools-function_tool.mdx",
    "content": "---\ntitle: function_tool\nsidebarTitle: function_tool\n---\n\n# `fastmcp.tools.function_tool`\n\n\nStandalone @tool decorator for FastMCP.\n\n## Functions\n\n### `tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L378\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntool(name_or_fn: str | Callable[..., Any] | None = None) -> Any\n```\n\n\nStandalone decorator to mark a function as an MCP tool.\n\nReturns the original function with metadata attached. Register with a server\nusing mcp.add_tool().\n\n\n## Classes\n\n### `DecoratedTool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L59\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProtocol for functions decorated with @tool.\n\n\n### `ToolMeta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L68\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nMetadata attached to functions by the @tool decorator.\n\n\n### `FunctionTool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L90\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n**Methods:**\n\n#### `to_mcp_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L94\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_mcp_tool(self, **overrides: Any) -> mcp.types.Tool\n```\n\nConvert the FastMCP tool to an MCP tool.\n\nExtends the base implementation to add task execution mode if enabled.\n\n\n#### `from_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L113\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_function(cls, fn: Callable[..., Any]) -> FunctionTool\n```\n\nCreate a FunctionTool from a function.\n\n**Args:**\n- `fn`: The function to wrap\n- `metadata`: ToolMeta object with all configuration. If provided,\nindividual parameters must not be passed.\n- `name, title, etc.`: Individual parameters for backwards compatibility.\nCannot be used together with metadata parameter.\n\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L256\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any]) -> ToolResult\n```\n\nRun the tool with arguments.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L301\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this tool with docket for background execution.\n\nFunctionTool registers the underlying function, which has the user's\nDepends parameters for docket to resolve.\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L311\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution\n```\n\nSchedule this tool for background execution via docket.\n\nFunctionTool splats the arguments dict since .fn expects **kwargs.\n\n**Args:**\n- `docket`: The Docket instance\n- `arguments`: Tool arguments\n- `fn_key`: Function lookup key in Docket registry (defaults to self.key)\n- `task_key`: Redis storage key for the result\n- `**kwargs`: Additional kwargs passed to docket.add()\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-tools-tool_transform.mdx",
    "content": "---\ntitle: tool_transform\nsidebarTitle: tool_transform\n---\n\n# `fastmcp.tools.tool_transform`\n\n## Functions\n\n### `forward` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L41\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nforward(**kwargs: Any) -> ToolResult\n```\n\n\nForward to parent tool with argument transformation applied.\n\nThis function can only be called from within a transformed tool's custom\nfunction. It applies argument transformation (renaming, validation) before\ncalling the parent tool.\n\nFor example, if the parent tool has args `x` and `y`, but the transformed\ntool has args `a` and `b`, and an `transform_args` was provided that maps `x` to\n`a` and `y` to `b`, then `forward(a=1, b=2)` will call the parent tool with\n`x=1` and `y=2`.\n\n**Args:**\n- `**kwargs`: Arguments to forward to the parent tool (using transformed names).\n\n**Returns:**\n- The ToolResult from the parent tool execution.\n\n**Raises:**\n- `RuntimeError`: If called outside a transformed tool context.\n- `TypeError`: If provided arguments don't match the transformed schema.\n\n\n### `forward_raw` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L71\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nforward_raw(**kwargs: Any) -> ToolResult\n```\n\n\nForward directly to parent tool without transformation.\n\nThis function bypasses all argument transformation and validation, calling the parent\ntool directly with the provided arguments. Use this when you need to call the parent\nwith its original parameter names and structure.\n\nFor example, if the parent tool has args `x` and `y`, then `forward_raw(x=1,\ny=2)` will call the parent tool with `x=1` and `y=2`.\n\n**Args:**\n- `**kwargs`: Arguments to pass directly to the parent tool (using original names).\n\n**Returns:**\n- The ToolResult from the parent tool execution.\n\n**Raises:**\n- `RuntimeError`: If called outside a transformed tool context.\n\n\n### `apply_transformations_to_tools` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L975\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\napply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool]\n```\n\n\nApply a list of transformations to a list of tools. Tools that do not have any transformations\nare left unchanged.\n\nNote: tools dict is keyed by prefixed key (e.g., \"tool:my_tool\"),\nbut transformations are keyed by tool name (e.g., \"my_tool\").\n\n\n## Classes\n\n### `ArgTransform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L98\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for transforming a parent tool's argument.\n\nThis class allows fine-grained control over how individual arguments are transformed\nwhen creating a new tool from an existing one. You can rename arguments, change their\ndescriptions, add default values, or hide them from clients while passing constants.\n\n**Attributes:**\n- `name`: New name for the argument. Use None to keep original name, or ... for no change.\n- `description`: New description for the argument. Use None to remove description, or ... for no change.\n- `default`: New default value for the argument. Use ... for no change.\n- `default_factory`: Callable that returns a default value. Cannot be used with default.\n- `type`: New type for the argument. Use ... for no change.\n- `hide`: If True, hide this argument from clients but pass a constant value to parent.\n- `required`: If True, make argument required (remove default). Use ... for no change.\n- `examples`: Examples for the argument. Use ... for no change.\n\n**Examples:**\n\nRename argument 'old_name' to 'new_name'\n```python\nArgTransform(name=\"new_name\")\n```\n\nChange description only\n```python\nArgTransform(description=\"Updated description\")\n```\n\nAdd a default value (makes argument optional)\n```python\nArgTransform(default=42)\n```\n\nAdd a default factory (makes argument optional)\n```python\nArgTransform(default_factory=lambda: time.time())\n```\n\nChange the type\n```python\nArgTransform(type=str)\n```\n\nHide the argument entirely from clients\n```python\nArgTransform(hide=True)\n```\n\nHide argument but pass a constant value to parent\n```python\nArgTransform(hide=True, default=\"constant_value\")\n```\n\nHide argument but pass a factory-generated value to parent\n```python\nArgTransform(hide=True, default_factory=lambda: uuid.uuid4().hex)\n```\n\nMake an optional parameter required (removes any default)\n```python\nArgTransform(required=True)\n```\n\nCombine multiple transformations\n```python\nArgTransform(name=\"new_name\", description=\"New desc\", default=None, type=int)\n```\n\n\n### `ArgTransformConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L212\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA model for requesting a single argument transform.\n\n\n**Methods:**\n\n#### `to_arg_transform` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L230\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_arg_transform(self) -> ArgTransform\n```\n\nConvert the argument transform to a FastMCP argument transform.\n\n\n### `TransformedTool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L236\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA tool that is transformed from another tool.\n\nThis class represents a tool that has been created by transforming another tool.\nIt supports argument renaming, schema modification, custom function injection,\nstructured output control, and provides context for the forward() and forward_raw() functions.\n\nThe transformation can be purely schema-based (argument renaming, dropping, etc.)\nor can include a custom function that uses forward() to call the parent tool\nwith transformed arguments. Output schemas and structured outputs are automatically\ninherited from the parent tool but can be overridden or disabled.\n\n**Attributes:**\n- `parent_tool`: The original tool that this tool was transformed from.\n- `fn`: The function to execute when this tool is called (either the forwarding\nfunction for pure transformations or a custom user function).\n- `forwarding_fn`: Internal function that handles argument transformation and\nvalidation when forward() is called from custom functions.\n\n\n**Methods:**\n\n#### `run` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L265\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun(self, arguments: dict[str, Any]) -> ToolResult\n```\n\nRun the tool with context set for forward() functions.\n\nThis method executes the tool's function while setting up the context\nthat allows forward() and forward_raw() to work correctly within custom\nfunctions.\n\n**Args:**\n- `arguments`: Dictionary of arguments to pass to the tool's function.\n\n**Returns:**\n- ToolResult object containing content and optional structured output.\n\n\n#### `from_tool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L361\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_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\n```\n\nCreate a transformed tool from a parent tool.\n\n**Args:**\n- `tool`: The parent tool to transform.\n- `transform_fn`: Optional custom function. Can use forward() and forward_raw()\nto call the parent tool. Functions with **kwargs receive transformed\nargument names.\n- `name`: New name for the tool. Defaults to parent tool's name.\n- `version`: New version for the tool. Defaults to parent tool's version.\n- `title`: New title for the tool. Defaults to parent tool's title.\n- `transform_args`: Optional transformations for parent tool arguments.\nOnly specified arguments are transformed, others pass through unchanged\\:\n- Simple rename (str)\n- Complex transformation (rename/description/default/drop) (ArgTransform)\n- Drop the argument (None)\n- `description`: New description. Defaults to parent's description.\n- `tags`: New tags. Defaults to parent's tags.\n- `annotations`: New annotations. Defaults to parent's annotations.\n- `output_schema`: Control output schema for structured outputs\\:\n- None (default)\\: Inherit from transform_fn if available, then parent tool\n- dict\\: Use custom output schema\n- False\\: Disable output schema and structured outputs\n- `serializer`: Deprecated. Return ToolResult from your tools for full control over serialization.\n- `meta`: Control meta information\\:\n- NotSet (default)\\: Inherit from parent tool\n- dict\\: Use custom meta information\n- None\\: Remove meta information\n\n**Returns:**\n- TransformedTool with the specified transformations.\n\n**Examples:**\n\n# Transform specific arguments only\n```python\nTool.from_tool(parent, transform_args={\"old\": \"new\"})  # Others unchanged\n```\n\n# Custom function with partial transforms\n```python\nasync def custom(x: int, y: int) -> str:\n    result = await forward(x=x, y=y)\n    return f\"Custom: {result}\"\n\nTool.from_tool(parent, transform_fn=custom, transform_args={\"a\": \"x\", \"b\": \"y\"})\n```\n\n# Using **kwargs (gets all args, transformed and untransformed)\n```python\nasync def flexible(**kwargs) -> str:\n    result = await forward(**kwargs)\n    return f\"Got: {kwargs}\"\n\nTool.from_tool(parent, transform_fn=flexible, transform_args={\"a\": \"x\"})\n```\n\n# Control structured outputs and schemas\n```python\n# Custom output schema\nTool.from_tool(parent, output_schema={\n    \"type\": \"object\",\n    \"properties\": {\"status\": {\"type\": \"string\"}}\n})\n\n# Disable structured outputs\nTool.from_tool(parent, output_schema=None)\n\n# Return ToolResult for full control\nasync def custom_output(**kwargs) -> ToolResult:\n    result = await forward(**kwargs)\n    return ToolResult(\n        content=[TextContent(text=\"Summary\")],\n        structured_content={\"processed\": True}\n    )\n```\n\n\n### `ToolTransformConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L921\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nProvides a way to transform a tool.\n\n\n**Methods:**\n\n#### `apply` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/tools/tool_transform.py#L954\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\napply(self, tool: Tool) -> TransformedTool\n```\n\nCreate a TransformedTool from a provided tool and this transformation configuration.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.utilities`\n\n\nFastMCP utility modules.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-async_utils.mdx",
    "content": "---\ntitle: async_utils\nsidebarTitle: async_utils\n---\n\n# `fastmcp.utilities.async_utils`\n\n\nAsync utilities for FastMCP.\n\n## Functions\n\n### `is_coroutine_function` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/async_utils.py#L15\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_coroutine_function(fn: Any) -> bool\n```\n\n\nCheck if a callable is a coroutine function, unwrapping functools.partial.\n\n``inspect.iscoroutinefunction`` returns ``False`` for\n``functools.partial`` objects wrapping an async function on Python < 3.12.\nThis helper unwraps any layers of ``partial`` before checking.\n\n\n### `call_sync_fn_in_threadpool` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/async_utils.py#L27\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncall_sync_fn_in_threadpool(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any\n```\n\n\nCall a sync function in a threadpool to avoid blocking the event loop.\n\nUses anyio.to_thread.run_sync which properly propagates contextvars,\nmaking this safe for functions that depend on context (like dependency injection).\n\n\n### `gather` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/async_utils.py#L52\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngather(*awaitables: Awaitable[T]) -> list[T] | list[T | BaseException]\n```\n\n\nRun awaitables concurrently and return results in order.\n\nUses anyio TaskGroup for structured concurrency.\n\n**Args:**\n- `*awaitables`: Awaitables to run concurrently\n- `return_exceptions`: If True, exceptions are returned in results.\n              If False, first exception cancels all and raises.\n\n**Returns:**\n- List of results in the same order as input awaitables.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-auth.mdx",
    "content": "---\ntitle: auth\nsidebarTitle: auth\n---\n\n# `fastmcp.utilities.auth`\n\n\nAuthentication utility helpers.\n\n## Functions\n\n### `decode_jwt_header` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/auth.py#L32\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndecode_jwt_header(token: str) -> dict[str, Any]\n```\n\n\nDecode JWT header without signature verification.\n\nUseful for extracting the key ID (kid) for JWKS lookup.\n\n**Args:**\n- `token`: JWT token string (header.payload.signature)\n\n**Returns:**\n- Decoded header as a dictionary\n\n**Raises:**\n- `ValueError`: If token is not a valid JWT format\n\n\n### `decode_jwt_payload` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/auth.py#L49\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndecode_jwt_payload(token: str) -> dict[str, Any]\n```\n\n\nDecode JWT payload without signature verification.\n\nUse only for tokens received directly from trusted sources (e.g., IdP token endpoints).\n\n**Args:**\n- `token`: JWT token string (header.payload.signature)\n\n**Returns:**\n- Decoded payload as a dictionary\n\n**Raises:**\n- `ValueError`: If token is not a valid JWT format\n\n\n### `parse_scopes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/auth.py#L66\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_scopes(value: Any) -> list[str] | None\n```\n\n\nParse scopes from environment variables or settings values.\n\nAccepts either a JSON array string, a comma- or space-separated string,\na list of strings, or ``None``. Returns a list of scopes or ``None`` if\nno value is provided.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-cli.mdx",
    "content": "---\ntitle: cli\nsidebarTitle: cli\n---\n\n# `fastmcp.utilities.cli`\n\n## Functions\n\n### `is_already_in_uv_subprocess` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L28\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_already_in_uv_subprocess() -> bool\n```\n\n\nCheck if we're already running in a FastMCP uv subprocess.\n\n\n### `load_and_merge_config` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L33\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_and_merge_config(server_spec: str | None, **cli_overrides) -> tuple[MCPServerConfig, str]\n```\n\n\nLoad config from server_spec and apply CLI overrides.\n\nThis consolidates the config parsing logic that was duplicated across\nrun, inspect, and dev commands.\n\n**Args:**\n- `server_spec`: Python file, config file, URL, or None to auto-detect\n- `cli_overrides`: CLI arguments that override config values\n\n**Returns:**\n- Tuple of (MCPServerConfig, resolved_server_spec)\n\n\n### `log_server_banner` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L201\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlog_server_banner(server: FastMCP[Any]) -> None\n```\n\n\nCreates and logs a formatted banner with server information and logo.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-components.mdx",
    "content": "---\ntitle: components\nsidebarTitle: components\n---\n\n# `fastmcp.utilities.components`\n\n## Functions\n\n### `get_fastmcp_metadata` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L26\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_fastmcp_metadata(meta: dict[str, Any] | None) -> FastMCPMeta\n```\n\n\nExtract FastMCP metadata from a component's meta dict.\n\nHandles both the current `fastmcp` namespace and the legacy `_fastmcp`\nnamespace for compatibility with older FastMCP servers.\n\n\n## Classes\n\n### `FastMCPMeta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L20\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n### `FastMCPComponent` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L74\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for FastMCP tools, prompts, resources, and resource templates.\n\n\n**Methods:**\n\n#### `make_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L125\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmake_key(cls, identifier: str) -> str\n```\n\nConstruct the lookup key for this component type.\n\n**Args:**\n- `identifier`: The raw identifier (name for tools/prompts, uri for resources)\n\n**Returns:**\n- A prefixed key like \"tool:name\" or \"resource:uri\"\n\n\n#### `key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L139\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nkey(self) -> str\n```\n\nThe globally unique lookup key for this component.\n\nFormat: \"{key_prefix}:{identifier}@{version}\" or \"{key_prefix}:{identifier}@\"\ne.g. \"tool:my_tool@v2\", \"tool:my_tool@\", \"resource:file://x.txt@\"\n\nThe @ suffix is ALWAYS present to enable unambiguous parsing of keys\n(URIs may contain @ characters, so we always include the delimiter).\n\nSubclasses should override this to use their specific identifier.\nBase implementation uses name.\n\n\n#### `get_meta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L154\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_meta(self) -> dict[str, Any]\n```\n\nGet the meta information about the component.\n\nReturns a dict that always includes a `fastmcp` key containing:\n- `tags`: sorted list of component tags\n- `version`: component version (only if set)\n\nInternal keys (prefixed with `_`) are stripped from the fastmcp namespace.\n\n\n#### `enable` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L202\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nenable(self) -> None\n```\n\nRemoved in 3.0. Use server.enable(keys=[...]) instead.\n\n\n#### `disable` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L209\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndisable(self) -> None\n```\n\nRemoved in 3.0. Use server.disable(keys=[...]) instead.\n\n\n#### `copy` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L216\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncopy(self) -> Self\n```\n\nCreate a copy of the component.\n\n\n#### `register_with_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L220\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nregister_with_docket(self, docket: Docket) -> None\n```\n\nRegister this component with docket for background execution.\n\nNo-ops if task_config.mode is \"forbidden\". Subclasses override to\nregister their callable (self.run, self.read, self.render, or self.fn).\n\n\n#### `add_to_docket` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L228\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nadd_to_docket(self, docket: Docket, *args: Any, **kwargs: Any) -> Execution\n```\n\nSchedule this component for background execution via docket.\n\nSubclasses override this to handle their specific calling conventions:\n- Tool: add_to_docket(docket, arguments: dict, **kwargs)\n- Resource: add_to_docket(docket, **kwargs)\n- ResourceTemplate: add_to_docket(docket, params: dict, **kwargs)\n- Prompt: add_to_docket(docket, arguments: dict | None, **kwargs)\n\nThe **kwargs are passed through to docket.add() (e.g., key=task_key).\n\n\n#### `get_span_attributes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/components.py#L250\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_span_attributes(self) -> dict[str, Any]\n```\n\nReturn span attributes for telemetry.\n\nSubclasses should call super() and merge their specific attributes.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-exceptions.mdx",
    "content": "---\ntitle: exceptions\nsidebarTitle: exceptions\n---\n\n# `fastmcp.utilities.exceptions`\n\n## Functions\n\n### `iter_exc` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/exceptions.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\niter_exc(group: BaseExceptionGroup)\n```\n\n### `get_catch_handlers` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/exceptions.py#L42\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]]\n```\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-http.mdx",
    "content": "---\ntitle: http\nsidebarTitle: http\n---\n\n# `fastmcp.utilities.http`\n\n## Functions\n\n### `find_available_port` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/http.py#L4\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfind_available_port() -> int\n```\n\n\nFind an available port by letting the OS assign one.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-inspect.mdx",
    "content": "---\ntitle: inspect\nsidebarTitle: inspect\n---\n\n# `fastmcp.utilities.inspect`\n\n\nUtilities for inspecting FastMCP instances.\n\n## Functions\n\n### `inspect_fastmcp_v2` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L100\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo\n```\n\n\nExtract information from a FastMCP v2.x instance.\n\n**Args:**\n- `mcp`: The FastMCP v2.x instance to inspect\n\n**Returns:**\n- FastMCPInfo dataclass containing the extracted information\n\n\n### `inspect_fastmcp_v1` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L236\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo\n```\n\n\nExtract information from a FastMCP v1.x instance using a Client.\n\n**Args:**\n- `mcp`: The FastMCP v1.x instance to inspect\n\n**Returns:**\n- FastMCPInfo dataclass containing the extracted information\n\n\n### `inspect_fastmcp` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L378\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ninspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo\n```\n\n\nExtract information from a FastMCP instance into a dataclass.\n\nThis function automatically detects whether the instance is FastMCP v1.x or v2.x\nand uses the appropriate extraction method.\n\n**Args:**\n- `mcp`: The FastMCP instance to inspect (v1.x or v2.x)\n\n**Returns:**\n- FastMCPInfo dataclass containing the extracted information\n\n\n### `format_fastmcp_info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L403\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_fastmcp_info(info: FastMCPInfo) -> bytes\n```\n\n\nFormat FastMCPInfo as FastMCP-specific JSON.\n\nThis includes FastMCP-specific fields like tags, enabled, annotations, etc.\n\n\n### `format_mcp_info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L432\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes\n```\n\n\nFormat server info as standard MCP protocol JSON.\n\nUses Client to get the standard MCP protocol format with camelCase fields.\nIncludes version metadata at the top level.\n\n\n### `format_info` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L465\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_info(mcp: FastMCP[Any] | FastMCP1x, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes\n```\n\n\nFormat server information according to the specified format.\n\n**Args:**\n- `mcp`: The FastMCP instance\n- `format`: Output format (\"fastmcp\" or \"mcp\")\n- `info`: Pre-extracted FastMCPInfo (optional, will be extracted if not provided)\n\n**Returns:**\n- JSON bytes in the requested format\n\n\n## Classes\n\n### `ToolInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L19\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation about a tool.\n\n\n### `PromptInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L35\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation about a prompt.\n\n\n### `ResourceInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L49\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation about a resource.\n\n\n### `TemplateInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L65\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation about a resource template.\n\n\n### `FastMCPInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L82\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation extracted from a FastMCP instance.\n\n\n### `InspectFormat` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/inspect.py#L396\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOutput format for inspect command.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-json_schema.mdx",
    "content": "---\ntitle: json_schema\nsidebarTitle: json_schema\n---\n\n# `fastmcp.utilities.json_schema`\n\n## Functions\n\n### `dereference_refs` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/json_schema.py#L76\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndereference_refs(schema: dict[str, Any]) -> dict[str, Any]\n```\n\n\nResolve all $ref references in a JSON schema by inlining definitions.\n\nThis function resolves $ref references that point to $defs, replacing them\nwith the actual definition content while preserving sibling keywords (like\ndescription, default, examples) that Pydantic places alongside $ref.\n\nThis is necessary because some MCP clients (e.g., VS Code Copilot) don't\nproperly handle $ref in tool input schemas.\n\nFor self-referencing/circular schemas where full dereferencing is not possible,\nthis function falls back to resolving only the root-level $ref while preserving\n$defs for nested references.\n\nOnly local ``$ref`` values (those starting with ``#``) are resolved.\nRemote URIs (``http://``, ``file://``, etc.) are stripped before\nresolution to prevent SSRF / local-file-inclusion attacks when proxying\nschemas from untrusted servers.\n\n**Args:**\n- `schema`: JSON schema dict that may contain $ref references\n\n**Returns:**\n- A new schema dict with $ref resolved where possible and $defs removed\n- when no longer needed\n\n\n### `resolve_root_ref` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/json_schema.py#L213\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nresolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]\n```\n\n\nResolve $ref at root level to meet MCP spec requirements.\n\nMCP specification requires outputSchema to have \"type\": \"object\" at the root level.\nWhen Pydantic generates schemas for self-referential models, it uses $ref at the\nroot level pointing to $defs. This function resolves such references by inlining\nthe referenced definition while preserving $defs for nested references.\n\n**Args:**\n- `schema`: JSON schema dict that may have $ref at root level\n\n**Returns:**\n- A new schema dict with root-level $ref resolved, or the original schema\n- if no resolution is needed\n\n\n### `compress_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/json_schema.py#L446\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncompress_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]\n```\n\n\nCompress and optimize a JSON schema for MCP compatibility.\n\n**Args:**\n- `schema`: The schema to compress\n- `prune_params`: List of parameter names to remove from properties\n- `prune_additional_properties`: Whether to remove additionalProperties\\: false.\nDefaults to False to maintain MCP client compatibility, as some clients\n(e.g., Claude) require additionalProperties\\: false for strict validation.\n- `prune_titles`: Whether to remove title fields from the schema\n- `dereference`: Whether to dereference $ref by inlining definitions.\nDefaults to False; dereferencing is typically handled by\nmiddleware at serve-time instead.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-json_schema_type.mdx",
    "content": "---\ntitle: json_schema_type\nsidebarTitle: json_schema_type\n---\n\n# `fastmcp.utilities.json_schema_type`\n\n\nConvert JSON Schema to Python types with validation.\n\nThe json_schema_to_type function converts a JSON Schema into a Python type that can be used\nfor validation with Pydantic. It supports:\n\n- Basic types (string, number, integer, boolean, null)\n- Complex types (arrays, objects)\n- Format constraints (date-time, email, uri)\n- Numeric constraints (minimum, maximum, multipleOf)\n- String constraints (minLength, maxLength, pattern)\n- Array constraints (minItems, maxItems, uniqueItems)\n- Object properties with defaults\n- References and recursive schemas\n- Enums and constants\n- Union types\n\nExample:\n    ```python\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"type\": \"string\", \"minLength\": 1},\n            \"age\": {\"type\": \"integer\", \"minimum\": 0},\n            \"email\": {\"type\": \"string\", \"format\": \"email\"}\n        },\n        \"required\": [\"name\", \"age\"]\n    }\n\n    # Name is optional and will be inferred from schema's \"title\" property if not provided\n    Person = json_schema_to_type(schema)\n    # Creates a validated dataclass with name, age, and optional email fields\n    ```\n\n\n## Functions\n\n### `json_schema_to_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/json_schema_type.py#L111\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\njson_schema_to_type(schema: Mapping[str, Any], name: str | None = None) -> type\n```\n\n\nConvert JSON schema to appropriate Python type with validation.\n\n**Args:**\n- `schema`: A JSON Schema dictionary defining the type structure and validation rules\n- `name`: Optional name for object schemas. Only allowed when schema type is \"object\".\nIf not provided for objects, name will be inferred from schema's \"title\"\nproperty or default to \"Root\".\n\n**Returns:**\n- A Python type (typically a dataclass for objects) with Pydantic validation\n\n**Raises:**\n- `ValueError`: If a name is provided for a non-object schema\n\n**Examples:**\n\nCreate a dataclass from an object schema:\n```python\nschema = {\n    \"type\": \"object\",\n    \"title\": \"Person\",\n    \"properties\": {\n        \"name\": {\"type\": \"string\", \"minLength\": 1},\n        \"age\": {\"type\": \"integer\", \"minimum\": 0},\n        \"email\": {\"type\": \"string\", \"format\": \"email\"}\n    },\n    \"required\": [\"name\", \"age\"]\n}\n\nPerson = json_schema_to_type(schema)\n# Creates a dataclass with name, age, and optional email fields:\n# @dataclass\n# class Person:\n#     name: str\n#     age: int\n#     email: str | None = None\n```\nPerson(name=\"John\", age=30)\n\nCreate a scalar type with constraints:\n```python\nschema = {\n    \"type\": \"string\",\n    \"minLength\": 3,\n    \"pattern\": \"^[A-Z][a-z]+$\"\n}\n\nNameType = json_schema_to_type(schema)\n# Creates Annotated[str, StringConstraints(min_length=3, pattern=\"^[A-Z][a-z]+$\")]\n\n@dataclass\nclass Name:\n    name: NameType\n```\n\n\n## Classes\n\n### `JSONSchema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/json_schema_type.py#L78\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-lifespan.mdx",
    "content": "---\ntitle: lifespan\nsidebarTitle: lifespan\n---\n\n# `fastmcp.utilities.lifespan`\n\n\nLifespan utilities for combining async context manager lifespans.\n\n## Functions\n\n### `combine_lifespans` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/lifespan.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncombine_lifespans(*lifespans: Callable[[AppT], AbstractAsyncContextManager[Mapping[str, Any] | None]]) -> Callable[[AppT], AbstractAsyncContextManager[dict[str, Any]]]\n```\n\n\nCombine multiple lifespans into a single lifespan.\n\nUseful when mounting FastMCP into FastAPI and you need to run\nboth your app's lifespan and the MCP server's lifespan.\n\nWorks with both FastAPI-style lifespans (yield None) and FastMCP-style\nlifespans (yield dict). Results are merged; later lifespans override\nearlier ones on key conflicts.\n\nLifespans are entered in order and exited in reverse order (LIFO).\n\n**Args:**\n- `*lifespans`: Lifespan context manager factories to combine.\n\n**Returns:**\n- A combined lifespan context manager factory.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-logging.mdx",
    "content": "---\ntitle: logging\nsidebarTitle: logging\n---\n\n# `fastmcp.utilities.logging`\n\n\nLogging utilities for FastMCP.\n\n## Functions\n\n### `get_logger` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/logging.py#L14\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_logger(name: str) -> logging.Logger\n```\n\n\nGet a logger nested under FastMCP namespace.\n\n**Args:**\n- `name`: the name of the logger, which will be prefixed with 'FastMCP.'\n\n**Returns:**\n- a configured logger instance\n\n\n### `configure_logging` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/logging.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconfigure_logging(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | int = 'INFO', logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any) -> None\n```\n\n\nConfigure logging for FastMCP.\n\n**Args:**\n- `logger`: the logger to configure\n- `level`: the log level to use\n- `rich_kwargs`: the parameters to use for creating RichHandler\n\n\n### `temporary_log_level` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/logging.py#L111\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntemporary_log_level(level: str | None, logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any)\n```\n\n\nContext manager to temporarily set log level and restore it afterwards.\n\n**Args:**\n- `level`: The temporary log level to set (e.g., \"DEBUG\", \"INFO\")\n- `logger`: Optional logger to configure (defaults to FastMCP logger)\n- `enable_rich_tracebacks`: Whether to enable rich tracebacks\n- `**rich_kwargs`: Additional parameters for RichHandler\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.utilities.mcp_server_config`\n\n\nFastMCP Configuration module.\n\nThis module provides versioned configuration support for FastMCP servers.\nThe current version is v1, which is re-exported here for convenience.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.utilities.mcp_server_config.v1`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.environments`\n\n\nEnvironment configuration for MCP servers.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.environments.base`\n\n## Classes\n\n### `Environment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py#L7\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase class for environment configuration.\n\n\n**Methods:**\n\n#### `build_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py#L13\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nbuild_command(self, command: list[str]) -> list[str]\n```\n\nBuild the full command with environment setup.\n\n**Args:**\n- `command`: Base command to wrap with environment setup\n\n**Returns:**\n- Full command ready for subprocess execution\n\n\n#### `prepare` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare(self, output_dir: Path | None = None) -> None\n```\n\nPrepare the environment (optional, can be no-op).\n\n**Args:**\n- `output_dir`: Directory for persistent environment setup\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx",
    "content": "---\ntitle: uv\nsidebarTitle: uv\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.environments.uv`\n\n## Classes\n\n### `UVEnvironment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L14\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for Python environment setup.\n\n\n**Methods:**\n\n#### `build_command` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L49\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nbuild_command(self, command: list[str]) -> list[str]\n```\n\nBuild complete uv run command with environment args and command to execute.\n\n**Args:**\n- `command`: Command to execute (e.g., [\"fastmcp\", \"run\", \"server.py\"])\n\n**Returns:**\n- Complete command ready for subprocess.run, including \"uv\" prefix if needed.\n- If no environment configuration is set, returns the command unchanged.\n\n\n#### `prepare` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L109\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare(self, output_dir: Path | None = None) -> None\n```\n\nPrepare the Python environment using uv.\n\n**Args:**\n- `output_dir`: Directory where the persistent uv project will be created.\n       If None, creates a temporary directory for ephemeral use.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx",
    "content": "---\ntitle: mcp_server_config\nsidebarTitle: mcp_server_config\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.mcp_server_config`\n\n\nFastMCP Configuration File Support.\n\nThis module provides support for fastmcp.json configuration files that allow\nusers to specify server settings in a declarative format instead of using\ncommand-line arguments.\n\n\n## Functions\n\n### `generate_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L416\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None\n```\n\n\nGenerate JSON schema for fastmcp.json files.\n\nThis is used to create the schema file that IDEs can use for\nvalidation and auto-completion.\n\n**Args:**\n- `output_path`: Optional path to write the schema to. If provided,\n        writes the schema and returns None. If not provided,\n        returns the schema as a dictionary.\n\n**Returns:**\n- JSON schema as a dictionary if output_path is None, otherwise None\n\n\n## Classes\n\n### `Deployment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for server deployment and runtime settings.\n\n\n**Methods:**\n\n#### `apply_runtime_settings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L85\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\napply_runtime_settings(self, config_path: Path | None = None) -> None\n```\n\nApply runtime settings like environment variables and working directory.\n\n**Args:**\n- `config_path`: Path to config file for resolving relative paths\n\nEnvironment variables support interpolation with ${VAR_NAME} syntax.\nFor example: \"API_URL\": \"https://api.${ENVIRONMENT}.example.com\"\nwill substitute the value of the ENVIRONMENT variable at runtime.\n\n\n### `MCPServerConfig` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L134\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nConfiguration for a FastMCP server.\n\nThis configuration file allows you to specify all settings needed to run\na FastMCP server in a declarative format.\n\n\n**Methods:**\n\n#### `validate_source` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L183\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_source(cls, v: dict | Source) -> SourceType\n```\n\nValidate and convert source to proper format.\n\nSupports:\n- Dict format: `{\"path\": \"server.py\", \"entrypoint\": \"app\"}`\n- FileSystemSource instance (passed through)\n\nNo string parsing happens here - that's only at CLI boundaries.\nMCPServerConfig works only with properly typed objects.\n\n\n#### `validate_environment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L199\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_environment(cls, v: dict | Any) -> EnvironmentType\n```\n\nEnsure environment has a type field for discrimination.\n\nFor backward compatibility, if no type is specified, default to \"uv\".\n\n\n#### `validate_deployment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L210\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nvalidate_deployment(cls, v: dict | Deployment) -> Deployment\n```\n\nValidate and convert deployment to Deployment.\n\nAccepts:\n- Deployment instance\n- dict that can be converted to Deployment\n\n\n#### `from_file` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L223\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_file(cls, file_path: Path) -> MCPServerConfig\n```\n\nLoad configuration from a JSON file.\n\n**Args:**\n- `file_path`: Path to the configuration file\n\n**Returns:**\n- MCPServerConfig instance\n\n**Raises:**\n- `FileNotFoundError`: If the file doesn't exist\n- `json.JSONDecodeError`: If the file is not valid JSON\n- `pydantic.ValidationError`: If the configuration is invalid\n\n\n#### `from_cli_args` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L246\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfrom_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> MCPServerConfig\n```\n\nCreate a config from CLI arguments.\n\nThis allows us to have a single code path where everything\ngoes through a config object.\n\n**Args:**\n- `source`: Server source (FileSystemSource instance)\n- `transport`: Transport protocol\n- `host`: Host for HTTP transport\n- `port`: Port for HTTP transport\n- `path`: URL path for server\n- `log_level`: Logging level\n- `python`: Python version\n- `dependencies`: Python packages to install\n- `requirements`: Path to requirements file\n- `project`: Path to project directory\n- `editable`: Path to install in editable mode\n- `env`: Environment variables\n- `cwd`: Working directory\n- `args`: Server arguments\n\n**Returns:**\n- MCPServerConfig instance\n\n\n#### `find_config` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L323\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfind_config(cls, start_path: Path | None = None) -> Path | None\n```\n\nFind a fastmcp.json file in the specified directory.\n\n**Args:**\n- `start_path`: Directory to look in (defaults to current directory)\n\n**Returns:**\n- Path to the configuration file, or None if not found\n\n\n#### `prepare` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L342\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None\n```\n\nPrepare environment and source for execution.\n\nWhen output_dir is provided, creates a persistent uv project.\nWhen output_dir is None, does ephemeral caching (for backwards compatibility).\n\n**Args:**\n- `skip_source`: Skip source preparation if True\n- `output_dir`: Directory to create the persistent uv project in (optional)\n\n\n#### `prepare_environment` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L363\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare_environment(self, output_dir: Path | None = None) -> None\n```\n\nPrepare the Python environment.\n\n**Args:**\n- `output_dir`: If provided, creates a persistent uv project in this directory.\n       If None, just populates uv's cache for ephemeral use.\n\nDelegates to the environment's prepare() method\n\n\n#### `prepare_source` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L374\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare_source(self) -> None\n```\n\nPrepare the source for loading.\n\nDelegates to the source's prepare() method.\n\n\n#### `run_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L381\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_server(self, **kwargs: Any) -> None\n```\n\nLoad and run the server with this configuration.\n\n**Args:**\n- `**kwargs`: Additional arguments to pass to server.run_async()\n     These override config settings\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.sources`\n\n*This module is empty or contains only private/internal implementations.*\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx",
    "content": "---\ntitle: base\nsidebarTitle: base\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.sources.base`\n\n## Classes\n\n### `Source` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py#L7\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nAbstract base class for all source types.\n\n\n**Methods:**\n\n#### `prepare` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nprepare(self) -> None\n```\n\nPrepare the source (download, clone, install, etc).\n\nFor sources that need preparation (e.g., git clone, download),\nthis method performs that preparation. For sources that don't\nneed preparation (e.g., local files), this is a no-op.\n\n\n#### `load_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py#L22\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_server(self) -> Any\n```\n\nLoad and return the FastMCP server instance.\n\nMust be called after prepare() if the source requires preparation.\nAll information needed to load the server should be available\nas attributes on the source instance.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx",
    "content": "---\ntitle: filesystem\nsidebarTitle: filesystem\n---\n\n# `fastmcp.utilities.mcp_server_config.v1.sources.filesystem`\n\n## Classes\n\n### `FileSystemSource` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py#L16\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSource for local Python files.\n\n\n**Methods:**\n\n#### `parse_path_with_object` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_path_with_object(cls, v: str) -> str\n```\n\nParse path:object syntax and extract the object name.\n\nThis validator runs before the model is created, allowing us to\nhandle the \"file.py:object\" syntax at the model boundary.\n\n\n#### `load_server` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py#L64\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nload_server(self) -> Any\n```\n\nLoad server from filesystem.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-__init__.mdx",
    "content": "---\ntitle: __init__\nsidebarTitle: __init__\n---\n\n# `fastmcp.utilities.openapi`\n\n\nOpenAPI utilities for FastMCP - refactored for better maintainability.\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-director.mdx",
    "content": "---\ntitle: director\nsidebarTitle: director\n---\n\n# `fastmcp.utilities.openapi.director`\n\n\nRequest director using openapi-core for stateless HTTP request building.\n\n## Classes\n\n### `RequestDirector` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/director.py#L16\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBuilds httpx.Request objects from HTTPRoute and arguments using openapi-core.\n\n\n**Methods:**\n\n#### `build` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/director.py#L23\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nbuild(self, route: HTTPRoute, flat_args: dict[str, Any], base_url: str = 'http://localhost') -> httpx.Request\n```\n\nConstructs a final httpx.Request object, handling all OpenAPI serialization.\n\n**Args:**\n- `route`: HTTPRoute containing OpenAPI operation details\n- `flat_args`: Flattened arguments from LLM (may include suffixed parameters)\n- `base_url`: Base URL for the request\n\n**Returns:**\n- httpx.Request: Properly formatted HTTP request\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx",
    "content": "---\ntitle: formatters\nsidebarTitle: formatters\n---\n\n# `fastmcp.utilities.openapi.formatters`\n\n\nParameter formatting functions for OpenAPI operations.\n\n## Functions\n\n### `format_array_parameter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_array_parameter(values: list, parameter_name: str, is_query_parameter: bool = False) -> str | list\n```\n\n\nFormat an array parameter according to OpenAPI specifications.\n\n**Args:**\n- `values`: List of values to format\n- `parameter_name`: Name of the parameter (for error messages)\n- `is_query_parameter`: If True, can return list for explode=True behavior\n\n**Returns:**\n- String (comma-separated) or list (for query params with explode=True)\n\n\n### `format_deep_object_parameter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L66\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_deep_object_parameter(param_value: dict, parameter_name: str) -> dict[str, str]\n```\n\n\nFormat a dictionary parameter for deep-object style serialization.\n\nAccording to OpenAPI 3.0 spec, deepObject style with explode=true serializes\nobject properties as separate query parameters with bracket notation.\n\nFor example, `{\"id\": \"123\", \"type\": \"user\"}` becomes\n`param[id]=123&param[type]=user`.\n\n**Args:**\n- `param_value`: Dictionary value to format\n- `parameter_name`: Name of the parameter\n\n**Returns:**\n- Dictionary with bracketed parameter names as keys\n\n\n### `generate_example_from_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L100\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ngenerate_example_from_schema(schema: JsonSchema | None) -> Any\n```\n\n\nGenerate a simple example value from a JSON schema dictionary.\nVery basic implementation focusing on types.\n\n\n### `format_json_for_description` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L183\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_json_for_description(data: Any, indent: int = 2) -> str\n```\n\n\nFormats Python data as a JSON string block for Markdown.\n\n\n### `format_description_with_responses` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L192\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nformat_description_with_responses(base_description: str, responses: dict[str, Any], parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str\n```\n\n\nFormats the base description string with response, parameter, and request body information.\n\n**Args:**\n- `base_description`: The initial description to be formatted.\n- `responses`: A dictionary of response information, keyed by status code.\n- `parameters`: A list of parameter information,\nincluding path and query parameters. Each parameter includes details such as name,\nlocation, whether it is required, and a description.\n- `request_body`: Information about the request body,\nincluding its description, whether it is required, and its content schema.\n\n**Returns:**\n- The formatted description string with additional details about responses, parameters,\n- and the request body.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx",
    "content": "---\ntitle: json_schema_converter\nsidebarTitle: json_schema_converter\n---\n\n# `fastmcp.utilities.openapi.json_schema_converter`\n\n\n\nClean OpenAPI 3.0 to JSON Schema converter for the experimental parser.\n\nThis module provides a systematic approach to converting OpenAPI 3.0 schemas\nto JSON Schema, inspired by py-openapi-schema-to-json-schema but optimized\nfor our specific use case.\n\n\n## Functions\n\n### `convert_openapi_schema_to_json_schema` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/json_schema_converter.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_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]\n```\n\n\nConvert an OpenAPI schema to JSON Schema format.\n\nThis is a clean, systematic approach that:\n1. Removes OpenAPI-specific fields\n2. Converts nullable fields to type arrays (for OpenAPI 3.0 only)\n3. Converts oneOf to anyOf for overlapping union handling\n4. Recursively processes nested schemas\n5. Optionally removes readOnly/writeOnly properties\n\n**Args:**\n- `schema`: OpenAPI schema dictionary\n- `openapi_version`: OpenAPI version for optimization\n- `remove_read_only`: Whether to remove readOnly properties\n- `remove_write_only`: Whether to remove writeOnly properties\n- `convert_one_of_to_any_of`: Whether to convert oneOf to anyOf\n\n**Returns:**\n- JSON Schema-compatible dictionary\n\n\n### `convert_schema_definitions` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/json_schema_converter.py#L322\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nconvert_schema_definitions(schema_definitions: dict[str, Any] | None, openapi_version: str | None = None, **kwargs) -> dict[str, Any]\n```\n\n\nConvert a dictionary of OpenAPI schema definitions to JSON Schema.\n\n**Args:**\n- `schema_definitions`: Dictionary of schema definitions\n- `openapi_version`: OpenAPI version for optimization\n- `**kwargs`: Additional arguments passed to convert_openapi_schema_to_json_schema\n\n**Returns:**\n- Dictionary of converted schema definitions\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-models.mdx",
    "content": "---\ntitle: models\nsidebarTitle: models\n---\n\n# `fastmcp.utilities.openapi.models`\n\n\nIntermediate Representation (IR) models for OpenAPI operations.\n\n## Classes\n\n### `ParameterInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/models.py#L17\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRepresents a single parameter for an HTTP operation in our IR.\n\n\n### `RequestBodyInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/models.py#L29\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRepresents the request body for an HTTP operation in our IR.\n\n\n### `ResponseInfo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/models.py#L39\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nRepresents response information in our IR.\n\n\n### `HTTPRoute` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/models.py#L47\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nIntermediate Representation for a single OpenAPI operation.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-parser.mdx",
    "content": "---\ntitle: parser\nsidebarTitle: parser\n---\n\n# `fastmcp.utilities.openapi.parser`\n\n\nOpenAPI parsing logic for converting OpenAPI specs to HTTPRoute objects.\n\n## Functions\n\n### `parse_openapi_to_http_routes` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/parser.py#L55\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute]\n```\n\n\nParses an OpenAPI schema dictionary into a list of HTTPRoute objects\nusing the openapi-pydantic library.\n\nSupports both OpenAPI 3.0.x and 3.1.x versions.\n\n\n## Classes\n\n### `OpenAPIParser` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/parser.py#L109\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nUnified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1.\n\n\n**Methods:**\n\n#### `parse` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/parser.py#L663\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse(self) -> list[HTTPRoute]\n```\n\nParse the OpenAPI schema into HTTP routes.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx",
    "content": "---\ntitle: schemas\nsidebarTitle: schemas\n---\n\n# `fastmcp.utilities.openapi.schemas`\n\n\nSchema manipulation utilities for OpenAPI operations.\n\n## Functions\n\n### `clean_schema_for_display` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/schemas.py#L12\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nclean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None\n```\n\n\nClean up a schema dictionary for display by removing internal/complex fields.\n\n\n### `extract_output_schema_from_responses` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/openapi/schemas.py#L474\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nextract_output_schema_from_responses(responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None, openapi_version: str | None = None) -> dict[str, Any] | None\n```\n\n\nExtract output schema from OpenAPI responses for use as MCP tool output schema.\n\nThis function finds the first successful response (200, 201, 202, 204) with a\nJSON-compatible content type and extracts its schema. If the schema is not an\nobject type, it wraps it to comply with MCP requirements.\n\n**Args:**\n- `responses`: Dictionary of ResponseInfo objects keyed by status code\n- `schema_definitions`: Optional schema definitions to include in the output schema\n- `openapi_version`: OpenAPI version string, used to optimize nullable field handling\n\n**Returns:**\n- MCP-compliant output schema with potential wrapping, or None if no suitable schema found\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-pagination.mdx",
    "content": "---\ntitle: pagination\nsidebarTitle: pagination\n---\n\n# `fastmcp.utilities.pagination`\n\n\nPagination utilities for MCP list operations.\n\n## Functions\n\n### `paginate_sequence` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/pagination.py#L50\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\npaginate_sequence(items: Sequence[T], cursor: str | None, page_size: int) -> tuple[list[T], str | None]\n```\n\n\nPaginate a sequence of items.\n\n**Args:**\n- `items`: The full sequence to paginate.\n- `cursor`: Optional cursor from a previous request. None for first page.\n- `page_size`: Maximum number of items per page.\n\n**Returns:**\n- Tuple of (page_items, next_cursor). next_cursor is None if no more pages.\n\n**Raises:**\n- `ValueError`: If the cursor is invalid.\n\n\n## Classes\n\n### `CursorState` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/pagination.py#L16\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInternal representation of pagination cursor state.\n\nThe cursor encodes the offset into the result set. This is opaque to clients\nper the MCP spec - they should not parse or modify cursors.\n\n\n**Methods:**\n\n#### `encode` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/pagination.py#L25\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nencode(self) -> str\n```\n\nEncode cursor state to an opaque string.\n\n\n#### `decode` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/pagination.py#L31\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndecode(cls, cursor: str) -> CursorState\n```\n\nDecode cursor from an opaque string.\n\n**Raises:**\n- `ValueError`: If the cursor is invalid or malformed.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-skills.mdx",
    "content": "---\ntitle: skills\nsidebarTitle: skills\n---\n\n# `fastmcp.utilities.skills`\n\n\nClient utilities for discovering and downloading skills from MCP servers.\n\n## Functions\n\n### `list_skills` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L43\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nlist_skills(client: Client) -> list[SkillSummary]\n```\n\n\nList all available skills from an MCP server.\n\nDiscovers skills by finding resources with URIs matching the\n`skill://{name}/SKILL.md` pattern.\n\n**Args:**\n- `client`: Connected FastMCP client\n\n**Returns:**\n- List of SkillSummary objects with name, description, and URI\n\n\n### `get_skill_manifest` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L87\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_skill_manifest(client: Client, skill_name: str) -> SkillManifest\n```\n\n\nGet the manifest for a specific skill.\n\n**Args:**\n- `client`: Connected FastMCP client\n- `skill_name`: Name of the skill\n\n**Returns:**\n- SkillManifest with file listing\n\n**Raises:**\n- `ValueError`: If manifest cannot be read or parsed\n\n\n### `download_skill` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L127\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndownload_skill(client: Client, skill_name: str, target_dir: str | Path) -> Path\n```\n\n\nDownload a skill and all its files to a local directory.\n\nCreates a subdirectory named after the skill containing all files.\n\n**Args:**\n- `client`: Connected FastMCP client\n- `skill_name`: Name of the skill to download\n- `target_dir`: Directory where skill folder will be created\n- `overwrite`: If True, overwrite existing skill directory. If False\n(default), raise FileExistsError if directory exists.\n\n**Returns:**\n- Path to the downloaded skill directory\n\n**Raises:**\n- `ValueError`: If skill cannot be found or downloaded\n- `FileExistsError`: If skill directory exists and overwrite=False\n\n\n### `sync_skills` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L218\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nsync_skills(client: Client, target_dir: str | Path) -> list[Path]\n```\n\n\nDownload all available skills from a server.\n\n**Args:**\n- `client`: Connected FastMCP client\n- `target_dir`: Directory where skill folders will be created\n- `overwrite`: If True, overwrite existing files\n\n**Returns:**\n- List of paths to downloaded skill directories\n\n\n## Classes\n\n### `SkillSummary` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L18\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSummary information about a skill available on a server.\n\n\n### `SkillFile` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L27\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nInformation about a file within a skill.\n\n\n### `SkillManifest` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/skills.py#L36\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nFull manifest of a skill including all files.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-tests.mdx",
    "content": "---\ntitle: tests\nsidebarTitle: tests\n---\n\n# `fastmcp.utilities.tests`\n\n## Functions\n\n### `temporary_settings` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/tests.py#L24\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ntemporary_settings(**kwargs: Any)\n```\n\n\nTemporarily override FastMCP setting values.\n\n**Args:**\n- `**kwargs`: The settings to override, including nested settings.\n\n\n### `run_server_in_process` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/tests.py#L75\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_server_in_process(server_fn: Callable[..., None], *args: Any, **kwargs: Any) -> Generator[str, None, None]\n```\n\n\nContext manager that runs a FastMCP server in a separate process and\nreturns the server URL. When the context manager is exited, the server process is killed.\n\n**Args:**\n- `server_fn`: The function that runs a FastMCP server. FastMCP servers are\nnot pickleable, so we need a function that creates and runs one.\n- `*args`: Arguments to pass to the server function.\n- `provide_host_and_port`: Whether to provide the host and port to the server function as kwargs.\n- `host`: Host to bind the server to (default\\: \"127.0.0.1\").\n- `port`: Port to bind the server to (default\\: find available port).\n- `**kwargs`: Keyword arguments to pass to the server function.\n\n**Returns:**\n- The server URL.\n\n\n### `run_server_async` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/tests.py#L143\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nrun_server_async(server: FastMCP, port: int | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str = '/mcp', host: str = '127.0.0.1') -> AsyncGenerator[str, None]\n```\n\n\nStart a FastMCP server as an asyncio task for in-process async testing.\n\nThis is the recommended way to test FastMCP servers. It runs the server\nas an async task in the same process, eliminating subprocess coordination,\nsleeps, and cleanup issues.\n\n**Args:**\n- `server`: FastMCP server instance\n- `port`: Port to bind to (default\\: find available port)\n- `transport`: Transport type (\"http\", \"streamable-http\", or \"sse\")\n- `path`: URL path for the server (default\\: \"/mcp\")\n- `host`: Host to bind to (default\\: \"127.0.0.1\")\n\n\n## Classes\n\n### `HeadlessOAuth` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/tests.py#L225\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nOAuth provider that bypasses browser interaction for testing.\n\nThis simulates the complete OAuth flow programmatically by making HTTP requests\ninstead of opening a browser and running a callback server. Useful for automated testing.\n\n\n**Methods:**\n\n#### `redirect_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/tests.py#L238\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nredirect_handler(self, authorization_url: str) -> None\n```\n\nMake HTTP request to authorization URL and store response for callback handler.\n\n\n#### `callback_handler` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/tests.py#L244\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncallback_handler(self) -> tuple[str, str | None]\n```\n\nParse stored response and return (auth_code, state).\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-timeout.mdx",
    "content": "---\ntitle: timeout\nsidebarTitle: timeout\n---\n\n# `fastmcp.utilities.timeout`\n\n\nTimeout normalization utilities.\n\n## Functions\n\n### `normalize_timeout_to_timedelta` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/timeout.py#L8\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nnormalize_timeout_to_timedelta(value: int | float | datetime.timedelta | None) -> datetime.timedelta | None\n```\n\n\nNormalize a timeout value to a timedelta.\n\n**Args:**\n- `value`: Timeout value as int/float (seconds), timedelta, or None\n\n**Returns:**\n- timedelta if value provided, None otherwise\n\n\n### `normalize_timeout_to_seconds` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/timeout.py#L28\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nnormalize_timeout_to_seconds(value: int | float | datetime.timedelta | None) -> float | None\n```\n\n\nNormalize a timeout value to seconds (float).\n\n**Args:**\n- `value`: Timeout value as int/float (seconds), timedelta, or None.\nZero values are treated as \"disabled\" and return None.\n\n**Returns:**\n- float seconds if value provided and non-zero, None otherwise\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-token_cache.mdx",
    "content": "---\ntitle: token_cache\nsidebarTitle: token_cache\n---\n\n# `fastmcp.utilities.token_cache`\n\n\nIn-memory cache for token verification results.\n\nProvides a generic TTL-based cache for ``AccessToken`` objects, designed to\nreduce repeated network calls during opaque-token verification.  Only\n*successful* verifications should be cached; errors and failures must be\nretried on every request.\n\nExample:\n    ```python\n    from fastmcp.utilities.token_cache import TokenCache\n\n    cache = TokenCache(ttl_seconds=300, max_size=10000)\n\n    # On cache miss, call the upstream verifier and store the result.\n    hit, token = cache.get(raw_token)\n    if not hit:\n        token = await _call_upstream(raw_token)\n        if token is not None:\n            cache.set(raw_token, token)\n    ```\n\n\n## Classes\n\n### `TokenCache` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/token_cache.py#L46\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nTTL-based in-memory cache for ``AccessToken`` objects.\n\nFeatures:\n- SHA-256 hashed cache keys (fixed size, regardless of token length).\n- Per-entry TTL that respects both the configured ``ttl_seconds`` and the\n  token's own ``expires_at`` claim (whichever is sooner).\n- Bounded size with FIFO eviction when the cache is full.\n- Periodic cleanup of expired entries to prevent unbounded growth.\n- Defensive deep copies on both store and retrieve to prevent\n  callers from mutating cached values.\n\nCaching is disabled when ``ttl_seconds`` is ``None`` or ``0``, or\nwhen ``max_size`` is ``0``.  Negative values raise ``ValueError``.\n\n\n**Methods:**\n\n#### `enabled` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/token_cache.py#L89\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nenabled(self) -> bool\n```\n\nReturn whether caching is active.\n\n\n#### `get` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/token_cache.py#L95\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget(self, token: str) -> tuple[bool, AccessToken | None]\n```\n\nLook up a cached verification result.\n\n**Returns:**\n- ``(True, AccessToken)`` on a cache hit, ``(False, None)`` on a miss\n- or when caching is disabled.  The returned ``AccessToken`` is a deep\n- copy that is safe to mutate.\n\n\n#### `set` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/token_cache.py#L118\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nset(self, token: str, result: AccessToken) -> None\n```\n\nStore a *successful* verification result.\n\nOnly successful verifications should be cached.  Failures (inactive\ntokens, missing scopes, HTTP errors, timeouts) must **not** be cached\nso that transient problems do not produce sticky false negatives.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-types.mdx",
    "content": "---\ntitle: types\nsidebarTitle: types\n---\n\n# `fastmcp.utilities.types`\n\n\nCommon types used across FastMCP.\n\n## Functions\n\n### `get_fn_name` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L34\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_fn_name(fn: Callable[..., Any]) -> str\n```\n\n### `get_cached_typeadapter` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L45\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_cached_typeadapter(cls: T) -> TypeAdapter[T]\n```\n\n\nTypeAdapters are heavy objects, and in an application context we'd typically\ncreate them once in a global scope and reuse them as often as possible.\nHowever, this isn't feasible for user-generated functions. Instead, we use a\ncache to minimize the cost of creating them as much as possible.\n\n\n### `issubclass_safe` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L123\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nissubclass_safe(cls: type, base: type) -> bool\n```\n\n\nCheck if cls is a subclass of base, even if cls is a type variable.\n\n\n### `is_class_member_of_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L133\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_class_member_of_type(cls: Any, base: type) -> bool\n```\n\n\nCheck if cls is a member of base, even if cls is a type variable.\n\nBase can be a type, a UnionType, or an Annotated type. Generic types are not\nconsidered members (e.g. T is not a member of list\\[T]).\n\n\n### `find_kwarg_by_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L155\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nfind_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None\n```\n\n\nFind the name of the kwarg that is of type kwarg_type.\n\nIncludes union types that contain the kwarg_type, as well as Annotated types.\n\n\n### `create_function_without_params` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L181\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_function_without_params(fn: Callable[..., Any], exclude_params: list[str]) -> Callable[..., Any]\n```\n\n\nCreate a new function with the same code but without the specified parameters in annotations.\n\nThis is used to exclude parameters from type adapter processing when they can't be serialized.\nThe excluded parameters are removed from the function's __annotations__ dictionary.\n\n\n### `replace_type` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L454\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nreplace_type(type_, type_map: dict[type, type])\n```\n\n\nGiven a (possibly generic, nested, or otherwise complex) type, replaces all\ninstances of old_type with new_type.\n\nThis is useful for transforming types when creating tools.\n\n**Args:**\n- `type_`: The type to replace instances of old_type with new_type.\n- `old_type`: The type to replace.\n- `new_type`: The type to replace old_type with.\n\nExamples:\n```python\n>>> replace_type(list[int | bool], {int: str})\nlist[str | bool]\n\n>>> replace_type(list[list[int]], {int: str})\nlist[list[str]]\n```\n\n\n## Classes\n\n### `FastMCPBaseModel` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L38\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nBase model for FastMCP models.\n\n\n### `Image` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L238\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nHelper class for returning images from tools.\n\n\n**Methods:**\n\n#### `to_image_content` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L289\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.ImageContent\n```\n\nConvert to MCP ImageContent.\n\n\n#### `to_data_uri` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L304\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_data_uri(self, mime_type: str | None = None) -> str\n```\n\nGet image as a data URI.\n\n\n### `Audio` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L310\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nHelper class for returning audio from tools.\n\n\n**Methods:**\n\n#### `to_audio_content` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L347\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.AudioContent\n```\n\n### `File` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L368\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nHelper class for returning file data from tools.\n\n\n**Methods:**\n\n#### `to_resource_content` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L407\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nto_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.EmbeddedResource\n```\n\n### `ContextSamplingFallbackProtocol` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/types.py#L491\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-ui.mdx",
    "content": "---\ntitle: ui\nsidebarTitle: ui\n---\n\n# `fastmcp.utilities.ui`\n\n\n\nShared UI utilities for FastMCP HTML pages.\n\nThis module provides reusable HTML/CSS components for OAuth callbacks,\nconsent pages, and other user-facing interfaces.\n\n\n## Functions\n\n### `create_page` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L453\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_page(content: str, title: str = 'FastMCP', additional_styles: str = '', csp_policy: str = \"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'\") -> str\n```\n\n\nCreate a complete HTML page with FastMCP styling.\n\n**Args:**\n- `content`: HTML content to place inside the page\n- `title`: Page title\n- `additional_styles`: Extra CSS to include\n- `csp_policy`: Content Security Policy header value.\nIf empty string \"\", the CSP meta tag is omitted entirely.\n\n**Returns:**\n- Complete HTML page as string\n\n\n### `create_logo` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L501\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_logo(icon_url: str | None = None, alt_text: str = 'FastMCP') -> str\n```\n\n\nCreate logo HTML.\n\n**Args:**\n- `icon_url`: Optional custom icon URL. If not provided, uses the FastMCP logo.\n- `alt_text`: Alt text for the logo image.\n\n**Returns:**\n- HTML for logo image tag.\n\n\n### `create_status_message` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L516\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_status_message(message: str, is_success: bool = True) -> str\n```\n\n\nCreate a status message with icon.\n\n**Args:**\n- `message`: Status message text\n- `is_success`: True for success (✓), False for error (✕)\n\n**Returns:**\n- HTML for status message\n\n\n### `create_info_box` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L539\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_info_box(content: str, is_error: bool = False, centered: bool = False, monospace: bool = False) -> str\n```\n\n\nCreate an info box.\n\n**Args:**\n- `content`: HTML content for the info box\n- `is_error`: True for error styling, False for normal\n- `centered`: True to center the text, False for left-aligned\n- `monospace`: True to use gray monospace font styling instead of blue\n\n**Returns:**\n- HTML for info box\n\n\n### `create_detail_box` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L568\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_detail_box(rows: list[tuple[str, str]]) -> str\n```\n\n\nCreate a detail box with key-value pairs.\n\n**Args:**\n- `rows`: List of (label, value) tuples\n\n**Returns:**\n- HTML for detail box\n\n\n### `create_button_group` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L591\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_button_group(buttons: list[tuple[str, str, str]]) -> str\n```\n\n\nCreate a group of buttons.\n\n**Args:**\n- `buttons`: List of (text, value, css_class) tuples\n\n**Returns:**\n- HTML for button group\n\n\n### `create_secure_html_response` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/ui.py#L609\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncreate_secure_html_response(html: str, status_code: int = 200) -> HTMLResponse\n```\n\n\nCreate an HTMLResponse with security headers.\n\nAdds X-Frame-Options: DENY to prevent clickjacking attacks per MCP security best practices.\n\n**Args:**\n- `html`: HTML content to return\n- `status_code`: HTTP status code\n\n**Returns:**\n- HTMLResponse with security headers\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-version_check.mdx",
    "content": "---\ntitle: version_check\nsidebarTitle: version_check\n---\n\n# `fastmcp.utilities.version_check`\n\n\nVersion checking utilities for FastMCP.\n\n## Functions\n\n### `get_latest_version` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/version_check.py#L98\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nget_latest_version(include_prereleases: bool = False) -> str | None\n```\n\n\nGet the latest version of FastMCP from PyPI, using cache when available.\n\n**Args:**\n- `include_prereleases`: If True, include pre-release versions.\n\n**Returns:**\n- The latest version string, or None if unavailable.\n\n\n### `check_for_newer_version` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/version_check.py#L124\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncheck_for_newer_version() -> str | None\n```\n\n\nCheck if a newer version of FastMCP is available.\n\n**Returns:**\n- The latest version string if newer than current, None otherwise.\n\n"
  },
  {
    "path": "docs/python-sdk/fastmcp-utilities-versions.mdx",
    "content": "---\ntitle: versions\nsidebarTitle: versions\n---\n\n# `fastmcp.utilities.versions`\n\n\nVersion comparison utilities for component versioning.\n\nThis module provides utilities for comparing component versions. Versions are\nstrings that are first attempted to be parsed as PEP 440 versions (using the\n`packaging` library), falling back to lexicographic string comparison.\n\nExamples:\n    - \"1\", \"2\", \"10\" → parsed as PEP 440, compared semantically (1 < 2 < 10)\n    - \"1.0\", \"2.0\" → parsed as PEP 440\n    - \"v1.0\" → 'v' prefix stripped, parsed as \"1.0\"\n    - \"2025-01-15\" → not valid PEP 440, compared as strings\n    - None → sorts lowest (unversioned components)\n\n\n## Functions\n\n### `parse_version_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L190\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nparse_version_key(version: str | None) -> VersionKey\n```\n\n\nParse a version string into a sortable key.\n\n**Args:**\n- `version`: The version string, or None for unversioned.\n\n**Returns:**\n- A VersionKey suitable for sorting.\n\n\n### `version_sort_key` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L202\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nversion_sort_key(component: FastMCPComponent) -> VersionKey\n```\n\n\nGet a sort key for a component based on its version.\n\nUse with sorted() or max() to order components by version.\n\n**Args:**\n- `component`: The component to get a sort key for.\n\n**Returns:**\n- A sortable VersionKey.\n\n\n### `compare_versions` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L222\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ncompare_versions(a: str | None, b: str | None) -> int\n```\n\n\nCompare two version strings.\n\n**Args:**\n- `a`: First version string (or None).\n- `b`: Second version string (or None).\n\n**Returns:**\n- -1 if a &lt; b, 0 if a == b, 1 if a &gt; b.\n\n\n### `is_version_greater` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L244\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nis_version_greater(a: str | None, b: str | None) -> bool\n```\n\n\nCheck if version a is greater than version b.\n\n**Args:**\n- `a`: First version string (or None).\n- `b`: Second version string (or None).\n\n**Returns:**\n- True if a > b, False otherwise.\n\n\n### `max_version` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L257\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmax_version(a: str | None, b: str | None) -> str | None\n```\n\n\nReturn the greater of two versions.\n\n**Args:**\n- `a`: First version string (or None).\n- `b`: Second version string (or None).\n\n**Returns:**\n- The greater version, or None if both are None.\n\n\n### `min_version` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L274\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmin_version(a: str | None, b: str | None) -> str | None\n```\n\n\nReturn the lesser of two versions.\n\n**Args:**\n- `a`: First version string (or None).\n- `b`: Second version string (or None).\n\n**Returns:**\n- The lesser version, or None if both are None.\n\n\n### `dedupe_with_versions` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L291\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\ndedupe_with_versions(components: Sequence[C], key_fn: Callable[[C], str]) -> list[C]\n```\n\n\nDeduplicate components by key, keeping highest version.\n\nGroups components by key, selects the highest version from each group,\nand injects available versions into meta if any component is versioned.\n\n**Args:**\n- `components`: Sequence of components to deduplicate.\n- `key_fn`: Function to extract the grouping key from a component.\n\n**Returns:**\n- Deduplicated list with versions injected into meta.\n\n\n## Classes\n\n### `VersionSpec` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L31\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nSpecification for filtering components by version.\n\nUsed by transforms and providers to filter components to a specific\nversion or version range. Unversioned components (version=None) always\nmatch any spec.\n\n**Args:**\n- `gte`: If set, only versions >= this value match.\n- `lt`: If set, only versions < this value match.\n- `eq`: If set, only this exact version matches (gte/lt ignored).\n\n\n**Methods:**\n\n#### `matches` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L48\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nmatches(self, version: str | None) -> bool\n```\n\nCheck if a version matches this spec.\n\n**Args:**\n- `version`: The version to check, or None for unversioned.\n- `match_none`: Whether unversioned (None) components match. Defaults to True\nfor backward compatibility with retrieval operations. Set to False\nwhen filtering (e.g., enable/disable) to exclude unversioned components\nfrom version-specific rules.\n\n**Returns:**\n- True if the version matches the spec.\n\n\n#### `intersect` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L81\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n```python\nintersect(self, other: VersionSpec | None) -> VersionSpec\n```\n\nReturn a spec that satisfies both this spec and other.\n\nUsed by transforms to combine caller constraints with filter constraints.\nFor example, if a VersionFilter has lt=\"3.0\" and caller requests eq=\"1.0\",\nthe intersection validates \"1.0\" is in range and returns the exact spec.\n\n**Args:**\n- `other`: Another spec to intersect with, or None.\n\n**Returns:**\n- A VersionSpec that matches only versions satisfying both specs.\n\n\n### `VersionKey` <sup><a href=\"https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/utilities/versions.py#L117\" target=\"_blank\"><Icon icon=\"github\" style=\"width: 14px; height: 14px;\" /></a></sup>\n\n\nA comparable version key that handles None, PEP 440 versions, and strings.\n\nComparison order:\n1. None (unversioned) sorts lowest\n2. PEP 440 versions sort by semantic version order\n3. Invalid versions (strings) sort lexicographically\n4. When comparing PEP 440 vs string, PEP 440 comes first\n\n"
  },
  {
    "path": "docs/servers/auth/authentication.mdx",
    "content": "---\ntitle: Authentication\nsidebarTitle: Overview\ndescription: Secure your FastMCP server with flexible authentication patterns, from simple API keys to full OAuth 2.1 integration with external identity providers.\nicon: user-shield\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nAuthentication in MCP presents unique challenges that differ from traditional web applications. MCP clients need to discover authentication requirements automatically, negotiate OAuth flows without user intervention, and work seamlessly across different identity providers. FastMCP addresses these challenges by providing authentication patterns that integrate with the MCP protocol while remaining simple to implement and deploy.\n\n<Tip>\nAuthentication applies only to FastMCP's HTTP-based transports (`http` and `sse`). The STDIO transport inherits security from its local execution environment.\n</Tip>\n\n<Warning>\n**Authentication is rapidly evolving in MCP.** The specification and best practices are changing quickly. FastMCP aims to provide stable, secure patterns that adapt to these changes while keeping your code simple and maintainable.\n</Warning>\n\n## MCP Authentication Challenges\n\nTraditional web authentication assumes a human user with a browser who can interact with login forms and consent screens. MCP clients are often automated systems that need to authenticate without human intervention. This creates several unique requirements:\n\n**Automatic Discovery**: MCP clients must discover authentication requirements by examining server metadata rather than encountering login redirects.\n\n**Programmatic OAuth**: OAuth flows must work without human interaction, relying on pre-configured credentials or Dynamic Client Registration.\n\n**Token Management**: Clients need to obtain, refresh, and manage tokens automatically across multiple MCP servers.\n\n**Protocol Integration**: Authentication must integrate cleanly with MCP's transport mechanisms and error handling.\n\nThese challenges mean that not all authentication approaches work well with MCP. The patterns that do work fall into three categories based on the level of authentication responsibility your server assumes.\n\n## Authentication Responsibility\n\nAuthentication responsibility exists on a spectrum. Your MCP server can validate tokens created elsewhere, coordinate with external identity providers, or handle the complete authentication lifecycle internally. Each approach involves different trade-offs between simplicity, security, and control.\n\n### Token Validation\n\nYour server validates tokens but delegates their creation to external systems. This approach treats your MCP server as a pure resource server that trusts tokens signed by known issuers.\n\nToken validation works well when you already have authentication infrastructure that can issue structured tokens like JWTs. Your existing API gateway, microservices platform, or enterprise SSO system becomes the source of truth for user identity, while your MCP server focuses on its core functionality.\n\nThe key insight is that token validation separates authentication (proving who you are) from authorization (determining what you can do). Your MCP server receives proof of identity in the form of a signed token and makes access decisions based on the claims within that token.\n\nThis pattern excels in microservices architectures where multiple services need to validate the same tokens, or when integrating MCP servers into existing systems that already handle user authentication.\n\n### External Identity Providers\n\nYour server coordinates with established identity providers to create seamless authentication experiences for MCP clients. This approach leverages OAuth 2.0 and OpenID Connect protocols to delegate user authentication while maintaining control over authorization decisions.\n\nExternal identity providers handle the complex aspects of authentication: user credential verification, multi-factor authentication, account recovery, and security monitoring. Your MCP server receives tokens from these trusted providers and validates them using the provider's public keys.\n\nThe MCP protocol's support for Dynamic Client Registration makes this pattern particularly powerful. MCP clients can automatically discover your authentication requirements and register themselves with your identity provider without manual configuration.\n\nThis approach works best for production applications that need enterprise-grade authentication features without the complexity of building them from scratch. It scales well across multiple applications and provides consistent user experiences.\n\n### Full OAuth Implementation\n\nYour server implements a complete OAuth 2.0 authorization server, handling everything from user credential verification to token lifecycle management. This approach provides maximum control at the cost of significant complexity.\n\nFull OAuth implementation means building user interfaces for login and consent, implementing secure credential storage, managing token lifecycles, and maintaining ongoing security updates. The complexity extends beyond initial implementation to include threat monitoring, compliance requirements, and keeping pace with evolving security best practices.\n\nThis pattern makes sense only when you need complete control over the authentication process, operate in air-gapped environments, or have specialized requirements that external providers cannot meet.\n\n## FastMCP Authentication Providers\n\nFastMCP translates these authentication responsibility levels into a variety of concrete classes that handle the complexities of MCP protocol integration. You can build on these classes to handle the complexities of MCP protocol integration.\n\n### TokenVerifier\n\n`TokenVerifier` provides pure token validation without OAuth metadata endpoints. This class focuses on the essential task of determining whether a token is valid and extracting authorization information from its claims.\n\nThe implementation handles JWT signature verification, expiration checking, and claim extraction. It validates tokens against known issuers and audiences, ensuring that tokens intended for your server are not accepted by other systems.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nauth = JWTVerifier(\n    jwks_uri=\"https://your-auth-system.com/.well-known/jwks.json\",\n    issuer=\"https://your-auth-system.com\", \n    audience=\"your-mcp-server\"\n)\n\nmcp = FastMCP(name=\"Protected Server\", auth=auth)\n```\n\nThis example configures token validation against a JWT issuer. The `JWTVerifier` will fetch public keys from the JWKS endpoint and validate incoming tokens against those keys. Only tokens with the correct issuer and audience claims will be accepted.\n\n`TokenVerifier` works well when you control both the token issuer and your MCP server, or when integrating with existing JWT-based infrastructure.\n\n→ **Complete guide**: [Token Verification](/servers/auth/token-verification)\n\n### RemoteAuthProvider\n\n`RemoteAuthProvider` enables authentication with identity providers that **support Dynamic Client Registration (DCR)**, such as Descope and WorkOS AuthKit. With DCR, MCP clients can automatically register themselves with the identity provider and obtain credentials without any manual configuration.\n\nThis class combines token validation with OAuth discovery metadata. It extends `TokenVerifier` functionality by adding OAuth 2.0 protected resource endpoints that advertise your authentication requirements. MCP clients examine these endpoints to understand which identity providers you trust and how to obtain valid tokens.\n\nThe key requirement is that your identity provider must support DCR - the ability for clients to dynamically register and obtain credentials. This is what enables the seamless, automated authentication flow that MCP requires.\n\nFor example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully supports DCR:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\nauth = AuthKitProvider(\n    authkit_domain=\"https://your-project.authkit.app\",\n    base_url=\"https://your-fastmcp-server.com\"\n)\n\nmcp = FastMCP(name=\"Enterprise Server\", auth=auth)\n```\n\nThis example uses WorkOS AuthKit as the external identity provider. The `AuthKitProvider` automatically configures token validation against WorkOS and provides the OAuth metadata that MCP clients need for automatic authentication.\n\n`RemoteAuthProvider` is ideal for production applications when your identity provider supports Dynamic Client Registration (DCR). This enables fully automated authentication without manual client configuration.\n\n→ **Complete guide**: [Remote OAuth](/servers/auth/remote-oauth)\n\n### OAuthProxy\n\n<VersionBadge version=\"2.12.0\" />\n\n`OAuthProxy` enables authentication with OAuth providers that **don't support Dynamic Client Registration (DCR)**, such as GitHub, Google, Azure, AWS, and most traditional enterprise identity systems.\n\nWhen identity providers require manual app registration and fixed credentials, `OAuthProxy` bridges the gap. It presents a DCR-compliant interface to MCP clients (accepting any registration request) while using your pre-registered credentials with the upstream provider. The proxy handles the complexity of callback forwarding, enabling dynamic client callbacks to work with providers that require fixed redirect URIs.\n\nThis class solves the fundamental incompatibility between MCP's expectation of dynamic registration and traditional OAuth providers' requirement for manual app registration.\n\nFor example, the built-in `GitHubProvider` extends `OAuthProxy` to work with GitHub's OAuth system:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=\"Ov23li...\",  # Your GitHub OAuth App ID\n    client_secret=\"abc123...\",  # Your GitHub OAuth App Secret\n    base_url=\"https://your-server.com\"\n)\n\nmcp = FastMCP(name=\"GitHub-Protected Server\", auth=auth)\n```\n\nThis example uses the GitHub provider, which extends `OAuthProxy` with GitHub-specific token validation. The proxy handles the complete OAuth flow while making GitHub's non-DCR authentication work seamlessly with MCP clients.\n\n`OAuthProxy` is essential when integrating with OAuth providers that don't support DCR. This includes most established providers like GitHub, Google, and Azure, which require manual app registration through their developer consoles.\n\n→ **Complete guide**: [OAuth Proxy](/servers/auth/oauth-proxy)\n\n### OAuthProvider\n\n`OAuthProvider` implements a complete OAuth 2.0 authorization server within your MCP server. This class handles the full authentication lifecycle from user credential verification to token management.\n\nThe implementation provides all required OAuth endpoints including authorization, token, and discovery endpoints. It manages client registration, user consent, and token lifecycle while integrating with your user storage and authentication logic.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.oauth import MyOAuthProvider\n\nauth = MyOAuthProvider(\n    user_store=your_user_database,\n    client_store=your_client_registry,\n    # Additional configuration...\n)\n\nmcp = FastMCP(name=\"Auth Server\", auth=auth)\n```\n\nThis example shows the basic structure of a custom OAuth provider. The actual implementation requires significant additional configuration for user management, client registration, and security policies.\n\n`OAuthProvider` should be used only when you have specific requirements that external providers cannot meet and the expertise to implement OAuth securely.\n\n→ **Complete guide**: [Full OAuth Server](/servers/auth/full-oauth-server)\n\n### MultiAuth\n\n<VersionBadge version=\"3.1.0\" />\n\n`MultiAuth` composes multiple authentication sources into a single `auth` provider. When a server needs to accept tokens from different issuers — for example, an OAuth proxy for interactive clients alongside JWT verification for machine-to-machine tokens — `MultiAuth` tries each source in order and accepts the first successful verification.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import MultiAuth, OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nauth = MultiAuth(\n    server=OAuthProxy(\n        issuer_url=\"https://login.example.com/...\",\n        client_id=\"my-app\",\n        client_secret=\"secret\",\n        base_url=\"https://my-server.com\",\n    ),\n    verifiers=[\n        JWTVerifier(\n            jwks_uri=\"https://internal-issuer.example.com/.well-known/jwks.json\",\n            issuer=\"https://internal-issuer.example.com\",\n            audience=\"my-mcp-server\",\n        ),\n    ],\n)\n\nmcp = FastMCP(\"My Server\", auth=auth)\n```\n\nThe server (if provided) owns all OAuth routes and metadata. Verifiers contribute only token verification logic. This keeps the MCP discovery surface clean while supporting multiple token sources.\n\n→ **Complete guide**: [Multiple Auth Sources](/servers/auth/multi-auth)\n\n## Configuration\n\nAuthentication providers are configured programmatically by instantiating them directly in your code with their required parameters. This makes dependencies explicit and allows your IDE to provide helpful autocompletion and type checking.\n\nFor production deployments, load sensitive values like client secrets from environment variables:\n\n```python\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\n# Load secrets from environment variables\nauth = GitHubProvider(\n    client_id=os.environ.get(\"GITHUB_CLIENT_ID\"),\n    client_secret=os.environ.get(\"GITHUB_CLIENT_SECRET\"),\n    base_url=os.environ.get(\"BASE_URL\", \"http://localhost:8000\")\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\nThis approach keeps secrets out of your codebase while maintaining explicit configuration. You can use any environment variable names you prefer - there are no special prefixes required.\n\n## Choosing Your Implementation\n\nThe authentication approach you choose depends on your existing infrastructure, security requirements, and operational constraints.\n\n**For OAuth providers without DCR support (GitHub, Google, Azure, AWS, most enterprise systems), use OAuth Proxy.** These providers require manual app registration through their developer consoles. OAuth Proxy bridges the gap by presenting a DCR-compliant interface to MCP clients while using your fixed credentials with the provider. The proxy's callback forwarding pattern enables dynamic client ports to work with providers that require fixed redirect URIs.\n\n**For identity providers with DCR support (Descope, WorkOS AuthKit, modern auth platforms), use RemoteAuthProvider.** These providers allow clients to dynamically register and obtain credentials without manual configuration. This enables the fully automated authentication flow that MCP is designed for, providing the best user experience and simplest implementation.\n\n**Token validation works well when you already have authentication infrastructure that issues structured tokens.** If your organization already uses JWT-based systems, API gateways, or enterprise SSO that can generate tokens, this approach integrates seamlessly while keeping your MCP server focused on its core functionality. The simplicity comes from leveraging existing investment in authentication infrastructure.\n\n**When you need tokens from multiple sources, use MultiAuth.** This is common in hybrid architectures where interactive clients authenticate through an OAuth proxy while backend services send JWT tokens directly. `MultiAuth` composes an optional auth server with additional token verifiers, trying each source in order until one succeeds.\n\n**Full OAuth implementation should be avoided unless you have compelling reasons that external providers cannot address.** Air-gapped environments, specialized compliance requirements, or unique organizational constraints might justify this approach, but it requires significant security expertise and ongoing maintenance commitment. The complexity extends far beyond initial implementation to include threat monitoring, security updates, and keeping pace with evolving attack vectors.\n\nFastMCP's architecture supports migration between these approaches as your requirements evolve. You can integrate with existing token systems initially and migrate to external identity providers as your application scales, or implement custom solutions when your requirements outgrow standard patterns."
  },
  {
    "path": "docs/servers/auth/full-oauth-server.mdx",
    "content": "---\ntitle: Full OAuth Server\nsidebarTitle: Full OAuth Server\ndescription: Build a self-contained authentication system where your FastMCP server manages users, issues tokens, and validates them.\nicon: users-between-lines\n\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\n<Warning>\n**This is an extremely advanced pattern that most users should avoid.** Building a secure OAuth 2.1 server requires deep expertise in authentication protocols, cryptography, and security best practices. The complexity extends far beyond initial implementation to include ongoing security monitoring, threat response, and compliance maintenance.\n\n**Use [Remote OAuth](/servers/auth/remote-oauth) instead** unless you have compelling requirements that external identity providers cannot meet, such as air-gapped environments or specialized compliance needs.\n</Warning>\n\nThe Full OAuth Server pattern exists to support the MCP protocol specification's requirements. Your FastMCP server becomes both an Authorization Server and Resource Server, handling the complete authentication lifecycle from user login to token validation.\n\nThis documentation exists for completeness - the vast majority of applications should use external identity providers instead.\n\n## OAuthProvider\n\nFastMCP provides the `OAuthProvider` abstract class that implements the OAuth 2.1 specification. To use this pattern, you must subclass `OAuthProvider` and implement all required abstract methods.\n\n<Note>\n`OAuthProvider` handles OAuth endpoints, protocol flows, and security requirements, but delegates all storage, user management, and business logic to your implementation of the abstract methods.\n</Note>\n\n## Required Implementation\n\nYou must implement these abstract methods to create a functioning OAuth server:\n\n### Client Management\n\n<Card icon=\"code\" title=\"Client Management Methods\">\n<ParamField body=\"get_client\" type=\"async method\">\n  Retrieve client information by ID from your database.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client_id\" type=\"str\">\n      Client identifier to look up\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"OAuthClientInformationFull | None\" type=\"return type\">\n      Client information object or `None` if client not found\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"register_client\" type=\"async method\">\n  Store new client registration information in your database.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client_info\" type=\"OAuthClientInformationFull\">\n      Complete client registration information to store\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"None\" type=\"return type\">\n      No return value\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\n### Authorization Flow\n\n<Card icon=\"code\" title=\"Authorization Flow Methods\">\n<ParamField body=\"authorize\" type=\"async method\">\n  Handle authorization request and return redirect URL. Must implement user authentication and consent collection.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client making the authorization request\n    </ParamField>\n    <ParamField body=\"params\" type=\"AuthorizationParams\">\n      Authorization request parameters from the client\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"str\" type=\"return type\">\n      Redirect URL to send the client to\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"load_authorization_code\" type=\"async method\">\n  Load authorization code from storage by code string. Return `None` if code is invalid or expired.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client attempting to use the authorization code\n    </ParamField>\n    <ParamField body=\"authorization_code\" type=\"str\">\n      Authorization code string to look up\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"AuthorizationCode | None\" type=\"return type\">\n      Authorization code object or `None` if not found\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\n### Token Management\n\n<Card icon=\"code\" title=\"Token Management Methods\">\n<ParamField body=\"exchange_authorization_code\" type=\"async method\">\n  Exchange authorization code for access and refresh tokens. Must validate code and create new tokens.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client exchanging the authorization code\n    </ParamField>\n    <ParamField body=\"authorization_code\" type=\"AuthorizationCode\">\n      Valid authorization code object to exchange\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"OAuthToken\" type=\"return type\">\n      New OAuth token containing access and refresh tokens\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"load_refresh_token\" type=\"async method\">\n  Load refresh token from storage by token string. Return `None` if token is invalid or expired.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client attempting to use the refresh token\n    </ParamField>\n    <ParamField body=\"refresh_token\" type=\"str\">\n      Refresh token string to look up\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"RefreshToken | None\" type=\"return type\">\n      Refresh token object or `None` if not found\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"exchange_refresh_token\" type=\"async method\">\n  Exchange refresh token for new access/refresh token pair. Must validate scopes and token.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client using the refresh token\n    </ParamField>\n    <ParamField body=\"refresh_token\" type=\"RefreshToken\">\n      Valid refresh token object to exchange\n    </ParamField>\n    <ParamField body=\"scopes\" type=\"list[str]\">\n      Requested scopes for the new access token\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"OAuthToken\" type=\"return type\">\n      New OAuth token with updated access and refresh tokens\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"load_access_token\" type=\"async method\">\n  Load an access token by its token string.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"token\" type=\"str\">\n      The access token to verify\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"AccessToken | None\" type=\"return type\">\n      The access token object, or `None` if the token is invalid\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"revoke_token\" type=\"async method\">\n  Revoke access or refresh token, marking it as invalid in storage.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"token\" type=\"AccessToken | RefreshToken\">\n      Token object to revoke and mark invalid\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"None\" type=\"return type\">\n      No return value\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"verify_token\" type=\"async method\">\n  Verify bearer token for incoming requests. Return `AccessToken` if valid, `None` if invalid.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"token\" type=\"str\">\n      Bearer token string from incoming request\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"AccessToken | None\" type=\"return type\">\n      Access token object if valid, `None` if invalid or expired\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\nEach method must handle storage, validation, security, and error cases according to the OAuth 2.1 specification. The implementation complexity is substantial and requires expertise in OAuth security considerations.\n\n<Warning>\n**Security Notice:** OAuth server implementation involves numerous security considerations including PKCE, state parameters, redirect URI validation, token binding, replay attack prevention, and secure storage requirements. Mistakes can lead to serious security vulnerabilities.\n</Warning>"
  },
  {
    "path": "docs/servers/auth/multi-auth.mdx",
    "content": "---\ntitle: Multiple Auth Sources\nsidebarTitle: Multiple Auth Sources\ndescription: Accept tokens from multiple authentication sources with a single server.\nicon: layer-group\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"3.1.0\" />\n\nProduction servers often need to accept tokens from multiple authentication sources. An interactive application might authenticate through an OAuth proxy, while a backend service sends machine-to-machine JWT tokens directly. `MultiAuth` composes these sources into a single `auth` provider so every valid token is accepted regardless of where it was issued.\n\n## Understanding MultiAuth\n\n`MultiAuth` wraps an optional auth server (like `OAuthProxy`) together with one or more token verifiers (like `JWTVerifier`). When a request arrives with a bearer token, `MultiAuth` tries each source in order and accepts the first successful verification.\n\nThe auth server, if provided, is tried first. It owns all OAuth routes and metadata — the verifiers contribute only token verification logic. This keeps the MCP discovery surface clean: one set of routes, one set of metadata, multiple verification paths.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import MultiAuth, OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nauth = MultiAuth(\n    server=OAuthProxy(\n        issuer_url=\"https://login.example.com/...\",\n        client_id=\"my-app\",\n        client_secret=\"secret\",\n        base_url=\"https://my-server.com\",\n    ),\n    verifiers=[\n        JWTVerifier(\n            jwks_uri=\"https://internal-issuer.example.com/.well-known/jwks.json\",\n            issuer=\"https://internal-issuer.example.com\",\n            audience=\"my-mcp-server\",\n        ),\n    ],\n)\n\nmcp = FastMCP(\"My Server\", auth=auth)\n```\n\nInteractive MCP clients authenticate through the OAuth proxy as usual. Backend services skip OAuth entirely and send a JWT signed by the internal issuer. Both paths are validated, and the first match wins.\n\n## Verification Order\n\n`MultiAuth` checks sources in a deterministic order:\n\n1. **Server** (if provided) — the full auth provider's `verify_token` runs first\n2. **Verifiers** — each `TokenVerifier` is tried in list order\n\nThe first source that returns a valid `AccessToken` wins. If every source returns `None`, the request receives a 401 response.\n\nThis ordering means the server acts as the \"primary\" authentication path, with verifiers as fallbacks for tokens the server doesn't recognize.\n\n## Verifiers Only\n\nYou don't always need a full OAuth server. If your server only needs to accept tokens from multiple issuers, pass verifiers without a server:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import MultiAuth\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, StaticTokenVerifier\n\nauth = MultiAuth(\n    verifiers=[\n        JWTVerifier(\n            jwks_uri=\"https://issuer-a.example.com/.well-known/jwks.json\",\n            issuer=\"https://issuer-a.example.com\",\n            audience=\"my-server\",\n        ),\n        JWTVerifier(\n            jwks_uri=\"https://issuer-b.example.com/.well-known/jwks.json\",\n            issuer=\"https://issuer-b.example.com\",\n            audience=\"my-server\",\n        ),\n    ],\n)\n\nmcp = FastMCP(\"Multi-Issuer Server\", auth=auth)\n```\n\nWithout a server, no OAuth routes or metadata are served. This is appropriate for internal systems where clients already know how to obtain tokens.\n\n## API Reference\n\n### MultiAuth\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `server` | `AuthProvider \\| None` | Optional auth provider that owns routes and OAuth metadata. Also tried first for token verification. |\n| `verifiers` | `list[TokenVerifier] \\| TokenVerifier` | One or more token verifiers tried after the server. |\n| `base_url` | `str \\| None` | Override the base URL. Defaults to the server's `base_url`. |\n| `required_scopes` | `list[str] \\| None` | Override required scopes. Defaults to the server's scopes. |\n"
  },
  {
    "path": "docs/servers/auth/oauth-proxy.mdx",
    "content": "---\ntitle: OAuth Proxy\nsidebarTitle: OAuth Proxy\ndescription: Bridge traditional OAuth providers to work seamlessly with MCP's authentication flow.\nicon: share\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.12.0\" />\n\nThe OAuth proxy enables FastMCP servers to authenticate with OAuth providers that **don't support Dynamic Client Registration (DCR)**. This includes virtually all traditional OAuth providers: GitHub, Google, Azure, AWS, Discord, Facebook, and most enterprise identity systems. For providers that do support DCR (like Descope and WorkOS AuthKit), use [`RemoteAuthProvider`](/servers/auth/remote-oauth) instead.\n\nMCP clients expect to register automatically and obtain credentials on the fly, but traditional providers require manual app registration through their developer consoles. The OAuth proxy bridges this gap by presenting a DCR-compliant interface to MCP clients while using your pre-registered credentials with the upstream provider. When a client attempts to register, the proxy returns your fixed credentials. When a client initiates authorization, the proxy handles the complexity of callback forwarding—storing the client's dynamic callback URL, using its own fixed callback with the provider, then forwarding back to the client after token exchange.\n\nThis approach enables any MCP client (whether using random localhost ports or fixed URLs like Claude.ai) to authenticate with any traditional OAuth provider, all while maintaining full OAuth 2.1 and PKCE security.\n\n<Note>\n  For providers that support OIDC discovery (Auth0, Google with OIDC\n  configuration, Azure AD), consider using [`OIDC\n  Proxy`](/servers/auth/oidc-proxy) for automatic configuration. OIDC Proxy\n  extends the OAuth proxy to automatically discover endpoints from the provider's\n  `/.well-known/openid-configuration` URL, simplifying setup.\n</Note>\n\n## Implementation\n\n### Provider Setup Requirements\n\nBefore using the OAuth proxy, you need to register your application with your OAuth provider:\n\n1. **Register your application** in the provider's developer console (GitHub Settings, Google Cloud Console, Azure Portal, etc.)\n2. **Configure the redirect URI** as your FastMCP server URL plus your chosen callback path:\n   - Default: `https://your-server.com/auth/callback`\n   - Custom: `https://your-server.com/your/custom/path` (if you set `redirect_path`)\n   - Development: `http://localhost:8000/auth/callback`\n3. **Obtain your credentials**: Client ID and Client Secret\n4. **Note the OAuth endpoints**: Authorization URL and Token URL (usually found in the provider's OAuth documentation)\n\n<Warning>\n  The redirect URI you configure with your provider must exactly match your\n  FastMCP server's URL plus the callback path. If you customize `redirect_path`\n  in the OAuth proxy, update your provider's redirect URI accordingly.\n</Warning>\n\n### Basic Setup\n\nHere's how to implement the OAuth proxy with any provider:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Configure token verification for your provider\n# See the Token Verification guide for provider-specific setups\ntoken_verifier = JWTVerifier(\n    jwks_uri=\"https://your-provider.com/.well-known/jwks.json\",\n    issuer=\"https://your-provider.com\",\n    audience=\"your-app-id\"\n)\n\n# Create the OAuth proxy\nauth = OAuthProxy(\n    # Provider's OAuth endpoints (from their documentation)\n    upstream_authorization_endpoint=\"https://provider.com/oauth/authorize\",\n    upstream_token_endpoint=\"https://provider.com/oauth/token\",\n\n    # Your registered app credentials\n    upstream_client_id=\"your-client-id\",\n    upstream_client_secret=\"your-client-secret\",\n\n    # Token validation (see Token Verification guide)\n    token_verifier=token_verifier,\n\n    # Your FastMCP server's public URL\n    base_url=\"https://your-server.com\",\n\n    # Optional: customize the callback path (default is \"/auth/callback\")\n    # redirect_path=\"/custom/callback\",\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\n### Configuration Parameters\n\n<Card icon=\"code\" title=\"OAuthProxy Parameters\">\n<ParamField body=\"upstream_authorization_endpoint\" type=\"str\" required>\n  URL of your OAuth provider's authorization endpoint (e.g., `https://github.com/login/oauth/authorize`)\n</ParamField>\n\n<ParamField body=\"upstream_token_endpoint\" type=\"str\" required>\n  URL of your OAuth provider's token endpoint (e.g.,\n  `https://github.com/login/oauth/access_token`)\n</ParamField>\n\n<ParamField body=\"upstream_client_id\" type=\"str\" required>\n  Client ID from your registered OAuth application\n</ParamField>\n\n<ParamField body=\"upstream_client_secret\" type=\"str | None\">\n  Client secret from your registered OAuth application. Optional for PKCE public\n  clients or when using alternative credentials (e.g., managed identity client\n  assertions via a subclass). When omitted, `jwt_signing_key` must be provided\n  explicitly since it cannot be derived from the secret.\n</ParamField>\n\n<ParamField body=\"token_verifier\" type=\"TokenVerifier\" required>\n  A [`TokenVerifier`](/servers/auth/token-verification) instance to validate the\n  provider's tokens\n</ParamField>\n\n<ParamField body=\"base_url\" type=\"AnyHttpUrl | str\" required>\n  Public URL where OAuth endpoints will be accessible, **including any mount path** (e.g., `https://your-server.com/api`).\n\n  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).\n</ParamField>\n\n<ParamField body=\"redirect_path\" type=\"str\" default=\"/auth/callback\">\n  Path for OAuth callbacks. Must match the redirect URI configured in your OAuth\n  application\n</ParamField>\n\n<ParamField body=\"upstream_revocation_endpoint\" type=\"str | None\">\n  Optional URL of provider's token revocation endpoint\n</ParamField>\n\n<ParamField body=\"issuer_url\" type=\"AnyHttpUrl | str | None\">\n  Issuer URL for OAuth authorization server metadata (defaults to `base_url`).\n\n  When `issuer_url` has a path component (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.\n\n  **Default behavior (recommended for most cases):**\n  ```python\n  auth = GitHubProvider(\n      base_url=\"http://localhost:8000/api\",  # OAuth endpoints under /api\n      # issuer_url defaults to base_url - path-aware discovery works automatically\n  )\n  ```\n\n  **When to set explicitly:**\n  Set `issuer_url` to root level only if you want multiple MCP servers to share a single discovery endpoint:\n  ```python\n  auth = GitHubProvider(\n      base_url=\"http://localhost:8000/api\",\n      issuer_url=\"http://localhost:8000\"  # Shared root-level discovery\n  )\n  ```\n\n  See the [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for complete mounting examples.\n</ParamField>\n\n<ParamField body=\"service_documentation_url\" type=\"AnyHttpUrl | str | None\">\n  Optional URL to your service documentation\n</ParamField>\n\n<ParamField body=\"forward_pkce\" type=\"bool\" default=\"True\">\n  Whether to forward PKCE (Proof Key for Code Exchange) to the upstream OAuth\n  provider. When enabled and the client uses PKCE, the proxy generates its own\n  PKCE parameters to send upstream while separately validating the client's\n  PKCE. This ensures end-to-end PKCE security at both layers (client-to-proxy\n  and proxy-to-upstream). - `True` (default): Forward PKCE for providers that\n  support it (Google, Azure, AWS, GitHub, etc.) - `False`: Disable only if upstream\n  provider doesn't support PKCE\n</ParamField>\n\n<ParamField body=\"token_endpoint_auth_method\" type=\"str | None\">\n  Token endpoint authentication method for the upstream OAuth server. Controls\n  how the proxy authenticates when exchanging authorization codes and refresh\n  tokens with the upstream provider. - `\"client_secret_basic\"`: Send credentials\n  in Authorization header (most common) - `\"client_secret_post\"`: Send\n  credentials in request body (required by some providers) - `\"none\"`: No\n  authentication (for public clients) - `None` (default): Uses authlib's default\n  (typically `\"client_secret_basic\"`) Set this if your provider requires a\n  specific authentication method and the default doesn't work.\n</ParamField>\n\n<ParamField body=\"allowed_client_redirect_uris\" type=\"list[str] | None\">\n  List of allowed redirect URI patterns for MCP clients. Patterns support\n  wildcards (e.g., `\"http://localhost:*\"`, `\"https://*.example.com/*\"`). -\n  `None` (default): All redirect URIs allowed (for MCP/DCR compatibility) -\n  Empty list `[]`: No redirect URIs allowed - Custom list: Only matching\n  patterns allowed These patterns apply to MCP client loopback redirects, NOT\n  the upstream OAuth app redirect URI.\n</ParamField>\n\n<ParamField body=\"valid_scopes\" type=\"list[str] | None\">\n  List of all possible valid scopes for the OAuth provider. These are advertised\n  to clients through the `/.well-known` endpoints. Defaults to `required_scopes`\n  from your TokenVerifier if not specified.\n</ParamField>\n\n<ParamField body=\"extra_authorize_params\" type=\"dict[str, str] | None\">\n  Additional parameters to forward to the upstream authorization endpoint. Useful for provider-specific parameters that aren't part of the standard OAuth2 flow.\n  \n  For example, Auth0 requires an `audience` parameter to issue JWT tokens:\n  ```python\n  extra_authorize_params={\"audience\": \"https://api.example.com\"}\n  ```\n  \n  These parameters are added to every authorization request sent to the upstream provider.\n</ParamField>\n\n<ParamField body=\"extra_token_params\" type=\"dict[str, str] | None\">\n  Additional parameters to forward to the upstream token endpoint during code exchange and token refresh. Useful for provider-specific requirements during token operations.\n\nFor example, some providers require additional context during token exchange:\n\n```python\nextra_token_params={\"audience\": \"https://api.example.com\"}\n```\n\nThese parameters are included in all token requests to the upstream provider.\n\n</ParamField>\n\n<ParamField body=\"client_storage\" type=\"AsyncKeyValue | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Storage backend for persisting OAuth client registrations and upstream tokens.\n\n  **Default behavior:**\n  By default, clients are automatically persisted to an encrypted disk store, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. The disk store is encrypted using a key derived from the JWT Signing Key (which is derived from the upstream client secret by default). For client registrations to survive upstream client secret rotation, you should provide a JWT Signing Key or your own client_storage.\n\nFor production deployments with multiple servers or cloud deployments, see [Storage Backends](/servers/storage-backends) for available options.\n\n<Warning>\n  **When providing custom storage**, wrap it in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest:\n\n  ```python\n  from key_value.aio.stores.redis import RedisStore\n  from key_value.aio.wrappers.encryption import FernetEncryptionWrapper\n  from cryptography.fernet import Fernet\n  import os\n\n  auth = OAuthProxy(\n      ...,\n      jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n      client_storage=FernetEncryptionWrapper(\n          key_value=RedisStore(host=\"redis.example.com\", port=6379),\n          fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n      )\n  )\n  ```\n\n  Without encryption, upstream OAuth tokens are stored in plaintext.\n</Warning>\n\nTesting with in-memory storage (unencrypted):\n\n```python\nfrom key_value.aio.stores.memory import MemoryStore\n\n# Use in-memory storage for testing (clients lost on restart)\nauth = OAuthProxy(..., client_storage=MemoryStore())\n```\n\n</ParamField>\n\n<ParamField body=\"jwt_signing_key\" type=\"str | bytes | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.\n\n  **Default behavior (`None`):**\n  Derives a 32-byte key using PBKDF2 from the upstream client secret.\n\n  **For production:**\n  Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the key derived from the upstream client secret. This allows you to manage keys securely in cloud environments, allows keys to work across multiple instances, and allows you to rotate keys without losing client registrations.\n\n  ```python\n  import os\n\n  auth = OAuthProxy(\n      ...,\n      jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],  # Any sufficiently complex string!\n      client_storage=RedisStore(...)  # Persistent storage\n  )\n  ```\n\n  See [HTTP Deployment - OAuth Token Security](/deployment/http#oauth-token-security) for complete production setup.\n</ParamField>\n\n\n<ParamField body=\"require_authorization_consent\" type=\"bool\" default=\"True\">\n  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.\n\n  **Default behavior (True):**\n  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.\n\n  **Disabling consent (False):**\n  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.\n\n  ```python\n  # Development/testing only - skip consent screen\n  auth = OAuthProxy(\n      ...,\n      require_authorization_consent=False  # ⚠️ Security warning: only for local/testing\n  )\n  ```\n\n  <Warning>\n    Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.\n  </Warning>\n</ParamField>\n\n<ParamField body=\"consent_csp_policy\" type=\"str | None\" default=\"None\">\n  Content Security Policy for the consent page.\n\n  - `None` (default): Uses the built-in CSP policy with appropriate directives for form submission\n  - Empty string `\"\"`: Disables CSP entirely (no meta tag rendered)\n  - Custom string: Uses the provided value as the CSP policy\n\n  This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives.\n\n  ```python\n  # Disable CSP entirely (let org CSP policies apply)\n  auth = OAuthProxy(..., consent_csp_policy=\"\")\n\n  # Use custom CSP policy\n  auth = OAuthProxy(..., consent_csp_policy=\"default-src 'self'; style-src 'unsafe-inline'\")\n  ```\n</ParamField>\n</Card>\n\n### Using Built-in Providers\n\nFastMCP includes pre-configured providers for common services:\n\n```python\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=\"your-github-app-id\",\n    client_secret=\"your-github-app-secret\",\n    base_url=\"https://your-server.com\"\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\nAvailable providers include `GitHubProvider`, `GoogleProvider`, and others. These handle token verification automatically.\n\n### Token Verification\n\nThe OAuth proxy requires a compatible `TokenVerifier` to validate tokens from your provider. Different providers use different token formats:\n\n- **JWT tokens** (Google, Azure): Use `JWTVerifier` with the provider's JWKS endpoint\n- **Opaque tokens with RFC 7662 introspection** (Auth0, Okta, WorkOS): Use `IntrospectionTokenVerifier`\n- **Opaque tokens (provider-specific)** (GitHub, Discord): Use provider-specific verifiers like `GitHubTokenVerifier`\n\nSee the [Token Verification guide](/servers/auth/token-verification) for detailed setup instructions for your provider.\n\n### Scope Configuration\n\nOAuth scopes control what permissions your application requests from users. They're configured through your `TokenVerifier` (required for the OAuth proxy to validate tokens from your provider). Set `required_scopes` to automatically request the permissions your application needs:\n\n```python\nJWTVerifier(..., required_scopes = [\"read:user\", \"write:data\"])\n```\n\nDynamic clients created by the proxy will automatically include these scopes in their authorization requests. See the [Token Verification](#token-verification) section below for detailed setup.\n\n### Custom Parameters\n\nSome OAuth providers require additional parameters beyond the standard OAuth2 flow. Use `extra_authorize_params` and `extra_token_params` to pass provider-specific requirements. For example, Auth0 requires an `audience` parameter to issue JWT tokens instead of opaque tokens:\n\n```python\nauth = OAuthProxy(\n    upstream_authorization_endpoint=\"https://your-domain.auth0.com/authorize\",\n    upstream_token_endpoint=\"https://your-domain.auth0.com/oauth/token\",\n    upstream_client_id=\"your-auth0-client-id\",\n    upstream_client_secret=\"your-auth0-client-secret\",\n\n    # Auth0-specific audience parameter\n    extra_authorize_params={\"audience\": \"https://your-api-identifier.com\"},\n    extra_token_params={\"audience\": \"https://your-api-identifier.com\"},\n\n    token_verifier=JWTVerifier(\n        jwks_uri=\"https://your-domain.auth0.com/.well-known/jwks.json\",\n        issuer=\"https://your-domain.auth0.com/\",\n        audience=\"https://your-api-identifier.com\"\n    ),\n    base_url=\"https://your-server.com\"\n)\n```\n\nThe proxy also automatically forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them.\n\n## OAuth Flow\n\n```mermaid\nsequenceDiagram\n    participant Client as MCP Client<br/>(localhost:random)\n    participant User as User\n    participant Proxy as FastMCP OAuth Proxy<br/>(server:8000)\n    participant Provider as OAuth Provider<br/>(GitHub, etc.)\n\n    Note over Client, Proxy: Dynamic Registration (Local)\n    Client->>Proxy: 1. POST /register<br/>redirect_uri: localhost:54321/callback\n    Proxy-->>Client: 2. Returns fixed upstream credentials\n\n    Note over Client, User: Authorization with User Consent\n    Client->>Proxy: 3. GET /authorize<br/>redirect_uri=localhost:54321/callback<br/>code_challenge=CLIENT_CHALLENGE\n    Note over Proxy: Store transaction with client PKCE<br/>Generate proxy PKCE pair\n    Proxy->>User: 4. Show consent page<br/>(client details, redirect URI, scopes)\n    User->>Proxy: 5. Approve/deny consent\n    Note over Proxy: Set consent binding cookie<br/>(binds browser to this flow)\n    Proxy->>Provider: 6. Redirect to provider<br/>redirect_uri=server:8000/auth/callback<br/>code_challenge=PROXY_CHALLENGE\n\n    Note over Provider, Proxy: Provider Callback\n    Provider->>Proxy: 7. GET /auth/callback<br/>with authorization code\n    Note over Proxy: Verify consent binding cookie<br/>(reject if missing or mismatched)\n    Proxy->>Provider: 8. Exchange code for tokens<br/>code_verifier=PROXY_VERIFIER\n    Provider-->>Proxy: 9. Access & refresh tokens\n\n    Note over Proxy, Client: Client Callback Forwarding\n    Proxy->>Client: 10. Redirect to localhost:54321/callback<br/>with new authorization code\n\n    Note over Client, Proxy: Token Exchange\n    Client->>Proxy: 11. POST /token with code<br/>code_verifier=CLIENT_VERIFIER\n    Proxy-->>Client: 12. Returns FastMCP JWT tokens\n```\n\nThe flow diagram above illustrates the complete OAuth proxy pattern. Let's understand each phase:\n\n### Registration Phase\n\nWhen an MCP client calls `/register` with its dynamic callback URL, the proxy responds with your pre-configured upstream credentials. The client stores these credentials believing it has registered a new app. Meanwhile, the proxy records the client's callback URL for later use.\n\n### Authorization Phase\n\nThe client initiates OAuth by redirecting to the proxy's `/authorize` endpoint. The proxy:\n\n1. Stores the client's transaction with its PKCE challenge\n2. Generates its own PKCE parameters for upstream security\n3. Shows the user a consent page with the client's details, redirect URI, and requested scopes\n4. If the user approves (or the client was previously approved), sets a consent binding cookie and redirects to the upstream provider using the fixed callback URL\n\nThis dual-PKCE approach maintains end-to-end security at both the client-to-proxy and proxy-to-provider layers. The consent step protects against confused deputy attacks by ensuring you explicitly approve each client before it can complete authorization, and the consent binding cookie ensures that only the browser that approved consent can complete the callback.\n\n### Callback Phase\n\nAfter user authorization, the provider redirects back to the proxy's fixed callback URL. The proxy:\n\n1. Verifies the consent binding cookie matches the transaction (rejecting requests from a different browser)\n2. Exchanges the authorization code for tokens with the provider\n3. Stores these tokens temporarily\n4. Generates a new authorization code for the client\n5. Redirects to the client's original dynamic callback URL\n\n### Token Exchange Phase\n\nFinally, the client exchanges its authorization code with the proxy. The proxy validates the client's PKCE verifier, then issues its own FastMCP JWT tokens (rather than forwarding the upstream provider's tokens). See [Token Architecture](#token-architecture) for details on this design.\n\nThis entire flow is transparent to the MCP client—it experiences a standard OAuth flow with dynamic registration, unaware that a proxy is managing the complexity behind the scenes.\n\n### Token Architecture\n\nThe OAuth proxy implements a **token factory pattern**: instead of directly forwarding tokens from the upstream OAuth provider, it issues its own JWT tokens to MCP clients. This maintains proper OAuth 2.0 token audience boundaries and enables better security controls.\n\n**How it works:**\n\nWhen an MCP client completes authorization, the proxy:\n\n1. **Receives upstream tokens** from the OAuth provider (GitHub, Google, etc.)\n2. **Encrypts and stores** these tokens using Fernet encryption (AES-128-CBC + HMAC-SHA256)\n3. **Issues FastMCP JWT tokens** to the client, signed with HS256\n\nThe FastMCP JWT contains minimal claims: issuer, audience, client ID, scopes, expiration, and a unique token identifier (JTI). The JTI acts as a reference linking to the encrypted upstream token.\n\n**Token validation:**\n\nWhen a client makes an MCP request with its FastMCP token:\n\n1. **FastMCP validates the JWT** signature, expiration, issuer, and audience\n2. **Looks up the upstream token** using the JTI from the validated JWT\n3. **Decrypts and validates** the upstream token with the provider\n\nThis two-tier validation ensures that FastMCP tokens can only be used with this server (via audience validation) while maintaining full upstream token security.\n\nThis architecture also prevents [token passthrough](#token-passthrough) — see the [Security](#security) section for details.\n\n**Token expiry alignment:**\n\nFastMCP token lifetimes match the upstream token lifetimes. When the upstream token expires, the FastMCP token also expires, maintaining consistent security boundaries.\n\n**Refresh tokens:**\n\nThe proxy issues its own refresh tokens that map to upstream refresh tokens. When a client uses a FastMCP refresh token, the proxy refreshes the upstream token and issues a new FastMCP access token.\n\n### PKCE Forwarding\n\nThe OAuth proxy automatically handles PKCE (Proof Key for Code Exchange) when working with providers that support or require it. The proxy generates its own PKCE parameters to send upstream while separately validating the client's PKCE, ensuring end-to-end security at both layers.\n\nThis is enabled by default via the `forward_pkce` parameter and works seamlessly with providers like Google, Azure AD, and GitHub. Only disable it for legacy providers that don't support PKCE:\n\n```python\n# Disable PKCE forwarding only if upstream doesn't support it\nauth = OAuthProxy(\n    ...,\n    forward_pkce=False  # Default is True\n)\n```\n\n### Redirect URI Validation\n\nWhile the OAuth proxy accepts all redirect URIs by default (for DCR compatibility), you can restrict which clients can connect by specifying allowed patterns:\n\n```python\n# Allow only localhost clients (common for development)\nauth = OAuthProxy(\n    # ... other parameters ...\n    allowed_client_redirect_uris=[\n        \"http://localhost:*\",\n        \"http://127.0.0.1:*\"\n    ]\n)\n\n# Allow specific known clients\nauth = OAuthProxy(\n    # ... other parameters ...\n    allowed_client_redirect_uris=[\n        \"http://localhost:*\",\n        \"https://claude.ai/api/mcp/auth_callback\",\n        \"https://*.mycompany.com/auth/*\"  # Wildcard patterns supported\n    ]\n)\n```\n\nCheck your server logs for \"Client registered with redirect_uri\" messages to identify what URLs your clients use.\n\n## CIMD Support\n\n<VersionBadge version=\"3.0.0\" />\n\nThe OAuth proxy supports **Client ID Metadata Documents (CIMD)**, an alternative to Dynamic Client Registration where clients host a static JSON document at an HTTPS URL. Instead of registering dynamically, clients simply provide their CIMD URL as their `client_id`, and the server fetches and validates the metadata.\n\nCIMD clients appear in the consent screen with a verified domain badge, giving users confidence about which application is requesting access. This provides stronger identity verification than DCR, where any client can claim any name.\n\n### How CIMD Works\n\nWhen a client presents an HTTPS URL as its `client_id` (for example, `https://myapp.example.com/oauth/client.json`), the OAuth proxy recognizes it as a CIMD client and:\n\n1. Fetches the JSON document from that URL\n2. Validates that the document's `client_id` field matches the URL\n3. Extracts client metadata (name, redirect URIs, scopes, etc.)\n4. Stores the client persistently alongside DCR clients\n5. Shows the verified domain in the consent screen\n\nThis flow happens transparently. MCP clients that support CIMD simply provide their metadata URL instead of registering, and the OAuth proxy handles the rest.\n\n### CIMD Configuration\n\nCIMD support is enabled by default for `OAuthProxy`.\n\n<Card icon=\"code\" title=\"CIMD Parameters\">\n<ParamField body=\"enable_cimd\" type=\"bool\" default=\"True\">\n  Whether to accept CIMD URLs as client identifiers. When enabled, clients can use HTTPS URLs pointing to metadata documents as their `client_id` instead of registering via DCR.\n</ParamField>\n</Card>\n\n### Private Key JWT Authentication\n\nCIMD clients can authenticate using `private_key_jwt` instead of the default `none` authentication method. This provides cryptographic proof of client identity by signing JWT assertions with a private key, while the server verifies using the client's public key from their CIMD document.\n\nTo use `private_key_jwt`, the CIMD document must include either a `jwks_uri` (URL to fetch the public key set) or inline `jwks` (the key set directly in the document):\n\n```json\n{\n  \"client_id\": \"https://myapp.example.com/oauth/client.json\",\n  \"client_name\": \"My Secure App\",\n  \"redirect_uris\": [\"http://localhost:*/callback\"],\n  \"token_endpoint_auth_method\": \"private_key_jwt\",\n  \"jwks_uri\": \"https://myapp.example.com/.well-known/jwks.json\"\n}\n```\n\nThe OAuth proxy validates JWT assertions according to RFC 7523, checking the signature, issuer, audience, subject claims, and preventing replay attacks via JTI tracking.\n\n### Security Considerations\n\nCIMD provides several security advantages over DCR:\n\n- **Verified identity**: The domain in the `client_id` URL is verified by HTTPS, so users know which organization is requesting access\n- **No registration required**: Clients don't need to store or manage dynamically-issued credentials\n- **Redirect URI enforcement**: CIMD documents must declare `redirect_uris`, which are enforced by the proxy (wildcard patterns supported)\n- **SSRF protection**: The OAuth proxy blocks fetches to localhost, private IPs, and reserved addresses\n- **Replay prevention**: For `private_key_jwt` clients, JTI claims are tracked to prevent assertion replay\n- **Cache-aware fetching**: CIMD documents are cached according to HTTP cache headers and revalidated when required\n\nCIMD is enabled by default. To disable it entirely (for example, to require all clients to register via DCR), set `enable_cimd=False` explicitly:\n\n```python\nauth = OAuthProxy(\n    ...,\n    enable_cimd=False,\n)\n```\n\n## Security\n\n### Key and Storage Management\n\n<VersionBadge version=\"2.13.0\" />\nThe OAuth proxy requires cryptographic keys for JWT signing and storage encryption, plus persistent storage to maintain valid tokens across server restarts.\n\n**Default behavior (appropriate for development only):**\n- **Mac/Windows**: FastMCP automatically generates keys and stores them in your system keyring. Storage defaults to disk. Tokens survive server restarts. This is **only** suitable for development and local testing.\n- **Linux**: Keys are ephemeral (random salt at startup). Storage defaults to memory. Tokens become invalid on server restart.\n\n**For production:**\nConfigure the following parameters together: provide a unique `jwt_signing_key` (for signing FastMCP JWTs), and a shared `client_storage` backend (for storing tokens). Both are required for production deployments. Use a network-accessible storage backend like Redis or DynamoDB rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** (see the `client_storage` parameter documentation above for examples). The keys accept any secret string and derive proper cryptographic keys using HKDF. See [OAuth Token Security](/deployment/http#oauth-token-security) and [Storage Backends](/servers/storage-backends) for complete production setup.\n\n### Confused Deputy Attacks\n\n<VersionBadge version=\"2.13.0\" />\n\nA confused deputy attack allows a malicious client to steal your authorization by tricking you into granting it access under your identity.\n\nThe OAuth proxy works by bridging DCR clients to traditional auth providers, which means that multiple MCP clients connect through a single upstream OAuth application. An attacker can exploit this shared application by registering a malicious client with their own redirect URI, then sending you an authorization link. When you click it, your browser goes through the OAuth flow—but since you may have already authorized this OAuth app before, the provider might auto-approve the request. The authorization code then gets sent to the attacker's redirect URI instead of a legitimate client, giving them access under your credentials.\n\n#### Mitigation\n\nFastMCP's OAuth proxy defends against confused deputy attacks with two layers of protection:\n\n**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.\n\n![](/assets/images/oauth-proxy-consent-screen.png)\n\nThe 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.\n\n**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.\n\n**Learn more:**\n- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance\n- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky\n\n### Token Passthrough\n\n[Token passthrough](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#token-passthrough) occurs when an intermediary exposes upstream tokens to downstream clients, allowing those clients to impersonate the intermediary or access services they shouldn't reach.\n\n#### Client-facing mitigation\n\nThe OAuth proxy's [token factory architecture](#token-architecture) prevents this by design. MCP clients only ever receive FastMCP-issued JWTs — the upstream provider token is never sent to the client. A FastMCP JWT is scoped to your server and cannot be used to access the upstream provider directly, even if intercepted.\n\n#### Calling downstream services\n\nWhen your MCP server needs to call other APIs on behalf of the authenticated user, avoid forwarding the upstream token directly — this reintroduces the token passthrough problem in the other direction. Instead, use a token exchange flow like [OAuth 2.0 Token Exchange (RFC 8693)](https://datatracker.ietf.org/doc/html/rfc8693) or your provider's equivalent (such as Azure's [On-Behalf-Of flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow)) to obtain a new token scoped to the downstream service.\n\nThe upstream token is available in your tool functions via `get_access_token()` or the `CurrentAccessToken` dependency, which you can use as the assertion for a token exchange. The exchanged token will be scoped to the specific downstream service and identify your MCP server as the authorized intermediary, maintaining proper audience boundaries throughout the chain.\n\n## Production Configuration\n\nFor production deployments, load sensitive credentials from environment variables:\n\n```python\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\n# Load secrets from environment variables\nauth = GitHubProvider(\n    client_id=os.environ.get(\"GITHUB_CLIENT_ID\"),\n    client_secret=os.environ.get(\"GITHUB_CLIENT_SECRET\"),\n    base_url=os.environ.get(\"BASE_URL\", \"https://your-production-server.com\")\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n\n@mcp.tool\ndef protected_tool(data: str) -> str:\n    \"\"\"This tool is now protected by OAuth.\"\"\"\n    return f\"Processed: {data}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\nThis keeps secrets out of your codebase while maintaining explicit configuration.\n"
  },
  {
    "path": "docs/servers/auth/oidc-proxy.mdx",
    "content": "---\ntitle: OIDC Proxy\nsidebarTitle: OIDC Proxy\ndescription: Bridge OIDC providers to work seamlessly with MCP's authentication flow.\nicon: share\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.12.4\" />\n\nThe OIDC proxy enables FastMCP servers to authenticate with OIDC providers that **don't support Dynamic Client Registration (DCR)** out of the box. This includes OAuth providers like: Auth0, Google, Azure, AWS, etc. For providers that do support DCR (like WorkOS AuthKit), use [`RemoteAuthProvider`](/servers/auth/remote-oauth) instead.\n\nThe OIDC proxy is built upon [`OAuthProxy`](/servers/auth/oauth-proxy) so it has all the same functionality under the covers.\n\n## Implementation\n\n### Provider Setup Requirements\n\nBefore using the OIDC proxy, you need to register your application with your OAuth provider:\n\n1. **Register your application** in the provider's developer console (Auth0 Applications, Google Cloud Console, Azure Portal, etc.)\n2. **Configure the redirect URI** as your FastMCP server URL plus your chosen callback path:\n   - Default: `https://your-server.com/auth/callback`\n   - Custom: `https://your-server.com/your/custom/path` (if you set `redirect_path`)\n   - Development: `http://localhost:8000/auth/callback`\n3. **Obtain your credentials**: Client ID and Client Secret\n\n<Warning>\n  The redirect URI you configure with your provider must exactly match your\n  FastMCP server's URL plus the callback path. If you customize `redirect_path`\n  in the OIDC proxy, update your provider's redirect URI accordingly.\n</Warning>\n\n### Basic Setup\n\nHere's how to implement the OIDC proxy with any provider:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.oidc_proxy import OIDCProxy\n\n# Create the OIDC proxy\nauth = OIDCProxy(\n    # Provider's configuration URL\n    config_url=\"https://provider.com/.well-known/openid-configuration\",\n\n    # Your registered app credentials\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n\n    # Your FastMCP server's public URL\n    base_url=\"https://your-server.com\",\n\n    # Optional: customize the callback path (default is \"/auth/callback\")\n    # redirect_path=\"/custom/callback\",\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\n### Configuration Parameters\n\n<Card icon=\"code\" title=\"OIDCProxy Parameters\">\n<ParamField body=\"config_url\" type=\"str\" required>\n  URL of your OAuth provider's OIDC configuration\n</ParamField>\n\n<ParamField body=\"client_id\" type=\"str\" required>\n  Client ID from your registered OAuth application\n</ParamField>\n\n<ParamField body=\"client_secret\" type=\"str | None\">\n  Client secret from your registered OAuth application. Optional for PKCE public\n  clients. When omitted, `jwt_signing_key` must be provided.\n</ParamField>\n\n<ParamField body=\"base_url\" type=\"AnyHttpUrl | str\" required>\n  Public URL of your FastMCP server (e.g., `https://your-server.com`)\n</ParamField>\n\n<ParamField body=\"strict\" type=\"bool | None\">\n  Strict flag for configuration validation. When True, requires all OIDC\n  mandatory fields.\n</ParamField>\n\n<ParamField body=\"audience\" type=\"str | None\">\n  Audience parameter for OIDC providers that require it (e.g., Auth0). This is\n  typically your API identifier.\n</ParamField>\n\n<ParamField body=\"timeout_seconds\" type=\"int | None\" default=\"10\">\n  HTTP request timeout in seconds for fetching OIDC configuration\n</ParamField>\n\n<ParamField body=\"token_verifier\" type=\"TokenVerifier | None\">\n\n<VersionBadge version=\"2.13.1\" />\n  Custom token verifier for validating tokens. When provided, FastMCP uses your custom verifier instead of creating a default `JWTVerifier`.\n\n  Cannot be used with `algorithm` or `required_scopes` parameters - configure these on your verifier instead. The verifier's `required_scopes` are automatically loaded and advertised.\n</ParamField>\n\n<ParamField body=\"algorithm\" type=\"str | None\">\n  JWT algorithm to use for token verification (e.g., \"RS256\"). If not specified,\n  uses the provider's default. Only used when `token_verifier` is not provided.\n</ParamField>\n\n<ParamField body=\"required_scopes\" type=\"list[str] | None\">\n  List of OAuth scopes for token validation. These are automatically\n  included in authorization requests. Only used when `token_verifier` is not provided.\n</ParamField>\n\n<ParamField body=\"redirect_path\" type=\"str\" default=\"/auth/callback\">\n  Path for OAuth callbacks. Must match the redirect URI configured in your OAuth\n  application\n</ParamField>\n\n<ParamField body=\"allowed_client_redirect_uris\" type=\"list[str] | None\">\n  List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., `\"http://localhost:*\"`, `\"https://*.example.com/*\"`).\n  - `None` (default): All redirect URIs allowed (for MCP/DCR compatibility)\n  - Empty list `[]`: No redirect URIs allowed\n  - Custom list: Only matching patterns allowed\n\nThese patterns apply to MCP client loopback redirects, NOT the upstream OAuth app redirect URI.\n\n</ParamField>\n\n<ParamField body=\"token_endpoint_auth_method\" type=\"str | None\">\n  Token endpoint authentication method for the upstream OAuth server. Controls how the proxy authenticates when exchanging authorization codes and refresh tokens with the upstream provider.\n  - `\"client_secret_basic\"`: Send credentials in Authorization header (most common)\n  - `\"client_secret_post\"`: Send credentials in request body (required by some providers)\n  - `\"none\"`: No authentication (for public clients)\n  - `None` (default): Uses authlib's default (typically `\"client_secret_basic\"`)\n\nSet this if your provider requires a specific authentication method and the default doesn't work.\n\n</ParamField>\n\n<ParamField body=\"jwt_signing_key\" type=\"str | bytes | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.\n\n  **Default behavior (`None`):**\n  - **Mac/Windows**: Auto-managed via system keyring. Keys are generated once and persisted, surviving server restarts with zero configuration. Keys are automatically derived from server attributes, so this approach, while convenient, is **only** suitable for development and local testing. For production, you must provide an explicit secret.\n  - **Linux**: Ephemeral (random salt at startup). Tokens become invalid on server restart, triggering client re-authentication.\n\n  **For production:**\n  Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the auto-generated one.\n</ParamField>\n\n<ParamField body=\"client_storage\" type=\"AsyncKeyValue | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Storage backend for persisting OAuth client registrations and upstream tokens.\n\n  **Default behavior:**\n  - **Mac/Windows**: Encrypted DiskStore in your platform's data directory (derived from `platformdirs`)\n  - **Linux**: MemoryStore (ephemeral - clients lost on restart)\n\n  By default on Mac/Windows, clients are automatically persisted to encrypted disk storage, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. On Linux where keyring isn't available, ephemeral storage is used to match the ephemeral key strategy.\n\nFor production deployments with multiple servers or cloud deployments, use a network-accessible storage backend rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.** See [Storage Backends](/servers/storage-backends) for available options.\n\nTesting with in-memory storage (unencrypted):\n\n```python\nfrom key_value.aio.stores.memory import MemoryStore\n\n# Use in-memory storage for testing (clients lost on restart)\nauth = OIDCProxy(..., client_storage=MemoryStore())\n```\n\nProduction with encrypted Redis storage:\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\nimport os\n\nauth = OIDCProxy(\n    ...,\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(host=\"redis.example.com\", port=6379),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n```\n\n</ParamField>\n\n<ParamField body=\"require_authorization_consent\" type=\"bool\" default=\"True\">\n  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.\n</ParamField>\n\n<ParamField body=\"consent_csp_policy\" type=\"str | None\" default=\"None\">\n  Content Security Policy for the consent page.\n\n  - `None` (default): Uses the built-in CSP policy with appropriate directives for form submission\n  - Empty string `\"\"`: Disables CSP entirely (no meta tag rendered)\n  - Custom string: Uses the provided value as the CSP policy\n\n  This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives.\n</ParamField>\n</Card>\n\n### Using Built-in Providers\n\nFastMCP includes pre-configured OIDC providers for common services:\n\n```python\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\n\nauth = Auth0Provider(\n    config_url=\"https://.../.well-known/openid-configuration\",\n    client_id=\"your-auth0-client-id\",\n    client_secret=\"your-auth0-client-secret\",\n    audience=\"https://...\",\n    base_url=\"https://localhost:8000\"\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\nAvailable providers include `Auth0Provider` at present.\n\n### Scope Configuration\n\nOAuth scopes are configured with `required_scopes` to automatically request the permissions your application needs.\n\nDynamic clients created by the proxy will automatically include these scopes in their authorization requests.\n\n## CIMD Support\n\n<VersionBadge version=\"3.0.0\" />\n\nThe OIDC proxy inherits full CIMD (Client ID Metadata Document) support from `OAuthProxy`. Clients can use HTTPS URLs as their `client_id` instead of registering dynamically, and the proxy will fetch and validate their metadata document.\n\nSee the [OAuth Proxy CIMD documentation](/servers/auth/oauth-proxy#cimd-support) for complete details on how CIMD works, including private key JWT authentication and security considerations.\n\nThe CIMD-related parameters available on `OIDCProxy` are:\n\n<Card icon=\"code\" title=\"CIMD Parameters\">\n<ParamField body=\"enable_cimd\" type=\"bool\" default=\"True\">\n  Whether to accept CIMD URLs as client identifiers.\n</ParamField>\n</Card>\n\n## Production Configuration\n\nFor production deployments, load sensitive credentials from environment variables:\n\n```python\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\n\n# Load secrets from environment variables\nauth = Auth0Provider(\n    config_url=os.environ.get(\"AUTH0_CONFIG_URL\"),\n    client_id=os.environ.get(\"AUTH0_CLIENT_ID\"),\n    client_secret=os.environ.get(\"AUTH0_CLIENT_SECRET\"),\n    audience=os.environ.get(\"AUTH0_AUDIENCE\"),\n    base_url=os.environ.get(\"BASE_URL\", \"https://localhost:8000\")\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n\n@mcp.tool\ndef protected_tool(data: str) -> str:\n    \"\"\"This tool is now protected by OAuth.\"\"\"\n    return f\"Processed: {data}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\nThis keeps secrets out of your codebase while maintaining explicit configuration.\n"
  },
  {
    "path": "docs/servers/auth/remote-oauth.mdx",
    "content": "---\ntitle: Remote OAuth\nsidebarTitle: Remote OAuth\ndescription: Integrate your FastMCP server with external identity providers like Descope, WorkOS, Auth0, and corporate SSO systems.\nicon: camera-cctv\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nRemote OAuth integration allows your FastMCP server to leverage external identity providers that **support Dynamic Client Registration (DCR)**. With DCR, MCP clients can automatically register themselves with the identity provider and obtain credentials without any manual configuration. This provides enterprise-grade authentication with fully automated flows, making it ideal for production applications with modern identity providers.\n\n<Tip>\n**When to use RemoteAuthProvider vs OAuth Proxy:**\n- **RemoteAuthProvider**: For providers WITH Dynamic Client Registration (Descope, WorkOS AuthKit, modern OIDC providers)\n- **OAuth Proxy**: For providers WITHOUT Dynamic Client Registration (GitHub, Google, Azure, AWS, Discord, etc.)\n\nRemoteAuthProvider requires DCR support for fully automated client registration and authentication.\n</Tip>\n\n## DCR-Enabled Providers\n\nRemoteAuthProvider works with identity providers that support **Dynamic Client Registration (DCR)** - a critical capability that enables automated authentication flows:\n\n| Feature | DCR Providers (RemoteAuth) | Non-DCR Providers (OAuth Proxy) |\n|---------|---------------------------|--------------------------------|\n| **Client Registration** | Automatic via API | Manual in provider console |\n| **Credentials** | Dynamic per client | Fixed app credentials |\n| **Configuration** | Zero client config | Pre-shared credentials |\n| **Examples** | Descope, WorkOS AuthKit, modern OIDC | GitHub, Google, Azure |\n| **FastMCP Class** | `RemoteAuthProvider` | [`OAuthProxy`](/servers/auth/oauth-proxy) |\n\nIf your provider doesn't support DCR (most traditional OAuth providers), you'll need to use [`OAuth Proxy`](/servers/auth/oauth-proxy) instead, which bridges the gap between MCP's DCR expectations and fixed OAuth credentials.\n\n## The Remote OAuth Challenge\n\nTraditional OAuth flows assume human users with web browsers who can interact with login forms, consent screens, and redirects. MCP clients operate differently - they're often automated systems that need to authenticate programmatically without human intervention.\n\nThis creates several unique requirements that standard OAuth implementations don't address well:\n\n**Automatic Discovery**: MCP clients must discover authentication requirements by examining server metadata rather than encountering HTTP redirects. They need to know which identity provider to use and how to reach it before making any authenticated requests.\n\n**Programmatic Registration**: Clients need to register themselves with identity providers automatically. Manual client registration doesn't work when clients might be dynamically created tools or services.\n\n**Seamless Token Management**: Clients must obtain, store, and refresh tokens without user interaction. The authentication flow needs to work in headless environments where no human is available to complete OAuth consent flows.\n\n**Protocol Integration**: The authentication process must integrate cleanly with MCP's JSON-RPC transport layer and error handling mechanisms.\n\nThese requirements mean that your MCP server needs to do more than just validate tokens - it needs to provide discovery metadata that enables MCP clients to understand and navigate your authentication requirements automatically.\n\n## MCP Authentication Discovery\n\nMCP authentication discovery relies on well-known endpoints that clients can examine to understand your authentication requirements. Your server becomes a bridge between MCP clients and your chosen identity provider.\n\nThe core discovery endpoint is `/.well-known/oauth-protected-resource`, which tells clients that your server requires OAuth authentication and identifies the authorization servers you trust. This endpoint contains static metadata that points clients to your identity provider without requiring any dynamic lookups.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant FastMCPServer as FastMCP Server\n    participant ExternalIdP as Identity Provider\n\n    Client->>FastMCPServer: 1. GET /.well-known/oauth-protected-resource\n    FastMCPServer-->>Client: 2. \"Use https://my-idp.com for auth\"\n    \n    note over Client, ExternalIdP: Client goes directly to the IdP\n    Client->>ExternalIdP: 3. Authenticate & get token via DCR\n    ExternalIdP-->>Client: 4. Access token\n    \n    Client->>FastMCPServer: 5. MCP request with Bearer token\n    FastMCPServer->>FastMCPServer: 6. Verify token signature\n    FastMCPServer-->>Client: 7. MCP response\n```\n\nThis flow separates concerns cleanly: your MCP server handles resource protection and token validation, while your identity provider handles user authentication and token issuance. The client coordinates between these systems using standardized OAuth discovery mechanisms.\n\n## FastMCP Remote Authentication\n\n<VersionBadge version=\"2.11.1\" />\n\nFastMCP provides `RemoteAuthProvider` to handle the complexities of remote OAuth integration. This class combines token validation capabilities with the OAuth discovery metadata that MCP clients require.\n\n### RemoteAuthProvider\n\n`RemoteAuthProvider` works by composing a [`TokenVerifier`](/servers/auth/token-verification) with authorization server information. A `TokenVerifier` is another FastMCP authentication class that focuses solely on token validation - signature verification, expiration checking, and claim extraction. The `RemoteAuthProvider` takes that token validation capability and adds the OAuth discovery endpoints that enable MCP clients to automatically find and authenticate with your identity provider.\n\nThis composition pattern means you can use any token validation strategy while maintaining consistent OAuth discovery behavior:\n- **JWT tokens**: Use `JWTVerifier` for self-contained tokens\n- **Opaque tokens**: Use `IntrospectionTokenVerifier` for RFC 7662 introspection\n- **Custom validation**: Implement your own `TokenVerifier` subclass\n\nThe separation allows you to change token validation approaches without affecting the client discovery experience.\n\nThe class automatically generates the required OAuth metadata endpoints using the MCP SDK's standardized route creation functions. This ensures compatibility with MCP clients while reducing the implementation complexity for server developers.\n\n### Basic Implementation\n\nMost applications can use `RemoteAuthProvider` directly without subclassing. The implementation requires a `TokenVerifier` instance, a list of trusted authorization servers, and your server's URL for metadata generation.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import RemoteAuthProvider\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom pydantic import AnyHttpUrl\n\n# Configure token validation for your identity provider\ntoken_verifier = JWTVerifier(\n    jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n    issuer=\"https://auth.yourcompany.com\",\n    audience=\"mcp-production-api\"\n)\n\n# Create the remote auth provider\nauth = RemoteAuthProvider(\n    token_verifier=token_verifier,\n    authorization_servers=[AnyHttpUrl(\"https://auth.yourcompany.com\")],\n    base_url=\"https://api.yourcompany.com\",  # Your server base URL\n    # Optional: restrict allowed client redirect URIs (defaults to all for DCR compatibility)\n    allowed_client_redirect_uris=[\"http://localhost:*\", \"http://127.0.0.1:*\"]\n)\n\nmcp = FastMCP(name=\"Company API\", auth=auth)\n```\n\nThis configuration creates a server that accepts tokens issued by `auth.yourcompany.com` and provides the OAuth discovery metadata that MCP clients need. The `JWTVerifier` handles token validation using your identity provider's public keys, while the `RemoteAuthProvider` generates the required OAuth endpoints.\n\nThe `authorization_servers` list tells MCP clients which identity providers you trust. The `base_url` identifies your server in OAuth metadata, enabling proper token audience validation. **Important**: The `base_url` should point to your server base URL - for example, if your MCP server is accessible at `https://api.yourcompany.com/mcp`, use `https://api.yourcompany.com` as the base URL.\n\n### Overriding Advertised Scopes\n\nSome identity providers use different scope formats for authorization requests versus token claims. For example, Azure AD requires clients to request full URI scopes like `api://client-id/read`, but the token's `scp` claim contains just `read`. The `scopes_supported` parameter lets you advertise the full-form scopes in metadata while validating against the short form:\n\n```python\nauth = RemoteAuthProvider(\n    token_verifier=token_verifier,\n    authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    base_url=\"https://api.example.com\",\n    scopes_supported=[\"api://my-api/read\", \"api://my-api/write\"],\n)\n```\n\nWhen not set, `scopes_supported` defaults to the token verifier's `required_scopes`. For Azure AD specifically, see the [AzureJWTVerifier](/integrations/azure#token-verification-only-managed-identity) which handles this automatically.\n\n### Custom Endpoints\n\nYou can extend `RemoteAuthProvider` to add additional endpoints beyond the standard OAuth protected resource metadata. These don't have to be OAuth-specific - you can add any endpoints your authentication integration requires.\n\n```python\nimport httpx\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nclass CompanyAuthProvider(RemoteAuthProvider):\n    def __init__(self):\n        token_verifier = JWTVerifier(\n            jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n            issuer=\"https://auth.yourcompany.com\",\n            audience=\"mcp-production-api\"\n        )\n        \n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.yourcompany.com\")],\n            base_url=\"https://api.yourcompany.com\"  # Your server base URL\n        )\n    \n    def get_routes(self) -> list[Route]:\n        \"\"\"Add custom endpoints to the standard protected resource routes.\"\"\"\n        \n        # Get the standard OAuth protected resource routes\n        routes = super().get_routes()\n        \n        # Add authorization server metadata forwarding for client convenience\n        async def authorization_server_metadata(request):\n            async with httpx.AsyncClient() as client:\n                response = await client.get(\n                    \"https://auth.yourcompany.com/.well-known/oauth-authorization-server\"\n                )\n                response.raise_for_status()\n                return JSONResponse(response.json())\n        \n        routes.append(\n            Route(\"/.well-known/oauth-authorization-server\", authorization_server_metadata)\n        )\n        \n        return routes\n\nmcp = FastMCP(name=\"Company API\", auth=CompanyAuthProvider())\n```\n\nThis pattern uses `super().get_routes()` to get the standard protected resource routes, then adds additional endpoints as needed. A common use case is providing authorization server metadata forwarding, which allows MCP clients to discover your identity provider's capabilities through your MCP server rather than contacting the identity provider directly.\n\n## WorkOS AuthKit Integration\n\nWorkOS AuthKit provides an excellent example of remote OAuth integration. The `AuthKitProvider` demonstrates how to implement both token validation and OAuth metadata forwarding in a production-ready package.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\nauth = AuthKitProvider(\n    authkit_domain=\"https://your-project.authkit.app\",\n    base_url=\"https://your-mcp-server.com\"\n)\n\nmcp = FastMCP(name=\"Protected Application\", auth=auth)\n```\n\nThe `AuthKitProvider` automatically configures JWT validation against WorkOS's public keys and provides both protected resource metadata and authorization server metadata forwarding. This implementation handles the complete remote OAuth integration with minimal configuration.\n\nWorkOS's support for Dynamic Client Registration makes it particularly well-suited for MCP applications. Clients can automatically register themselves with your WorkOS project and obtain the credentials needed for authentication without manual intervention.\n\n→ **Complete WorkOS tutorial**: [AuthKit Integration Guide](/integrations/authkit)\n\n## Client Redirect URI Security\n\n<Note>\n`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR:\n\n- `None` (default): All redirect URIs allowed (for DCR compatibility)\n- Custom list: Specify allowed patterns with wildcard support\n- Empty list `[]`: No redirect URIs allowed\n\nThis provides defense-in-depth even though DCR providers typically validate redirect URIs themselves.\n</Note>\n\n## Implementation Considerations\n\nRemote OAuth integration requires careful attention to several technical details that affect reliability and security.\n\n**Token Validation Performance**: Your server validates every incoming token by checking signatures against your identity provider's public keys. Consider implementing key caching and rotation handling to minimize latency while maintaining security.\n\n**Error Handling**: Network issues with your identity provider can affect token validation. Implement appropriate timeouts, retry logic, and graceful degradation to maintain service availability during identity provider outages.\n\n**Audience Validation**: Ensure that tokens intended for your server are not accepted by other applications. Proper audience validation prevents token misuse across different services in your ecosystem.\n\n**Scope Management**: Map token scopes to your application's permission model consistently. Consider how scope changes affect existing tokens and plan for smooth permission updates.\n\nThe complexity of these considerations reinforces why external identity providers are recommended over custom OAuth implementations. Established providers handle these technical details with extensive testing and operational experience."
  },
  {
    "path": "docs/servers/auth/token-verification.mdx",
    "content": "---\ntitle: Token Verification\nsidebarTitle: Token Verification\ndescription: Protect your server by validating bearer tokens issued by external systems.\nicon: key\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nToken verification enables your FastMCP server to validate bearer tokens issued by external systems without participating in user authentication flows. Your server acts as a pure resource server, focusing on token validation and authorization decisions while delegating identity management to other systems in your infrastructure.\n\n<Note>\nToken verification operates somewhat outside the formal MCP authentication flow, which expects OAuth-style discovery. It's best suited for internal systems, microservices architectures, or when you have full control over token generation and distribution.\n</Note>\n\n## Understanding Token Verification\n\nToken verification addresses scenarios where authentication responsibility is distributed across multiple systems. Your MCP server receives structured tokens containing identity and authorization information, validates their authenticity, and makes access control decisions based on their contents.\n\nThis pattern emerges naturally in microservices architectures where a central authentication service issues tokens that multiple downstream services validate independently. It also works well when integrating MCP servers into existing systems that already have established token-based authentication mechanisms.\n\n### The Token Verification Model\n\nToken verification treats your MCP server as a resource server in OAuth terminology. The key insight is that token validation and token issuance are separate concerns that can be handled by different systems.\n\n**Token Issuance**: Another system (API gateway, authentication service, or identity provider) handles user authentication and creates signed tokens containing identity and permission information.\n\n**Token Validation**: Your MCP server receives these tokens, verifies their authenticity using cryptographic signatures, and extracts authorization information from their claims.\n\n**Access Control**: Based on token contents, your server determines what resources, tools, and prompts the client can access.\n\nThis separation allows your MCP server to focus on its core functionality while leveraging existing authentication infrastructure. The token acts as a portable proof of identity that travels with each request.\n\n### Token Security Considerations\n\nToken-based authentication relies on cryptographic signatures to ensure token integrity. Your MCP server validates tokens using public keys corresponding to the private keys used for token creation. This asymmetric approach means your server never needs access to signing secrets.\n\nToken validation must address several security requirements: signature verification ensures tokens haven't been tampered with, expiration checking prevents use of stale tokens, and audience validation ensures tokens intended for your server aren't accepted by other systems.\n\nThe challenge in MCP environments is that clients need to obtain valid tokens before making requests, but the MCP protocol doesn't provide built-in discovery mechanisms for token endpoints. Clients must obtain tokens through separate channels or prior configuration.\n\n\n## TokenVerifier Class\n\nFastMCP provides the `TokenVerifier` class to handle token validation complexity while remaining flexible about token sources and validation strategies.\n\n`TokenVerifier` focuses exclusively on token validation without providing OAuth discovery metadata. This makes it ideal for internal systems where clients already know how to obtain tokens, or for microservices that trust tokens from known issuers.\n\nThe class validates token signatures, checks expiration timestamps, and extracts authorization information from token claims. It supports various token formats and validation strategies while maintaining a consistent interface for authorization decisions.\n\nYou can subclass `TokenVerifier` to implement custom validation logic for specialized token formats or validation requirements. The base class handles common patterns while allowing extension for unique use cases.\n\n## JWT Token Verification\n\nJSON Web Tokens (JWTs) represent the most common token format for modern applications. FastMCP's `JWTVerifier` validates JWTs using industry-standard cryptographic techniques and claim validation.\n\n### JWKS Endpoint Integration\n\nJWKS endpoint integration provides the most flexible approach for production systems. The verifier automatically fetches public keys from a JSON Web Key Set endpoint, enabling automatic key rotation without server configuration changes.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Configure JWT verification against your identity provider\nverifier = JWTVerifier(\n    jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n    issuer=\"https://auth.yourcompany.com\",\n    audience=\"mcp-production-api\"\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThis configuration creates a server that validates JWTs issued by `auth.yourcompany.com`. The verifier periodically fetches public keys from the JWKS endpoint and validates incoming tokens against those keys. Only tokens with the correct issuer and audience claims will be accepted.\n\nThe `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server.\n\n### Symmetric Key Verification (HMAC)\n\nSymmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators.\n\nThis approach is commonly used in microservices architectures where services share a secret key, or when your authentication service and MCP server are both managed by the same organization. The HMAC algorithms (HS256, HS384, HS512) provide strong security when the shared secret is properly managed.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Use a shared secret for symmetric key verification\nverifier = JWTVerifier(\n    public_key=\"your-shared-secret-key-minimum-32-chars\",  # Despite the name, this accepts symmetric secrets\n    issuer=\"internal-auth-service\",\n    audience=\"mcp-internal-api\",\n    algorithm=\"HS256\"  # or HS384, HS512 for stronger security\n)\n\nmcp = FastMCP(name=\"Internal API\", auth=verifier)\n```\n\nThe verifier will validate tokens signed with the same secret using the specified HMAC algorithm. This approach offers several advantages for internal systems:\n\n- **Simplicity**: No key pair management or certificate distribution\n- **Performance**: HMAC operations are typically faster than RSA\n- **Compatibility**: Works well with existing microservice authentication patterns\n\n<Note>\nThe parameter is named `public_key` for backwards compatibility, but when using HMAC algorithms (HS256/384/512), it accepts the symmetric secret string.\n</Note>\n\n<Warning>\n**Security Considerations for Symmetric Keys:**\n- Use a strong, randomly generated secret (minimum 32 characters recommended)\n- Never expose the secret in logs, error messages, or version control\n- Implement secure key distribution and rotation mechanisms\n- Consider using asymmetric keys (RSA/ECDSA) for external-facing APIs\n</Warning>\n\n### Static Public Key Verification\n\nStatic public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Use a static public key for token verification\npublic_key_pem = \"\"\"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----\"\"\"\n\nverifier = JWTVerifier(\n    public_key=public_key_pem,\n    issuer=\"https://auth.yourcompany.com\",\n    audience=\"mcp-production-api\"\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThis configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.\n## Opaque Token Verification\n\nMany authorization servers issue opaque tokens rather than self-contained JWTs. Opaque tokens are random strings that carry no information themselves - the authorization server maintains their state and validation requires querying the server. FastMCP supports opaque token validation through OAuth 2.0 Token Introspection (RFC 7662).\n\n### Understanding Opaque Tokens\n\nOpaque tokens differ fundamentally from JWTs in their verification model. Where JWTs carry signed claims that can be validated locally, opaque tokens require network calls to the issuing authorization server for validation. The authorization server maintains token state and can revoke tokens immediately, providing stronger security guarantees for sensitive operations.\n\nThis approach trades performance (network latency on each validation) for security and flexibility. Authorization servers can revoke opaque tokens instantly, implement complex authorization logic, and maintain detailed audit logs of token usage. Many enterprise OAuth providers default to opaque tokens for these security advantages.\n\n### Token Introspection Protocol\n\nRFC 7662 standardizes how resource servers validate opaque tokens. The protocol defines an introspection endpoint where resource servers authenticate using client credentials and receive token metadata including active status, scopes, expiration, and subject identity.\n\nFastMCP implements this protocol through the `IntrospectionTokenVerifier` class, handling authentication, request formatting, and response parsing according to the specification.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\n\n# Configure introspection with your OAuth provider\nverifier = IntrospectionTokenVerifier(\n    introspection_url=\"https://auth.yourcompany.com/oauth/introspect\",\n    client_id=\"mcp-resource-server\",\n    client_secret=\"your-client-secret\",\n    required_scopes=[\"api:read\", \"api:write\"]\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThe verifier authenticates to the introspection endpoint using client credentials and queries it whenever a bearer token arrives. FastMCP checks whether the token is active and has sufficient scopes before allowing access.\n\nTwo standard client authentication methods are supported, both defined in RFC 6749:\n\n- **`client_secret_basic`** (default): Sends credentials via HTTP Basic Auth header\n- **`client_secret_post`**: Sends credentials in the POST request body\n\nMost OAuth providers support both methods, though some may require one specifically. Configure the authentication method with the `client_auth_method` parameter:\n\n```python\n# Use POST body authentication instead of Basic Auth\nverifier = IntrospectionTokenVerifier(\n    introspection_url=\"https://auth.yourcompany.com/oauth/introspect\",\n    client_id=\"mcp-resource-server\",\n    client_secret=\"your-client-secret\",\n    client_auth_method=\"client_secret_post\",\n    required_scopes=[\"api:read\", \"api:write\"]\n)\n```\n\n## Development and Testing\n\nDevelopment environments often need simpler token management without the complexity of full JWT infrastructure. FastMCP provides tools specifically designed for these scenarios.\n\n### Static Token Verification\n\nStatic token verification enables rapid development by accepting predefined tokens with associated claims. This approach eliminates the need for token generation infrastructure during development and testing.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n# Define development tokens and their associated claims\nverifier = StaticTokenVerifier(\n    tokens={\n        \"dev-alice-token\": {\n            \"client_id\": \"alice@company.com\",\n            \"scopes\": [\"read:data\", \"write:data\", \"admin:users\"]\n        },\n        \"dev-guest-token\": {\n            \"client_id\": \"guest-user\",\n            \"scopes\": [\"read:data\"]\n        }\n    },\n    required_scopes=[\"read:data\"]\n)\n\nmcp = FastMCP(name=\"Development Server\", auth=verifier)\n```\n\nClients can now authenticate using `Authorization: Bearer dev-alice-token` headers. The server will recognize the token and load the associated claims for authorization decisions. This approach enables immediate development without external dependencies.\n\n<Warning>\nStatic token verification stores tokens as plain text and should never be used in production environments. It's designed exclusively for development and testing scenarios.\n</Warning>\n\n\n### Debug/Custom Token Verification\n\n<VersionBadge version=\"2.13.1\" />\n\nThe `DebugTokenVerifier` provides maximum flexibility for testing and special cases where standard token verification isn't applicable. It delegates validation to a user-provided callable, making it useful for prototyping, testing scenarios, or handling opaque tokens without introspection endpoints.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n# Accept all tokens (useful for rapid development)\nverifier = DebugTokenVerifier()\n\nmcp = FastMCP(name=\"Development Server\", auth=verifier)\n```\n\nBy default, `DebugTokenVerifier` accepts any non-empty token as valid. This eliminates authentication barriers during early development, allowing you to focus on core functionality before adding security.\n\nFor more controlled testing, provide custom validation logic:\n\n```python\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n# Synchronous validation - check token prefix\nverifier = DebugTokenVerifier(\n    validate=lambda token: token.startswith(\"dev-\"),\n    client_id=\"development-client\",\n    scopes=[\"read\", \"write\"]\n)\n\nmcp = FastMCP(name=\"Development Server\", auth=verifier)\n```\n\nThe validation callable can also be async, enabling database lookups or external service calls:\n\n```python\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n# Asynchronous validation - check against cache\nasync def validate_token(token: str) -> bool:\n    # Check if token exists in Redis, database, etc.\n    return await redis.exists(f\"valid_tokens:{token}\")\n\nverifier = DebugTokenVerifier(\n    validate=validate_token,\n    client_id=\"api-client\",\n    scopes=[\"api:access\"]\n)\n\nmcp = FastMCP(name=\"Custom API\", auth=verifier)\n```\n\n**Use Cases:**\n\n- **Testing**: Accept any token during integration tests without setting up token infrastructure\n- **Prototyping**: Quickly validate concepts without authentication complexity\n- **Opaque tokens without introspection**: When you have tokens from an IDP that provides no introspection endpoint, and you're willing to accept tokens without validation (validation happens later at the upstream service)\n- **Custom token formats**: Implement validation for non-standard token formats or legacy systems\n\n<Warning>\n`DebugTokenVerifier` bypasses standard security checks. Only use in controlled environments (development, testing) or when you fully understand the security implications. For production, use proper JWT or introspection-based verification.\n</Warning>\n\n### Test Token Generation\n\nTest token generation helps when you need to test JWT verification without setting up complete identity infrastructure. FastMCP includes utilities for generating test key pairs and signed tokens.\n\n```python\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\n# Generate a key pair for testing\nkey_pair = RSAKeyPair.generate()\n\n# Configure your server with the public key\nverifier = JWTVerifier(\n    public_key=key_pair.public_key,\n    issuer=\"https://test.yourcompany.com\",\n    audience=\"test-mcp-server\"\n)\n\n# Generate a test token using the private key\ntest_token = key_pair.create_token(\n    subject=\"test-user-123\",\n    issuer=\"https://test.yourcompany.com\", \n    audience=\"test-mcp-server\",\n    scopes=[\"read\", \"write\", \"admin\"]\n)\n\nprint(f\"Test token: {test_token}\")\n```\n\nThis pattern enables comprehensive testing of JWT validation logic without depending on external token issuers. The generated tokens are cryptographically valid and will pass all standard JWT validation checks.\n\n## HTTP Client Customization\n\n<VersionBadge version=\"2.18.0\" />\n\nAll token verifiers that make HTTP calls accept an optional `http_client` parameter. This lets you provide your own `httpx.AsyncClient` for connection pooling, custom TLS configuration, or proxy settings.\n\n### Connection Pooling\n\nBy default, each token verification call creates a fresh HTTP client. Under high load, this means repeated TCP connections and TLS handshakes. Providing a shared client enables connection pooling across calls:\n\n```python\nimport httpx\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\n\n# Create a shared client with connection pooling\nhttp_client = httpx.AsyncClient(\n    timeout=10,\n    limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),\n)\n\nverifier = IntrospectionTokenVerifier(\n    introspection_url=\"https://auth.yourcompany.com/oauth/introspect\",\n    client_id=\"mcp-resource-server\",\n    client_secret=\"your-client-secret\",\n    http_client=http_client,\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThe same pattern works for `JWTVerifier` when using JWKS endpoints:\n\n```python\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nverifier = JWTVerifier(\n    jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n    issuer=\"https://auth.yourcompany.com\",\n    http_client=http_client,\n)\n```\n\n<Warning>\n`JWTVerifier` does not support `http_client` when `ssrf_safe=True`. SSRF-safe mode requires a hardened transport that validates DNS resolution and connection targets, which cannot be guaranteed with a user-provided client. Attempting to use both will raise a `ValueError`.\n</Warning>\n\n<Note>\nWhen you provide an `http_client`, you are responsible for its lifecycle. The verifier will not close it. Use the server's `lifespan` to manage client cleanup:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\n\nhttp_client = httpx.AsyncClient(timeout=10)\n\nverifier = IntrospectionTokenVerifier(\n    introspection_url=\"https://auth.example.com/introspect\",\n    client_id=\"my-service\",\n    client_secret=\"secret\",\n    http_client=http_client,\n)\n\n@asynccontextmanager\nasync def lifespan(app):\n    yield\n    await http_client.aclose()\n\nmcp = FastMCP(name=\"My API\", auth=verifier, lifespan=lifespan)\n```\n</Note>\n\nThe convenience providers (`GitHubProvider`, `GoogleProvider`, `DiscordProvider`, `WorkOSProvider`, `AzureProvider`) also accept `http_client` and pass it through to their internal token verifier.\n\n## Production Configuration\n\nFor production deployments, load sensitive configuration from environment variables:\n\n```python\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Load configuration from environment variables\n# Parse comma-separated scopes if provided\nscopes_env = os.environ.get(\"JWT_REQUIRED_SCOPES\")\nrequired_scopes = scopes_env.split(\",\") if scopes_env else None\n\nverifier = JWTVerifier(\n    jwks_uri=os.environ.get(\"JWT_JWKS_URI\"),\n    issuer=os.environ.get(\"JWT_ISSUER\"),\n    audience=os.environ.get(\"JWT_AUDIENCE\"),\n    required_scopes=required_scopes,\n)\n\nmcp = FastMCP(name=\"Production API\", auth=verifier)\n```\n\nThis keeps configuration out of your codebase while maintaining explicit setup.\n\nThis approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration.\n\n"
  },
  {
    "path": "docs/servers/authorization.mdx",
    "content": "---\ntitle: Authorization\nsidebarTitle: Authorization\ndescription: Control access to components using callable-based authorization checks that filter visibility and enforce permissions.\nicon: shield-halved\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"3.0.0\" />\n\nAuthorization controls what authenticated users can do with your FastMCP server. While [authentication](/servers/auth/authentication) verifies identity (who you are), authorization determines access (what you can do). FastMCP provides a callable-based authorization system that works at both the component level and globally via middleware.\n\nThe authorization model centers on a simple concept: callable functions that receive context about the current request and return `True` to allow access or `False` to deny it. Multiple checks combine with AND logic, meaning all checks must pass for access to be granted.\n\n<Note>\nAuthorization relies on OAuth tokens which are only available with HTTP transports (SSE, Streamable HTTP). In STDIO mode, there's no OAuth mechanism, so `get_access_token()` returns `None` and all auth checks are skipped.\n</Note>\n\n<Note>\nWhen an `AuthProvider` is configured, all requests to the MCP endpoint must carry a valid token—unauthenticated requests are rejected at the transport level before any auth checks run. Authorization checks therefore differentiate between authenticated users based on their scopes and claims, not between authenticated and unauthenticated users.\n</Note>\n\n## Auth Checks\n\nAn auth check is any callable that accepts an `AuthContext` and returns a boolean. Auth checks can be synchronous or asynchronous, so checks that need to perform async operations (like reading server state or calling external services) work naturally.\n\n```python\nfrom fastmcp.server.auth import AuthContext\n\ndef my_custom_check(ctx: AuthContext) -> bool:\n    # ctx.token is AccessToken | None\n    # ctx.component is the Tool, Resource, or Prompt being accessed\n    return ctx.token is not None and \"special\" in ctx.token.scopes\n```\n\nFastMCP provides two built-in auth checks that cover common authorization patterns.\n\n### require_scopes\n\nScope-based authorization checks that the token contains all specified OAuth scopes. When multiple scopes are provided, all must be present (AND logic).\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import require_scopes\n\nmcp = FastMCP(\"Scoped Server\")\n\n@mcp.tool(auth=require_scopes(\"admin\"))\ndef admin_operation() -> str:\n    \"\"\"Requires the 'admin' scope.\"\"\"\n    return \"Admin action completed\"\n\n@mcp.tool(auth=require_scopes(\"read\", \"write\"))\ndef read_write_operation() -> str:\n    \"\"\"Requires both 'read' AND 'write' scopes.\"\"\"\n    return \"Read/write action completed\"\n```\n\n### restrict_tag\n\nTag-based restrictions apply scope requirements conditionally. If a component has the specified tag, the token must have the required scopes. Components without the tag are unaffected.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import restrict_tag\nfrom fastmcp.server.middleware import AuthMiddleware\n\nmcp = FastMCP(\n    \"Tagged Server\",\n    middleware=[\n        AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))\n    ]\n)\n\n@mcp.tool(tags={\"admin\"})\ndef admin_tool() -> str:\n    \"\"\"Tagged 'admin', so requires 'admin' scope.\"\"\"\n    return \"Admin only\"\n\n@mcp.tool(tags={\"public\"})\ndef public_tool() -> str:\n    \"\"\"Not tagged 'admin', so no scope required by the restriction.\"\"\"\n    return \"Anyone can access\"\n```\n\n### Combining Checks\n\nMultiple auth checks can be combined by passing a list. All checks must pass for authorization to succeed (AND logic).\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import require_scopes\n\nmcp = FastMCP(\"Combined Auth Server\")\n\n@mcp.tool(auth=[require_scopes(\"admin\"), require_scopes(\"write\")])\ndef secure_admin_action() -> str:\n    \"\"\"Requires both 'admin' AND 'write' scopes.\"\"\"\n    return \"Secure admin action\"\n```\n\n### Custom Auth Checks\n\nAny callable that accepts `AuthContext` and returns `bool` can serve as an auth check. This enables authorization logic based on token claims, component metadata, or external systems.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import AuthContext\n\nmcp = FastMCP(\"Custom Auth Server\")\n\ndef require_premium_user(ctx: AuthContext) -> bool:\n    \"\"\"Check for premium user status in token claims.\"\"\"\n    if ctx.token is None:\n        return False\n    return ctx.token.claims.get(\"premium\", False) is True\n\ndef require_access_level(minimum_level: int):\n    \"\"\"Factory function for level-based authorization.\"\"\"\n    def check(ctx: AuthContext) -> bool:\n        if ctx.token is None:\n            return False\n        user_level = ctx.token.claims.get(\"level\", 0)\n        return user_level >= minimum_level\n    return check\n\n@mcp.tool(auth=require_premium_user)\ndef premium_feature() -> str:\n    \"\"\"Only for premium users.\"\"\"\n    return \"Premium content\"\n\n@mcp.tool(auth=require_access_level(5))\ndef advanced_feature() -> str:\n    \"\"\"Requires access level 5 or higher.\"\"\"\n    return \"Advanced feature\"\n```\n\n### Async Auth Checks\n\nAuth checks can be `async` functions, which is useful when the authorization decision depends on asynchronous operations like reading server state or querying external services.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import AuthContext\n\nmcp = FastMCP(\"Async Auth Server\")\n\nasync def check_user_permissions(ctx: AuthContext) -> bool:\n    \"\"\"Async auth check that reads server state.\"\"\"\n    if ctx.token is None:\n        return False\n    user_id = ctx.token.claims.get(\"sub\")\n    # Async operations work naturally in auth checks\n    permissions = await fetch_user_permissions(user_id)\n    return \"admin\" in permissions\n\n@mcp.tool(auth=check_user_permissions)\ndef admin_tool() -> str:\n    return \"Admin action completed\"\n```\n\nSync and async checks can be freely combined in a list — each check is handled according to its type.\n\n### Error Handling\n\nAuth checks can raise exceptions for explicit denial with custom messages:\n\n- **`AuthorizationError`**: Propagates with its custom message, useful for explaining why access was denied\n- **Other exceptions**: Masked for security (logged internally, treated as denial)\n\n```python\nfrom fastmcp.server.auth import AuthContext\nfrom fastmcp.exceptions import AuthorizationError\n\ndef require_verified_email(ctx: AuthContext) -> bool:\n    \"\"\"Require verified email with explicit denial message.\"\"\"\n    if ctx.token is None:\n        raise AuthorizationError(\"Authentication required\")\n    if not ctx.token.claims.get(\"email_verified\"):\n        raise AuthorizationError(\"Email verification required\")\n    return True\n```\n\n## Component-Level Authorization\n\nThe `auth` parameter on decorators controls visibility and access for individual components. When auth checks fail for the current request, the component is hidden from list responses and direct access returns not-found.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import require_scopes\n\nmcp = FastMCP(\"Component Auth Server\")\n\n@mcp.tool(auth=require_scopes(\"write\"))\ndef write_tool() -> str:\n    \"\"\"Only visible to users with 'write' scope.\"\"\"\n    return \"Written\"\n\n@mcp.resource(\"secret://data\", auth=require_scopes(\"read\"))\ndef secret_resource() -> str:\n    \"\"\"Only visible to users with 'read' scope.\"\"\"\n    return \"Secret data\"\n\n@mcp.prompt(auth=require_scopes(\"admin\"))\ndef admin_prompt() -> str:\n    \"\"\"Only visible to users with 'admin' scope.\"\"\"\n    return \"Admin prompt content\"\n```\n\n<Note>\nComponent-level `auth` controls both visibility (list filtering) and access (direct lookups return not-found for unauthorized requests). Additionally use `AuthMiddleware` to apply server-wide authorization rules and get explicit `AuthorizationError` responses on unauthorized execution attempts.\n</Note>\n\n## Server-Level Authorization\n\nFor server-wide authorization enforcement, use `AuthMiddleware`. This middleware applies auth checks globally to all components—filtering list responses and blocking unauthorized execution with explicit `AuthorizationError` responses.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import require_scopes\nfrom fastmcp.server.middleware import AuthMiddleware\n\nmcp = FastMCP(\n    \"Enforced Auth Server\",\n    middleware=[AuthMiddleware(auth=require_scopes(\"api\"))]\n)\n\n@mcp.tool\ndef any_tool() -> str:\n    \"\"\"Requires 'api' scope to see AND call.\"\"\"\n    return \"Protected\"\n```\n\n### Component Auth + Middleware\n\nComponent-level `auth` and `AuthMiddleware` work together as complementary layers. The middleware applies server-wide rules to all components, while component-level auth adds per-component requirements. Both layers are checked—all checks must pass.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import require_scopes, restrict_tag\nfrom fastmcp.server.middleware import AuthMiddleware\n\nmcp = FastMCP(\n    \"Layered Auth Server\",\n    middleware=[\n        AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))\n    ]\n)\n\n# Requires \"write\" scope (component-level)\n# Also requires \"admin\" scope if tagged \"admin\" (middleware-level)\n@mcp.tool(auth=require_scopes(\"write\"), tags={\"admin\"})\ndef admin_write() -> str:\n    \"\"\"Requires both 'write' AND 'admin' scopes.\"\"\"\n    return \"Admin write\"\n\n# Requires \"write\" scope (component-level only)\n@mcp.tool(auth=require_scopes(\"write\"))\ndef user_write() -> str:\n    \"\"\"Requires 'write' scope.\"\"\"\n    return \"User write\"\n```\n\n### Tag-Based Global Authorization\n\nA common pattern uses `restrict_tag` with `AuthMiddleware` to apply scope requirements based on component tags.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import restrict_tag\nfrom fastmcp.server.middleware import AuthMiddleware\n\nmcp = FastMCP(\n    \"Tag-Based Auth Server\",\n    middleware=[\n        AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"])),\n        AuthMiddleware(auth=restrict_tag(\"write\", scopes=[\"write\"])),\n    ]\n)\n\n@mcp.tool(tags={\"admin\"})\ndef delete_all_data() -> str:\n    \"\"\"Requires 'admin' scope.\"\"\"\n    return \"Deleted\"\n\n@mcp.tool(tags={\"write\"})\ndef update_record(id: str, data: str) -> str:\n    \"\"\"Requires 'write' scope.\"\"\"\n    return f\"Updated {id}\"\n\n@mcp.tool\ndef read_record(id: str) -> str:\n    \"\"\"No tag restrictions, accessible to all.\"\"\"\n    return f\"Record {id}\"\n```\n\n## Accessing Tokens in Tools\n\nTools can access the current authentication token using `get_access_token()` from `fastmcp.server.dependencies`. This enables tools to make decisions based on user identity or permissions beyond simple authorization checks.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_access_token\n\nmcp = FastMCP(\"Token Access Server\")\n\n@mcp.tool\ndef personalized_greeting() -> str:\n    \"\"\"Greet the user based on their token claims.\"\"\"\n    token = get_access_token()\n\n    if token is None:\n        return \"Hello, guest!\"\n\n    name = token.claims.get(\"name\", \"user\")\n    return f\"Hello, {name}!\"\n\n@mcp.tool\ndef user_dashboard() -> dict:\n    \"\"\"Return user-specific data based on token.\"\"\"\n    token = get_access_token()\n\n    if token is None:\n        return {\"error\": \"Not authenticated\"}\n\n    return {\n        \"client_id\": token.client_id,\n        \"scopes\": token.scopes,\n        \"claims\": token.claims,\n    }\n```\n\n## Reference\n\n### AccessToken\n\nThe `AccessToken` object contains information extracted from the OAuth token.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `token` | `str` | The raw token string |\n| `client_id` | `str \\| None` | OAuth client identifier |\n| `scopes` | `list[str]` | Granted OAuth scopes |\n| `expires_at` | `datetime \\| None` | Token expiration time |\n| `claims` | `dict[str, Any]` | All JWT claims or custom token data |\n\n### AuthContext\n\nThe `AuthContext` dataclass is passed to all auth check functions.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `token` | `AccessToken \\| None` | Current access token, or `None` if unauthenticated |\n| `component` | `Tool \\| Resource \\| Prompt` | The component being accessed |\n\nAccess to the component object enables authorization decisions based on metadata like tags, name, or custom properties.\n\n```python\nfrom fastmcp.server.auth import AuthContext\n\ndef require_matching_tag(ctx: AuthContext) -> bool:\n    \"\"\"Require a scope matching each of the component's tags.\"\"\"\n    if ctx.token is None:\n        return False\n    user_scopes = set(ctx.token.scopes)\n    return ctx.component.tags.issubset(user_scopes)\n```\n\n### Imports\n\n```python\nfrom fastmcp.server.auth import (\n    AccessToken,       # Token with .token, .client_id, .scopes, .expires_at, .claims\n    AuthContext,       # Context with .token, .component\n    AuthCheck,         # Type alias: sync or async Callable[[AuthContext], bool]\n    require_scopes,    # Built-in: requires specific scopes\n    restrict_tag,      # Built-in: tag-based scope requirements\n    run_auth_checks,   # Utility: run checks with AND logic\n)\n\nfrom fastmcp.server.middleware import AuthMiddleware\n```\n"
  },
  {
    "path": "docs/servers/composition.mdx",
    "content": "---\ntitle: Composing Servers\nsidebarTitle: Composition\ndescription: Combine multiple servers into one\nicon: puzzle-piece\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.2.0\" />\n\nAs your application grows, you'll want to split it into focused servers — one for weather, one for calendar, one for admin — and combine them into a single server that clients connect to. That's what `mount()` does.\n\nWhen you mount a server, all its tools, resources, and prompts become available through the parent. The connection is live: add a tool to the child after mounting, and it's immediately visible through the parent.\n\n```python\nfrom fastmcp import FastMCP\n\nweather = FastMCP(\"Weather\")\n\n@weather.tool\ndef get_forecast(city: str) -> str:\n    \"\"\"Get weather forecast for a city.\"\"\"\n    return f\"Sunny in {city}\"\n\n@weather.resource(\"data://cities\")\ndef list_cities() -> list[str]:\n    \"\"\"List supported cities.\"\"\"\n    return [\"London\", \"Paris\", \"Tokyo\"]\n\nmain = FastMCP(\"MainApp\")\nmain.mount(weather)\n\n# main now serves get_forecast and data://cities\n```\n\n## Mounting External Servers\n\nMount remote HTTP servers or subprocess-based MCP servers using `create_proxy()`:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server import create_proxy\n\nmcp = FastMCP(\"Orchestrator\")\n\n# Mount a remote HTTP server (URLs work directly)\nmcp.mount(create_proxy(\"http://api.example.com/mcp\"), namespace=\"api\")\n\n# Mount local Python scripts (file paths work directly)\nmcp.mount(create_proxy(\"./my_server.py\"), namespace=\"local\")\n```\n\n### Mounting npm/uvx Packages\n\nFor npm packages or Python tools, use the config dict format:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server import create_proxy\n\nmcp = FastMCP(\"Orchestrator\")\n\n# Mount npm package via config\ngithub_config = {\n    \"mcpServers\": {\n        \"default\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@modelcontextprotocol/server-github\"]\n        }\n    }\n}\nmcp.mount(create_proxy(github_config), namespace=\"github\")\n\n# Mount Python tool via config\nsqlite_config = {\n    \"mcpServers\": {\n        \"default\": {\n            \"command\": \"uvx\",\n            \"args\": [\"mcp-server-sqlite\", \"--db\", \"data.db\"]\n        }\n    }\n}\nmcp.mount(create_proxy(sqlite_config), namespace=\"db\")\n```\n\nOr use explicit transport classes:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server import create_proxy\nfrom fastmcp.client.transports import NpxStdioTransport, UvxStdioTransport\n\nmcp = FastMCP(\"Orchestrator\")\n\nmcp.mount(\n    create_proxy(NpxStdioTransport(package=\"@modelcontextprotocol/server-github\")),\n    namespace=\"github\"\n)\nmcp.mount(\n    create_proxy(UvxStdioTransport(tool_name=\"mcp-server-sqlite\", tool_args=[\"--db\", \"data.db\"])),\n    namespace=\"db\"\n)\n```\n\nFor advanced configuration, see [Proxying](/servers/providers/proxy).\n\n## Namespacing\n\n<VersionBadge version=\"3.0.0\" />\n\nWhen mounting multiple servers, use namespaces to avoid naming conflicts:\n\n```python\nweather = FastMCP(\"Weather\")\ncalendar = FastMCP(\"Calendar\")\n\n@weather.tool\ndef get_data() -> str:\n    return \"Weather data\"\n\n@calendar.tool\ndef get_data() -> str:\n    return \"Calendar data\"\n\nmain = FastMCP(\"Main\")\nmain.mount(weather, namespace=\"weather\")\nmain.mount(calendar, namespace=\"calendar\")\n\n# Tools are now:\n# - weather_get_data\n# - calendar_get_data\n```\n\n### How Namespacing Works\n\n| Component Type | Without Namespace | With `namespace=\"api\"` |\n|----------------|-------------------|------------------------|\n| Tool | `my_tool` | `api_my_tool` |\n| Prompt | `my_prompt` | `api_my_prompt` |\n| Resource | `data://info` | `data://api/info` |\n| Template | `data://{id}` | `data://api/{id}` |\n\nNamespacing uses [transforms](/servers/transforms/transforms) under the hood.\n\n## Dynamic Composition\n\nBecause `mount()` creates a live link, you can add components to a child server after mounting and they'll be immediately available through the parent:\n\n```python\nmain = FastMCP(\"Main\")\nmain.mount(dynamic_server, namespace=\"dynamic\")\n\n# Add a tool AFTER mounting - it's accessible through main\n@dynamic_server.tool\ndef added_later() -> str:\n    return \"Added after mounting!\"\n```\n\n## Tag Filtering\n\n<VersionBadge version=\"3.0.0\" />\n\nParent server tag filters apply recursively to mounted servers:\n\n```python\napi_server = FastMCP(\"API\")\n\n@api_server.tool(tags={\"production\"})\ndef prod_endpoint() -> str:\n    return \"Production data\"\n\n@api_server.tool(tags={\"development\"})\ndef dev_endpoint() -> str:\n    return \"Debug data\"\n\n# Mount with production filter\nprod_app = FastMCP(\"Production\")\nprod_app.mount(api_server, namespace=\"api\")\nprod_app.enable(tags={\"production\"}, only=True)\n\n# Only prod_endpoint (namespaced as api_prod_endpoint) is visible\n```\n\n## Performance Considerations\n\nOperations like `list_tools()` on the parent are affected by the performance of all mounted servers. This is particularly noticeable with:\n\n- HTTP-based mounted servers (300-400ms vs 1-2ms for local tools)\n- Mounted servers with slow initialization\n- Deep mounting hierarchies\n\nIf low latency is critical, consider implementing caching strategies or limiting mounting depth.\n\n## Custom Routes\n\n<VersionBadge version=\"2.4.0\" />\n\nCustom HTTP routes defined with `@server.custom_route()` are also forwarded when mounting:\n\n```python\nsubserver = FastMCP(\"Sub\")\n\n@subserver.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check():\n    return {\"status\": \"ok\"}\n\nmain = FastMCP(\"Main\")\nmain.mount(subserver, namespace=\"sub\")\n\n# /health is now accessible through main's HTTP app\n```\n\n## Conflict Resolution\n\n<VersionBadge version=\"3.0.0\" />\n\nWhen mounting multiple servers with the same namespace (or no namespace), the **most recently mounted** server takes precedence for conflicting component names:\n\n```python\nserver_a = FastMCP(\"A\")\nserver_b = FastMCP(\"B\")\n\n@server_a.tool\ndef shared_tool() -> str:\n    return \"From A\"\n\n@server_b.tool\ndef shared_tool() -> str:\n    return \"From B\"\n\nmain = FastMCP(\"Main\")\nmain.mount(server_a)\nmain.mount(server_b)\n\n# shared_tool returns \"From B\" (most recently mounted)\n```\n"
  },
  {
    "path": "docs/servers/context.mdx",
    "content": "---\ntitle: MCP Context\nsidebarTitle: Context\ndescription: Access MCP capabilities like logging, progress, and resources within your MCP objects.\nicon: rectangle-code\ntag: NEW\n---\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nWhen defining FastMCP [tools](/servers/tools), [resources](/servers/resources), resource templates, or [prompts](/servers/prompts), your functions might need to interact with the underlying MCP session or access advanced server capabilities. FastMCP provides the `Context` object for this purpose.\n\n<Note>\nYou access Context through FastMCP's dependency injection system. For other injectable values like HTTP requests, access tokens, and custom dependencies, see [Dependency Injection](/servers/dependency-injection).\n</Note>\n\n## What Is Context?\n\nThe `Context` object provides a clean interface to access MCP features within your functions, including:\n\n- **Logging**: Send debug, info, warning, and error messages back to the client\n- **Progress Reporting**: Update the client on the progress of long-running operations\n- **Resource Access**: List and read data from resources registered with the server\n- **Prompt Access**: List and retrieve prompts registered with the server\n- **LLM Sampling**: Request the client's LLM to generate text based on provided messages\n- **User Elicitation**: Request structured input from users during tool execution\n- **Session State**: Store data that persists across requests within an MCP session\n- **Session Visibility**: [Control which components are visible](/servers/visibility#per-session-visibility) to the current session\n- **Request Information**: Access metadata about the current request\n- **Server Access**: When needed, access the underlying FastMCP server instance\n\n## Accessing the Context\n\n<VersionBadge version=\"2.14\" />\n\nThe preferred way to access context is using the `CurrentContext()` dependency:\n\n```python {1, 6}\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentContext\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(name=\"Context Demo\")\n\n@mcp.tool\nasync def process_file(file_uri: str, ctx: Context = CurrentContext()) -> str:\n    \"\"\"Processes a file, using context for logging and resource access.\"\"\"\n    await ctx.info(f\"Processing {file_uri}\")\n    return \"Processed file\"\n```\n\nThis works with tools, resources, and prompts:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentContext\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(name=\"Context Demo\")\n\n@mcp.resource(\"resource://user-data\")\nasync def get_user_data(ctx: Context = CurrentContext()) -> dict:\n    await ctx.debug(\"Fetching user data\")\n    return {\"user_id\": \"example\"}\n\n@mcp.prompt\nasync def data_analysis_request(dataset: str, ctx: Context = CurrentContext()) -> str:\n    return f\"Please analyze the following dataset: {dataset}\"\n```\n\n**Key Points:**\n\n- Dependency parameters are automatically excluded from the MCP schema—clients never see them.\n- Context methods are async, so your function usually needs to be async as well.\n- **Each MCP request receives a new context object.** Context is scoped to a single request; state or data set in one request will not be available in subsequent requests.\n- Context is only available during a request; attempting to use context methods outside a request will raise errors.\n\n### Legacy Type-Hint Injection\n\nFor backwards compatibility, you can still access context by simply adding a parameter with the `Context` type hint. FastMCP will automatically inject the context instance:\n\n```python {1, 6}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"Context Demo\")\n\n@mcp.tool\nasync def process_file(file_uri: str, ctx: Context) -> str:\n    \"\"\"Processes a file, using context for logging and resource access.\"\"\"\n    # Context is injected automatically based on the type hint\n    return \"Processed file\"\n```\n\nThis approach still works for tools, resources, and prompts. The parameter name doesn't matter—only the `Context` type hint is important. The type hint can also be a union (`Context | None`) or use `Annotated[]`.\n\n### Via `get_context()` Function\n\n<VersionBadge version=\"2.2.11\" />\n\nFor code nested deeper within your function calls where passing context through parameters is inconvenient, use `get_context()` to retrieve the active context from anywhere within a request's execution flow:\n\n```python {2,9}\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_context\n\nmcp = FastMCP(name=\"Dependency Demo\")\n\n# Utility function that needs context but doesn't receive it as a parameter\nasync def process_data(data: list[float]) -> dict:\n    # Get the active context - only works when called within a request\n    ctx = get_context()\n    await ctx.info(f\"Processing {len(data)} data points\")\n\n@mcp.tool\nasync def analyze_dataset(dataset_name: str) -> dict:\n    # Call utility function that uses context internally\n    data = load_data(dataset_name)\n    await process_data(data)\n```\n\n**Important Notes:**\n\n- The `get_context()` function should only be used within the context of a server request. Calling it outside of a request will raise a `RuntimeError`.\n- The `get_context()` function is server-only and should not be used in client code.\n\n## Context Capabilities\n\nFastMCP provides several advanced capabilities through the context object. Each capability has dedicated documentation with comprehensive examples and best practices:\n\n### Logging\n\nSend debug, info, warning, and error messages back to the MCP client for visibility into function execution.\n\n```python\nawait ctx.debug(\"Starting analysis\")\nawait ctx.info(f\"Processing {len(data)} items\") \nawait ctx.warning(\"Deprecated parameter used\")\nawait ctx.error(\"Processing failed\")\n```\n\nSee [Server Logging](/servers/logging) for complete documentation and examples.\n### Client Elicitation\n\n<VersionBadge version=\"2.10.0\" />\n\nRequest structured input from clients during tool execution, enabling interactive workflows and progressive disclosure. This is a new feature in the 6/18/2025 MCP spec.\n\n```python\nresult = await ctx.elicit(\"Enter your name:\", response_type=str)\nif result.action == \"accept\":\n    name = result.data\n```\n\nSee [User Elicitation](/servers/elicitation) for detailed examples and supported response types.\n\n### LLM Sampling\n\n<VersionBadge version=\"2.0.0\" />\n\nRequest the client's LLM to generate text based on provided messages, useful for leveraging AI capabilities within your tools.\n\n```python\nresponse = await ctx.sample(\"Analyze this data\", temperature=0.7)\n```\n\nSee [LLM Sampling](/servers/sampling) for comprehensive usage and advanced techniques.\n\n\n### Progress Reporting\n\nUpdate clients on the progress of long-running operations, enabling progress indicators and better user experience.\n\n```python\nawait ctx.report_progress(progress=50, total=100)  # 50% complete\n```\n\nSee [Progress Reporting](/servers/progress) for detailed patterns and examples.\n\n### Resource Access\n\nList and read data from resources registered with your FastMCP server, allowing access to files, configuration, or dynamic content.\n\n```python\n# List available resources\nresources = await ctx.list_resources()\n\n# Read a specific resource\ncontent_list = await ctx.read_resource(\"resource://config\")\ncontent = content_list[0].content\n```\n\n**Method signatures:**\n- **`ctx.list_resources() -> list[MCPResource]`**: <VersionBadge version=\"2.13.0\" /> Returns list of all available resources\n- **`ctx.read_resource(uri: str | AnyUrl) -> list[ReadResourceContents]`**: Returns a list of resource content parts\n\n### Prompt Access\n\n<VersionBadge version=\"2.13.0\" />\n\nList and retrieve prompts registered with your FastMCP server, allowing tools and middleware to discover and use available prompts programmatically.\n\n```python\n# List available prompts\nprompts = await ctx.list_prompts()\n\n# Get a specific prompt with arguments\nresult = await ctx.get_prompt(\"analyze_data\", {\"dataset\": \"users\"})\nmessages = result.messages\n```\n\n**Method signatures:**\n- **`ctx.list_prompts() -> list[MCPPrompt]`**: Returns list of all available prompts\n- **`ctx.get_prompt(name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult`**: Get a specific prompt with optional arguments\n\n### Session State\n\n<VersionBadge version=\"3.0.0\" />\n\nStore data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"stateful-app\")\n\n@mcp.tool\nasync def increment_counter(ctx: Context) -> int:\n    \"\"\"Increment a counter that persists across tool calls.\"\"\"\n    count = await ctx.get_state(\"counter\") or 0\n    await ctx.set_state(\"counter\", count + 1)\n    return count + 1\n\n@mcp.tool\nasync def get_counter(ctx: Context) -> int:\n    \"\"\"Get the current counter value.\"\"\"\n    return await ctx.get_state(\"counter\") or 0\n```\n\nEach client session has its own isolated state—two different clients calling `increment_counter` will each have their own counter.\n\n**Method signatures:**\n- **`await ctx.set_state(key, value, *, serializable=True)`**: Store a value in session state\n- **`await ctx.get_state(key)`**: Retrieve a value (returns None if not found)\n- **`await ctx.delete_state(key)`**: Remove a value from session state\n\n<Note>\nState methods are async and require `await`. State expires after 1 day to prevent unbounded memory growth.\n</Note>\n\n#### Non-Serializable Values\n\nBy default, state values must be JSON-serializable (dicts, lists, strings, numbers, etc.) so they can be persisted across requests. For non-serializable values like HTTP clients or database connections, pass `serializable=False`:\n\n```python\n@mcp.tool\nasync def my_tool(ctx: Context) -> str:\n    # This object can't be JSON-serialized\n    client = SomeHTTPClient(base_url=\"https://api.example.com\")\n    await ctx.set_state(\"client\", client, serializable=False)\n\n    # Retrieve it later in the same request\n    client = await ctx.get_state(\"client\")\n    return await client.fetch(\"/data\")\n```\n\nValues stored with `serializable=False` only live for the current MCP request (a single tool call, resource read, or prompt render). They will not be available in subsequent requests within the session.\n\n#### Custom Storage Backends\n\nBy default, session state uses an in-memory store suitable for single-server deployments. For distributed or serverless deployments, provide a custom storage backend:\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\n\n# Use Redis for distributed state\nmcp = FastMCP(\"distributed-app\", session_state_store=RedisStore(...))\n```\n\nAny 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.\n\n#### State During Initialization\n\nState 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.\n\n### Session Visibility\n\n<VersionBadge version=\"3.0.0\" />\n\nTools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_visibility()`. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/visibility#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation.\n\n### Change Notifications\n\n<VersionBadge version=\"3.0.0\" />\n\nFastMCP automatically sends list change notifications when components (such as tools, resources, or prompts) are added, removed, enabled, or disabled. In rare cases where you need to manually trigger these notifications, you can use the context's notification methods:\n\n```python\nimport mcp.types\n\n@mcp.tool\nasync def custom_tool_management(ctx: Context) -> str:\n    \"\"\"Example of manual notification after custom tool changes.\"\"\"\n    await ctx.send_notification(mcp.types.ToolListChangedNotification())\n    await ctx.send_notification(mcp.types.ResourceListChangedNotification())\n    await ctx.send_notification(mcp.types.PromptListChangedNotification())\n    return \"Notifications sent\"\n```\n\nThese methods are primarily used internally by FastMCP's automatic notification system and most users will not need to invoke them directly.\n\n### FastMCP Server\n\nTo access the underlying FastMCP server instance, you can use the `ctx.fastmcp` property:\n\n```python\n@mcp.tool\nasync def my_tool(ctx: Context) -> None:\n    # Access the FastMCP server instance\n    server_name = ctx.fastmcp.name\n    ...\n```\n\n### Transport\n\n<VersionBadge version=\"3.0.0\" />\n\nThe `ctx.transport` property indicates which transport is being used to run the server. This is useful when your tool needs to behave differently depending on whether the server is running over STDIO, SSE, or Streamable HTTP. For example, you might want to return shorter responses over STDIO or adjust timeout behavior based on transport characteristics.\n\nThe transport type is set once when the server starts and remains constant for the server's lifetime. It returns `None` when called outside of a server context (for example, in unit tests or when running code outside of an MCP request).\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"example\")\n\n@mcp.tool\ndef connection_info(ctx: Context) -> str:\n    if ctx.transport == \"stdio\":\n        return \"Connected via STDIO\"\n    elif ctx.transport == \"sse\":\n        return \"Connected via SSE\"\n    elif ctx.transport == \"streamable-http\":\n        return \"Connected via Streamable HTTP\"\n    else:\n        return \"Transport unknown\"\n```\n\n**Property signature:** `ctx.transport -> Literal[\"stdio\", \"sse\", \"streamable-http\"] | None`\n\n### MCP Request\n\nAccess metadata about the current request and client.\n\n```python\n@mcp.tool\nasync def request_info(ctx: Context) -> dict:\n    \"\"\"Return information about the current request.\"\"\"\n    return {\n        \"request_id\": ctx.request_id,\n        \"client_id\": ctx.client_id or \"Unknown client\"\n    }\n```\n\n**Available Properties:**\n\n- **`ctx.request_id -> str`**: Get the unique ID for the current MCP request\n- **`ctx.client_id -> str | None`**: Get the ID of the client making the request, if provided during initialization\n- **`ctx.session_id -> str`**: Get the MCP session ID for session-based data sharing. Raises `RuntimeError` if the MCP session is not yet established.\n\n#### Request Context Availability\n\n<VersionBadge version=\"2.13.1\" />\n\nThe `ctx.request_context` property provides access to the underlying MCP request context, but returns `None` when the MCP session has not been established yet. This typically occurs:\n\n- During middleware execution in the `on_request` hook before the MCP handshake completes\n- During the initialization phase of client connections\n\nThe MCP request context is distinct from the HTTP request. For HTTP transports, HTTP request data may be available even when the MCP session is not yet established.\n\nTo safely access the request context in situations where it may not be available:\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom fastmcp.server.dependencies import get_http_request\n\nmcp = FastMCP(name=\"Session Aware Demo\")\n\n@mcp.tool\nasync def session_info(ctx: Context) -> dict:\n    \"\"\"Return session information when available.\"\"\"\n\n    # Check if MCP session is available\n    if ctx.request_context:\n        # MCP session available - can access MCP-specific attributes\n        return {\n            \"session_id\": ctx.session_id,\n            \"request_id\": ctx.request_id,\n            \"has_meta\": ctx.request_context.meta is not None\n        }\n    else:\n        # MCP session not available - use HTTP helpers for request data (if using HTTP transport)\n        request = get_http_request()\n        return {\n            \"message\": \"MCP session not available\",\n            \"user_agent\": request.headers.get(\"user-agent\", \"Unknown\")\n        }\n```\n\nFor HTTP request access that works regardless of MCP session availability (when using HTTP transports), use the [HTTP request helpers](/servers/dependency-injection#http-request) like `get_http_request()` and `get_http_headers()`.\n\n#### Client Metadata\n\n<VersionBadge version=\"2.13.1\" />\n\nClients can send contextual information with their requests using the `meta` parameter. This metadata is accessible through `ctx.request_context.meta` and is available for all MCP operations (tools, resources, prompts).\n\nThe `meta` field is `None` when clients don't provide metadata. When provided, metadata is accessible via attribute access (e.g., `meta.user_id`) rather than dictionary access. The structure of metadata is determined by the client making the request.\n\n```python\n@mcp.tool\ndef send_email(to: str, subject: str, body: str, ctx: Context) -> str:\n    \"\"\"Send an email, logging metadata about the request.\"\"\"\n\n    # Access client-provided metadata\n    meta = ctx.request_context.meta\n\n    if meta:\n        # Meta is accessed as an object with attribute access\n        user_id = meta.user_id if hasattr(meta, 'user_id') else None\n        trace_id = meta.trace_id if hasattr(meta, 'trace_id') else None\n\n        # Use metadata for logging, observability, etc.\n        if trace_id:\n            log_with_trace(f\"Sending email for user {user_id}\", trace_id)\n\n    # Send the email...\n    return f\"Email sent to {to}\"\n```\n\n<Warning>\nThe MCP request is part of the low-level MCP SDK and intended for advanced use cases. Most users will not need to use it directly.\n</Warning>\n\n"
  },
  {
    "path": "docs/servers/dependency-injection.mdx",
    "content": "---\ntitle: Dependency Injection\nsidebarTitle: Dependencies\ndescription: Inject runtime values like HTTP requests, access tokens, and custom dependencies into your MCP components.\nicon: syringe\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\nFastMCP uses dependency injection to provide runtime values to your tools, resources, and prompts. Instead of passing context through every layer of your code, you declare what you need as parameter defaults—FastMCP resolves them automatically when your function runs.\n\nThe dependency injection system is powered by [Docket](https://github.com/chrisguidry/docket) and its dependency system [uncalled-for](https://github.com/chrisguidry/uncalled-for). Core DI features like `Depends()` and `CurrentContext()` work without installing Docket. For background tasks and advanced task-related dependencies, install `fastmcp[tasks]`. For comprehensive coverage of dependency patterns, see the [Docket dependency documentation](https://docket.lol/en/latest/dependency-injection/).\n\n<Note>\nDependency parameters are automatically excluded from the MCP schema—clients never see them as callable parameters. This separation keeps your function signatures clean while giving you access to the runtime context you need.\n</Note>\n\n## How Dependency Injection Works\n\nDependency injection in FastMCP follows a simple pattern: declare a parameter with a recognized type annotation or a dependency default value, and FastMCP injects the resolved value at runtime.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def my_tool(query: str, ctx: Context) -> str:\n    await ctx.info(f\"Processing: {query}\")\n    return f\"Results for: {query}\"\n```\n\nWhen a client calls `my_tool`, they only see `query` as a parameter. The `ctx` parameter is injected automatically because it has a `Context` type annotation—FastMCP recognizes this and provides the active context for the request.\n\nThis works identically for tools, resources, resource templates, and prompts.\n\n### Explicit Dependencies with CurrentContext\n\nFor more explicit code, you can use `CurrentContext()` as a default value instead of relying on the type annotation:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentContext\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def my_tool(query: str, ctx: Context = CurrentContext()) -> str:\n    await ctx.info(f\"Processing: {query}\")\n    return f\"Results for: {query}\"\n```\n\nBoth approaches work identically. The type-annotation approach is more concise; the explicit `CurrentContext()` approach makes the dependency injection visible in the signature.\n\n## Built-in Dependencies\n\n### MCP Context\n\nThe MCP Context provides logging, progress reporting, resource access, and other request-scoped operations. See [MCP Context](/servers/context) for the full API.\n\n**Dependency injection:** Use a `Context` type annotation (FastMCP injects automatically) or `CurrentContext()`:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def process_data(data: str, ctx: Context) -> str:\n    await ctx.info(f\"Processing: {data}\")\n    return \"Done\"\n\n\n# Or explicitly with CurrentContext()\nfrom fastmcp.dependencies import CurrentContext\n\n@mcp.tool\nasync def process_data(data: str, ctx: Context = CurrentContext()) -> str:\n    ...\n```\n\n**Function:** Use `get_context()` in helper functions or middleware:\n\n```python\nfrom fastmcp.server.dependencies import get_context\n\nasync def log_something(message: str):\n    ctx = get_context()\n    await ctx.info(message)\n```\n\n### Server Instance\n\n<VersionBadge version=\"2.14\" />\n\nAccess the FastMCP server instance for introspection or server-level configuration.\n\n**Dependency injection:** Use `CurrentFastMCP()`:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentFastMCP\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def server_info(server: FastMCP = CurrentFastMCP()) -> str:\n    return f\"Server: {server.name}\"\n```\n\n**Function:** Use `get_server()`:\n\n```python\nfrom fastmcp.server.dependencies import get_server\n\ndef get_server_name() -> str:\n    return get_server().name\n```\n\n### HTTP Request\n\n<VersionBadge version=\"2.2.11\" />\n\nAccess the Starlette Request when running over HTTP transports (SSE or Streamable HTTP).\n\n**Dependency injection:** Use `CurrentRequest()`:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentRequest\nfrom starlette.requests import Request\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def client_info(request: Request = CurrentRequest()) -> dict:\n    return {\n        \"user_agent\": request.headers.get(\"user-agent\", \"Unknown\"),\n        \"client_ip\": request.client.host if request.client else \"Unknown\",\n    }\n```\n\n**Function:** Use `get_http_request()`:\n\n```python\nfrom fastmcp.server.dependencies import get_http_request\n\ndef get_client_ip() -> str:\n    request = get_http_request()\n    return request.client.host if request.client else \"Unknown\"\n```\n\n<Note>\nBoth raise `RuntimeError` when called outside an HTTP context (e.g., STDIO transport). Use HTTP Headers if you need graceful fallback.\n</Note>\n\n### HTTP Headers\n\n<VersionBadge version=\"2.2.11\" />\n\nAccess HTTP headers with graceful fallback—returns an empty dictionary when no HTTP request is available, making it safe for code that might run over any transport.\n\n**Dependency injection:** Use `CurrentHeaders()`:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentHeaders\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def get_auth_type(headers: dict = CurrentHeaders()) -> str:\n    auth = headers.get(\"authorization\", \"\")\n    return \"Bearer\" if auth.startswith(\"Bearer \") else \"None\"\n```\n\n**Function:** Use `get_http_headers()`:\n\n```python\nfrom fastmcp.server.dependencies import get_http_headers\n\ndef get_user_agent() -> str:\n    headers = get_http_headers()\n    return headers.get(\"user-agent\", \"Unknown\")\n```\n\nBy default, problematic headers like `host` and `content-length` are excluded. Use `get_http_headers(include_all=True)` to include all headers.\n\n### Access Token\n\n<VersionBadge version=\"2.11.0\" />\n\nAccess the authenticated user's token when your server uses authentication.\n\n**Dependency injection:** Use `CurrentAccessToken()` (raises if not authenticated):\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentAccessToken\nfrom fastmcp.server.auth import AccessToken\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:\n    return token.claims.get(\"sub\", \"unknown\")\n```\n\n**Function:** Use `get_access_token()` (returns `None` if not authenticated):\n\n```python\nfrom fastmcp.server.dependencies import get_access_token\n\n@mcp.tool\nasync def get_user_info() -> dict:\n    token = get_access_token()\n    if token is None:\n        return {\"authenticated\": False}\n    return {\"authenticated\": True, \"user\": token.claims.get(\"sub\")}\n```\n\nThe `AccessToken` object provides:\n\n- **`client_id`**: The OAuth client identifier\n- **`scopes`**: List of granted permission scopes\n- **`expires_at`**: Token expiration timestamp (if available)\n- **`claims`**: Dictionary of all token claims (JWT claims or provider-specific data)\n\n### Token Claims\n\nWhen you need just one specific value from the token—like a user ID or tenant identifier—`TokenClaim()` extracts it directly without needing the full token object.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import TokenClaim\n\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.tool\nasync def add_expense(\n    amount: float,\n    user_id: str = TokenClaim(\"oid\"),  # Azure object ID\n) -> dict:\n    await db.insert({\"user_id\": user_id, \"amount\": amount})\n    return {\"status\": \"created\", \"user_id\": user_id}\n```\n\n`TokenClaim()` raises a `RuntimeError` if the claim doesn't exist, listing available claims to help with debugging.\n\nCommon claims vary by identity provider:\n\n| Provider | User ID Claim | Email Claim | Name Claim |\n|----------|--------------|-------------|------------|\n| Azure/Entra | `oid` | `email` | `name` |\n| GitHub | `sub` | `email` | `name` |\n| Google | `sub` | `email` | `name` |\n| Auth0 | `sub` | `email` | `name` |\n\n### Background Task Dependencies\n\n<VersionBadge version=\"2.3.0\" />\n\nFor background task execution, FastMCP provides dependencies that integrate with [Docket](https://github.com/chrisguidry/docket). These require installing `fastmcp[tasks]`.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentDocket, CurrentWorker, Progress\n\nmcp = FastMCP(\"Task Demo\")\n\n\n@mcp.tool(task=True)\nasync def long_running_task(\n    data: str,\n    docket=CurrentDocket(),\n    worker=CurrentWorker(),\n    progress=Progress(),\n) -> str:\n    await progress.set_total(100)\n\n    for i in range(100):\n        # Process chunk...\n        await progress.increment()\n        await progress.set_message(f\"Processing chunk {i + 1}\")\n\n    return \"Complete\"\n```\n\n- **`CurrentDocket()`**: Access the Docket instance for scheduling additional background work\n- **`CurrentWorker()`**: Access the worker processing tasks (name, concurrency settings)\n- **`Progress()`**: Track task progress with atomic updates\n\n<Note>\nTask dependencies require `pip install 'fastmcp[tasks]'`. They're only available within task-enabled components (`task=True`). For comprehensive task patterns, see the [Docket documentation](https://chrisguidry.github.io/docket/dependencies/).\n</Note>\n\n## Custom Dependencies\n\nBeyond the built-in dependencies, you can create your own to inject configuration, database connections, API clients, or any other values your functions need.\n\n### Using Depends()\n\nThe `Depends()` function wraps any callable and injects its return value. This works with synchronous functions, async functions, and async context managers.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(\"Custom Deps Demo\")\n\n\ndef get_config() -> dict:\n    return {\"api_url\": \"https://api.example.com\", \"timeout\": 30}\n\n\nasync def get_user_id() -> int:\n    # Could fetch from database, external service, etc.\n    return 42\n\n\n@mcp.tool\nasync def fetch_data(\n    query: str,\n    config: dict = Depends(get_config),\n    user_id: int = Depends(get_user_id),\n) -> str:\n    return f\"User {user_id} fetching '{query}' from {config['api_url']}\"\n```\n\n### Caching\n\nDependencies are cached per-request. If multiple parameters use the same dependency, or if nested dependencies share a common dependency, it's resolved once and the same instance is reused.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(\"Caching Demo\")\n\n\ndef get_db_connection():\n    print(\"Connecting to database...\")  # Only printed once per request\n    return {\"connection\": \"active\"}\n\n\ndef get_user_repo(db=Depends(get_db_connection)):\n    return {\"db\": db, \"type\": \"user\"}\n\n\ndef get_order_repo(db=Depends(get_db_connection)):\n    return {\"db\": db, \"type\": \"order\"}\n\n\n@mcp.tool\nasync def process_order(\n    order_id: str,\n    users=Depends(get_user_repo),\n    orders=Depends(get_order_repo),\n) -> str:\n    # Both repos share the same db connection\n    return f\"Processed order {order_id}\"\n```\n\n### Resource Management\n\nFor dependencies that need cleanup—database connections, file handles, HTTP clients—use an async context manager. The cleanup code runs after your function completes, even if an error occurs.\n\n```python\nfrom contextlib import asynccontextmanager\n\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(\"Resource Demo\")\n\n\n@asynccontextmanager\nasync def get_database():\n    db = await connect_to_database()\n    try:\n        yield db\n    finally:\n        await db.close()\n\n\n@mcp.tool\nasync def query_users(sql: str, db=Depends(get_database)) -> list:\n    return await db.execute(sql)\n```\n\n### Nested Dependencies\n\nDependencies can depend on other dependencies. FastMCP resolves them in the correct order and applies caching across the dependency tree.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(\"Nested Demo\")\n\n\ndef get_base_url() -> str:\n    return \"https://api.example.com\"\n\n\ndef get_api_client(base_url: str = Depends(get_base_url)) -> dict:\n    return {\"base_url\": base_url, \"version\": \"v1\"}\n\n\n@mcp.tool\nasync def call_api(endpoint: str, client: dict = Depends(get_api_client)) -> str:\n    return f\"Calling {client['base_url']}/{client['version']}/{endpoint}\"\n```\n\nFor advanced dependency patterns—like `TaskArgument()` for accessing task parameters, or custom `Dependency` subclasses—see the [Docket dependency documentation](https://chrisguidry.github.io/docket/dependencies/).\n"
  },
  {
    "path": "docs/servers/elicitation.mdx",
    "content": "---\ntitle: User Elicitation\nsidebarTitle: Elicitation\ndescription: Request structured input from users during tool execution through the MCP context.\nicon: message-question\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.10.0\" />\n\nUser elicitation allows MCP servers to request structured input from users during tool execution. Instead of requiring all inputs upfront, tools can interactively ask for missing parameters, clarification, or additional context as needed.\n\nElicitation enables tools to pause execution and request specific information from users:\n\n- **Missing parameters**: Ask for required information not provided initially\n- **Clarification requests**: Get user confirmation or choices for ambiguous scenarios\n- **Progressive disclosure**: Collect complex information step-by-step\n- **Dynamic workflows**: Adapt tool behavior based on user responses\n\nFor example, a file management tool might ask \"Which directory should I create?\" or a data analysis tool might request \"What date range should I analyze?\"\n\n## Overview\n\nUse the `ctx.elicit()` method within any tool function to request user input. Specify the message to display and the type of response you expect.\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom dataclasses import dataclass\n\nmcp = FastMCP(\"Elicitation Server\")\n\n@dataclass\nclass UserInfo:\n    name: str\n    age: int\n\n@mcp.tool\nasync def collect_user_info(ctx: Context) -> str:\n    \"\"\"Collect user information through interactive prompts.\"\"\"\n    result = await ctx.elicit(\n        message=\"Please provide your information\",\n        response_type=UserInfo\n    )\n\n    if result.action == \"accept\":\n        user = result.data\n        return f\"Hello {user.name}, you are {user.age} years old\"\n    elif result.action == \"decline\":\n        return \"Information not provided\"\n    else:  # cancel\n        return \"Operation cancelled\"\n```\n\nThe elicitation result contains an `action` field indicating how the user responded:\n\n| Action | Description |\n|--------|-------------|\n| `accept` | User provided valid input—data is available in the `data` field |\n| `decline` | User chose not to provide the requested information |\n| `cancel` | User cancelled the entire operation |\n\nFastMCP also provides typed result classes for pattern matching:\n\n```python\nfrom fastmcp.server.elicitation import (\n    AcceptedElicitation,\n    DeclinedElicitation,\n    CancelledElicitation,\n)\n\n@mcp.tool\nasync def pattern_example(ctx: Context) -> str:\n    result = await ctx.elicit(\"Enter your name:\", response_type=str)\n\n    match result:\n        case AcceptedElicitation(data=name):\n            return f\"Hello {name}!\"\n        case DeclinedElicitation():\n            return \"No name provided\"\n        case CancelledElicitation():\n            return \"Operation cancelled\"\n```\n\n### Multi-Turn Elicitation\n\nTools can make multiple elicitation calls to gather information progressively:\n\n```python\n@mcp.tool\nasync def plan_meeting(ctx: Context) -> str:\n    \"\"\"Plan a meeting by gathering details step by step.\"\"\"\n\n    title_result = await ctx.elicit(\"What's the meeting title?\", response_type=str)\n    if title_result.action != \"accept\":\n        return \"Meeting planning cancelled\"\n\n    duration_result = await ctx.elicit(\"Duration in minutes?\", response_type=int)\n    if duration_result.action != \"accept\":\n        return \"Meeting planning cancelled\"\n\n    priority_result = await ctx.elicit(\n        \"Is this urgent?\",\n        response_type=[\"yes\", \"no\"]\n    )\n    if priority_result.action != \"accept\":\n        return \"Meeting planning cancelled\"\n\n    urgent = priority_result.data == \"yes\"\n    return f\"Meeting '{title_result.data}' for {duration_result.data} minutes (Urgent: {urgent})\"\n```\n\n### Client Requirements\n\nElicitation requires the client to implement an elicitation handler. If a client doesn't support elicitation, calls to `ctx.elicit()` will raise an error indicating that elicitation is not supported.\n\nSee [Client Elicitation](/clients/elicitation) for details on how clients handle these requests.\n\n## Schema and Response Types\n\nThe server must send a schema to the client indicating the type of data it expects in response to the elicitation request. The MCP spec only supports a limited subset of JSON Schema types for elicitation responses—specifically JSON **objects** with **primitive** properties including `string`, `number` (or `integer`), `boolean`, and `enum` fields.\n\nFastMCP makes it easy to request a broader range of types, including scalars (e.g. `str`) or no response at all, by automatically wrapping them in MCP-compatible object schemas.\n\n### Scalar Types\n\nYou can request simple scalar data types for basic input, such as a string, integer, or boolean. When you request a scalar type, FastMCP automatically wraps it in an object schema for MCP spec compatibility. Clients will see a schema requesting a single \"value\" field of the requested type. Once clients respond, the provided object is \"unwrapped\" and the scalar value is returned directly in the `data` field.\n\n<CodeGroup>\n```python title=\"String\"\n@mcp.tool\nasync def get_user_name(ctx: Context) -> str:\n    result = await ctx.elicit(\"What's your name?\", response_type=str)\n\n    if result.action == \"accept\":\n        return f\"Hello, {result.data}!\"\n    return \"No name provided\"\n```\n```python title=\"Integer\"\n@mcp.tool\nasync def pick_a_number(ctx: Context) -> str:\n    result = await ctx.elicit(\"Pick a number!\", response_type=int)\n\n    if result.action == \"accept\":\n        return f\"You picked {result.data}\"\n    return \"No number provided\"\n```\n```python title=\"Boolean\"\n@mcp.tool\nasync def pick_a_boolean(ctx: Context) -> str:\n    result = await ctx.elicit(\"True or false?\", response_type=bool)\n\n    if result.action == \"accept\":\n        return f\"You picked {result.data}\"\n    return \"No boolean provided\"\n```\n</CodeGroup>\n\n### No Response\n\nSometimes, 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.\n\n```python\n@mcp.tool\nasync def approve_action(ctx: Context) -> str:\n    result = await ctx.elicit(\"Approve this action?\", response_type=None)\n\n    if result.action == \"accept\":\n        return do_action()\n    else:\n        raise ValueError(\"Action rejected\")\n```\n\n### Constrained Options\n\nConstrain the user's response to a specific set of values using a `Literal` type, Python enum, or a list of strings as a convenient shortcut.\n\n<CodeGroup>\n```python title=\"List of strings\"\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"What priority level?\",\n        response_type=[\"low\", \"medium\", \"high\"],\n    )\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data}\"\n```\n```python title=\"Literal type\"\nfrom typing import Literal\n\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"What priority level?\",\n        response_type=Literal[\"low\", \"medium\", \"high\"]\n    )\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data}\"\n    return \"No priority set\"\n```\n```python title=\"Python enum\"\nfrom enum import Enum\n\nclass Priority(Enum):\n    LOW = \"low\"\n    MEDIUM = \"medium\"\n    HIGH = \"high\"\n\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    result = await ctx.elicit(\"What priority level?\", response_type=Priority)\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data.value}\"\n    return \"No priority set\"\n```\n</CodeGroup>\n\n### Multi-Select\n\n<VersionBadge version=\"2.14.0\" />\n\nEnable multi-select by wrapping your choices in an additional list level. This allows users to select multiple values from the available options.\n\n<CodeGroup>\n```python title=\"List of strings\"\n@mcp.tool\nasync def select_tags(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"Choose tags\",\n        response_type=[[\"bug\", \"feature\", \"documentation\"]]  # Note: list of a list\n    )\n\n    if result.action == \"accept\":\n        tags = result.data\n        return f\"Selected tags: {', '.join(tags)}\"\n```\n```python title=\"list[Enum] type\"\nfrom enum import Enum\n\nclass Tag(Enum):\n    BUG = \"bug\"\n    FEATURE = \"feature\"\n    DOCS = \"documentation\"\n\n@mcp.tool\nasync def select_tags(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"Choose tags\",\n        response_type=list[Tag]\n    )\n    if result.action == \"accept\":\n        tags = [tag.value for tag in result.data]\n        return f\"Selected: {', '.join(tags)}\"\n```\n</CodeGroup>\n\n### Titled Options\n\n<VersionBadge version=\"2.14.0\" />\n\nFor better UI display, provide human-readable titles for enum options. FastMCP generates SEP-1330 compliant schemas using the `oneOf` pattern with `const` and `title` fields.\n\n```python\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"What priority level?\",\n        response_type={\n            \"low\": {\"title\": \"Low Priority\"},\n            \"medium\": {\"title\": \"Medium Priority\"},\n            \"high\": {\"title\": \"High Priority\"}\n        }\n    )\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data}\"\n```\n\nFor multi-select with titles, wrap the dict in a list:\n\n```python\n@mcp.tool\nasync def select_priorities(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"Choose priorities\",\n        response_type=[{\n            \"low\": {\"title\": \"Low Priority\"},\n            \"medium\": {\"title\": \"Medium Priority\"},\n            \"high\": {\"title\": \"High Priority\"}\n        }]\n    )\n\n    if result.action == \"accept\":\n        return f\"Selected: {', '.join(result.data)}\"\n```\n\n### Structured Responses\n\nRequest structured data with multiple fields by using a dataclass, typed dict, or Pydantic model as the response type. Note that the MCP spec only supports shallow objects with scalar (string, number, boolean) or enum properties.\n\n```python\nfrom dataclasses import dataclass\nfrom typing import Literal\n\n@dataclass\nclass TaskDetails:\n    title: str\n    description: str\n    priority: Literal[\"low\", \"medium\", \"high\"]\n    due_date: str\n\n@mcp.tool\nasync def create_task(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"Please provide task details\",\n        response_type=TaskDetails\n    )\n\n    if result.action == \"accept\":\n        task = result.data\n        return f\"Created task: {task.title} (Priority: {task.priority})\"\n    return \"Task creation cancelled\"\n```\n\n### Default Values\n\n<VersionBadge version=\"2.14.0\" />\n\nProvide default values for elicitation fields using Pydantic's `Field(default=...)`. Clients will pre-populate form fields with these defaults. Fields with default values are automatically marked as optional.\n\n```python\nfrom pydantic import BaseModel, Field\nfrom enum import Enum\n\nclass Priority(Enum):\n    LOW = \"low\"\n    MEDIUM = \"medium\"\n    HIGH = \"high\"\n\nclass TaskDetails(BaseModel):\n    title: str = Field(description=\"Task title\")\n    description: str = Field(default=\"\", description=\"Task description\")\n    priority: Priority = Field(default=Priority.MEDIUM, description=\"Task priority\")\n\n@mcp.tool\nasync def create_task(ctx: Context) -> str:\n    result = await ctx.elicit(\"Please provide task details\", response_type=TaskDetails)\n    if result.action == \"accept\":\n        return f\"Created: {result.data.title}\"\n    return \"Task creation cancelled\"\n```\n\nDefault values are supported for strings, integers, numbers, booleans, and enums.\n"
  },
  {
    "path": "docs/servers/icons.mdx",
    "content": "---\ntitle: Icons\ndescription: Add visual icons to your servers, tools, resources, and prompts\nicon: image\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.13.0\" />\n\nIcons provide visual representations for your MCP servers and components, helping client applications present better user interfaces. When displayed in MCP clients, icons help users quickly identify and navigate your server's capabilities.\n\n## Icon Format\n\nIcons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies a source URL or data URI, and optionally includes MIME type and size information.\n\n```python\nfrom mcp.types import Icon\n\nicon = Icon(\n    src=\"https://example.com/icon.png\",\n    mimeType=\"image/png\",\n    sizes=[\"48x48\"]\n)\n```\n\nThe fields serve different purposes:\n\n- **src**: URL or data URI pointing to the icon image\n- **mimeType** (optional): MIME type of the image (e.g., \"image/png\", \"image/svg+xml\")\n- **sizes** (optional): Array of size descriptors (e.g., [\"48x48\"], [\"any\"])\n\n## Server Icons\n\nAdd icons and a website URL to your server for display in client applications. Multiple icons at different sizes help clients choose the best resolution for their display context.\n\n```python\nfrom fastmcp import FastMCP\nfrom mcp.types import Icon\n\nmcp = FastMCP(\n    name=\"WeatherService\",\n    website_url=\"https://weather.example.com\",\n    icons=[\n        Icon(\n            src=\"https://weather.example.com/icon-48.png\",\n            mimeType=\"image/png\",\n            sizes=[\"48x48\"]\n        ),\n        Icon(\n            src=\"https://weather.example.com/icon-96.png\",\n            mimeType=\"image/png\",\n            sizes=[\"96x96\"]\n        ),\n    ]\n)\n```\n\nServer icons appear in MCP client interfaces to help users identify your server among others they may have installed.\n\n## Component Icons\n\nIcons can be added to individual tools, resources, resource templates, and prompts. This helps users visually distinguish between different component types and purposes.\n\n### Tool Icons\n\n```python\nfrom mcp.types import Icon\n\n@mcp.tool(\n    icons=[Icon(src=\"https://example.com/calculator-icon.png\")]\n)\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\n### Resource Icons\n\n```python\n@mcp.resource(\n    \"config://settings\",\n    icons=[Icon(src=\"https://example.com/config-icon.png\")]\n)\ndef get_settings() -> dict:\n    \"\"\"Retrieve application settings.\"\"\"\n    return {\"theme\": \"dark\", \"language\": \"en\"}\n```\n\n### Resource Template Icons\n\n```python\n@mcp.resource(\n    \"user://{user_id}/profile\",\n    icons=[Icon(src=\"https://example.com/user-icon.png\")]\n)\ndef get_user_profile(user_id: str) -> dict:\n    \"\"\"Get a user's profile.\"\"\"\n    return {\"id\": user_id, \"name\": f\"User {user_id}\"}\n```\n\n### Prompt Icons\n\n```python\n@mcp.prompt(\n    icons=[Icon(src=\"https://example.com/prompt-icon.png\")]\n)\ndef analyze_code(code: str):\n    \"\"\"Create a prompt for code analysis.\"\"\"\n    return f\"Please analyze this code:\\n\\n{code}\"\n```\n\n## Using Data URIs\n\nFor small icons or when you want to embed the icon directly without external dependencies, use data URIs. This approach eliminates the need for hosting and ensures the icon is always available.\n\n```python\nfrom mcp.types import Icon\nfrom fastmcp.utilities.types import Image\n\n# SVG icon as data URI\nsvg_icon = Icon(\n    src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+\",\n    mimeType=\"image/svg+xml\"\n)\n\n@mcp.tool(icons=[svg_icon])\ndef my_tool() -> str:\n    \"\"\"A tool with an embedded SVG icon.\"\"\"\n    return \"result\"\n```\n\n### Generating Data URIs from Files\n\nFastMCP provides the `Image` utility class to convert local image files into data URIs.\n\n```python\nfrom mcp.types import Icon\nfrom fastmcp.utilities.types import Image\n\n# Generate a data URI from a local image file\nimg = Image(path=\"./assets/brand/favicon.png\")\nicon = Icon(src=img.to_data_uri())\n\n@mcp.tool(icons=[icon])\ndef file_icon_tool() -> str:\n    \"\"\"A tool with an icon generated from a local file.\"\"\"\n    return \"result\"\n```\n\nThis approach is useful when you have local image assets and want to embed them directly in your server definition.\n"
  },
  {
    "path": "docs/servers/lifespan.mdx",
    "content": "---\ntitle: Lifespans\nsidebarTitle: Lifespan\ndescription: Server-level setup and teardown with composable lifespans\nicon: heart-pulse\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nLifespans let you run code once when the server starts and clean up when it stops. Unlike per-session handlers, lifespans run exactly once regardless of how many clients connect.\n\n## Basic Usage\n\nUse the `@lifespan` decorator to define a lifespan:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.lifespan import lifespan\n\n@lifespan\nasync def app_lifespan(server):\n    # Setup: runs once when server starts\n    print(\"Starting up...\")\n    try:\n        yield {\"started_at\": \"2024-01-01\"}\n    finally:\n        # Teardown: runs when server stops\n        print(\"Shutting down...\")\n\nmcp = FastMCP(\"MyServer\", lifespan=app_lifespan)\n```\n\nThe dict you yield becomes the **lifespan context**, accessible from tools.\n\n<Note>\nAlways use `try/finally` for cleanup code to ensure it runs even if the server is cancelled.\n</Note>\n\n## Accessing Lifespan Context\n\nAccess the lifespan context in tools via `ctx.lifespan_context`:\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom fastmcp.server.lifespan import lifespan\n\n@lifespan\nasync def app_lifespan(server):\n    # Initialize shared state\n    data = {\"users\": [\"alice\", \"bob\"]}\n    yield {\"data\": data}\n\nmcp = FastMCP(\"MyServer\", lifespan=app_lifespan)\n\n@mcp.tool\ndef list_users(ctx: Context) -> list[str]:\n    data = ctx.lifespan_context[\"data\"]\n    return data[\"users\"]\n```\n\n## Composing Lifespans\n\nCompose multiple lifespans with the `|` operator:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.lifespan import lifespan\n\n@lifespan\nasync def config_lifespan(server):\n    config = {\"debug\": True, \"version\": \"1.0\"}\n    yield {\"config\": config}\n\n@lifespan\nasync def data_lifespan(server):\n    data = {\"items\": []}\n    yield {\"data\": data}\n\n# Compose with |\nmcp = FastMCP(\"MyServer\", lifespan=config_lifespan | data_lifespan)\n```\n\nComposed lifespans:\n- Enter in order (left to right)\n- Exit in reverse order (right to left)\n- Merge their context dicts (later values overwrite earlier on conflict)\n\n## Backwards Compatibility\n\nExisting `@asynccontextmanager` lifespans still work when passed directly to FastMCP:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastmcp import FastMCP\n\n@asynccontextmanager\nasync def legacy_lifespan(server):\n    yield {\"key\": \"value\"}\n\nmcp = FastMCP(\"MyServer\", lifespan=legacy_lifespan)\n```\n\nTo compose an `@asynccontextmanager` function with `@lifespan` functions, wrap it with `ContextManagerLifespan`:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastmcp.server.lifespan import lifespan, ContextManagerLifespan\n\n@asynccontextmanager\nasync def legacy_lifespan(server):\n    yield {\"legacy\": True}\n\n@lifespan\nasync def new_lifespan(server):\n    yield {\"new\": True}\n\n# Wrap the legacy lifespan explicitly for composition\ncombined = ContextManagerLifespan(legacy_lifespan) | new_lifespan\n```\n\n## With FastAPI\n\nWhen mounting FastMCP into FastAPI, use `combine_lifespans` to run both your app's lifespan and the MCP server's lifespan:\n\n```python\nfrom contextlib import asynccontextmanager\n\nfrom fastapi import FastAPI\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.lifespan import combine_lifespans\n\n@asynccontextmanager\nasync def app_lifespan(app):\n    print(\"FastAPI starting...\")\n    yield\n    print(\"FastAPI shutting down...\")\n\nmcp = FastMCP(\"Tools\")\nmcp_app = mcp.http_app()\n\napp = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))\napp.mount(\"/mcp\", mcp_app)\n```\n\nSee the [FastAPI integration guide](/integrations/fastapi#combining-lifespans) for full details.\n"
  },
  {
    "path": "docs/servers/logging.mdx",
    "content": "---\ntitle: Client Logging\nsidebarTitle: Logging\ndescription: Send log messages back to MCP clients through the context.\nicon: receipt\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<Tip>\nThis documentation covers **MCP client logging**—sending messages from your server to MCP clients. For standard server-side logging (e.g., writing to files, console), use `fastmcp.utilities.logging.get_logger()` or Python's built-in `logging` module.\n</Tip>\n\nServer logging allows MCP tools to send debug, info, warning, and error messages back to the client. Unlike standard Python logging, MCP server logging sends messages directly to the client, making them visible in the client's interface or logs.\n\n## Basic Usage\n\nUse the context logging methods within any tool function:\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"LoggingDemo\")\n\n@mcp.tool\nasync def analyze_data(data: list[float], ctx: Context) -> dict:\n    \"\"\"Analyze numerical data with comprehensive logging.\"\"\"\n    await ctx.debug(\"Starting analysis of numerical data\")\n    await ctx.info(f\"Analyzing {len(data)} data points\")\n\n    try:\n        if not data:\n            await ctx.warning(\"Empty data list provided\")\n            return {\"error\": \"Empty data list\"}\n\n        result = sum(data) / len(data)\n        await ctx.info(f\"Analysis complete, average: {result}\")\n        return {\"average\": result, \"count\": len(data)}\n\n    except Exception as e:\n        await ctx.error(f\"Analysis failed: {str(e)}\")\n        raise\n```\n\n## Log Levels\n\n| Level | Use Case |\n|-------|----------|\n| `ctx.debug()` | Detailed execution information for diagnosing problems |\n| `ctx.info()` | General information about normal program execution |\n| `ctx.warning()` | Potentially harmful situations that don't prevent execution |\n| `ctx.error()` | Error events that might still allow the application to continue |\n\n## Structured Logging\n\nAll logging methods accept an `extra` parameter for sending structured data to the client. This is useful for creating rich, queryable logs.\n\n```python\n@mcp.tool\nasync def process_transaction(transaction_id: str, amount: float, ctx: Context):\n    await ctx.info(\n        f\"Processing transaction {transaction_id}\",\n        extra={\n            \"transaction_id\": transaction_id,\n            \"amount\": amount,\n            \"currency\": \"USD\"\n        }\n    )\n```\n\n## Server-Side Logs\n\nMessages sent to clients via `ctx.log()` and its convenience methods are also logged to the server's log at `DEBUG` level. Enable debug logging on the `fastmcp.server.context.to_client` logger to see these messages:\n\n```python\nimport logging\nfrom fastmcp.utilities.logging import get_logger\n\nto_client_logger = get_logger(name=\"fastmcp.server.context.to_client\")\nto_client_logger.setLevel(level=logging.DEBUG)\n```\n\n## Client Handling\n\nLog messages are sent to the client through the MCP protocol. How clients handle these messages depends on their implementation—development clients may display logs in real-time, production clients may store them for analysis, and integration clients may forward them to external logging systems.\n\nSee [Client Logging](/clients/logging) for details on how clients handle server log messages.\n"
  },
  {
    "path": "docs/servers/middleware.mdx",
    "content": "---\ntitle: Middleware\nsidebarTitle: Middleware\ndescription: Add cross-cutting functionality to your MCP server with middleware that intercepts and modifies requests and responses.\nicon: layer-group\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.9.0\" />\n\nMiddleware adds behavior that applies across multiple operations—authentication, logging, rate limiting, or request transformation—without modifying individual tools or resources.\n\n<Tip>\nMCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification.\n</Tip>\n\n## Overview\n\nMCP middleware forms a pipeline around your server's operations. When a request arrives, it flows through each middleware in order—each can inspect, modify, or reject the request before passing it along. After the operation completes, the response flows back through the same middleware in reverse order.\n\n```\nRequest → Middleware A → Middleware B → Handler → Middleware B → Middleware A → Response\n```\n\nThis bidirectional flow means middleware can:\n- **Pre-process**: Validate authentication, log incoming requests, check rate limits\n- **Post-process**: Transform responses, record timing metrics, handle errors consistently\n\nThe key decision point is `call_next(context)`. Calling it continues the chain; not calling it stops processing entirely.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass LoggingMiddleware(Middleware):\n    async def on_message(self, context: MiddlewareContext, call_next):\n        print(f\"→ {context.method}\")\n        result = await call_next(context)\n        print(f\"← {context.method}\")\n        return result\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(LoggingMiddleware())\n```\n\n### Execution Order\n\nMiddleware executes in the order added to the server. The first middleware runs first on the way in and last on the way out:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware\nfrom fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware\nfrom fastmcp.server.middleware.logging import LoggingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(ErrorHandlingMiddleware())   # 1st in, last out\nmcp.add_middleware(RateLimitingMiddleware())    # 2nd in, 2nd out\nmcp.add_middleware(LoggingMiddleware())         # 3rd in, first out\n```\n\nThis ordering matters. Place error handling early so it catches exceptions from all subsequent middleware. Place logging late so it records the actual execution after other middleware has processed the request.\n\n### Server Composition\n\nWhen using [mounted servers](/servers/composition), middleware behavior follows a clear hierarchy:\n\n- **Parent middleware** runs for all requests, including those routed to mounted servers\n- **Mounted server middleware** only runs for requests handled by that specific server\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.logging import LoggingMiddleware\n\nparent = FastMCP(\"Parent\")\nparent.add_middleware(AuthMiddleware())  # Runs for ALL requests\n\nchild = FastMCP(\"Child\")\nchild.add_middleware(LoggingMiddleware())  # Only runs for child's tools\n\nparent.mount(child, namespace=\"child\")\n```\n\nRequests to `child_tool` flow through the parent's `AuthMiddleware` first, then through the child's `LoggingMiddleware`.\n\n## Hooks\n\nRather 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:\n\n| Level | Hooks | Purpose |\n|-------|-------|---------|\n| Message | `on_message` | All MCP traffic (requests and notifications) |\n| Type | `on_request`, `on_notification` | Requests expecting responses vs fire-and-forget |\n| Operation | `on_call_tool`, `on_read_resource`, `on_get_prompt`, etc. | Specific MCP operations |\n\nWhen a client calls a tool, the middleware chain processes `on_message` first, then `on_request`, then `on_call_tool`. This hierarchy lets you target exactly the right scope—use `on_message` for logging everything, `on_request` for authentication, and `on_call_tool` for tool-specific behavior.\n\n### Hook Signature\n\nEvery hook follows the same pattern:\n\n```python\nasync def hook_name(self, context: MiddlewareContext, call_next) -> result_type:\n    # Pre-processing\n    result = await call_next(context)\n    # Post-processing\n    return result\n```\n\n**Parameters:**\n- `context` — `MiddlewareContext` containing request information\n- `call_next` — Async function to continue the middleware chain\n\n**Returns:** The appropriate result type for the hook (varies by operation).\n\n### MiddlewareContext\n\nThe `context` parameter provides access to request details:\n\n| Attribute | Type | Description |\n|-----------|------|-------------|\n| `method` | `str` | MCP method name (e.g., `\"tools/call\"`) |\n| `source` | `str` | Origin: `\"client\"` or `\"server\"` |\n| `type` | `str` | Message type: `\"request\"` or `\"notification\"` |\n| `message` | `object` | The MCP message data |\n| `timestamp` | `datetime` | When the request was received |\n| `fastmcp_context` | `Context` | FastMCP context object (if available) |\n\n### Message Hooks\n\n#### on_message\n\nCalled for every MCP message—both requests and notifications.\n\n```python\nasync def on_message(self, context: MiddlewareContext, call_next):\n    result = await call_next(context)\n    return result\n```\n\nUse for: Logging, metrics, or any cross-cutting concern that applies to all traffic.\n\n#### on_request\n\nCalled for MCP requests that expect a response.\n\n```python\nasync def on_request(self, context: MiddlewareContext, call_next):\n    result = await call_next(context)\n    return result\n```\n\nUse for: Authentication, authorization, request validation.\n\n#### on_notification\n\nCalled for fire-and-forget MCP notifications.\n\n```python\nasync def on_notification(self, context: MiddlewareContext, call_next):\n    await call_next(context)\n    # Notifications don't return values\n```\n\nUse for: Event logging, async side effects.\n\n### Operation Hooks\n\n#### on_call_tool\n\nCalled when a tool is executed. The `context.message` contains `name` (tool name) and `arguments` (dict).\n\n```python\nasync def on_call_tool(self, context: MiddlewareContext, call_next):\n    tool_name = context.message.name\n    args = context.message.arguments\n    result = await call_next(context)\n    return result\n```\n\n**Returns:** Tool execution result or raises `ToolError`.\n\n#### on_read_resource\n\nCalled when a resource is read. The `context.message` contains `uri` (resource URI).\n\n```python\nasync def on_read_resource(self, context: MiddlewareContext, call_next):\n    uri = context.message.uri\n    result = await call_next(context)\n    return result\n```\n\n**Returns:** Resource content.\n\n#### on_get_prompt\n\nCalled when a prompt is retrieved. The `context.message` contains `name` (prompt name) and `arguments` (dict).\n\n```python\nasync def on_get_prompt(self, context: MiddlewareContext, call_next):\n    prompt_name = context.message.name\n    result = await call_next(context)\n    return result\n```\n\n**Returns:** Prompt messages.\n\n#### on_list_tools\n\nCalled when listing available tools. Returns a list of FastMCP `Tool` objects before MCP conversion.\n\n```python\nasync def on_list_tools(self, context: MiddlewareContext, call_next):\n    tools = await call_next(context)\n    # Filter or modify the tool list\n    return tools\n```\n\n**Returns:** `list[Tool]` — Can be filtered before returning to client.\n\n#### on_list_resources\n\nCalled when listing available resources. Returns FastMCP `Resource` objects.\n\n```python\nasync def on_list_resources(self, context: MiddlewareContext, call_next):\n    resources = await call_next(context)\n    return resources\n```\n\n**Returns:** `list[Resource]`\n\n#### on_list_resource_templates\n\nCalled when listing resource templates.\n\n```python\nasync def on_list_resource_templates(self, context: MiddlewareContext, call_next):\n    templates = await call_next(context)\n    return templates\n```\n\n**Returns:** `list[ResourceTemplate]`\n\n#### on_list_prompts\n\nCalled when listing available prompts.\n\n```python\nasync def on_list_prompts(self, context: MiddlewareContext, call_next):\n    prompts = await call_next(context)\n    return prompts\n```\n\n**Returns:** `list[Prompt]`\n\n#### on_initialize\n\n<VersionBadge version=\"2.13.0\" />\n\nCalled when a client connects and initializes the session. This hook cannot modify the initialization response.\n\n```python\nfrom mcp import McpError\nfrom mcp.types import ErrorData\n\nasync def on_initialize(self, context: MiddlewareContext, call_next):\n    client_info = context.message.params.get(\"clientInfo\", {})\n    client_name = client_info.get(\"name\", \"unknown\")\n\n    # Reject before call_next to send error to client\n    if client_name == \"blocked-client\":\n        raise McpError(ErrorData(code=-32000, message=\"Client not supported\"))\n\n    await call_next(context)\n    print(f\"Client {client_name} initialized\")\n```\n\n**Returns:** `None` — The initialization response is handled internally by the MCP protocol.\n\n<Warning>\nRaising `McpError` after `call_next()` will only log the error, not send it to the client. The response has already been sent. Always reject **before** `call_next()`.\n</Warning>\n\n### Raw Handler\n\nFor complete control over all messages, override `__call__` instead of individual hooks:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass RawMiddleware(Middleware):\n    async def __call__(self, context: MiddlewareContext, call_next):\n        print(f\"Processing: {context.method}\")\n        result = await call_next(context)\n        print(f\"Completed: {context.method}\")\n        return result\n```\n\nThis bypasses the hook dispatch system entirely. Use when you need uniform handling regardless of message type.\n\n### Session Availability\n\n<VersionBadge version=\"2.13.1\" />\n\nThe MCP session may not be available during certain phases like initialization. Check before accessing session-specific attributes:\n\n```python\nasync def on_request(self, context: MiddlewareContext, call_next):\n    ctx = context.fastmcp_context\n\n    if ctx.request_context:\n        # MCP session available\n        session_id = ctx.session_id\n        request_id = ctx.request_id\n    else:\n        # Session not yet established (e.g., during initialization)\n        # Use HTTP helpers if needed\n        from fastmcp.server.dependencies import get_http_headers\n        headers = get_http_headers()\n\n    return await call_next(context)\n```\n\nFor HTTP-specific data (headers, client IP) when using HTTP transports, see [HTTP Requests](/servers/context#http-requests).\n\n## Built-in Middleware\n\nFastMCP includes production-ready middleware for common server concerns.\n\n### Logging\n\n```python\nfrom fastmcp.server.middleware.logging import LoggingMiddleware, StructuredLoggingMiddleware\n```\n\n`LoggingMiddleware` provides human-readable request and response logging. `StructuredLoggingMiddleware` outputs JSON-formatted logs for aggregation tools like Datadog or Splunk.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.logging import LoggingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(LoggingMiddleware(\n    include_payloads=True,\n    max_payload_length=1000\n))\n```\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `include_payloads` | `bool` | `False` | Log request/response content |\n| `max_payload_length` | `int` | `500` | Truncate payloads beyond this length |\n| `logger` | `Logger` | module logger | Custom logger instance |\n\n### Timing\n\n```python\nfrom fastmcp.server.middleware.timing import TimingMiddleware, DetailedTimingMiddleware\n```\n\n`TimingMiddleware` logs execution duration for all requests. `DetailedTimingMiddleware` provides per-operation timing with separate tracking for tools, resources, and prompts.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.timing import TimingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(TimingMiddleware())\n```\n\n### Caching\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n```\n\nCaches tool calls, resource reads, and list operations with TTL-based expiration.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(ResponseCachingMiddleware())\n```\n\nEach operation type can be configured independently using settings classes:\n\n```python\nfrom fastmcp.server.middleware.caching import (\n    ResponseCachingMiddleware,\n    CallToolSettings,\n    ListToolsSettings,\n    ReadResourceSettings\n)\n\nmcp.add_middleware(ResponseCachingMiddleware(\n    list_tools_settings=ListToolsSettings(ttl=30),\n    call_tool_settings=CallToolSettings(included_tools=[\"expensive_tool\"]),\n    read_resource_settings=ReadResourceSettings(enabled=False)\n))\n```\n\n| Settings Class | Configures |\n|----------------|------------|\n| `ListToolsSettings` | `on_list_tools` caching |\n| `CallToolSettings` | `on_call_tool` caching |\n| `ListResourcesSettings` | `on_list_resources` caching |\n| `ReadResourceSettings` | `on_read_resource` caching |\n| `ListPromptsSettings` | `on_list_prompts` caching |\n| `GetPromptSettings` | `on_get_prompt` caching |\n\nEach settings class accepts:\n- `enabled` — Enable/disable caching for this operation\n- `ttl` — Time-to-live in seconds\n- `included_*` / `excluded_*` — Whitelist or blacklist specific items\n\nFor persistence or distributed deployments, configure a different storage backend:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.disk import DiskStore\n\nmcp.add_middleware(ResponseCachingMiddleware(\n    cache_storage=DiskStore(directory=\"cache\")\n))\n```\n\nSee [Storage Backends](/servers/storage-backends) for complete options.\n\n<Note>\nCache keys are based on the operation name and arguments only — they do not include user or session identity. If your tools return user-specific data derived from auth context (e.g., headers or session state) rather than from the request arguments, you should either disable caching for those tools or ensure user identity is part of the tool arguments.\n</Note>\n\n### Rate Limiting\n\n```python\nfrom fastmcp.server.middleware.rate_limiting import (\n    RateLimitingMiddleware,\n    SlidingWindowRateLimitingMiddleware\n)\n```\n\n`RateLimitingMiddleware` uses a token bucket algorithm allowing controlled bursts. `SlidingWindowRateLimitingMiddleware` provides precise time-window rate limiting without burst allowance.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(RateLimitingMiddleware(\n    max_requests_per_second=10.0,\n    burst_capacity=20\n))\n```\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `max_requests_per_second` | `float` | `10.0` | Sustained request rate |\n| `burst_capacity` | `int` | `20` | Maximum burst size |\n| `client_id_func` | `Callable` | `None` | Custom client identification |\n\nFor sliding window rate limiting:\n\n```python\nfrom fastmcp.server.middleware.rate_limiting import SlidingWindowRateLimitingMiddleware\n\nmcp.add_middleware(SlidingWindowRateLimitingMiddleware(\n    max_requests=100,\n    window_minutes=1\n))\n```\n\n### Error Handling\n\n```python\nfrom fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware, RetryMiddleware\n```\n\n`ErrorHandlingMiddleware` provides centralized error logging and transformation. `RetryMiddleware` automatically retries with exponential backoff for transient failures.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(ErrorHandlingMiddleware(\n    include_traceback=True,\n    transform_errors=True,\n    error_callback=my_error_callback\n))\n```\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `include_traceback` | `bool` | `False` | Include stack traces in logs |\n| `transform_errors` | `bool` | `False` | Convert exceptions to MCP errors |\n| `error_callback` | `Callable` | `None` | Custom callback on errors |\n\nFor automatic retries:\n\n```python\nfrom fastmcp.server.middleware.error_handling import RetryMiddleware\n\nmcp.add_middleware(RetryMiddleware(\n    max_retries=3,\n    retry_exceptions=(ConnectionError, TimeoutError)\n))\n```\n\n### Ping\n\n<VersionBadge version=\"3.0.0\" />\n\n```python\nfrom fastmcp.server.middleware import PingMiddleware\n```\n\nKeeps long-lived connections alive by sending periodic pings.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware import PingMiddleware\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(PingMiddleware(interval_ms=5000))\n```\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `interval_ms` | `int` | `30000` | Ping interval in milliseconds |\n\nThe ping task starts on the first message and stops automatically when the session ends. Most useful for stateful HTTP connections; has no effect on stateless connections.\n\n### Response Limiting\n\n<VersionBadge version=\"3.0.0\" />\n\n```python\nfrom fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware\n```\n\nLarge tool responses can overwhelm LLM context windows or cause memory issues. You can add response-limiting middleware to enforce size constraints on tool outputs.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware\n\nmcp = FastMCP(\"MyServer\")\n\n# Limit all tool responses to 500KB\nmcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000))\n\n@mcp.tool\ndef search(query: str) -> str:\n    # This could return a very large result\n    return \"x\" * 1_000_000  # 1MB response\n\n# When called, the response will be truncated to ~500KB with:\n# \"...\\n\\n[Response truncated due to size limit]\"\n```\n\nWhen a response exceeds the limit, the middleware extracts all text content, joins it together, truncates to fit within the limit, and returns a single `TextContent` block. For non-text responses, the serialized JSON is used as the text source.\n\n<Note>\nIf a tool defines an `output_schema`, truncated responses will no longer conform to that schema — the client will receive a plain `TextContent` block instead of the expected structured output. Keep this in mind when setting size limits for tools with structured responses.\n</Note>\n\n```python\n# Limit only specific tools\nmcp.add_middleware(ResponseLimitingMiddleware(\n    max_size=100_000,\n    tools=[\"search\", \"fetch_data\"],\n))\n```\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `max_size` | `int` | `1_000_000` | Maximum response size in bytes (1MB default) |\n| `truncation_suffix` | `str` | `\"\\n\\n[Response truncated due to size limit]\"` | Suffix appended to truncated responses |\n| `tools` | `list[str] \\| None` | `None` | Limit only these tools (None = all tools) |\n\n### Combining Middleware\n\nOrder matters. Place middleware that should run first (on the way in) earliest:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware\nfrom fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware\nfrom fastmcp.server.middleware.timing import TimingMiddleware\nfrom fastmcp.server.middleware.logging import LoggingMiddleware\n\nmcp = FastMCP(\"Production Server\")\n\nmcp.add_middleware(ErrorHandlingMiddleware())   # Catch all errors\nmcp.add_middleware(RateLimitingMiddleware(max_requests_per_second=50))\nmcp.add_middleware(TimingMiddleware())\nmcp.add_middleware(LoggingMiddleware())\n\n@mcp.tool\ndef my_tool(data: str) -> str:\n    return f\"Processed: {data}\"\n```\n\n## Custom Middleware\n\nWhen the built-in middleware doesn't fit your needs—custom authentication schemes, domain-specific logging, or request transformation—subclass `Middleware` and override the hooks you need.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass CustomMiddleware(Middleware):\n    async def on_request(self, context: MiddlewareContext, call_next):\n        # Pre-processing\n        print(f\"→ {context.method}\")\n\n        result = await call_next(context)\n\n        # Post-processing\n        print(f\"← {context.method}\")\n        return result\n\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(CustomMiddleware())\n```\n\nOverride only the hooks relevant to your use case. Unoverridden hooks pass through automatically.\n\n### Denying Requests\n\nRaise the appropriate error type to stop processing and return an error to the client.\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.exceptions import ToolError\n\nclass AuthMiddleware(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        tool_name = context.message.name\n\n        if tool_name in [\"delete_all\", \"admin_config\"]:\n            raise ToolError(\"Access denied: requires admin privileges\")\n\n        return await call_next(context)\n```\n\n| Operation | Error Type |\n|-----------|------------|\n| Tool calls | `ToolError` |\n| Resource reads | `ResourceError` |\n| Prompt retrieval | `PromptError` |\n| General requests | `McpError` |\n\nDo not return error values or skip `call_next()` to indicate errors—raise exceptions for proper error propagation.\n\n### Modifying Requests\n\nChange the message before passing it down the chain.\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass InputSanitizer(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        if context.message.name == \"search\":\n            # Normalize search query\n            query = context.message.arguments.get(\"query\", \"\")\n            context.message.arguments[\"query\"] = query.strip().lower()\n\n        return await call_next(context)\n```\n\n### Modifying Responses\n\nTransform results after the handler executes.\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass ResponseEnricher(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        result = await call_next(context)\n\n        if context.message.name == \"get_data\" and result.structured_content:\n            result.structured_content[\"processed_by\"] = \"enricher\"\n\n        return result\n```\n\nFor more complex tool transformations, consider [Transforms](/servers/transforms/transforms) instead.\n\n### Filtering Lists\n\nList operations return FastMCP objects that you can filter before they reach the client. When filtering list results, also block execution in the corresponding operation hook to maintain consistency:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.exceptions import ToolError\n\nclass PrivateToolFilter(Middleware):\n    async def on_list_tools(self, context: MiddlewareContext, call_next):\n        tools = await call_next(context)\n        return [tool for tool in tools if \"private\" not in tool.tags]\n\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        if context.fastmcp_context:\n            tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)\n            if \"private\" in tool.tags:\n                raise ToolError(\"Tool not found\")\n\n        return await call_next(context)\n```\n\n### Accessing Component Metadata\n\nDuring execution hooks, component metadata (like tags) isn't directly available. Look up the component through the server:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.exceptions import ToolError\n\nclass TagBasedAuth(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        if context.fastmcp_context:\n            try:\n                tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)\n\n                if \"requires-auth\" in tool.tags:\n                    # Check authentication here\n                    pass\n\n            except Exception:\n                pass  # Let execution handle missing tools\n\n        return await call_next(context)\n```\n\nThe same pattern works for resources and prompts:\n\n```python\nresource = await context.fastmcp_context.fastmcp.get_resource(context.message.uri)\nprompt = await context.fastmcp_context.fastmcp.get_prompt(context.message.name)\n```\n\n### Storing State\n\n<VersionBadge version=\"2.11.0\" />\n\nMiddleware can store state that tools access later through the FastMCP context.\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass UserMiddleware(Middleware):\n    async def on_request(self, context: MiddlewareContext, call_next):\n        # Extract user from headers (HTTP transport)\n        from fastmcp.server.dependencies import get_http_headers\n        headers = get_http_headers() or {}\n        user_id = headers.get(\"x-user-id\", \"anonymous\")\n\n        # Store for tools to access\n        if context.fastmcp_context:\n            context.fastmcp_context.set_state(\"user_id\", user_id)\n\n        return await call_next(context)\n```\n\nTools retrieve the state:\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef get_user_data(ctx: Context) -> str:\n    user_id = ctx.get_state(\"user_id\")\n    return f\"Data for user: {user_id}\"\n```\n\nSee [Context State Management](/servers/context#state-management) for details.\n\n### Constructor Parameters\n\nInitialize middleware with configuration:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass ConfigurableMiddleware(Middleware):\n    def __init__(self, api_key: str, rate_limit: int = 100):\n        self.api_key = api_key\n        self.rate_limit = rate_limit\n        self.request_counts = {}\n\n    async def on_request(self, context: MiddlewareContext, call_next):\n        # Use self.api_key, self.rate_limit, etc.\n        return await call_next(context)\n\nmcp.add_middleware(ConfigurableMiddleware(\n    api_key=\"secret\",\n    rate_limit=50\n))\n```\n\n### Error Handling in Custom Middleware\n\nWrap `call_next()` to handle errors from downstream middleware and handlers.\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass ErrorLogger(Middleware):\n    async def on_request(self, context: MiddlewareContext, call_next):\n        try:\n            return await call_next(context)\n        except Exception as e:\n            print(f\"Error in {context.method}: {type(e).__name__}: {e}\")\n            raise  # Re-raise to let error propagate\n```\n\nCatching and not re-raising suppresses the error entirely. Usually you want to log and re-raise.\n\n### Complete Example\n\nAuthentication middleware checking API keys for specific tools:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.server.dependencies import get_http_headers\nfrom fastmcp.exceptions import ToolError\n\nclass ApiKeyAuth(Middleware):\n    def __init__(self, valid_keys: set[str], protected_tools: set[str]):\n        self.valid_keys = valid_keys\n        self.protected_tools = protected_tools\n\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        tool_name = context.message.name\n\n        if tool_name not in self.protected_tools:\n            return await call_next(context)\n\n        headers = get_http_headers() or {}\n        api_key = headers.get(\"x-api-key\")\n\n        if api_key not in self.valid_keys:\n            raise ToolError(f\"Invalid API key for protected tool: {tool_name}\")\n\n        return await call_next(context)\n\nmcp = FastMCP(\"Secure Server\")\nmcp.add_middleware(ApiKeyAuth(\n    valid_keys={\"key-1\", \"key-2\"},\n    protected_tools={\"delete_user\", \"admin_panel\"}\n))\n\n@mcp.tool\ndef delete_user(user_id: str) -> str:\n    return f\"Deleted user {user_id}\"\n\n@mcp.tool\ndef get_user(user_id: str) -> str:\n    return f\"User {user_id}\"  # Not protected\n```\n"
  },
  {
    "path": "docs/servers/pagination.mdx",
    "content": "---\ntitle: Pagination\nsidebarTitle: Pagination\ndescription: Control how servers return large lists of components to clients.\nicon: page\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nWhen a server exposes many tools, resources, or prompts, returning them all in a single response can be impractical. MCP supports pagination for list operations, allowing servers to return results in manageable chunks that clients can fetch incrementally.\n\n## Server Configuration\n\nBy default, FastMCP servers return all components in a single response for backward compatibility. To enable pagination, set the `list_page_size` parameter when creating your server. This value determines the maximum number of items returned per page across all list operations.\n\n```python\nfrom fastmcp import FastMCP\n\n# Enable pagination with 50 items per page\nserver = FastMCP(\"ComponentRegistry\", list_page_size=50)\n\n# Register tools (in practice, these might come from a database or config)\n@server.tool\ndef search(query: str) -> str:\n    return f\"Results for: {query}\"\n\n@server.tool\ndef analyze(data: str) -> dict:\n    return {\"status\": \"analyzed\", \"data\": data}\n\n# ... many more tools, resources, prompts\n```\n\nWhen `list_page_size` is configured, the `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` endpoints all paginate their responses. Each response includes a `nextCursor` field when more results exist, which clients use to fetch subsequent pages.\n\n### Cursor Format\n\nCursors are opaque base64-encoded strings per the MCP specification. Clients should treat them as black boxes, passing them unchanged between requests. The cursor encodes the offset into the result set, but this is an implementation detail that may change.\n\n## Client Behavior\n\nThe FastMCP Client handles pagination transparently. Convenience methods like `list_tools()`, `list_resources()`, `list_resource_templates()`, and `list_prompts()` automatically fetch all pages and return the complete list. Existing code continues to work without modification.\n\n```python\nfrom fastmcp import Client\n\nasync with Client(server) as client:\n    # Returns all 200 tools, fetching pages automatically\n    tools = await client.list_tools()\n    print(f\"Total tools: {len(tools)}\")  # 200\n```\n\n### Manual Pagination\n\nFor scenarios where you want to process results incrementally (memory-constrained environments, progress reporting, or early termination), use the `_mcp` variants with explicit cursor handling.\n\n```python\nfrom fastmcp import Client\n\nasync with Client(server) as client:\n    # Fetch first page\n    result = await client.list_tools_mcp()\n    print(f\"Page 1: {len(result.tools)} tools\")\n\n    # Continue fetching while more pages exist\n    while result.nextCursor:\n        result = await client.list_tools_mcp(cursor=result.nextCursor)\n        print(f\"Next page: {len(result.tools)} tools\")\n```\n\nThe `_mcp` methods return the raw MCP protocol objects, which include both the items and the `nextCursor` for the next page. When `nextCursor` is `None`, you've reached the end of the result set.\n\nAll four list operations support manual pagination:\n\n| Operation | Convenience Method | Manual Method |\n|-----------|-------------------|---------------|\n| Tools | `list_tools()` | `list_tools_mcp(cursor=...)` |\n| Resources | `list_resources()` | `list_resources_mcp(cursor=...)` |\n| Resource Templates | `list_resource_templates()` | `list_resource_templates_mcp(cursor=...)` |\n| Prompts | `list_prompts()` | `list_prompts_mcp(cursor=...)` |\n\n## When to Use Pagination\n\nPagination becomes valuable when your server exposes a large number of components. Consider enabling it when:\n\n- Your server dynamically generates many components (e.g., from a database or file system)\n- Memory usage is a concern for clients\n- You want to reduce initial response latency\n\nFor servers with a fixed, modest number of components (fewer than 100), pagination adds complexity without meaningful benefit. The default behavior of returning everything in one response is simpler and efficient for typical use cases.\n"
  },
  {
    "path": "docs/servers/progress.mdx",
    "content": "---\ntitle: Progress Reporting\nsidebarTitle: Progress\ndescription: Update clients on the progress of long-running operations through the MCP context.\nicon: chart-line\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nProgress reporting allows MCP tools to notify clients about the progress of long-running operations. Clients can display progress indicators and provide better user experience during time-consuming tasks.\n\n## Basic Usage\n\nUse `ctx.report_progress()` to send progress updates to the client. The method accepts a `progress` value representing how much work is complete, and an optional `total` representing the full scope of work.\n\n```python\nfrom fastmcp import FastMCP, Context\nimport asyncio\n\nmcp = FastMCP(\"ProgressDemo\")\n\n@mcp.tool\nasync def process_items(items: list[str], ctx: Context) -> dict:\n    \"\"\"Process a list of items with progress updates.\"\"\"\n    total = len(items)\n    results = []\n\n    for i, item in enumerate(items):\n        await ctx.report_progress(progress=i, total=total)\n        await asyncio.sleep(0.1)\n        results.append(item.upper())\n\n    await ctx.report_progress(progress=total, total=total)\n    return {\"processed\": len(results), \"results\": results}\n```\n\n## Progress Patterns\n\n| Pattern | Description | Example |\n|---------|-------------|---------|\n| Percentage | Progress as 0-100 percentage | `progress=75, total=100` |\n| Absolute | Completed items of a known count | `progress=3, total=10` |\n| Indeterminate | Progress without known endpoint | `progress=files_found` (no total) |\n\nFor multi-stage operations, map each stage to a portion of the total progress range. A four-stage operation might allocate 0-25% to validation, 25-60% to export, 60-80% to transform, and 80-100% to import.\n\n## Client Requirements\n\nProgress reporting requires clients to support progress handling. Clients must send a `progressToken` in the initial request to receive progress updates. If no progress token is provided, progress calls have no effect (they don't error).\n\nSee [Client Progress](/clients/progress) for details on implementing client-side progress handling.\n"
  },
  {
    "path": "docs/servers/prompts.mdx",
    "content": "---\ntitle: Prompts\nsidebarTitle: Prompts\ndescription: Create reusable, parameterized prompt templates for MCP clients.\nicon: message-lines\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nPrompts are reusable message templates that help LLMs generate structured, purposeful responses. FastMCP simplifies defining these templates, primarily using the `@mcp.prompt` decorator.\n\n## What Are Prompts?\n\nPrompts provide parameterized message templates for LLMs. When a client requests a prompt:\n\n1.  FastMCP finds the corresponding prompt definition.\n2.  If it has parameters, they are validated against your function signature.\n3.  Your function executes with the validated inputs.\n4.  The generated message(s) are returned to the LLM to guide its response.\n\nThis allows you to define consistent, reusable templates that LLMs can use across different clients and contexts.\n\n## Prompts\n\n### The `@prompt` Decorator\n\nThe most common way to define a prompt is by decorating a Python function. The decorator uses the function name as the prompt's identifier.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.prompts import Message\n\nmcp = FastMCP(name=\"PromptServer\")\n\n# Basic prompt returning a string (converted to user message automatically)\n@mcp.prompt\ndef ask_about_topic(topic: str) -> str:\n    \"\"\"Generates a user message asking for an explanation of a topic.\"\"\"\n    return f\"Can you please explain the concept of '{topic}'?\"\n\n# Prompt returning multiple messages\n@mcp.prompt\ndef generate_code_request(language: str, task_description: str) -> list[Message]:\n    \"\"\"Generates a conversation for code generation.\"\"\"\n    return [\n        Message(f\"Write a {language} function that performs the following task: {task_description}\"),\n        Message(\"I'll help you write that function.\", role=\"assistant\"),\n    ]\n```\n\n**Key Concepts:**\n\n*   **Name:** By default, the prompt name is taken from the function name.\n*   **Parameters:** The function parameters define the inputs needed to generate the prompt.\n*   **Inferred Metadata:** By default:\n    *   Prompt Name: Taken from the function name (`ask_about_topic`).\n    *   Prompt Description: Taken from the function's docstring.\n<Tip>\nFunctions 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.\n</Tip>\n\n#### Decorator Arguments\n\nWhile FastMCP infers the name and description from your function, you can override these and add additional metadata using arguments to the `@mcp.prompt` decorator:\n\n```python\n@mcp.prompt(\n    name=\"analyze_data_request\",          # Custom prompt name\n    description=\"Creates a request to analyze data with specific parameters\",  # Custom description\n    tags={\"analysis\", \"data\"},            # Optional categorization tags\n    meta={\"version\": \"1.1\", \"author\": \"data-team\"}  # Custom metadata\n)\ndef data_analysis_prompt(\n    data_uri: str = Field(description=\"The URI of the resource containing the data.\"),\n    analysis_type: str = Field(default=\"summary\", description=\"Type of analysis.\")\n) -> str:\n    \"\"\"This docstring is ignored when description is provided.\"\"\"\n    return f\"Please perform a '{analysis_type}' analysis on the data found at {data_uri}.\"\n```\n\n<Card icon=\"code\" title=\"@prompt Decorator Arguments\">\n<ParamField body=\"name\" type=\"str | None\">\n  Sets the explicit prompt name exposed via MCP. If not provided, uses the function name\n</ParamField>\n\n<ParamField body=\"title\" type=\"str | None\">\n  A human-readable title for the prompt\n</ParamField>\n\n<ParamField body=\"description\" type=\"str | None\">\n  Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose\n</ParamField>\n\n<ParamField body=\"tags\" type=\"set[str] | None\">\n  A set of strings used to categorize the prompt. These can be used by the server and, in some cases, by clients to filter or group available prompts.\n</ParamField>\n\n<ParamField body=\"enabled\" type=\"bool\" default=\"True\">\n  <Warning>Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.</Warning>\n  A boolean to enable or disable the prompt. See [Component Visibility](#component-visibility) for the recommended approach.\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  Optional list of icon representations for this prompt. See [Icons](/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.11.0\" />\n\n  Optional meta information about the prompt. This data is passed through to the MCP client as the `meta` field of the client-side prompt object and can be used for custom metadata, versioning, or other application-specific purposes.\n</ParamField>\n\n<ParamField body=\"version\" type=\"str | int | None\">\n  <VersionBadge version=\"3.0.0\" />\n\n  Optional version identifier for this prompt. See [Versioning](/servers/versioning) for details.\n</ParamField>\n</Card>\n\n#### Using with Methods\n\nFor decorating instance or class methods, use the standalone `@prompt` decorator and register the bound method. See [Tools: Using with Methods](/servers/tools#using-with-methods) for the pattern.\n\n### Argument Types\n\n<VersionBadge version=\"2.9.0\" />\n\nThe MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP:\n\n1. **Automatically converts** string arguments from MCP clients to the expected types\n2. **Generates helpful descriptions** showing the exact JSON string format needed\n3. **Preserves direct usage** - you can still call prompts with properly typed arguments\n\nSince the MCP specification only allows string arguments, clients need to know what string format to use for complex types. FastMCP solves this by automatically enhancing the argument descriptions with JSON schema information, making it clear to both humans and LLMs how to format their arguments.\n\n<CodeGroup>\n\n```python Python Code\n@mcp.prompt\ndef analyze_data(\n    numbers: list[int],\n    metadata: dict[str, str], \n    threshold: float\n) -> str:\n    \"\"\"Analyze numerical data.\"\"\"\n    avg = sum(numbers) / len(numbers)\n    return f\"Average: {avg}, above threshold: {avg > threshold}\"\n```\n\n```json Resulting MCP Prompt\n{\n  \"name\": \"analyze_data\",\n  \"description\": \"Analyze numerical data.\",\n  \"arguments\": [\n    {\n      \"name\": \"numbers\",\n      \"description\": \"Provide as a JSON string matching the following schema: {\\\"items\\\":{\\\"type\\\":\\\"integer\\\"},\\\"type\\\":\\\"array\\\"}\",\n      \"required\": true\n    },\n    {\n      \"name\": \"metadata\", \n      \"description\": \"Provide as a JSON string matching the following schema: {\\\"additionalProperties\\\":{\\\"type\\\":\\\"string\\\"},\\\"type\\\":\\\"object\\\"}\",\n      \"required\": true\n    },\n    {\n      \"name\": \"threshold\",\n      \"description\": \"Provide as a JSON string matching the following schema: {\\\"type\\\":\\\"number\\\"}\",\n      \"required\": true\n    }\n  ]\n}\n```\n\n</CodeGroup>\n\n**MCP clients will call this prompt with string arguments:**\n```json\n{\n  \"numbers\": \"[1, 2, 3, 4, 5]\",\n  \"metadata\": \"{\\\"source\\\": \\\"api\\\", \\\"version\\\": \\\"1.0\\\"}\",\n  \"threshold\": \"2.5\"\n}\n```\n\n**But you can still call it directly with proper types:**\n```python\n# This also works for direct calls\nresult = await prompt.render({\n    \"numbers\": [1, 2, 3, 4, 5],\n    \"metadata\": {\"source\": \"api\", \"version\": \"1.0\"}, \n    \"threshold\": 2.5\n})\n```\n\n<Warning>\nKeep your type annotations simple when using this feature. Complex nested types or custom classes may not convert reliably from JSON strings. The automatically generated schema descriptions are the only guidance users receive about the expected format.\n\nGood choices: `list[int]`, `dict[str, str]`, `float`, `bool`\nAvoid: Complex Pydantic models, deeply nested structures, custom classes\n</Warning>\n\n### Return Values\n\nPrompt functions must return one of these types:\n\n-   **`str`**: Sent as a single user message.\n-   **`list[Message | str]`**: A sequence of messages (a conversation). Strings are auto-converted to user Messages.\n-   **`PromptResult`**: Full control over messages, description, and metadata. See [PromptResult](#promptresult) below.\n\n```python\nfrom fastmcp.prompts import Message\n\n@mcp.prompt\ndef roleplay_scenario(character: str, situation: str) -> list[Message]:\n    \"\"\"Sets up a roleplaying scenario with initial messages.\"\"\"\n    return [\n        Message(f\"Let's roleplay. You are {character}. The situation is: {situation}\"),\n        Message(\"Okay, I understand. I am ready. What happens next?\", role=\"assistant\")\n    ]\n```\n\n#### Message\n\n<VersionBadge version=\"3.0.0\" />\n\n`Message` provides a user-friendly wrapper for prompt messages with automatic serialization.\n\n```python\nfrom fastmcp.prompts import Message\n\n# String content (user role by default)\nMessage(\"Hello, world!\")\n\n# Explicit role\nMessage(\"I can help with that.\", role=\"assistant\")\n\n# Auto-serialized to JSON text\nMessage({\"key\": \"value\"})\nMessage([\"item1\", \"item2\"])\n```\n\n`Message` accepts two fields:\n\n**`content`** - The message content. Strings pass through directly. Other types (dict, list, BaseModel) are automatically JSON-serialized to text.\n\n**`role`** - The message role, either `\"user\"` (default) or `\"assistant\"`.\n\n<Card title=\"Message\">\n<ParamField body=\"content\" type=\"Any\" required>\n  The content data. Strings pass through directly. Other types (dict, list, BaseModel) are automatically JSON-serialized.\n</ParamField>\n<ParamField body=\"role\" type=\"Literal['user', 'assistant']\" default=\"user\">\n  The message role.\n</ParamField>\n</Card>\n\n#### PromptResult\n\n<VersionBadge version=\"3.0.0\" />\n\n`PromptResult` gives you explicit control over prompt responses: multiple messages, roles, and metadata at both the message and result level.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.prompts import PromptResult, Message\n\nmcp = FastMCP(name=\"PromptServer\")\n\n@mcp.prompt\ndef code_review(code: str) -> PromptResult:\n    \"\"\"Returns a code review prompt with metadata.\"\"\"\n    return PromptResult(\n        messages=[\n            Message(f\"Please review this code:\\n\\n```\\n{code}\\n```\"),\n            Message(\"I'll analyze this code for issues.\", role=\"assistant\"),\n        ],\n        description=\"Code review prompt\",\n        meta={\"review_type\": \"security\", \"priority\": \"high\"}\n    )\n```\n\nFor simple cases, you can pass a string directly to `PromptResult`:\n\n```python\nreturn PromptResult(\"Please help me with this task\")  # auto-converts to single Message\n```\n\n<Card title=\"PromptResult\">\n<ParamField body=\"messages\" type=\"str | list[Message]\" required>\n  Messages to return. Strings are wrapped as a single user Message.\n</ParamField>\n<ParamField body=\"description\" type=\"str | None\">\n  Optional description of the prompt result. If not provided, defaults to the prompt's docstring.\n</ParamField>\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  Result-level metadata, included in the MCP response's `_meta` field. Use this for runtime metadata like categorization, priority, or other client-specific data.\n</ParamField>\n</Card>\n\n<Note>\nThe `meta` field in `PromptResult` is for runtime metadata specific to this render response. This is separate from the `meta` parameter in `@mcp.prompt(meta={...})`, which provides static metadata about the prompt definition itself (returned when listing prompts).\n</Note>\n\nYou can still return plain `str` or `list[Message | str]` from your prompt functions—`PromptResult` is opt-in for when you need to include metadata.\n\n### Required vs. Optional Parameters\n\nParameters in your function signature are considered **required** unless they have a default value.\n\n```python\n@mcp.prompt\ndef data_analysis_prompt(\n    data_uri: str,                        # Required - no default value\n    analysis_type: str = \"summary\",       # Optional - has default value\n    include_charts: bool = False          # Optional - has default value\n) -> str:\n    \"\"\"Creates a request to analyze data with specific parameters.\"\"\"\n    prompt = f\"Please perform a '{analysis_type}' analysis on the data found at {data_uri}.\"\n    if include_charts:\n        prompt += \" Include relevant charts and visualizations.\"\n    return prompt\n```\n\nIn this example, the client *must* provide `data_uri`. If `analysis_type` or `include_charts` are omitted, their default values will be used.\n\n### Component Visibility\n\n<VersionBadge version=\"3.0.0\" />\n\nYou can control which prompts are enabled for clients using server-level enabled control. Disabled prompts don't appear in `list_prompts` and can't be called.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.prompt(tags={\"public\"})\ndef public_prompt(topic: str) -> str:\n    return f\"Discuss: {topic}\"\n\n@mcp.prompt(tags={\"internal\"})\ndef internal_prompt() -> str:\n    return \"Internal system prompt\"\n\n# Disable specific prompts by key\nmcp.disable(keys={\"prompt:internal_prompt\"})\n\n# Disable prompts by tag\nmcp.disable(tags={\"internal\"})\n\n# Or use allowlist mode - only enable prompts with specific tags\nmcp.enable(tags={\"public\"}, only=True)\n```\n\nSee [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control.\n\n### Async Prompts\n\nFastMCP supports both standard (`def`) and asynchronous (`async def`) functions as prompts. Synchronous functions automatically run in a threadpool to avoid blocking the event loop.\n\n```python\n# Synchronous prompt (runs in threadpool)\n@mcp.prompt\ndef simple_question(question: str) -> str:\n    \"\"\"Generates a simple question to ask the LLM.\"\"\"\n    return f\"Question: {question}\"\n\n# Asynchronous prompt\n@mcp.prompt\nasync def data_based_prompt(data_id: str) -> str:\n    \"\"\"Generates a prompt based on data that needs to be fetched.\"\"\"\n    # In a real scenario, you might fetch data from a database or API\n    async with aiohttp.ClientSession() as session:\n        async with session.get(f\"https://api.example.com/data/{data_id}\") as response:\n            data = await response.json()\n            return f\"Analyze this data: {data['content']}\"\n```\n\nUse `async def` when your prompt function performs I/O operations like network requests or database queries, since async is more efficient than threadpool dispatch.\n\n### Accessing MCP Context\n\n<VersionBadge version=\"2.2.5\" />\n\nPrompts can access additional MCP information and features through the `Context` object. To access it, add a parameter to your prompt function with a type annotation of `Context`:\n\n```python {6}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"PromptServer\")\n\n@mcp.prompt\nasync def generate_report_request(report_type: str, ctx: Context) -> str:\n    \"\"\"Generates a request for a report.\"\"\"\n    return f\"Please create a {report_type} report. Request ID: {ctx.request_id}\"\n```\n\nFor full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).\n\n### Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends `notifications/prompts/list_changed` notifications to connected clients when prompts are added, enabled, or disabled. This allows clients to stay up-to-date with the current prompt set without manually polling for changes.\n\n```python\n@mcp.prompt\ndef example_prompt() -> str:\n    return \"Hello!\"\n\n# These operations trigger notifications:\nmcp.add_prompt(example_prompt)               # Sends prompts/list_changed notification\nmcp.disable(keys={\"prompt:example_prompt\"})  # Sends prompts/list_changed notification\nmcp.enable(keys={\"prompt:example_prompt\"})   # Sends prompts/list_changed notification\n```\n\nNotifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.\n\nClients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their prompt lists or update their interfaces.\n\n## Server Behavior\n\n### Duplicate Prompts\n\n<VersionBadge version=\"2.1.0\" />\n\nYou can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\n    name=\"PromptServer\",\n    on_duplicate_prompts=\"error\"  # Raise an error if a prompt name is duplicated\n)\n\n@mcp.prompt\ndef greeting(): return \"Hello, how can I help you today?\"\n\n# This registration attempt will raise a ValueError because\n# \"greeting\" is already registered and the behavior is \"error\".\n# @mcp.prompt\n# def greeting(): return \"Hi there! What can I do for you?\"\n```\n\nThe duplicate behavior options are:\n\n-   `\"warn\"` (default): Logs a warning, and the new prompt replaces the old one.\n-   `\"error\"`: Raises a `ValueError`, preventing the duplicate registration.\n-   `\"replace\"`: Silently replaces the existing prompt with the new one.\n-   `\"ignore\"`: Keeps the original prompt and ignores the new registration attempt.\n\n## Versioning\n\n<VersionBadge version=\"3.0.0\" />\n\nPrompts support versioning, allowing you to maintain multiple implementations under the same name while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.\n"
  },
  {
    "path": "docs/servers/providers/custom.mdx",
    "content": "---\ntitle: Custom Providers\nsidebarTitle: Custom\ndescription: Build providers that source components from any data source\nicon: code\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nCustom providers let you source components from anywhere - databases, APIs, configuration systems, or dynamic runtime logic. If you can write Python code to fetch or generate a component, you can wrap it in a provider.\n\n## When to Build Custom\n\nThe built-in providers handle common cases: decorators (`LocalProvider`), composition (`FastMCPProvider`), and proxying (`ProxyProvider`). Build a custom provider when your components come from somewhere else:\n\n- **Database-backed tools**: Admin users define tools in a database, and your server exposes them dynamically\n- **API-backed resources**: Resources that fetch content from external services on demand\n- **Configuration-driven components**: Components loaded from YAML/JSON config files at startup\n- **Multi-tenant systems**: Different users see different tools based on their permissions\n- **Plugin systems**: Third-party code registers components at runtime\n\n## Providers vs Middleware\n\nBoth providers and [middleware](/servers/middleware) can influence what components a client sees, but they work at different levels.\n\n**Providers** are objects that source components. They make it easy to reason about where tools, resources, and prompts come from - a database, another server, an API.\n\n**Middleware** intercepts individual requests. It's well-suited for request-specific decisions like logging, rate limiting, or authentication.\n\nYou *could* use middleware to dynamically add tools based on request context. But it's often cleaner to have a provider source all possible tools, then use middleware or [visibility controls](/servers/visibility) to filter what each request can see. This separation makes it easier to reason about how components are sourced and how they interact with other server machinery.\n\n## The Provider Interface\n\nA provider implements protected `_list_*` methods that return available components. The public `list_*` methods handle transforms automatically - you override the underscore-prefixed versions:\n\n```python\nfrom collections.abc import Sequence\nfrom fastmcp.server.providers import Provider\nfrom fastmcp.tools import Tool\nfrom fastmcp.resources import Resource\nfrom fastmcp.prompts import Prompt\n\nclass MyProvider(Provider):\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"Return all tools this provider offers.\"\"\"\n        return []\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"Return all resources this provider offers.\"\"\"\n        return []\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"Return all prompts this provider offers.\"\"\"\n        return []\n```\n\nYou only need to implement the methods for component types you provide. The base class returns empty sequences by default.\n\nThe `_get_*` methods (`_get_tool`, `_get_resource`, `_get_prompt`) have default implementations that search through the list results. Override them only if you can fetch individual components more efficiently than iterating the full list.\n\n## What Providers Return\n\nProviders return component objects that are ready to use. When a client calls a tool, FastMCP invokes the tool's function - your provider isn't involved in execution. This means the `Tool`, `Resource`, or `Prompt` you return must actually work.\n\nThe easiest way to create components is from functions:\n\n```python\nfrom fastmcp.tools import Tool\n\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n\ntool = Tool.from_function(add)\n```\n\nThe function's type hints become the input schema, and the docstring becomes the description. You can override these:\n\n```python\ntool = Tool.from_function(\n    add,\n    name=\"calculator_add\",\n    description=\"Add two integers together\"\n)\n```\n\nSimilar `from_function` methods exist for `Resource` and `Prompt`.\n\n## Registering Providers\n\nAdd providers when creating the server:\n\n```python\nmcp = FastMCP(\n    \"MyServer\",\n    providers=[\n        DatabaseProvider(db_url),\n        ConfigProvider(config_path),\n    ]\n)\n```\n\nOr add them after creation:\n\n```python\nmcp = FastMCP(\"MyServer\")\nmcp.add_provider(DatabaseProvider(db_url))\n```\n\n## A Simple Provider\n\nHere's a minimal provider that serves tools from a dictionary:\n\n```python\nfrom collections.abc import Callable, Sequence\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import Provider\nfrom fastmcp.tools import Tool\n\nclass DictProvider(Provider):\n    def __init__(self, tools: dict[str, Callable]):\n        super().__init__()\n        self._tools = [\n            Tool.from_function(fn, name=name)\n            for name, fn in tools.items()\n        ]\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        return self._tools\n```\n\nUse it like this:\n\n```python\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n\ndef multiply(a: int, b: int) -> int:\n    \"\"\"Multiply two numbers.\"\"\"\n    return a * b\n\nmcp = FastMCP(\"Calculator\", providers=[\n    DictProvider({\"add\": add, \"multiply\": multiply})\n])\n```\n\n## Lifecycle Management\n\nProviders often need to set up connections when the server starts and clean them up when it stops. Override the `lifespan` method:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom collections.abc import AsyncIterator, Sequence\n\nclass DatabaseProvider(Provider):\n    def __init__(self, db_url: str):\n        super().__init__()\n        self.db_url = db_url\n        self.db = None\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        self.db = await connect_database(self.db_url)\n        try:\n            yield\n        finally:\n            await self.db.close()\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        rows = await self.db.fetch(\"SELECT * FROM tools\")\n        return [self._make_tool(row) for row in rows]\n```\n\nFastMCP calls your provider's `lifespan` during server startup and shutdown. The connection is available to your methods while the server runs.\n\n## Full Example: API-Backed Resources\n\nHere's a complete provider that fetches resources from an external REST API:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom collections.abc import AsyncIterator, Sequence\nfrom fastmcp.server.providers import Provider\nfrom fastmcp.resources import Resource\nimport httpx\n\nclass ApiResourceProvider(Provider):\n    \"\"\"Provides resources backed by an external API.\"\"\"\n\n    def __init__(self, base_url: str, api_key: str):\n        super().__init__()\n        self.base_url = base_url\n        self.api_key = api_key\n        self.client = None\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        self.client = httpx.AsyncClient(\n            base_url=self.base_url,\n            headers={\"Authorization\": f\"Bearer {self.api_key}\"}\n        )\n        try:\n            yield\n        finally:\n            await self.client.aclose()\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        response = await self.client.get(\"/resources\")\n        response.raise_for_status()\n        return [\n            self._make_resource(item)\n            for item in response.json()[\"items\"]\n        ]\n\n    def _make_resource(self, data: dict) -> Resource:\n        resource_id = data[\"id\"]\n\n        async def read_content() -> str:\n            response = await self.client.get(\n                f\"/resources/{resource_id}/content\"\n            )\n            return response.text\n\n        return Resource.from_function(\n            read_content,\n            uri=f\"api://resources/{resource_id}\",\n            name=data[\"name\"],\n            description=data.get(\"description\", \"\"),\n            mime_type=data.get(\"mime_type\", \"text/plain\")\n        )\n```\n\nRegister it like any other provider:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"API Resources\", providers=[\n    ApiResourceProvider(\"https://api.example.com\", \"my-api-key\")\n])\n```\n"
  },
  {
    "path": "docs/servers/providers/filesystem.mdx",
    "content": "---\ntitle: Filesystem Provider\nsidebarTitle: Filesystem\ndescription: Automatic component discovery from Python files\nicon: folder-tree\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\n`FileSystemProvider` scans a directory for Python files and automatically registers functions decorated with `@tool`, `@resource`, or `@prompt`. This enables a file-based organization pattern similar to Next.js routing, where your project structure becomes your component registry.\n\n## Why Filesystem Discovery\n\nTraditional FastMCP servers require coordination between files. Either your tool files import the server to call `@server.tool()`, or your server file imports all the tool modules. Both approaches create coupling that some developers prefer to avoid.\n\n`FileSystemProvider` eliminates this coordination. Each file is self-contained—it uses standalone decorators (`@tool`, `@resource`, `@prompt`) that don't require access to a server instance. The provider discovers these files at startup, so you can add new tools without modifying your server file.\n\nThis is a convention some teams prefer, not necessarily better for all projects. The tradeoffs:\n\n- **No coordination**: Files don't import the server; server doesn't import files\n- **Predictable naming**: Function names become component names (unless overridden)\n- **Development mode**: Optionally re-scan files on every request for rapid iteration\n\n## Quick Start\n\nCreate a provider pointing to your components directory, then pass it to your server. Use `Path(__file__).parent` to make the path relative to your server file.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import FileSystemProvider\n\nmcp = FastMCP(\"MyServer\", providers=[FileSystemProvider(Path(__file__).parent / \"mcp\")])\n```\n\nIn your `mcp/` directory, create Python files with decorated functions.\n\n```python\n# mcp/tools/greet.py\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    \"\"\"Greet someone by name.\"\"\"\n    return f\"Hello, {name}!\"\n```\n\nWhen the server starts, `FileSystemProvider` scans the directory, imports all Python files, and registers any decorated functions it finds.\n\n## Decorators\n\nFastMCP provides standalone decorators that mark functions for discovery: `@tool` from `fastmcp.tools`, `@resource` from `fastmcp.resources`, and `@prompt` from `fastmcp.prompts`. These support the full syntax of server-bound decorators—all the same parameters work identically.\n\n### @tool\n\nMark a function as a tool. The function name becomes the tool name by default.\n\n```python\nfrom fastmcp.tools import tool\n\n@tool\ndef calculate_sum(a: float, b: float) -> float:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\nCustomize the tool with optional parameters.\n\n```python\nfrom fastmcp.tools import tool\n\n@tool(\n    name=\"add-numbers\",\n    description=\"Add two numbers together.\",\n    tags={\"math\", \"arithmetic\"},\n)\ndef add(a: float, b: float) -> float:\n    return a + b\n```\n\nThe decorator supports all standard tool options: `name`, `title`, `description`, `icons`, `tags`, `output_schema`, `annotations`, and `meta`.\n\n### @resource\n\nMark a function as a resource. Unlike `@tool`, the `@resource` decorator requires a URI argument.\n\n```python\nfrom fastmcp.resources import resource\n\n@resource(\"config://app\")\ndef get_app_config() -> str:\n    \"\"\"Get application configuration.\"\"\"\n    return '{\"version\": \"1.0\"}'\n```\n\nURIs with template parameters create resource templates. The provider automatically detects whether to register a static resource or a template based on whether the URI contains `{parameters}` or the function has arguments.\n\n```python\nfrom fastmcp.resources import resource\n\n@resource(\"users://{user_id}/profile\")\ndef get_user_profile(user_id: str) -> str:\n    \"\"\"Get a user's profile by ID.\"\"\"\n    return f'{{\"id\": \"{user_id}\", \"name\": \"User\"}}'\n```\n\nThe decorator supports: `uri` (required), `name`, `title`, `description`, `icons`, `mime_type`, `tags`, `annotations`, and `meta`.\n\n### @prompt\n\nMark a function as a prompt template.\n\n```python\nfrom fastmcp.prompts import prompt\n\n@prompt\ndef code_review(code: str, language: str = \"python\") -> str:\n    \"\"\"Generate a code review prompt.\"\"\"\n    return f\"Please review this {language} code:\\n\\n```{language}\\n{code}\\n```\"\n```\n\n```python\nfrom fastmcp.prompts import prompt\n\n@prompt(name=\"explain-concept\", tags={\"education\"})\ndef explain(topic: str) -> str:\n    \"\"\"Generate an explanation prompt.\"\"\"\n    return f\"Explain {topic} using clear examples and analogies.\"\n```\n\nThe decorator supports: `name`, `title`, `description`, `icons`, `tags`, and `meta`.\n\n## Directory Structure\n\nThe 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.\n\n```\nmcp/\n├── tools/\n│   ├── greeting.py      # @tool functions\n│   └── calculator.py    # @tool functions\n├── resources/\n│   └── config.py        # @resource functions\n└── prompts/\n    └── assistant.py     # @prompt functions\n```\n\nYou can also put all components in a single file or organize by feature rather than type.\n\n```\nmcp/\n├── user_management.py   # @tool, @resource, @prompt for users\n├── billing.py           # @tool, @resource for billing\n└── analytics.py         # @tool for analytics\n```\n\n## Discovery Rules\n\nThe provider follows these rules when scanning:\n\n| Rule | Behavior |\n|------|----------|\n| File extensions | Only `.py` files are scanned |\n| `__init__.py` | Skipped (used for package structure, not components) |\n| `__pycache__` | Skipped |\n| Private functions | Functions starting with `_` are ignored, even if decorated |\n| No decorators | Files without `@tool`, `@resource`, or `@prompt` are silently skipped |\n| Multiple components | A single file can contain any number of decorated functions |\n\n### Package Imports\n\nIf 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.\n\n```python\n# mcp/__init__.py exists\n\n# mcp/tools/greeting.py\nfrom ..helpers import format_name  # Relative imports work\n\n@tool\ndef greet(name: str) -> str:\n    return f\"Hello, {format_name(name)}!\"\n```\n\nWithout `__init__.py`, files are imported directly using `importlib.util.spec_from_file_location`.\n\n## Reload Mode\n\nDuring development, you may want changes to component files to take effect without restarting the server. Enable reload mode to re-scan the directory on every request.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp.server.providers import FileSystemProvider\n\nprovider = FileSystemProvider(Path(__file__).parent / \"mcp\", reload=True)\n```\n\nWith `reload=True`, the provider:\n\n1. Re-discovers all Python files on each request\n2. Re-imports modules that have changed\n3. Updates the component registry with any new, modified, or removed components\n\n<Warning>\nReload mode adds overhead to every request. Use it only during development, not in production.\n</Warning>\n\n## Error Handling\n\nWhen a file fails to import (syntax error, missing dependency, etc.), the provider logs a warning and continues scanning other files. Failed imports don't prevent the server from starting.\n\n```\nWARNING - Failed to import /path/to/broken.py: No module named 'missing_dep'\n```\n\nThe provider tracks which files have failed and only re-logs warnings when the file's modification time changes. This prevents log spam when a broken file is repeatedly scanned in reload mode.\n\n## Example Project\n\nA complete example is available in the repository at `examples/filesystem-provider/`. The structure demonstrates the recommended organization.\n\n```\nexamples/filesystem-provider/\n├── server.py                    # Server entry point\n└── mcp/\n    ├── tools/\n    │   ├── greeting.py          # greet, farewell tools\n    │   └── calculator.py        # add, multiply tools\n    ├── resources/\n    │   └── config.py            # Static and templated resources\n    └── prompts/\n        └── assistant.py         # code_review, explain prompts\n```\n\nThe server entry point is minimal.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import FileSystemProvider\n\nprovider = FileSystemProvider(\n    root=Path(__file__).parent / \"mcp\",\n    reload=True,\n)\n\nmcp = FastMCP(\"FilesystemDemo\", providers=[provider])\n```\n\nRun with `fastmcp run examples/filesystem-provider/server.py` or inspect with `fastmcp inspect examples/filesystem-provider/server.py`.\n"
  },
  {
    "path": "docs/servers/providers/local.mdx",
    "content": "---\ntitle: Local Provider\nsidebarTitle: Local\ndescription: The default provider for decorator-registered components\nicon: house\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\n`LocalProvider` stores components that you define directly on your server. When you use `@mcp.tool`, `@mcp.resource`, or `@mcp.prompt`, you're adding components to your server's `LocalProvider`.\n\n## How It Works\n\nEvery FastMCP server has a `LocalProvider` as its first provider. Components registered via decorators or direct methods are stored here:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n# These are stored in the server's `LocalProvider`\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet someone by name.\"\"\"\n    return f\"Hello, {name}!\"\n\n@mcp.resource(\"data://config\")\ndef get_config() -> str:\n    \"\"\"Return configuration data.\"\"\"\n    return '{\"version\": \"1.0\"}'\n\n@mcp.prompt\ndef analyze(topic: str) -> str:\n    \"\"\"Create an analysis prompt.\"\"\"\n    return f\"Please analyze: {topic}\"\n```\n\nThe `LocalProvider` is always queried first when clients request components, ensuring that your directly-defined components take precedence over those from mounted or proxied servers.\n\n## Component Registration\n\n### Using Decorators\n\nThe most common way to register components:\n\n```python\n@mcp.tool\ndef my_tool(x: int) -> str:\n    return str(x)\n\n@mcp.resource(\"data://info\")\ndef my_resource() -> str:\n    return \"info\"\n\n@mcp.prompt\ndef my_prompt(topic: str) -> str:\n    return f\"Discuss: {topic}\"\n```\n\n### Using Direct Methods\n\nYou can also add pre-built component objects:\n\n```python\nfrom fastmcp.tools import Tool\n\n# Create a tool object\nmy_tool = Tool.from_function(some_function, name=\"custom_tool\")\n\n# Add it to the server\nmcp.add_tool(my_tool)\nmcp.add_resource(my_resource)\nmcp.add_prompt(my_prompt)\n```\n\n### Removing Components\n\nRemove components by name or URI:\n\n```python\nmcp.local_provider.remove_tool(\"my_tool\")\nmcp.local_provider.remove_resource(\"data://info\")\nmcp.local_provider.remove_prompt(\"my_prompt\")\n```\n\n## Duplicate Handling\n\nWhen you try to add a component that already exists, the behavior depends on the `on_duplicate` setting:\n\n| Mode | Behavior |\n|------|----------|\n| `\"error\"` (default) | Raise `ValueError` |\n| `\"warn\"` | Log warning and replace |\n| `\"replace\"` | Silently replace |\n| `\"ignore\"` | Keep existing component |\n\nConfigure this when creating the server:\n\n```python\nmcp = FastMCP(\"MyServer\", on_duplicate=\"warn\")\n```\n\n## Component Visibility\n\n<VersionBadge version=\"3.0.0\" />\n\nComponents can be dynamically enabled or disabled at runtime. Disabled components don't appear in listings and can't be called.\n\n```python\n@mcp.tool(tags={\"admin\"})\ndef delete_all() -> str:\n    \"\"\"Delete everything.\"\"\"\n    return \"Deleted\"\n\n@mcp.tool\ndef get_status() -> str:\n    \"\"\"Get system status.\"\"\"\n    return \"OK\"\n\n# Disable admin tools\nmcp.disable(tags={\"admin\"})\n\n# Or only enable specific tools\nmcp.enable(keys={\"tool:get_status\"}, only=True)\n```\n\nSee [Visibility](/servers/visibility) for the full documentation on keys, tags, allowlist mode, and provider-level control.\n\n## Standalone LocalProvider\n\nYou can create a LocalProvider independently and attach it to multiple servers:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import LocalProvider\n\n# Create a reusable provider\nshared_tools = LocalProvider()\n\n@shared_tools.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n@shared_tools.resource(\"data://version\")\ndef get_version() -> str:\n    return \"1.0.0\"\n\n# Attach to multiple servers\nserver1 = FastMCP(\"Server1\", providers=[shared_tools])\nserver2 = FastMCP(\"Server2\", providers=[shared_tools])\n```\n\nThis is useful for:\n- Sharing components across servers\n- Testing components in isolation\n- Building reusable component libraries\n\nStandalone providers also support visibility control with `enable()` and `disable()`. See [Visibility](/servers/visibility) for details.\n"
  },
  {
    "path": "docs/servers/providers/overview.mdx",
    "content": "---\ntitle: Providers\nsidebarTitle: Overview\ndescription: How FastMCP sources tools, resources, and prompts\nicon: layer-group\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nEvery FastMCP server has one or more component providers. A provider is a source of tools, resources, and prompts - it's what makes components available to clients.\n\n## What Is a Provider?\n\nWhen a client connects to your server and asks \"what tools do you have?\", FastMCP asks each provider that question and combines the results. When a client calls a specific tool, FastMCP finds which provider has it and delegates the call.\n\nYou're already using providers. When you write `@mcp.tool`, you're adding a tool to your server's `LocalProvider` - the default provider that stores components you define directly in code. You just don't have to think about it for simple servers.\n\nProviders become important when your components come from multiple sources: another FastMCP server to include, a remote MCP server to proxy, or a database where tools are defined dynamically. Each source gets its own provider, and FastMCP queries them all seamlessly.\n\n## Why Providers?\n\nThe provider abstraction solves a common problem: as servers grow, you need to organize components across multiple sources without tangling everything together.\n\n**Composition**: Break a large server into focused modules. A \"weather\" server and a \"calendar\" server can each be developed independently, then mounted into a main server. Each mounted server becomes a `FastMCPProvider`.\n\n**Proxying**: Expose a remote MCP server through your local server. Maybe you're bridging transports (remote HTTP to local stdio) or aggregating multiple backends. Remote connections become `ProxyProvider` instances.\n\n**Dynamic sources**: Load tools from a database, generate them from an OpenAPI spec, or create them based on user permissions. Custom providers let components come from anywhere.\n\n## Built-in Providers\n\nFastMCP includes providers for common patterns:\n\n| Provider | What it does | How you use it |\n|----------|--------------|----------------|\n| `LocalProvider` | Stores components you define in code | `@mcp.tool`, `mcp.add_tool()` |\n| `FastMCPProvider` | Wraps another FastMCP server | `mcp.mount(server)` |\n| `ProxyProvider` | Connects to remote MCP servers | `create_proxy(client)` |\n\nMost users only interact with `LocalProvider` (through decorators) and occasionally mount or proxy other servers. The provider abstraction stays invisible until you need it.\n\n## Transforms\n\n[Transforms](/servers/transforms/transforms) modify components as they flow from providers to clients. Each transform sits in a chain, intercepting queries and modifying results before passing them along.\n\n| Transform | Purpose |\n|-----------|---------|\n| `Namespace` | Prefixes names to avoid conflicts |\n| `ToolTransform` | Modifies tool schemas (rename, description, arguments) |\n\nThe most common use is namespacing mounted servers to prevent name collisions. When you call `mount(server, namespace=\"api\")`, FastMCP creates a `Namespace` transform automatically.\n\nTransforms can be added to individual providers (affecting just that source) or to the server itself (affecting all components). See [Transforms](/servers/transforms/transforms) for the full picture.\n\n## Provider Order\n\nWhen a client requests a tool, FastMCP queries providers in registration order. The first provider that has the tool handles the request.\n\n`LocalProvider` is always first, so your decorator-defined tools take precedence. Additional providers are queried in the order you added them. This means if two providers have a tool with the same name, the first one wins.\n\n## When to Care About Providers\n\n**You can ignore providers entirely** if you're building a simple server with decorators. Just use `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` - FastMCP handles the rest.\n\n**Learn about providers when** you want to:\n- [Mount another server](/servers/composition) into yours\n- [Proxy a remote server](/servers/providers/proxy) through yours\n- [Control visibility state](/servers/visibility) of components\n- [Build dynamic sources](/servers/providers/custom) like database-backed tools\n\n## Next Steps\n\n- [Local](/servers/providers/local) - How decorators work\n- [Mounting](/servers/composition) - Compose servers together\n- [Proxying](/servers/providers/proxy) - Connect to remote servers\n- [Transforms](/servers/transforms/transforms) - Namespace, rename, and modify components\n- [Visibility](/servers/visibility) - Control which components clients can access\n- [Custom](/servers/providers/custom) - Build your own providers\n"
  },
  {
    "path": "docs/servers/providers/proxy.mdx",
    "content": "---\ntitle: MCP Proxy Provider\nsidebarTitle: MCP Proxy\ndescription: Source components from other MCP servers\nicon: arrows-retweet\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nThe Proxy Provider sources components from another MCP server through a client connection. This lets you expose any MCP server's tools, resources, and prompts through your own server, whether the source is local or accessed over the network.\n\n## Why Use Proxy Provider\n\nThe Proxy Provider enables:\n\n- **Bridge transports**: Make an HTTP server available via stdio, or vice versa\n- **Aggregate servers**: Combine multiple source servers into one unified server\n- **Add security**: Act as a controlled gateway with authentication and authorization\n- **Simplify access**: Provide a stable endpoint even if backend servers change\n\n```mermaid\nsequenceDiagram\n    participant Client as Your Client\n    participant Proxy as FastMCP Proxy\n    participant Backend as Source Server\n\n    Client->>Proxy: MCP Request (stdio)\n    Proxy->>Backend: MCP Request (HTTP/stdio/SSE)\n    Backend-->>Proxy: MCP Response\n    Proxy-->>Client: MCP Response\n```\n\n## Quick Start\n\n<VersionBadge version=\"2.10.3\" />\n\nCreate a proxy using `create_proxy()`:\n\n```python\nfrom fastmcp.server import create_proxy\n\n# create_proxy() accepts URLs, file paths, and transports directly\nproxy = create_proxy(\"http://example.com/mcp\", name=\"MyProxy\")\n\nif __name__ == \"__main__\":\n    proxy.run()\n```\n\nThis gives you:\n- Safe concurrent request handling\n- Automatic forwarding of MCP features (sampling, elicitation, etc.)\n- Session isolation to prevent context mixing\n\n<Tip>\nTo mount a proxy inside another FastMCP server, see [Mounting External Servers](/servers/composition#mounting-external-servers).\n</Tip>\n\n## Transport Bridging\n\nA common use case is bridging transports between servers:\n\n```python\nfrom fastmcp.server import create_proxy\n\n# Bridge HTTP server to local stdio\nhttp_proxy = create_proxy(\"http://example.com/mcp/sse\", name=\"HTTP-to-stdio\")\n\n# Run locally via stdio for Claude Desktop\nif __name__ == \"__main__\":\n    http_proxy.run()  # Defaults to stdio\n```\n\nOr expose a local server via HTTP:\n\n```python\nfrom fastmcp.server import create_proxy\n\n# Bridge local server to HTTP\nlocal_proxy = create_proxy(\"local_server.py\", name=\"stdio-to-HTTP\")\n\nif __name__ == \"__main__\":\n    local_proxy.run(transport=\"http\", host=\"0.0.0.0\", port=8080)\n```\n\n## Session Isolation\n\n<VersionBadge version=\"2.10.3\" />\n\n`create_proxy()` provides session isolation - each request gets its own isolated backend session:\n\n```python\nfrom fastmcp.server import create_proxy\n\n# Each request creates a fresh backend session (recommended)\nproxy = create_proxy(\"backend_server.py\")\n\n# Multiple clients can use this proxy simultaneously:\n# - Client A calls a tool → gets isolated session\n# - Client B calls a tool → gets different session\n# - No context mixing\n```\n\n### Shared Sessions\n\nIf you pass an already-connected client, the proxy reuses that session:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.server import create_proxy\n\nasync with Client(\"backend_server.py\") as connected_client:\n    # This proxy reuses the connected session\n    proxy = create_proxy(connected_client)\n\n    # ⚠️ Warning: All requests share the same session\n```\n\n<Warning>\nShared sessions may cause context mixing in concurrent scenarios. Use only in single-threaded situations or with explicit synchronization.\n</Warning>\n\n## MCP Feature Forwarding\n\n<VersionBadge version=\"2.10.3\" />\n\nProxies automatically forward MCP protocol features:\n\n| Feature | Description |\n|---------|-------------|\n| Roots | Filesystem root access requests |\n| Sampling | LLM completion requests |\n| Elicitation | User input requests |\n| Logging | Log messages from backend |\n| Progress | Progress notifications |\n\n```python\nfrom fastmcp.server import create_proxy\n\n# All features forwarded automatically\nproxy = create_proxy(\"advanced_backend.py\")\n\n# When the backend:\n# - Requests LLM sampling → forwarded to your client\n# - Logs messages → appear in your client\n# - Reports progress → shown in your client\n```\n\n### Disabling Features\n\nSelectively disable forwarding:\n\n```python\nfrom fastmcp.server.providers.proxy import ProxyClient\n\nbackend = ProxyClient(\n    \"backend_server.py\",\n    sampling_handler=None,  # Disable LLM sampling\n    log_handler=None        # Disable log forwarding\n)\n```\n\n## Configuration-Based Proxies\n\n<VersionBadge version=\"2.4.0\" />\n\nCreate proxies from configuration dictionaries:\n\n```python\nfrom fastmcp.server import create_proxy\n\nconfig = {\n    \"mcpServers\": {\n        \"default\": {\n            \"url\": \"https://example.com/mcp\",\n            \"transport\": \"http\"\n        }\n    }\n}\n\nproxy = create_proxy(config, name=\"Config-Based Proxy\")\n```\n\n### Multi-Server Proxies\n\nCombine multiple servers with automatic namespacing:\n\n```python\nfrom fastmcp.server import create_proxy\n\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather-api.example.com/mcp\",\n            \"transport\": \"http\"\n        },\n        \"calendar\": {\n            \"url\": \"https://calendar-api.example.com/mcp\",\n            \"transport\": \"http\"\n        }\n    }\n}\n\n# Creates unified proxy with prefixed components:\n# - weather_get_forecast\n# - calendar_add_event\ncomposite = create_proxy(config, name=\"Composite\")\n```\n\n## Component Prefixing\n\nProxied components follow standard prefixing rules:\n\n| Component Type | Pattern |\n|----------------|---------|\n| Tools | `{prefix}_{tool_name}` |\n| Prompts | `{prefix}_{prompt_name}` |\n| Resources | `protocol://{prefix}/path` |\n| Templates | `protocol://{prefix}/...` |\n\n## Mirrored Components\n\n<VersionBadge version=\"2.10.5\" />\n\nComponents from a proxy server are \"mirrored\" - they reflect the remote server's state and cannot be modified directly.\n\nTo modify a proxied component (like disabling it), create a local copy:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server import create_proxy\n\nproxy = create_proxy(\"backend_server.py\")\n\n# Get mirrored tool\nmirrored_tool = await proxy.get_tool(\"useful_tool\")\n\n# Create modifiable local copy\nlocal_tool = mirrored_tool.copy()\n\n# Add to your own server\nmy_server = FastMCP(\"MyServer\")\nmy_server.add_tool(local_tool)\n\n# Now you can control enabled state\nmy_server.disable(keys={local_tool.key})\n```\n\n## Performance Considerations\n\nProxying introduces network latency:\n\n| Operation | Local | Proxied (HTTP) |\n|-----------|-------|----------------|\n| `list_tools()` | 1-2ms | 300-400ms |\n| `call_tool()` | 1-2ms | 200-500ms |\n\nWhen mounting proxy servers, this latency affects all operations on the parent server.\n\n### Component List Caching\n\n<VersionBadge version=\"3.2.0\" />\n\n`ProxyProvider` caches the backend's component lists (tools, resources, templates, prompts) so that individual lookups — like resolving a tool by name during `call_tool` — don't require a separate backend connection. The cache stores raw component metadata and is shared across all proxy sessions; per-session visibility, auth, and transforms are still applied after cache lookup by the server layer. The cache refreshes whenever an explicit `list_*` call is made, and entries expire after a configurable TTL (default 300 seconds).\n\nFor backends whose component lists change dynamically, disable caching by setting `cache_ttl=0`.\n\n```python\nfrom fastmcp.server.providers.proxy import ProxyProvider, ProxyClient\n\n# Default 300s TTL\nprovider = ProxyProvider(lambda: ProxyClient(\"http://backend/mcp\"))\n\n# Custom TTL\nprovider = ProxyProvider(lambda: ProxyClient(\"http://backend/mcp\"), cache_ttl=60)\n\n# Disable caching\nprovider = ProxyProvider(lambda: ProxyClient(\"http://backend/mcp\"), cache_ttl=0)\n```\n\n### Session Reuse for Stateless Backends\n\nBy default, each tool call opens a fresh MCP session to the backend. This is the safe default because it prevents state from leaking between requests. However, for stateless HTTP backends where there's no session state to protect, this overhead is unnecessary.\n\nYou can reuse a single backend session by providing a client factory that returns the same client instance:\n\n```python\nfrom fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient\n\nbase_client = ProxyClient(\"http://backend:8000/mcp\")\nshared_client = base_client.new()\n\nproxy = FastMCPProxy(\n    client_factory=lambda: shared_client,\n    name=\"ReusedSessionProxy\",\n)\n```\n\nThis eliminates the MCP initialization handshake on every call, which can dramatically reduce latency under load. The `Client` uses reference counting for its session lifecycle, so concurrent callers sharing the same instance is safe.\n\n<Warning>\nOnly reuse sessions when you know the backend is stateless (e.g. stateless HTTP). For stateful backends (stdio processes, servers that track session state), use the default fresh-session behavior to avoid context mixing.\n</Warning>\n\n## Advanced Usage\n\n### FastMCPProxy Class\n\nFor explicit session control, use `FastMCPProxy` directly:\n\n```python\nfrom fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient\n\n# Custom session factory\ndef create_client():\n    return ProxyClient(\"backend_server.py\")\n\nproxy = FastMCPProxy(client_factory=create_client)\n```\n\nThis gives you full control over session creation and reuse strategies.\n\n### Adding Proxied Components to Existing Server\n\nMount a proxy to add components from another server:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server import create_proxy\n\nserver = FastMCP(\"My Server\")\n\n# Add local tools\n@server.tool\ndef local_tool() -> str:\n    return \"Local result\"\n\n# Mount proxied tools from another server\nexternal = create_proxy(\"http://external-server/mcp\")\nserver.mount(external)\n\n# Now server has both local and proxied tools\n```\n"
  },
  {
    "path": "docs/servers/providers/skills.mdx",
    "content": "---\ntitle: Skills Provider\nsidebarTitle: Skills\ndescription: Expose agent skills as MCP resources\nicon: wand-magic-sparkles\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nAgent skills are directories containing instructions and supporting files that teach an AI assistant how to perform specific tasks. Tools like Claude Code, Cursor, and VS Code Copilot each have their own skills directories where users can add custom capabilities. The Skills Provider exposes these skill directories as MCP resources, making skills discoverable and shareable across different AI tools and clients.\n\n## Why Skills as Resources\n\nSkills live in platform-specific directories (`~/.claude/skills/`, `~/.cursor/skills/`, etc.) and typically contain a main instruction file plus supporting reference materials. When you want to share skills between tools or access them from a custom client, you need a way to discover and retrieve these files programmatically.\n\nThe Skills Provider solves this by exposing each skill as a set of MCP resources. A client can list available skills, read the main instruction file, check the manifest to see what supporting files exist, and fetch any file it needs. This transforms local skill directories into a standardized API that works with any MCP client.\n\n## Quick Start\n\nCreate a provider pointing to your skills directory, then add it to your server.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\nmcp = FastMCP(\"Skills Server\")\nmcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / \".claude\" / \"skills\"))\n```\n\nEach subdirectory containing a `SKILL.md` file becomes a discoverable skill. Clients can then list resources to see available skills and read them as needed.\n\n```python\nfrom fastmcp import Client\n\nasync with Client(mcp) as client:\n    # List all skill resources\n    resources = await client.list_resources()\n    for r in resources:\n        print(r.uri)  # skill://my-skill/SKILL.md, skill://my-skill/_manifest, ...\n\n    # Read a skill's main instruction file\n    result = await client.read_resource(\"skill://my-skill/SKILL.md\")\n    print(result[0].text)\n```\n\n## Skill Structure\n\nA skill is a directory containing a main instruction file (default: `SKILL.md`) and optionally supporting files. The directory name becomes the skill's identifier.\n\n```\n~/.claude/skills/\n├── pdf-processing/\n│   ├── SKILL.md           # Main instructions\n│   ├── reference.md       # Supporting documentation\n│   └── examples/\n│       └── sample.pdf\n└── code-review/\n    └── SKILL.md\n```\n\nThe main file can include YAML frontmatter to provide metadata. If no frontmatter exists, the provider extracts a description from the first meaningful line of content.\n\n```markdown\n---\ndescription: Process and extract information from PDF documents\n---\n\n# PDF Processing\n\nInstructions for handling PDFs...\n```\n\n## Resource URIs\n\nEach skill exposes three types of resources, all using the `skill://` URI scheme.\n\nThe main instruction file contains the primary skill content. This is the resource clients read to understand what a skill does and how to use it.\n\n```\nskill://pdf-processing/SKILL.md\n```\n\nThe manifest is a synthetic JSON resource listing all files in the skill directory with their sizes and SHA256 hashes. Clients use this to discover supporting files and verify content integrity.\n\n```\nskill://pdf-processing/_manifest\n```\n\nReading the manifest returns structured file information.\n\n```json\n{\n  \"skill\": \"pdf-processing\",\n  \"files\": [\n    {\"path\": \"SKILL.md\", \"size\": 1234, \"hash\": \"sha256:abc123...\"},\n    {\"path\": \"reference.md\", \"size\": 567, \"hash\": \"sha256:def456...\"},\n    {\"path\": \"examples/sample.pdf\", \"size\": 89012, \"hash\": \"sha256:ghi789...\"}\n  ]\n}\n```\n\nSupporting files are any additional files in the skill directory. These might be reference documentation, code examples, or binary assets.\n\n```\nskill://pdf-processing/reference.md\nskill://pdf-processing/examples/sample.pdf\n```\n\n## Provider Architecture\n\nThe Skills Provider uses a two-layer architecture to handle both single skills and skill directories.\n\n### SkillProvider\n\n`SkillProvider` handles a single skill directory. It loads the main file, parses any frontmatter, scans for supporting files, and creates the appropriate resources.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import SkillProvider\n\nmcp = FastMCP(\"Single Skill\")\nmcp.add_provider(SkillProvider(Path.home() / \".claude\" / \"skills\" / \"pdf-processing\"))\n```\n\nUse `SkillProvider` when you want to expose exactly one skill, or when you need fine-grained control over individual skill configuration.\n\n### SkillsDirectoryProvider\n\n`SkillsDirectoryProvider` scans one or more root directories and creates a `SkillProvider` for each valid skill folder it finds. A folder is considered a valid skill if it contains the main file (default: `SKILL.md`).\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\nmcp = FastMCP(\"Skills\")\nmcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / \".claude\" / \"skills\"))\n```\n\nWhen scanning multiple root directories, provide them as a list. The first directory takes precedence if the same skill name appears in multiple roots.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\nmcp = FastMCP(\"Skills\")\nmcp.add_provider(SkillsDirectoryProvider(roots=[\n    Path.cwd() / \".claude\" / \"skills\",      # Project-level skills first\n    Path.home() / \".claude\" / \"skills\",     # User-level fallback\n]))\n```\n\n## Vendor Providers\n\nFastMCP includes pre-configured providers for popular AI coding tools. Each vendor provider extends `SkillsDirectoryProvider` with the appropriate default directory for that platform.\n\n| Provider | Default Directory |\n|----------|-------------------|\n| `ClaudeSkillsProvider` | `~/.claude/skills/` |\n| `CursorSkillsProvider` | `~/.cursor/skills/` |\n| `VSCodeSkillsProvider` | `~/.copilot/skills/` |\n| `CodexSkillsProvider` | `/etc/codex/skills/` and `~/.codex/skills/` |\n| `GeminiSkillsProvider` | `~/.gemini/skills/` |\n| `GooseSkillsProvider` | `~/.config/agents/skills/` |\n| `CopilotSkillsProvider` | `~/.copilot/skills/` |\n| `OpenCodeSkillsProvider` | `~/.config/opencode/skills/` |\n\nVendor providers accept the same configuration options as `SkillsDirectoryProvider` (except for `roots`, which is locked to the platform default).\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import ClaudeSkillsProvider\n\nmcp = FastMCP(\"Claude Skills\")\nmcp.add_provider(ClaudeSkillsProvider())  # Uses ~/.claude/skills/\n```\n\n`CodexSkillsProvider` scans both system-level (`/etc/codex/skills/`) and user-level (`~/.codex/skills/`) directories, with system skills taking precedence.\n\n## Supporting Files Disclosure\n\nThe `supporting_files` parameter controls how supporting files (everything except the main file and manifest) appear to clients.\n\n### Template Mode (Default)\n\nWith `supporting_files=\"template\"`, supporting files are accessed through a `ResourceTemplate` rather than being listed as individual resources. Clients see only the main file and manifest in `list_resources()`, then discover supporting files by reading the manifest.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\n# Default behavior - supporting files hidden from list_resources()\nprovider = SkillsDirectoryProvider(\n    roots=Path.home() / \".claude\" / \"skills\",\n    supporting_files=\"template\",  # This is the default\n)\n```\n\nThis keeps the resource list compact when skills contain many files. Clients that need supporting files read the manifest first, then request specific files by URI.\n\n### Resources Mode\n\nWith `supporting_files=\"resources\"`, every file in every skill appears as an individual resource in `list_resources()`. Clients get full enumeration upfront without needing to read manifests.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\n# All files visible as individual resources\nprovider = SkillsDirectoryProvider(\n    roots=Path.home() / \".claude\" / \"skills\",\n    supporting_files=\"resources\",\n)\n```\n\nUse this mode when clients need to discover all available files without additional round trips, or when integrating with tools that expect flat resource lists.\n\n## Reload Mode\n\nEnable reload mode to re-scan the skills directory on every request. Changes to skills take effect immediately without restarting the server.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\nprovider = SkillsDirectoryProvider(\n    roots=Path.home() / \".claude\" / \"skills\",\n    reload=True,\n)\n```\n\nWith `reload=True`, the provider re-discovers skills on each `list_resources()` or `read_resource()` call. New skills appear, removed skills disappear, and modified content reflects current file state.\n\n<Warning>\nReload mode adds overhead to every request. Use it during development when you're actively editing skills, but disable it in production.\n</Warning>\n\n## Client Utilities\n\nFastMCP provides utilities for downloading skills from any MCP server that exposes them. These are standalone functions in `fastmcp.utilities.skills`.\n\n### Discovering Skills\n\nUse `list_skills()` to see what skills are available on a server.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.utilities.skills import list_skills\n\nasync with Client(\"http://skills-server/mcp\") as client:\n    skills = await list_skills(client)\n    for skill in skills:\n        print(f\"{skill.name}: {skill.description}\")\n```\n\n### Downloading Skills\n\nUse `download_skill()` to download a single skill, or `sync_skills()` to download all available skills.\n\n```python\nfrom pathlib import Path\n\nfrom fastmcp import Client\nfrom fastmcp.utilities.skills import download_skill, sync_skills\n\nasync with Client(\"http://skills-server/mcp\") as client:\n    # Download one skill\n    path = await download_skill(client, \"pdf-processing\", Path.home() / \".claude\" / \"skills\")\n\n    # Or download all skills\n    paths = await sync_skills(client, Path.home() / \".claude\" / \"skills\")\n```\n\nBoth functions accept an `overwrite` parameter. When `False` (default), existing skills are skipped. When `True`, existing files are replaced.\n\n### Inspecting Manifests\n\nUse `get_skill_manifest()` to see what files a skill contains before downloading.\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.utilities.skills import get_skill_manifest\n\nasync with Client(\"http://skills-server/mcp\") as client:\n    manifest = await get_skill_manifest(client, \"pdf-processing\")\n    for file in manifest.files:\n        print(f\"{file.path} ({file.size} bytes, {file.hash})\")\n```\n"
  },
  {
    "path": "docs/servers/resources.mdx",
    "content": "---\ntitle: Resources & Templates\nsidebarTitle: Resources\ndescription: Expose data sources and dynamic content generators to your MCP client.\nicon: folder-open\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nResources represent data or files that an MCP client can read, and resource templates extend this concept by allowing clients to request dynamically generated resources based on parameters passed in the URI.\n\nFastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator.\n\n## What Are Resources?\n\nResources provide read-only access to data for the LLM or client application. When a client requests a resource URI:\n\n1.  FastMCP finds the corresponding resource definition.\n2.  If it's dynamic (defined by a function), the function is executed.\n3.  The content (text, JSON, binary data) is returned to the client.\n\nThis allows LLMs to access files, database content, configuration, or dynamically generated information relevant to the conversation.\n\n## Resources\n\n### The `@resource` Decorator\n\nThe most common way to define a resource is by decorating a Python function. The decorator requires the resource's unique URI.\n\n```python\nimport json\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Basic dynamic resource returning a string\n@mcp.resource(\"resource://greeting\")\ndef get_greeting() -> str:\n    \"\"\"Provides a simple greeting message.\"\"\"\n    return \"Hello from FastMCP Resources!\"\n\n# Resource returning JSON data\n@mcp.resource(\"data://config\")\ndef get_config() -> str:\n    \"\"\"Provides application configuration as JSON.\"\"\"\n    return json.dumps({\n        \"theme\": \"dark\",\n        \"version\": \"1.2.0\",\n        \"features\": [\"tools\", \"resources\"],\n    })\n```\n\n**Key Concepts:**\n\n*   **URI:** The first argument to `@resource` is the unique URI (e.g., `\"resource://greeting\"`) clients use to request this data.\n*   **Lazy Loading:** The decorated function (`get_greeting`, `get_config`) is only executed when a client specifically requests that resource URI via `resources/read`.\n*   **Inferred Metadata:** By default:\n    *   Resource Name: Taken from the function name (`get_greeting`).\n    *   Resource Description: Taken from the function's docstring.\n\n#### Decorator Arguments\n\nYou can customize the resource's properties using arguments in the `@mcp.resource` decorator:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Example specifying metadata\n@mcp.resource(\n    uri=\"data://app-status\",      # Explicit URI (required)\n    name=\"ApplicationStatus\",     # Custom name\n    description=\"Provides the current status of the application.\", # Custom description\n    mime_type=\"application/json\", # Explicit MIME type\n    tags={\"monitoring\", \"status\"}, # Categorization tags\n    meta={\"version\": \"2.1\", \"team\": \"infrastructure\"}  # Custom metadata\n)\ndef get_application_status() -> str:\n    \"\"\"Internal function description (ignored if description is provided above).\"\"\"\n    return json.dumps({\"status\": \"ok\", \"uptime\": 12345, \"version\": mcp.settings.version})\n```\n\n<Card icon=\"code\" title=\"@resource Decorator Arguments\">\n<ParamField body=\"uri\" type=\"str\" required>\n  The unique identifier for the resource\n</ParamField>\n\n<ParamField body=\"name\" type=\"str | None\">\n  A human-readable name. If not provided, defaults to function name\n</ParamField>\n\n<ParamField body=\"description\" type=\"str | None\">\n  Explanation of the resource. If not provided, defaults to docstring\n</ParamField>\n\n<ParamField body=\"mime_type\" type=\"str | None\">\n  Specifies the content type. FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types\n</ParamField>\n\n<ParamField body=\"tags\" type=\"set[str] | None\">\n  A set of strings used to categorize the resource. These can be used by the server and, in some cases, by clients to filter or group available resources.\n</ParamField>\n\n<ParamField body=\"enabled\" type=\"bool\" default=\"True\">\n  <Warning>Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.</Warning>\n  A boolean to enable or disable the resource. See [Component Visibility](#component-visibility) for the recommended approach.\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  Optional list of icon representations for this resource or template. See [Icons](/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"annotations\" type=\"Annotations | dict | None\">\n    An optional `Annotations` object or dictionary to add additional metadata about the resource.\n  <Expandable title=\"Annotations attributes\">\n    <ParamField body=\"readOnlyHint\" type=\"bool | None\">\n      If true, the resource is read-only and does not modify its environment.\n    </ParamField>\n    <ParamField body=\"idempotentHint\" type=\"bool | None\">\n      If true, reading the resource repeatedly will have no additional effect on its environment.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.11.0\" />\n\n  Optional meta information about the resource. This data is passed through to the MCP client as the `meta` field of the client-side resource object and can be used for custom metadata, versioning, or other application-specific purposes.\n</ParamField>\n\n<ParamField body=\"version\" type=\"str | int | None\">\n  <VersionBadge version=\"3.0.0\" />\n\n  Optional version identifier for this resource. See [Versioning](/servers/versioning) for details.\n</ParamField>\n</Card>\n\n#### Using with Methods\n\nFor decorating instance or class methods, use the standalone `@resource` decorator and register the bound method. See [Tools: Using with Methods](/servers/tools#using-with-methods) for the pattern.\n\n### Return Values\n\nResource functions must return one of three types:\n\n-   **`str`**: Sent as `TextResourceContents` (with `mime_type=\"text/plain\"` by default).\n-   **`bytes`**: Base64 encoded and sent as `BlobResourceContents`. You should specify an appropriate `mime_type` (e.g., `\"image/png\"`, `\"application/octet-stream\"`).\n-   **`ResourceResult`**: Full control over contents, MIME types, and metadata. See [ResourceResult](#resourceresult) below.\n\n<Note>\nTo return structured data like dicts or lists, serialize them to JSON strings using `json.dumps()`. This explicit approach ensures your type checker catches errors during development rather than at runtime when a client reads the resource.\n</Note>\n\n#### ResourceResult\n\n<VersionBadge version=\"3.0.0\" />\n\n`ResourceResult` gives you explicit control over resource responses: multiple content items, per-item MIME types, and metadata at both the item and result level.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.resources import ResourceResult, ResourceContent\n\nmcp = FastMCP()\n\n@mcp.resource(\"data://users\")\ndef get_users() -> ResourceResult:\n    return ResourceResult(\n        contents=[\n            ResourceContent(content='[{\"id\": 1}]', mime_type=\"application/json\"),\n            ResourceContent(content=\"# Users\\n...\", mime_type=\"text/markdown\"),\n        ],\n        meta={\"total\": 1}\n    )\n```\n\n`ResourceContent` accepts three fields:\n\n**`content`** - The actual resource content. Can be `str` (text content) or `bytes` (binary content). This is the data that will be returned to the client.\n\n**`mime_type`** - Optional MIME type for the content. Defaults to `\"text/plain\"` for string content and `\"application/octet-stream\"` for binary content.\n\n**`meta`** - Optional metadata dictionary that will be included in the MCP response's `meta` field. Use this for runtime metadata like Content Security Policy headers, caching hints, or other client-specific data.\n\nFor simple cases, you can pass `str` or `bytes` directly to `ResourceResult`:\n\n```python\nreturn ResourceResult(\"plain text\")           # auto-converts to ResourceContent\nreturn ResourceResult(b\"\\x00\\x01\\x02\")         # binary content\n```\n\n<Card title=\"ResourceResult\">\n<ParamField body=\"contents\" type=\"str | bytes | list[ResourceContent]\" required>\n  Content to return. Strings and bytes are wrapped in a single `ResourceContent`. Use a list of `ResourceContent` for multiple items or custom MIME types.\n</ParamField>\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  Result-level metadata, included in the MCP response's `_meta` field.\n</ParamField>\n</Card>\n\n<Card title=\"ResourceContent\">\n<ParamField body=\"content\" type=\"Any\" required>\n  The content data. Strings and bytes pass through directly. Other types (dict, list, BaseModel) are automatically JSON-serialized.\n</ParamField>\n<ParamField body=\"mime_type\" type=\"str | None\">\n  MIME type. Defaults to `text/plain` for strings, `application/octet-stream` for bytes, `application/json` for serialized objects.\n</ParamField>\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  Item-level metadata for this specific content.\n</ParamField>\n</Card>\n\n### Component Visibility\n\n<VersionBadge version=\"3.0.0\" />\n\nYou can control which resources are enabled for clients using server-level enabled control. Disabled resources don't appear in `list_resources` and can't be read.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.resource(\"data://public\", tags={\"public\"})\ndef get_public(): return \"public\"\n\n@mcp.resource(\"data://secret\", tags={\"internal\"})\ndef get_secret(): return \"secret\"\n\n# Disable specific resources by key\nmcp.disable(keys={\"resource:data://secret\"})\n\n# Disable resources by tag\nmcp.disable(tags={\"internal\"})\n\n# Or use allowlist mode - only enable resources with specific tags\nmcp.enable(tags={\"public\"}, only=True)\n```\n\nSee [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control.\n\n\n### Accessing MCP Context\n\n<VersionBadge version=\"2.2.5\" />\n\nResources and resource templates can access additional MCP information and features through the `Context` object. To access it, add a parameter to your resource function with a type annotation of `Context`:\n\n```python {6, 14}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"DataServer\")\n\n@mcp.resource(\"resource://system-status\")\nasync def get_system_status(ctx: Context) -> str:\n    \"\"\"Provides system status information.\"\"\"\n    return json.dumps({\n        \"status\": \"operational\",\n        \"request_id\": ctx.request_id\n    })\n\n@mcp.resource(\"resource://{name}/details\")\nasync def get_details(name: str, ctx: Context) -> str:\n    \"\"\"Get details for a specific name.\"\"\"\n    return json.dumps({\n        \"name\": name,\n        \"accessed_at\": ctx.request_id\n    })\n```\n\nFor full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).\n\n\n### Async Resources\n\nFastMCP supports both `async def` and regular `def` resource functions. Synchronous functions automatically run in a threadpool to avoid blocking the event loop.\n\nFor I/O-bound operations, async functions are more efficient:\n\n```python\nimport aiofiles\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n@mcp.resource(\"file:///app/data/important_log.txt\", mime_type=\"text/plain\")\nasync def read_important_log() -> str:\n    \"\"\"Reads content from a specific log file asynchronously.\"\"\"\n    try:\n        async with aiofiles.open(\"/app/data/important_log.txt\", mode=\"r\") as f:\n            content = await f.read()\n        return content\n    except FileNotFoundError:\n        return \"Log file not found.\"\n```\n\n\n### Resource Classes\n\nWhile `@mcp.resource` is ideal for dynamic content, you can directly register pre-defined resources (like static files or simple text) using `mcp.add_resource()` and concrete `Resource` subclasses.\n\n```python\nfrom pathlib import Path\nfrom fastmcp import FastMCP\nfrom fastmcp.resources import FileResource, TextResource, DirectoryResource\n\nmcp = FastMCP(name=\"DataServer\")\n\n# 1. Exposing a static file directly\nreadme_path = Path(\"./README.md\").resolve()\nif readme_path.exists():\n    # Use a file:// URI scheme\n    readme_resource = FileResource(\n        uri=f\"file://{readme_path.as_posix()}\",\n        path=readme_path, # Path to the actual file\n        name=\"README File\",\n        description=\"The project's README.\",\n        mime_type=\"text/markdown\",\n        tags={\"documentation\"}\n    )\n    mcp.add_resource(readme_resource)\n\n# 2. Exposing simple, predefined text\nnotice_resource = TextResource(\n    uri=\"resource://notice\",\n    name=\"Important Notice\",\n    text=\"System maintenance scheduled for Sunday.\",\n    tags={\"notification\"}\n)\nmcp.add_resource(notice_resource)\n\n# 3. Exposing a directory listing\ndata_dir_path = Path(\"./app_data\").resolve()\nif data_dir_path.is_dir():\n    data_listing_resource = DirectoryResource(\n        uri=\"resource://data-files\",\n        path=data_dir_path, # Path to the directory\n        name=\"Data Directory Listing\",\n        description=\"Lists files available in the data directory.\",\n        recursive=False # Set to True to list subdirectories\n    )\n    mcp.add_resource(data_listing_resource) # Returns JSON list of files\n```\n\n**Common Resource Classes:**\n\n-   `TextResource`: For simple string content.\n-   `BinaryResource`: For raw `bytes` content.\n-   `FileResource`: Reads content from a local file path. Handles text/binary modes and lazy reading.\n-   `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`).\n-   `DirectoryResource`: Lists files in a local directory (returns JSON).\n-   (`FunctionResource`: Internal class used by `@mcp.resource`).\n\nUse these when the content is static or sourced directly from a file/URL, bypassing the need for a dedicated Python function.\n\n### Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends `notifications/resources/list_changed` notifications to connected clients when resources or templates are added, enabled, or disabled. This allows clients to stay up-to-date with the current resource set without manually polling for changes.\n\n```python\n@mcp.resource(\"data://example\")\ndef example_resource() -> str:\n    return \"Hello!\"\n\n# These operations trigger notifications:\nmcp.add_resource(example_resource)                   # Sends resources/list_changed notification\nmcp.disable(keys={\"resource:data://example\"})        # Sends resources/list_changed notification\nmcp.enable(keys={\"resource:data://example\"})         # Sends resources/list_changed notification\n```\n\nNotifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.\n\nClients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their resource lists or update their interfaces.\n\n### Annotations\n\n<VersionBadge version=\"2.11.0\" />\n\nFastMCP allows you to add specialized metadata to your resources through annotations. These annotations communicate how resources behave to client applications without consuming token context in LLM prompts.\n\nAnnotations serve several purposes in client applications:\n- Indicating whether resources are read-only or may have side effects\n- Describing the safety profile of resources (idempotent vs. non-idempotent)\n- Helping clients optimize caching and access patterns\n\nYou can add annotations to a resource using the `annotations` parameter in the `@mcp.resource` decorator:\n\n```python\n@mcp.resource(\n    \"data://config\",\n    annotations={\n        \"readOnlyHint\": True,\n        \"idempotentHint\": True\n    }\n)\ndef get_config() -> str:\n    \"\"\"Get application configuration.\"\"\"\n    return json.dumps({\"version\": \"1.0\", \"debug\": False})\n```\n\nFastMCP supports these standard annotations:\n\n| Annotation | Type | Default | Purpose |\n| :--------- | :--- | :------ | :------ |\n| `readOnlyHint` | boolean | true | Indicates if the resource only provides data without side effects |\n| `idempotentHint` | boolean | true | Indicates if repeated reads have the same effect as a single read |\n\nRemember that annotations help make better user experiences but should be treated as advisory hints. They help client applications present appropriate UI elements and optimize access patterns, but won't enforce behavior on their own. Always focus on making your annotations accurately represent what your resource actually does.\n\n## Resource Templates\n\nResource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature.\n\nResource templates share most configuration options with regular resources (name, description, mime_type, tags, annotations), but add the ability to define URI parameters that map to function parameters.\n\nResource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `\"user://profile/{name}\"` is registered, MCP clients could request `\"user://profile/ford\"` or `\"user://profile/marvin\"` to retrieve either of those two user profiles as resources, without having to register each resource individually.\n\n<Tip>\nFunctions with `*args` are not supported as resource templates. However, unlike tools and prompts, resource templates do support `**kwargs` because the URI template defines specific parameter names that will be collected and passed as keyword arguments.\n</Tip>\n\nHere is a complete example that shows how to define two resource templates:\n\n```python\nimport json\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Template URI includes {city} placeholder\n@mcp.resource(\"weather://{city}/current\")\ndef get_weather(city: str) -> str:\n    \"\"\"Provides weather information for a specific city.\"\"\"\n    return json.dumps({\n        \"city\": city.capitalize(),\n        \"temperature\": 22,\n        \"condition\": \"Sunny\",\n        \"unit\": \"celsius\"\n    })\n\n# Template with multiple parameters and annotations\n@mcp.resource(\n    \"repos://{owner}/{repo}/info\",\n    annotations={\n        \"readOnlyHint\": True,\n        \"idempotentHint\": True\n    }\n)\ndef get_repo_info(owner: str, repo: str) -> str:\n    \"\"\"Retrieves information about a GitHub repository.\"\"\"\n    return json.dumps({\n        \"owner\": owner,\n        \"name\": repo,\n        \"full_name\": f\"{owner}/{repo}\",\n        \"stars\": 120,\n        \"forks\": 48\n    })\n```\n\nWith these two templates defined, clients can request a variety of resources:\n- `weather://london/current` → Returns weather for London\n- `weather://paris/current` → Returns weather for Paris\n- `repos://PrefectHQ/fastmcp/info` → Returns info about the PrefectHQ/fastmcp repository\n- `repos://prefecthq/prefect/info` → Returns info about the prefecthq/prefect repository\n\n### RFC 6570 URI Templates\n\n\nFastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/html/rfc6570) for resource templates, providing a standardized way to define parameterized URIs. This includes support for simple expansion, wildcard path parameters, and form-style query parameters.\n\n#### Wildcard Parameters\n\n<VersionBadge version=\"2.2.4\" />\n\nResource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross \"/\" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.\n\n```python {15, 23}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n\n# Standard parameter only matches one segment\n@mcp.resource(\"files://{filename}\")\ndef get_file(filename: str) -> str:\n    \"\"\"Retrieves a file by name.\"\"\"\n    # Will only match files://<single-segment>\n    return f\"File content for: {filename}\"\n\n\n# Wildcard parameter can match multiple segments\n@mcp.resource(\"path://{filepath*}\")\ndef get_path_content(filepath: str) -> str:\n    \"\"\"Retrieves content at a specific path.\"\"\"\n    # Can match path://docs/server/resources.mdx\n    return f\"Content at path: {filepath}\"\n\n\n# Mixing standard and wildcard parameters\n@mcp.resource(\"repo://{owner}/{path*}/template.py\")\ndef get_template_file(owner: str, path: str) -> dict:\n    \"\"\"Retrieves a file from a specific repository and path, but\n    only if the resource ends with `template.py`\"\"\"\n    # Can match repo://PrefectHQ/fastmcp/src/resources/template.py\n    return {\n        \"owner\": owner,\n        \"path\": path + \"/template.py\",\n        \"content\": f\"File at {path}/template.py in {owner}'s repository\"\n    }\n```\n\nWildcard parameters are useful when:\n\n- Working with file paths or hierarchical data\n- Creating APIs that need to capture variable-length path segments\n- Building URL-like patterns similar to REST APIs\n\nNote that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.\n\n#### Query Parameters\n\n<VersionBadge version=\"2.13.0\" />\n\nFastMCP supports RFC 6570 form-style query parameters using the `{?param1,param2}` syntax. Query parameters provide a clean way to pass optional configuration to resources without cluttering the path.\n\nQuery parameters must be optional function parameters (have default values), while path parameters map to required function parameters. This enforces a clear separation: required data goes in the path, optional configuration in query params.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Basic query parameters\n@mcp.resource(\"data://{id}{?format}\")\ndef get_data(id: str, format: str = \"json\") -> str:\n    \"\"\"Retrieve data in specified format.\"\"\"\n    if format == \"xml\":\n        return f\"<data id='{id}' />\"\n    return f'{{\"id\": \"{id}\"}}'\n\n# Multiple query parameters with type coercion\n@mcp.resource(\"api://{endpoint}{?version,limit,offset}\")\ndef call_api(endpoint: str, version: int = 1, limit: int = 10, offset: int = 0) -> dict:\n    \"\"\"Call API endpoint with pagination.\"\"\"\n    return {\n        \"endpoint\": endpoint,\n        \"version\": version,\n        \"limit\": limit,\n        \"offset\": offset,\n        \"results\": fetch_results(endpoint, version, limit, offset)\n    }\n\n# Query parameters with wildcards\n@mcp.resource(\"files://{path*}{?encoding,lines}\")\ndef read_file(path: str, encoding: str = \"utf-8\", lines: int = 100) -> str:\n    \"\"\"Read file with optional encoding and line limit.\"\"\"\n    return read_file_content(path, encoding, lines)\n```\n\n**Example requests:**\n- `data://123` → Uses default format `\"json\"`\n- `data://123?format=xml` → Uses format `\"xml\"`\n- `api://users?version=2&limit=50` → `version=2, limit=50, offset=0`\n- `files://src/main.py?encoding=ascii&lines=50` → Custom encoding and line limit\n\nFastMCP automatically coerces query parameter string values to the correct types based on your function's type hints (`int`, `float`, `bool`, `str`).\n\n**Query parameters vs. hidden defaults:**\n\nQuery parameters expose optional configuration to clients. To hide optional parameters from clients entirely (always use defaults), simply omit them from the URI template:\n\n```python\n# Clients CAN override max_results via query string\n@mcp.resource(\"search://{query}{?max_results}\")\ndef search_configurable(query: str, max_results: int = 10) -> dict:\n    return {\"query\": query, \"limit\": max_results}\n\n# Clients CANNOT override max_results (not in URI template)\n@mcp.resource(\"search://{query}\")\ndef search_fixed(query: str, max_results: int = 10) -> dict:\n    return {\"query\": query, \"limit\": max_results}\n```\n\n### Template Parameter Rules\n\n<VersionBadge version=\"2.2.0\" />\n\nFastMCP enforces these validation rules when creating resource templates:\n\n1. **Required function parameters** (no default values) must appear in the URI path template\n2. **Query parameters** (specified with `{?param}` syntax) must be optional function parameters with default values\n3. **All URI template parameters** (path and query) must exist as function parameters\n\nOptional function parameters (those with default values) can be:\n- Included as query parameters (`{?param}`) - clients can override via query string\n- Omitted from URI template - always uses default value, not exposed to clients\n- Used in alternative path templates - enables multiple ways to access the same resource\n\n**Multiple templates for one function:**\n\nCreate multiple resource templates that expose the same function through different URI patterns by manually applying decorators:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Define a user lookup function that can be accessed by different identifiers\ndef lookup_user(name: str | None = None, email: str | None = None) -> dict:\n    \"\"\"Look up a user by either name or email.\"\"\"\n    if email:\n        return find_user_by_email(email)  # pseudocode\n    elif name:\n        return find_user_by_name(name)  # pseudocode\n    else:\n        return {\"error\": \"No lookup parameters provided\"}\n\n# Manually apply multiple decorators to the same function\nmcp.resource(\"users://email/{email}\")(lookup_user)\nmcp.resource(\"users://name/{name}\")(lookup_user)\n```\n\nNow an LLM or client can retrieve user information in two different ways:\n- `users://email/alice@example.com` → Looks up user by email (with name=None)\n- `users://name/Bob` → Looks up user by name (with email=None)\n\nThis approach allows a single function to be registered with multiple URI patterns while keeping the implementation clean and straightforward.\n\nTemplates provide a powerful way to expose parameterized data access points following REST-like principles.\n\n## Error Handling\n\n<VersionBadge version=\"2.4.1\" />\n\nIf your resource function encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ResourceError`.\n\nBy default, all exceptions (including their details) are logged and converted into an MCP error response to be sent back to the client LLM. This helps the LLM understand failures and react appropriately.\n\nIf you want to mask internal error details for security reasons, you can:\n\n1. Use the `mask_error_details=True` parameter when creating your `FastMCP` instance:\n```python\nmcp = FastMCP(name=\"SecureServer\", mask_error_details=True)\n```\n\n2. Or use `ResourceError` to explicitly control what error information is sent to clients:\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import ResourceError\n\nmcp = FastMCP(name=\"DataServer\")\n\n@mcp.resource(\"resource://safe-error\")\ndef fail_with_details() -> str:\n    \"\"\"This resource provides detailed error information.\"\"\"\n    # ResourceError contents are always sent back to clients,\n    # regardless of mask_error_details setting\n    raise ResourceError(\"Unable to retrieve data: file not found\")\n\n@mcp.resource(\"resource://masked-error\")\ndef fail_with_masked_details() -> str:\n    \"\"\"This resource masks internal error details when mask_error_details=True.\"\"\"\n    # This message would be masked if mask_error_details=True\n    raise ValueError(\"Sensitive internal file path: /etc/secrets.conf\")\n\n@mcp.resource(\"data://{id}\")\ndef get_data_by_id(id: str) -> dict:\n    \"\"\"Template resources also support the same error handling pattern.\"\"\"\n    if id == \"secure\":\n        raise ValueError(\"Cannot access secure data\")\n    elif id == \"missing\":\n        raise ResourceError(\"Data ID 'missing' not found in database\")\n    return {\"id\": id, \"value\": \"data\"}\n```\n\nWhen `mask_error_details=True`, only error messages from `ResourceError` will include details, other exceptions will be converted to a generic message.\n\n## Server Behavior\n\n### Duplicate Resources\n\n<VersionBadge version=\"2.1.0\" />\n\nYou can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\n    name=\"ResourceServer\",\n    on_duplicate_resources=\"error\" # Raise error on duplicates\n)\n\n@mcp.resource(\"data://config\")\ndef get_config_v1(): return {\"version\": 1}\n\n# This registration attempt will raise a ValueError because\n# \"data://config\" is already registered and the behavior is \"error\".\n# @mcp.resource(\"data://config\")\n# def get_config_v2(): return {\"version\": 2}\n```\n\nThe duplicate behavior options are:\n\n-   `\"warn\"` (default): Logs a warning, and the new resource/template replaces the old one.\n-   `\"error\"`: Raises a `ValueError`, preventing the duplicate registration.\n-   `\"replace\"`: Silently replaces the existing resource/template with the new one.\n-   `\"ignore\"`: Keeps the original resource/template and ignores the new registration attempt.\n\n## Versioning\n\n<VersionBadge version=\"3.0.0\" />\n\nResources and resource templates support versioning, allowing you to maintain multiple implementations under the same URI while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns."
  },
  {
    "path": "docs/servers/sampling.mdx",
    "content": "---\ntitle: Sampling\nsidebarTitle: Sampling\ndescription: Request LLM text generation from the client or a configured provider through the MCP context.\nicon: robot\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.0.0\" />\n\nLLM sampling allows your MCP tools to request text generation from an LLM during execution. This enables tools to leverage AI capabilities for analysis, generation, reasoning, and more—without the client needing to orchestrate multiple calls.\n\nBy default, sampling requests are routed to the client's LLM. You can also configure a fallback handler to use a specific provider (like OpenAI) when the client doesn't support sampling, or to always use your own LLM regardless of client capabilities.\n\n## Overview\n\nThe simplest use of sampling is passing a prompt string to `ctx.sample()`. The method sends the prompt to the LLM, waits for the complete response, and returns a `SamplingResult`. You can access the generated text through the `.text` attribute.\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def summarize(content: str, ctx: Context) -> str:\n    \"\"\"Generate a summary of the provided content.\"\"\"\n    result = await ctx.sample(f\"Please summarize this:\\n\\n{content}\")\n    return result.text or \"\"\n```\n\nThe `SamplingResult` also provides `.result` (identical to `.text` for plain text responses) and `.history` containing the full message exchange—useful if you need to continue the conversation or debug the interaction.\n\n### System Prompts\n\nSystem prompts let you establish the LLM's role and behavioral guidelines before it processes your request. This is useful for controlling tone, enforcing constraints, or providing context that shouldn't clutter the user-facing prompt.\n\n````python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def generate_code(concept: str, ctx: Context) -> str:\n    \"\"\"Generate a Python code example for a concept.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Write a Python example demonstrating '{concept}'.\",\n        system_prompt=(\n            \"You are an expert Python programmer. \"\n            \"Provide concise, working code without explanations.\"\n        ),\n        temperature=0.7,\n        max_tokens=300\n    )\n    return f\"```python\\n{result.text}\\n```\"\n````\n\nThe `temperature` parameter controls randomness—higher values (up to 1.0) produce more varied outputs, while lower values make responses more deterministic. The `max_tokens` parameter limits response length.\n\n### Model Preferences\n\nModel preferences let you hint at which LLM the client should use for a request. You can pass a single model name or a list of preferences in priority order. These are hints rather than requirements—the actual model used depends on what the client has available.\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def technical_analysis(data: str, ctx: Context) -> str:\n    \"\"\"Analyze data using a reasoning-focused model.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Analyze this data:\\n\\n{data}\",\n        model_preferences=[\"claude-opus-4-5\", \"gpt-5-2\"],\n        temperature=0.2,\n    )\n    return result.text or \"\"\n```\n\nUse model preferences when different tasks benefit from different model characteristics. Creative writing might prefer faster models with higher temperature, while complex analysis might benefit from larger reasoning-focused models.\n\n### Multi-Turn Conversations\n\nFor requests that need conversational context, construct a list of `SamplingMessage` objects representing the conversation history. Each message has a `role` (\"user\" or \"assistant\") and `content` (a `TextContent` object).\n\n```python\nfrom mcp.types import SamplingMessage, TextContent\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def contextual_analysis(query: str, data: str, ctx: Context) -> str:\n    \"\"\"Analyze data with conversational context.\"\"\"\n    messages = [\n        SamplingMessage(\n            role=\"user\",\n            content=TextContent(type=\"text\", text=f\"Here's my data: {data}\"),\n        ),\n        SamplingMessage(\n            role=\"assistant\",\n            content=TextContent(type=\"text\", text=\"I see the data. What would you like to know?\"),\n        ),\n        SamplingMessage(\n            role=\"user\",\n            content=TextContent(type=\"text\", text=query),\n        ),\n    ]\n    result = await ctx.sample(messages=messages)\n    return result.text or \"\"\n```\n\nThe LLM receives the full conversation thread and responds with awareness of the preceding context.\n\n### Fallback Handlers\n\nClient support for sampling is optional—some clients may not implement it. To ensure your tools work regardless of client capabilities, configure a `sampling_handler` that sends requests directly to an LLM provider.\n\nFastMCP provides built-in handlers for [OpenAI and Anthropic APIs](/clients/sampling#built-in-handlers). These handlers support the full sampling API including tools, automatically converting your Python functions to each provider's format.\n\n<Note>\nInstall handlers with `pip install fastmcp[openai]` or `pip install fastmcp[anthropic]`.\n</Note>\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\nserver = FastMCP(\n    name=\"My Server\",\n    sampling_handler=OpenAISamplingHandler(default_model=\"gpt-4o-mini\"),\n    sampling_handler_behavior=\"fallback\",\n)\n```\n\nThe `sampling_handler_behavior` parameter controls when the handler is used:\n\n- **`\"fallback\"`** (default): Use the handler only when the client doesn't support sampling. This lets capable clients use their own LLM while ensuring your tools still work with clients that lack sampling support.\n- **`\"always\"`**: Always use the handler, bypassing the client entirely. Use this when you need guaranteed control over which LLM processes requests—for cost control, compliance requirements, or when specific model characteristics are essential.\n\n## Structured Output\n\n<VersionBadge version=\"2.14.1\" />\n\nWhen you need validated, typed data instead of free-form text, use the `result_type` parameter. FastMCP ensures the LLM returns data matching your type, handling validation and retries automatically.\n\nThe `result_type` parameter accepts Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`. When you specify a result type, FastMCP automatically creates a `final_response` tool that the LLM calls to provide its response. If validation fails, the error is sent back to the LLM for retry.\n\n```python\nfrom pydantic import BaseModel\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\nclass SentimentResult(BaseModel):\n    sentiment: str\n    confidence: float\n    reasoning: str\n\n@mcp.tool\nasync def analyze_sentiment(text: str, ctx: Context) -> SentimentResult:\n    \"\"\"Analyze text sentiment with structured output.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Analyze the sentiment of: {text}\",\n        result_type=SentimentResult,\n    )\n    return result.result  # A validated SentimentResult object\n```\n\nWhen you call this tool, the LLM returns a structured response that FastMCP validates against your Pydantic model. You access the validated object through `result.result`, while `result.text` contains the JSON representation.\n\n### Structured Output with Tools\n\nCombine structured output with tools for agentic workflows that return validated data. The LLM uses your tools to gather information, then returns a response matching your type.\n\n```python\nfrom pydantic import BaseModel\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\ndef search(query: str) -> str:\n    \"\"\"Search the web for information.\"\"\"\n    return f\"Results for: {query}\"\n\ndef fetch_url(url: str) -> str:\n    \"\"\"Fetch content from a URL.\"\"\"\n    return f\"Content from: {url}\"\n\nclass ResearchResult(BaseModel):\n    summary: str\n    sources: list[str]\n    confidence: float\n\n@mcp.tool\nasync def research(topic: str, ctx: Context) -> ResearchResult:\n    \"\"\"Research a topic and return structured findings.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Research: {topic}\",\n        tools=[search, fetch_url],\n        result_type=ResearchResult,\n    )\n    return result.result\n```\n\n<Note>\nStructured output with automatic validation only applies to `sample()`. With `sample_step()`, you must manage structured output yourself.\n</Note>\n\n## Tool Use\n\n<VersionBadge version=\"2.14.1\" />\n\nSampling with tools enables agentic workflows where the LLM can call functions to gather information before responding. This implements [SEP-1577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577), allowing the LLM to autonomously orchestrate multi-step operations.\n\nPass Python functions to the `tools` parameter, and FastMCP handles the execution loop automatically—calling tools, returning results to the LLM, and continuing until the LLM provides a final response.\n\n### Defining Tools\n\nDefine regular Python functions with type hints and docstrings. FastMCP extracts the function's name, docstring, and parameter types to create tool schemas that the LLM can understand.\n\n```python\nfrom fastmcp import FastMCP, Context\n\ndef search(query: str) -> str:\n    \"\"\"Search the web for information.\"\"\"\n    return f\"Results for: {query}\"\n\ndef get_time() -> str:\n    \"\"\"Get the current time.\"\"\"\n    from datetime import datetime\n    return datetime.now().strftime(\"%H:%M:%S\")\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def research(question: str, ctx: Context) -> str:\n    \"\"\"Answer questions using available tools.\"\"\"\n    result = await ctx.sample(\n        messages=question,\n        tools=[search, get_time],\n    )\n    return result.text or \"\"\n```\n\nThe LLM sees each function's signature and docstring, using this information to decide when and how to call them. Tool errors are caught and sent back to the LLM, allowing it to recover gracefully. An internal safety limit prevents infinite loops.\n\n### Custom Tool Definitions\n\nFor custom names or descriptions, use `SamplingTool.from_function()`:\n\n```python\nfrom fastmcp.server.sampling import SamplingTool\n\ntool = SamplingTool.from_function(\n    my_func,\n    name=\"custom_name\",\n    description=\"Custom description\"\n)\n\nresult = await ctx.sample(messages=\"...\", tools=[tool])\n```\n\n### Error Handling\n\nBy default, when a sampling tool raises an exception, the error message (including details) is sent back to the LLM so it can attempt recovery. To prevent sensitive information from leaking to the LLM, use the `mask_error_details` parameter:\n\n```python\nresult = await ctx.sample(\n    messages=question,\n    tools=[search],\n    mask_error_details=True,  # Generic error messages only\n)\n```\n\nWhen `mask_error_details=True`, tool errors become generic messages like `\"Error executing tool 'search'\"` instead of exposing stack traces or internal details.\n\nTo intentionally provide specific error messages to the LLM regardless of masking, raise `ToolError`:\n\n```python\nfrom fastmcp.exceptions import ToolError\n\ndef search(query: str) -> str:\n    \"\"\"Search for information.\"\"\"\n    if not query.strip():\n        raise ToolError(\"Search query cannot be empty\")\n    return f\"Results for: {query}\"\n```\n\n`ToolError` messages always pass through to the LLM, making it the escape hatch for errors you want the LLM to see and handle.\n\n### Concurrent Tool Execution\n\nBy default, tools execute sequentially — one at a time, in order. When your tools are independent (no shared state between them), you can execute them in parallel with `tool_concurrency`:\n\n```python\nresult = await ctx.sample(\n    messages=\"Research these three topics\",\n    tools=[search, fetch_url],\n    tool_concurrency=0,  # Unlimited parallel execution\n)\n```\n\nThe `tool_concurrency` parameter controls how many tools run at once:\n\n- **`None`** (default): Sequential execution\n- **`0`**: Unlimited parallel execution\n- **`N > 0`**: Execute at most N tools concurrently\n\nFor tools that must not run concurrently (file writes, shared state mutations, etc.), mark them as `sequential` when creating the `SamplingTool`:\n\n```python\nfrom fastmcp.server.sampling import SamplingTool\n\ndb_writer = SamplingTool.from_function(\n    write_to_db,\n    sequential=True,  # Forces all tools in the batch to run sequentially\n)\n\nresult = await ctx.sample(\n    messages=\"Process this data\",\n    tools=[search, db_writer],\n    tool_concurrency=0,  # Would be parallel, but db_writer forces sequential\n)\n```\n\n<Note>\nWhen any tool in a batch has `sequential=True`, the entire batch executes sequentially regardless of `tool_concurrency`. This is a conservative guarantee — if one tool needs ordering, all tools in that batch respect it.\n</Note>\n\n### Client Requirements\n\n<Note>\nSampling with tools requires the client to advertise the `sampling.tools` capability. FastMCP clients do this automatically. For external clients that don't support tool-enabled sampling, configure a fallback handler with `sampling_handler_behavior=\"always\"`.\n</Note>\n\n## Advanced Control\n\n<VersionBadge version=\"2.14.1\" />\n\nWhile `sample()` handles the tool execution loop automatically, some scenarios require fine-grained control over each step. The `sample_step()` method makes a single LLM call and returns a `SampleStep` containing the response and updated history.\n\nUnlike `sample()`, `sample_step()` is stateless—it doesn't remember previous calls. You control the conversation by passing the full message history each time. The returned `step.history` includes all messages up through the current response, making it easy to continue the loop.\n\nUse `sample_step()` when you need to:\n\n- Inspect tool calls before they execute\n- Implement custom termination conditions\n- Add logging, metrics, or checkpointing between steps\n- Build custom agentic loops with domain-specific logic\n\n### Basic Loop\n\nBy default, `sample_step()` executes any tool calls and includes the results in the history. Call it in a loop, passing the updated history each time, until a stop condition is met.\n\n```python\nfrom mcp.types import SamplingMessage\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\ndef search(query: str) -> str:\n    return f\"Results for: {query}\"\n\ndef get_time() -> str:\n    return \"12:00 PM\"\n\n@mcp.tool\nasync def controlled_agent(question: str, ctx: Context) -> str:\n    \"\"\"Agent with manual loop control.\"\"\"\n    messages: list[str | SamplingMessage] = [question]\n\n    while True:\n        step = await ctx.sample_step(\n            messages=messages,\n            tools=[search, get_time],\n        )\n\n        if step.is_tool_use:\n            # Tools already executed (execute_tools=True by default)\n            for call in step.tool_calls:\n                print(f\"Called tool: {call.name}\")\n\n        if not step.is_tool_use:\n            return step.text or \"\"\n\n        messages = step.history\n```\n\n### SampleStep Properties\n\nEach `SampleStep` provides information about what the LLM returned:\n\n| Property | Description |\n|----------|-------------|\n| `step.is_tool_use` | True if the LLM requested tool calls |\n| `step.tool_calls` | List of tool calls requested (if any) |\n| `step.text` | The text content (if any) |\n| `step.history` | All messages exchanged so far |\n\nThe contents of `step.history` depend on `execute_tools`:\n- **`execute_tools=True`** (default): Includes tool results, ready for the next iteration\n- **`execute_tools=False`**: Includes the assistant's tool request, but you add results yourself\n\n### Manual Tool Execution\n\nSet `execute_tools=False` to handle tool execution yourself. When disabled, `step.history` contains the user message and the assistant's response with tool calls—but no tool results. You execute the tools and append the results as a user message.\n\n```python\nfrom mcp.types import SamplingMessage, ToolResultContent, TextContent\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def research(question: str, ctx: Context) -> str:\n    \"\"\"Research with manual tool handling.\"\"\"\n\n    def search(query: str) -> str:\n        return f\"Results for: {query}\"\n\n    def get_time() -> str:\n        return \"12:00 PM\"\n\n    tools = {\"search\": search, \"get_time\": get_time}\n    messages: list[SamplingMessage] = [question]\n\n    while True:\n        step = await ctx.sample_step(\n            messages=messages,\n            tools=list(tools.values()),\n            execute_tools=False,\n        )\n\n        if not step.is_tool_use:\n            return step.text or \"\"\n\n        # Execute tools and collect results\n        tool_results = []\n        for call in step.tool_calls:\n            fn = tools[call.name]\n            result = fn(**call.input)\n            tool_results.append(\n                ToolResultContent(\n                    type=\"tool_result\",\n                    toolUseId=call.id,\n                    content=[TextContent(type=\"text\", text=result)],\n                )\n            )\n\n        messages = list(step.history)\n        messages.append(SamplingMessage(role=\"user\", content=tool_results))\n```\n\nTo report an error to the LLM, set `isError=True` on the tool result:\n\n```python\ntool_result = ToolResultContent(\n    type=\"tool_result\",\n    toolUseId=call.id,\n    content=[TextContent(type=\"text\", text=\"Permission denied\")],\n    isError=True,\n)\n```\n\n## Method Reference\n\n<Card icon=\"code\" title=\"ctx.sample()\">\n<ResponseField name=\"ctx.sample\" type=\"async method\">\n  Request text generation from the LLM, running to completion automatically.\n\n  <Expandable title=\"Parameters\">\n    <ResponseField name=\"messages\" type=\"str | list[str | SamplingMessage]\">\n      The prompt to send. Can be a simple string or a list of messages for multi-turn conversations.\n    </ResponseField>\n\n    <ResponseField name=\"system_prompt\" type=\"str | None\" default=\"None\">\n      Instructions that establish the LLM's role and behavior.\n    </ResponseField>\n\n    <ResponseField name=\"temperature\" type=\"float | None\" default=\"None\">\n      Controls randomness (0.0 = deterministic, 1.0 = creative).\n    </ResponseField>\n\n    <ResponseField name=\"max_tokens\" type=\"int | None\" default=\"512\">\n      Maximum tokens to generate.\n    </ResponseField>\n\n    <ResponseField name=\"model_preferences\" type=\"str | list[str] | None\" default=\"None\">\n      Hints for which model the client should use.\n    </ResponseField>\n\n    <ResponseField name=\"tools\" type=\"list[Callable] | None\" default=\"None\">\n      Functions the LLM can call during sampling.\n    </ResponseField>\n\n    <ResponseField name=\"result_type\" type=\"type[T] | None\" default=\"None\">\n      A type for validated structured output. Supports Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`.\n    </ResponseField>\n\n    <ResponseField name=\"mask_error_details\" type=\"bool | None\" default=\"None\">\n      If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking and provide specific error messages to the LLM.\n    </ResponseField>\n\n    <ResponseField name=\"tool_concurrency\" type=\"int | None\" default=\"None\">\n      Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency. If any tool has `sequential=True`, all tools execute sequentially regardless.\n    </ResponseField>\n\n  </Expandable>\n\n  <Expandable title=\"Response\">\n    <ResponseField name=\"SamplingResult[T]\" type=\"dataclass\">\n      - `.text`: The raw text response (or JSON for structured output)\n      - `.result`: The typed result—same as `.text` for plain text, or a validated Pydantic object for structured output\n      - `.history`: All messages exchanged during sampling\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n<Card icon=\"code\" title=\"ctx.sample_step()\">\n<ResponseField name=\"ctx.sample_step\" type=\"async method\">\n  Make a single LLM sampling call. Use this for fine-grained control over the sampling loop.\n\n  <Expandable title=\"Parameters\">\n    <ResponseField name=\"messages\" type=\"str | list[str | SamplingMessage]\">\n      The prompt or conversation history.\n    </ResponseField>\n\n    <ResponseField name=\"system_prompt\" type=\"str | None\" default=\"None\">\n      Instructions that establish the LLM's role and behavior.\n    </ResponseField>\n\n    <ResponseField name=\"temperature\" type=\"float | None\" default=\"None\">\n      Controls randomness (0.0 = deterministic, 1.0 = creative).\n    </ResponseField>\n\n    <ResponseField name=\"max_tokens\" type=\"int | None\" default=\"512\">\n      Maximum tokens to generate.\n    </ResponseField>\n\n    <ResponseField name=\"tools\" type=\"list[Callable] | None\" default=\"None\">\n      Functions the LLM can call during sampling.\n    </ResponseField>\n\n    <ResponseField name=\"tool_choice\" type=\"str | None\" default=\"None\">\n      Controls tool usage: `\"auto\"`, `\"required\"`, or `\"none\"`.\n    </ResponseField>\n\n    <ResponseField name=\"execute_tools\" type=\"bool\" default=\"True\">\n      If True, execute tool calls and append results to history. If False, return immediately with tool calls available for manual execution.\n    </ResponseField>\n\n    <ResponseField name=\"mask_error_details\" type=\"bool | None\" default=\"None\">\n      If True, mask detailed error messages from tool execution.\n    </ResponseField>\n\n    <ResponseField name=\"tool_concurrency\" type=\"int | None\" default=\"None\">\n      Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency.\n    </ResponseField>\n  </Expandable>\n\n  <Expandable title=\"Response\">\n    <ResponseField name=\"SampleStep\" type=\"dataclass\">\n      - `.response`: The raw LLM response\n      - `.history`: Messages including input, assistant response, and tool results\n      - `.is_tool_use`: True if the LLM requested tool execution\n      - `.tool_calls`: List of tool calls (if any)\n      - `.text`: The text content (if any)\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n"
  },
  {
    "path": "docs/servers/server.mdx",
    "content": "---\ntitle: The FastMCP Server\nsidebarTitle: Overview\ndescription: The core FastMCP server class for building MCP applications\nicon: server\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nThe `FastMCP` class is the central piece of every FastMCP application. It acts as the container for your tools, resources, and prompts, managing communication with MCP clients and orchestrating the entire server lifecycle.\n\n## Creating a Server\n\nAt its simplest, a FastMCP server just needs a name. Everything else has sensible defaults.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n```\n\nInstructions help clients (and the LLMs behind them) understand what your server does and how to use it effectively.\n\n```python\nmcp = FastMCP(\n    \"DataAnalysis\",\n    instructions=\"Provides tools for analyzing numerical datasets. Start with get_summary() for an overview.\",\n)\n```\n\n## Components\n\nFastMCP servers expose three types of components to clients, each serving a distinct role in the MCP protocol.\n\n**Tools** are functions that clients invoke to perform actions or access external systems.\n\n```python\n@mcp.tool\ndef multiply(a: float, b: float) -> float:\n    \"\"\"Multiplies two numbers together.\"\"\"\n    return a * b\n```\n\n**Resources** expose data that clients can read — passive data sources rather than invocable functions.\n\n```python\n@mcp.resource(\"data://config\")\ndef get_config() -> dict:\n    return {\"theme\": \"dark\", \"version\": \"1.0\"}\n```\n\n**Prompts** are reusable message templates that guide LLM interactions.\n\n```python\n@mcp.prompt\ndef analyze_data(data_points: list[float]) -> str:\n    formatted_data = \", \".join(str(point) for point in data_points)\n    return f\"Please analyze these data points: {formatted_data}\"\n```\n\nEach component type has detailed documentation: [Tools](/servers/tools), [Resources](/servers/resources) (including [Resource Templates](/servers/resources#resource-templates)), and [Prompts](/servers/prompts).\n\n## Running the Server\n\nStart your server by calling `mcp.run()`. The `if __name__` guard ensures compatibility with MCP clients that launch your server as a subprocess.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet a user by name.\"\"\"\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\nFastMCP supports several transports:\n- **STDIO** (default): For local integrations and CLI tools\n- **HTTP**: For web services using the Streamable HTTP protocol\n- **SSE**: Legacy web transport (deprecated)\n\n```python\n# Run with HTTP transport\nmcp.run(transport=\"http\", host=\"127.0.0.1\", port=9000)\n```\n\nThe server can also be run using the FastMCP CLI. For detailed information on transports and deployment, see [Running Your Server](/deployment/running-server).\n\n\n## Configuration Reference\n\nThe `FastMCP` constructor accepts parameters organized into four categories: identity, composition, behavior, and handlers.\n\n### Identity\n\nThese parameters control how your server presents itself to clients.\n\n<Card>\n<ParamField body=\"name\" type=\"str\" default=\"FastMCP\">\n  A human-readable name for your server, shown in client applications and logs\n</ParamField>\n\n<ParamField body=\"instructions\" type=\"str | None\">\n  Description of how to interact with this server. Clients surface these instructions to help LLMs understand the server's purpose and available functionality\n</ParamField>\n\n<ParamField body=\"version\" type=\"str | None\">\n  Version string for your server. Defaults to the FastMCP library version if not provided\n</ParamField>\n\n<ParamField body=\"website_url\" type=\"str | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  URL to a website with more information about your server. Displayed in client applications\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  List of icon representations for your server. See [Icons](/servers/icons) for details\n</ParamField>\n</Card>\n\n### Composition\n\nThese parameters control what your server is built from — its components, middleware, providers, and lifecycle.\n\n<Card>\n<ParamField body=\"tools\" type=\"list[Tool | Callable] | None\">\n  Tools to register on the server. An alternative to the `@mcp.tool` decorator when you need to add tools programmatically\n</ParamField>\n\n<ParamField body=\"auth\" type=\"OAuthProvider | TokenVerifier | None\">\n  Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration\n</ParamField>\n\n<ParamField body=\"middleware\" type=\"list[Middleware] | None\">\n  [Middleware](/servers/middleware) that intercepts and transforms every MCP message flowing through the server — requests, responses, and notifications in both directions. Use for cross-cutting concerns like logging, error handling, and rate limiting\n</ParamField>\n\n<ParamField body=\"providers\" type=\"list[Provider] | None\">\n  [Providers](/servers/providers) that supply tools, resources, and prompts dynamically. Providers are queried at request time, so they can serve components from databases, APIs, or other external sources\n</ParamField>\n\n<ParamField body=\"transforms\" type=\"list[Transform] | None\">\n  <VersionBadge version=\"3.1.0\" />\n\n  Server-level [transforms](/servers/transforms/transforms) to apply to all components. Transforms modify how tools, resources, and prompts are presented to clients — for example, [search transforms](/servers/transforms/tool-search) replace large catalogs with on-demand discovery\n</ParamField>\n\n<ParamField body=\"lifespan\" type=\"Lifespan | AsyncContextManager | None\">\n  Server-level setup and teardown logic that runs when the server starts and stops. See [Lifespans](/servers/lifespan) for composable lifespans\n</ParamField>\n</Card>\n\n### Behavior\n\nThese parameters tune how the server processes requests and communicates with clients.\n\n<Card>\n<ParamField body=\"on_duplicate\" type='Literal[\"warn\", \"error\", \"replace\", \"ignore\"]' default=\"warn\">\n  How to handle duplicate component registrations\n</ParamField>\n\n<ParamField body=\"strict_input_validation\" type=\"bool\" default=\"False\">\n  <VersionBadge version=\"2.13.0\" />\n\n  When `False` (default), FastMCP uses Pydantic's flexible validation that coerces compatible inputs (e.g., `\"10\"` → `10` for int parameters). When `True`, validates inputs against the exact JSON Schema before calling your function, rejecting type mismatches. See [Input Validation Modes](/servers/tools#input-validation-modes) for details\n</ParamField>\n\n<ParamField body=\"mask_error_details\" type=\"bool | None\">\n  When `True`, replaces internal error details in tool/resource responses with a generic message to avoid leaking implementation details to clients. Defaults to the `FASTMCP_MASK_ERROR_DETAILS` environment variable\n</ParamField>\n\n<ParamField body=\"list_page_size\" type=\"int | None\" default=\"None\">\n  <VersionBadge version=\"3.0.0\" />\n\n  Maximum items per page for list operations (`tools/list`, `resources/list`, etc.). When `None`, all results are returned in a single response. See [Pagination](/servers/pagination) for details\n</ParamField>\n\n<ParamField body=\"tasks\" type=\"bool | None\" default=\"False\">\n  Enable background task support. When `True`, tools and resources can return `CreateTaskResult` to run work asynchronously while the client polls for results\n</ParamField>\n\n<ParamField body=\"client_log_level\" type=\"LoggingLevel | None\">\n  <VersionBadge version=\"3.2.0\" />\n\n  Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Individual clients can override this per-session using the MCP `logging/setLevel` request. One of `\"debug\"`, `\"info\"`, `\"notice\"`, `\"warning\"`, `\"error\"`, `\"critical\"`, `\"alert\"`, or `\"emergency\"`\n</ParamField>\n\n<ParamField body=\"dereference_schemas\" type=\"bool\" default=\"True\">\n  Automatically dereference `$ref` pointers in JSON schemas generated from complex Pydantic models. Most clients require flat schemas without `$ref`, so this should usually stay enabled\n</ParamField>\n</Card>\n\n### Handlers and Storage\n\nThese parameters provide custom handlers for MCP capabilities and persistent storage for session state.\n\n<Card>\n<ParamField body=\"sampling_handler\" type=\"SamplingHandler | None\">\n  Custom handler for MCP sampling requests (server-initiated LLM calls). See [Sampling](/servers/sampling) for details\n</ParamField>\n\n<ParamField body=\"sampling_handler_behavior\" type='Literal[\"always\", \"fallback\"] | None' default=\"fallback\">\n  When `\"fallback\"`, the sampling handler is used only when no tool-specific handler exists. When `\"always\"`, this handler is used for all sampling requests\n</ParamField>\n\n<ParamField body=\"session_state_store\" type=\"AsyncKeyValue | None\">\n  Persistent key-value store for session state that survives across requests. Defaults to an in-memory store. Provide a custom implementation for persistence across server restarts\n</ParamField>\n</Card>\n\n\n## Tag-Based Filtering\n\n<VersionBadge version=\"2.8.0\" />\n\nTags let you categorize components and selectively expose them. This is useful for creating different views of your server for different environments or user types.\n\n```python\n@mcp.tool(tags={\"public\", \"utility\"})\ndef public_tool() -> str:\n    return \"This tool is public\"\n\n@mcp.tool(tags={\"internal\", \"admin\"})\ndef admin_tool() -> str:\n    return \"This tool is for admins only\"\n```\n\nThe filtering logic works as follows:\n- **Enable with `only=True`**: Switches to allowlist mode — only components with at least one matching tag are exposed\n- **Disable**: Components with any matching tag are hidden\n- **Precedence**: Later calls override earlier ones, so call `disable` after `enable` to exclude from an allowlist\n\n<Tip>\nTo ensure a component is never exposed, you can set `enabled=False` on the component itself. See the component-specific documentation for details.\n</Tip>\n\n```python\n# Only expose components tagged with \"public\"\nmcp = FastMCP()\nmcp.enable(tags={\"public\"}, only=True)\n\n# Hide components tagged as \"internal\" or \"deprecated\"\nmcp = FastMCP()\nmcp.disable(tags={\"internal\", \"deprecated\"})\n\n# Combine both: show admin tools but hide deprecated ones\nmcp = FastMCP()\nmcp.enable(tags={\"admin\"}, only=True).disable(tags={\"deprecated\"})\n```\n\nThis filtering applies to all component types (tools, resources, resource templates, and prompts) and affects both listing and access.\n\n## Custom Routes\n\nWhen running with HTTP transport, you can add custom web routes alongside your MCP endpoint using the `@custom_route` decorator.\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.requests import Request\nfrom starlette.responses import PlainTextResponse\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check(request: Request) -> PlainTextResponse:\n    return PlainTextResponse(\"OK\")\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")  # Health check at http://localhost:8000/health\n```\n\nCustom routes are useful for health checks, status endpoints, and simple webhooks. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).\n"
  },
  {
    "path": "docs/servers/storage-backends.mdx",
    "content": "---\ntitle: Storage Backends\nsidebarTitle: Storage Backends\ndescription: Configure persistent and distributed storage for caching and OAuth state management\nicon: database\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nFastMCP uses pluggable storage backends for caching responses and managing OAuth state. By default, all storage is in-memory, which is perfect for development but doesn't persist across restarts. FastMCP includes support for multiple storage backends, and you can easily extend it with custom implementations.\n\n<Tip>\nThe storage layer is powered by **[py-key-value-aio](https://github.com/strawgate/py-key-value)**, an async key-value library maintained by a core FastMCP maintainer. This library provides a unified interface for multiple backends, making it easy to swap implementations based on your deployment needs.\n</Tip>\n\n## Available Backends\n\n### In-Memory Storage\n\n**Best for:** Development, testing, single-process deployments\n\nIn-memory storage is the default for all FastMCP storage needs. It's fast, requires no setup, and is perfect for getting started.\n\n```python\nfrom key_value.aio.stores.memory import MemoryStore\n\n# Used by default - no configuration needed\n# But you can also be explicit:\ncache_store = MemoryStore()\n```\n\n**Characteristics:**\n- ✅ No setup required\n- ✅ Very fast\n- ❌ Data lost on restart\n- ❌ Not suitable for multi-process deployments\n\n### File Storage\n\n**Best for:** Single-server production deployments, persistent caching\n\nFile storage persists data to the filesystem as one JSON file per key, allowing it to survive server restarts. This is the default backend for OAuth storage on Mac and Windows.\n\n```python\nfrom pathlib import Path\nfrom key_value.aio.stores.filetree import (\n    FileTreeStore,\n    FileTreeV1KeySanitizationStrategy,\n    FileTreeV1CollectionSanitizationStrategy,\n)\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\nstorage_dir = Path(\"/var/cache/fastmcp\")\nstore = FileTreeStore(\n    data_directory=storage_dir,\n    key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(storage_dir),\n    collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(storage_dir),\n)\n\n# Persistent response cache\nmiddleware = ResponseCachingMiddleware(cache_storage=store)\n```\n\nThe sanitization strategies ensure keys and collection names are safe for the filesystem — alphanumeric names pass through as-is for readability, while special characters are hashed to prevent path traversal.\n\n**Characteristics:**\n- ✅ Data persists across restarts\n- ✅ No external dependencies\n- ✅ Human-readable files on disk\n- ❌ Not suitable for distributed deployments\n- ❌ Filesystem access required\n\n### Redis\n\n**Best for:** Distributed production deployments, shared caching across multiple servers\n\n<Note>\nRedis support requires an optional dependency: `pip install 'py-key-value-aio[redis]'`\n</Note>\n\nRedis provides distributed caching and state management, ideal for production deployments with multiple server instances.\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\n# Distributed response cache\nmiddleware = ResponseCachingMiddleware(\n    cache_storage=RedisStore(host=\"redis.example.com\", port=6379)\n)\n```\n\nWith authentication:\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\n\ncache_store = RedisStore(\n    host=\"redis.example.com\",\n    port=6379,\n    password=\"your-redis-password\"\n)\n```\n\nFor OAuth token storage:\n\n```python\nimport os\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.redis import RedisStore\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=\"https://your-server.com\",\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=RedisStore(host=\"redis.example.com\", port=6379)\n)\n```\n\n**Characteristics:**\n- ✅ Distributed and highly available\n- ✅ Fast in-memory performance\n- ✅ Works across multiple server instances\n- ✅ Built-in TTL support\n- ❌ Requires Redis infrastructure\n- ❌ Network latency vs local storage\n\n### Other Backends from py-key-value-aio\n\nThe py-key-value-aio library includes additional implementations for various storage systems:\n\n- **DynamoDB** - AWS distributed database\n- **MongoDB** - NoSQL document store\n- **Elasticsearch** - Distributed search and analytics\n- **Memcached** - Distributed memory caching\n- **RocksDB** - Embedded high-performance key-value store\n- **Valkey** - Redis-compatible server\n\nFor configuration details on these backends, consult the [py-key-value-aio documentation](https://github.com/strawgate/py-key-value).\n\n<Warning>\nBefore using these backends in production, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have specific constraints that make them unsuitable for production use.\n</Warning>\n\n## Use Cases in FastMCP\n\n### Server-Side OAuth Token Storage\n\nThe [OAuth Proxy](/servers/auth/oauth-proxy) and OAuth auth providers use storage for persisting OAuth client registrations and upstream tokens. **By default, storage is automatically encrypted using `FernetEncryptionWrapper`.** When providing custom storage, wrap it in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.\n\n**Development (default behavior):**\n\nBy default, FastMCP automatically manages keys and storage based on your platform:\n- **Mac/Windows**: Keys are auto-managed via system keyring, storage defaults to disk. Suitable **only** for development and local testing.\n- **Linux**: Keys are ephemeral, storage defaults to memory.\n\nNo configuration needed:\n\n```python\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=\"your-id\",\n    client_secret=\"your-secret\",\n    base_url=\"https://your-server.com\"\n)\n```\n\n**Production:**\n\nFor production deployments, configure explicit keys and persistent network-accessible storage with encryption:\n\n```python\nimport os\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=\"https://your-server.com\",\n    # Explicit JWT signing key (required for production)\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    # Encrypted persistent storage (required for production)\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(host=\"redis.example.com\", port=6379),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n```\n\nBoth parameters are required for production. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. See [OAuth Token Security](/deployment/http#oauth-token-security) and [Key and Storage Management](/servers/auth/oauth-proxy#key-and-storage-management) for complete setup details.\n\n### Response Caching Middleware\n\nThe [Response Caching Middleware](/servers/middleware#caching-middleware) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter:\n\n```python\nfrom pathlib import Path\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.filetree import (\n    FileTreeStore,\n    FileTreeV1KeySanitizationStrategy,\n    FileTreeV1CollectionSanitizationStrategy,\n)\n\nmcp = FastMCP(\"My Server\")\n\ncache_dir = Path(\"cache\")\ncache_store = FileTreeStore(\n    data_directory=cache_dir,\n    key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(cache_dir),\n    collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(cache_dir),\n)\n\n# Cache to disk instead of memory\nmcp.add_middleware(ResponseCachingMiddleware(cache_storage=cache_store))\n```\n\nFor multi-server deployments sharing a Redis instance:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.prefix_collections import PrefixCollectionsWrapper\n\nbase_store = RedisStore(host=\"redis.example.com\")\nnamespaced_store = PrefixCollectionsWrapper(\n    key_value=base_store,\n    prefix=\"my-server\"\n)\n\nmiddleware = ResponseCachingMiddleware(cache_storage=namespaced_store)\n```\n\n### Client-Side OAuth Token Storage\n\nThe [FastMCP Client](/clients/client) uses storage for persisting OAuth tokens locally. By default, tokens are stored in memory:\n\n```python\nfrom pathlib import Path\nfrom fastmcp.client.auth import OAuthClientProvider\nfrom key_value.aio.stores.filetree import (\n    FileTreeStore,\n    FileTreeV1KeySanitizationStrategy,\n    FileTreeV1CollectionSanitizationStrategy,\n)\n\n# Store tokens on disk for persistence across restarts\ntoken_dir = Path(\"~/.local/share/fastmcp/tokens\").expanduser()\ntoken_storage = FileTreeStore(\n    data_directory=token_dir,\n    key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(token_dir),\n    collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(token_dir),\n)\n\noauth_provider = OAuthClientProvider(\n    mcp_url=\"https://your-mcp-server.com/mcp/sse\",\n    token_storage=token_storage\n)\n```\n\nThis allows clients to reconnect without re-authenticating after restarts.\n\n## Choosing a Backend\n\n| Backend | Development | Single Server | Multi-Server | Cloud Native |\n|---------|-------------|---------------|--------------|--------------|\n| Memory | ✅ Best | ⚠️ Limited | ❌ | ❌ |\n| File | ✅ Good | ✅ Recommended | ❌ | ⚠️ |\n| Redis | ⚠️ Overkill | ✅ Good | ✅ Best | ✅ Best |\n| DynamoDB | ❌ | ⚠️ | ✅ | ✅ Best (AWS) |\n| MongoDB | ❌ | ⚠️ | ✅ | ✅ Good |\n\n**Decision tree:**\n\n1. **Just starting?** Use **Memory** (default) - no configuration needed\n2. **Single server, needs persistence?** Use **File**\n3. **Multiple servers or cloud deployment?** Use **Redis** or **DynamoDB**\n4. **Existing infrastructure?** Look for a matching py-key-value-aio backend\n\n## More Resources\n\n- [py-key-value-aio GitHub](https://github.com/strawgate/py-key-value) - Full library documentation\n- [Response Caching Middleware](/servers/middleware#caching-middleware) - Using storage for caching\n- [OAuth Token Security](/deployment/http#oauth-token-security) - Production OAuth configuration\n- [HTTP Deployment](/deployment/http) - Complete deployment guide\n"
  },
  {
    "path": "docs/servers/tasks.mdx",
    "content": "---\ntitle: Background Tasks\nsidebarTitle: Background Tasks\ndescription: Run long-running operations asynchronously with progress tracking\nicon: clock\ntag: \"NEW\"\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.14.0\" />\n\n<Tip>\nBackground tasks require the `tasks` optional extra. See [installation instructions](#enabling-background-tasks) below.\n</Tip>\n\nFastMCP implements the MCP background task protocol ([SEP-1686](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)), giving your servers a production-ready distributed task scheduler with a single decorator change.\n\n<Tip>\n**What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service, where it processes millions of concurrent tasks every day. Docket is now open-sourced for the community.\n</Tip>\n\n\n## What Are MCP Background Tasks?\n\nIn MCP, all component interactions are blocking by default. When a client calls a tool, reads a resource, or fetches a prompt, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience.\n\nThe MCP background task protocol solves this by letting clients:\n1. **Start** an operation and receive a task ID immediately\n2. **Track** progress as the operation runs\n3. **Retrieve** the result when ready\n\nFastMCP handles all of this for you. Add `task=True` to your decorator, and your function gains full background execution with progress reporting, distributed processing, and horizontal scaling.\n\n### MCP Background Tasks vs Python Concurrency\n\nYou can always use Python's concurrency primitives (asyncio, threads, multiprocessing) or external task queues in your FastMCP servers. FastMCP is just Python—run code however you like.\n\nMCP background tasks are different: they're **protocol-native**. This means MCP clients that support the task protocol can start operations, receive progress updates, and retrieve results through the standard MCP interface. The coordination happens at the protocol level, not inside your application code.\n\n## Enabling Background Tasks\n\n<VersionBadge version=\"3.0.0\" /> Background tasks require the `tasks` extra:\n\n```bash\npip install \"fastmcp[tasks]\"\n```\n\nAdd `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution.\n\n```python {6}\nimport asyncio\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(task=True)\nasync def slow_computation(duration: int) -> str:\n    \"\"\"A long-running operation.\"\"\"\n    for i in range(duration):\n        await asyncio.sleep(1)\n    return f\"Completed in {duration} seconds\"\n```\n\nWhen a client requests background execution, the call returns immediately with a task ID. The work executes in a background worker, and the client can poll for status or wait for the result.\n\n<Warning>\nBackground tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time.\n</Warning>\n\n## Execution Modes\n\nFor fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The MCP task protocol defines three execution modes:\n\n| Mode | Client calls without task | Client calls with task |\n|------|--------------------------|------------------------|\n| `\"forbidden\"` | Executes synchronously | Error: task not supported |\n| `\"optional\"` | Executes synchronously | Executes as background task |\n| `\"required\"` | Error: task required | Executes as background task |\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.tasks import TaskConfig\n\nmcp = FastMCP(\"MyServer\")\n\n# Supports both sync and background execution (default when task=True)\n@mcp.tool(task=TaskConfig(mode=\"optional\"))\nasync def flexible_task() -> str:\n    return \"Works either way\"\n\n# Requires background execution - errors if client doesn't request task\n@mcp.tool(task=TaskConfig(mode=\"required\"))\nasync def must_be_background() -> str:\n    return \"Only runs as a background task\"\n\n# No task support (default when task=False or omitted)\n@mcp.tool(task=TaskConfig(mode=\"forbidden\"))\nasync def sync_only() -> str:\n    return \"Never runs as background task\"\n```\n\nThe boolean shortcuts map to these modes:\n- `task=True` → `TaskConfig(mode=\"optional\")`\n- `task=False` → `TaskConfig(mode=\"forbidden\")`\n\n### Poll Interval\n\n<VersionBadge version=\"2.15.0\" />\n\nWhen clients poll for task status, the server tells them how frequently to check back. By default, FastMCP suggests a 5-second interval, but you can customize this per component:\n\n```python\nfrom datetime import timedelta\nfrom fastmcp import FastMCP\nfrom fastmcp.server.tasks import TaskConfig\n\nmcp = FastMCP(\"MyServer\")\n\n# Poll every 2 seconds for a fast-completing task\n@mcp.tool(task=TaskConfig(mode=\"optional\", poll_interval=timedelta(seconds=2)))\nasync def quick_task() -> str:\n    return \"Done quickly\"\n\n# Poll every 30 seconds for a long-running task\n@mcp.tool(task=TaskConfig(mode=\"optional\", poll_interval=timedelta(seconds=30)))\nasync def slow_task() -> str:\n    return \"Eventually done\"\n```\n\nShorter intervals give clients faster feedback but increase server load. Longer intervals reduce load but delay status updates.\n\n### Server-Wide Default\n\nTo enable background task support for all components by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`.\n\n```python\nmcp = FastMCP(\"MyServer\", tasks=True)\n```\n\n<Warning>\nIf your server defines any synchronous tools, resources, or prompts, you will need to explicitly set `task=False` on their decorators to avoid an error.\n</Warning>\n\n### Graceful Degradation\n\nWhen a client requests background execution but the component has `mode=\"forbidden\"`, FastMCP executes synchronously and returns the result inline. This follows the SEP-1686 specification for graceful degradation—clients can always request background execution without worrying about server capabilities.\n\nConversely, when a component has `mode=\"required\"` but the client doesn't request background execution, FastMCP returns an error indicating that task execution is required.\n\n### Configuration\n\n| Environment Variable | Default | Description |\n|---------------------|---------|-------------|\n| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |\n\n## Backends\n\nFastMCP supports two backends for task execution, each with different tradeoffs.\n\n### In-Memory Backend (Default)\n\nThe in-memory backend (`memory://`) requires zero configuration and works out of the box.\n\n**Advantages:**\n- No external dependencies\n- Simple single-process deployment\n\n**Disadvantages:**\n- **Ephemeral**: If the server restarts, all pending tasks are lost\n- **Higher latency**: ~250ms task pickup time vs single-digit milliseconds with Redis\n- **No horizontal scaling**: Single process only—you cannot add additional workers\n\n### Redis Backend\n\nFor production deployments, use Redis (or Valkey) as your backend by setting `FASTMCP_DOCKET_URL=redis://localhost:6379`.\n\n**Advantages:**\n- **Persistent**: Tasks survive server restarts\n- **Fast**: Single-digit millisecond task pickup latency\n- **Scalable**: Add workers to distribute load across processes or machines\n\n## Workers\n\nEvery FastMCP server with task-enabled components automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.\n\nTo scale horizontally, add more workers using the CLI:\n\n```bash\nfastmcp tasks worker server.py\n```\n\nEach additional worker pulls tasks from the same queue, distributing load across processes. Configure worker concurrency via environment:\n\n```bash\nexport FASTMCP_DOCKET_CONCURRENCY=20\nfastmcp tasks worker server.py\n```\n\n<Note>\nAdditional workers only work with Redis/Valkey backends. The in-memory backend is single-process only.\n</Note>\n\n<Warning>\nTask-enabled components must be defined at server startup to be registered with all workers. Components added dynamically after the server starts will not be available for background execution.\n</Warning>\n\n## Progress Reporting\n\nThe `Progress` dependency lets you report progress back to clients. Inject it as a parameter with a default value, and FastMCP will provide the active progress reporter.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Progress\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(task=True)\nasync def process_files(files: list[str], progress: Progress = Progress()) -> str:\n    await progress.set_total(len(files))\n\n    for file in files:\n        await progress.set_message(f\"Processing {file}\")\n        # ... do work ...\n        await progress.increment()\n\n    return f\"Processed {len(files)} files\"\n```\n\nThe progress API:\n- `await progress.set_total(n)` — Set the total number of steps\n- `await progress.increment(amount=1)` — Increment progress\n- `await progress.set_message(text)` — Update the status message\n\nProgress works in both immediate and background execution modes—you can use the same code regardless of how the client invokes your function.\n\n## Docket Dependencies\n\nFastMCP exposes Docket's full dependency injection system within your task-enabled functions. Beyond `Progress`, you can access the Docket instance, worker information, and use advanced features like retries and timeouts.\n\n```python\nfrom docket import Docket, Worker\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(task=True)\nasync def my_task(\n    progress: Progress = Progress(),\n    docket: Docket = CurrentDocket(),\n    worker: Worker = CurrentWorker(),\n) -> str:\n    # Schedule additional background work\n    await docket.add(another_task, arg1, arg2)\n\n    # Access worker metadata\n    worker_name = worker.name\n\n    return \"Done\"\n```\n\nWith `CurrentDocket()`, you can schedule additional background tasks, chain work together, and coordinate complex workflows. See the [Docket documentation](https://chrisguidry.github.io/docket/) for the complete API, including retry policies, timeouts, and custom dependencies.\n"
  },
  {
    "path": "docs/servers/telemetry.mdx",
    "content": "---\ntitle: OpenTelemetry\nsidebarTitle: Telemetry\ndescription: Native OpenTelemetry instrumentation for distributed tracing.\nicon: chart-line\ntag: NEW\n---\n\nFastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for tool, prompt, resource, and resource template operations, providing visibility into server behavior, request handling, and provider delegation chains.\n\n## How It Works\n\nFastMCP uses the OpenTelemetry API for instrumentation. This means:\n\n- **Zero configuration required** - Instrumentation is always active\n- **No overhead when unused** - Without an SDK, all operations are no-ops\n- **Bring your own SDK** - You control collection, export, and sampling\n- **Works with any OTEL backend** - Jaeger, Zipkin, Datadog, New Relic, etc.\n\n## Enabling Telemetry\n\nThe easiest way to export traces is using `opentelemetry-instrument`, which configures the SDK automatically:\n\n```bash\npip install opentelemetry-distro opentelemetry-exporter-otlp\nopentelemetry-bootstrap -a install\n```\n\nThen run your server with tracing enabled:\n\n```bash\nopentelemetry-instrument \\\n  --service_name my-fastmcp-server \\\n  --exporter_otlp_endpoint http://localhost:4317 \\\n  fastmcp run server.py\n```\n\nOr configure via environment variables:\n\n```bash\nexport OTEL_SERVICE_NAME=my-fastmcp-server\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317\n\nopentelemetry-instrument fastmcp run server.py\n```\n\nThis works with any OTLP-compatible backend (Jaeger, Zipkin, Grafana Tempo, Datadog, etc.) and requires no changes to your FastMCP code.\n\n<Card title=\"OpenTelemetry Python Documentation\" icon=\"book\" href=\"https://opentelemetry.io/docs/languages/python/\">\n  Learn more about the OpenTelemetry Python SDK, auto-instrumentation, and available exporters.\n</Card>\n\n## Tracing\n\nFastMCP creates spans for all MCP operations, providing end-to-end visibility into request handling.\n\n### Server Spans\n\nThe server creates spans for each operation using [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):\n\n| Span Name | Description |\n|-----------|-------------|\n| `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) |\n| `resources/read {uri}` | Resource read (e.g., `resources/read config://database`) |\n| `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) |\n\nFor mounted servers, an additional `delegate {name}` span shows the delegation to the child server.\n\n### Client Spans\n\nThe FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read {uri}`, `prompts/get {name}`).\n\n### Span Hierarchy\n\nSpans form a hierarchy showing the request flow. For mounted servers:\n\n```\ntools/call weather_forecast (CLIENT)\n  └── tools/call weather_forecast (SERVER, provider=FastMCPProvider)\n        └── delegate get_weather (INTERNAL)\n              └── tools/call get_weather (SERVER, provider=LocalProvider)\n```\n\nFor proxy providers connecting to remote servers:\n\n```\ntools/call remote_search (CLIENT)\n  └── tools/call remote_search (SERVER, provider=ProxyProvider)\n        └── [remote server spans via trace context propagation]\n```\n\n## Programmatic Configuration\n\nFor more control, configure the SDK in your Python code before importing FastMCP:\n\n```python\nfrom opentelemetry import trace\nfrom opentelemetry.sdk.trace import TracerProvider\nfrom opentelemetry.sdk.trace.export import BatchSpanProcessor\nfrom opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter\n\n# Configure the SDK with OTLP exporter\nprovider = TracerProvider()\nprocessor = BatchSpanProcessor(OTLPSpanExporter(endpoint=\"http://localhost:4317\"))\nprovider.add_span_processor(processor)\ntrace.set_tracer_provider(provider)\n\n# Now import and use FastMCP - traces will be exported automatically\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"my-server\")\n\n@mcp.tool()\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n```\n\n<Tip>\nThe SDK must be configured **before** importing FastMCP to ensure the tracer provider is set when FastMCP initializes.\n</Tip>\n\n### Local Development\n\nFor quick local trace visualization, [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer) is a lightweight single-binary tool:\n\n```bash\n# macOS\nbrew install nico-barbas/brew/otel-desktop-viewer\n\n# Or download from GitHub releases\n```\n\nRun it alongside your server:\n\n```bash\n# Terminal 1: Start the viewer (UI at http://localhost:8000, OTLP on :4317)\notel-desktop-viewer\n\n# Terminal 2: Run your server with tracing\nopentelemetry-instrument fastmcp run server.py\n```\n\nFor more features, use [Jaeger](https://www.jaegertracing.io/):\n\n```bash\ndocker run -d --name jaeger \\\n  -p 16686:16686 \\\n  -p 4317:4317 \\\n  jaegertracing/all-in-one:latest\n```\n\nThen view traces at http://localhost:16686\n\n## Custom Spans\n\nYou can add your own spans using the FastMCP tracer:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.telemetry import get_tracer\n\nmcp = FastMCP(\"custom-spans\")\n\n@mcp.tool()\nasync def complex_operation(input: str) -> str:\n    tracer = get_tracer()\n\n    with tracer.start_as_current_span(\"parse_input\") as span:\n        span.set_attribute(\"input.length\", len(input))\n        parsed = parse(input)\n\n    with tracer.start_as_current_span(\"process_data\") as span:\n        span.set_attribute(\"data.count\", len(parsed))\n        result = process(parsed)\n\n    return result\n```\n\n## Error Handling\n\nWhen errors occur, spans are automatically marked with error status and the exception is recorded:\n\n```python\n@mcp.tool()\ndef risky_operation() -> str:\n    raise ValueError(\"Something went wrong\")\n\n# The span will have:\n# - status = ERROR\n# - exception event with stack trace\n```\n\n## Attributes Reference\n\n### RPC Semantic Conventions\n\nStandard [RPC semantic conventions](https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/):\n\n| Attribute | Value |\n|-----------|-------|\n| `rpc.system` | `\"mcp\"` |\n| `rpc.service` | Server name |\n| `rpc.method` | MCP protocol method |\n\n### MCP Semantic Conventions\n\nFastMCP implements the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):\n\n| Attribute | Description |\n|-----------|-------------|\n| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`) |\n| `mcp.session.id` | Session identifier for the MCP connection |\n| `mcp.resource.uri` | The resource URI (for resource operations) |\n\n### Auth Attributes\n\nStandard [identity attributes](https://opentelemetry.io/docs/specs/semconv/attributes-registry/enduser/):\n\n| Attribute | Description |\n|-----------|-------------|\n| `enduser.id` | Client ID from access token (when authenticated) |\n| `enduser.scope` | Space-separated OAuth scopes (when authenticated) |\n\n### FastMCP Custom Attributes\n\nAll custom attributes use the `fastmcp.` prefix for features unique to FastMCP:\n\n| Attribute | Description |\n|-----------|-------------|\n| `fastmcp.server.name` | Server name |\n| `fastmcp.component.type` | `tool`, `resource`, `prompt`, or `resource_template` |\n| `fastmcp.component.key` | Full component identifier (e.g., `tool:greet`) |\n| `fastmcp.provider.type` | Provider class (`LocalProvider`, `FastMCPProvider`, `ProxyProvider`) |\n\nProvider-specific attributes for delegation context:\n\n| Attribute | Description |\n|-----------|-------------|\n| `fastmcp.delegate.original_name` | Original tool/prompt name before namespacing |\n| `fastmcp.delegate.original_uri` | Original resource URI before namespacing |\n| `fastmcp.proxy.backend_name` | Remote server tool/prompt name |\n| `fastmcp.proxy.backend_uri` | Remote server resource URI |\n\n## Testing with Telemetry\n\nFor testing, use the in-memory exporter:\n\n```python\nimport pytest\nfrom collections.abc import Generator\nfrom opentelemetry import trace\nfrom opentelemetry.sdk.trace import TracerProvider\nfrom opentelemetry.sdk.trace.export import SimpleSpanProcessor\nfrom opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter\n\nfrom fastmcp import FastMCP\n\n@pytest.fixture\ndef trace_exporter() -> Generator[InMemorySpanExporter, None, None]:\n    exporter = InMemorySpanExporter()\n    provider = TracerProvider()\n    provider.add_span_processor(SimpleSpanProcessor(exporter))\n    original_provider = trace.get_tracer_provider()\n    trace.set_tracer_provider(provider)\n    yield exporter\n    exporter.clear()\n    trace.set_tracer_provider(original_provider)\n\nasync def test_tool_creates_span(trace_exporter: InMemorySpanExporter) -> None:\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool()\n    def hello() -> str:\n        return \"world\"\n\n    await mcp.call_tool(\"hello\", {})\n\n    spans = trace_exporter.get_finished_spans()\n    assert any(s.name == \"tools/call hello\" for s in spans)\n```\n"
  },
  {
    "path": "docs/servers/testing.mdx",
    "content": "---\ntitle: Testing your FastMCP Server\nsidebarTitle: Testing\ndescription: How to test your FastMCP server.\nicon: vial\n---\n\nThe best way to ensure a reliable and maintainable FastMCP Server is to test it! The FastMCP Client combined with Pytest provides a simple and powerful way to test your FastMCP servers.\n\n## Prerequisites\n\nTesting FastMCP servers requires `pytest-asyncio` to handle async test functions and fixtures. Install it as a development dependency:\n\n```bash\npip install pytest-asyncio\n```\n\nWe recommend configuring pytest to automatically handle async tests by setting the asyncio mode to `auto` in your `pyproject.toml`:\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\n```\n\nThis eliminates the need to decorate every async test with `@pytest.mark.asyncio`.\n\n## Testing with Pytest Fixtures\n\nUsing Pytest Fixtures, you can wrap your FastMCP Server in a Client instance that makes interacting with your server fast and easy. This is especially useful when building your own MCP Servers and enables a tight development loop by allowing you to avoid using a separate tool like MCP Inspector during development:\n\n```python\nimport pytest\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\n\nfrom my_project.main import mcp\n\n@pytest.fixture\nasync def main_mcp_client():\n    async with Client(transport=mcp) as mcp_client:\n        yield mcp_client\n\nasync def test_list_tools(main_mcp_client: Client[FastMCPTransport]):\n    list_tools = await main_mcp_client.list_tools()\n\n    assert len(list_tools) == 5\n```\n\nWe recommend the [inline-snapshot library](https://github.com/15r10nk/inline-snapshot) for asserting complex data structures coming from your MCP Server. This library allows you to write tests that are easy to read and understand, and are also easy to update when the data structure changes. \n\n```python\nfrom inline_snapshot import snapshot\n\nasync def test_list_tools(main_mcp_client: Client[FastMCPTransport]):\n    list_tools = await main_mcp_client.list_tools()\n\n    assert list_tools == snapshot()\n```\n\nSimply run `pytest --inline-snapshot=fix,create` to fill in the `snapshot()` with actual data.\n\n<Tip>\nFor values that change you can leverage the [dirty-equals](https://github.com/samuelcolvin/dirty-equals) library to perform flexible equality assertions on dynamic or non-deterministic values.\n</Tip>\n\nUsing the pytest `parametrize` decorator, you can easily test your tools with a wide variety of inputs.\n\n```python\nimport pytest\nfrom my_project.main import mcp\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\n@pytest.fixture\nasync def main_mcp_client():\n    async with Client(mcp) as client:\n        yield client\n\n\n@pytest.mark.parametrize(\n    \"first_number, second_number, expected\",\n    [\n        (1, 2, 3),\n        (2, 3, 5),\n        (3, 4, 7),\n    ],\n)\nasync def test_add(\n    first_number: int,\n    second_number: int,\n    expected: int,\n    main_mcp_client: Client[FastMCPTransport],\n):\n    result = await main_mcp_client.call_tool(\n        name=\"add\", arguments={\"x\": first_number, \"y\": second_number}\n    )\n    assert result.data is not None\n    assert isinstance(result.data, int)\n    assert result.data == expected\n```\n\n<Tip>\nThe [FastMCP Repository contains thousands of tests](https://github.com/PrefectHQ/fastmcp/tree/main/tests) for the FastMCP Client and Server. Everything from connecting to remote MCP servers, to testing tools, resources, and prompts is covered, take a look for inspiration!\n</Tip>"
  },
  {
    "path": "docs/servers/tools.mdx",
    "content": "---\ntitle: Tools\nsidebarTitle: Tools\ndescription: Expose functions as executable capabilities for your MCP client.\nicon: wrench\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nTools are the core building blocks that allow your LLM to interact with external systems, execute code, and access data that isn't in its training data. In FastMCP, tools are Python functions exposed to LLMs through the MCP protocol.\n\nTools in FastMCP transform regular Python functions into capabilities that LLMs can invoke during conversations. When an LLM decides to use a tool:\n\n1.  It sends a request with parameters based on the tool's schema.\n2.  FastMCP validates these parameters against your function's signature.\n3.  Your function executes with the validated inputs.\n4.  The result is returned to the LLM, which can use it in its response.\n\nThis allows LLMs to perform tasks like querying databases, calling APIs, making calculations, or accessing files—extending their capabilities beyond what's in their training data.\n\n\n## The `@tool` Decorator\n\nCreating a tool is as simple as decorating a Python function with `@mcp.tool`:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"CalculatorServer\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n```\n\nWhen this tool is registered, FastMCP automatically:\n- Uses the function name (`add`) as the tool name.\n- Uses the function's docstring (`Adds two integer numbers...`) as the tool description.\n- Generates an input schema based on the function's parameters and type annotations.\n- Handles parameter validation and error reporting.\n\nThe way you define your Python function dictates how the tool appears and behaves for the LLM client.\n\n<Tip>\nFunctions with `*args` or `**kwargs` are not supported as tools. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.\n</Tip>\n\n### Decorator Arguments\n\nWhile FastMCP infers the name and description from your function, you can override these and add additional metadata using arguments to the `@mcp.tool` decorator:\n\n```python\n@mcp.tool(\n    name=\"find_products\",           # Custom tool name for the LLM\n    description=\"Search the product catalog with optional category filtering.\", # Custom description\n    tags={\"catalog\", \"search\"},      # Optional tags for organization/filtering\n    meta={\"version\": \"1.2\", \"author\": \"product-team\"}  # Custom metadata\n)\ndef search_products_implementation(query: str, category: str | None = None) -> list[dict]:\n    \"\"\"Internal function description (ignored if description is provided above).\"\"\"\n    # Implementation...\n    print(f\"Searching for '{query}' in category '{category}'\")\n    return [{\"id\": 2, \"name\": \"Another Product\"}]\n```\n\n<Card icon=\"code\" title=\"@tool Decorator Arguments\">\n<ParamField body=\"name\" type=\"str | None\">\n  Sets the explicit tool name exposed via MCP. If not provided, uses the function name\n</ParamField>\n\n<ParamField body=\"description\" type=\"str | None\">\n  Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose\n</ParamField>\n\n<ParamField body=\"tags\" type=\"set[str] | None\">\n  A set of strings used to categorize the tool. These can be used by the server and, in some cases, by clients to filter or group available tools.\n</ParamField>\n\n<ParamField body=\"enabled\" type=\"bool\" default=\"True\">\n  <Warning>Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.</Warning>\n  A boolean to enable or disable the tool. See [Component Visibility](#component-visibility) for the recommended approach.\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  Optional list of icon representations for this tool. See [Icons](/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"annotations\" type=\"ToolAnnotations | dict | None\">\n    An optional `ToolAnnotations` object or dictionary to add additional metadata about the tool.\n  <Expandable title=\"ToolAnnotations attributes\">\n    <ParamField body=\"title\" type=\"str | None\">\n      A human-readable title for the tool.\n    </ParamField>\n    <ParamField body=\"readOnlyHint\" type=\"bool | None\">\n      If true, the tool does not modify its environment.\n    </ParamField>\n    <ParamField body=\"destructiveHint\" type=\"bool | None\">\n      If true, the tool may perform destructive updates to its environment.\n    </ParamField>\n    <ParamField body=\"idempotentHint\" type=\"bool | None\">\n      If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment.\n    </ParamField>\n    <ParamField body=\"openWorldHint\" type=\"bool | None\">\n      If true, this tool may interact with an \"open world\" of external entities. If false, the tool's domain of interaction is closed.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.11.0\" />\n\n  Optional meta information about the tool. This data is passed through to the MCP client as the `meta` field of the client-side tool object and can be used for custom metadata, versioning, or other application-specific purposes.\n</ParamField>\n\n<ParamField body=\"timeout\" type=\"float | None\">\n  <VersionBadge version=\"3.0.0\" />\n\n  Execution timeout in seconds. If the tool takes longer than this to complete, an MCP error is returned to the client. See [Timeouts](#timeouts) for details.\n</ParamField>\n\n<ParamField body=\"version\" type=\"str | int | None\">\n  <VersionBadge version=\"3.0.0\" />\n\n  Optional version identifier for this tool. See [Versioning](/servers/versioning) for details.\n</ParamField>\n\n<ParamField body=\"output_schema\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.10.0\" />\n\n  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.\n</ParamField>\n</Card>\n\n### Using with Methods\n\nThe `@mcp.tool` decorator registers tools immediately, which doesn't work with instance or class methods (you'd see `self` or `cls` as required parameters). For methods, use the standalone `@tool` decorator to attach metadata, then register the bound method:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import tool\n\nclass Calculator:\n    def __init__(self, multiplier: int):\n        self.multiplier = multiplier\n\n    @tool()\n    def multiply(self, x: int) -> int:\n        \"\"\"Multiply x by the instance multiplier.\"\"\"\n        return x * self.multiplier\n\ncalc = Calculator(multiplier=3)\nmcp = FastMCP()\nmcp.add_tool(calc.multiply)  # Registers with correct schema (only 'x', not 'self')\n```\n\n### Async Support\n\nFastMCP supports both asynchronous (`async def`) and synchronous (`def`) functions as tools. Synchronous tools automatically run in a threadpool to avoid blocking the event loop, so multiple tool calls can execute concurrently even if individual tools perform blocking operations.\n\n```python\nfrom fastmcp import FastMCP\nimport time\n\nmcp = FastMCP()\n\n@mcp.tool\ndef slow_tool(x: int) -> int:\n    \"\"\"This sync function won't block other concurrent requests.\"\"\"\n    time.sleep(2)  # Runs in threadpool, not on the event loop\n    return x * 2\n```\n\nFor 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.\n\n## Arguments\n\nBy 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.\n\n<Note>\nFastMCP automatically dereferences `$ref` entries in tool schemas to ensure compatibility with MCP clients that don't fully support JSON Schema references (e.g., VS Code Copilot, Claude Desktop). This means complex Pydantic models with shared types are inlined in the schema rather than using `$defs` references.\n\nDereferencing happens at serve-time via middleware, so your schemas are stored with `$ref` intact and only inlined when sent to clients. If you know your clients handle `$ref` correctly and prefer smaller schemas, you can opt out:\n\n```python\nmcp = FastMCP(\"my-server\", dereference_schemas=False)\n```\n</Note>\n\n### Type Annotations\n\nMCP tools have typed arguments, and FastMCP uses type annotations to determine those types. Therefore, you should use standard Python type annotations for tool arguments:\n\n```python\n@mcp.tool\ndef analyze_text(\n    text: str,\n    max_tokens: int = 100,\n    language: str | None = None\n) -> dict:\n    \"\"\"Analyze the provided text.\"\"\"\n    # Implementation...\n```\n\nFastMCP supports a wide range of type annotations, including all Pydantic types:\n\n| Type Annotation         | Example                       | Description                         |\n| :---------------------- | :---------------------------- | :---------------------------------- |\n| Basic types             | `int`, `float`, `str`, `bool` | Simple scalar values |\n| Binary data             | `bytes`                       | Binary content (raw strings, not auto-decoded base64) |\n| Date and Time           | `datetime`, `date`, `timedelta` | Date and time objects (ISO format strings) |\n| Collection types        | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items |\n| Optional types          | `float \\| None`, `Optional[float]`| Parameters that may be null/omitted |\n| Union types             | `str \\| int`, `Union[str, int]`| Parameters accepting multiple types |\n| Constrained types       | `Literal[\"A\", \"B\"]`, `Enum`   | Parameters with specific allowed values |\n| Paths                   | `Path`                        | File system paths (auto-converted from strings) |\n| UUIDs                   | `UUID`                        | Universally unique identifiers (auto-converted from strings) |\n| Pydantic models         | `UserData`                    | Complex structured data with validation |\n\nFastMCP supports all types that Pydantic supports as fields, including all Pydantic custom types. A few FastMCP-specific behaviors to note:\n\n**Binary Data**: `bytes` parameters accept raw strings without automatic base64 decoding. For base64 data, use `str` and decode manually with `base64.b64decode()`.\n\n**Enums**: Clients send enum values (`\"red\"`), not names (`\"RED\"`). Your function receives the Enum member (`Color.RED`).\n\n**Paths and UUIDs**: String inputs are automatically converted to `Path` and `UUID` objects.\n\n**Pydantic Models**: Must be provided as JSON objects (dicts), not stringified JSON. Even with flexible validation, `{\"user\": {\"name\": \"Alice\"}}` works, but `{\"user\": '{\"name\": \"Alice\"}'}` does not.\n\n### Optional Arguments\n\nFastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.\n\n```python\n@mcp.tool\ndef search_products(\n    query: str,                   # Required - no default value\n    max_results: int = 10,        # Optional - has default value\n    sort_by: str = \"relevance\",   # Optional - has default value\n    category: str | None = None   # Optional - can be None\n) -> list[dict]:\n    \"\"\"Search the product catalog.\"\"\"\n    # Implementation...\n```\n\nIn this example, the LLM must provide a `query` parameter, while `max_results`, `sort_by`, and `category` will use their default values if not explicitly provided.\n\n### Validation Modes\n\n<VersionBadge version=\"2.13.0\" />\n\nBy default, FastMCP uses Pydantic's flexible validation that coerces compatible inputs to match your type annotations. This improves compatibility with LLM clients that may send string representations of values (like `\"10\"` for an integer parameter).\n\nIf you need stricter validation that rejects any type mismatches, you can enable strict input validation. Strict mode uses the MCP SDK's built-in JSON Schema validation to validate inputs against the exact schema before passing them to your function:\n\n```python\n# Enable strict validation for this server\nmcp = FastMCP(\"StrictServer\", strict_input_validation=True)\n\n@mcp.tool\ndef add_numbers(a: int, b: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n\n# With strict_input_validation=True, sending {\"a\": \"10\", \"b\": \"20\"} will fail\n# With strict_input_validation=False (default), it will be coerced to integers\n```\n\n**Validation Behavior Comparison:**\n\n| Input Type | strict_input_validation=False (default) | strict_input_validation=True |\n| :--------- | :-------------------------------------- | :--------------------------- |\n| String integers (`\"10\"` for `int`) | ✅ Coerced to integer | ❌ Validation error |\n| String floats (`\"3.14\"` for `float`) | ✅ Coerced to float | ❌ Validation error |\n| String booleans (`\"true\"` for `bool`) | ✅ Coerced to boolean | ❌ Validation error |\n| Lists with string elements (`[\"1\", \"2\"]` for `list[int]`) | ✅ Elements coerced | ❌ Validation error |\n| Pydantic model fields with type mismatches | ✅ Fields coerced | ❌ Validation error |\n| Invalid values (`\"abc\"` for `int`) | ❌ Validation error | ❌ Validation error |\n\n<Note>\n**Note on Pydantic Models:** Even with `strict_input_validation=False`, Pydantic model parameters must be provided as JSON objects (dicts), not as stringified JSON. For example, `{\"user\": {\"name\": \"Alice\"}}` works, but `{\"user\": '{\"name\": \"Alice\"}'}` does not.\n</Note>\n\nThe default flexible validation mode is recommended for most use cases as it handles common LLM client behaviors gracefully while still providing strong type safety through Pydantic's validation.\n\n### Parameter Metadata\n\nYou can provide additional metadata about parameters in several ways:\n\n#### Simple String Descriptions\n\n<VersionBadge version=\"2.11.0\" />\n\nFor basic parameter descriptions, you can use a convenient shorthand with `Annotated`:\n\n```python \nfrom typing import Annotated\n\n@mcp.tool\ndef process_image(\n    image_url: Annotated[str, \"URL of the image to process\"],\n    resize: Annotated[bool, \"Whether to resize the image\"] = False,\n    width: Annotated[int, \"Target width in pixels\"] = 800,\n    format: Annotated[str, \"Output image format\"] = \"jpeg\"\n) -> dict:\n    \"\"\"Process an image with optional resizing.\"\"\"\n    # Implementation...\n```\n\nThis shorthand syntax is equivalent to using `Field(description=...)` but more concise for simple descriptions.\n\n<Tip>\nThis shorthand syntax is only applied to `Annotated` types with a single string description. \n</Tip>\n\n#### Advanced Metadata with Field\n\nFor validation constraints and advanced metadata, use Pydantic's `Field` class with `Annotated`:\n\n```python\nfrom typing import Annotated\nfrom pydantic import Field\n\n@mcp.tool\ndef process_image(\n    image_url: Annotated[str, Field(description=\"URL of the image to process\")],\n    resize: Annotated[bool, Field(description=\"Whether to resize the image\")] = False,\n    width: Annotated[int, Field(description=\"Target width in pixels\", ge=1, le=2000)] = 800,\n    format: Annotated[\n        Literal[\"jpeg\", \"png\", \"webp\"], \n        Field(description=\"Output image format\")\n    ] = \"jpeg\"\n) -> dict:\n    \"\"\"Process an image with optional resizing.\"\"\"\n    # Implementation...\n```\n\n\nYou can also use the Field as a default value, though the Annotated approach is preferred:\n\n```python\n@mcp.tool\ndef search_database(\n    query: str = Field(description=\"Search query string\"),\n    limit: int = Field(10, description=\"Maximum number of results\", ge=1, le=100)\n) -> list:\n    \"\"\"Search the database with the provided query.\"\"\"\n    # Implementation...\n```\n\nField provides several validation and documentation features:\n- `description`: Human-readable explanation of the parameter (shown to LLMs)\n- `ge`/`gt`/`le`/`lt`: Greater/less than (or equal) constraints\n- `min_length`/`max_length`: String or collection length constraints\n- `pattern`: Regex pattern for string validation\n- `default`: Default value if parameter is omitted\n\n### Hiding Parameters from the LLM\n\n<VersionBadge version=\"2.14.0\" />\n\nTo inject values at runtime without exposing them to the LLM (such as `user_id`, credentials, or database connections), use dependency injection with `Depends()`. Parameters using `Depends()` are automatically excluded from the tool schema:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP()\n\ndef get_user_id() -> str:\n    return \"user_123\"  # Injected at runtime\n\n@mcp.tool\ndef get_user_details(user_id: str = Depends(get_user_id)) -> str:\n    # user_id is injected by the server, not provided by the LLM\n    return f\"Details for {user_id}\"\n```\n\nSee [Custom Dependencies](/servers/context#custom-dependencies) for more details on dependency injection.\n\n## Return Values\n\n\nFastMCP tools can return data in two complementary formats: **traditional content blocks** (like text and images) and **structured outputs** (machine-readable JSON). When you add return type annotations, FastMCP automatically generates **output schemas** to validate the structured data and enables clients to deserialize results back to Python objects.\n\nUnderstanding how these three concepts work together:\n\n- **Return Values**: What your Python function returns (determines both content blocks and structured data)\n- **Structured Outputs**: JSON data sent alongside traditional content for machine processing  \n- **Output Schemas**: JSON Schema declarations that describe and validate the structured output format\n\nThe following sections explain each concept in detail.\n\n### Content Blocks\n\nFastMCP automatically converts tool return values into appropriate MCP content blocks:\n\n- **`str`**: Sent as `TextContent`\n- **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (within an `EmbeddedResource`)\n- **`fastmcp.utilities.types.Image`**: Sent as `ImageContent`\n- **`fastmcp.utilities.types.Audio`**: Sent as `AudioContent`\n- **`fastmcp.utilities.types.File`**: Sent as base64-encoded `EmbeddedResource`\n- **MCP SDK content blocks**: Sent as-is\n- **A list of any of the above**: Converts each item according to the above rules\n- **`None`**: Results in an empty response\n\n#### Media Helper Classes\n\nFastMCP provides helper classes for returning images, audio, and files. When you return one of these classes, either directly or as part of a list, FastMCP automatically converts it to the appropriate MCP content block. For example, if you return a `fastmcp.utilities.types.Image` object, FastMCP will convert it to an MCP `ImageContent` block with the correct MIME type and base64 encoding.\n\n```python\nfrom fastmcp.utilities.types import Image, Audio, File\n\n@mcp.tool\ndef get_chart() -> Image:\n    \"\"\"Generate a chart image.\"\"\"\n    return Image(path=\"chart.png\")\n\n@mcp.tool\ndef get_multiple_charts() -> list[Image]:\n    \"\"\"Return multiple charts.\"\"\"\n    return [Image(path=\"chart1.png\"), Image(path=\"chart2.png\")]\n```\n\n<Tip>\nHelper classes are only automatically converted to MCP content blocks when returned **directly** or as part of a **list**. For more complex containers like dicts, you can manually convert them to MCP types:\n\n```python\n# ✅ Automatic conversion\nreturn Image(path=\"chart.png\")\nreturn [Image(path=\"chart1.png\"), \"text content\"]\n\n# ❌ Will not be automatically converted\nreturn {\"image\": Image(path=\"chart.png\")}\n\n# ✅ Manual conversion for nested use\nreturn {\"image\": Image(path=\"chart.png\").to_image_content()}\n```\n</Tip>\n\nEach helper class accepts either `path=` or `data=` (mutually exclusive):\n- **`path`**: File path (string or Path object) - MIME type detected from extension\n- **`data`**: Raw bytes - requires `format=` parameter for MIME type\n- **`format`**: Optional format override (e.g., \"png\", \"wav\", \"pdf\")\n- **`name`**: Optional name for `File` when using `data=`\n- **`annotations`**: Optional MCP annotations for the content\n\n### Structured Output\n\n<VersionBadge version=\"2.10.0\" />\n\nThe 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content) structured content, which is a new way to return data from tools. Structured content is a JSON object that is sent alongside traditional content. FastMCP automatically creates structured outputs alongside traditional content when your tool returns data that has a JSON object representation. This provides machine-readable JSON data that clients can deserialize back to Python objects.\n\n**Automatic Structured Content Rules:**\n- **Object-like results** (`dict`, Pydantic models, dataclasses) → Always become structured content (even without output schema)  \n- **Non-object results** (`int`, `str`, `list`) → Only become structured content if there's an output schema to validate/serialize them\n- **All results** → Always become traditional content blocks for backward compatibility\n\n<Note>\nThis automatic behavior enables clients to receive machine-readable data alongside human-readable content without requiring explicit output schemas for object-like returns.\n</Note>\n\n#### Dictionaries and Objects\n\nWhen your tool returns a dictionary, dataclass, or Pydantic model, FastMCP automatically creates structured content from it. The structured content contains the actual object data, making it easy for clients to deserialize back to native objects.\n\n<CodeGroup>\n```python Tool Definition\n@mcp.tool\ndef get_user_data(user_id: str) -> dict:\n    \"\"\"Get user data.\"\"\"\n    return {\"name\": \"Alice\", \"age\": 30, \"active\": True}\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"{\\n  \\\"name\\\": \\\"Alice\\\",\\n  \\\"age\\\": 30,\\n  \\\"active\\\": true\\n}\"\n    }\n  ],\n  \"structuredContent\": {\n    \"name\": \"Alice\",\n    \"age\": 30,\n    \"active\": true\n  }\n}\n```\n</CodeGroup>\n\n#### Primitives and Collections\n\nWhen your tool returns a primitive type (int, str, bool) or a collection (list, set), FastMCP needs a return type annotation to generate structured content. The annotation tells FastMCP how to validate and serialize the result.\n\nWithout a type annotation, the tool only produces `content`:\n\n<CodeGroup>\n```python Tool Definition\n@mcp.tool\ndef calculate_sum(a: int, b: int):\n    \"\"\"Calculate sum without return annotation.\"\"\"\n    return a + b  # Returns 8\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"8\"\n    }\n  ]\n}\n```\n</CodeGroup>\n\nWhen you add a return annotation, such as `-> int`, FastMCP generates `structuredContent` by wrapping the primitive value in a `{\"result\": ...}` object, since JSON schemas require object-type roots for structured output:\n\n<CodeGroup>\n```python Tool Definition\n@mcp.tool\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Calculate sum with return annotation.\"\"\"\n    return a + b  # Returns 8\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"8\"\n    }\n  ],\n  \"structuredContent\": {\n    \"result\": 8\n  }\n}\n```\n</CodeGroup>\n\n#### Typed Models\n\nReturn type annotations work with any type that can be converted to a JSON schema. Dataclasses and Pydantic models are particularly useful because FastMCP extracts their field definitions to create detailed schemas.\n\n<CodeGroup>\n```python Tool Definition\nfrom dataclasses import dataclass\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@dataclass\nclass Person:\n    name: str\n    age: int\n    email: str\n\n@mcp.tool\ndef get_user_profile(user_id: str) -> Person:\n    \"\"\"Get a user's profile information.\"\"\"\n    return Person(\n        name=\"Alice\",\n        age=30,\n        email=\"alice@example.com\",\n    )\n```\n\n```json Generated Output Schema\n{\n  \"properties\": {\n    \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n    \"age\": {\"title\": \"Age\", \"type\": \"integer\"},\n    \"email\": {\"title\": \"Email\", \"type\": \"string\"}\n  },\n  \"required\": [\"name\", \"age\", \"email\"],\n  \"title\": \"Person\",\n  \"type\": \"object\"\n}\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"{\\\"name\\\": \\\"Alice\\\", \\\"age\\\": 30, \\\"email\\\": \\\"alice@example.com\\\"}\"\n    }\n  ],\n  \"structuredContent\": {\n    \"name\": \"Alice\",\n    \"age\": 30,\n    \"email\": \"alice@example.com\"\n  }\n}\n```\n</CodeGroup>\n\nThe `Person` dataclass becomes an output schema (second tab) that describes the expected format. When executed, clients receive the result (third tab) with both `content` and `structuredContent` fields.\n\n### Output Schemas\n\n<VersionBadge version=\"2.10.0\" />\n\nThe 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) output schemas, which are a new way to describe the expected output format of a tool. When an output schema is provided, the tool *must* return structured output that matches the schema.\n\nWhen you add return type annotations to your functions, FastMCP automatically generates JSON schemas that describe the expected output format. These schemas help MCP clients understand and validate the structured data they receive.\n\n#### Primitive Type Wrapping\n\nFor primitive return types (like `int`, `str`, `bool`), FastMCP automatically wraps the result under a `\"result\"` key to create valid structured output:\n\n<CodeGroup>\n```python Primitive Return Type\n@mcp.tool\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\n```json Generated Schema (Wrapped)\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"result\": {\"type\": \"integer\"}\n  },\n  \"x-fastmcp-wrap-result\": true\n}\n```\n\n```json Structured Output\n{\n  \"result\": 8\n}\n```\n</CodeGroup>\n\n#### Manual Schema Control\n\nYou can override the automatically generated schema by providing a custom `output_schema`:\n\n```python\n@mcp.tool(output_schema={\n    \"type\": \"object\", \n    \"properties\": {\n        \"data\": {\"type\": \"string\"},\n        \"metadata\": {\"type\": \"object\"}\n    }\n})\ndef custom_schema_tool() -> dict:\n    \"\"\"Tool with custom output schema.\"\"\"\n    return {\"data\": \"Hello\", \"metadata\": {\"version\": \"1.0\"}}\n```\n\nSchema generation works for most common types including basic types, collections, union types, Pydantic models, TypedDict structures, and dataclasses.\n\n<Warning>\n**Important Constraints**: \n- Output schemas must be object types (`\"type\": \"object\"`)\n- If you provide an output schema, your tool **must** return structured output that matches it\n- However, you can provide structured output without an output schema (using `ToolResult`)\n</Warning>\n\n### ToolResult and Metadata\n\nFor complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata.\n\n```python\nfrom fastmcp.tools.tool import ToolResult\nfrom mcp.types import TextContent\n\n@mcp.tool\ndef advanced_tool() -> ToolResult:\n    \"\"\"Tool with full control over output.\"\"\"\n    return ToolResult(\n        content=[TextContent(type=\"text\", text=\"Human-readable summary\")],\n        structured_content={\"data\": \"value\", \"count\": 42},\n        meta={\"execution_time_ms\": 145}\n    )\n```\n\n`ToolResult` accepts three fields:\n\n**`content`** - The traditional MCP content blocks that clients display to users. Can be a string (automatically converted to `TextContent`), a list of MCP content blocks, or any serializable value (converted to JSON string). At least one of `content` or `structured_content` must be provided.\n\n```python\n# Simple string\nToolResult(content=\"Hello, world!\")\n\n# List of content blocks\nToolResult(content=[\n    TextContent(type=\"text\", text=\"Result: 42\"),\n    ImageContent(type=\"image\", data=\"base64...\", mimeType=\"image/png\")\n])\n```\n\n**`structured_content`** - A dictionary containing structured data that matches your tool's output schema. This enables clients to programmatically process the results. If you provide `structured_content`, it must be a dictionary or `None`. If only `structured_content` is provided, it will also be used as `content` (converted to JSON string).\n\n```python\nToolResult(\n    content=\"Found 3 users\",\n    structured_content={\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}\n)\n```\n\n**`meta`** \n<VersionBadge version=\"2.13.1\" />\nRuntime metadata about the tool execution. Use this for performance metrics, debugging information, or any client-specific data that doesn't belong in the content or structured output.\n\n```python\nToolResult(\n    content=\"Analysis complete\",\n    structured_content={\"result\": \"positive\"},\n    meta={\n        \"execution_time_ms\": 145,\n        \"model_version\": \"2.1\",\n        \"confidence\": 0.95\n    }\n)\n```\n\n<Note>\nThe `meta` field in `ToolResult` is for runtime metadata about tool execution (e.g., execution time, performance metrics). This is separate from the `meta` parameter in `@mcp.tool(meta={...})`, which provides static metadata about the tool definition itself.\n</Note>\n\nWhen returning `ToolResult`, you have full control - FastMCP won't automatically wrap or transform your data. `ToolResult` can be returned with or without an output schema.\n\n### Custom Serialization\n\nWhen you need custom serialization (like YAML, Markdown tables, or specialized formats), return `ToolResult` with your serialized content. This makes the serialization explicit and visible in your tool's code:\n\n```python\nimport yaml\nfrom fastmcp import FastMCP\nfrom fastmcp.tools.tool import ToolResult\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef get_config() -> ToolResult:\n    \"\"\"Returns configuration as YAML.\"\"\"\n    data = {\"api_key\": \"abc123\", \"debug\": True, \"rate_limit\": 100}\n    return ToolResult(\n        content=yaml.dump(data, sort_keys=False),\n        structured_content=data\n    )\n```\n\n<Tip>\nFor reusable serialization across multiple tools, create a wrapper decorator that returns `ToolResult`. This lets you compose serializers with other behaviors (logging, validation, caching) and keeps the serialization visible at the tool definition. See [examples/custom_tool_serializer_decorator.py](https://github.com/PrefectHQ/fastmcp/blob/main/examples/custom_tool_serializer_decorator.py) for a complete implementation.\n</Tip>\n\n## Error Handling\n\n<VersionBadge version=\"2.4.1\" />\n\nIf your tool encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ToolError`.\n\nBy default, all exceptions (including their details) are logged and converted into an MCP error response to be sent back to the client LLM. This helps the LLM understand failures and react appropriately.\n\nIf you want to mask internal error details for security reasons, you can:\n\n1. Use the `mask_error_details=True` parameter when creating your `FastMCP` instance:\n```python\nmcp = FastMCP(name=\"SecureServer\", mask_error_details=True)\n```\n\n2. Or use `ToolError` to explicitly control what error information is sent to clients:\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import ToolError\n\n@mcp.tool\ndef divide(a: float, b: float) -> float:\n    \"\"\"Divide a by b.\"\"\"\n\n    if b == 0:\n        # Error messages from ToolError are always sent to clients,\n        # regardless of mask_error_details setting\n        raise ToolError(\"Division by zero is not allowed.\")\n    \n    # If mask_error_details=True, this message would be masked\n    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):\n        raise TypeError(\"Both arguments must be numbers.\")\n        \n    return a / b\n```\n\nWhen `mask_error_details=True`, only error messages from `ToolError` will include details, other exceptions will be converted to a generic message.\n\n## Timeouts\n\n<VersionBadge version=\"3.0.0\" />\n\nTools can specify a `timeout` parameter to limit how long execution can take. When the timeout is exceeded, the client receives an MCP error and the tool stops processing. This protects your server from unexpectedly slow operations that could block resources or leave clients waiting indefinitely.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.tool(timeout=30.0)\nasync def fetch_data(url: str) -> dict:\n    \"\"\"Fetch data with a 30-second timeout.\"\"\"\n    # If this takes longer than 30 seconds,\n    # the client receives an MCP error\n    ...\n```\n\nTimeouts are specified in seconds as a float. When a tool exceeds its timeout, FastMCP returns an MCP error with code `-32000` and a message indicating which tool timed out and how long it ran. Both sync and async tools support timeouts—sync functions run in thread pools, so the timeout applies to the entire operation regardless of execution model.\n\n<Note>\nTools must explicitly opt-in to timeouts. There is no server-level default timeout setting.\n</Note>\n\n### Timeouts vs Background Tasks\n\nTimeouts apply to **foreground execution**—when a tool runs directly in response to a client request. They protect your server from tools that unexpectedly hang due to network issues, resource contention, or other transient problems.\n\n<Warning>\nThe `timeout` parameter does **not** apply to background tasks. When a tool runs as a background task (`task=True`), execution happens in a Docket worker where the FastMCP timeout is not enforced.\n\nFor task timeouts, use Docket's `Timeout` dependency directly in your function signature:\n\n```python\nfrom datetime import timedelta\nfrom docket import Timeout\n\n@mcp.tool(task=True)\nasync def long_running_task(\n    data: str,\n    timeout: Timeout = Timeout(timedelta(minutes=10))\n) -> str:\n    \"\"\"Task with a 10-minute timeout enforced by Docket.\"\"\"\n    ...\n```\n\nSee the [Docket documentation](https://chrisguidry.github.io/docket/dependencies/#task-timeouts) for more on task timeouts and retries.\n</Warning>\n\nWhen a tool times out, FastMCP logs a warning suggesting task mode. For operations you know will be long-running, use `task=True` instead—background tasks offload work to distributed workers and let clients poll for progress.\n\n## Component Visibility\n\n<VersionBadge version=\"3.0.0\" />\n\nYou can control which tools are enabled for clients using server-level enabled control. Disabled tools don't appear in `list_tools` and can't be called.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(tags={\"admin\"})\ndef admin_action() -> str:\n    \"\"\"Admin-only action.\"\"\"\n    return \"Done\"\n\n@mcp.tool(tags={\"public\"})\ndef public_action() -> str:\n    \"\"\"Public action.\"\"\"\n    return \"Done\"\n\n# Disable specific tools by key\nmcp.disable(keys={\"tool:admin_action\"})\n\n# Disable tools by tag\nmcp.disable(tags={\"admin\"})\n\n# Or use allowlist mode - only enable tools with specific tags\nmcp.enable(tags={\"public\"}, only=True)\n```\n\nSee [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control.\n\n## MCP Annotations\n\n<VersionBadge version=\"2.2.7\" />\n\nFastMCP allows you to add specialized metadata to your tools through annotations. These annotations communicate how tools behave to client applications without consuming token context in LLM prompts.\n\nAnnotations serve several purposes in client applications:\n- Adding user-friendly titles for display purposes\n- Indicating whether tools modify data or systems\n- Describing the safety profile of tools (destructive vs. non-destructive)\n- Signaling if tools interact with external systems\n\nYou can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:\n\n```python\n@mcp.tool(\n    annotations={\n        \"title\": \"Calculate Sum\",\n        \"readOnlyHint\": True,\n        \"openWorldHint\": False\n    }\n)\ndef calculate_sum(a: float, b: float) -> float:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\nFastMCP supports these standard annotations:\n\n| Annotation | Type | Default | Purpose |\n| :--------- | :--- | :------ | :------ |\n| `title` | string | - | Display name for user interfaces |\n| `readOnlyHint` | boolean | false | Indicates if the tool only reads without making changes |\n| `destructiveHint` | boolean | true | For non-readonly tools, signals if changes are destructive |\n| `idempotentHint` | boolean | false | Indicates if repeated identical calls have the same effect as a single call |\n| `openWorldHint` | boolean | true | Specifies if the tool interacts with external systems |\n\nRemember that annotations help make better user experiences but should be treated as advisory hints. They help client applications present appropriate UI elements and safety controls, but won't enforce security boundaries on their own. Always focus on making your annotations accurately represent what your tool actually does.\n\n### Using Annotation Hints\n\nMCP clients like Claude and ChatGPT use annotation hints to determine when to skip confirmation prompts and how to present tools to users. The most commonly used hint is `readOnlyHint`, which signals that a tool only reads data without making changes.\n\n**Read-only tools** improve user experience by:\n- Skipping confirmation prompts for safe operations\n- Allowing broader access without security concerns\n- Enabling more aggressive batching and caching\n\nMark a tool as read-only when it retrieves data, performs calculations, or checks status without modifying state:\n\n```python\nfrom fastmcp import FastMCP\nfrom mcp.types import ToolAnnotations\n\nmcp = FastMCP(\"Data Server\")\n\n@mcp.tool(annotations={\"readOnlyHint\": True})\ndef get_user(user_id: str) -> dict:\n    \"\"\"Retrieve user information by ID.\"\"\"\n    return {\"id\": user_id, \"name\": \"Alice\"}\n\n@mcp.tool(\n    annotations=ToolAnnotations(\n        readOnlyHint=True,\n        idempotentHint=True,  # Same result for repeated calls\n        openWorldHint=False   # Only internal data\n    )\n)\ndef search_products(query: str) -> list[dict]:\n    \"\"\"Search the product catalog.\"\"\"\n    return [{\"id\": 1, \"name\": \"Widget\", \"price\": 29.99}]\n\n# Write operations - no readOnlyHint\n@mcp.tool()\ndef update_user(user_id: str, name: str) -> dict:\n    \"\"\"Update user information.\"\"\"\n    return {\"id\": user_id, \"name\": name, \"updated\": True}\n\n@mcp.tool(annotations={\"destructiveHint\": True})\ndef delete_user(user_id: str) -> dict:\n    \"\"\"Permanently delete a user account.\"\"\"\n    return {\"deleted\": user_id}\n```\n\nFor tools that write to databases, send notifications, create/update/delete resources, or trigger workflows, omit `readOnlyHint` or set it to `False`. Use `destructiveHint=True` for operations that cannot be undone.\n\nClient-specific behavior:\n- **ChatGPT**: Skips confirmation prompts for read-only tools in Chat mode (see [ChatGPT integration](/integrations/chatgpt))\n- **Claude**: Uses hints to understand tool safety profiles and make better execution decisions\n\n## Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends `notifications/tools/list_changed` notifications to connected clients when tools are added, removed, enabled, or disabled. This allows clients to stay up-to-date with the current tool set without manually polling for changes.\n\n```python\n@mcp.tool\ndef example_tool() -> str:\n    return \"Hello!\"\n\n# These operations trigger notifications:\nmcp.add_tool(example_tool)              # Sends tools/list_changed notification\nmcp.disable(keys={\"tool:example_tool\"}) # Sends tools/list_changed notification\nmcp.enable(keys={\"tool:example_tool\"})  # Sends tools/list_changed notification\nmcp.local_provider.remove_tool(\"example_tool\")  # Sends tools/list_changed notification\n```\n\nNotifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.\n\nClients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their tool lists or update their interfaces.\n\n## Accessing the MCP Context\n\nTools can access MCP features like logging, reading resources, or reporting progress through the `Context` object. To use it, add a parameter to your tool function with the type hint `Context`.\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"ContextDemo\")\n\n@mcp.tool\nasync def process_data(data_uri: str, ctx: Context) -> dict:\n    \"\"\"Process data from a resource with progress reporting.\"\"\"\n    await ctx.info(f\"Processing data from {data_uri}\")\n    \n    # Read a resource\n    resource = await ctx.read_resource(data_uri)\n    data = resource[0].content if resource else \"\"\n    \n    # Report progress\n    await ctx.report_progress(progress=50, total=100)\n    \n    # Example request to the client's LLM for help\n    summary = await ctx.sample(f\"Summarize this in 10 words: {data[:200]}\")\n    \n    await ctx.report_progress(progress=100, total=100)\n    return {\n        \"length\": len(data),\n        \"summary\": summary.text\n    }\n```\n\nThe Context object provides access to:\n\n- **Logging**: `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`\n- **Progress Reporting**: `ctx.report_progress(progress, total)`\n- **Resource Access**: `ctx.read_resource(uri)`\n- **LLM Sampling**: `ctx.sample(...)`\n- **Request Information**: `ctx.request_id`, `ctx.client_id`\n\nFor full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).\n\n## Server Behavior\n\n### Duplicate Tools\n\n<VersionBadge version=\"2.1.0\" />\n\nYou can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\n    name=\"StrictServer\",\n    # Configure behavior for duplicate tool names\n    on_duplicate_tools=\"error\"\n)\n\n@mcp.tool\ndef my_tool(): return \"Version 1\"\n\n# This will now raise a ValueError because 'my_tool' already exists\n# and on_duplicate_tools is set to \"error\".\n# @mcp.tool\n# def my_tool(): return \"Version 2\"\n```\n\nThe duplicate behavior options are:\n\n-   `\"warn\"` (default): Logs a warning and the new tool replaces the old one.\n-   `\"error\"`: Raises a `ValueError`, preventing the duplicate registration.\n-   `\"replace\"`: Silently replaces the existing tool with the new one.\n-   `\"ignore\"`: Keeps the original tool and ignores the new registration attempt.\n\n### Removing Tools\n\n<VersionBadge version=\"2.3.4\" />\n\nYou can dynamically remove tools from a server through its [local provider](/servers/providers/local):\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DynamicToolServer\")\n\n@mcp.tool\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\nmcp.local_provider.remove_tool(\"calculate_sum\")\n```\n\n## Versioning\n\n<VersionBadge version=\"3.0.0\" />\n\nTools support versioning, allowing you to maintain multiple implementations under the same name while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.\n"
  },
  {
    "path": "docs/servers/transforms/code-mode.mdx",
    "content": "---\ntitle: Code Mode\nsidebarTitle: Code Mode\ndescription: Let LLMs write Python to orchestrate tools in a sandbox\nicon: flask\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.1.0\" />\n\n<Warning>\nCodeMode is experimental. The core interface is stable, but the specific discovery tools and their parameters may evolve as we learn more about what works best in practice.\n</Warning>\n\nStandard MCP tool usage has two scaling problems. First, every tool in the catalog is loaded into the LLM's context upfront — with hundreds of tools, that's tens of thousands of tokens spent before the LLM even reads the user's request. Second, every tool call is a round-trip: the LLM calls a tool, the result passes back through the context window, the LLM reasons about it, calls another tool, and so on. Intermediate results that only exist to feed the next step still burn tokens flowing through the model.\n\nCodeMode solves both problems. Instead of seeing your entire tool catalog, the LLM gets meta-tools for discovering what's available and for writing and executing code that calls the tools it needs. It discovers on demand, writes a script that chains tool calls in a sandbox, and gets back only the final answer.\n\nThe approach was introduced by Cloudflare in [Code Mode](https://blog.cloudflare.com/code-mode/) and explored further by Anthropic in [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp).\n\n## Getting Started\n\n<Tip>\nCodeMode requires the `code-mode` extra for sandbox support. Install it with `pip install \"fastmcp[code-mode]\"`.\n</Tip>\n\nYou take a normal server with normally registered tools and add a `CodeMode` transform. The transform wraps your existing tools in the code mode machinery — your tool functions don't change at all:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\n\nmcp = FastMCP(\"Server\", transforms=[CodeMode()])\n\n@mcp.tool\ndef add(x: int, y: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return x + y\n\n@mcp.tool\ndef multiply(x: int, y: int) -> int:\n    \"\"\"Multiply two numbers.\"\"\"\n    return x * y\n```\n\nClients connecting to this server no longer see `add` and `multiply` directly. Instead, they see the meta-tools that CodeMode provides — tools for discovering what's available and executing code against it. The original tools are still there, but they're accessed through the CodeMode layer.\n\n## Discovery\n\nBefore the LLM can write code that calls your tools, it needs to know what tools exist and how to call them. This is the **discovery** process — the LLM uses meta-tools to learn about your tool catalog, then writes code against what it finds.\n\nThe fundamental tradeoff is **tokens vs. round-trips**. Each discovery step is an LLM round-trip: the model calls a tool, waits for the response, reasons about it, then decides what to do next. More steps mean less wasted context (each step is targeted) but more latency and API calls. Fewer steps mean the LLM gets information upfront but pays for detail it might not need.\n\nBy default, CodeMode gives the LLM three tools — `search`, `get_schema`, and `execute` — creating a three-stage discovery flow:\n\n<Steps>\n<Step title=\"Search for tools\">\nFirst, the LLM uses the `search` meta-tool to find tools by keyword.\n\nFor example, it might do `search(query=\"math numbers\")` and receive the following response:\n\n```\n- add: Add two numbers.\n- multiply: Multiply two numbers.\n```\n\nThis lets the LLM know which tools are available and what they do, significantly reducing the surface area it needs to consider.\n\n</Step>\n<Step title=\"Get parameter details for the tools\">\nNext, the LLM calls `get_schema` to get parameter details for the tools it found in the previous step.\n\nFor example, it might do `get_schema(tools=[\"add\", \"multiply\"])` and receive the following response:\n\n```\n### add\n\nAdd two numbers.\n\n**Parameters**\n- `x` (integer, required)\n- `y` (integer, required)\n\n### multiply\n\nMultiply two numbers.\n\n**Parameters**\n- `x` (integer, required)\n- `y` (integer, required)\n```\n\nNow the LLM knows the parameters for the tools it found, and can write code that chains the tool calls. If it needed more detail, it could have called `get_schema` with `detail=\"full\"` to get the complete JSON schema.\n\n</Step>\n<Step title=\"Write and execute code that chains the tool calls\">\nFinally, the LLM writes and executes code that chains the tool calls in a Python sandbox. Inside the sandbox, `call_tool(name, params)` is the only function available. The LLM uses this to compose tools into a workflow and return a final result.\n\nFor example, it might write the following code and call the `execute` tool with it:\n\n```python\na = await call_tool(\"add\", {\"x\": 3, \"y\": 4})\nb = await call_tool(\"multiply\", {\"x\": a, \"y\": 2})\nreturn b\n```\n\nThe result is returned to the LLM.\n</Step>\n</Steps>\n\nThis three-stage flow works well for most servers — each step pulls in only the information needed for the next one, keeping context usage minimal. But CodeMode's discovery surface is fully configurable. The sections below explain each built-in discovery tool and how to combine them into different patterns.\n\n## Discovery Tools\n\nCodeMode ships with four built-in discovery tools: `Search`, `GetSchemas`, `GetTags`, and `ListTools`. By default, only `Search` and `GetSchemas` are enabled. Each tool supports a `default_detail` parameter that sets the default verbosity level, and the LLM can override the detail level on any individual call.\n\n### Detail Levels\n\n`Search` and `GetSchemas` share the same three detail levels, so the same `detail` value produces the same output format regardless of which tool the LLM calls:\n\n| Level | Output | Token cost |\n|---|---|---|\n| `\"brief\"` | Tool names and one-line descriptions | Cheapest — good for scanning |\n| `\"detailed\"` | Compact markdown with parameter names, types, and required markers | Medium — often enough to write code |\n| `\"full\"` | Complete JSON schema | Most expensive — everything |\n\n`Search` defaults to `\"brief\"` and `GetSchemas` defaults to `\"detailed\"`.\n\n### Search\n\n`Search` finds tools by natural-language query using BM25 ranking. At its default `\"brief\"` detail, results include just tool names and descriptions — enough to decide which tools are worth inspecting further. The LLM can request `\"detailed\"` to get parameter schemas inline, or `\"full\"` for the complete JSON.\n\nSearch results include an annotation like `\"2 of 10 tools:\"` when the result set is smaller than the full catalog, so the LLM knows there are more tools to discover with different queries.\n\nYou can cap result count with `default_limit`. The LLM can also override the limit per call. This is useful for large catalogs where you want to keep search results focused:\n\n```python\nSearch(default_limit=5)  # return at most 5 results per search\n```\n\nIf your tools use [tags](/servers/tools#tags), Search also accepts a `tags` parameter so the LLM can narrow results to specific categories before searching.\n\n### GetSchemas\n\n`GetSchemas` returns parameter details for specific tools by name. At its default `\"detailed\"` level, it renders compact markdown with parameter names, types, and required markers. At `\"full\"`, it returns the complete JSON schema — useful when tools have deeply nested parameters that the compact format doesn't capture.\n\n### GetTags\n\n`GetTags` lets the LLM browse tools by category using [tag](/servers/tools#tags) metadata. At brief detail, the LLM sees tag names with counts. At full detail, it sees tools listed under each tag:\n\n```\n- math (3 tools)\n- text (2 tools)\n- untagged (1 tool)\n```\n\n`GetTags` isn't included in the defaults — add it when browsing by category would help the LLM orient itself in a large catalog. The LLM can browse tags first, then pass specific tags into Search to narrow results.\n\n### ListTools\n\n`ListTools` dumps the entire catalog at whatever detail level the LLM requests. It supports the same three detail levels as `Search` and `GetSchemas`, defaulting to `\"brief\"`.\n\n`ListTools` isn't included in the defaults — for large catalogs, search-based discovery is more token-efficient. But for smaller catalogs (under ~20 tools), letting the LLM see everything upfront can be faster than multiple search round-trips:\n\n```python\nfrom fastmcp.experimental.transforms.code_mode import CodeMode, ListTools, GetSchemas\n\ncode_mode = CodeMode(\n    discovery_tools=[ListTools(), GetSchemas()],\n)\n```\n\n## Discovery Patterns\n\nThe right discovery configuration depends on your server — how many tools you have and how complex their parameters are. It may be tempting to minimize round-trips by collapsing everything into fewer steps, but for the complex servers that benefit most from CodeMode, our experience is that staged discovery leads to better results. Flooding the LLM with detailed schemas for tools it doesn't end up using can hurt more than the extra round-trip costs. Each pattern below is a complete, copyable configuration.\n\n### Three-Stage\n\nThe default. The LLM searches for candidates, inspects schemas for the ones it wants, then writes code. Best for **large or complex tool sets** where you want to minimize context usage — the LLM only pays for schemas it actually needs.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\n\nmcp = FastMCP(\"Server\", transforms=[CodeMode()])\n```\n\nIf your tools use [tags](/servers/tools#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\nfrom fastmcp.experimental.transforms.code_mode import GetTags, Search, GetSchemas\n\ncode_mode = CodeMode(\n    discovery_tools=[GetTags(), Search(), GetSchemas()],\n)\n\nmcp = FastMCP(\"Server\", transforms=[code_mode])\n```\n\n### Two-Stage\n\nSearch returns parameter schemas inline, so the LLM can go straight from search to execute. Best for **smaller catalogs** where the extra tokens per search result are a reasonable price for one fewer round-trip.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\nfrom fastmcp.experimental.transforms.code_mode import Search, GetSchemas\n\ncode_mode = CodeMode(\n    discovery_tools=[Search(default_detail=\"detailed\"), GetSchemas()],\n)\n\nmcp = FastMCP(\"Server\", transforms=[code_mode])\n```\n\n`GetSchemas` is still available as a fallback — the LLM can call it with `detail=\"full\"` if it encounters a tool with complex nested parameters where the compact markdown isn't enough.\n\n### Single-Stage\n\nSkip discovery entirely and bake tool instructions into the execute tool's description. Best for **very simple servers** where the LLM already knows what tools are available — maybe there are only a few, or they're described in the system prompt.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\n\ncode_mode = CodeMode(\n    discovery_tools=[],\n    execute_description=(\n        \"Available tools:\\n\"\n        \"- add(x: int, y: int) -> int: Add two numbers\\n\"\n        \"- multiply(x: int, y: int) -> int: Multiply two numbers\\n\\n\"\n        \"Write Python using `await call_tool(name, params)` and `return` the result.\"\n    ),\n)\n\nmcp = FastMCP(\"Server\", transforms=[code_mode])\n```\n\n## Custom Discovery Tools\n\nDiscovery tools are composable — you can mix the built-ins with your own. Each discovery tool is a callable that receives catalog access and returns a `Tool`. The catalog accessor is a function (not the catalog itself) because the catalog is request-scoped — different users may see different tools based on auth.\n\nHere's a minimal example:\n\n```python\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\nfrom fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas\nfrom fastmcp.server.context import Context\nfrom fastmcp.tools.tool import Tool\n\ndef list_all_tools(get_catalog: GetToolCatalog) -> Tool:\n    async def list_tools(ctx: Context) -> str:\n        \"\"\"List all available tool names.\"\"\"\n        tools = await get_catalog(ctx)\n        return \", \".join(t.name for t in tools)\n\n    return Tool.from_function(fn=list_tools, name=\"list_tools\")\n\ncode_mode = CodeMode(discovery_tools=[list_all_tools, GetSchemas()])\n```\n\nThe LLM sees the docstring of each discovery tool's inner function as its description — that's how it learns what each tool does and when to use it. Write docstrings that explain what the tool returns and when the LLM should call it.\n\nDiscovery tools and the execute tool can also have custom names:\n\n```python\nfrom fastmcp.experimental.transforms.code_mode import Search, GetSchemas\n\ncode_mode = CodeMode(\n    discovery_tools=[\n        Search(name=\"find_tools\"),\n        GetSchemas(name=\"describe\"),\n    ],\n    execute_tool_name=\"run_workflow\",\n)\n\nmcp = FastMCP(\"Server\", transforms=[code_mode])\n```\n\n## Sandbox Configuration\n\n### Resource Limits\n\nThe default `MontySandboxProvider` can enforce execution limits — timeouts, memory caps, recursion depth, and more. Without limits, LLM-generated scripts can run indefinitely.\n\n```python\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\nfrom fastmcp.experimental.transforms.code_mode import MontySandboxProvider\n\nsandbox = MontySandboxProvider(\n    limits={\"max_duration_secs\": 10, \"max_memory\": 50_000_000},\n)\n\nmcp = FastMCP(\"Server\", transforms=[CodeMode(sandbox_provider=sandbox)])\n```\n\nAll keys are optional — omit any to leave that dimension uncapped:\n\n| Key | Type | Description |\n|---|---|---|\n| `max_duration_secs` | `float` | Maximum wall-clock execution time |\n| `max_memory` | `int` | Memory ceiling in bytes |\n| `max_allocations` | `int` | Cap on total object allocations |\n| `max_recursion_depth` | `int` | Maximum recursion depth |\n| `gc_interval` | `int` | Garbage collection frequency |\n\n### Custom Sandbox Providers\n\nYou can replace the default sandbox with any object implementing the `SandboxProvider` protocol:\n\n```python\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\nfrom fastmcp.experimental.transforms.code_mode import SandboxProvider\n\nclass RemoteSandboxProvider:\n    async def run(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        external_functions: dict[str, Callable[..., Any]] | None = None,\n    ) -> Any:\n        # Send code to your remote sandbox runtime\n        ...\n\nmcp = FastMCP(\n    \"Server\",\n    transforms=[CodeMode(sandbox_provider=RemoteSandboxProvider())],\n)\n```\n\nThe `external_functions` dict contains async callables injected into the sandbox scope — `execute` uses this to provide `call_tool`.\n"
  },
  {
    "path": "docs/servers/transforms/namespace.mdx",
    "content": "---\ntitle: Namespace Transform\nsidebarTitle: Namespace\ndescription: Prefix component names to prevent conflicts\nicon: tag\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nThe `Namespace` transform prefixes all component names, preventing conflicts when composing multiple servers.\n\nTools and prompts receive an underscore-separated prefix. Resources and templates receive a path-segment prefix in their URIs.\n\n| Component | Original | With `Namespace(\"api\")` |\n|-----------|----------|-------------------------|\n| Tool | `my_tool` | `api_my_tool` |\n| Prompt | `my_prompt` | `api_my_prompt` |\n| Resource | `data://info` | `data://api/info` |\n| Template | `data://{id}` | `data://api/{id}` |\n\nThe most common use is through the `mount()` method's `namespace` parameter.\n\n```python\nfrom fastmcp import FastMCP\n\nweather = FastMCP(\"Weather\")\ncalendar = FastMCP(\"Calendar\")\n\n@weather.tool\ndef get_data() -> str:\n    return \"Weather data\"\n\n@calendar.tool\ndef get_data() -> str:\n    return \"Calendar data\"\n\n# Without namespacing, these would conflict\nmain = FastMCP(\"Main\")\nmain.mount(weather, namespace=\"weather\")\nmain.mount(calendar, namespace=\"calendar\")\n\n# Clients see: weather_get_data, calendar_get_data\n```\n\nYou can also apply namespacing directly using the `Namespace` transform.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import Namespace\n\nmcp = FastMCP(\"Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# Namespace all components\nmcp.add_transform(Namespace(\"api\"))\n\n# Tool is now: api_greet\n```\n"
  },
  {
    "path": "docs/servers/transforms/namespacing.mdx",
    "content": "---\ntitle: Namespacing\nsidebarTitle: Namespacing\ndescription: Namespace and transform components with transforms\nicon: wand-magic-sparkles\nredirect: /servers/transforms/transforms\n---\n\nThis page has moved to [Transforms](/servers/transforms/transforms).\n"
  },
  {
    "path": "docs/servers/transforms/prompts-as-tools.mdx",
    "content": "---\ntitle: Prompts as Tools\nsidebarTitle: Prompts as Tools\ndescription: Expose prompts to tool-only clients\nicon: message-lines\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nSome MCP clients only support tools. They cannot list or get prompts directly because they lack prompt protocol support. The `PromptsAsTools` transform bridges this gap by generating tools that provide access to your server's prompts.\n\nWhen you add `PromptsAsTools` to a server, it creates two tools that clients can call instead of using the prompt protocol:\n\n- **`list_prompts`** returns JSON describing all available prompts and their arguments\n- **`get_prompt`** renders a specific prompt with provided arguments\n\nThis means any client that can call tools can now access prompts, even if the client has no native prompt support.\n\n## Basic Usage\n\nPass your FastMCP server to `PromptsAsTools` when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to prompt operations automatically, exactly as it would for direct `prompts/get` calls.\n\n<Note>\n`PromptsAsTools` (and `ResourcesAsTools`) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server's middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of prompts, create a dedicated FastMCP server for those prompts and apply the transform there.\n</Note>\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import PromptsAsTools\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.prompt\ndef analyze_code(code: str, language: str = \"python\") -> str:\n    \"\"\"Analyze code for potential issues.\"\"\"\n    return f\"Analyze this {language} code:\\n{code}\"\n\n@mcp.prompt\ndef explain_concept(concept: str) -> str:\n    \"\"\"Explain a programming concept.\"\"\"\n    return f\"Explain: {concept}\"\n\n# Add the transform - creates list_prompts and get_prompt tools\nmcp.add_transform(PromptsAsTools(mcp))\n```\n\nClients now see three items: whatever tools you defined directly, plus `list_prompts` and `get_prompt`.\n\n## Listing Prompts\n\nThe `list_prompts` tool returns JSON with metadata for each prompt, including its arguments.\n\n```python\nresult = await client.call_tool(\"list_prompts\", {})\nprompts = json.loads(result.data)\n# [\n#   {\n#     \"name\": \"analyze_code\",\n#     \"description\": \"Analyze code for potential issues.\",\n#     \"arguments\": [\n#       {\"name\": \"code\", \"description\": null, \"required\": true},\n#       {\"name\": \"language\", \"description\": null, \"required\": false}\n#     ]\n#   },\n#   {\n#     \"name\": \"explain_concept\",\n#     \"description\": \"Explain a programming concept.\",\n#     \"arguments\": [\n#       {\"name\": \"concept\", \"description\": null, \"required\": true}\n#     ]\n#   }\n#]\n```\n\nEach argument includes:\n- `name`: The argument name\n- `description`: Optional description from type hints or docstrings\n- `required`: Whether the argument must be provided\n\n## Getting Prompts\n\nThe `get_prompt` tool accepts a prompt name and optional arguments dict. It returns the rendered prompt as JSON with a messages array.\n\n```python\n# Prompt with required and optional arguments\nresult = await client.call_tool(\n    \"get_prompt\",\n    {\n        \"name\": \"analyze_code\",\n        \"arguments\": {\n            \"code\": \"x = 1\\nprint(x)\",\n            \"language\": \"python\"\n        }\n    }\n)\n\nresponse = json.loads(result.data)\n# {\n#   \"messages\": [\n#     {\n#       \"role\": \"user\",\n#       \"content\": \"Analyze this python code:\\nx = 1\\nprint(x)\"\n#     }\n#   ]\n# }\n```\n\nIf a prompt has no arguments, you can omit the `arguments` field or pass an empty dict:\n\n```python\nresult = await client.call_tool(\n    \"get_prompt\",\n    {\"name\": \"simple_prompt\"}\n)\n```\n\n## Message Format\n\nRendered prompts return a messages array following the standard MCP format. Each message includes:\n- `role`: The message role (\"user\" or \"assistant\")\n- `content`: The message text content\n\nMulti-message prompts are supported - the array will contain all messages in order.\n\n## Binary Content\n\nUnlike resources, prompts always return text content. There is no binary encoding needed.\n"
  },
  {
    "path": "docs/servers/transforms/resources-as-tools.mdx",
    "content": "---\ntitle: Resources as Tools\nsidebarTitle: Resources as Tools\ndescription: Expose resources to tool-only clients\nicon: toolbox\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nSome MCP clients only support tools. They cannot list or read resources directly because they lack resource protocol support. The `ResourcesAsTools` transform bridges this gap by generating tools that provide access to your server's resources.\n\nWhen you add `ResourcesAsTools` to a server, it creates two tools that clients can call instead of using the resource protocol:\n\n- **`list_resources`** returns JSON describing all available resources and templates\n- **`read_resource`** reads a specific resource by URI\n\nThis means any client that can call tools can now access resources, even if the client has no native resource support.\n\n## Basic Usage\n\nPass your FastMCP server to `ResourcesAsTools` when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to resource operations automatically, exactly as it would for direct `resources/read` calls.\n\n<Note>\n`ResourcesAsTools` (and `PromptsAsTools`) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server's middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of resources, create a dedicated FastMCP server for those resources and apply the transform there.\n</Note>\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import ResourcesAsTools\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.resource(\"config://app\")\ndef app_config() -> str:\n    \"\"\"Application configuration.\"\"\"\n    return '{\"app_name\": \"My App\", \"version\": \"1.0.0\"}'\n\n@mcp.resource(\"user://{user_id}/profile\")\ndef user_profile(user_id: str) -> str:\n    \"\"\"Get a user's profile by ID.\"\"\"\n    return f'{{\"user_id\": \"{user_id}\", \"name\": \"User {user_id}\"}}'\n\n# Add the transform - creates list_resources and read_resource tools\nmcp.add_transform(ResourcesAsTools(mcp))\n```\n\nClients now see three tools: whatever tools you defined directly, plus `list_resources` and `read_resource`.\n\nBoth generated tools are annotated with `readOnlyHint=True`, since they only read data. Clients that respect tool annotations (like Cursor) can use this to auto-confirm these tool calls without prompting the user.\n\n## Static Resources vs Templates\n\nResources come in two forms, and the `list_resources` tool distinguishes between them in its JSON output.\n\nStatic resources have fixed URIs. They represent concrete data that exists at a known location. In the listing output, static resources include a `uri` field containing the exact URI to request.\n\nResource templates have parameterized URIs with placeholders like `{user_id}`. They represent patterns for accessing dynamic data. In the listing output, templates include a `uri_template` field showing the pattern with its placeholders.\n\nWhen a client calls `list_resources`, it receives JSON like this:\n\n```json\n[\n  {\n    \"uri\": \"config://app\",\n    \"name\": \"app_config\",\n    \"description\": \"Application configuration.\",\n    \"mime_type\": \"text/plain\"\n  },\n  {\n    \"uri_template\": \"user://{user_id}/profile\",\n    \"name\": \"user_profile\",\n    \"description\": \"Get a user's profile by ID.\"\n  }\n]\n```\n\nThe client can distinguish resource types by checking which field is present: `uri` for static resources, `uri_template` for templates.\n\n## Reading Resources\n\nThe `read_resource` tool accepts a single `uri` argument. For static resources, pass the exact URI. For templates, fill in the placeholders with actual values.\n\n```python\n# Reading a static resource\nresult = await client.call_tool(\"read_resource\", {\"uri\": \"config://app\"})\nprint(result.data)  # '{\"app_name\": \"My App\", \"version\": \"1.0.0\"}'\n\n# Reading a templated resource - fill in {user_id} with an actual ID\nresult = await client.call_tool(\"read_resource\", {\"uri\": \"user://42/profile\"})\nprint(result.data)  # '{\"user_id\": \"42\", \"name\": \"User 42\"}'\n```\n\nThe transform handles template matching automatically. When you request `user://42/profile`, it matches against the `user://{user_id}/profile` template, extracts `user_id=42`, and calls your resource function with that parameter.\n\n## Binary Content\n\nResources that return binary data (like images or files) are automatically base64-encoded when read through the `read_resource` tool. This ensures binary content can be transmitted as a string in the tool response.\n\n```python\n@mcp.resource(\"data://binary\", mime_type=\"application/octet-stream\")\ndef binary_data() -> bytes:\n    return b\"\\x00\\x01\\x02\\x03\"\n\n# Client receives base64-encoded string\nresult = await client.call_tool(\"read_resource\", {\"uri\": \"data://binary\"})\ndecoded = base64.b64decode(result.data)  # b'\\x00\\x01\\x02\\x03'\n```\n\n"
  },
  {
    "path": "docs/servers/transforms/tool-search.mdx",
    "content": "---\ntitle: Tool Search\nsidebarTitle: Tool Search\ndescription: Replace large tool catalogs with on-demand search\nicon: magnifying-glass\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.1.0\" />\n\nWhen a server exposes hundreds or thousands of tools, sending the full catalog to an LLM wastes tokens and degrades tool selection accuracy. Search transforms solve this by replacing the tool listing with a search interface — the LLM discovers tools on demand instead of receiving everything upfront.\n\n## How It Works\n\nWhen you add a search transform, `list_tools()` returns just two synthetic tools instead of the full catalog:\n\n- **`search_tools`** finds tools matching a query and returns their full definitions\n- **`call_tool`** executes a discovered tool by name\n\nThe original tools are still callable. They're hidden from the listing but remain fully functional — the search transform controls *discovery*, not *access*.\n\nBoth synthetic tools search across tool names, descriptions, parameter names, and parameter descriptions. A search for `\"email\"` would match a tool named `send_email`, a tool with \"email\" in its description, or a tool with an `email_address` parameter.\n\nSearch results are returned in the same JSON format as `list_tools`, including the full input schema, so the LLM can construct valid calls immediately without a second round-trip.\n\n## Search Strategies\n\nFastMCP provides two search transforms. They share the same interface — two synthetic tools, same configuration options — but differ in how they match queries to tools.\n\n### Regex Search\n\n`RegexSearchTransform` matches tools against a regex pattern using case-insensitive `re.search`. It has zero overhead and no index to build, making it a good default when the LLM knows roughly what it's looking for.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms.search import RegexSearchTransform\n\nmcp = FastMCP(\"My Server\", transforms=[RegexSearchTransform()])\n\n@mcp.tool\ndef search_database(query: str, limit: int = 10) -> list[dict]:\n    \"\"\"Search the database for records matching the query.\"\"\"\n    ...\n\n@mcp.tool\ndef delete_record(record_id: str) -> bool:\n    \"\"\"Delete a record from the database by its ID.\"\"\"\n    ...\n\n@mcp.tool\ndef send_email(to: str, subject: str, body: str) -> bool:\n    \"\"\"Send an email to the given recipient.\"\"\"\n    ...\n```\n\nThe LLM's `search_tools` call takes a `pattern` parameter — a regex string:\n\n```python\n# Exact substring match\nresult = await client.call_tool(\"search_tools\", {\"pattern\": \"database\"})\n# Returns: search_database, delete_record\n\n# Regex pattern\nresult = await client.call_tool(\"search_tools\", {\"pattern\": \"send.*email|notify\"})\n# Returns: send_email\n```\n\nResults are returned in catalog order. If the pattern is invalid regex, the search returns an empty list rather than raising an error.\n\n### BM25 Search\n\n`BM25SearchTransform` ranks tools by relevance using the [BM25 Okapi](https://en.wikipedia.org/wiki/Okapi_BM25) algorithm. It's better for natural language queries because it scores each tool based on term frequency and document rarity, returning results ranked by relevance rather than filtering by match/no-match.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms.search import BM25SearchTransform\n\nmcp = FastMCP(\"My Server\", transforms=[BM25SearchTransform()])\n\n# ... define tools ...\n```\n\nThe LLM's `search_tools` call takes a `query` parameter — natural language:\n\n```python\nresult = await client.call_tool(\"search_tools\", {\n    \"query\": \"tools for deleting things from the database\"\n})\n# Returns: delete_record ranked first, search_database second\n```\n\nBM25 builds an in-memory index from the searchable text of all tools. The index is created lazily on the first search and automatically rebuilt whenever the tool catalog changes — for example, when tools are added, removed, or have their descriptions updated. The staleness check is based on a hash of all searchable text, so description changes are detected even when tool names stay the same.\n\n### Which to Choose\n\nUse **regex** when your LLM is good at constructing targeted patterns and you want deterministic, predictable results. Regex is also simpler to debug — you can see exactly what pattern was sent.\n\nUse **BM25** when your LLM tends to describe what it needs in natural language, or when your tool catalog has nuanced descriptions where relevance ranking adds value. BM25 handles partial matches and synonyms better because it scores on individual terms rather than requiring a single pattern to match.\n\n## Configuration\n\nBoth search transforms accept the same configuration options.\n\n### Limiting Results\n\nBy default, search returns at most 5 tools. Adjust `max_results` based on your catalog size and how much context you want the LLM to receive per search:\n\n```python\nmcp.add_transform(RegexSearchTransform(max_results=10))\nmcp.add_transform(BM25SearchTransform(max_results=3))\n```\n\nWith regex, results stop as soon as the limit is reached (first N matches in catalog order). With BM25, all tools are scored and the top N by relevance are returned.\n\n### Pinning Tools\n\nSome tools should always be visible regardless of search. Use `always_visible` to pin them in the listing alongside the synthetic tools:\n\n```python\nmcp.add_transform(RegexSearchTransform(\n    always_visible=[\"help\", \"status\"],\n))\n\n# list_tools returns: help, status, search_tools, call_tool\n```\n\nPinned tools appear directly in `list_tools` so the LLM can call them without searching. They're excluded from search results to avoid duplication.\n\n### Custom Tool Names\n\nThe default names `search_tools` and `call_tool` can be changed to avoid conflicts with real tools:\n\n```python\nmcp.add_transform(RegexSearchTransform(\n    search_tool_name=\"find_tools\",\n    call_tool_name=\"run_tool\",\n))\n```\n\n## The `call_tool` Proxy\n\nThe `call_tool` proxy forwards calls to the real tool. When a client calls `call_tool(name=\"search_database\", arguments={...})`, the proxy resolves `search_database` through the server's normal tool pipeline — including transforms and middleware — and executes it.\n\nThe proxy rejects attempts to call the synthetic tools themselves. `call_tool(name=\"call_tool\")` raises an error rather than recursing.\n\n<Note>\nTools discovered through search can also be called directly via `client.call_tool(\"search_database\", {...})` without going through the proxy. The proxy exists for LLMs that only know about the tools returned by `list_tools` and need a way to invoke discovered tools through a tool they can see.\n</Note>\n\n## Auth and Visibility\n\nSearch results respect the full authorization pipeline. Tools filtered by middleware, visibility transforms, or component-level auth checks won't appear in search results.\n\nThe search tool queries `list_tools()` through the complete pipeline at search time, so the same filtering that controls what a client sees in the listing also controls what they can discover through search.\n\n```python\nfrom fastmcp.server.transforms import Visibility\nfrom fastmcp.server.transforms.search import RegexSearchTransform\n\nmcp = FastMCP(\"My Server\")\n\n# ... define tools ...\n\n# Disable admin tools globally\nmcp.add_transform(Visibility(False, tags={\"admin\"}))\n\n# Add search — admin tools won't appear in results\nmcp.add_transform(RegexSearchTransform())\n```\n\nSession-level visibility changes (via `ctx.disable_components()`) are also reflected immediately in search results.\n"
  },
  {
    "path": "docs/servers/transforms/tool-transformation.mdx",
    "content": "---\ntitle: Tool Transformation\nsidebarTitle: Tool Transformation\ndescription: Modify tool schemas - rename, reshape arguments, and customize behavior\nicon: wrench\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nTool transformation lets you modify tool schemas - renaming tools, changing descriptions, adjusting tags, and reshaping argument schemas. FastMCP provides two mechanisms that share the same configuration options but differ in timing.\n\n**Deferred transformation** with `ToolTransform` applies modifications when tools flow through a transform chain. Use this for tools from mounted servers, proxies, or other providers where you don't control the source directly.\n\n**Immediate transformation** with `Tool.from_tool()` creates a modified tool object right away. Use this when you have direct access to a tool and want to transform it before registration.\n\n## ToolTransform\n\nThe `ToolTransform` class is a transform that modifies tools as they flow through a provider. Provide a dictionary mapping original tool names to their transformation configuration.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import ToolTransform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\n\nmcp = FastMCP(\"Server\")\n\n@mcp.tool\ndef verbose_internal_data_fetcher(query: str) -> str:\n    \"\"\"Fetches data from the internal database.\"\"\"\n    return f\"Results for: {query}\"\n\n# Rename the tool to something simpler\nmcp.add_transform(ToolTransform({\n    \"verbose_internal_data_fetcher\": ToolTransformConfig(\n        name=\"search\",\n        description=\"Search the database.\",\n    )\n}))\n\n# Clients see \"search\" with the cleaner description\n```\n\n`ToolTransform` is useful when you want to modify tools from mounted or proxied servers without changing the original source.\n\n## Tool.from_tool()\n\nUse `Tool.from_tool()` when you have the tool object and want to create a transformed version for registration.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool, tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\n# Create a tool without registering it\n@tool\ndef search(q: str, limit: int = 10) -> list[str]:\n    \"\"\"Search for items.\"\"\"\n    return [f\"Result {i} for {q}\" for i in range(limit)]\n\n# Transform it before registration\nbetter_search = Tool.from_tool(\n    search,\n    name=\"find_items\",\n    description=\"Find items matching your search query.\",\n    transform_args={\n        \"q\": ArgTransform(\n            name=\"query\",\n            description=\"The search terms to look for.\",\n        ),\n    },\n)\n\nmcp = FastMCP(\"Server\")\nmcp.add_tool(better_search)\n```\n\nThe standalone `@tool` decorator (from `fastmcp.tools`) creates a Tool object without registering it to any server. This separates creation from registration, letting you transform tools before deciding where they go.\n\n## Modification Options\n\nBoth mechanisms support the same modifications.\n\n**Tool-level options:**\n\n| Option | Description |\n|--------|-------------|\n| `name` | New name for the tool |\n| `description` | New description |\n| `title` | Human-readable title |\n| `tags` | Set of tags for categorization |\n| `annotations` | MCP ToolAnnotations |\n| `meta` | Custom metadata dictionary |\n| `enabled` | Whether the tool is visible to clients (default `True`) |\n\n**Argument-level options** (via `ArgTransform` or `ArgTransformConfig`):\n\n| Option | Description |\n|--------|-------------|\n| `name` | Rename the argument |\n| `description` | New description for the argument |\n| `default` | New default value |\n| `default_factory` | Callable that generates a default (requires `hide=True`) |\n| `hide` | Remove from client-visible schema |\n| `required` | Make an optional argument required |\n| `type` | Change the argument's type |\n| `examples` | Example values for the argument |\n\n## Hiding Arguments\n\nHide arguments to simplify the interface or inject values the client shouldn't control.\n\n```python\nfrom fastmcp.tools.tool_transform import ArgTransform\n\n# Hide with a constant value\ntransform_args = {\n    \"api_key\": ArgTransform(hide=True, default=\"secret-key\"),\n}\n\n# Hide with a dynamic value\nimport uuid\ntransform_args = {\n    \"request_id\": ArgTransform(hide=True, default_factory=lambda: str(uuid.uuid4())),\n}\n```\n\nHidden arguments disappear from the tool's schema. The client never sees them, but the underlying function receives the configured value.\n\n<Warning>\n`default_factory` requires `hide=True`. Visible arguments need static defaults that can be represented in JSON Schema.\n</Warning>\n\n## Renaming Arguments\n\nRename arguments to make them more intuitive for LLMs or match your API conventions.\n\n```python\nfrom fastmcp.tools import Tool, tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\n@tool\ndef search(q: str, n: int = 10) -> list[str]:\n    \"\"\"Search for items.\"\"\"\n    return []\n\nbetter_search = Tool.from_tool(\n    search,\n    transform_args={\n        \"q\": ArgTransform(name=\"query\", description=\"Search terms\"),\n        \"n\": ArgTransform(name=\"max_results\", description=\"Maximum results to return\"),\n    },\n)\n```\n\n## Custom Transform Functions\n\nFor advanced scenarios, provide a `transform_fn` that intercepts tool execution. The function can validate inputs, modify outputs, or add custom logic while still calling the original tool via `forward()`.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool, tool\nfrom fastmcp.tools.tool_transform import forward, ArgTransform\n\n@tool\ndef divide(a: float, b: float) -> float:\n    \"\"\"Divide a by b.\"\"\"\n    return a / b\n\nasync def safe_divide(numerator: float, denominator: float) -> float:\n    if denominator == 0:\n        raise ValueError(\"Cannot divide by zero\")\n    return await forward(numerator=numerator, denominator=denominator)\n\nsafe_division = Tool.from_tool(\n    divide,\n    name=\"safe_divide\",\n    transform_fn=safe_divide,\n    transform_args={\n        \"a\": ArgTransform(name=\"numerator\"),\n        \"b\": ArgTransform(name=\"denominator\"),\n    },\n)\n\nmcp = FastMCP(\"Server\")\nmcp.add_tool(safe_division)\n```\n\nThe `forward()` function handles argument mapping automatically. Call it with the transformed argument names, and it maps them back to the original function's parameters.\n\nFor direct access to the original function without mapping, use `forward_raw()` with the original parameter names.\n\n## Context-Aware Tool Factories\n\nYou can write functions that act as \"factories,\" generating specialized versions of a tool for different contexts. For example, create a `get_my_data` tool for the current user by hiding the `user_id` parameter and providing it automatically.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool, tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\n# A generic tool that requires a user_id\n@tool\ndef get_user_data(user_id: str, query: str) -> str:\n    \"\"\"Fetch data for a specific user.\"\"\"\n    return f\"Data for user {user_id}: {query}\"\n\n\ndef create_user_tool(user_id: str) -> Tool:\n    \"\"\"Factory that creates a user-specific version of get_user_data.\"\"\"\n    return Tool.from_tool(\n        get_user_data,\n        name=\"get_my_data\",\n        description=\"Fetch your data. No need to specify a user ID.\",\n        transform_args={\n            \"user_id\": ArgTransform(hide=True, default=user_id),\n        },\n    )\n\n\n# Create a server with a tool customized for the current user\nmcp = FastMCP(\"User Server\")\ncurrent_user_id = \"user-123\"  # e.g., from auth context\nmcp.add_tool(create_user_tool(current_user_id))\n\n# Clients see \"get_my_data(query: str)\" — user_id is injected automatically\n```\n\nThis pattern is useful for multi-tenant servers where each connection gets tools pre-configured with their identity, or for wrapping generic tools with environment-specific defaults.\n"
  },
  {
    "path": "docs/servers/transforms/transforms.mdx",
    "content": "---\ntitle: Transforms Overview\nsidebarTitle: Overview\ndescription: Modify components as they flow through your server\nicon: wand-magic-sparkles\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nTransforms modify components as they flow from providers to clients. When a client asks \"what tools do you have?\", the request passes through each transform in the chain. Each transform can modify the components before passing them along.\n\n## Mental Model\n\nThink of transforms as filters in a pipeline. Components flow from providers through transforms to reach clients:\n\n```\nProvider → [Transform A] → [Transform B] → Client\n```\n\nWhen listing components, transforms receive sequences and return transformed sequences—a pure function pattern. When getting a specific component by name, transforms use a middleware pattern with `call_next`, working in reverse: mapping the client's requested name back to the original, then transforming the result.\n\n## Built-in Transforms\n\nFastMCP provides several transforms for common use cases:\n\n- **[Namespace](/servers/transforms/namespace)** - Prefix component names to prevent conflicts when composing servers\n- **[Tool Transformation](/servers/transforms/tool-transformation)** - Rename tools, modify descriptions, reshape arguments\n- **[Enabled](/servers/visibility)** - Control which components are visible at runtime\n- **[Tool Search](/servers/transforms/tool-search)** - Replace large tool catalogs with on-demand search\n- **[Resources as Tools](/servers/transforms/resources-as-tools)** - Expose resources to tool-only clients\n- **[Prompts as Tools](/servers/transforms/prompts-as-tools)** - Expose prompts to tool-only clients\n- **[Code Mode (Experimental)](/servers/transforms/code-mode)** - Replace many tools with programmable `search` + `execute`\n\n## Server vs Provider Transforms\n\nTransforms can be added at two levels, each serving different purposes.\n\n### Provider-Level Transforms\n\nProvider transforms apply to components from a specific provider. They run first, modifying components before they reach the server level.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.server.transforms import Namespace, ToolTransform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\n\nsub_server = FastMCP(\"Sub\")\n\n@sub_server.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\n# Create provider and add transforms\nprovider = FastMCPProvider(sub_server)\nprovider.add_transform(Namespace(\"api\"))\nprovider.add_transform(ToolTransform({\n    \"api_process\": ToolTransformConfig(description=\"Process data through the API\"),\n}))\n\nmain = FastMCP(\"Main\", providers=[provider])\n# Tool is now: api_process with updated description\n```\n\nWhen using `mount()`, the returned provider reference lets you add transforms directly.\n\n```python\nmain = FastMCP(\"Main\")\nmount = main.mount(sub_server, namespace=\"api\")\nmount.add_transform(ToolTransform({...}))\n```\n\n### Server-Level Transforms\n\nServer transforms apply to all components from all providers. They run after provider transforms, seeing the already-transformed names.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import Namespace\n\nmcp = FastMCP(\"Server\", transforms=[Namespace(\"v1\")])\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# All tools become v1_toolname\n```\n\nServer-level transforms are useful for API versioning or applying consistent naming across your entire server.\n\n### Transform Order\n\nTransforms stack in the order they're added. The first transform added is innermost (closest to the provider), and subsequent transforms wrap it.\n\n```python\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.server.transforms import Namespace, ToolTransform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\n\nprovider = FastMCPProvider(server)\nprovider.add_transform(Namespace(\"api\"))           # Applied first\nprovider.add_transform(ToolTransform({             # Sees namespaced names\n    \"api_verbose_name\": ToolTransformConfig(name=\"short\"),\n}))\n\n# Flow: \"verbose_name\" -> \"api_verbose_name\" -> \"short\"\n```\n\nWhen a client requests \"short\", the transforms reverse the mapping: ToolTransform maps \"short\" to \"api_verbose_name\", then Namespace strips the prefix to find \"verbose_name\" in the provider.\n\n## Custom Transforms\n\nCreate custom transforms by subclassing `Transform` and overriding the methods you need.\n\n```python\nfrom collections.abc import Sequence\nfrom fastmcp.server.transforms import Transform, GetToolNext\nfrom fastmcp.tools.tool import Tool\n\nclass TagFilter(Transform):\n    \"\"\"Filter tools to only those with specific tags.\"\"\"\n\n    def __init__(self, required_tags: set[str]):\n        self.required_tags = required_tags\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        return [t for t in tools if t.tags & self.required_tags]\n\n    async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:\n        tool = await call_next(name)\n        if tool and tool.tags & self.required_tags:\n            return tool\n        return None\n```\n\nThe `Transform` base class provides default implementations that pass through unchanged. Override only the methods relevant to your transform.\n\nEach component type has two methods with different patterns:\n\n| Method | Pattern | Purpose |\n|--------|---------|---------|\n| `list_tools(tools)` | Pure function | Transform the sequence of tools |\n| `get_tool(name, call_next)` | Middleware | Transform lookup by name |\n| `list_resources(resources)` | Pure function | Transform the sequence of resources |\n| `get_resource(uri, call_next)` | Middleware | Transform lookup by URI |\n| `list_resource_templates(templates)` | Pure function | Transform the sequence of templates |\n| `get_resource_template(uri, call_next)` | Middleware | Transform template lookup by URI |\n| `list_prompts(prompts)` | Pure function | Transform the sequence of prompts |\n| `get_prompt(name, call_next)` | Middleware | Transform lookup by name |\n\nList methods receive sequences directly and return transformed sequences. Get methods use `call_next` for routing flexibility—when a client requests \"new_name\", your transform maps it back to \"original_name\" before calling `call_next()`.\n\n```python\nclass PrefixTransform(Transform):\n    def __init__(self, prefix: str):\n        self.prefix = prefix\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        return [t.model_copy(update={\"name\": f\"{self.prefix}_{t.name}\"}) for t in tools]\n\n    async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:\n        # Reverse the prefix to find the original\n        if not name.startswith(f\"{self.prefix}_\"):\n            return None\n        original = name[len(self.prefix) + 1:]\n        tool = await call_next(original)\n        if tool:\n            return tool.model_copy(update={\"name\": name})\n        return None\n```\n"
  },
  {
    "path": "docs/servers/versioning.mdx",
    "content": "---\ntitle: Versioning\nsidebarTitle: Versioning\ndescription: Serve multiple API versions from a single codebase\nicon: code-branch\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nComponent versioning lets you maintain multiple implementations of the same tool, resource, or prompt under a single identifier. You register each version, and FastMCP handles the rest: clients see the highest version by default, but you can filter to expose exactly the versions you want.\n\nThe primary use case is serving different API versions from one codebase. Instead of maintaining separate deployments for v1 and v2 clients, you version your components and use `VersionFilter` to create distinct API surfaces.\n\n## Versioned API Surfaces\n\nConsider a server that needs to support both v1 and v2 clients. The v2 API adds new parameters to existing tools, and you want both versions to coexist cleanly. Define your components on a shared provider, then create separate servers with different version filters.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import LocalProvider\nfrom fastmcp.server.transforms import VersionFilter\n\n# Define versioned components on a shared provider\ncomponents = LocalProvider()\n\n@components.tool(version=\"1.0\")\ndef calculate(x: int, y: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return x + y\n\n@components.tool(version=\"2.0\")\ndef calculate(x: int, y: int, z: int = 0) -> int:\n    \"\"\"Add two or three numbers.\"\"\"\n    return x + y + z\n\n# Create servers that share the provider with different filters\napi_v1 = FastMCP(\"API v1\", providers=[components])\napi_v1.add_transform(VersionFilter(version_lt=\"2.0\"))\n\napi_v2 = FastMCP(\"API v2\", providers=[components])\napi_v2.add_transform(VersionFilter(version_gte=\"2.0\"))\n```\n\nClients connecting to `api_v1` see the two-argument `calculate`. Clients connecting to `api_v2` see the three-argument version. Both servers share the same component definitions.\n\n`VersionFilter` accepts two keyword-only parameters that mirror comparison operators: `version_gte` (greater than or equal) and `version_lt` (less than). You can use either or both to define your version range.\n\n```python\n# Versions < 3.0 (v1.x and v2.x)\nVersionFilter(version_lt=\"3.0\")\n\n# Versions >= 2.0 (v2.x and later)\nVersionFilter(version_gte=\"2.0\")\n\n# Versions in range [2.0, 3.0) (only v2.x)\nVersionFilter(version_gte=\"2.0\", version_lt=\"3.0\")\n```\n\n<Note>\n**Unversioned components are exempt from version filtering by default.** Set `include_unversioned=False` to exclude them. Including them by default ensures that adding version filtering to a server with mixed versioned and unversioned components doesn't accidentally hide the unversioned ones. To prevent confusion, FastMCP forbids mixing versioned and unversioned components with the same name.\n</Note>\n\n### Filtering Mounted Servers\n\nWhen you mount child servers and apply a `VersionFilter` to the parent, the filter applies to components from mounted servers as well. Range filtering (`version_gte` and `version_lt`) is handled at the provider level, meaning mounted servers don't need to know about the parent's version constraints.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import VersionFilter\n\n# Child server with versioned components\nchild = FastMCP(\"Child\")\n\n@child.tool(version=\"1.0\")\ndef process(data: str) -> str:\n    return data.upper()\n\n@child.tool(version=\"2.0\")\ndef process(data: str, mode: str = \"default\") -> str:\n    return data.upper() if mode == \"default\" else data.lower()\n\n# Parent server mounts child and applies version filter\nparent = FastMCP(\"Parent\")\nparent.mount(child, namespace=\"child\")\nparent.add_transform(VersionFilter(version_lt=\"2.0\"))\n\n# Clients see only child_process v1.0\n```\n\nThe parent's `VersionFilter` sees components after they've been namespaced, but filters based on version regardless of namespace. This lets you apply version policies consistently across your entire server hierarchy.\n\n## Declaring Versions\n\nAdd a `version` parameter to any component decorator. FastMCP stores versions as strings and groups components by their identifier (name for tools and prompts, URI for resources).\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.tool(version=\"1.0\")\ndef process(data: str) -> str:\n    \"\"\"Original processing.\"\"\"\n    return data.upper()\n\n@mcp.tool(version=\"2.0\")\ndef process(data: str, mode: str = \"default\") -> str:\n    \"\"\"Enhanced processing with mode selection.\"\"\"\n    if mode == \"reverse\":\n        return data[::-1].upper()\n    return data.upper()\n```\n\nBoth versions are registered. When a client lists tools, they see only `process` with version 2.0 (the highest). When they invoke `process`, version 2.0 executes. The same pattern applies to resources and prompts.\n\n### Versioned vs Unversioned Components\n\nFor any given component name, you must choose one approach: either version all implementations or version none of them. Mixing versioned and unversioned components with the same name raises an error at registration time.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.tool\ndef calculate(x: int, y: int) -> int:\n    \"\"\"Unversioned tool.\"\"\"\n    return x + y\n\n@mcp.tool(version=\"2.0\")  # Raises ValueError\ndef calculate(x: int, y: int, z: int = 0) -> int:\n    \"\"\"Cannot mix versioned with unversioned.\"\"\"\n    return x + y + z\n```\n\nThe error message explains the conflict: \"Cannot add versioned tool 'calculate' (version='2.0'): an unversioned tool with this name already exists. Either version all components or none.\"\n\nThis restriction helps keep version filtering behavior predictable.\n\nResources and prompts follow the same pattern.\n\n```python\n@mcp.resource(\"config://app\", version=\"1.0\")\ndef config_v1() -> str:\n    return '{\"format\": \"legacy\"}'\n\n@mcp.resource(\"config://app\", version=\"2.0\")\ndef config_v2() -> str:\n    return '{\"format\": \"modern\", \"schema\": \"v2\"}'\n\n@mcp.prompt(version=\"1.0\")\ndef summarize(text: str) -> str:\n    return f\"Summarize: {text}\"\n\n@mcp.prompt(version=\"2.0\")\ndef summarize(text: str, style: str = \"concise\") -> str:\n    return f\"Summarize in a {style} style: {text}\"\n```\n\n### Version Discovery\n\nWhen clients list components, each versioned component includes metadata about all available versions. This lets clients discover what versions exist before deciding which to use. The `meta.fastmcp.versions` field contains all registered versions sorted from highest to lowest.\n\n```python\nfrom fastmcp import Client\n\nasync with Client(server) as client:\n    tools = await client.list_tools()\n\n    for tool in tools:\n        if tool.meta:\n            fastmcp_meta = tool.meta.get(\"fastmcp\", {})\n            # Current version being returned (highest by default)\n            print(f\"Version: {fastmcp_meta.get('version')}\")\n            # All available versions for this component\n            print(f\"Available: {fastmcp_meta.get('versions')}\")\n```\n\nFor a tool with versions `\"1.0\"` and `\"2.0\"`, listing returns the `2.0` implementation with `meta.fastmcp.version` set to `\"2.0\"` and `meta.fastmcp.versions` set to `[\"2.0\", \"1.0\"]`. Unversioned components omit these fields entirely.\n\nThis discovery mechanism enables clients to make informed decisions about which version to request, support graceful degradation when newer versions introduce breaking changes, or display version information in developer tools.\n\n## Requesting Specific Versions\n\nBy default, clients receive and invoke the highest version of each component. When you need a specific version, FastMCP provides two approaches: the FastMCP client API for Python applications, and the MCP protocol mechanism for any MCP-compatible client.\n\n### FastMCP Client\n\nThe FastMCP client's `call_tool` and `get_prompt` methods accept an optional `version` parameter. When specified, the server executes that exact version instead of the highest.\n\n```python\nfrom fastmcp import Client\n\nasync with Client(server) as client:\n    # Call the highest version (default behavior)\n    result = await client.call_tool(\"calculate\", {\"x\": 1, \"y\": 2})\n\n    # Call a specific version\n    result_v1 = await client.call_tool(\"calculate\", {\"x\": 1, \"y\": 2}, version=\"1.0\")\n\n    # Get a specific prompt version\n    prompt = await client.get_prompt(\"summarize\", {\"text\": \"...\"}, version=\"1.0\")\n```\n\nIf the requested version doesn't exist, the server raises a `NotFoundError`. This ensures you get exactly what you asked for rather than silently falling back to a different version.\n\n### MCP Protocol\n\nFor generic MCP clients that don't have built-in version support, pass the version through the `_meta` field in arguments. FastMCP servers extract the version from `_meta.fastmcp.version` before processing.\n\n<CodeGroup>\n```json Tool Call Arguments\n{\n  \"x\": 1,\n  \"y\": 2,\n  \"_meta\": {\n    \"fastmcp\": {\n      \"version\": \"1.0\"\n    }\n  }\n}\n```\n\n```json Prompt Arguments\n{\n  \"text\": \"Summarize this document...\",\n  \"_meta\": {\n    \"fastmcp\": {\n      \"version\": \"1.0\"\n    }\n  }\n}\n```\n</CodeGroup>\n\nThe `_meta` field is part of the MCP request params, not arguments, so your component implementation never sees it. This convention allows version selection to work across any MCP client without requiring protocol changes. The FastMCP client handles this automatically when you pass the `version` parameter.\n\n## Version Comparison\n\nFastMCP compares versions to determine which is \"highest\" when multiple versions share an identifier. The comparison behavior depends on the version format.\n\nFor [PEP 440](https://peps.python.org/pep-0440/) versions (like `\"1.0\"`, `\"2.1.3\"`, `\"1.0a1\"`), FastMCP uses semantic comparison where numeric segments are compared as numbers.\n\n```python\n# PEP 440 versions compare semantically\n\"1\" < \"2\" < \"10\"           # Numeric order (not \"1\" < \"10\" < \"2\")\n\"1.9\" < \"1.10\"             # Numeric order (not \"1.10\" < \"1.9\")\n\"1.0a1\" < \"1.0b1\" < \"1.0\"  # Pre-releases sort before releases\n```\n\nFor other formats (dates, custom schemes), FastMCP falls back to lexicographic string comparison. This works well for ISO dates and other naturally sortable formats.\n\n```python\n# Non-PEP 440 versions compare as strings\n\"2025-01-15\" < \"2025-02-01\"  # ISO dates sort correctly\n\"alpha\" < \"beta\"             # Alphabetical order\n```\n\nThe `v` prefix is stripped before comparison, so `\"v1.0\"` and `\"1.0\"` are treated as equal for sorting purposes.\n\n## Retrieving Specific Versions\n\nServer-side code can retrieve specific versions rather than just the highest. This is useful during migrations when you need to compare behavior between versions or access legacy implementations.\n\nThe `get_tool`, `get_resource`, and `get_prompt` methods accept an optional `version` parameter. Without it, they return the highest version. With it, they return exactly that version.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.tool(version=\"1.0\")\ndef add(x: int, y: int) -> int:\n    return x + y\n\n@mcp.tool(version=\"2.0\")\ndef add(x: int, y: int) -> int:\n    return x + y + 100  # Different behavior\n\n# Get highest version (default)\ntool = await mcp.get_tool(\"add\")\nprint(tool.version)  # \"2.0\"\n\n# Get specific version\ntool_v1 = await mcp.get_tool(\"add\", version=\"1.0\")\nprint(tool_v1.version)  # \"1.0\"\n```\n\nIf the requested version doesn't exist, a `NotFoundError` is raised.\n\n## Removing Versions\n\nThe `remove_tool`, `remove_resource`, and `remove_prompt` methods on the server's [local provider](/servers/providers/local) accept an optional `version` parameter that controls what gets removed.\n\n```python\n# Remove ALL versions of a component\nmcp.local_provider.remove_tool(\"calculate\")\n\n# Remove only a specific version\nmcp.local_provider.remove_tool(\"calculate\", version=\"1.0\")\n```\n\nWhen you remove a specific version, other versions remain registered. When you remove without specifying a version, all versions are removed.\n\n## Migration Workflow\n\nVersioning supports gradual migration when updating component behavior. You can deploy new versions alongside old ones, verify the new behavior works correctly, then clean up.\n\nWhen migrating an existing unversioned component to use versioning, start by assigning an initial version to your existing implementation. Then add the new version alongside it.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.tool(version=\"1.0\")\ndef process_data(input: str) -> str:\n    \"\"\"Original implementation, now versioned.\"\"\"\n    return legacy_process(input)\n\n@mcp.tool(version=\"2.0\")\ndef process_data(input: str, options: dict | None = None) -> str:\n    \"\"\"Updated implementation with new options parameter.\"\"\"\n    return modern_process(input, options or {})\n```\n\nClients automatically see version 2.0 (the highest). During the transition, your server code can still access the original implementation via `get_tool(\"process_data\", version=\"1.0\")`.\n\nOnce the migration is complete, remove the old version.\n\n```python\nmcp.local_provider.remove_tool(\"process_data\", version=\"1.0\")\n```\n"
  },
  {
    "path": "docs/servers/visibility.mdx",
    "content": "---\ntitle: Component Visibility\nsidebarTitle: Visibility\ndescription: Control which components are available to clients\nicon: toggle-on\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"3.0.0\" />\n\nComponents can be dynamically enabled or disabled at runtime. A disabled tool disappears from listings and cannot be called. This enables runtime access control, feature flags, and context-aware component exposure.\n\n## Component Visibility\n\nEvery FastMCP server provides `enable()` and `disable()` methods for controlling component availability.\n\n### Disabling Components\n\nThe `disable()` method marks components as disabled. Disabled components are filtered out from all client queries.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Server\")\n\n@mcp.tool(tags={\"admin\"})\ndef delete_everything() -> str:\n    \"\"\"Delete all data.\"\"\"\n    return \"Deleted\"\n\n@mcp.tool(tags={\"admin\"})\ndef reset_system() -> str:\n    \"\"\"Reset the system.\"\"\"\n    return \"Reset\"\n\n@mcp.tool\ndef get_status() -> str:\n    \"\"\"Get system status.\"\"\"\n    return \"OK\"\n\n# Disable admin tools\nmcp.disable(tags={\"admin\"})\n\n# Clients only see: get_status\n```\n\n### Enabling Components\n\nThe `enable()` method re-enables previously disabled components.\n\n```python\n# Re-enable admin tools\nmcp.enable(tags={\"admin\"})\n\n# Clients now see all three tools\n```\n\n## Keys and Tags\n\nVisibility filtering works with two identifiers: keys (for specific components) and tags (for groups).\n\n### Component Keys\n\nEvery component has a unique key in the format `{type}:{identifier}`.\n\n| Component | Key Format | Example |\n|-----------|------------|---------|\n| Tool | `tool:{name}` | `tool:delete_everything` |\n| Resource | `resource:{uri}` | `resource:data://config` |\n| Template | `template:{uri}` | `template:file://{path}` |\n| Prompt | `prompt:{name}` | `prompt:analyze` |\n\nUse keys to target specific components.\n\n```python\n# Disable a specific tool\nmcp.disable(keys={\"tool:delete_everything\"})\n\n# Disable multiple specific components\nmcp.disable(keys={\"tool:reset_system\", \"resource:data://secrets\"})\n```\n\n### Tags\n\nTags group components for bulk operations. Define tags when creating components, then filter by them.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Server\")\n\n@mcp.tool(tags={\"public\", \"read\"})\ndef get_data() -> str:\n    return \"data\"\n\n@mcp.tool(tags={\"admin\", \"write\"})\ndef set_data(value: str) -> str:\n    return f\"Set: {value}\"\n\n@mcp.tool(tags={\"admin\", \"dangerous\"})\ndef delete_data() -> str:\n    return \"Deleted\"\n\n# Disable all admin tools\nmcp.disable(tags={\"admin\"})\n\n# Disable all dangerous tools (some overlap with admin)\nmcp.disable(tags={\"dangerous\"})\n```\n\nA component is disabled if it has **any** of the disabled tags. The component doesn't need all the tags; one match is enough.\n\n### Combining Keys and Tags\n\nYou can specify both keys and tags in a single call. The filters combine additively.\n\n```python\n# Disable specific tools AND all dangerous-tagged components\nmcp.disable(keys={\"tool:debug_info\"}, tags={\"dangerous\"})\n```\n\n## Allowlist Mode\n\nBy default, visibility filtering uses blocklist mode: everything is enabled unless explicitly disabled. The `only=True` parameter switches to allowlist mode, where **only** specified components are enabled.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Server\")\n\n@mcp.tool(tags={\"safe\"})\ndef read_only_operation() -> str:\n    return \"Read\"\n\n@mcp.tool(tags={\"safe\"})\ndef list_items() -> list[str]:\n    return [\"a\", \"b\", \"c\"]\n\n@mcp.tool(tags={\"dangerous\"})\ndef delete_all() -> str:\n    return \"Deleted\"\n\n@mcp.tool\ndef untagged_tool() -> str:\n    return \"Untagged\"\n\n# Only enable safe tools - everything else is disabled\nmcp.enable(tags={\"safe\"}, only=True)\n\n# Clients see: read_only_operation, list_items\n# Disabled: delete_all, untagged_tool\n```\n\nAllowlist mode is useful for restrictive environments where you want to explicitly opt-in components rather than opt-out.\n\n### Allowlist Behavior\n\nWhen you call `enable(only=True)`:\n\n1. Default visibility state switches to \"disabled\"\n2. Previous allowlists are cleared\n3. Only specified keys/tags become enabled\n\n```python\n# Start fresh - only enable these specific tools\nmcp.enable(keys={\"tool:safe_read\", \"tool:safe_write\"}, only=True)\n\n# Later, switch to a different allowlist\nmcp.enable(tags={\"production\"}, only=True)\n```\n\n### Ordering and Overrides\n\nLater `enable()` and `disable()` calls override earlier ones. This lets you create broad rules with specific exceptions.\n\n```python\nmcp.enable(tags={\"api\"}, only=True)  # Allow all api-tagged\nmcp.disable(keys={\"tool:api_admin\"})  # Later disable overrides for this tool\n\n# api_admin is disabled because the later disable() overrides the allowlist\n```\n\nYou can always re-enable something that was disabled by adding another `enable()` call after it.\n\n## Server vs Provider\n\nVisibility state operates at two levels: the server and individual providers.\n\n### Server-Level\n\nServer-level visibility state applies to all components from all providers. When you call `mcp.enable()` or `mcp.disable()`, you're filtering the final view that clients see.\n\n```python\nfrom fastmcp import FastMCP\n\nmain = FastMCP(\"Main\")\nmain.mount(sub_server, namespace=\"api\")\n\n@main.tool(tags={\"internal\"})\ndef local_debug() -> str:\n    return \"Debug\"\n\n# Disable internal tools from ALL sources\nmain.disable(tags={\"internal\"})\n```\n\n### Provider-Level\n\nEach provider can add its own visibility transforms. These run before server-level transforms, so the server can override provider-level disables.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import LocalProvider\n\n# Create provider with visibility control\nadmin_tools = LocalProvider()\n\n@admin_tools.tool(tags={\"admin\"})\ndef admin_action() -> str:\n    return \"Admin\"\n\n@admin_tools.tool\ndef regular_action() -> str:\n    return \"Regular\"\n\n# Disable at provider level\nadmin_tools.disable(tags={\"admin\"})\n\n# Server can override if needed\nmcp = FastMCP(\"Server\", providers=[admin_tools])\nmcp.enable(names={\"admin_action\"})  # Re-enables despite provider disable\n```\n\nProvider-level transforms are useful for setting default visibility that servers can selectively override.\n\n### Layered Transforms\n\nProvider transforms run first, then server transforms. Later transforms override earlier ones, so the server has final say.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import LocalProvider\n\nprovider = LocalProvider()\n\n@provider.tool(tags={\"feature\", \"beta\"})\ndef new_feature() -> str:\n    return \"New\"\n\n# Provider enables feature-tagged\nprovider.enable(tags={\"feature\"}, only=True)\n\n# Server disables beta-tagged (runs after provider)\nmcp = FastMCP(\"Server\", providers=[provider])\nmcp.disable(tags={\"beta\"})\n\n# new_feature is disabled (server's later disable overrides provider's enable)\n```\n\n## Per-Session Visibility\n\nServer-level visibility changes affect all connected clients simultaneously. When you need different clients to see different components, use per-session visibility instead.\n\nSession visibility lets individual sessions customize their view of available components. When a tool calls `ctx.enable_components()` or `ctx.disable_components()`, those rules apply only to the current session. Other sessions continue to see the global defaults. This enables patterns like progressive disclosure, role-based access, and on-demand feature activation.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(\"Session-Aware Server\")\n\n@mcp.tool(tags={\"premium\"})\ndef premium_analysis(data: str) -> str:\n    \"\"\"Advanced analysis available to premium users.\"\"\"\n    return f\"Premium analysis of: {data}\"\n\n@mcp.tool\nasync def unlock_premium(ctx: Context) -> str:\n    \"\"\"Unlock premium features for this session.\"\"\"\n    await ctx.enable_components(tags={\"premium\"})\n    return \"Premium features unlocked\"\n\n@mcp.tool\nasync def reset_features(ctx: Context) -> str:\n    \"\"\"Reset to default feature set.\"\"\"\n    await ctx.reset_visibility()\n    return \"Features reset to defaults\"\n\n# Premium tools are disabled globally by default\nmcp.disable(tags={\"premium\"})\n```\n\nAll sessions start with `premium_analysis` hidden. When a session calls `unlock_premium`, that session gains access to premium tools while other sessions remain unaffected. Calling `reset_features` returns the session to the global defaults.\n\n### How Session Rules Work\n\nSession rules override global transforms. When listing components, FastMCP first applies global enable/disable rules, then applies session-specific rules on top. Rules within a session accumulate, and later rules override earlier ones for the same component.\n\n```python\n@mcp.tool\nasync def customize_session(ctx: Context) -> str:\n    # Enable finance tools for this session\n    await ctx.enable_components(tags={\"finance\"})\n\n    # Also enable admin tools\n    await ctx.enable_components(tags={\"admin\"})\n\n    # Later: disable a specific admin tool\n    await ctx.disable_components(names={\"dangerous_admin_tool\"})\n\n    return \"Session customized\"\n```\n\nEach call adds a rule to the session. The `dangerous_admin_tool` ends up disabled because its disable rule was added after the admin enable rule.\n\n### Filter Criteria\n\nThe session visibility methods accept the same filter criteria as `server.enable()` and `server.disable()`:\n\n| Parameter | Description |\n|-----------|-------------|\n| `names` | Component names or URIs to match |\n| `keys` | Component keys (e.g., `{\"tool:my_tool\"}`) |\n| `tags` | Tags to match (component must have at least one) |\n| `version` | Version specification to match |\n| `components` | Component types (`{\"tool\"}`, `{\"resource\"}`, `{\"prompt\"}`, `{\"template\"}`) |\n| `match_all` | If `True`, matches all components regardless of other criteria |\n\n```python\nfrom fastmcp.utilities.versions import VersionSpec\n\n@mcp.tool\nasync def enable_recent_tools(ctx: Context) -> str:\n    \"\"\"Enable only tools from version 2.0.0 or later.\"\"\"\n    await ctx.enable_components(\n        version=VersionSpec(gte=\"2.0.0\"),\n        components={\"tool\"}\n    )\n    return \"Recent tools enabled\"\n```\n\n### Automatic Notifications\n\nWhen session visibility changes, FastMCP automatically sends notifications to that session. Clients receive `ToolListChangedNotification`, `ResourceListChangedNotification`, and `PromptListChangedNotification` so they can refresh their component lists. These notifications go only to the affected session.\n\nWhen you specify the `components` parameter, FastMCP optimizes by sending only the relevant notifications:\n\n```python\n# Only sends ToolListChangedNotification\nawait ctx.enable_components(tags={\"finance\"}, components={\"tool\"})\n\n# Sends all three notifications (no components filter)\nawait ctx.enable_components(tags={\"finance\"})\n```\n\n### Namespace Activation Pattern\n\nA common pattern organizes tools into namespaces using tag prefixes, disables them globally, then provides activation tools that unlock namespaces on demand:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nserver = FastMCP(\"Multi-Domain Assistant\")\n\n# Finance namespace\n@server.tool(tags={\"namespace:finance\"})\ndef analyze_portfolio(symbols: list[str]) -> str:\n    return f\"Analysis for: {', '.join(symbols)}\"\n\n@server.tool(tags={\"namespace:finance\"})\ndef get_market_data(symbol: str) -> dict:\n    return {\"symbol\": symbol, \"price\": 150.25}\n\n# Admin namespace\n@server.tool(tags={\"namespace:admin\"})\ndef list_users() -> list[str]:\n    return [\"alice\", \"bob\", \"charlie\"]\n\n# Activation tools - always visible\n@server.tool\nasync def activate_finance(ctx: Context) -> str:\n    await ctx.enable_components(tags={\"namespace:finance\"})\n    return \"Finance tools activated\"\n\n@server.tool\nasync def activate_admin(ctx: Context) -> str:\n    await ctx.enable_components(tags={\"namespace:admin\"})\n    return \"Admin tools activated\"\n\n@server.tool\nasync def deactivate_all(ctx: Context) -> str:\n    await ctx.reset_visibility()\n    return \"All namespaces deactivated\"\n\n# Disable namespace tools globally\nserver.disable(tags={\"namespace:finance\", \"namespace:admin\"})\n```\n\nSessions start seeing only the activation tools. Calling `activate_finance` reveals finance tools for that session only. Multiple namespaces can be activated independently, and `deactivate_all` returns to the initial state.\n\n### Method Reference\n\n- **`await ctx.enable_components(...) -> None`**: Enable matching components for this session\n- **`await ctx.disable_components(...) -> None`**: Disable matching components for this session\n- **`await ctx.reset_visibility() -> None`**: Clear all session rules, returning to global defaults\n\n## Client Notifications\n\nWhen visibility state changes, FastMCP automatically notifies connected clients. Clients supporting the MCP notification protocol receive `list_changed` events and can refresh their component lists.\n\nThis happens automatically. You don't need to trigger notifications manually.\n\n```python\n# This automatically notifies clients\nmcp.disable(tags={\"maintenance\"})\n\n# Clients receive: tools/list_changed, resources/list_changed, etc.\n```\n\n## Filtering Logic\n\nUnderstanding the filtering logic helps when debugging visibility state issues.\n\nThe `is_enabled()` function checks a component's internal metadata:\n\n1. If the component has `meta.fastmcp._internal.visibility = False`, it's disabled\n2. If the component has `meta.fastmcp._internal.visibility = True`, it's enabled\n3. If no visibility state is set, the component is enabled by default\n\nWhen multiple `enable()` and `disable()` calls are made, transforms are applied in order. **Later transforms override earlier ones**, so the last matching transform wins.\n\n## The Visibility Transform\n\nUnder the hood, `enable()` and `disable()` add `Visibility` transforms to the server or provider. The `Visibility` transform marks components with visibility metadata, and the server applies the final filter after all provider and server transforms complete.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import Visibility\n\nmcp = FastMCP(\"Server\")\n\n# Using the convenience method (recommended)\nmcp.disable(names={\"secret_tool\"})\n\n# Equivalent to:\nmcp.add_transform(Visibility(False, names={\"secret_tool\"}))\n```\n\nServer-level transforms override provider-level transforms. If a component is disabled at the provider level but enabled at the server level, the server-level `enable()` can re-enable it.\n"
  },
  {
    "path": "docs/snippets/local-focus.mdx",
    "content": "export const LocalFocusTip = () => {\n    return (\n        <Tip>\n            <strong>This integration focuses on running local FastMCP server files with STDIO transport.</strong> For remote servers running with HTTP or SSE transport, use your client's native configuration - FastMCP's integrations focus on simplifying the complex local setup with dependencies and <code>uv</code> commands.\n        </Tip>\n    );\n};"
  },
  {
    "path": "docs/snippets/version-badge.mdx",
    "content": "export const VersionBadge = ({ version }) => {\n    return (\n        <Badge stroke size=\"lg\" icon='gift' iconType='regular' className=\"version-badge\">\n            New in version <code>{version}</code>\n        </Badge>\n    );\n};"
  },
  {
    "path": "docs/snippets/youtube-embed.mdx",
    "content": "export const YouTubeEmbed = ({ videoId, title }) => {\n    return (\n        <iframe\n            className=\"w-full aspect-video rounded-md\"\n            src={`https://www.youtube.com/embed/${videoId}`}\n            title={title}\n            frameBorder=\"0\"\n            allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n            allowFullScreen\n        />\n    );\n};"
  },
  {
    "path": "docs/tutorials/create-mcp-server.mdx",
    "content": "---\ntitle: \"How to Create an MCP Server in Python\"\nsidebarTitle: \"Creating an MCP Server\"\ndescription: \"A step-by-step guide to building a Model Context Protocol (MCP) server using Python and FastMCP, from basic tools to dynamic resources.\"\nicon: server\n---\n\nSo you want to build a Model Context Protocol (MCP) server in Python. The goal is to create a service that can provide tools and data to AI models like Claude, Gemini, or others that support the protocol. While the [MCP specification](https://modelcontextprotocol.io/specification/) is powerful, implementing it from scratch involves a lot of boilerplate: handling JSON-RPC, managing session state, and correctly formatting requests and responses.\n\nThis is where **FastMCP** comes in. It's a high-level framework that handles all the protocol complexities for you, letting you focus on what matters: writing the Python functions that power your server.\n\nThis guide will walk you through creating a fully-featured MCP server from scratch using FastMCP.\n\n<Tip>\nEvery code block in this tutorial is a complete, runnable example. You can copy and paste it into a file and run it, or paste it directly into a Python REPL like IPython to try it out.\n</Tip>\n\n### Prerequisites\n\nMake sure you have FastMCP installed. If not, follow the [installation guide](/getting-started/installation).\n\n```bash\npip install fastmcp\n```\n\n\n## Step 1: Create the Basic Server\n\nEvery FastMCP application starts with an instance of the `FastMCP` class. This object acts as the container for all your tools and resources.\n\nCreate a new file called `my_mcp_server.py`:\n\n```python my_mcp_server.py\nfrom fastmcp import FastMCP\n\n# Create a server instance with a descriptive name\nmcp = FastMCP(name=\"My First MCP Server\")\n```\n\nThat's it! You have a valid (though empty) MCP server. Now, let's add some functionality.\n\n## Step 2: Add a Tool\n\nTools are functions that an LLM can execute. Let's create a simple tool that adds two numbers.\n\nTo do this, simply write a standard Python function and decorate it with `@mcp.tool`.\n\n```python my_mcp_server.py {5-8}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n```\n\nFastMCP automatically handles the rest:\n- **Tool Name:** It uses the function name (`add`) as the tool's name.\n- **Description:** It uses the function's docstring as the tool's description for the LLM.\n- **Schema:** It inspects the type hints (`a: int`, `b: int`) to generate a JSON schema for the inputs.\n\nThis is the core philosophy of FastMCP: **write Python, not protocol boilerplate.**\n\n## Step 3: Expose Data with Resources\n\nResources provide read-only data to the LLM. You can define a resource by decorating a function with `@mcp.resource`, providing a unique URI.\n\nLet's expose a simple configuration dictionary as a resource.\n\n```python my_mcp_server.py {10-13}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n```\n\nWhen a client requests the URI `resource://config`, FastMCP will execute the `get_config` function and return its output (serialized as JSON) to the client. The function is only called when the resource is requested, enabling lazy-loading of data.\n\n## Step 4: Generate Dynamic Content with Resource Templates\n\nSometimes, you need to generate resources based on parameters. This is what **Resource Templates** are for. You define them using the same `@mcp.resource` decorator but with placeholders in the URI.\n\nLet's create a template that provides a personalized greeting.\n\n```python my_mcp_server.py {15-17}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n\n@mcp.resource(\"greetings://{name}\")\ndef personalized_greeting(name: str) -> str:\n    \"\"\"Generates a personalized greeting for the given name.\"\"\"\n    return f\"Hello, {name}! Welcome to the MCP server.\"\n```\n\nNow, clients can request dynamic URIs:\n- `greetings://Ford` will call `personalized_greeting(name=\"Ford\")`.\n- `greetings://Marvin` will call `personalized_greeting(name=\"Marvin\")`.\n\nFastMCP automatically maps the `{name}` placeholder in the URI to the `name` parameter in your function.\n\n## Step 5: Run the Server\n\nTo make your server executable, add a `__main__` block to your script that calls `mcp.run()`.\n\n```python my_mcp_server.py {19-20}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n\n@mcp.resource(\"greetings://{name}\")\ndef personalized_greeting(name: str) -> str:\n    \"\"\"Generates a personalized greeting for the given name.\"\"\"\n    return f\"Hello, {name}! Welcome to the MCP server.\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\nNow you can run your server from the command line:\n```bash\npython my_mcp_server.py\n```\nThis starts the server using the default **STDIO transport**, which is how clients like Claude Desktop communicate with local servers. To learn about other transports, like HTTP, see the [Running Your Server](/deployment/running-server) guide.\n\n## The Complete Server\n\nHere is the full code for `my_mcp_server.py` (click to expand):\n\n```python my_mcp_server.py [expandable]\nfrom fastmcp import FastMCP\n\n# 1. Create the server\nmcp = FastMCP(name=\"My First MCP Server\")\n\n# 2. Add a tool\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n# 3. Add a static resource\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n\n# 4. Add a resource template for dynamic content\n@mcp.resource(\"greetings://{name}\")\ndef personalized_greeting(name: str) -> str:\n    \"\"\"Generates a personalized greeting for the given name.\"\"\"\n    return f\"Hello, {name}! Welcome to the MCP server.\"\n\n# 5. Make the server runnable\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Next Steps\n\nYou've successfully built an MCP server! From here, you can explore more advanced topics:\n\n-   [**Tools in Depth**](/servers/tools): Learn about asynchronous tools, error handling, and custom return types.\n-   [**Resources & Templates**](/servers/resources): Discover different resource types, including files and HTTP endpoints.\n-   [**Prompts**](/servers/prompts): Create reusable prompt templates for your LLM.\n-   [**Running Your Server**](/deployment/running-server): Deploy your server with different transports like HTTP.\n\n"
  },
  {
    "path": "docs/tutorials/mcp.mdx",
    "content": "---\ntitle: \"What is the Model Context Protocol (MCP)?\"\nsidebarTitle: \"What is MCP?\"\ndescription: \"An introduction to the core concepts of the Model Context Protocol (MCP), explaining what it is, why it's useful, and how it works.\"\nicon: \"diagram-project\"\n---\n\nThe Model Context Protocol (MCP) is an open standard designed to solve a fundamental problem in AI development: how can Large Language Models (LLMs) reliably and securely interact with external tools, data, and services?\n\nIt's the **bridge between the probabilistic, non-deterministic world of AI and the deterministic, reliable world of your code and data.**\n\nWhile you could build a custom REST API for your LLM, MCP provides a specialized, standardized \"port\" for AI-native communication. Think of it as **USB-C for AI**: a single, well-defined interface for connecting any compliant LLM to any compliant tool or data source.\n\nThis guide provides a high-level overview of the protocol itself. We'll use **FastMCP**, the leading Python framework for MCP, to illustrate the concepts with simple code examples.\n\n## Why Do We Need a Protocol?\n\nWith countless APIs already in existence, the most common question is: \"Why do we need another one?\"\n\nThe answer lies in **standardization**. The AI ecosystem is fragmented. Every model provider has its own way of defining and calling tools. MCP's goal is to create a common language that offers several key advantages:\n\n1.  **Interoperability:** Build one MCP server, and it can be used by any MCP-compliant client (Claude, Gemini, OpenAI, custom agents, etc.) without custom integration code. This is the protocol's most important promise.\n2.  **Discoverability:** Clients can dynamically ask a server what it's capable of at runtime. They receive a structured, machine-readable \"menu\" of tools and resources.\n3.  **Security & Safety:** MCP provides a clear, sandboxed boundary. An LLM can't execute arbitrary code on your server; it can only *request* to run the specific, typed, and validated functions you explicitly expose.\n4.  **Composability:** You can build small, specialized MCP servers and combine them to create powerful, complex applications.\n\n## Core MCP Components \n\nAn MCP server exposes its capabilities through three primary components: Tools, Resources, and Prompts.\n\n### Tools: Executable Actions\n\nTools are functions that the LLM can ask the server to execute. They are the action-oriented part of MCP.\n\nIn the spirit of a REST API, you can think of **Tools as being like `POST` requests.** They are used to *perform an action*, *change state*, or *trigger a side effect*, like sending an email, adding a user to a database, or making a calculation.\n\nWith FastMCP, creating a tool is as simple as decorating a Python function.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n# This function is now an MCP tool named \"get_weather\"\n@mcp.tool\ndef get_weather(city: str) -> dict:\n    \"\"\"Gets the current weather for a specific city.\"\"\"\n    # In a real app, this would call a weather API\n    return {\"city\": city, \"temperature\": \"72F\", \"forecast\": \"Sunny\"}\n```\n\n[**Learn more about Tools →**](/servers/tools)\n\n### Resources: Read-Only Data\n\nResources are data sources that the LLM can read. They are used to load information into the LLM's context, providing it with knowledge it doesn't have from its training data.\n\nFollowing the REST API analogy, **Resources are like `GET` requests.** Their purpose is to *retrieve information* idempotently, ideally without causing side effects. A resource can be anything from a static text file to a dynamic piece of data from a database. Each resource is identified by a unique URI.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n# This function provides a resource at the URI \"system://status\"\n@mcp.resource(\"system://status\")\ndef get_system_status() -> dict:\n    \"\"\"Returns the current operational status of the service.\"\"\"\n    return {\"status\": \"all systems normal\"}\n```\n\n#### Resource Templates\n\nYou can also create **Resource Templates** for dynamic data. A client could request `users://42/profile` to get the profile for a specific user.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n# This template provides user data for any given user ID\n@mcp.resource(\"users://{user_id}/profile\")\ndef get_user_profile(user_id: str) -> dict:\n    \"\"\"Returns the profile for a specific user.\"\"\"\n    # Fetch user from a database...\n    return {\"id\": user_id, \"name\": \"Zaphod Beeblebrox\"}\n```\n\n[**Learn more about Resources & Templates →**](/servers/resources)\n\n### Prompts: Reusable Instructions\n\nPrompts are reusable, parameterized message templates. They provide a way to define consistent, structured instructions that a client can request to guide the LLM's behavior for a specific task.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.prompt\ndef summarize_text(text_to_summarize: str) -> str:\n    \"\"\"Creates a prompt asking the LLM to summarize a piece of text.\"\"\"\n    return f\"\"\"\n        Please provide a concise, one-paragraph summary of the following text:\n        \n        {text_to_summarize}\n        \"\"\"\n```\n\n[**Learn more about Prompts →**](/servers/prompts)\n\n## Advanced Capabilities\n\nBeyond the core components, MCP also supports more advanced interaction patterns, such as a server requesting that the *client's* LLM generate a completion (known as **sampling**), or a server sending asynchronous **notifications** to a client. These features enable more complex, bidirectional workflows and are fully supported by FastMCP.\n\n## Next Steps\n\nNow that you understand the core concepts of the Model Context Protocol, you're ready to start building. The best place to begin is our step-by-step tutorial.\n\n[**Tutorial: How to Create an MCP Server in Python →**](/tutorials/create-mcp-server)\n"
  },
  {
    "path": "docs/tutorials/rest-api.mdx",
    "content": "---\ntitle: \"How to Connect an LLM to a REST API\"\nsidebarTitle: \"Connect LLMs to REST APIs\"\ndescription: \"A step-by-step guide to making any REST API with an OpenAPI spec available to LLMs using FastMCP.\"\nicon: \"plug\"\n---\n\nYou've built a powerful REST API, and now you want your LLM to be able to use it. Manually writing a wrapper function for every single endpoint is tedious, error-prone, and hard to maintain.\n\nThis is where **FastMCP** shines. If your API has an OpenAPI (or Swagger) specification, FastMCP can automatically convert your entire API into a fully-featured MCP server, making every endpoint available as a secure, typed tool for your AI model.\n\nThis guide will walk you through converting a public REST API into an MCP server in just a few lines of code.\n\n<Tip>\nEvery code block in this tutorial is a complete, runnable example. You can copy and paste it into a file and run it, or paste it directly into a Python REPL like IPython to try it out.\n</Tip>\n\n### Prerequisites\n\nMake sure you have FastMCP installed. If not, follow the [installation guide](/getting-started/installation).\n\n```bash\npip install fastmcp\n```\n\n## Step 1: Choose a Target API\n\nFor this tutorial, we'll use the [JSONPlaceholder API](https://jsonplaceholder.typicode.com/), a free, fake online REST API for testing and prototyping. It's perfect because it's simple and has a public OpenAPI specification.\n\n-   **API Base URL:** `https://jsonplaceholder.typicode.com`\n-   **OpenAPI Spec URL:** We'll use a community-provided spec for it.\n\n## Step 2: Create the MCP Server\n\nNow for the magic. We'll use `FastMCP.from_openapi`. This method takes an `httpx.AsyncClient` configured for your API and its OpenAPI specification, and automatically converts **every endpoint** into a callable MCP `Tool`.\n\n<Tip>\nLearn more about working with OpenAPI specs in the [OpenAPI integration docs](/integrations/openapi).\n</Tip>\n\n<Note>\nFor this tutorial, we'll use a simplified OpenAPI spec directly in the code. In a real project, you would typically load the spec from a URL or local file.\n</Note>\n\nCreate a file named `api_server.py`:\n\n```python api_server.py {31-35}\nimport httpx\nfrom fastmcp import FastMCP\n\n# Create an HTTP client for the target API\nclient = httpx.AsyncClient(base_url=\"https://jsonplaceholder.typicode.com\")\n\n# Define a simplified OpenAPI spec for JSONPlaceholder\nopenapi_spec = {\n    \"openapi\": \"3.0.0\",\n    \"info\": {\"title\": \"JSONPlaceholder API\", \"version\": \"1.0\"},\n    \"paths\": {\n        \"/users\": {\n            \"get\": {\n                \"summary\": \"Get all users\",\n                \"operationId\": \"get_users\",\n                \"responses\": {\"200\": {\"description\": \"A list of users.\"}}\n            }\n        },\n        \"/users/{id}\": {\n            \"get\": {\n                \"summary\": \"Get a user by ID\",\n                \"operationId\": \"get_user_by_id\",\n                \"parameters\": [{\"name\": \"id\", \"in\": \"path\", \"required\": True, \"schema\": {\"type\": \"integer\"}}],\n                \"responses\": {\"200\": {\"description\": \"A single user.\"}}\n            }\n        }\n    }\n}\n\n# Create the MCP server from the OpenAPI spec\nmcp = FastMCP.from_openapi(\n    openapi_spec=openapi_spec,\n    client=client,\n    name=\"JSONPlaceholder MCP Server\"\n)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\nAnd that's it! With just a few lines of code, you've created an MCP server that exposes the entire JSONPlaceholder API as a collection of tools.\n\n## Step 3: Test the Generated Server\n\nLet's verify that our new MCP server works. We can use the `fastmcp.Client` to connect to it and inspect its tools.\n\n<Tip>\nLearn more about the FastMCP client in the [client docs](/clients/client).\n</Tip>\n\nCreate a separate file, `api_client.py`:\n\n```python api_client.py {2, 6, 9, 16}\nimport asyncio\nfrom fastmcp import Client\n\nasync def main():\n    # Connect to the MCP server we just created\n    async with Client(\"http://127.0.0.1:8000/mcp\") as client:\n        \n        # List the tools that were automatically generated\n        tools = await client.list_tools()\n        print(\"Generated Tools:\")\n        for tool in tools:\n            print(f\"- {tool.name}\")\n            \n        # Call one of the generated tools\n        print(\"\\n\\nCalling tool 'get_user_by_id'...\")\n        user = await client.call_tool(\"get_user_by_id\", {\"id\": 1})\n        print(f\"Result:\\n{user.data}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nFirst, run your server:\n```bash\npython api_server.py\n```\n\nThen, in another terminal, run the client:\n```bash\npython api_client.py\n```\n\nYou should see a list of generated tools (`get_users`, `get_user_by_id`) and the result of calling the `get_user_by_id` tool, which fetches data from the live JSONPlaceholder API.\n\n![](/assets/images/tutorial-rest-api-result.png)\n\n\n## Step 4: Customizing Route Maps\n\nBy default, FastMCP converts every API endpoint into an MCP `Tool`. This ensures maximum compatibility with contemporary LLM clients, many of which **only support the `tools` part of the MCP specification.**\n\nHowever, for clients that support the full MCP spec, representing `GET` requests as `Resources` can be more semantically correct and efficient.\n\nFastMCP allows users to customize this behavior using the concept of \"route maps\". A `RouteMap` is a mapping of an API route to an MCP type. FastMCP checks each API route against your custom maps in order. If a route matches a map, it's converted to the specified `mcp_type`. Any route that doesn't match your custom maps will fall back to the default behavior (becoming a `Tool`).\n\n<Tip>\nLearn more about route maps in the [OpenAPI integration docs](/integrations/openapi#route-mapping).\n</Tip>\n\nHere’s how you can add custom route maps to turn `GET` requests into `Resources` and `ResourceTemplates` (if they have path parameters):\n\n```python api_server_with_resources.py {3, 37-42}\nimport httpx\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\n\n# Create an HTTP client for the target API\nclient = httpx.AsyncClient(base_url=\"https://jsonplaceholder.typicode.com\")\n\n# Define a simplified OpenAPI spec for JSONPlaceholder\nopenapi_spec = {\n    \"openapi\": \"3.0.0\",\n    \"info\": {\"title\": \"JSONPlaceholder API\", \"version\": \"1.0\"},\n    \"paths\": {\n        \"/users\": {\n            \"get\": {\n                \"summary\": \"Get all users\",\n                \"operationId\": \"get_users\",\n                \"responses\": {\"200\": {\"description\": \"A list of users.\"}}\n            }\n        },\n        \"/users/{id}\": {\n            \"get\": {\n                \"summary\": \"Get a user by ID\",\n                \"operationId\": \"get_user_by_id\",\n                \"parameters\": [{\"name\": \"id\", \"in\": \"path\", \"required\": True, \"schema\": {\"type\": \"integer\"}}],\n                \"responses\": {\"200\": {\"description\": \"A single user.\"}}\n            }\n        }\n    }\n}\n\n# Create the MCP server with custom route mapping\nmcp = FastMCP.from_openapi(\n    openapi_spec=openapi_spec,\n    client=client,\n    name=\"JSONPlaceholder MCP Server\",\n    route_maps=[\n        # Map GET requests with path parameters (e.g., /users/{id}) to ResourceTemplate\n        RouteMap(methods=[\"GET\"], pattern=r\".*\\{.*\\}.*\", mcp_type=MCPType.RESOURCE_TEMPLATE),\n        # Map all other GET requests to Resource\n        RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE),\n    ]\n)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\nWith this configuration:\n- `GET /users/{id}` becomes a `ResourceTemplate`.\n- `GET /users` becomes a `Resource`.\n- Any `POST`, `PUT`, etc. endpoints would still become `Tools` by default."
  },
  {
    "path": "docs/unify-intent.js",
    "content": "// Load Unify intent tag on authentication pages only\n(function () {\n  if (typeof window === \"undefined\") return;\n\n  function isAuthPage() {\n    var path = window.location.pathname;\n    return path.includes(\"/servers/auth/\") || path.includes(\"/clients/auth/\");\n  }\n\n  function loadUnify() {\n    var e = [\n      \"identify\",\n      \"page\",\n      \"startAutoPage\",\n      \"stopAutoPage\",\n      \"startAutoIdentify\",\n      \"stopAutoIdentify\",\n    ];\n    function t(o) {\n      return Object.assign(\n        [],\n        e.reduce(function (r, n) {\n          r[n] = function () {\n            return o.push([n, [].slice.call(arguments)]), o;\n          };\n          return r;\n        }, {}),\n      );\n    }\n    if (!window.unify) window.unify = t(window.unify);\n    if (!window.unifyBrowser) window.unifyBrowser = t(window.unifyBrowser);\n\n    var n = document.createElement(\"script\");\n    n.async = true;\n    n.setAttribute(\n      \"src\",\n      \"https://tag.unifyintent.com/v1/Rj9KrQqMhyYcU5qfJtVszE/script.js\",\n    );\n    n.setAttribute(\n      \"data-api-key\",\n      \"wk_SBvJ4jyD_wRgPAHCNJb89seVmREhcj2NspRpxAywi\",\n    );\n    n.setAttribute(\"id\", \"unifytag\");\n    (document.body || document.head).appendChild(n);\n  }\n\n  function update() {\n    if (isAuthPage() && !document.getElementById(\"unifytag\")) {\n      loadUnify();\n    } else if (!isAuthPage() && document.getElementById(\"unifytag\")) {\n      document.getElementById(\"unifytag\").remove();\n    }\n  }\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", update);\n  } else {\n    update();\n  }\n\n  var lastUrl = location.href;\n  new MutationObserver(function () {\n    if (location.href !== lastUrl) {\n      lastUrl = location.href;\n      setTimeout(update, 100);\n    }\n  }).observe(document.body, { subtree: true, childList: true });\n})();\n"
  },
  {
    "path": "docs/updates.mdx",
    "content": "---\ntitle: \"FastMCP Updates\"\nsidebarTitle: \"Updates\"\nicon: \"sparkles\"\ntag: NEW\n---\n\n<Update label=\"FastMCP 3.0.2\" description=\"February 22, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP v3.0.2: Threecovery Mode II\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.2\"\ncta=\"Read the release notes\"\n>\nTwo community-contributed fixes: auth headers from MCP transport no longer leak through to downstream OpenAPI APIs, and background task workers now correctly receive the originating request ID. Plus a new docs example for context-aware tool factories.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 3.0.1\" description=\"February 20, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP v3.0.1: Three-covery Mode\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.1\"\ncta=\"Read the release notes\"\n>\nFirst patch after 3.0 — mostly smoothing out rough edges discovered in the wild. The big ones: middleware state that wasn't surviving the trip to tool handlers now does, `Tool.from_tool()` accepts callables again, OpenAPI schemas with circular references no longer crash discovery, and decorator overloads now return the correct types in function mode.\n\n🔐 **OIDC `verify_id_token`** — New option for providers that issue opaque access tokens but standard JWT id_tokens. Verifies identity via the id_token while using the access_token for upstream API calls.\n\n🐞 **11 bug fixes** — State serialization, future annotations with `Context`/`Depends`, OpenAI handler deprecation warnings, type checker compatibility, and more.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 3.0.0\" description=\"February 18, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP v3.0.0: Three at Last\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0\"\ncta=\"Read the release notes\"\nimg=\"assets/updates/release-3-0.png\"\n>\nFastMCP 3.0 is stable. Two betas, two release candidates, 21 new contributors, and more than 100,000 pre-release installs later — the architecture held up, the upgrade path was smooth, and we're shipping it.\n\nThe surface API is largely unchanged — `@mcp.tool()` still works exactly as before. What changed is everything underneath: a provider/transform architecture that makes FastMCP extensible, observable, and composable in ways v2 couldn't support.\n\n🔌 **Build servers from anything** — `FileSystemProvider`, `OpenAPIProvider`, `ProxyProvider`, `SkillsProvider`, and composable transforms that rename, namespace, filter, version, and secure components as they flow to clients.\n\n🔐 **Ship to production** — Component versioning, granular authorization with async auth checks, CIMD, Static Client Registration, Azure OBO, OpenTelemetry tracing, and background tasks with distributed Redis notification.\n\n💾 **Adapt per session** — Session state persists across requests, and `ctx.enable_components()` / `ctx.disable_components()` let servers adapt dynamically per client.\n\n⚡ **Develop faster** — `--reload`, standalone decorators, automatic threadpool dispatch, tool timeouts, pagination, and concurrent tool execution.\n\n🖥️ **CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, `fastmcp generate-cli`, and `fastmcp install` for Claude Desktop, Cursor, and Goose.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 3.0.0rc1\" description=\"February 12, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP v3.0.0rc1: RC-ing is Believing\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0rc1\"\ncta=\"Read the release notes\"\n>\nFastMCP 3 RC1 means we believe the API is stable. Beta 2 drew a wave of real-world adoption — production deployments, migration reports, integration testing — and the feedback overwhelmingly confirmed that the architecture works. This release closes gaps that surfaced under load: auth flows that needed to be async, background tasks that needed reliable notification delivery, and APIs still carrying beta-era naming. If nothing unexpected surfaces, this is what 3.0.0 looks like.\n\n🚨 **Breaking Changes** — The `ui=` parameter is now `app=` with a unified `AppConfig` class, and 16 `FastMCP()` constructor kwargs have been removed after months of deprecation warnings.\n\n🔐 **Auth Improvements** — Async `auth=` checks, Static Client Registration for servers without DCR, and declarative Azure OBO flows via dependency injection.\n\n⚡ **Concurrent Sampling** — `context.sample()` can now execute multiple tool calls in parallel with `tool_concurrency=0`.\n\n📡 **Background Task Notifications** — A distributed Redis queue replaces polling for progress updates and elicitation relay.\n\n✅ **OpenAPI Output Validation** — `validate_output=False` disables strict schema checking for imperfect backend APIs.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 3.0.0b2\" description=\"February 7, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP v3.0.0b2: 2 Fast 2 Beta\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0b2\"\ncta=\"Read the release notes\"\n>\nBeta 2 reflects the huge number of people that kicked the tires on Beta 1. Seven new contributors landed changes, and early migration reports went smoother than expected. Most of Beta 2 is refinement — fixing what people found, filling gaps from real usage, hardening edges — but a few new features landed along the way.\n\n🖥️ **Client CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, and `fastmcp generate-cli` turn any MCP server into something you can poke at from a terminal.\n\n🔐 **CIMD** (Client ID Metadata Documents) adds an alternative to Dynamic Client Registration for OAuth.\n\n📱 **MCP Apps** — Spec-level compliance for the MCP Apps extension with `ui://` resource scheme and typed UI metadata.\n\n⏳ **Background Task Context** — `Context` now works transparently in Docket workers with Redis-based coordination.\n\n🛡️ **ResponseLimitingMiddleware** caps tool response sizes with UTF-8-safe truncation.\n\n🪿 **Goose Integration** — `fastmcp install goose` for one-command server installation into Goose.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 3.0.0b1\" description=\"January 20, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 3.0.0b1: This Beta Work\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0b1\"\ncta=\"Read the release notes\"\n>\nFastMCP 3.0 rebuilds the framework around three primitives: components, providers, and transforms. Providers source components dynamically—from decorators, filesystems, OpenAPI specs, remote servers, or anywhere else. Transforms modify components as they flow to clients. The features that required specialized subsystems in v2 now compose naturally from these building blocks.\n\n🔌 **Provider Architecture** unifies how components are sourced with `FileSystemProvider`, `SkillsProvider`, `OpenAPIProvider`, and `ProxyProvider`.\n\n🔄 **Transforms** add middleware for components—namespace, rename, filter by version, control visibility.\n\n📋 **Component Versioning** lets you register multiple versions of the same tool with automatic highest-version selection.\n\n💾 **Session-Scoped State** persists across requests, with per-session visibility control.\n\n⚡ **DX Improvements** include `--reload` for development, automatic threadpool dispatch, tool timeouts, pagination, and OpenTelemetry tracing.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.5\" description=\"February 3, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.5: Sealed Docket\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5\"\ncta=\"Read the release notes\"\n>\nFixes a memory leak in the memory:// docket broker where cancelled tasks accumulated instead of being cleaned up. Bumps pydocket to ≥0.17.2.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.4\" description=\"January 22, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.4: Package Deal\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.4\"\ncta=\"Read the release notes\"\n>\nFixes a fresh install bug where the packaging library was missing as a direct dependency, plus backports $ref dereferencing in tool schemas and a task capabilities location fix.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.3\" description=\"January 12, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.3: Time After Timeout\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.3\"\ncta=\"Read the release notes\"\n>\nSometimes five seconds just isn't enough. This release fixes an HTTP transport bug that was cutting connections short, along with OAuth and Redis fixes, better ASGI support, and CLI update notifications so you never miss a beat.\n\n⏱️ **HTTP transport timeout fix** restores MCP's 30-second default connect timeout, which was incorrectly defaulting to 5 seconds.\n\n🔧 **Infrastructure fixes** including OAuth token storage TTL, Redis key prefixing for ACL isolation, and ContextVar propagation for ASGI-mounted servers with background tasks.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.2\" description=\"December 31, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.2: Port Authority\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.2\"\ncta=\"Read the release notes\"\n>\nA wave of community contributions arrives safely in the 2.x line. Important backports from 3.0 improve OpenAPI 3.1 compatibility, MCP spec compliance for output schemas and elicitation, and correct a subtle base_url fallback issue.\n\n🔧 **OpenAPI 3.1 support** fixes version detection to properly handle 3.1 specs alongside 3.0.\n\n📋 **MCP spec compliance** for root-level `$ref` resolution in output schemas and titled enum elicitation schemas.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.1\" description=\"December 15, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.1: 'Tis a Gift to Be Sample\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.1\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.14.1 introduces sampling with tools (SEP-1577), enabling servers to pass tools to `ctx.sample()` for agentic workflows where the LLM can automatically execute tool calls in a loop.\n\n🤖 **Sampling with tools** lets servers leverage client LLM capabilities for multi-step agentic workflows. The new `ctx.sample_step()` method provides single LLM calls with tool inspection, while `result_type` enables structured outputs via validated Pydantic models.\n\n🔧 **AnthropicSamplingHandler** joins the existing OpenAI handler, and both are now promoted from experimental to production-ready status with a unified API.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.0\" description=\"December 11, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.0: Task and You Shall Receive\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.0\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.14 begins adopting the MCP 2025-11-25 specification, introducing protocol-native background tasks that enable long-running operations to report progress without blocking clients.\n\n⏳ **Background Tasks (SEP-1686)** let you add `task=True` to any async tool decorator. Powered by [Docket](https://github.com/chrisguidry/docket) for enterprise task scheduling—in-memory backends work out-of-the-box, Redis enables persistence and horizontal scaling.\n\n🔧 **OpenAPI Parser Promoted** from experimental to standard with improved performance through single-pass schema processing.\n\n📋 **MCP Spec Updates** including SSE polling (SEP-1699), multi-select elicitation (SEP-1330), and tool name validation (SEP-986). Also removes deprecated APIs accumulated across 2.x.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.3\" description=\"December 3, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.3: Pin-ish Line\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.3\"\ncta=\"Read the release notes\"\n>\nPins `mcp<1.23` as a precaution due to MCP SDK changes related to the 11/25/25 protocol update that break certain FastMCP patches and workarounds. FastMCP 2.14 introduces proper support for the updated protocol.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.2\" description=\"December 1, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.2: Refreshing Changes\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.2\"\ncta=\"Read the release notes\"\n>\nPolishes the authentication stack with improvements to token refresh, scope handling, and multi-instance deployments.\n\n🎮 **Discord OAuth provider** added as a built-in authentication option.\n\n🔄 **Token refresh fixes** for Azure and Google providers, plus OAuth proxy improvements for multi-instance deployments.\n\n🎨 **Icon support** added to proxy classes for richer UX.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.1\" description=\"November 15, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.1: Heavy Meta\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.1\"\ncta=\"Read the release notes\"\n>\nIntroduces meta parameter support for `ToolResult`, enabling tools to return supplementary metadata alongside results for patterns like OpenAI's Apps SDK.\n\n🏷️ **Meta parameters** let tools return supplementary metadata alongside results.\n\n🔐 **New auth providers** for OCI and Supabase, plus custom token verifiers with DebugTokenVerifier for development.\n\n🔒 **Security fixes** for CVE-2025-61920 and safer Cursor deeplink URL validation on Windows.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.0\" description=\"October 25, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.0: Cache Me If You Can\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.0\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.13 \"Cache Me If You Can\" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.\n\n💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more!\n\n🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions, while the OAuth proxy now issues its own tokens with automatic key derivation. RFC 7662 token introspection support enables enterprise auth flows, and path prefix mounting enables OAuth-protected servers to integrate into existing web applications. FastMCP now supports out-of-the-box authentication with [WorkOS](https://gofastmcp.com/integrations/workos) and [AuthKit](https://gofastmcp.com/integrations/authkit), [GitHub](https://gofastmcp.com/integrations/github), [Google](https://gofastmcp.com/integrations/google), [Azure](https://gofastmcp.com/integrations/azure) (Entra ID), [AWS Cognito](https://gofastmcp.com/integrations/aws-cognito), [Auth0](https://gofastmcp.com/integrations/auth0), [Descope](https://gofastmcp.com/integrations/descope), [Scalekit](https://gofastmcp.com/integrations/scalekit), [JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification), and [RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol).\n\n⚡ **Response Caching Middleware** dramatically improves performance for expensive operations, while **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session.\n\n✨ **Developer experience improvements** include Pydantic input validation, icon support, RFC 6570 query parameters for resource templates, improved Context API methods, and async file/directory resources.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.5\" description=\"October 17, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.5: Safety Pin\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.5\"\ncta=\"Read the release notes\"\n>\nPins MCP SDK version below 1.17 to ensure the `.well-known` payload appears in the expected location when using FastMCP auth providers with composite applications.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.4\" description=\"September 26, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.4: OIDC What You Did There\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.4\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.12.4 adds comprehensive OIDC support and expands authentication options with AWS Cognito and Descope providers. The release also includes improvements to logging middleware, URL handling for nested resources, persistent OAuth client registration storage, and various fixes to the experimental OpenAPI parser.\n\n🔐 **OIDC Configuration** brings native support for OpenID Connect, enabling seamless integration with enterprise identity providers.\n\n🏢 **Enterprise Authentication** expands with AWS Cognito and Descope providers, broadening the authentication ecosystem.\n\n🛠️ **Improved Reliability** through enhanced URL handling, persistent OAuth storage, and numerous parser fixes based on community feedback.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.3\" description=\"September 17, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.3: Double Time\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.3\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.12.3 focuses on performance and developer experience improvements. This release includes optimized auth provider imports that reduce server startup time, enhanced OIDC authentication flows, and automatic inline snapshot creation for testing.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.2\" description=\"September 3, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.2: Perchance to Stream\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.2\"\ncta=\"Read the release notes\"\n>\nHotfix for streamable-http transport validation in fastmcp.json configuration files, resolving a parsing error when CLI arguments were merged against the configuration spec.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.1\" description=\"September 3, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.1: OAuth to Joy\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.1\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.12.1 strengthens OAuth proxy implementation with improved client storage reliability, PKCE forwarding, configurable token endpoint authentication methods, and expanded scope handling based on extensive community testing.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12\" description=\"August 31, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.12: Auth to the Races\" \nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.0\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.12 represents one of our most significant releases to date. After extensive testing and iteration with the community, we're shipping major improvements to authentication, configuration, and MCP feature adoption.\n\n🔐 **OAuth Proxy** bridges the gap for providers that don't support Dynamic Client Registration, enabling authentication with GitHub, Google, WorkOS, and Azure through minimal configuration.\n\n📋 **Declarative JSON Configuration** introduces `fastmcp.json` as the single source of truth for server settings, making MCP servers as portable and shareable as container images.\n\n🧠 **Sampling API Fallback** tackles adoption challenges by letting servers generate completions server-side when clients don't support the feature, encouraging innovation while maintaining compatibility.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.11\" description=\"August 1, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.11: Auth to a Good Start\" \nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.0\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.11 brings enterprise-ready authentication and dramatic performance improvements.\n\n🔒 **Comprehensive OAuth 2.1 Support** with WorkOS AuthKit integration, Dynamic Client Registration, and support for separate resource and authorization servers.\n\n⚡ **Experimental OpenAPI Parser** delivers dramatic performance gains through single-pass schema processing and optimized memory usage (enable with environment variable).\n\n💾 **Enhanced State Management** provides persistent state across tool calls with a simple dictionary interface, improving context handling and type annotations.\n\nThis release emphasizes speed and simplicity while setting the foundation for future enterprise features.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.10\" description=\"July 2, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.10: Great Spec-tations\" \nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.0\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.10 achieves full compliance with the 6/18/2025 MCP specification update, introducing powerful new communication patterns.\n\n💬 **Elicitation Support** enables dynamic server-client communication and \"human-in-the-loop\" workflows, allowing servers to request additional information during execution.\n\n📊 **Output Schemas** provide structured outputs for tools, making results more predictable and easier to parse programmatically.\n\n🛠️ **Enhanced HTTP Routing** with OpenAPI extensions support and configurable algorithms for more flexible API integration.\n\nThis release includes a breaking change to `client.call_tool()` return signatures but significantly expands the interaction capabilities of MCP servers.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.9\" description=\"June 23, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card \ntitle=\"FastMCP 2.9: MCP-Native Middleware\" href=\"https://www.jlowin.dev/blog/fastmcp-2-9-middleware\" \nimg=\"https://jlowin.dev/_image?href=%2F_astro%2Fhero.BkVTdeBk.jpg&w=1200&h=630&f=png\" \ncta=\"Read more\"  \n>\nFastMCP 2.9 is a major release that, among other things, introduces two important features that push beyond the basic MCP protocol. \n\n🤝 *MCP Middleware* brings a flexible middleware system for intercepting and controlling server operations - think authentication, logging, rate limiting, and custom business logic without touching core protocol code. \n\n✨ *Server-side type conversion* for prompts solves a major developer pain point: while MCP requires string arguments, your functions can now work with native Python types like lists and dictionaries, with automatic conversion handling the complexity.\n\nThese features transform FastMCP from a simple protocol implementation into a powerful framework for building sophisticated MCP applications. Combined with the new `File` utility for binary data and improvements to authentication and serialization, this release makes FastMCP significantly more flexible and developer-friendly while maintaining full protocol compliance.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.8\" description=\"June 11, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card \ntitle=\"FastMCP 2.8: Transform and Roll Out\" href=\"https://www.jlowin.dev/blog/fastmcp-2-8-tool-transformation\" \nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.su3kspkP.png&w=1000&h=500&f=webp\" \ncta=\"Read more\"  \n>\nFastMCP 2.8 is here, and it's all about taking control of your tools.\n\nThis release is packed with new features for curating the perfect LLM experience:\n\n🛠️ Tool Transformation\n\nThe headline feature lets you wrap any tool—from your own code, a third-party library, or an OpenAPI spec—to create an enhanced, LLM-friendly version. You can rename arguments, rewrite descriptions, and hide parameters without touching the original code.\n\nThis feature was developed in close partnership with Bill Easton. As Bill brilliantly [put it](https://www.linkedin.com/posts/williamseaston_huge-thanks-to-william-easton-for-providing-activity-7338011349525983232-Mw6T?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAAd6d0B3uL9zpCsq9eYWKi3HIvb8eN_r_Q), \"Tool transformation flips Prompt Engineering on its head: stop writing tool-friendly LLM prompts and start providing LLM-friendly tools.\"\n\n🏷️ Component Control\n\nNow that you're transforming tools, you need a way to hide the old ones! In FastMCP 2.8 you can programmatically enable/disable any component, and for everyone who's been asking what FastMCP's tags are for—they finally have a purpose! You can now use tags to declaratively filter which components are exposed to your clients.\n\n🚀 Pragmatic by Default\n\nLastly, to ensure maximum compatibility with the ecosystem, we've made the pragmatic decision to default all OpenAPI routes to Tools, making your entire API immediately accessible to any tool-using agent. When the industry catches up and supports resources, we'll restore the old default -- but no reason you should do extra work before OpenAI, Anthropic, or Google!\n\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.7\" description=\"June 6, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.7: Pare Programming\" href=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.0\" \nimg=\"assets/updates/release-2-7.png\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.7 has been released!\n\nMost notably, it introduces the highly requested (and Pythonic) \"naked\" decorator usage:\n\n```python {3}\nmcp = FastMCP()\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    return a + b\n```\n\nIn addition, decorators now return the objects they create, instead of the decorated function. This is an important usability enhancement.\n\nThe bulk of the update is focused on improving the FastMCP internals, including a few breaking internal changes to private APIs. A number of functions that have clung on since 1.0 are now deprecated.\n</Card>\n</Update>\n\n\n\n<Update label=\"FastMCP 2.6\" description=\"June 2, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card \ntitle=\"FastMCP 2.6: Blast Auth\" href=\"https://www.jlowin.dev/blog/fastmcp-2-6\" \nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.Bsu8afiw.png&w=1000&h=500&f=webp\" \ncta=\"Read more\"  \n>\nFastMCP 2.6 is here!\n\nThis release introduces first-class authentication for MCP servers and clients, including pragmatic Bearer token support and seamless OAuth 2.1 integration. This release aligns with how major AI platforms are adopting MCP today, making it easier than ever to securely connect your tools to real-world AI models. Dive into the update and secure your stack with minimal friction.\n</Card>\n</Update>\n\n<Update description=\"May 21, 2025\" label=\"Vibe-Testing\" tags={[\"Blog Posts\", \"Tutorials\"]}>\n<Card\ntitle=\"Stop Vibe-Testing Your MCP Server\"\nhref=\"https://www.jlowin.dev/blog/stop-vibe-testing-mcp-servers\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.BUPy9I9c.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nYour tests are bad and you should feel bad.\n\nStop vibe-testing your MCP server through LLM guesswork. FastMCP 2.0 introduces in-memory testing for fast, deterministic, and fully Pythonic validation of your MCP logic—no network, no subprocesses, no vibes.\n\n</Card>\n</Update>\n\n\n<Update description=\"May 8, 2025\" label=\"10,000 Stars\" tags={[\"Blog Posts\"]}>\n<Card\ntitle=\"Reflecting on FastMCP at 10k stars 🌟\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-2-10k-stars\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.Cnvci9Q_.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nIn just six weeks since its relaunch, FastMCP has surpassed 10,000 GitHub stars—becoming the fastest-growing OSS project in our orbit. What started as a personal itch has become the backbone of Python-based MCP servers, powering a rapidly expanding ecosystem. While the protocol itself evolves, FastMCP continues to lead with clarity, developer experience, and opinionated tooling. Here’s to what’s next.\n\n</Card>\n</Update>\n\n<Update description=\"May 8, 2025\" label=\"FastMCP 2.3\" tags={[\"Blog Posts\", \"Releases\"]}>\n<Card\ntitle=\"Now Streaming: FastMCP 2.3\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-2-3-streamable-http\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.M_hv6gEB.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nFastMCP 2.3 introduces full support for Streamable HTTP, a modern alternative to SSE that simplifies MCP deployments over the web. It’s efficient, reliable, and now the default HTTP transport. Just run your server with transport=\"http\" and connect clients via a standard URL—FastMCP handles the rest. No special setup required. This release makes deploying MCP servers easier and more portable than ever.\n\n</Card>\n</Update>\n\n<Update description=\"April 23, 2025\" label=\"Proxy Servers\" tags={[\"Blog Posts\", \"Tutorials\"]}>\n<Card\ntitle=\"MCP Proxy Servers with FastMCP 2.0\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-proxy\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Frobot-hero.DpmAqgui.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nEven AI needs a good travel adapter 🔌\n\n\nFastMCP now supports proxying arbitrary MCP servers, letting you run a local FastMCP instance that transparently forwards requests to any remote or third-party server—regardless of transport. This enables transport bridging (e.g., stdio ⇄ SSE), simplified client configuration, and powerful gateway patterns. Proxies are fully composable with other FastMCP servers, letting you mount or import them just like local servers. Use `FastMCP.from_client()` to wrap any backend in a clean, Pythonic proxy.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.0\" description=\"April 16, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card\ntitle=\"Introducing FastMCP 2.0 🚀\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-2\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.DpbmGNrr.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nThis major release reimagines FastMCP as a full ecosystem platform, with powerful new features for composition, integration, and client interaction. You can now compose local and remote servers, proxy arbitrary MCP servers (with transport translation), and generate MCP servers from OpenAPI or FastAPI apps. A new client infrastructure supports advanced workflows like LLM sampling. \n\nFastMCP 2.0 builds on the success of v1 with a cleaner, more flexible foundation—try it out today!\n</Card>\n</Update>\n\n\n\n<Update label=\"Official SDK\" description=\"December 3, 2024\" tags={[\"Announcements\"]}>\n<Card\ntitle=\"FastMCP is joining the official MCP Python SDK!\"\nhref=\"https://bsky.app/profile/jlowin.dev/post/3lch4xk5cf22c\"\nicon=\"sparkles\"\ncta=\"Read the announcement\"\n>\nFastMCP 1.0 will become part of the official MCP Python SDK!\n</Card>\n</Update>\n\n\n\n<Update label=\"FastMCP 1.0\" description=\"December 1, 2024\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card\ntitle=\"Introducing FastMCP 🚀\"\nhref=\"https://www.jlowin.dev/blog/introducing-fastmcp\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Ffastmcp.Bep7YlTw.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\nBecause life's too short for boilerplate.\n\nThis is where it all started. FastMCP’s launch post introduced a clean, Pythonic way to build MCP servers without the protocol overhead. Just write functions; FastMCP handles the rest. What began as a weekend project quickly became the foundation of a growing ecosystem.\n</Card>\n</Update>\n\n"
  },
  {
    "path": "docs/v2/changelog.mdx",
    "content": "---\ntitle: \"Changelog\"\nicon: \"list-check\"\nrss: true\n---\n\n<Update label=\"v2.14.5\" description=\"2026-02-03\">\n\n**[v2.14.5: Sealed Docket](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5)**\n\nFixes a memory leak in the memory:// docket broker where cancelled tasks accumulated instead of being cleaned up. Bumps pydocket to ≥0.17.2.\n\n## What's Changed\n### Enhancements 🔧\n* Bump pydocket to 0.17.2 (memory leak fix) by [@chrisguidry](https://github.com/chrisguidry) in [#2992](https://github.com/PrefectHQ/fastmcp/pull/2992)\n\n**Full Changelog**: [v2.14.4...v2.14.5](https://github.com/PrefectHQ/fastmcp/compare/v2.14.4...v2.14.5)\n\n</Update>\n\n<Update label=\"v2.14.4\" description=\"2026-01-22\">\n\n**[v2.14.4: Package Deal](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.4)**\n\nFixes a fresh install bug where the packaging library was missing as a direct dependency, plus backports from 3.x for $ref dereferencing in tool schemas and a task capabilities location fix.\n\n## What's Changed\n### Enhancements 🔧\n* Add release notes for v2.14.2 and v2.14.3 by [@jlowin](https://github.com/jlowin) in [#2851](https://github.com/PrefectHQ/fastmcp/pull/2851)\n### Fixes 🐞\n* Backport: Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2861](https://github.com/PrefectHQ/fastmcp/pull/2861)\n* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2874](https://github.com/PrefectHQ/fastmcp/pull/2874)\n* Add missing packaging dependency by [@jlowin](https://github.com/jlowin) in [#2989](https://github.com/PrefectHQ/fastmcp/pull/2989)\n\n**Full Changelog**: [v2.14.3...v2.14.4](https://github.com/PrefectHQ/fastmcp/compare/v2.14.3...v2.14.4)\n\n</Update>\n\n<Update label=\"v2.14.3\" description=\"2026-01-12\">\n\n**[v2.14.3: Time After Timeout](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.3)**\n\nSometimes five seconds just isn't enough. This release fixes an HTTP transport bug that was cutting connections short, along with OAuth and Redis fixes, better ASGI support, and CLI update notifications so you never miss a beat.\n\n## What's Changed\n### Enhancements 🔧\n* Add debug logging for OAuth token expiry diagnostics by [@jlowin](https://github.com/jlowin) in [#2789](https://github.com/PrefectHQ/fastmcp/pull/2789)\n* Add CLI update notifications by [@jlowin](https://github.com/jlowin) in [#2839](https://github.com/PrefectHQ/fastmcp/pull/2839)\n* Use pip instead of uv pip in upgrade instructions by [@jlowin](https://github.com/jlowin) in [#2841](https://github.com/PrefectHQ/fastmcp/pull/2841)\n### Fixes 🐞\n* Backport OAuth token storage TTL fix to release/2.x by [@jlowin](https://github.com/jlowin) in [#2798](https://github.com/PrefectHQ/fastmcp/pull/2798)\n* Prefix Redis keys with docket name for ACL isolation (2.x backport) by [@chrisguidry](https://github.com/chrisguidry) in [#2812](https://github.com/PrefectHQ/fastmcp/pull/2812)\n* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2843](https://github.com/PrefectHQ/fastmcp/pull/2843)\n* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2848](https://github.com/PrefectHQ/fastmcp/pull/2848)\n\n**Full Changelog**: [v2.14.2...v2.14.3](https://github.com/PrefectHQ/fastmcp/compare/v2.14.2...v2.14.3)\n\n</Update>\n\n<Update label=\"v2.14.2\" description=\"2025-12-31\">\n\n**[v2.14.2: Port Authority](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.2)**\n\nFastMCP 2.14.2 brings a wave of community contributions safely into the 2.x line. A variety of important fixes backported from 3.0 work improve OpenAPI 3.1 compatibility, MCP spec compliance for output schemas and elicitation, and correct a subtle base_url fallback issue. The CLI now gently reminds you that FastMCP 3.0 is on the horizon.\n\n## What's Changed\n### Enhancements 🔧\n* Pin MCP under 2.x by [@jlowin](https://github.com/jlowin) in [#2709](https://github.com/PrefectHQ/fastmcp/pull/2709)\n* Add auth_route parameter to SupabaseProvider by [@EloiZalczer](https://github.com/EloiZalczer) in [#2760](https://github.com/PrefectHQ/fastmcp/pull/2760)\n* Update CLI banner with FastMCP 3.0 notice by [@jlowin](https://github.com/jlowin) in [#2765](https://github.com/PrefectHQ/fastmcp/pull/2765)\n### Fixes 🐞\n* Let FastMCPError propagate unchanged from managers by [@jlowin](https://github.com/jlowin) in [#2697](https://github.com/PrefectHQ/fastmcp/pull/2697)\n* Fix test cleanup for uvicorn 0.39+ context isolation by [@jlowin](https://github.com/jlowin) in [#2696](https://github.com/PrefectHQ/fastmcp/pull/2696)\n* Bump pydocket to 0.16.3 to fix worker cleanup race condition by [@chrisguidry](https://github.com/chrisguidry) in [#2700](https://github.com/PrefectHQ/fastmcp/pull/2700)\n* Fix Prefect website URL in docs footer by [@mgoldsborough](https://github.com/mgoldsborough) in [#2705](https://github.com/PrefectHQ/fastmcp/pull/2705)\n* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2727](https://github.com/PrefectHQ/fastmcp/pull/2727)\n* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2763](https://github.com/PrefectHQ/fastmcp/pull/2763)\n* Fix openapi_version check to include 3.1 by [@deeleeramone](https://github.com/deeleeramone) in [#2769](https://github.com/PrefectHQ/fastmcp/pull/2769)\n* Fix titled enum elicitation schema to comply with MCP spec by [@jlowin](https://github.com/jlowin) in [#2774](https://github.com/PrefectHQ/fastmcp/pull/2774)\n* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2782](https://github.com/PrefectHQ/fastmcp/pull/2782)\n* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2785](https://github.com/PrefectHQ/fastmcp/pull/2785)\n### Docs 📚\n* Add v3 breaking changes notice to README and docs by [@jlowin](https://github.com/jlowin) in [#2713](https://github.com/PrefectHQ/fastmcp/pull/2713)\n* Add changelog entries for v2.13.1 through v2.14.1 by [@jlowin](https://github.com/jlowin) in [#2724](https://github.com/PrefectHQ/fastmcp/pull/2724)\n* conference to 2.x branch by [@aaazzam](https://github.com/aaazzam) in [#2787](https://github.com/PrefectHQ/fastmcp/pull/2787)\n\n**Full Changelog**: [v2.14.1...v2.14.2](https://github.com/PrefectHQ/fastmcp/compare/v2.14.1...v2.14.2)\n\n</Update>\n\n<Update label=\"v2.14.1\" description=\"2025-12-15\">\n\n**[v2.14.1: 'Tis a Gift to Be Sample](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.1)**\n\nFastMCP 2.14.1 introduces sampling with tools (SEP-1577), enabling servers to pass tools to `ctx.sample()` for agentic workflows where the LLM can automatically execute tool calls in a loop. The new `ctx.sample_step()` method provides single LLM calls that return `SampleStep` objects for custom control flow, while `result_type` enables structured outputs via validated Pydantic models.\n\n🤖 **AnthropicSamplingHandler** joins the existing OpenAI handler, providing multi-provider sampling support out of the box.\n\n⚡ **OpenAISamplingHandler promoted** from experimental status—sampling handlers are now production-ready with a unified API.\n\n## What's Changed\n### New Features 🎉\n* Sampling with tools by [@jlowin](https://github.com/jlowin) in [#2538](https://github.com/PrefectHQ/fastmcp/pull/2538)\n* Add AnthropicSamplingHandler by [@jlowin](https://github.com/jlowin) in [#2677](https://github.com/PrefectHQ/fastmcp/pull/2677)\n### Enhancements 🔧\n* Add Python 3.13 to ubuntu CI by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)\n* Remove legacy task initialization workaround by [@jlowin](https://github.com/jlowin) in [#2649](https://github.com/PrefectHQ/fastmcp/pull/2649)\n* Consolidate session state reset logic by [@jlowin](https://github.com/jlowin) in [#2651](https://github.com/PrefectHQ/fastmcp/pull/2651)\n* Unify SamplingHandler; promote OpenAI from experimental by [@jlowin](https://github.com/jlowin) in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)\n* Add `tool_names` parameter to mount() for name customization by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)\n* Use streamable HTTP client API from MCP SDK by [@jlowin](https://github.com/jlowin) in [#2678](https://github.com/PrefectHQ/fastmcp/pull/2678)\n* Deprecate `exclude_args` in favor of Depends() by [@jlowin](https://github.com/jlowin) in [#2693](https://github.com/PrefectHQ/fastmcp/pull/2693)\n### Fixes 🐞\n* Fix prompt tasks to return mcp.types.PromptMessage by [@jlowin](https://github.com/jlowin) in [#2650](https://github.com/PrefectHQ/fastmcp/pull/2650)\n* Fix Windows test warnings by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)\n* Cleanup cancelled connection startup by [@jlowin](https://github.com/jlowin) in [#2679](https://github.com/PrefectHQ/fastmcp/pull/2679)\n* Fix tool choice bug in sampling examples by [@shawnthapa](https://github.com/shawnthapa) in [#2686](https://github.com/PrefectHQ/fastmcp/pull/2686)\n### Docs 📚\n* Simplify Docket tip wording by [@chrisguidry](https://github.com/chrisguidry) in [#2662](https://github.com/PrefectHQ/fastmcp/pull/2662)\n### Other Changes 🦾\n* Bump pydocket to ≥0.15.5 by [@jlowin](https://github.com/jlowin) in [#2694](https://github.com/PrefectHQ/fastmcp/pull/2694)\n\n## New Contributors\n* [@shawnthapa](https://github.com/shawnthapa) made their first contribution in [#2686](https://github.com/PrefectHQ/fastmcp/pull/2686)\n\n**Full Changelog**: [v2.14.0...v2.14.1](https://github.com/PrefectHQ/fastmcp/compare/v2.14.0...v2.14.1)\n\n</Update>\n\n<Update label=\"v2.14.0\" description=\"2025-12-11\">\n\n**[v2.14.0: Task and You Shall Receive](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.0)**\n\nFastMCP 2.14 begins adopting the MCP 2025-11-25 specification, introducing protocol-native background tasks (SEP-1686) that enable long-running operations to report progress without blocking clients. The experimental OpenAPI parser graduates to standard, the `OpenAISamplingHandler` is promoted from experimental, and deprecated APIs accumulated across the 2.x series are removed.\n\n⏳ **Background Tasks** let you add `task=True` to any async tool decorator to run operations in the background with progress tracking. Powered by [Docket](https://github.com/chrisguidry/docket), an enterprise task scheduler handling millions of concurrent tasks daily—in-memory backends work out-of-the-box, and Redis URLs enable persistence and horizontal scaling.\n\n🔧 **OpenAPI Parser Promoted** from experimental to standard with improved performance through single-pass schema processing and cleaner abstractions.\n\n📋 **MCP 2025-11-25 Specification Support** including SSE polling and event resumability (SEP-1699), multi-select enum elicitation schemas (SEP-1330), default values for elicitation (SEP-1034), and tool name validation at registration time (SEP-986).\n\n## Breaking Changes\n- Docket is always enabled; task execution is forbidden through proxies\n- Task protocol enabled by default\n- Removed deprecated settings, imports, and methods accumulated across 2.x series\n\n## What's Changed\n### New Features 🎉\n* OpenAPI parser is now the default by [@jlowin](https://github.com/jlowin) in [#2583](https://github.com/PrefectHQ/fastmcp/pull/2583)\n* Implement SEP-1686: Background Tasks by [@jlowin](https://github.com/jlowin) in [#2550](https://github.com/PrefectHQ/fastmcp/pull/2550)\n### Enhancements 🔧\n* Expose InitializeResult in middleware by [@jlowin](https://github.com/jlowin) in [#2562](https://github.com/PrefectHQ/fastmcp/pull/2562)\n* Update MCP SDK auth compatibility by [@jlowin](https://github.com/jlowin) in [#2574](https://github.com/PrefectHQ/fastmcp/pull/2574)\n* Validate tool names at registration (SEP-986) by [@jlowin](https://github.com/jlowin) in [#2588](https://github.com/PrefectHQ/fastmcp/pull/2588)\n* Support SEP-1034 and SEP-1330 for elicitation by [@jlowin](https://github.com/jlowin) in [#2595](https://github.com/PrefectHQ/fastmcp/pull/2595)\n* Implement SSE polling (SEP-1699) by [@jlowin](https://github.com/jlowin) in [#2612](https://github.com/PrefectHQ/fastmcp/pull/2612)\n* Expose session ID callback by [@jlowin](https://github.com/jlowin) in [#2628](https://github.com/PrefectHQ/fastmcp/pull/2628)\n### Fixes 🐞\n* Fix OAuth metadata discovery by [@jlowin](https://github.com/jlowin) in [#2565](https://github.com/PrefectHQ/fastmcp/pull/2565)\n* Fix fastapi.cli package structure by [@jlowin](https://github.com/jlowin) in [#2570](https://github.com/PrefectHQ/fastmcp/pull/2570)\n* Correct OAuth error codes by [@jlowin](https://github.com/jlowin) in [#2578](https://github.com/PrefectHQ/fastmcp/pull/2578)\n* Prevent function signature modification by [@jlowin](https://github.com/jlowin) in [#2590](https://github.com/PrefectHQ/fastmcp/pull/2590)\n* Fix proxy client kwargs by [@jlowin](https://github.com/jlowin) in [#2605](https://github.com/PrefectHQ/fastmcp/pull/2605)\n* Fix nested server routing by [@jlowin](https://github.com/jlowin) in [#2618](https://github.com/PrefectHQ/fastmcp/pull/2618)\n* Use access token expiry fallback by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)\n* Handle transport cleanup exceptions by [@jlowin](https://github.com/jlowin) in [#2642](https://github.com/PrefectHQ/fastmcp/pull/2642)\n### Docs 📚\n* Add OCI and Supabase integration docs by [@jlowin](https://github.com/jlowin) in [#2580](https://github.com/PrefectHQ/fastmcp/pull/2580)\n* Add v2.14.0 upgrade guide by [@jlowin](https://github.com/jlowin) in [#2598](https://github.com/PrefectHQ/fastmcp/pull/2598)\n* Rewrite background tasks documentation by [@jlowin](https://github.com/jlowin) in [#2620](https://github.com/PrefectHQ/fastmcp/pull/2620)\n* Document read-only tool patterns by [@jlowin](https://github.com/jlowin) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)\n\n## New Contributors\n11 total contributors including 7 first-time participants.\n\n**Full Changelog**: [v2.13.3...v2.14.0](https://github.com/PrefectHQ/fastmcp/compare/v2.13.3...v2.14.0)\n\n</Update>\n\n<Update label=\"v2.13.3\" description=\"2025-12-03\">\n\n**[v2.13.3: Pin-ish Line](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.3)**\n\nFastMCP 2.13.3 pins `mcp<1.23` as a precautionary measure. MCP SDK 1.23 introduced changes related to the November 25, 2025 MCP protocol update that break certain FastMCP patches and workarounds, particularly around OAuth implementation details. FastMCP 2.14 introduces proper support for the updated protocol and requires `mcp>=1.23`.\n\n## What's Changed\n### Fixes 🐞\n* Pin MCP SDK below 1.23 by [@jlowin](https://github.com/jlowin) in [#2545](https://github.com/PrefectHQ/fastmcp/pull/2545)\n\n**Full Changelog**: [v2.13.2...v2.13.3](https://github.com/PrefectHQ/fastmcp/compare/v2.13.2...v2.13.3)\n\n</Update>\n\n<Update label=\"v2.13.2\" description=\"2025-12-01\">\n\n**[v2.13.2: Refreshing Changes](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.2)**\n\nFastMCP 2.13.2 polishes the authentication stack with improvements to token refresh, scope handling, and multi-instance deployments. Discord was added as a built-in OAuth provider, Azure and Google token handling became more reliable, and proxy classes now properly forward icons and titles.\n\n## What's Changed\n### New Features 🎉\n* Add Discord OAuth provider by [@jlowin](https://github.com/jlowin) in [#2480](https://github.com/PrefectHQ/fastmcp/pull/2480)\n### Enhancements 🔧\n* Descope Provider updates for new well-known URLs by [@anvibanga](https://github.com/anvibanga) in [#2465](https://github.com/PrefectHQ/fastmcp/pull/2465)\n* Scalekit provider improvements by [@jlowin](https://github.com/jlowin) in [#2472](https://github.com/PrefectHQ/fastmcp/pull/2472)\n* Add CSP customization for consent screens by [@jlowin](https://github.com/jlowin) in [#2488](https://github.com/PrefectHQ/fastmcp/pull/2488)\n* Add icon support to proxy classes by [@jlowin](https://github.com/jlowin) in [#2495](https://github.com/PrefectHQ/fastmcp/pull/2495)\n### Fixes 🐞\n* Google Provider now defaults to refresh token support by [@jlowin](https://github.com/jlowin) in [#2468](https://github.com/PrefectHQ/fastmcp/pull/2468)\n* Fix Azure OAuth token refresh with unprefixed scopes by [@jlowin](https://github.com/jlowin) in [#2475](https://github.com/PrefectHQ/fastmcp/pull/2475)\n* Prevent `$defs` mutation during tool transforms by [@jlowin](https://github.com/jlowin) in [#2482](https://github.com/PrefectHQ/fastmcp/pull/2482)\n* Fix OAuth proxy refresh token storage for multi-instance deployments by [@jlowin](https://github.com/jlowin) in [#2490](https://github.com/PrefectHQ/fastmcp/pull/2490)\n* Fix stale token issue after OAuth refresh by [@jlowin](https://github.com/jlowin) in [#2498](https://github.com/PrefectHQ/fastmcp/pull/2498)\n* Fix Azure provider OIDC scope handling by [@jlowin](https://github.com/jlowin) in [#2505](https://github.com/PrefectHQ/fastmcp/pull/2505)\n\n## New Contributors\n7 new contributors made their first FastMCP contributions in this release.\n\n**Full Changelog**: [v2.13.1...v2.13.2](https://github.com/PrefectHQ/fastmcp/compare/v2.13.1...v2.13.2)\n\n</Update>\n\n<Update label=\"v2.13.1\" description=\"2025-11-15\">\n\n**[v2.13.1: Heavy Meta](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.1)**\n\nFastMCP 2.13.1 introduces meta parameter support for `ToolResult`, enabling tools to return supplementary metadata alongside results. This supports emerging use cases like OpenAI's Apps SDK. The release also brings improved OAuth functionality with custom token verifiers including a new DebugTokenVerifier, and adds OCI and Supabase authentication providers.\n\n🏷️ **Meta parameters for ToolResult** enable tools to return supplementary metadata alongside results, supporting patterns like OpenAI's Apps SDK integration.\n\n🔐 **Custom token verifiers** with DebugTokenVerifier for development, plus Azure Government support through a `base_authority` parameter and Supabase authentication algorithm configuration.\n\n🔒 **Security fixes** address CVE-2025-61920 through authlib updates and validate Cursor deeplink URLs using safer Windows APIs.\n\n## What's Changed\n### New Features 🎉\n* Add meta parameter support for ToolResult by [@jlowin](https://github.com/jlowin) in [#2350](https://github.com/PrefectHQ/fastmcp/pull/2350)\n* Add OCI authentication provider by [@jlowin](https://github.com/jlowin) in [#2365](https://github.com/PrefectHQ/fastmcp/pull/2365)\n* Add Supabase authentication provider by [@jlowin](https://github.com/jlowin) in [#2378](https://github.com/PrefectHQ/fastmcp/pull/2378)\n### Enhancements 🔧\n* Add custom token verifier support to OIDCProxy by [@jlowin](https://github.com/jlowin) in [#2355](https://github.com/PrefectHQ/fastmcp/pull/2355)\n* Add DebugTokenVerifier for development by [@jlowin](https://github.com/jlowin) in [#2362](https://github.com/PrefectHQ/fastmcp/pull/2362)\n* Add Azure Government support via base_authority parameter by [@jlowin](https://github.com/jlowin) in [#2385](https://github.com/PrefectHQ/fastmcp/pull/2385)\n* Add Supabase authentication algorithm configuration by [@jlowin](https://github.com/jlowin) in [#2392](https://github.com/PrefectHQ/fastmcp/pull/2392)\n### Fixes 🐞\n* Security: Update authlib for CVE-2025-61920 by [@jlowin](https://github.com/jlowin) in [#2398](https://github.com/PrefectHQ/fastmcp/pull/2398)\n* Validate Cursor deeplink URLs using safer Windows APIs by [@jlowin](https://github.com/jlowin) in [#2405](https://github.com/PrefectHQ/fastmcp/pull/2405)\n* Exclude MCP SDK 1.21.1 due to integration test failures by [@jlowin](https://github.com/jlowin) in [#2422](https://github.com/PrefectHQ/fastmcp/pull/2422)\n\n## New Contributors\n18 new contributors joined in this release across 70+ pull requests.\n\n**Full Changelog**: [v2.13.0...v2.13.1](https://github.com/PrefectHQ/fastmcp/compare/v2.13.0...v2.13.1)\n\n</Update>\n\n<Update label=\"v2.13.0\" description=\"2025-10-25\">\n\n**[v2.13.0: Cache Me If You Can](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.0)**\n\nFastMCP 2.13 \"Cache Me If You Can\" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.\n\n💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more! OAuth providers now automatically persist tokens across restarts, and developers can store arbitrary state without reaching for external databases. This foundation enables long-running sessions, cached credentials, and stateful applications built on MCP.\n\n🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions while providing a clean UX with customizable branding. The OAuth proxy now issues its own tokens with automatic key derivation from client secrets, and RFC 7662 token introspection support enables enterprise auth flows. Path prefix mounting enables OAuth-protected servers to integrate into existing web applications under custom paths like `/api`, and MCP 1.17+ compliance with RFC 9728 ensures protocol compatibility. Combined with improved error handling and platform-aware token storage, OAuth is now production-ready and security-hardened for serious applications.\n\nFastMCP now supports out-of-the-box authentication with:\n- **[WorkOS](https://gofastmcp.com/integrations/workos)** and **[AuthKit](https://gofastmcp.com/integrations/authkit)**\n- **[GitHub](https://gofastmcp.com/integrations/github)**\n- **[Google](https://gofastmcp.com/integrations/google)**\n- **[Azure](https://gofastmcp.com/integrations/azure)** (Entra ID)\n- **[AWS Cognito](https://gofastmcp.com/integrations/aws-cognito)**\n- **[Auth0](https://gofastmcp.com/integrations/auth0)**\n- **[Descope](https://gofastmcp.com/integrations/descope)**\n- **[Scalekit](https://gofastmcp.com/integrations/scalekit)**\n- **[JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification)**\n- **[RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol)**\n\n⚡ **Response Caching Middleware** dramatically improves performance for expensive operations. Cache tool and resource responses with configurable TTLs, reducing redundant API calls and speeding up repeated queries.\n\n🔄 **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session. This fixes a long-standing source of confusion in the MCP SDK and enables proper resource management for database connections, background tasks, and other server-level state. Note: this is a breaking behavioral change if you were using the `lifespan` parameter.\n\n✨ **Developer experience improvements** include Pydantic input validation for better type safety, icon support for richer UX, RFC 6570 query parameters for resource templates, improved Context API methods (list_resources, list_prompts, get_prompt), and async file/directory resources.\n\nThis release includes contributions from **20** new contributors and represents the largest feature set in a while. Thank you to everyone who tested preview builds and filed issues - your feedback shaped these improvements!\n\n**Full Changelog**: [v2.12.5...v2.13.0](https://github.com/PrefectHQ/fastmcp/compare/v2.12.5...v2.13.0)\n\n</Update>\n\n<Update label=\"v2.12.5\" description=\"2025-10-17\">\n\n**[v2.12.5: Safety Pin](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.5)**\n\nFastMCP 2.12.5 is a point release that pins the MCP SDK version below 1.17, which introduced a change affecting FastMCP users with auth providers mounted as part of a larger application. This ensures the `.well-known` payload appears in the expected location when using FastMCP authentication providers with composite applications.\n\n## What's Changed\n\n### Fixes 🐞\n* Pin MCP SDK version below 1.17 by [@jlowin](https://github.com/jlowin) in [a1b2c3d](https://github.com/PrefectHQ/fastmcp/commit/dab2b316ddc3883b7896a86da21cacb68da01e5c)\n\n**Full Changelog**: [v2.12.4...v2.12.5](https://github.com/PrefectHQ/fastmcp/compare/v2.12.4...v2.12.5)\n\n</Update>\n\n<Update label=\"v2.12.4\" description=\"2025-09-26\">\n\n**[v2.12.4: OIDC What You Did There](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.4)**\n\nFastMCP 2.12.4 adds comprehensive OIDC support and expands authentication options with AWS Cognito and Descope providers. The release also includes improvements to logging middleware, URL handling for nested resources, persistent OAuth client registration storage, and various fixes to the experimental OpenAPI parser.\n\n## What's Changed\n### New Features 🎉\n* feat: Add support for OIDC configuration by [@ruhulio](https://github.com/ruhulio) in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n### Enhancements 🔧\n* feat: Move the Starlette context middleware to the front by [@akkuman](https://github.com/akkuman) in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* Refactor Logging and Structured Logging Middleware by [@strawgate](https://github.com/strawgate) in [#1805](https://github.com/PrefectHQ/fastmcp/pull/1805)\n* Update pull_request_template.md by [@jlowin](https://github.com/jlowin) in [#1824](https://github.com/PrefectHQ/fastmcp/pull/1824)\n* chore: Set redirect_path default in function by [@ruhulio](https://github.com/ruhulio) in [#1833](https://github.com/PrefectHQ/fastmcp/pull/1833)\n* feat: Set instructions in code by [@attiks](https://github.com/attiks) in [#1838](https://github.com/PrefectHQ/fastmcp/pull/1838)\n* Automatically Create inline Snapshots by [@strawgate](https://github.com/strawgate) in [#1779](https://github.com/PrefectHQ/fastmcp/pull/1779)\n* chore: Cleanup Auth0 redirect_path initialization by [@ruhulio](https://github.com/ruhulio) in [#1842](https://github.com/PrefectHQ/fastmcp/pull/1842)\n* feat: Add support for Descope Authentication by [@anvibanga](https://github.com/anvibanga) in [#1853](https://github.com/PrefectHQ/fastmcp/pull/1853)\n* Update descope version badges by [@jlowin](https://github.com/jlowin) in [#1870](https://github.com/PrefectHQ/fastmcp/pull/1870)\n* Update welcome images by [@jlowin](https://github.com/jlowin) in [#1884](https://github.com/PrefectHQ/fastmcp/pull/1884)\n* Fix rounded edges of image by [@jlowin](https://github.com/jlowin) in [#1886](https://github.com/PrefectHQ/fastmcp/pull/1886)\n* optimize test suite by [@zzstoatzz](https://github.com/zzstoatzz) in [#1893](https://github.com/PrefectHQ/fastmcp/pull/1893)\n* Enhancement: client completions support context_arguments by [@isijoe](https://github.com/isijoe) in [#1906](https://github.com/PrefectHQ/fastmcp/pull/1906)\n* Update Descope icon by [@anvibanga](https://github.com/anvibanga) in [#1912](https://github.com/PrefectHQ/fastmcp/pull/1912)\n* Add AWS Cognito OAuth Provider for Enterprise Authentication by [@stephaneberle9](https://github.com/stephaneberle9) in [#1873](https://github.com/PrefectHQ/fastmcp/pull/1873)\n* Fix typos discovered by codespell by [@cclauss](https://github.com/cclauss) in [#1922](https://github.com/PrefectHQ/fastmcp/pull/1922)\n* Use lowercase namespace for fastmcp logger by [@jlowin](https://github.com/jlowin) in [#1791](https://github.com/PrefectHQ/fastmcp/pull/1791)\n### Fixes 🐞\n* Update quickstart.mdx by [@radi-dev](https://github.com/radi-dev) in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n* Remove extraneous union import by [@jlowin](https://github.com/jlowin) in [#1823](https://github.com/PrefectHQ/fastmcp/pull/1823)\n* Delay import of Provider classes until FastMCP Server Creation by [@strawgate](https://github.com/strawgate) in [#1820](https://github.com/PrefectHQ/fastmcp/pull/1820)\n* fix: correct documentation link in deprecation warning by [@strawgate](https://github.com/strawgate) in [#1828](https://github.com/PrefectHQ/fastmcp/pull/1828)\n* fix: Increase default 3s timeout on Pytest by [@dacamposol](https://github.com/dacamposol) in [#1866](https://github.com/PrefectHQ/fastmcp/pull/1866)\n* fix: Improve URL handling in OIDCConfiguration by [@ruhulio](https://github.com/ruhulio) in [#1850](https://github.com/PrefectHQ/fastmcp/pull/1850)\n* fix: correct typing for on_read_resource middleware method by [@strawgate](https://github.com/strawgate) in [#1858](https://github.com/PrefectHQ/fastmcp/pull/1858)\n* feat(experimental/openapi): replace $ref in additionalProperties; add tests by [@jlowin](https://github.com/jlowin) in [#1735](https://github.com/PrefectHQ/fastmcp/pull/1735)\n* Honor client supplied scopes during registration by [@dmikusa](https://github.com/dmikusa) in [#1860](https://github.com/PrefectHQ/fastmcp/pull/1860)\n* Fix: FastAPI list parameter parsing in experimental OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#1834](https://github.com/PrefectHQ/fastmcp/pull/1834)\n* Add log level support for stdio and HTTP transports by [@jlowin](https://github.com/jlowin) in [#1840](https://github.com/PrefectHQ/fastmcp/pull/1840)\n* Fix OAuth pre-flight check to accept HTTP 200 responses by [@jlowin](https://github.com/jlowin) in [#1874](https://github.com/PrefectHQ/fastmcp/pull/1874)\n* Fix: Preserve OpenAPI parameter descriptions in experimental parser by [@shlomo666](https://github.com/shlomo666) in [#1877](https://github.com/PrefectHQ/fastmcp/pull/1877)\n* Add persistent storage for OAuth client registrations by [@jlowin](https://github.com/jlowin) in [#1879](https://github.com/PrefectHQ/fastmcp/pull/1879)\n* docs: update release dates based on github releases by [@lodu](https://github.com/lodu) in [#1890](https://github.com/PrefectHQ/fastmcp/pull/1890)\n* Small updates to Sampling types by [@strawgate](https://github.com/strawgate) in [#1882](https://github.com/PrefectHQ/fastmcp/pull/1882)\n* remove lockfile smart_home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#1892](https://github.com/PrefectHQ/fastmcp/pull/1892)\n* Fix: Remove JSON schema title metadata while preserving parameters named 'title' by [@jlowin](https://github.com/jlowin) in [#1872](https://github.com/PrefectHQ/fastmcp/pull/1872)\n* Fix: get_resource_url nested URL handling by [@raphael-linx](https://github.com/raphael-linx) in [#1914](https://github.com/PrefectHQ/fastmcp/pull/1914)\n* Clean up code for creating the resource url by [@jlowin](https://github.com/jlowin) in [#1916](https://github.com/PrefectHQ/fastmcp/pull/1916)\n* Fix route count logging in OpenAPI server by [@zzstoatzz](https://github.com/zzstoatzz) in [#1928](https://github.com/PrefectHQ/fastmcp/pull/1928)\n### Docs 📚\n* docs: make Gemini CLI integration discoverable by [@jackwotherspoon](https://github.com/jackwotherspoon) in [#1827](https://github.com/PrefectHQ/fastmcp/pull/1827)\n* docs: update NEW tags for AI assistant integrations by [@jackwotherspoon](https://github.com/jackwotherspoon) in [#1829](https://github.com/PrefectHQ/fastmcp/pull/1829)\n* Update wordmark by [@jlowin](https://github.com/jlowin) in [#1832](https://github.com/PrefectHQ/fastmcp/pull/1832)\n* docs: improve OAuth and OIDC Proxy documentation by [@jlowin](https://github.com/jlowin) in [#1880](https://github.com/PrefectHQ/fastmcp/pull/1880)\n* Update readme + welcome docs by [@jlowin](https://github.com/jlowin) in [#1883](https://github.com/PrefectHQ/fastmcp/pull/1883)\n* Update dark mode image in README by [@jlowin](https://github.com/jlowin) in [#1885](https://github.com/PrefectHQ/fastmcp/pull/1885)\n\n## New Contributors\n* [@radi-dev](https://github.com/radi-dev) made their first contribution in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n* [@akkuman](https://github.com/akkuman) made their first contribution in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* [@ruhulio](https://github.com/ruhulio) made their first contribution in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n* [@attiks](https://github.com/attiks) made their first contribution in [#1838](https://github.com/PrefectHQ/fastmcp/pull/1838)\n* [@anvibanga](https://github.com/anvibanga) made their first contribution in [#1853](https://github.com/PrefectHQ/fastmcp/pull/1853)\n* [@shlomo666](https://github.com/shlomo666) made their first contribution in [#1877](https://github.com/PrefectHQ/fastmcp/pull/1877)\n* [@lodu](https://github.com/lodu) made their first contribution in [#1890](https://github.com/PrefectHQ/fastmcp/pull/1890)\n* [@isijoe](https://github.com/isijoe) made their first contribution in [#1906](https://github.com/PrefectHQ/fastmcp/pull/1906)\n* [@raphael-linx](https://github.com/raphael-linx) made their first contribution in [#1914](https://github.com/PrefectHQ/fastmcp/pull/1914)\n* [@stephaneberle9](https://github.com/stephaneberle9) made their first contribution in [#1873](https://github.com/PrefectHQ/fastmcp/pull/1873)\n* [@cclauss](https://github.com/cclauss) made their first contribution in [#1922](https://github.com/PrefectHQ/fastmcp/pull/1922)\n\n**Full Changelog**: [v2.12.3...v2.12.4](https://github.com/PrefectHQ/fastmcp/compare/v2.12.3...v2.12.4)\n\n</Update>\n\n<Update label=\"v2.12.3\" description=\"2025-09-17\">\n\n**[v2.12.3: Double Time](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.3)**\n\nFastMCP 2.12.3 focuses on performance and developer experience improvements based on community feedback. This release includes optimized auth provider imports that reduce server startup time, enhanced OIDC authentication flows with proper token management, and several reliability fixes for OAuth proxy configurations. The addition of automatic inline snapshot creation significantly improves the testing experience for contributors.\n\n## What's Changed\n### New Features 🎉\n* feat: Support setting MCP log level via transport configuration by [@jlowin](https://github.com/jlowin) in [#1756](https://github.com/PrefectHQ/fastmcp/pull/1756)\n### Enhancements 🔧\n* Add client-side auth support for mcp install cursor command by [@jlowin](https://github.com/jlowin) in [#1747](https://github.com/PrefectHQ/fastmcp/pull/1747)\n* Automatically Create inline Snapshots by [@strawgate](https://github.com/strawgate) in [#1779](https://github.com/PrefectHQ/fastmcp/pull/1779)\n* Use lowercase namespace for fastmcp logger by [@jlowin](https://github.com/jlowin) in [#1791](https://github.com/PrefectHQ/fastmcp/pull/1791)\n### Fixes 🐞\n* fix: correct merge mistake during auth0 refactor by [@strawgate](https://github.com/strawgate) in [#1742](https://github.com/PrefectHQ/fastmcp/pull/1742)\n* Remove extraneous union import by [@jlowin](https://github.com/jlowin) in [#1823](https://github.com/PrefectHQ/fastmcp/pull/1823)\n* Delay import of Provider classes until FastMCP Server Creation by [@strawgate](https://github.com/strawgate) in [#1820](https://github.com/PrefectHQ/fastmcp/pull/1820)\n* fix: refactor OIDC configuration provider for proper token management by [@strawgate](https://github.com/strawgate) in [#1751](https://github.com/PrefectHQ/fastmcp/pull/1751)\n* Fix smart_home example imports by [@strawgate](https://github.com/strawgate) in [#1753](https://github.com/PrefectHQ/fastmcp/pull/1753)\n* fix: correct oauth proxy initialization of client by [@strawgate](https://github.com/strawgate) in [#1759](https://github.com/PrefectHQ/fastmcp/pull/1759)\n* Fix: return empty string when prompts have no arguments by [@jlowin](https://github.com/jlowin) in [#1766](https://github.com/PrefectHQ/fastmcp/pull/1766)\n* Fix async server callbacks by [@strawgate](https://github.com/strawgate) in [#1774](https://github.com/PrefectHQ/fastmcp/pull/1774)\n* Fix error when retrieving Completion API errors by [@strawgate](https://github.com/strawgate) in [#1785](https://github.com/PrefectHQ/fastmcp/pull/1785)\n* fix: correct documentation link in deprecation warning by [@strawgate](https://github.com/strawgate) in [#1828](https://github.com/PrefectHQ/fastmcp/pull/1828)\n### Docs 📚\n* Add migration docs for 2.12 by [@jlowin](https://github.com/jlowin) in [#1745](https://github.com/PrefectHQ/fastmcp/pull/1745)\n* Update docs for default sampling implementation to mention OpenAI API Key by [@strawgate](https://github.com/strawgate) in [#1763](https://github.com/PrefectHQ/fastmcp/pull/1763)\n* Add tip about sampling prompts and user_context to sampling documentation by [@jlowin](https://github.com/jlowin) in [#1764](https://github.com/PrefectHQ/fastmcp/pull/1764)\n* Update quickstart.mdx by [@radi-dev](https://github.com/radi-dev) in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n### Other Changes 🦾\n* Replace Marvin with Claude Code in CI by [@jlowin](https://github.com/jlowin) in [#1800](https://github.com/PrefectHQ/fastmcp/pull/1800)\n* Refactor logging and structured logging middleware by [@strawgate](https://github.com/strawgate) in [#1805](https://github.com/PrefectHQ/fastmcp/pull/1805)\n* feat: Move the Starlette context middleware to the front by [@akkuman](https://github.com/akkuman) in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* feat: Add support for OIDC configuration by [@ruhulio](https://github.com/ruhulio) in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n\n## New Contributors\n* [@radi-dev](https://github.com/radi-dev) made their first contribution in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)\n* [@akkuman](https://github.com/akkuman) made their first contribution in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)\n* [@ruhulio](https://github.com/ruhulio) made their first contribution in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)\n\n**Full Changelog**: [v2.12.2...v2.12.3](https://github.com/PrefectHQ/fastmcp/compare/v2.12.2...v2.12.3)\n\n</Update>\n\n<Update label=\"v2.12.2\" description=\"2025-09-03\">\n\n**[v2.12.2: Perchance to Stream](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.2)**\n\nThis is a hotfix for a bug where the `streamable-http` transport was not recognized as a valid option in `fastmcp.json` configuration files, despite being supported by the CLI. This resulted in a parsing error when the CLI arguments were merged against the configuration spec. \n\n## What's Changed\n### Fixes 🐞\n* Fix streamable-http transport validation in fastmcp.json config by [@jlowin](https://github.com/jlowin) in [#1739](https://github.com/PrefectHQ/fastmcp/pull/1739)\n\n**Full Changelog**: [v2.12.1...v2.12.2](https://github.com/PrefectHQ/fastmcp/compare/v2.12.1...v2.12.2)\n\n</Update>\n\n<Update label=\"v2.12.1\" description=\"2025-09-03\">\n\n**[v2.12.1: OAuth to Joy](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.1)**\n\nFastMCP 2.12.1 strengthens the OAuth proxy implementation based on extensive community testing and feedback. This release improves client storage reliability, adds PKCE forwarding for enhanced security, introduces configurable token endpoint authentication methods, and expands scope handling—all addressing real-world integration challenges discovered since 2.12.0. The enhanced test suite with mock providers ensures these improvements are robust and maintainable.\n\n## Breaking Changes\n- **OAuth Proxy**: Users of built-in IDP integrations should note that `resource_server_url` has been renamed to `base_url` for clarity and consistency\n\n## What's Changed\n### Enhancements 🔧\n* Make openai dependency optional by [@jlowin](https://github.com/jlowin) in [#1701](https://github.com/PrefectHQ/fastmcp/pull/1701)\n* Remove orphaned OAuth proxy code by [@jlowin](https://github.com/jlowin) in [#1722](https://github.com/PrefectHQ/fastmcp/pull/1722)\n* Expose valid scopes from OAuthProxy metadata by [@dmikusa](https://github.com/dmikusa) in [#1717](https://github.com/PrefectHQ/fastmcp/pull/1717)\n* OAuth proxy PKCE forwarding by [@jlowin](https://github.com/jlowin) in [#1733](https://github.com/PrefectHQ/fastmcp/pull/1733)\n* Add token_endpoint_auth_method parameter to OAuthProxy by [@jlowin](https://github.com/jlowin) in [#1736](https://github.com/PrefectHQ/fastmcp/pull/1736)\n* Clean up and enhance OAuth proxy tests with mock provider by [@jlowin](https://github.com/jlowin) in [#1738](https://github.com/PrefectHQ/fastmcp/pull/1738)\n### Fixes 🐞\n* refactor: replace auth provider registry with ImportString by [@jlowin](https://github.com/jlowin) in [#1710](https://github.com/PrefectHQ/fastmcp/pull/1710)\n* Fix OAuth resource URL handling and WWW-Authenticate header by [@jlowin](https://github.com/jlowin) in [#1706](https://github.com/PrefectHQ/fastmcp/pull/1706)\n* Fix OAuth proxy client storage and add retry logic by [@jlowin](https://github.com/jlowin) in [#1732](https://github.com/PrefectHQ/fastmcp/pull/1732)\n### Docs 📚\n* Fix documentation: use StreamableHttpTransport for headers in testing by [@jlowin](https://github.com/jlowin) in [#1702](https://github.com/PrefectHQ/fastmcp/pull/1702)\n* docs: add performance warnings for mounted servers and proxies by [@strawgate](https://github.com/strawgate) in [#1669](https://github.com/PrefectHQ/fastmcp/pull/1669)\n* Update documentation around scopes for google by [@jlowin](https://github.com/jlowin) in [#1703](https://github.com/PrefectHQ/fastmcp/pull/1703)\n* Add deployment information to quickstart by [@seanpwlms](https://github.com/seanpwlms) in [#1433](https://github.com/PrefectHQ/fastmcp/pull/1433)\n* Update quickstart by [@jlowin](https://github.com/jlowin) in [#1728](https://github.com/PrefectHQ/fastmcp/pull/1728)\n* Add development docs for FastMCP by [@jlowin](https://github.com/jlowin) in [#1719](https://github.com/PrefectHQ/fastmcp/pull/1719)\n### Other Changes 🦾\n* Set generics without bounds to default=Any by [@strawgate](https://github.com/strawgate) in [#1648](https://github.com/PrefectHQ/fastmcp/pull/1648)\n\n## New Contributors\n* [@dmikusa](https://github.com/dmikusa) made their first contribution in [#1717](https://github.com/PrefectHQ/fastmcp/pull/1717)\n* [@seanpwlms](https://github.com/seanpwlms) made their first contribution in [#1433](https://github.com/PrefectHQ/fastmcp/pull/1433)\n\n**Full Changelog**: [v2.12.0...v2.12.1](https://github.com/PrefectHQ/fastmcp/compare/v2.12.0...v2.12.1)\n\n</Update>\n\n<Update label=\"v2.12.0\" description=\"2025-08-31\">\n\n**[v2.12.0: Auth to the Races](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.0)**\n\nFastMCP 2.12 represents one of our most significant releases to date, both in scope and community involvement. After extensive testing and iteration with the community, we're shipping major improvements to authentication, configuration, and MCP feature adoption.\n\n🔐 **OAuth Proxy for Broader Provider Support** addresses a fundamental challenge: while MCP requires Dynamic Client Registration (DCR), many popular OAuth providers don't support it. The new OAuth proxy bridges this gap, enabling FastMCP servers to authenticate with providers like GitHub, Google, WorkOS, and Azure through minimal configuration. These native integrations ship today, with more providers planned based on community needs.\n\n📋 **Declarative JSON Configuration** introduces a standardized, portable way to describe and deploy MCP servers. The `fastmcp.json` configuration file becomes the single source of truth for dependencies, transport settings, entrypoints, and server metadata. This foundation sets the stage for future capabilities like transformations and remote sources, moving toward a world where MCP servers are as portable and shareable as container images.\n\n🧠 **Sampling API Fallback** tackles the chicken-and-egg problem limiting adoption of advanced MCP features. Sampling—where servers request LLM completions from clients—is powerful but underutilized due to limited client support. FastMCP now lets server authors define fallback handlers that generate sampling completions server-side when clients don't support the feature, encouraging adoption while maintaining compatibility.\n\nThis release took longer than usual to ship, and for good reason: the community's aggressive testing and feedback on the authentication system helped us reach a level of stability we're confident in. There's certainly more work ahead, but these foundations position FastMCP to handle increasingly complex use cases while remaining approachable for developers.\n\nThank you to our new contributors and everyone who tested preview builds. Your feedback directly shaped these features.\n\n## What's Changed\n### New Features 🎉\n* Add OAuth proxy that allows authentication with social IDPs without DCR support by [@jlowin](https://github.com/jlowin) in [#1434](https://github.com/PrefectHQ/fastmcp/pull/1434)\n* feat: introduce declarative JSON configuration system by [@jlowin](https://github.com/jlowin) in [#1517](https://github.com/PrefectHQ/fastmcp/pull/1517)\n* ✨ Fallback to a Completions API when Sampling is not available by [@strawgate](https://github.com/strawgate) in [#1145](https://github.com/PrefectHQ/fastmcp/pull/1145)\n* Implement typed source system for FastMCP declarative configuration by [@jlowin](https://github.com/jlowin) in [#1607](https://github.com/PrefectHQ/fastmcp/pull/1607)\n### Enhancements 🔧\n* Support importing custom_route endpoints when mounting servers by [@jlowin](https://github.com/jlowin) in [#1470](https://github.com/PrefectHQ/fastmcp/pull/1470)\n* Remove unnecessary asserts by [@jlowin](https://github.com/jlowin) in [#1484](https://github.com/PrefectHQ/fastmcp/pull/1484)\n* Add Claude issue triage by [@jlowin](https://github.com/jlowin) in [#1510](https://github.com/PrefectHQ/fastmcp/pull/1510)\n* Inline dedupe prompt by [@jlowin](https://github.com/jlowin) in [#1512](https://github.com/PrefectHQ/fastmcp/pull/1512)\n* Improve stdio and mcp_config clean-up by [@strawgate](https://github.com/strawgate) in [#1444](https://github.com/PrefectHQ/fastmcp/pull/1444)\n* involve kwargs to pass parameters on creating RichHandler for logging customization. by [@itaru2622](https://github.com/itaru2622) in [#1504](https://github.com/PrefectHQ/fastmcp/pull/1504)\n* Move SDK docs generation to post-merge workflow by [@jlowin](https://github.com/jlowin) in [#1513](https://github.com/PrefectHQ/fastmcp/pull/1513)\n* Improve label triage guidance by [@jlowin](https://github.com/jlowin) in [#1516](https://github.com/PrefectHQ/fastmcp/pull/1516)\n* Add code review guidelines for agents by [@jlowin](https://github.com/jlowin) in [#1520](https://github.com/PrefectHQ/fastmcp/pull/1520)\n* Remove trailing slash in unit tests by [@jlowin](https://github.com/jlowin) in [#1535](https://github.com/PrefectHQ/fastmcp/pull/1535)\n* Update OAuth callback UI branding by [@jlowin](https://github.com/jlowin) in [#1536](https://github.com/PrefectHQ/fastmcp/pull/1536)\n* Fix Marvin workflow to support development tools by [@jlowin](https://github.com/jlowin) in [#1537](https://github.com/PrefectHQ/fastmcp/pull/1537)\n* Add mounted_components_raise_on_load_error setting for debugging by [@jlowin](https://github.com/jlowin) in [#1534](https://github.com/PrefectHQ/fastmcp/pull/1534)\n* feat: Add --workspace flag to fastmcp install cursor by [@jlowin](https://github.com/jlowin) in [#1522](https://github.com/PrefectHQ/fastmcp/pull/1522)\n* switch from `pyright` to `ty` by [@zzstoatzz](https://github.com/zzstoatzz) in [#1545](https://github.com/PrefectHQ/fastmcp/pull/1545)\n* feat: trigger Marvin workflow on PR body content by [@jlowin](https://github.com/jlowin) in [#1549](https://github.com/PrefectHQ/fastmcp/pull/1549)\n* Add WorkOS and Azure OAuth providers by [@jlowin](https://github.com/jlowin) in [#1550](https://github.com/PrefectHQ/fastmcp/pull/1550)\n* Adjust timeout for slow MCP Server shutdown test by [@strawgate](https://github.com/strawgate) in [#1561](https://github.com/PrefectHQ/fastmcp/pull/1561)\n* Update banner by [@jlowin](https://github.com/jlowin) in [#1567](https://github.com/PrefectHQ/fastmcp/pull/1567)\n* Added import of AuthProxy to auth __init__ by [@KaliszS](https://github.com/KaliszS) in [#1568](https://github.com/PrefectHQ/fastmcp/pull/1568)\n* Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582)\n* Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588)\n* Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591)\n* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)\n* Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)\n* Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604)\n* Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581)\n* chore: Automate docs and schema updates via PRs by [@jlowin](https://github.com/jlowin) in [#1611](https://github.com/PrefectHQ/fastmcp/pull/1611)\n* Experiment with haiku for limited workflows by [@jlowin](https://github.com/jlowin) in [#1613](https://github.com/PrefectHQ/fastmcp/pull/1613)\n* feat: Improve GitHub workflow automation for schema and SDK docs by [@jlowin](https://github.com/jlowin) in [#1615](https://github.com/PrefectHQ/fastmcp/pull/1615)\n* Consolidate server loading logic into FileSystemSource by [@jlowin](https://github.com/jlowin) in [#1614](https://github.com/PrefectHQ/fastmcp/pull/1614)\n* Prevent Haiku Marvin from commenting when there are no duplicates by [@jlowin](https://github.com/jlowin) in [#1622](https://github.com/PrefectHQ/fastmcp/pull/1622)\n* chore: Add clarifying note to automated PR bodies by [@jlowin](https://github.com/jlowin) in [#1623](https://github.com/PrefectHQ/fastmcp/pull/1623)\n* feat: introduce inline snapshots by [@strawgate](https://github.com/strawgate) in [#1605](https://github.com/PrefectHQ/fastmcp/pull/1605)\n* Improve fastmcp.json environment configuration and project-based deployments by [@jlowin](https://github.com/jlowin) in [#1631](https://github.com/PrefectHQ/fastmcp/pull/1631)\n* fix: allow passing query params in OAuthProxy upstream authorization url by [@danb27](https://github.com/danb27) in [#1630](https://github.com/PrefectHQ/fastmcp/pull/1630)\n* Support multiple --with-editable flags in CLI commands by [@jlowin](https://github.com/jlowin) in [#1634](https://github.com/PrefectHQ/fastmcp/pull/1634)\n* feat: support comma separated oauth scopes by [@jlowin](https://github.com/jlowin) in [#1642](https://github.com/PrefectHQ/fastmcp/pull/1642)\n* Add allowed_client_redirect_uris to OAuth provider subclasses by [@jlowin](https://github.com/jlowin) in [#1662](https://github.com/PrefectHQ/fastmcp/pull/1662)\n* Consolidate CLI config parsing and prevent infinite loops by [@jlowin](https://github.com/jlowin) in [#1660](https://github.com/PrefectHQ/fastmcp/pull/1660)\n* Internal refactor: mcp server config by [@jlowin](https://github.com/jlowin) in [#1672](https://github.com/PrefectHQ/fastmcp/pull/1672)\n* Refactor Environment to support multiple runtime types by [@jlowin](https://github.com/jlowin) in [#1673](https://github.com/PrefectHQ/fastmcp/pull/1673)\n* Add type field to Environment base class by [@jlowin](https://github.com/jlowin) in [#1676](https://github.com/PrefectHQ/fastmcp/pull/1676)\n### Fixes 🐞\n* Fix breaking change: restore output_schema=False compatibility by [@jlowin](https://github.com/jlowin) in [#1482](https://github.com/PrefectHQ/fastmcp/pull/1482)\n* Fix #1506: Update tool filtering documentation from _meta to meta by [@maybenotconnor](https://github.com/maybenotconnor) in [#1511](https://github.com/PrefectHQ/fastmcp/pull/1511)\n* Fix pytest warnings by [@jlowin](https://github.com/jlowin) in [#1559](https://github.com/PrefectHQ/fastmcp/pull/1559)\n* nest schemas under assets by [@jlowin](https://github.com/jlowin) in [#1593](https://github.com/PrefectHQ/fastmcp/pull/1593)\n* Skip flaky windows test by [@jlowin](https://github.com/jlowin) in [#1596](https://github.com/PrefectHQ/fastmcp/pull/1596)\n* ACTUALLY move schemas to fastmcp.json by [@jlowin](https://github.com/jlowin) in [#1597](https://github.com/PrefectHQ/fastmcp/pull/1597)\n* Fix and centralize CLI path resolution by [@jlowin](https://github.com/jlowin) in [#1590](https://github.com/PrefectHQ/fastmcp/pull/1590)\n* Remove client info modifications by [@jlowin](https://github.com/jlowin) in [#1620](https://github.com/PrefectHQ/fastmcp/pull/1620)\n* Fix $defs being discarded in input schema of transformed tool by [@pldesch-chift](https://github.com/pldesch-chift) in [#1578](https://github.com/PrefectHQ/fastmcp/pull/1578)\n* Fix enum elicitation to use inline schemas for MCP compatibility by [@jlowin](https://github.com/jlowin) in [#1632](https://github.com/PrefectHQ/fastmcp/pull/1632)\n* Reuse session for `StdioTransport` in `Client.new` by [@strawgate](https://github.com/strawgate) in [#1635](https://github.com/PrefectHQ/fastmcp/pull/1635)\n* Feat: Configurable LoggingMiddleware payload serialization by [@vl-kp](https://github.com/vl-kp) in [#1636](https://github.com/PrefectHQ/fastmcp/pull/1636)\n* Fix OAuth redirect URI validation for DCR compatibility by [@jlowin](https://github.com/jlowin) in [#1661](https://github.com/PrefectHQ/fastmcp/pull/1661)\n* Add default scope handling in OAuth proxy by [@romanusyk](https://github.com/romanusyk) in [#1667](https://github.com/PrefectHQ/fastmcp/pull/1667)\n* Fix OAuth token expiry handling by [@jlowin](https://github.com/jlowin) in [#1671](https://github.com/PrefectHQ/fastmcp/pull/1671)\n* Add resource_server_url parameter to OAuth proxy providers by [@jlowin](https://github.com/jlowin) in [#1682](https://github.com/PrefectHQ/fastmcp/pull/1682)\n### Breaking Changes 🛫\n* Enhance inspect command with structured output and format options by [@jlowin](https://github.com/jlowin) in [#1481](https://github.com/PrefectHQ/fastmcp/pull/1481)\n### Docs 📚\n* Update changelog by [@jlowin](https://github.com/jlowin) in [#1453](https://github.com/PrefectHQ/fastmcp/pull/1453)\n* Update banner by [@jlowin](https://github.com/jlowin) in [#1472](https://github.com/PrefectHQ/fastmcp/pull/1472)\n* Update logo files by [@jlowin](https://github.com/jlowin) in [#1473](https://github.com/PrefectHQ/fastmcp/pull/1473)\n* Update deployment docs by [@jlowin](https://github.com/jlowin) in [#1486](https://github.com/PrefectHQ/fastmcp/pull/1486)\n* Update FastMCP Cloud screenshot by [@jlowin](https://github.com/jlowin) in [#1487](https://github.com/PrefectHQ/fastmcp/pull/1487)\n* Update authentication note in docs by [@jlowin](https://github.com/jlowin) in [#1488](https://github.com/PrefectHQ/fastmcp/pull/1488)\n* chore: Update installation.mdx version snippet by [@thomas-te](https://github.com/thomas-te) in [#1496](https://github.com/PrefectHQ/fastmcp/pull/1496)\n* Update fastmcp cloud server requirements by [@jlowin](https://github.com/jlowin) in [#1497](https://github.com/PrefectHQ/fastmcp/pull/1497)\n* Fix oauth pyright type checking by [@strawgate](https://github.com/strawgate) in [#1498](https://github.com/PrefectHQ/fastmcp/pull/1498)\n* docs: Fix type annotation in return value documentation by [@MaikelVeen](https://github.com/MaikelVeen) in [#1499](https://github.com/PrefectHQ/fastmcp/pull/1499)\n* Fix PromptMessage usage in docs example by [@jlowin](https://github.com/jlowin) in [#1515](https://github.com/PrefectHQ/fastmcp/pull/1515)\n* Create CODE_OF_CONDUCT.md by [@jlowin](https://github.com/jlowin) in [#1523](https://github.com/PrefectHQ/fastmcp/pull/1523)\n* Fixed wrong import path in new docs page by [@KaliszS](https://github.com/KaliszS) in [#1538](https://github.com/PrefectHQ/fastmcp/pull/1538)\n* Document symmetric key JWT verification support by [@jlowin](https://github.com/jlowin) in [#1586](https://github.com/PrefectHQ/fastmcp/pull/1586)\n* Update fastmcp.json schema path by [@jlowin](https://github.com/jlowin) in [#1595](https://github.com/PrefectHQ/fastmcp/pull/1595)\n### Dependencies 📦\n* Bump actions/create-github-app-token from 1 to 2 by [@dependabot](https://github.com/dependabot)[bot] in [#1436](https://github.com/PrefectHQ/fastmcp/pull/1436)\n* Bump astral-sh/setup-uv from 4 to 6 by [@dependabot](https://github.com/dependabot)[bot] in [#1532](https://github.com/PrefectHQ/fastmcp/pull/1532)\n* Bump actions/checkout from 4 to 5 by [@dependabot](https://github.com/dependabot)[bot] in [#1533](https://github.com/PrefectHQ/fastmcp/pull/1533)\n### Other Changes 🦾\n* Add dedupe workflow by [@jlowin](https://github.com/jlowin) in [#1454](https://github.com/PrefectHQ/fastmcp/pull/1454)\n* Update AGENTS.md by [@jlowin](https://github.com/jlowin) in [#1471](https://github.com/PrefectHQ/fastmcp/pull/1471)\n* Give Marvin the power of the Internet by [@strawgate](https://github.com/strawgate) in [#1475](https://github.com/PrefectHQ/fastmcp/pull/1475)\n* Update `just` error message for static checks by [@jlowin](https://github.com/jlowin) in [#1483](https://github.com/PrefectHQ/fastmcp/pull/1483)\n* Remove labeler by [@jlowin](https://github.com/jlowin) in [#1509](https://github.com/PrefectHQ/fastmcp/pull/1509)\n* update aproto server to handle rich links by [@zzstoatzz](https://github.com/zzstoatzz) in [#1556](https://github.com/PrefectHQ/fastmcp/pull/1556)\n* fix: enable triage bot for fork PRs using pull_request_target by [@jlowin](https://github.com/jlowin) in [#1557](https://github.com/PrefectHQ/fastmcp/pull/1557)\n\n## New Contributors\n* [@thomas-te](https://github.com/thomas-te) made their first contribution in [#1496](https://github.com/PrefectHQ/fastmcp/pull/1496)\n* [@maybenotconnor](https://github.com/maybenotconnor) made their first contribution in [#1511](https://github.com/PrefectHQ/fastmcp/pull/1511)\n* [@MaikelVeen](https://github.com/MaikelVeen) made their first contribution in [#1499](https://github.com/PrefectHQ/fastmcp/pull/1499)\n* [@KaliszS](https://github.com/KaliszS) made their first contribution in [#1538](https://github.com/PrefectHQ/fastmcp/pull/1538)\n* [@isra17](https://github.com/isra17) made their first contribution in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)\n* [@marvin-context-protocol](https://github.com/marvin-context-protocol)[bot] made their first contribution in [#1616](https://github.com/PrefectHQ/fastmcp/pull/1616)\n* [@pldesch-chift](https://github.com/pldesch-chift) made their first contribution in [#1578](https://github.com/PrefectHQ/fastmcp/pull/1578)\n* [@vl-kp](https://github.com/vl-kp) made their first contribution in [#1636](https://github.com/PrefectHQ/fastmcp/pull/1636)\n* [@romanusyk](https://github.com/romanusyk) made their first contribution in [#1667](https://github.com/PrefectHQ/fastmcp/pull/1667)\n\n**Full Changelog**: [v2.11.3...v2.12.0](https://github.com/PrefectHQ/fastmcp/compare/v2.11.3...v2.12.0)\n\n</Update>\n\n<Update label=\"v2.11.3\" description=\"2025-08-11\">\n\n**[v2.11.3: API-tite for Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.3)**\n\nThis release includes significant enhancements to the experimental OpenAPI parser and fixes a significant bug that led schemas not to be included in input/output schemas if they were transitive dependencies (e.g. A → B → C implies A depends on C). For users naively transforming large OpenAPI specs into MCP servers, this may result in ballooning payload sizes and necessitate curation.\n\n## What's Changed\n### Enhancements 🔧\n* Improve redirect handling to address 307's by [@jlowin](https://github.com/jlowin) in [#1387](https://github.com/PrefectHQ/fastmcp/pull/1387)\n* Ensure resource + template names are properly prefixed when importing/mounting by [@jlowin](https://github.com/jlowin) in [#1423](https://github.com/PrefectHQ/fastmcp/pull/1423)\n* fixes #1398: Add JWT claims to AccessToken by [@panargirakis](https://github.com/panargirakis) in [#1399](https://github.com/PrefectHQ/fastmcp/pull/1399)\n* Enable Protected Resource Metadata to provide resource_name and resou… by [@yannj-fr](https://github.com/yannj-fr) in [#1371](https://github.com/PrefectHQ/fastmcp/pull/1371)\n* Pin mcp SDK under 2.0 to avoid breaking changes by [@jlowin](https://github.com/jlowin) in [#1428](https://github.com/PrefectHQ/fastmcp/pull/1428)\n* Clean up complexity from PR #1426 by [@jlowin](https://github.com/jlowin) in [#1435](https://github.com/PrefectHQ/fastmcp/pull/1435)\n* Optimize OpenAPI payload size by 46% by [@jlowin](https://github.com/jlowin) in [#1452](https://github.com/PrefectHQ/fastmcp/pull/1452)\n* Update static checks by [@jlowin](https://github.com/jlowin) in [#1448](https://github.com/PrefectHQ/fastmcp/pull/1448)\n### Fixes 🐞\n* Fix client-side logging bug #1394 by [@chi2liu](https://github.com/chi2liu) in [#1397](https://github.com/PrefectHQ/fastmcp/pull/1397)\n* fix: Fix httpx_client_factory type annotation to match MCP SDK (#1402) by [@chi2liu](https://github.com/chi2liu) in [#1405](https://github.com/PrefectHQ/fastmcp/pull/1405)\n* Fix OpenAPI allOf handling at requestBody top level (#1378) by [@chi2liu](https://github.com/chi2liu) in [#1425](https://github.com/PrefectHQ/fastmcp/pull/1425)\n* Fix OpenAPI transitive references and performance (#1372) by [@jlowin](https://github.com/jlowin) in [#1426](https://github.com/PrefectHQ/fastmcp/pull/1426)\n* fix(type): lifespan is partially unknown by [@ykun9](https://github.com/ykun9) in [#1389](https://github.com/PrefectHQ/fastmcp/pull/1389)\n* Ensure transformed tools generate structured content by [@jlowin](https://github.com/jlowin) in [#1443](https://github.com/PrefectHQ/fastmcp/pull/1443)\n### Docs 📚\n* docs(client/logging): reflect corrected default log level mapping by [@jlowin](https://github.com/jlowin) in [#1403](https://github.com/PrefectHQ/fastmcp/pull/1403)\n* Add documentation for get_access_token() dependency function by [@jlowin](https://github.com/jlowin) in [#1446](https://github.com/PrefectHQ/fastmcp/pull/1446)\n### Other Changes 🦾\n* Add comprehensive tests for utilities.components module by [@chi2liu](https://github.com/chi2liu) in [#1395](https://github.com/PrefectHQ/fastmcp/pull/1395)\n* Consolidate agent instructions into AGENTS.md by [@jlowin](https://github.com/jlowin) in [#1404](https://github.com/PrefectHQ/fastmcp/pull/1404)\n* Fix performance test threshold to prevent flaky failures by [@jlowin](https://github.com/jlowin) in [#1406](https://github.com/PrefectHQ/fastmcp/pull/1406)\n* Update agents.md; add github instructions by [@jlowin](https://github.com/jlowin) in [#1410](https://github.com/PrefectHQ/fastmcp/pull/1410)\n* Add Marvin assistant by [@jlowin](https://github.com/jlowin) in [#1412](https://github.com/PrefectHQ/fastmcp/pull/1412)\n* Marvin: fix deprecated variable names by [@jlowin](https://github.com/jlowin) in [#1417](https://github.com/PrefectHQ/fastmcp/pull/1417)\n* Simplify action setup and add github tools for Marvin by [@jlowin](https://github.com/jlowin) in [#1419](https://github.com/PrefectHQ/fastmcp/pull/1419)\n* Update marvin workflow name by [@jlowin](https://github.com/jlowin) in [#1421](https://github.com/PrefectHQ/fastmcp/pull/1421)\n* Improve GitHub templates by [@jlowin](https://github.com/jlowin) in [#1422](https://github.com/PrefectHQ/fastmcp/pull/1422)\n\n## New Contributors\n* [@panargirakis](https://github.com/panargirakis) made their first contribution in [#1399](https://github.com/PrefectHQ/fastmcp/pull/1399)\n* [@ykun9](https://github.com/ykun9) made their first contribution in [#1389](https://github.com/PrefectHQ/fastmcp/pull/1389)\n* [@yannj-fr](https://github.com/yannj-fr) made their first contribution in [#1371](https://github.com/PrefectHQ/fastmcp/pull/1371)\n\n**Full Changelog**: [v2.11.2...v2.11.3](https://github.com/PrefectHQ/fastmcp/compare/v2.11.2...v2.11.3)\n\n</Update>\n\n<Update label=\"v2.11.2\" description=\"2025-08-06\">\n\n## [v2.11.2: Satis-factory](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.2)\n\n## What's Changed\n### Enhancements 🔧\n* Support factory functions in fastmcp run by [@jlowin](https://github.com/jlowin) in [#1384](https://github.com/PrefectHQ/fastmcp/pull/1384)\n* Add async support to client_factory in FastMCPProxy  (#1286) by [@bianning](https://github.com/bianning) in [#1375](https://github.com/PrefectHQ/fastmcp/pull/1375)\n### Fixes 🐞\n* Fix server_version field in inspect manifest by [@jlowin](https://github.com/jlowin) in [#1383](https://github.com/PrefectHQ/fastmcp/pull/1383)\n* Fix Settings field with both default and default_factory by [@jlowin](https://github.com/jlowin) in [#1380](https://github.com/PrefectHQ/fastmcp/pull/1380)\n### Other Changes 🦾\n* Remove unused arg by [@jlowin](https://github.com/jlowin) in [#1382](https://github.com/PrefectHQ/fastmcp/pull/1382)\n* Add remote auth provider tests by [@jlowin](https://github.com/jlowin) in [#1351](https://github.com/PrefectHQ/fastmcp/pull/1351)\n\n## New Contributors\n* [@bianning](https://github.com/bianning) made their first contribution in [#1375](https://github.com/PrefectHQ/fastmcp/pull/1375)\n\n**Full Changelog**: [v2.11.1...v2.11.2](https://github.com/PrefectHQ/fastmcp/compare/v2.11.1...v2.11.2)\n\n</Update>\n\n<Update label=\"v2.11.1\" description=\"2025-08-04\">\n\n## [v2.11.1: You're Better Auth Now](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.1)\n\n## What's Changed\n### New Features 🎉\n* Introduce `RemoteAuthProvider` for cleaner external identity provider integration, update docs by [@jlowin](https://github.com/jlowin) in [#1346](https://github.com/PrefectHQ/fastmcp/pull/1346)\n### Enhancements 🔧\n* perf: optimize string operations in OpenAPI parameter processing by [@chi2liu](https://github.com/chi2liu) in [#1342](https://github.com/PrefectHQ/fastmcp/pull/1342)\n### Fixes 🐞\n* Fix method-bound FunctionTool schemas by [@strawgate](https://github.com/strawgate) in [#1360](https://github.com/PrefectHQ/fastmcp/pull/1360)\n* Manually set `_key` after `model_copy()` to enable prefixing Transformed Tools by [@strawgate](https://github.com/strawgate) in [#1357](https://github.com/PrefectHQ/fastmcp/pull/1357)\n### Docs 📚\n* Docs updates by [@jlowin](https://github.com/jlowin) in [#1336](https://github.com/PrefectHQ/fastmcp/pull/1336)\n* Add 2.11 to changelog by [@jlowin](https://github.com/jlowin) in [#1337](https://github.com/PrefectHQ/fastmcp/pull/1337)\n* Update AuthKit vocab by [@jlowin](https://github.com/jlowin) in [#1338](https://github.com/PrefectHQ/fastmcp/pull/1338)\n* Fix typo in decorating-methods.mdx by [@Ozzuke](https://github.com/Ozzuke) in [#1344](https://github.com/PrefectHQ/fastmcp/pull/1344)\n\n## New Contributors\n* [@Ozzuke](https://github.com/Ozzuke) made their first contribution in [#1344](https://github.com/PrefectHQ/fastmcp/pull/1344)\n\n**Full Changelog**: [v2.11.0...v2.11.1](https://github.com/PrefectHQ/fastmcp/compare/v2.11.0...v2.11.1)\n\n</Update>\n\n<Update label=\"v2.11.0\" description=\"2025-08-01\">\n\n## [v2.11.0: Auth to a Good Start](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.0)\n\nFastMCP 2.11 doubles down on what developers need most: speed and simplicity. This massive release delivers significant performance improvements and a dramatically better developer experience.\n\n🔐 **Enterprise-Ready Authentication** brings comprehensive OAuth 2.1 support with WorkOS's AuthKit integration. The new AuthProvider interface leverages MCP's support for separate resource and authorization servers, handling API keys and remote authentication with Dynamic Client Registration. AuthKit integration means you can plug into existing enterprise identity systems without rebuilding your auth stack, setting the stage for plug-and-play auth that doesn't require users to become security experts overnight.\n\n⚡ The **Experimental OpenAPI Parser** delivers dramatic performance improvements through single-pass schema processing and optimized memory usage. OpenAPI integrations are now significantly faster, with cleaner, more maintainable code. _(Note: the experimental parser is disabled by default, set `FASTMCPEXPERIMENTALENABLENEWOPENAPIPARSER=1` to enable it. A message will be shown to all users on the legacy parser encouraging them to try the new one before it becomes the default.)_\n\n🧠 **Context State Management** finally gives you persistent state across tool calls with a simple dict interface, while enhanced meta support lets you expose rich component metadata to clients. Combined with improved type annotations, string-based argument descriptions, and UV transport support, this release makes FastMCP feel more intuitive than ever.\n\nThis release represents a TON of community contributions and sets the foundation for even more ambitious features ahead.\n\n## What's Changed\n### New Features 🎉\n* Introduce experimental OpenAPI parser with improved performance and maintainability by [@jlowin](https://github.com/jlowin) in [#1209](https://github.com/PrefectHQ/fastmcp/pull/1209)\n* Add state dict to Context (#1118) by [@mukulmurthy](https://github.com/mukulmurthy) in [#1160](https://github.com/PrefectHQ/fastmcp/pull/1160)\n* Expose FastMCP tags to clients via component `meta` dict by [@jlowin](https://github.com/jlowin) in [#1281](https://github.com/PrefectHQ/fastmcp/pull/1281)\n* Add _fastmcp meta namespace by [@jlowin](https://github.com/jlowin) in [#1290](https://github.com/PrefectHQ/fastmcp/pull/1290)\n* Add TokenVerifier protocol support alongside existing OAuthProvider authentication by [@jlowin](https://github.com/jlowin) in [#1297](https://github.com/PrefectHQ/fastmcp/pull/1297)\n* Add comprehensive OAuth 2.1 authentication system with WorkOS integration by [@jlowin](https://github.com/jlowin) in [#1327](https://github.com/PrefectHQ/fastmcp/pull/1327)\n### Enhancements 🔧\n* [🐶] Transform MCP Server Tools by [@strawgate](https://github.com/strawgate) in [#1132](https://github.com/PrefectHQ/fastmcp/pull/1132)\n* Add --python, --project, and --with-requirements options to CLI commands by [@jlowin](https://github.com/jlowin) in [#1190](https://github.com/PrefectHQ/fastmcp/pull/1190)\n* Support `fastmcp run mcp.json` by [@strawgate](https://github.com/strawgate) in [#1138](https://github.com/PrefectHQ/fastmcp/pull/1138)\n* Support from __future__ import annotations by [@jlowin](https://github.com/jlowin) in [#1199](https://github.com/PrefectHQ/fastmcp/pull/1199)\n* Optimize OpenAPI parser performance with single-pass schema processing by [@jlowin](https://github.com/jlowin) in [#1214](https://github.com/PrefectHQ/fastmcp/pull/1214)\n* Log tool name on transform validation error by [@strawgate](https://github.com/strawgate) in [#1238](https://github.com/PrefectHQ/fastmcp/pull/1238)\n* Refactor `get_http_request` and `context.session_id` by [@hopeful0](https://github.com/hopeful0) in [#1242](https://github.com/PrefectHQ/fastmcp/pull/1242)\n* Support creating tool argument descriptions from string annotations by [@jlowin](https://github.com/jlowin) in [#1255](https://github.com/PrefectHQ/fastmcp/pull/1255)\n* feat: Add Annotations support for resources and resource templates by [@chughtapan](https://github.com/chughtapan) in [#1260](https://github.com/PrefectHQ/fastmcp/pull/1260)\n* Add UV Transport by [@strawgate](https://github.com/strawgate) in [#1270](https://github.com/PrefectHQ/fastmcp/pull/1270)\n* Improve OpenAPI-to-JSONSchema conversion utilities by [@jlowin](https://github.com/jlowin) in [#1283](https://github.com/PrefectHQ/fastmcp/pull/1283)\n* Ensure proxy components forward meta dicts by [@jlowin](https://github.com/jlowin) in [#1282](https://github.com/PrefectHQ/fastmcp/pull/1282)\n* fix: server argument passing in CLI run command by [@chughtapan](https://github.com/chughtapan) in [#1293](https://github.com/PrefectHQ/fastmcp/pull/1293)\n* Add meta support to tool transformation utilities by [@jlowin](https://github.com/jlowin) in [#1295](https://github.com/PrefectHQ/fastmcp/pull/1295)\n* feat: Allow Resource Metadata URL as field in OAuthProvider by [@dacamposol](https://github.com/dacamposol) in [#1287](https://github.com/PrefectHQ/fastmcp/pull/1287)\n* Use a simple overwrite instead of a merge for meta by [@jlowin](https://github.com/jlowin) in [#1296](https://github.com/PrefectHQ/fastmcp/pull/1296)\n* Remove unused TimedCache by [@strawgate](https://github.com/strawgate) in [#1303](https://github.com/PrefectHQ/fastmcp/pull/1303)\n* refactor: standardize logging usage across OpenAPI utilities by [@chi2liu](https://github.com/chi2liu) in [#1322](https://github.com/PrefectHQ/fastmcp/pull/1322)\n* perf: optimize OpenAPI parsing by reducing dict copy operations by [@chi2liu](https://github.com/chi2liu) in [#1321](https://github.com/PrefectHQ/fastmcp/pull/1321)\n* Structured client-side logging by [@cjermain](https://github.com/cjermain) in [#1326](https://github.com/PrefectHQ/fastmcp/pull/1326)\n### Fixes 🐞\n* fix: preserve def reference when referenced in allOf / oneOf / anyOf by [@algirdasci](https://github.com/algirdasci) in [#1208](https://github.com/PrefectHQ/fastmcp/pull/1208)\n* fix: add type hint to custom_route decorator by [@zzstoatzz](https://github.com/zzstoatzz) in [#1210](https://github.com/PrefectHQ/fastmcp/pull/1210)\n* chore: typo by [@richardkmichael](https://github.com/richardkmichael) in [#1216](https://github.com/PrefectHQ/fastmcp/pull/1216)\n* fix: handle non-string $ref values in experimental OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#1217](https://github.com/PrefectHQ/fastmcp/pull/1217)\n* Skip repeated type conversion and validation in proxy client elicitation handler by [@chughtapan](https://github.com/chughtapan) in [#1222](https://github.com/PrefectHQ/fastmcp/pull/1222)\n* Ensure default fields are not marked nullable by [@jlowin](https://github.com/jlowin) in [#1224](https://github.com/PrefectHQ/fastmcp/pull/1224)\n* Fix stateful proxy client mixing in multi-proxies sessions by [@hopeful0](https://github.com/hopeful0) in [#1245](https://github.com/PrefectHQ/fastmcp/pull/1245)\n* Fix invalid async context manager usage in proxy documentation by [@zzstoatzz](https://github.com/zzstoatzz) in [#1246](https://github.com/PrefectHQ/fastmcp/pull/1246)\n* fix: experimental FastMCPOpenAPI server lost headers in request when __init__(client with headers) by [@itaru2622](https://github.com/itaru2622) in [#1254](https://github.com/PrefectHQ/fastmcp/pull/1254)\n* Fix typing, add tests for tool call middleware by [@jlowin](https://github.com/jlowin) in [#1269](https://github.com/PrefectHQ/fastmcp/pull/1269)\n* Fix: prune hidden parameter defs by [@muhammadkhalid-03](https://github.com/muhammadkhalid-03) in [#1257](https://github.com/PrefectHQ/fastmcp/pull/1257)\n* Fix nullable field handling in OpenAPI to JSON Schema conversion by [@jlowin](https://github.com/jlowin) in [#1279](https://github.com/PrefectHQ/fastmcp/pull/1279)\n* Ensure fastmcp run supports v1 servers by [@jlowin](https://github.com/jlowin) in [#1332](https://github.com/PrefectHQ/fastmcp/pull/1332)\n### Breaking Changes 🛫\n* Change server flag to --name by [@jlowin](https://github.com/jlowin) in [#1248](https://github.com/PrefectHQ/fastmcp/pull/1248)\n### Docs 📚\n* Remove unused import from FastAPI integration documentation by [@mariotaddeucci](https://github.com/mariotaddeucci) in [#1194](https://github.com/PrefectHQ/fastmcp/pull/1194)\n* Update fastapi docs by [@jlowin](https://github.com/jlowin) in [#1198](https://github.com/PrefectHQ/fastmcp/pull/1198)\n* Add docs for context state management by [@jlowin](https://github.com/jlowin) in [#1227](https://github.com/PrefectHQ/fastmcp/pull/1227)\n* Permit.io integration docs by [@orweis](https://github.com/orweis) in [#1226](https://github.com/PrefectHQ/fastmcp/pull/1226)\n* Update docs to reflect sync tools by [@jlowin](https://github.com/jlowin) in [#1234](https://github.com/PrefectHQ/fastmcp/pull/1234)\n* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1235](https://github.com/PrefectHQ/fastmcp/pull/1235)\n* Update SDK docs by [@jlowin](https://github.com/jlowin) in [#1236](https://github.com/PrefectHQ/fastmcp/pull/1236)\n* Update --name flag documentation for Cursor/Claude by [@adam-conway](https://github.com/adam-conway) in [#1239](https://github.com/PrefectHQ/fastmcp/pull/1239)\n* Add annotations docs by [@jlowin](https://github.com/jlowin) in [#1268](https://github.com/PrefectHQ/fastmcp/pull/1268)\n* Update openapi/fastapi URLs README.md by [@jbn](https://github.com/jbn) in [#1278](https://github.com/PrefectHQ/fastmcp/pull/1278)\n* Add 2.11 version badge for state management by [@jlowin](https://github.com/jlowin) in [#1289](https://github.com/PrefectHQ/fastmcp/pull/1289)\n* Add meta parameter support to tools, resources, templates, and prompts decorators by [@jlowin](https://github.com/jlowin) in [#1294](https://github.com/PrefectHQ/fastmcp/pull/1294)\n* docs: update get_state and set_state references by [@Maxi91f](https://github.com/Maxi91f) in [#1306](https://github.com/PrefectHQ/fastmcp/pull/1306)\n* Add unit tests and docs for denying tool calls with middleware by [@jlowin](https://github.com/jlowin) in [#1333](https://github.com/PrefectHQ/fastmcp/pull/1333)\n* Remove reference to stacked decorators by [@jlowin](https://github.com/jlowin) in [#1334](https://github.com/PrefectHQ/fastmcp/pull/1334)\n* Eunomia authorization server can run embedded within the MCP server by [@tommitt](https://github.com/tommitt) in [#1317](https://github.com/PrefectHQ/fastmcp/pull/1317)\n### Other Changes 🦾\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#1230](https://github.com/PrefectHQ/fastmcp/pull/1230)\n* Logcapture addition to test_server file by [@Sourav-Tripathy](https://github.com/Sourav-Tripathy) in [#1229](https://github.com/PrefectHQ/fastmcp/pull/1229)\n* Add tests for headers with both legacy and experimental openapi parser by [@jlowin](https://github.com/jlowin) in [#1259](https://github.com/PrefectHQ/fastmcp/pull/1259)\n* Small clean-up from MCP Tool Transform PR by [@strawgate](https://github.com/strawgate) in [#1267](https://github.com/PrefectHQ/fastmcp/pull/1267)\n* Add test for proxy tags visibility by [@jlowin](https://github.com/jlowin) in [#1302](https://github.com/PrefectHQ/fastmcp/pull/1302)\n* Add unit test for sampling with image messages by [@jlowin](https://github.com/jlowin) in [#1329](https://github.com/PrefectHQ/fastmcp/pull/1329)\n* Remove redundant resource_metadata_url assignment by [@jlowin](https://github.com/jlowin) in [#1328](https://github.com/PrefectHQ/fastmcp/pull/1328)\n* Update bug.yml by [@jlowin](https://github.com/jlowin) in [#1331](https://github.com/PrefectHQ/fastmcp/pull/1331)\n* Ensure validation errors are raised when masked by [@jlowin](https://github.com/jlowin) in [#1330](https://github.com/PrefectHQ/fastmcp/pull/1330)\n\n## New Contributors\n* [@mariotaddeucci](https://github.com/mariotaddeucci) made their first contribution in [#1194](https://github.com/PrefectHQ/fastmcp/pull/1194)\n* [@algirdasci](https://github.com/algirdasci) made their first contribution in [#1208](https://github.com/PrefectHQ/fastmcp/pull/1208)\n* [@chughtapan](https://github.com/chughtapan) made their first contribution in [#1222](https://github.com/PrefectHQ/fastmcp/pull/1222)\n* [@mukulmurthy](https://github.com/mukulmurthy) made their first contribution in [#1160](https://github.com/PrefectHQ/fastmcp/pull/1160)\n* [@orweis](https://github.com/orweis) made their first contribution in [#1226](https://github.com/PrefectHQ/fastmcp/pull/1226)\n* [@Sourav-Tripathy](https://github.com/Sourav-Tripathy) made their first contribution in [#1229](https://github.com/PrefectHQ/fastmcp/pull/1229)\n* [@adam-conway](https://github.com/adam-conway) made their first contribution in [#1239](https://github.com/PrefectHQ/fastmcp/pull/1239)\n* [@muhammadkhalid-03](https://github.com/muhammadkhalid-03) made their first contribution in [#1257](https://github.com/PrefectHQ/fastmcp/pull/1257)\n* [@jbn](https://github.com/jbn) made their first contribution in [#1278](https://github.com/PrefectHQ/fastmcp/pull/1278)\n* [@dacamposol](https://github.com/dacamposol) made their first contribution in [#1287](https://github.com/PrefectHQ/fastmcp/pull/1287)\n* [@chi2liu](https://github.com/chi2liu) made their first contribution in [#1322](https://github.com/PrefectHQ/fastmcp/pull/1322)\n* [@cjermain](https://github.com/cjermain) made their first contribution in [#1326](https://github.com/PrefectHQ/fastmcp/pull/1326)\n\n**Full Changelog**: [v2.10.6...v2.11.0](https://github.com/PrefectHQ/fastmcp/compare/v2.10.6...v2.11.0)\n\n</Update>\n\n<Update label=\"v2.10.6\" description=\"2025-07-19\">\n\n## [v2.10.6: Hymn for the Weekend](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.6)\n\nA special Saturday release with many fixes.\n\n## What's Changed\n### Enhancements 🔧\n* Resolve #1139 -- Implement include_context argument in Context.sample by [@codingjoe](https://github.com/codingjoe) in [#1141](https://github.com/PrefectHQ/fastmcp/pull/1141)\n* feat(settings): add log level normalization by [@ka2048](https://github.com/ka2048) in [#1171](https://github.com/PrefectHQ/fastmcp/pull/1171)\n* add server name to mounted server warnings by [@artificial-aidan](https://github.com/artificial-aidan) in [#1147](https://github.com/PrefectHQ/fastmcp/pull/1147)\n* Add StatefulProxyClient by [@hopeful0](https://github.com/hopeful0) in [#1109](https://github.com/PrefectHQ/fastmcp/pull/1109)\n### Fixes 🐞\n* Fix OpenAPI empty parameters by [@FabrizioSandri](https://github.com/FabrizioSandri) in [#1128](https://github.com/PrefectHQ/fastmcp/pull/1128)\n* Fix title field preservation in tool transformations by [@jlowin](https://github.com/jlowin) in [#1131](https://github.com/PrefectHQ/fastmcp/pull/1131)\n* Fix optional parameter validation in OpenAPI integration by [@jlowin](https://github.com/jlowin) in [#1135](https://github.com/PrefectHQ/fastmcp/pull/1135)\n* Do not silently exclude the \"context\" key from JSON body by [@melkamar](https://github.com/melkamar) in [#1153](https://github.com/PrefectHQ/fastmcp/pull/1153)\n* Fix tool output schema generation to respect Pydantic serialization aliases by [@zzstoatzz](https://github.com/zzstoatzz) in [#1148](https://github.com/PrefectHQ/fastmcp/pull/1148)\n* fix: _replace_ref_with_defs; ensure ref_path is string by [@itaru2622](https://github.com/itaru2622) in [#1164](https://github.com/PrefectHQ/fastmcp/pull/1164)\n* Fix nesting when making OpenAPI arrays and objects optional by [@melkamar](https://github.com/melkamar) in [#1178](https://github.com/PrefectHQ/fastmcp/pull/1178)\n* Fix `mcp-json` output format to include server name by [@jlowin](https://github.com/jlowin) in [#1185](https://github.com/PrefectHQ/fastmcp/pull/1185)\n* Only configure logging one time by [@jlowin](https://github.com/jlowin) in [#1187](https://github.com/PrefectHQ/fastmcp/pull/1187)\n### Docs 📚\n* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1127](https://github.com/PrefectHQ/fastmcp/pull/1127)\n* Eunomia Authorization with native FastMCP's Middleware by [@tommitt](https://github.com/tommitt) in [#1144](https://github.com/PrefectHQ/fastmcp/pull/1144)\n* update api ref for new `mdxify` version by [@zzstoatzz](https://github.com/zzstoatzz) in [#1182](https://github.com/PrefectHQ/fastmcp/pull/1182)\n### Other Changes 🦾\n* Expand empty parameter filtering and add comprehensive tests by [@jlowin](https://github.com/jlowin) in [#1129](https://github.com/PrefectHQ/fastmcp/pull/1129)\n* Add no-commit-to-branch hook by [@zzstoatzz](https://github.com/zzstoatzz) in [#1149](https://github.com/PrefectHQ/fastmcp/pull/1149)\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#1165](https://github.com/PrefectHQ/fastmcp/pull/1165)\n* skip on rate limit by [@zzstoatzz](https://github.com/zzstoatzz) in [#1183](https://github.com/PrefectHQ/fastmcp/pull/1183)\n* Remove deprecated proxy creation by [@jlowin](https://github.com/jlowin) in [#1186](https://github.com/PrefectHQ/fastmcp/pull/1186)\n* Separate integration tests from unit tests in CI by [@jlowin](https://github.com/jlowin) in [#1188](https://github.com/PrefectHQ/fastmcp/pull/1188)\n\n## New Contributors\n* [@FabrizioSandri](https://github.com/FabrizioSandri) made their first contribution in [#1128](https://github.com/PrefectHQ/fastmcp/pull/1128)\n* [@melkamar](https://github.com/melkamar) made their first contribution in [#1153](https://github.com/PrefectHQ/fastmcp/pull/1153)\n* [@codingjoe](https://github.com/codingjoe) made their first contribution in [#1141](https://github.com/PrefectHQ/fastmcp/pull/1141)\n* [@itaru2622](https://github.com/itaru2622) made their first contribution in [#1164](https://github.com/PrefectHQ/fastmcp/pull/1164)\n* [@ka2048](https://github.com/ka2048) made their first contribution in [#1171](https://github.com/PrefectHQ/fastmcp/pull/1171)\n* [@artificial-aidan](https://github.com/artificial-aidan) made their first contribution in [#1147](https://github.com/PrefectHQ/fastmcp/pull/1147)\n\n**Full Changelog**: [v2.10.5...v2.10.6](https://github.com/PrefectHQ/fastmcp/compare/v2.10.5...v2.10.6)\n\n</Update>\n\n<Update label=\"v2.10.5\" description=\"2025-07-11\">\n\n## [v2.10.5: Middle Management](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.5)\n\nA maintenance release focused on OpenAPI refinements and middleware fixes, plus console improvements.\n\n## What's Changed\n### Enhancements 🔧\n* Fix Claude Code CLI detection for npm global installations by [@jlowin](https://github.com/jlowin) in [#1106](https://github.com/PrefectHQ/fastmcp/pull/1106)\n* Fix OpenAPI parameter name collisions with location suffixing by [@jlowin](https://github.com/jlowin) in [#1107](https://github.com/PrefectHQ/fastmcp/pull/1107)\n* Add mirrored component support for proxy servers by [@jlowin](https://github.com/jlowin) in [#1105](https://github.com/PrefectHQ/fastmcp/pull/1105)\n### Fixes 🐞\n* Fix OpenAPI deepObject style parameter encoding by [@jlowin](https://github.com/jlowin) in [#1122](https://github.com/PrefectHQ/fastmcp/pull/1122)\n* xfail when github token is not set ('' or None) by [@jlowin](https://github.com/jlowin) in [#1123](https://github.com/PrefectHQ/fastmcp/pull/1123)\n* fix: replace oneOf with anyOf in OpenAPI output schemas by [@MagnusS0](https://github.com/MagnusS0) in [#1119](https://github.com/PrefectHQ/fastmcp/pull/1119)\n* Fix middleware list result types by [@jlowin](https://github.com/jlowin) in [#1125](https://github.com/PrefectHQ/fastmcp/pull/1125)\n* Improve console width for logo by [@jlowin](https://github.com/jlowin) in [#1126](https://github.com/PrefectHQ/fastmcp/pull/1126)\n### Docs 📚\n* Improve transport + integration docs by [@jlowin](https://github.com/jlowin) in [#1103](https://github.com/PrefectHQ/fastmcp/pull/1103)\n* Update proxy.mdx by [@coldfire-x](https://github.com/coldfire-x) in [#1108](https://github.com/PrefectHQ/fastmcp/pull/1108)\n### Other Changes 🦾\n* Update github remote server tests with secret by [@jlowin](https://github.com/jlowin) in [#1112](https://github.com/PrefectHQ/fastmcp/pull/1112)\n\n## New Contributors\n* [@coldfire-x](https://github.com/coldfire-x) made their first contribution in [#1108](https://github.com/PrefectHQ/fastmcp/pull/1108)\n* [@MagnusS0](https://github.com/MagnusS0) made their first contribution in [#1119](https://github.com/PrefectHQ/fastmcp/pull/1119)\n\n**Full Changelog**: [v2.10.4...v2.10.5](https://github.com/PrefectHQ/fastmcp/compare/v2.10.4...v2.10.5)\n\n</Update>\n\n<Update label=\"v2.10.4\" description=\"2025-07-09\">\n\n## [v2.10.4: Transport-ation](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.4)\n\nA quick fix to ensure the CLI accepts \"streamable-http\" as a valid transport option.\n\n## What's Changed\n### Fixes 🐞\n* Ensure the CLI accepts \"streamable-http\" as a valid transport by [@jlowin](https://github.com/jlowin) in [#1099](https://github.com/PrefectHQ/fastmcp/pull/1099)\n\n**Full Changelog**: [v2.10.3...v2.10.4](https://github.com/PrefectHQ/fastmcp/compare/v2.10.3...v2.10.4)\n\n</Update>\n\n<Update label=\"v2.10.3\" description=\"2025-07-09\">\n\n## [v2.10.3: CLI Me a River](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.3)\n\nA major CLI overhaul featuring a complete refactor from typer to cyclopts, new IDE integrations, and comprehensive OpenAPI improvements.\n\n## What's Changed\n### New Features 🎉\n* Refactor CLI from typer to cyclopts and add comprehensive tests by [@jlowin](https://github.com/jlowin) in [#1062](https://github.com/PrefectHQ/fastmcp/pull/1062)\n* Add output schema support for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#1073](https://github.com/PrefectHQ/fastmcp/pull/1073)\n### Enhancements 🔧\n* Add Cursor support via CLI integration by [@jlowin](https://github.com/jlowin) in [#1052](https://github.com/PrefectHQ/fastmcp/pull/1052)\n* Add Claude Code install integration by [@jlowin](https://github.com/jlowin) in [#1053](https://github.com/PrefectHQ/fastmcp/pull/1053)\n* Generate MCP JSON config output from CLI as new `fastmcp install` command by [@jlowin](https://github.com/jlowin) in [#1056](https://github.com/PrefectHQ/fastmcp/pull/1056)\n* Use isawaitable instead of iscoroutine by [@jlowin](https://github.com/jlowin) in [#1059](https://github.com/PrefectHQ/fastmcp/pull/1059)\n* feat: Add `--path` Option to CLI for HTTP/SSE Route by [@davidbk-legit](https://github.com/davidbk-legit) in [#1087](https://github.com/PrefectHQ/fastmcp/pull/1087)\n* Fix concurrent proxy client operations with session isolation by [@jlowin](https://github.com/jlowin) in [#1083](https://github.com/PrefectHQ/fastmcp/pull/1083)\n### Fixes 🐞\n* Refactor Client context management to avoid concurrency issue by [@hopeful0](https://github.com/hopeful0) in [#1054](https://github.com/PrefectHQ/fastmcp/pull/1054)\n* Keep json schema $defs on transform by [@strawgate](https://github.com/strawgate) in [#1066](https://github.com/PrefectHQ/fastmcp/pull/1066)\n* Ensure fastmcp version copy is plaintext by [@jlowin](https://github.com/jlowin) in [#1071](https://github.com/PrefectHQ/fastmcp/pull/1071)\n* Fix single-element list unwrapping in tool content by [@jlowin](https://github.com/jlowin) in [#1074](https://github.com/PrefectHQ/fastmcp/pull/1074)\n* Fix max recursion error when pruning OpenAPI definitions by [@dimitribarbot](https://github.com/dimitribarbot) in [#1092](https://github.com/PrefectHQ/fastmcp/pull/1092)\n* Fix OpenAPI tool name registration when modified by mcp_component_fn by [@jlowin](https://github.com/jlowin) in [#1096](https://github.com/PrefectHQ/fastmcp/pull/1096)\n### Docs 📚\n* Docs: add example of more concise way to use bearer auth by [@neilconway](https://github.com/neilconway) in [#1055](https://github.com/PrefectHQ/fastmcp/pull/1055)\n* Update favicon by [@jlowin](https://github.com/jlowin) in [#1058](https://github.com/PrefectHQ/fastmcp/pull/1058)\n* Update environment note by [@jlowin](https://github.com/jlowin) in [#1075](https://github.com/PrefectHQ/fastmcp/pull/1075)\n* Add fastmcp version --copy documentation by [@jlowin](https://github.com/jlowin) in [#1076](https://github.com/PrefectHQ/fastmcp/pull/1076)\n### Other Changes 🦾\n* Remove asserts and add documentation following #1054 by [@jlowin](https://github.com/jlowin) in [#1057](https://github.com/PrefectHQ/fastmcp/pull/1057)\n* Add --copy flag for fastmcp version by [@jlowin](https://github.com/jlowin) in [#1063](https://github.com/PrefectHQ/fastmcp/pull/1063)\n* Fix docstring format for fastmcp.client.Client by [@neilconway](https://github.com/neilconway) in [#1094](https://github.com/PrefectHQ/fastmcp/pull/1094)\n\n## New Contributors\n* [@neilconway](https://github.com/neilconway) made their first contribution in [#1055](https://github.com/PrefectHQ/fastmcp/pull/1055)\n* [@davidbk-legit](https://github.com/davidbk-legit) made their first contribution in [#1087](https://github.com/PrefectHQ/fastmcp/pull/1087)\n* [@dimitribarbot](https://github.com/dimitribarbot) made their first contribution in [#1092](https://github.com/PrefectHQ/fastmcp/pull/1092)\n\n**Full Changelog**: [v2.10.2...v2.10.3](https://github.com/PrefectHQ/fastmcp/compare/v2.10.2...v2.10.3)\n\n</Update>\n\n<Update label=\"v2.10.2\" description=\"2025-07-05\">\n\n## [v2.10.2: Forward March](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.2)\n\nThe headline feature of this release is the ability to \"forward\" advanced MCP interactions like logging, progress, and elicitation through proxy servers. If the remote server requests an elicitation, the proxy client will pass that request to the new, \"ultimate\" client.\n\n## What's Changed\n### New Features 🎉\n* Proxy support advanced MCP features by [@hopeful0](https://github.com/hopeful0) in [#1022](https://github.com/PrefectHQ/fastmcp/pull/1022)\n### Enhancements 🔧\n* Re-add splash screen by [@jlowin](https://github.com/jlowin) in [#1027](https://github.com/PrefectHQ/fastmcp/pull/1027)\n* Reduce banner padding by [@jlowin](https://github.com/jlowin) in [#1030](https://github.com/PrefectHQ/fastmcp/pull/1030)\n* Allow per-server timeouts in MCPConfig by [@cegersdoerfer](https://github.com/cegersdoerfer) in [#1031](https://github.com/PrefectHQ/fastmcp/pull/1031)\n* Support 'scp' claim for OAuth scopes in BearerAuthProvider by [@jlowin](https://github.com/jlowin) in [#1033](https://github.com/PrefectHQ/fastmcp/pull/1033)\n* Add path expansion to image/audio/file by [@jlowin](https://github.com/jlowin) in [#1038](https://github.com/PrefectHQ/fastmcp/pull/1038)\n* Ensure multi-client configurations use new ProxyClient by [@jlowin](https://github.com/jlowin) in [#1045](https://github.com/PrefectHQ/fastmcp/pull/1045)\n### Fixes 🐞\n* Expose stateless_http kwarg for mcp.run() by [@jlowin](https://github.com/jlowin) in [#1018](https://github.com/PrefectHQ/fastmcp/pull/1018)\n* Avoid propagating logs by [@jlowin](https://github.com/jlowin) in [#1042](https://github.com/PrefectHQ/fastmcp/pull/1042)\n### Docs 📚\n* Clean up docs by [@jlowin](https://github.com/jlowin) in [#1028](https://github.com/PrefectHQ/fastmcp/pull/1028)\n* Docs: clarify server URL paths for ChatGPT integration by [@thap2331](https://github.com/thap2331) in [#1017](https://github.com/PrefectHQ/fastmcp/pull/1017)\n### Other Changes 🦾\n* Split giant openapi test file into smaller files by [@jlowin](https://github.com/jlowin) in [#1034](https://github.com/PrefectHQ/fastmcp/pull/1034)\n* Add comprehensive OpenAPI 3.0 vs 3.1 compatibility tests by [@jlowin](https://github.com/jlowin) in [#1035](https://github.com/PrefectHQ/fastmcp/pull/1035)\n* Update banner and use console.log by [@jlowin](https://github.com/jlowin) in [#1041](https://github.com/PrefectHQ/fastmcp/pull/1041)\n\n## New Contributors\n* [@cegersdoerfer](https://github.com/cegersdoerfer) made their first contribution in [#1031](https://github.com/PrefectHQ/fastmcp/pull/1031)\n* [@hopeful0](https://github.com/hopeful0) made their first contribution in [#1022](https://github.com/PrefectHQ/fastmcp/pull/1022)\n* [@thap2331](https://github.com/thap2331) made their first contribution in [#1017](https://github.com/PrefectHQ/fastmcp/pull/1017)\n\n**Full Changelog**: [v2.10.1...v2.10.2](https://github.com/PrefectHQ/fastmcp/compare/v2.10.1...v2.10.2)\n\n</Update>\n\n<Update label=\"v2.10.1\" description=\"2025-07-02\">\n\n## [v2.10.1: Revert to Sender](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.1)\n\nA quick patch to revert the CLI banner that was added in v2.10.0.\n\n## What's Changed\n### Docs 📚\n* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1009](https://github.com/PrefectHQ/fastmcp/pull/1009)\n* Revert \"Add CLI banner\" by [@jlowin](https://github.com/jlowin) in [#1011](https://github.com/PrefectHQ/fastmcp/pull/1011)\n\n**Full Changelog**: [v2.10.0...v2.10.1](https://github.com/PrefectHQ/fastmcp/compare/v2.10.0...v2.10.1)\n\n</Update>\n\n<Update label=\"v2.10.0\" description=\"2024-07-01\">\n\n## [v2.10.0: Great Spec-tations](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.0)\n\nFastMCP 2.10 brings full compliance with the 6/18/2025 MCP spec update, introducing elicitation support for dynamic server-client communication and output schemas for structured tool responses. Please note that due to these changes, this release also includes a breaking change to the return signature of `client.call_tool()`.\n\n### Elicitation Support\nElicitation allows MCP servers to request additional information from clients during tool execution, enabling more interactive and dynamic server behavior. This opens up new possibilities for tools that need user input or confirmation during execution.\n\n### Output Schemas\nTools can now define structured output schemas, ensuring that responses conform to expected formats and making tool integration more predictable and type-safe.\n\n## What's Changed\n### New Features 🎉\n* MCP 6/18/25: Add output schema to tools by [@jlowin](https://github.com/jlowin) in [#901](https://github.com/PrefectHQ/fastmcp/pull/901)\n* MCP 6/18/25: Elicitation support by [@jlowin](https://github.com/jlowin) in [#889](https://github.com/PrefectHQ/fastmcp/pull/889)\n### Enhancements 🔧\n* Update types + tests for SDK changes by [@jlowin](https://github.com/jlowin) in [#888](https://github.com/PrefectHQ/fastmcp/pull/888)\n* MCP 6/18/25: Update auth primitives by [@jlowin](https://github.com/jlowin) in [#966](https://github.com/PrefectHQ/fastmcp/pull/966)\n* Add OpenAPI extensions support to HTTPRoute by [@maddymanu](https://github.com/maddymanu) in [#977](https://github.com/PrefectHQ/fastmcp/pull/977)\n* Add title field support to FastMCP components by [@jlowin](https://github.com/jlowin) in [#982](https://github.com/PrefectHQ/fastmcp/pull/982)\n* Support implicit Elicitation acceptance by [@jlowin](https://github.com/jlowin) in [#983](https://github.com/PrefectHQ/fastmcp/pull/983)\n* Support 'no response' elicitation requests by [@jlowin](https://github.com/jlowin) in [#992](https://github.com/PrefectHQ/fastmcp/pull/992)\n* Add Support for Configurable Algorithms by [@sstene1](https://github.com/sstene1) in [#997](https://github.com/PrefectHQ/fastmcp/pull/997)\n### Fixes 🐞\n* Improve stdio error handling to raise connection failures immediately by [@jlowin](https://github.com/jlowin) in [#984](https://github.com/PrefectHQ/fastmcp/pull/984)\n* Fix type hints for FunctionResource:fn by [@CfirTsabari](https://github.com/CfirTsabari) in [#986](https://github.com/PrefectHQ/fastmcp/pull/986)\n* Update link to OpenAI MCP example by [@mossbanay](https://github.com/mossbanay) in [#985](https://github.com/PrefectHQ/fastmcp/pull/985)\n* Fix output schema generation edge case by [@jlowin](https://github.com/jlowin) in [#995](https://github.com/PrefectHQ/fastmcp/pull/995)\n* Refactor array parameter formatting to reduce code duplication by [@jlowin](https://github.com/jlowin) in [#1007](https://github.com/PrefectHQ/fastmcp/pull/1007)\n* Fix OpenAPI array parameter explode handling by [@jlowin](https://github.com/jlowin) in [#1008](https://github.com/PrefectHQ/fastmcp/pull/1008)\n### Breaking Changes 🛫\n* MCP 6/18/25: Upgrade to mcp 1.10 by [@jlowin](https://github.com/jlowin) in [#887](https://github.com/PrefectHQ/fastmcp/pull/887)\n### Docs 📚\n* Update middleware imports and documentation by [@jlowin](https://github.com/jlowin) in [#999](https://github.com/PrefectHQ/fastmcp/pull/999)\n* Update OpenAI docs by [@jlowin](https://github.com/jlowin) in [#1001](https://github.com/PrefectHQ/fastmcp/pull/1001)\n* Add CLI banner by [@jlowin](https://github.com/jlowin) in [#1005](https://github.com/PrefectHQ/fastmcp/pull/1005)\n### Examples & Contrib 💡\n* Component Manager by [@gorocode](https://github.com/gorocode) in [#976](https://github.com/PrefectHQ/fastmcp/pull/976)\n### Other Changes 🦾\n* Minor auth improvements by [@jlowin](https://github.com/jlowin) in [#967](https://github.com/PrefectHQ/fastmcp/pull/967)\n* Add .ccignore for copychat by [@jlowin](https://github.com/jlowin) in [#1000](https://github.com/PrefectHQ/fastmcp/pull/1000)\n\n## New Contributors\n* [@maddymanu](https://github.com/maddymanu) made their first contribution in [#977](https://github.com/PrefectHQ/fastmcp/pull/977)\n* [@github0hello](https://github.com/github0hello) made their first contribution in [#979](https://github.com/PrefectHQ/fastmcp/pull/979)\n* [@tommitt](https://github.com/tommitt) made their first contribution in [#975](https://github.com/PrefectHQ/fastmcp/pull/975)\n* [@CfirTsabari](https://github.com/CfirTsabari) made their first contribution in [#986](https://github.com/PrefectHQ/fastmcp/pull/986)\n* [@mossbanay](https://github.com/mossbanay) made their first contribution in [#985](https://github.com/PrefectHQ/fastmcp/pull/985)\n* [@sstene1](https://github.com/sstene1) made their first contribution in [#997](https://github.com/PrefectHQ/fastmcp/pull/997)\n\n**Full Changelog**: [v2.9.2...v2.10.0](https://github.com/PrefectHQ/fastmcp/compare/v2.9.2...v2.10.0)\n\n</Update>\n\n<Update label=\"v2.9.2\" description=\"2024-06-26\">\n\n## [v2.9.2: Safety Pin](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.2)\n\nThis is a patch release to pin `mcp` below 1.10, which includes changes related to the 6/18/2025 MCP spec update and could potentially break functionality for some FastMCP users.\n\n## What's Changed\n### Docs 📚\n* Fix version badge for messages by [@jlowin](https://github.com/jlowin) in [#960](https://github.com/PrefectHQ/fastmcp/pull/960)\n### Dependencies 📦\n* Pin mcp dependency by [@jlowin](https://github.com/jlowin) in [#962](https://github.com/PrefectHQ/fastmcp/pull/962)\n\n**Full Changelog**: [v2.9.1...v2.9.2](https://github.com/PrefectHQ/fastmcp/compare/v2.9.1...v2.9.2)\n\n</Update>\n\n<Update label=\"v2.9.1\" description=\"2024-06-26\">\n\n## [v2.9.1: Call Me Maybe](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.1)\n\nFastMCP 2.9.1 introduces automatic MCP list change notifications, allowing servers to notify clients when tools, resources, or prompts are dynamically updated. This enables more responsive and adaptive MCP integrations.\n\n## What's Changed\n### New Features 🎉\n* Add automatic MCP list change notifications and client message handling by [@jlowin](https://github.com/jlowin) in [#939](https://github.com/PrefectHQ/fastmcp/pull/939)\n### Enhancements 🔧\n* Add debug logging to bearer token authentication by [@jlowin](https://github.com/jlowin) in [#952](https://github.com/PrefectHQ/fastmcp/pull/952)\n### Fixes 🐞\n* Fix duplicate error logging in exception handlers by [@jlowin](https://github.com/jlowin) in [#938](https://github.com/PrefectHQ/fastmcp/pull/938)\n* Fix parameter location enum handling in OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#953](https://github.com/PrefectHQ/fastmcp/pull/953)\n* Fix external schema reference handling in OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#954](https://github.com/PrefectHQ/fastmcp/pull/954)\n### Docs 📚\n* Update changelog for 2.9 release by [@jlowin](https://github.com/jlowin) in [#929](https://github.com/PrefectHQ/fastmcp/pull/929)\n* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#935](https://github.com/PrefectHQ/fastmcp/pull/935)\n* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#947](https://github.com/PrefectHQ/fastmcp/pull/947)\n* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#949](https://github.com/PrefectHQ/fastmcp/pull/949)\n### Examples & Contrib 💡\n* Add `create_thread` tool to bsky MCP server by [@zzstoatzz](https://github.com/zzstoatzz) in [#927](https://github.com/PrefectHQ/fastmcp/pull/927)\n* Update `mount_example.py` to work with current fastmcp API by [@rajephon](https://github.com/rajephon) in [#957](https://github.com/PrefectHQ/fastmcp/pull/957)\n\n## New Contributors\n* [@rajephon](https://github.com/rajephon) made their first contribution in [#957](https://github.com/PrefectHQ/fastmcp/pull/957)\n\n**Full Changelog**: [v2.9.0...v2.9.1](https://github.com/PrefectHQ/fastmcp/compare/v2.9.0...v2.9.1)\n\n</Update>\n\n<Update label=\"v2.9.0\" description=\"2024-06-23\">\n\n## [v2.9.0: Stuck in the Middleware With You](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.0)\n\nFastMCP 2.9 introduces two important features that push beyond the basic MCP protocol: MCP Middleware and server-side type conversion.\n\n### MCP Middleware\nMCP middleware lets you intercept and modify requests and responses at the protocol level, giving you powerful capabilities for logging, authentication, validation, and more. This is particularly useful for building production-ready MCP servers that need sophisticated request handling.\n\n### Server-side Type Conversion\nThis release also introduces server-side type conversion for prompt arguments, ensuring that data is properly formatted before being passed to your functions. This reduces the burden on individual tools and prompts to handle type validation and conversion.\n\n## What's Changed\n### New Features 🎉\n* Add File utility for binary data by [@gorocode](https://github.com/gorocode) in [#843](https://github.com/PrefectHQ/fastmcp/pull/843)\n* Consolidate prefix logic into FastMCP methods by [@jlowin](https://github.com/jlowin) in [#861](https://github.com/PrefectHQ/fastmcp/pull/861)\n* Add MCP Middleware by [@jlowin](https://github.com/jlowin) in [#870](https://github.com/PrefectHQ/fastmcp/pull/870)\n* Implement server-side type conversion for prompt arguments by [@jlowin](https://github.com/jlowin) in [#908](https://github.com/PrefectHQ/fastmcp/pull/908)\n### Enhancements 🔧\n* Fix tool description indentation issue by [@zfflxx](https://github.com/zfflxx) in [#845](https://github.com/PrefectHQ/fastmcp/pull/845)\n* Add version parameter to FastMCP constructor by [@mkyutani](https://github.com/mkyutani) in [#842](https://github.com/PrefectHQ/fastmcp/pull/842)\n* Update version to not be positional by [@jlowin](https://github.com/jlowin) in [#848](https://github.com/PrefectHQ/fastmcp/pull/848)\n* Add key to component by [@jlowin](https://github.com/jlowin) in [#869](https://github.com/PrefectHQ/fastmcp/pull/869)\n* Add session_id property to Context for data sharing by [@jlowin](https://github.com/jlowin) in [#881](https://github.com/PrefectHQ/fastmcp/pull/881)\n* Fix CORS documentation example by [@jlowin](https://github.com/jlowin) in [#895](https://github.com/PrefectHQ/fastmcp/pull/895)\n### Fixes 🐞\n* \"report_progress missing passing related_request_id causes notifications not working\" by [@alexsee](https://github.com/alexsee) in [#838](https://github.com/PrefectHQ/fastmcp/pull/838)\n* Fix JWT issuer validation to support string values per RFC 7519 by [@jlowin](https://github.com/jlowin) in [#892](https://github.com/PrefectHQ/fastmcp/pull/892)\n* Fix BearerAuthProvider audience type annotations by [@jlowin](https://github.com/jlowin) in [#894](https://github.com/PrefectHQ/fastmcp/pull/894)\n### Docs 📚\n* Add CLAUDE.md development guidelines by [@jlowin](https://github.com/jlowin) in [#880](https://github.com/PrefectHQ/fastmcp/pull/880)\n* Update context docs for session_id property by [@jlowin](https://github.com/jlowin) in [#882](https://github.com/PrefectHQ/fastmcp/pull/882)\n* Add API reference by [@zzstoatzz](https://github.com/zzstoatzz) in [#893](https://github.com/PrefectHQ/fastmcp/pull/893)\n* Fix API ref rendering by [@zzstoatzz](https://github.com/zzstoatzz) in [#900](https://github.com/PrefectHQ/fastmcp/pull/900)\n* Simplify docs nav by [@jlowin](https://github.com/jlowin) in [#902](https://github.com/PrefectHQ/fastmcp/pull/902)\n* Add fastmcp inspect command by [@jlowin](https://github.com/jlowin) in [#904](https://github.com/PrefectHQ/fastmcp/pull/904)\n* Update client docs by [@jlowin](https://github.com/jlowin) in [#912](https://github.com/PrefectHQ/fastmcp/pull/912)\n* Update docs nav by [@jlowin](https://github.com/jlowin) in [#913](https://github.com/PrefectHQ/fastmcp/pull/913)\n* Update integration documentation for Claude Desktop, ChatGPT, and Claude Code by [@jlowin](https://github.com/jlowin) in [#915](https://github.com/PrefectHQ/fastmcp/pull/915)\n* Add http as an alias for streamable http by [@jlowin](https://github.com/jlowin) in [#917](https://github.com/PrefectHQ/fastmcp/pull/917)\n* Clean up parameter documentation by [@jlowin](https://github.com/jlowin) in [#918](https://github.com/PrefectHQ/fastmcp/pull/918)\n* Add middleware examples for timing, logging, rate limiting, and error handling by [@jlowin](https://github.com/jlowin) in [#919](https://github.com/PrefectHQ/fastmcp/pull/919)\n* ControlFlow → FastMCP rename by [@jlowin](https://github.com/jlowin) in [#922](https://github.com/PrefectHQ/fastmcp/pull/922)\n### Examples & Contrib 💡\n* Add contrib.mcp_mixin support for annotations by [@rsp2k](https://github.com/rsp2k) in [#860](https://github.com/PrefectHQ/fastmcp/pull/860)\n* Add ATProto (Bluesky) MCP Server Example by [@zzstoatzz](https://github.com/zzstoatzz) in [#916](https://github.com/PrefectHQ/fastmcp/pull/916)\n* Fix path in atproto example pyproject by [@zzstoatzz](https://github.com/zzstoatzz) in [#920](https://github.com/PrefectHQ/fastmcp/pull/920)\n* Remove uv source in example by [@zzstoatzz](https://github.com/zzstoatzz) in [#921](https://github.com/PrefectHQ/fastmcp/pull/921)\n\n## New Contributors\n* [@alexsee](https://github.com/alexsee) made their first contribution in [#838](https://github.com/PrefectHQ/fastmcp/pull/838)\n* [@zfflxx](https://github.com/zfflxx) made their first contribution in [#845](https://github.com/PrefectHQ/fastmcp/pull/845)\n* [@mkyutani](https://github.com/mkyutani) made their first contribution in [#842](https://github.com/PrefectHQ/fastmcp/pull/842)\n* [@gorocode](https://github.com/gorocode) made their first contribution in [#843](https://github.com/PrefectHQ/fastmcp/pull/843)\n* [@rsp2k](https://github.com/rsp2k) made their first contribution in [#860](https://github.com/PrefectHQ/fastmcp/pull/860)\n* [@owtaylor](https://github.com/owtaylor) made their first contribution in [#897](https://github.com/PrefectHQ/fastmcp/pull/897)\n* [@Jason-CKY](https://github.com/Jason-CKY) made their first contribution in [#906](https://github.com/PrefectHQ/fastmcp/pull/906)\n\n**Full Changelog**: [v2.8.1...v2.9.0](https://github.com/PrefectHQ/fastmcp/compare/v2.8.1...v2.9.0)\n\n</Update>\n\n<Update label=\"v2.8.1\" description=\"2024-06-15\">\n\n## [v2.8.1: Sound Judgement](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.8.1)\n\n2.8.1 introduces audio support, as well as minor fixes and updates for deprecated features.\n\n### Audio Support\nThis release adds support for audio content in MCP tools and resources, expanding FastMCP's multimedia capabilities beyond text and images.\n\n## What's Changed\n### New Features 🎉\n* Add audio support by [@jlowin](https://github.com/jlowin) in [#833](https://github.com/PrefectHQ/fastmcp/pull/833)\n### Enhancements 🔧\n* Add flag for disabling deprecation warnings by [@jlowin](https://github.com/jlowin) in [#802](https://github.com/PrefectHQ/fastmcp/pull/802)\n* Add examples to Tool Arg Param transformation by [@strawgate](https://github.com/strawgate) in [#806](https://github.com/PrefectHQ/fastmcp/pull/806)\n### Fixes 🐞\n* Restore .settings access as deprecated by [@jlowin](https://github.com/jlowin) in [#800](https://github.com/PrefectHQ/fastmcp/pull/800)\n* Ensure handling of false http kwargs correctly; removed unused kwarg by [@jlowin](https://github.com/jlowin) in [#804](https://github.com/PrefectHQ/fastmcp/pull/804)\n* Bump mcp 1.9.4 by [@jlowin](https://github.com/jlowin) in [#835](https://github.com/PrefectHQ/fastmcp/pull/835)\n### Docs 📚\n* Update changelog for 2.8.0 by [@jlowin](https://github.com/jlowin) in [#794](https://github.com/PrefectHQ/fastmcp/pull/794)\n* Update welcome docs by [@jlowin](https://github.com/jlowin) in [#808](https://github.com/PrefectHQ/fastmcp/pull/808)\n* Update headers in docs by [@jlowin](https://github.com/jlowin) in [#809](https://github.com/PrefectHQ/fastmcp/pull/809)\n* Add MCP group to tutorials by [@jlowin](https://github.com/jlowin) in [#810](https://github.com/PrefectHQ/fastmcp/pull/810)\n* Add Community section to documentation by [@zzstoatzz](https://github.com/zzstoatzz) in [#819](https://github.com/PrefectHQ/fastmcp/pull/819)\n* Add 2.8 update by [@jlowin](https://github.com/jlowin) in [#821](https://github.com/PrefectHQ/fastmcp/pull/821)\n* Embed YouTube videos in community showcase by [@zzstoatzz](https://github.com/zzstoatzz) in [#820](https://github.com/PrefectHQ/fastmcp/pull/820)\n### Other Changes 🦾\n* Ensure http args are passed through by [@jlowin](https://github.com/jlowin) in [#803](https://github.com/PrefectHQ/fastmcp/pull/803)\n* Fix install link in readme by [@jlowin](https://github.com/jlowin) in [#836](https://github.com/PrefectHQ/fastmcp/pull/836)\n\n**Full Changelog**: [v2.8.0...v2.8.1](https://github.com/PrefectHQ/fastmcp/compare/v2.8.0...v2.8.1)\n\n</Update>\n\n<Update label=\"v2.8.0\" description=\"2024-06-10\">\n\n## [v2.8.0: Transform and Roll Out](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.8.0)\n\nFastMCP 2.8.0 introduces powerful new ways to customize and control your MCP servers! \n\n### Tool Transformation\n\nThe highlight of this release is first-class [**Tool Transformation**](/v2/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.\n\n### Component Control\nThis release also gives you more granular control over which components are exposed to clients. With new [**tag-based filtering**](/v2/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](/v2/servers/tools#disabling-tools), offering dynamic control over your server's capabilities.\n\n### Tools-by-Default\nFinally, to improve compatibility with a wider range of LLM clients, this release changes the default behavior for OpenAPI integration: all API endpoints are now converted to `Tools` by default. This is a **breaking change** but pragmatically necessitated by the fact that the majority of MCP clients available today are, sadly, only compatible with MCP tools. Therefore, this change significantly simplifies the out-of-the-box experience and ensures your entire API is immediately accessible to any tool-using agent.\n\n## What's Changed\n### New Features 🎉\n* First-class tool transformation by [@jlowin](https://github.com/jlowin) in [#745](https://github.com/PrefectHQ/fastmcp/pull/745)\n* Support enable/disable for all FastMCP components (tools, prompts, resources, templates) by [@jlowin](https://github.com/jlowin) in [#781](https://github.com/PrefectHQ/fastmcp/pull/781)\n* Add support for tag-based component filtering by [@jlowin](https://github.com/jlowin) in [#748](https://github.com/PrefectHQ/fastmcp/pull/748)\n* Allow tag assignments for OpenAPI by [@jlowin](https://github.com/jlowin) in [#791](https://github.com/PrefectHQ/fastmcp/pull/791)\n### Enhancements 🔧\n* Create common base class for components by [@jlowin](https://github.com/jlowin) in [#776](https://github.com/PrefectHQ/fastmcp/pull/776)\n* Move components to own file; add resource by [@jlowin](https://github.com/jlowin) in [#777](https://github.com/PrefectHQ/fastmcp/pull/777)\n* Update FastMCP component with __eq__ and __repr__ by [@jlowin](https://github.com/jlowin) in [#779](https://github.com/PrefectHQ/fastmcp/pull/779)\n* Remove open-ended and server-specific settings by [@jlowin](https://github.com/jlowin) in [#750](https://github.com/PrefectHQ/fastmcp/pull/750)\n### Fixes 🐞\n* Ensure client is only initialized once by [@jlowin](https://github.com/jlowin) in [#758](https://github.com/PrefectHQ/fastmcp/pull/758)\n* Fix field validator for resource by [@jlowin](https://github.com/jlowin) in [#778](https://github.com/PrefectHQ/fastmcp/pull/778)\n* Ensure proxies can overwrite remote tools without falling back to the remote by [@jlowin](https://github.com/jlowin) in [#782](https://github.com/PrefectHQ/fastmcp/pull/782)\n### Breaking Changes 🛫\n* Treat all openapi routes as tools by [@jlowin](https://github.com/jlowin) in [#788](https://github.com/PrefectHQ/fastmcp/pull/788)\n* Fix issue with global OpenAPI tags by [@jlowin](https://github.com/jlowin) in [#792](https://github.com/PrefectHQ/fastmcp/pull/792)\n### Docs 📚\n* Minor docs updates by [@jlowin](https://github.com/jlowin) in [#755](https://github.com/PrefectHQ/fastmcp/pull/755)\n* Add 2.7 update by [@jlowin](https://github.com/jlowin) in [#756](https://github.com/PrefectHQ/fastmcp/pull/756)\n* Reduce 2.7 image size by [@jlowin](https://github.com/jlowin) in [#757](https://github.com/PrefectHQ/fastmcp/pull/757)\n* Update updates.mdx by [@jlowin](https://github.com/jlowin) in [#765](https://github.com/PrefectHQ/fastmcp/pull/765)\n* Hide docs sidebar scrollbar by default by [@jlowin](https://github.com/jlowin) in [#766](https://github.com/PrefectHQ/fastmcp/pull/766)\n* Add \"stop vibe testing\" to tutorials by [@jlowin](https://github.com/jlowin) in [#767](https://github.com/PrefectHQ/fastmcp/pull/767)\n* Add docs links by [@jlowin](https://github.com/jlowin) in [#768](https://github.com/PrefectHQ/fastmcp/pull/768)\n* Fix: updated variable name under Gemini remote client by [@yrangana](https://github.com/yrangana) in [#769](https://github.com/PrefectHQ/fastmcp/pull/769)\n* Revert \"Hide docs sidebar scrollbar by default\" by [@jlowin](https://github.com/jlowin) in [#770](https://github.com/PrefectHQ/fastmcp/pull/770)\n* Add updates by [@jlowin](https://github.com/jlowin) in [#773](https://github.com/PrefectHQ/fastmcp/pull/773)\n* Add tutorials by [@jlowin](https://github.com/jlowin) in [#783](https://github.com/PrefectHQ/fastmcp/pull/783)\n* Update LLM-friendly docs by [@jlowin](https://github.com/jlowin) in [#784](https://github.com/PrefectHQ/fastmcp/pull/784)\n* Update oauth.mdx by [@JeremyCraigMartinez](https://github.com/JeremyCraigMartinez) in [#787](https://github.com/PrefectHQ/fastmcp/pull/787)\n* Add changelog by [@jlowin](https://github.com/jlowin) in [#789](https://github.com/PrefectHQ/fastmcp/pull/789)\n* Add tutorials by [@jlowin](https://github.com/jlowin) in [#790](https://github.com/PrefectHQ/fastmcp/pull/790)\n* Add docs for tag-based filtering by [@jlowin](https://github.com/jlowin) in [#793](https://github.com/PrefectHQ/fastmcp/pull/793)\n### Other Changes 🦾\n* Create dependabot.yml by [@jlowin](https://github.com/jlowin) in [#759](https://github.com/PrefectHQ/fastmcp/pull/759)\n* Bump astral-sh/setup-uv from 3 to 6 by [@dependabot](https://github.com/dependabot) in [#760](https://github.com/PrefectHQ/fastmcp/pull/760)\n* Add dependencies section to release by [@jlowin](https://github.com/jlowin) in [#761](https://github.com/PrefectHQ/fastmcp/pull/761)\n* Remove extra imports for MCPConfig by [@Maanas-Verma](https://github.com/Maanas-Verma) in [#763](https://github.com/PrefectHQ/fastmcp/pull/763)\n* Split out enhancements in release notes by [@jlowin](https://github.com/jlowin) in [#764](https://github.com/PrefectHQ/fastmcp/pull/764)\n\n## New Contributors\n* [@dependabot](https://github.com/dependabot) made their first contribution in [#760](https://github.com/PrefectHQ/fastmcp/pull/760)\n* [@Maanas-Verma](https://github.com/Maanas-Verma) made their first contribution in [#763](https://github.com/PrefectHQ/fastmcp/pull/763)\n* [@JeremyCraigMartinez](https://github.com/JeremyCraigMartinez) made their first contribution in [#787](https://github.com/PrefectHQ/fastmcp/pull/787)\n\n**Full Changelog**: [v2.7.1...v2.8.0](https://github.com/PrefectHQ/fastmcp/compare/v2.7.1...v2.8.0)\n\n</Update>\n\n<Update label=\"v2.7.1\" description=\"2024-06-08\">\n\n## [v2.7.1: The Bearer Necessities](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.1)\n\nThis release primarily contains a fix for parsing string tokens that are provided to FastMCP clients.\n\n### New Features 🎉\n\n* Respect cache setting, set default to 1 second by [@jlowin](https://github.com/jlowin) in [#747](https://github.com/PrefectHQ/fastmcp/pull/747)\n\n### Fixes 🐞\n\n* Ensure event store is properly typed by [@jlowin](https://github.com/jlowin) in [#753](https://github.com/PrefectHQ/fastmcp/pull/753)\n* Fix passing token string to client auth & add auth to MCPConfig clients by [@jlowin](https://github.com/jlowin) in [#754](https://github.com/PrefectHQ/fastmcp/pull/754)\n\n### Docs 📚\n\n* Docs : fix client to mcp\\_client in Gemini example by [@yrangana](https://github.com/yrangana) in [#734](https://github.com/PrefectHQ/fastmcp/pull/734)\n* update add tool docstring by [@strawgate](https://github.com/strawgate) in [#739](https://github.com/PrefectHQ/fastmcp/pull/739)\n* Fix contrib link by [@richardkmichael](https://github.com/richardkmichael) in [#749](https://github.com/PrefectHQ/fastmcp/pull/749)\n\n### Other Changes 🦾\n\n* Switch Pydantic defaults to kwargs by [@strawgate](https://github.com/strawgate) in [#731](https://github.com/PrefectHQ/fastmcp/pull/731)\n* Fix Typo in CLI module by [@wfclark5](https://github.com/wfclark5) in [#737](https://github.com/PrefectHQ/fastmcp/pull/737)\n* chore: fix prompt docstring by [@danb27](https://github.com/danb27) in [#752](https://github.com/PrefectHQ/fastmcp/pull/752)\n* Add accept to excluded headers by [@jlowin](https://github.com/jlowin) in [#751](https://github.com/PrefectHQ/fastmcp/pull/751)\n\n### New Contributors\n\n* [@wfclark5](https://github.com/wfclark5) made their first contribution in [#737](https://github.com/PrefectHQ/fastmcp/pull/737)\n* [@richardkmichael](https://github.com/richardkmichael) made their first contribution in [#749](https://github.com/PrefectHQ/fastmcp/pull/749)\n* [@danb27](https://github.com/danb27) made their first contribution in [#752](https://github.com/PrefectHQ/fastmcp/pull/752)\n\n**Full Changelog**: [v2.7.0...v2.7.1](https://github.com/PrefectHQ/fastmcp/compare/v2.7.0...v2.7.1)\n</Update>\n\n<Update label=\"v2.7.0\" description=\"2024-06-05\">\n\n## [v2.7.0: Pare Programming](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.0)\n\nThis is primarily a housekeeping release to remove or deprecate cruft that's accumulated since v1. Primarily, this release refactors FastMCP's internals in preparation for features planned in the next few major releases. However please note that as a result, this release has some minor breaking changes (which is why it's 2.7, not 2.6.2, in accordance with repo guidelines) though not to the core user-facing APIs.\n\n### Breaking Changes 🛫\n\n* decorators return the objects they create, not the decorated function\n* websockets is an optional dependency\n* methods on the server for automatically converting functions into tools/resources/prompts have been deprecated in favor of using the decorators directly\n\n### New Features 🎉\n\n* allow passing flags to servers by [@zzstoatzz](https://github.com/zzstoatzz) in [#690](https://github.com/PrefectHQ/fastmcp/pull/690)\n* replace $ref pointing to `#/components/schemas/` with `#/$defs/` by [@phateffect](https://github.com/phateffect) in [#697](https://github.com/PrefectHQ/fastmcp/pull/697)\n* Split Tool into Tool and FunctionTool by [@jlowin](https://github.com/jlowin) in [#700](https://github.com/PrefectHQ/fastmcp/pull/700)\n* Use strict basemodel for Prompt; relax from\\_function deprecation by [@jlowin](https://github.com/jlowin) in [#701](https://github.com/PrefectHQ/fastmcp/pull/701)\n* Formalize resource/functionresource replationship by [@jlowin](https://github.com/jlowin) in [#702](https://github.com/PrefectHQ/fastmcp/pull/702)\n* Formalize template/functiontemplate split by [@jlowin](https://github.com/jlowin) in [#703](https://github.com/PrefectHQ/fastmcp/pull/703)\n* Support flexible @tool decorator call patterns by [@jlowin](https://github.com/jlowin) in [#706](https://github.com/PrefectHQ/fastmcp/pull/706)\n* Ensure deprecation warnings have stacklevel=2 by [@jlowin](https://github.com/jlowin) in [#710](https://github.com/PrefectHQ/fastmcp/pull/710)\n* Allow naked prompt decorator by [@jlowin](https://github.com/jlowin) in [#711](https://github.com/PrefectHQ/fastmcp/pull/711)\n\n### Fixes 🐞\n\n* Updates / Fixes for Tool Content Conversion by [@strawgate](https://github.com/strawgate) in [#642](https://github.com/PrefectHQ/fastmcp/pull/642)\n* Fix pr labeler permissions by [@jlowin](https://github.com/jlowin) in [#708](https://github.com/PrefectHQ/fastmcp/pull/708)\n* remove -n auto by [@jlowin](https://github.com/jlowin) in [#709](https://github.com/PrefectHQ/fastmcp/pull/709)\n* Fix links in README.md by [@alainivars](https://github.com/alainivars) in [#723](https://github.com/PrefectHQ/fastmcp/pull/723)\n\nHappily, this release DOES permit the use of \"naked\" decorators to align with Pythonic practice:\n\n```python\n@mcp.tool\ndef my_tool():\n    ...\n```\n\n**Full Changelog**: [v2.6.2...v2.7.0](https://github.com/PrefectHQ/fastmcp/compare/v2.6.2...v2.7.0)\n</Update>\n\n<Update label=\"v2.6.1\" description=\"2024-06-03\">\n\n## [v2.6.1: Blast Auth (second ignition)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.6.1)\n\nThis is a patch release to restore py.typed in #686.\n\n### Docs 📚\n\n* Update readme by [@jlowin](https://github.com/jlowin) in [#679](https://github.com/PrefectHQ/fastmcp/pull/679)\n* Add gemini tutorial by [@jlowin](https://github.com/jlowin) in [#680](https://github.com/PrefectHQ/fastmcp/pull/680)\n* Fix : fix path error to CLI Documentation by [@yrangana](https://github.com/yrangana) in [#684](https://github.com/PrefectHQ/fastmcp/pull/684)\n* Update auth docs by [@jlowin](https://github.com/jlowin) in [#687](https://github.com/PrefectHQ/fastmcp/pull/687)\n\n### Other Changes 🦾\n\n* Remove deprecation notice by [@jlowin](https://github.com/jlowin) in [#677](https://github.com/PrefectHQ/fastmcp/pull/677)\n* Delete server.py by [@jlowin](https://github.com/jlowin) in [#681](https://github.com/PrefectHQ/fastmcp/pull/681)\n* Restore py.typed by [@jlowin](https://github.com/jlowin) in [#686](https://github.com/PrefectHQ/fastmcp/pull/686)\n\n### New Contributors\n\n* [@yrangana](https://github.com/yrangana) made their first contribution in [#684](https://github.com/PrefectHQ/fastmcp/pull/684)\n\n**Full Changelog**: [v2.6.0...v2.6.1](https://github.com/PrefectHQ/fastmcp/compare/v2.6.0...v2.6.1)\n</Update>\n\n<Update label=\"v2.6.0\" description=\"2024-06-02\">\n\n## [v2.6.0: Blast Auth](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.6.0)\n\n### New Features 🎉\n\n* Introduce MCP client oauth flow by [@jlowin](https://github.com/jlowin) in [#478](https://github.com/PrefectHQ/fastmcp/pull/478)\n* Support providing tools at init by [@jlowin](https://github.com/jlowin) in [#647](https://github.com/PrefectHQ/fastmcp/pull/647)\n* Simplify code for running servers in processes during tests by [@jlowin](https://github.com/jlowin) in [#649](https://github.com/PrefectHQ/fastmcp/pull/649)\n* Add basic bearer auth for server and client by [@jlowin](https://github.com/jlowin) in [#650](https://github.com/PrefectHQ/fastmcp/pull/650)\n* Support configuring bearer auth from env vars by [@jlowin](https://github.com/jlowin) in [#652](https://github.com/PrefectHQ/fastmcp/pull/652)\n* feat(tool): add support for excluding arguments from tool definition by [@deepak-stratforge](https://github.com/deepak-stratforge) in [#626](https://github.com/PrefectHQ/fastmcp/pull/626)\n* Add docs for server + client auth by [@jlowin](https://github.com/jlowin) in [#655](https://github.com/PrefectHQ/fastmcp/pull/655)\n\n### Fixes 🐞\n\n* fix: Support concurrency in FastMcpProxy (and Client) by [@Sillocan](https://github.com/Sillocan) in [#635](https://github.com/PrefectHQ/fastmcp/pull/635)\n* Ensure Client.close() cleans up client context appropriately by [@jlowin](https://github.com/jlowin) in [#643](https://github.com/PrefectHQ/fastmcp/pull/643)\n* Update client.mdx: ClientError namespace by [@mjkaye](https://github.com/mjkaye) in [#657](https://github.com/PrefectHQ/fastmcp/pull/657)\n\n### Docs 📚\n\n* Make FastMCPTransport support simulated Streamable HTTP Transport (didn't work) by [@jlowin](https://github.com/jlowin) in [#645](https://github.com/PrefectHQ/fastmcp/pull/645)\n* Document exclude\\_args by [@jlowin](https://github.com/jlowin) in [#653](https://github.com/PrefectHQ/fastmcp/pull/653)\n* Update welcome by [@jlowin](https://github.com/jlowin) in [#673](https://github.com/PrefectHQ/fastmcp/pull/673)\n* Add Anthropic + Claude desktop integration guides by [@jlowin](https://github.com/jlowin) in [#674](https://github.com/PrefectHQ/fastmcp/pull/674)\n* Minor docs design updates by [@jlowin](https://github.com/jlowin) in [#676](https://github.com/PrefectHQ/fastmcp/pull/676)\n\n### Other Changes 🦾\n\n* Update test typing by [@jlowin](https://github.com/jlowin) in [#646](https://github.com/PrefectHQ/fastmcp/pull/646)\n* Add OpenAI integration docs by [@jlowin](https://github.com/jlowin) in [#660](https://github.com/PrefectHQ/fastmcp/pull/660)\n\n### New Contributors\n\n* [@Sillocan](https://github.com/Sillocan) made their first contribution in [#635](https://github.com/PrefectHQ/fastmcp/pull/635)\n* [@deepak-stratforge](https://github.com/deepak-stratforge) made their first contribution in [#626](https://github.com/PrefectHQ/fastmcp/pull/626)\n* [@mjkaye](https://github.com/mjkaye) made their first contribution in [#657](https://github.com/PrefectHQ/fastmcp/pull/657)\n\n**Full Changelog**: [v2.5.2...v2.6.0](https://github.com/PrefectHQ/fastmcp/compare/v2.5.2...v2.6.0)\n</Update>\n\n<Update label=\"v2.5.2\" description=\"2024-05-29\">\n\n## [v2.5.2: Stayin' Alive](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.2)\n\n### New Features 🎉\n\n* Add graceful error handling for unreachable mounted servers by [@davenpi](https://github.com/davenpi) in [#605](https://github.com/PrefectHQ/fastmcp/pull/605)\n* Improve type inference from client transport by [@jlowin](https://github.com/jlowin) in [#623](https://github.com/PrefectHQ/fastmcp/pull/623)\n* Add keep\\_alive param to reuse subprocess by [@jlowin](https://github.com/jlowin) in [#624](https://github.com/PrefectHQ/fastmcp/pull/624)\n\n### Fixes 🐞\n\n* Fix handling tools without descriptions by [@jlowin](https://github.com/jlowin) in [#610](https://github.com/PrefectHQ/fastmcp/pull/610)\n* Don't print env vars to console when format is wrong by [@jlowin](https://github.com/jlowin) in [#615](https://github.com/PrefectHQ/fastmcp/pull/615)\n* Ensure behavior-affecting headers are excluded when forwarding proxies/openapi by [@jlowin](https://github.com/jlowin) in [#620](https://github.com/PrefectHQ/fastmcp/pull/620)\n\n### Docs 📚\n\n* Add notes about uv and claude desktop by [@jlowin](https://github.com/jlowin) in [#597](https://github.com/PrefectHQ/fastmcp/pull/597)\n\n### Other Changes 🦾\n\n* add init\\_timeout for mcp client by [@jfouret](https://github.com/jfouret) in [#607](https://github.com/PrefectHQ/fastmcp/pull/607)\n* Add init\\_timeout for mcp client (incl settings) by [@jlowin](https://github.com/jlowin) in [#609](https://github.com/PrefectHQ/fastmcp/pull/609)\n* Support for uppercase letters at the log level by [@ksawaray](https://github.com/ksawaray) in [#625](https://github.com/PrefectHQ/fastmcp/pull/625)\n\n### New Contributors\n\n* [@jfouret](https://github.com/jfouret) made their first contribution in [#607](https://github.com/PrefectHQ/fastmcp/pull/607)\n* [@ksawaray](https://github.com/ksawaray) made their first contribution in [#625](https://github.com/PrefectHQ/fastmcp/pull/625)\n\n**Full Changelog**: [v2.5.1...v2.5.2](https://github.com/PrefectHQ/fastmcp/compare/v2.5.1...v2.5.2)\n</Update>\n\n<Update label=\"v2.5.1\" description=\"2024-05-24\">\n\n## [v2.5.1: Route Awakening (Part 2)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.1)\n\n### Fixes 🐞\n\n* Ensure content-length is always stripped from client headers by [@jlowin](https://github.com/jlowin) in [#589](https://github.com/PrefectHQ/fastmcp/pull/589)\n\n### Docs 📚\n\n* Fix redundant section of docs by [@jlowin](https://github.com/jlowin) in [#583](https://github.com/PrefectHQ/fastmcp/pull/583)\n\n**Full Changelog**: [v2.5.0...v2.5.1](https://github.com/PrefectHQ/fastmcp/compare/v2.5.0...v2.5.1)\n</Update>\n\n<Update label=\"v2.5.0\" description=\"2024-05-24\">\n\n## [v2.5.0: Route Awakening](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.0)\n\nThis release introduces completely new tools for generating and customizing MCP servers from OpenAPI specs and FastAPI apps, including popular requests like mechanisms for determining what routes map to what MCP components; renaming routes; and customizing the generated MCP components.\n\n### New Features 🎉\n\n* Add FastMCP 1.0 server support for in-memory Client / Testing by [@jlowin](https://github.com/jlowin) in [#539](https://github.com/PrefectHQ/fastmcp/pull/539)\n* Minor addition: add transport to stdio server in mcpconfig, with default by [@jlowin](https://github.com/jlowin) in [#555](https://github.com/PrefectHQ/fastmcp/pull/555)\n* Raise an error if a Client is created with no servers in config by [@jlowin](https://github.com/jlowin) in [#554](https://github.com/PrefectHQ/fastmcp/pull/554)\n* Expose model preferences in `Context.sample` for flexible model selection. by [@davenpi](https://github.com/davenpi) in [#542](https://github.com/PrefectHQ/fastmcp/pull/542)\n* Ensure custom routes are respected by [@jlowin](https://github.com/jlowin) in [#558](https://github.com/PrefectHQ/fastmcp/pull/558)\n* Add client method to send cancellation notifications by [@davenpi](https://github.com/davenpi) in [#563](https://github.com/PrefectHQ/fastmcp/pull/563)\n* Enhance route map logic for include/exclude OpenAPI routes by [@jlowin](https://github.com/jlowin) in [#564](https://github.com/PrefectHQ/fastmcp/pull/564)\n* Add tag-based route maps by [@jlowin](https://github.com/jlowin) in [#565](https://github.com/PrefectHQ/fastmcp/pull/565)\n* Add advanced control of openAPI route creation by [@jlowin](https://github.com/jlowin) in [#566](https://github.com/PrefectHQ/fastmcp/pull/566)\n* Make error masking configurable by [@jlowin](https://github.com/jlowin) in [#550](https://github.com/PrefectHQ/fastmcp/pull/550)\n* Ensure client headers are passed through to remote servers by [@jlowin](https://github.com/jlowin) in [#575](https://github.com/PrefectHQ/fastmcp/pull/575)\n* Use lowercase name for headers when comparing by [@jlowin](https://github.com/jlowin) in [#576](https://github.com/PrefectHQ/fastmcp/pull/576)\n* Permit more flexible name generation for OpenAPI servers by [@jlowin](https://github.com/jlowin) in [#578](https://github.com/PrefectHQ/fastmcp/pull/578)\n* Ensure that tools/templates/prompts are compatible with callable objects by [@jlowin](https://github.com/jlowin) in [#579](https://github.com/PrefectHQ/fastmcp/pull/579)\n\n### Docs 📚\n\n* Add version badge for prefix formats by [@jlowin](https://github.com/jlowin) in [#537](https://github.com/PrefectHQ/fastmcp/pull/537)\n* Add versioning note to docs by [@jlowin](https://github.com/jlowin) in [#551](https://github.com/PrefectHQ/fastmcp/pull/551)\n* Bump 2.3.6 references to 2.4.0 by [@jlowin](https://github.com/jlowin) in [#567](https://github.com/PrefectHQ/fastmcp/pull/567)\n\n**Full Changelog**: [v2.4.0...v2.5.0](https://github.com/PrefectHQ/fastmcp/compare/v2.4.0...v2.5.0)\n</Update>\n\n<Update label=\"v2.4.0\" description=\"2024-05-21\">\n\n## [v2.4.0: Config and Conquer](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.4.0)\n\n**Note**: this release includes a backwards-incompatible change to how resources are prefixed when mounted in composed servers. However, it is only backwards-incompatible if users were running tests or manually loading resources by prefixed key; LLMs should not have any issue discovering the new route.\n\n### New Features 🎉\n\n* Allow \\* Methods and all routes as tools shortcuts by [@jlowin](https://github.com/jlowin) in [#520](https://github.com/PrefectHQ/fastmcp/pull/520)\n* Improved support for config dicts by [@jlowin](https://github.com/jlowin) in [#522](https://github.com/PrefectHQ/fastmcp/pull/522)\n* Support creating clients from MCP config dicts, including multi-server clients by [@jlowin](https://github.com/jlowin) in [#527](https://github.com/PrefectHQ/fastmcp/pull/527)\n* Make resource prefix format configurable by [@jlowin](https://github.com/jlowin) in [#534](https://github.com/PrefectHQ/fastmcp/pull/534)\n\n### Fixes 🐞\n\n* Avoid hanging on initializing server session by [@jlowin](https://github.com/jlowin) in [#523](https://github.com/PrefectHQ/fastmcp/pull/523)\n\n### Breaking Changes 🛫\n\n* Remove customizable separators; improve resource separator by [@jlowin](https://github.com/jlowin) in [#526](https://github.com/PrefectHQ/fastmcp/pull/526)\n\n### Docs 📚\n\n* Improve client documentation by [@jlowin](https://github.com/jlowin) in [#517](https://github.com/PrefectHQ/fastmcp/pull/517)\n\n### Other Changes 🦾\n\n* Ensure openapi path params are handled properly by [@jlowin](https://github.com/jlowin) in [#519](https://github.com/PrefectHQ/fastmcp/pull/519)\n* better error when missing lifespan by [@zzstoatzz](https://github.com/zzstoatzz) in [#521](https://github.com/PrefectHQ/fastmcp/pull/521)\n\n**Full Changelog**: [v2.3.5...v2.4.0](https://github.com/PrefectHQ/fastmcp/compare/v2.3.5...v2.4.0)\n</Update>\n\n<Update label=\"v2.3.5\" description=\"2024-05-20\">\n\n## [v2.3.5: Making Progress](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.5)\n\n### New Features 🎉\n\n* support messages in progress notifications by [@rickygenhealth](https://github.com/rickygenhealth) in [#471](https://github.com/PrefectHQ/fastmcp/pull/471)\n* feat: Add middleware option in server.run by [@Maxi91f](https://github.com/Maxi91f) in [#475](https://github.com/PrefectHQ/fastmcp/pull/475)\n* Add lifespan property to app by [@jlowin](https://github.com/jlowin) in [#483](https://github.com/PrefectHQ/fastmcp/pull/483)\n* Update `fastmcp run` to work with remote servers by [@jlowin](https://github.com/jlowin) in [#491](https://github.com/PrefectHQ/fastmcp/pull/491)\n* Add FastMCP.as\\_proxy() by [@jlowin](https://github.com/jlowin) in [#490](https://github.com/PrefectHQ/fastmcp/pull/490)\n* Infer sse transport from urls containing /sse by [@jlowin](https://github.com/jlowin) in [#512](https://github.com/PrefectHQ/fastmcp/pull/512)\n* Add progress handler to client by [@jlowin](https://github.com/jlowin) in [#513](https://github.com/PrefectHQ/fastmcp/pull/513)\n* Store the initialize result on the client by [@jlowin](https://github.com/jlowin) in [#509](https://github.com/PrefectHQ/fastmcp/pull/509)\n\n### Fixes 🐞\n\n* Remove patch and use upstream SSEServerTransport by [@jlowin](https://github.com/jlowin) in [#425](https://github.com/PrefectHQ/fastmcp/pull/425)\n\n### Docs 📚\n\n* Update transport docs by [@jlowin](https://github.com/jlowin) in [#458](https://github.com/PrefectHQ/fastmcp/pull/458)\n* update proxy docs + example by [@zzstoatzz](https://github.com/zzstoatzz) in [#460](https://github.com/PrefectHQ/fastmcp/pull/460)\n* doc(asgi): Change custom route example to PlainTextResponse by [@mcw0933](https://github.com/mcw0933) in [#477](https://github.com/PrefectHQ/fastmcp/pull/477)\n* Store FastMCP instance on app.state.fastmcp\\_server by [@jlowin](https://github.com/jlowin) in [#489](https://github.com/PrefectHQ/fastmcp/pull/489)\n* Improve AGENTS.md overview by [@jlowin](https://github.com/jlowin) in [#492](https://github.com/PrefectHQ/fastmcp/pull/492)\n* Update release numbers for anticipated version by [@jlowin](https://github.com/jlowin) in [#516](https://github.com/PrefectHQ/fastmcp/pull/516)\n\n### Other Changes 🦾\n\n* run tests on all PRs by [@jlowin](https://github.com/jlowin) in [#468](https://github.com/PrefectHQ/fastmcp/pull/468)\n* add null check by [@zzstoatzz](https://github.com/zzstoatzz) in [#473](https://github.com/PrefectHQ/fastmcp/pull/473)\n* strict typing for `server.py` by [@zzstoatzz](https://github.com/zzstoatzz) in [#476](https://github.com/PrefectHQ/fastmcp/pull/476)\n* Doc(quickstart): Fix import statements by [@mai-nakagawa](https://github.com/mai-nakagawa) in [#479](https://github.com/PrefectHQ/fastmcp/pull/479)\n* Add labeler by [@jlowin](https://github.com/jlowin) in [#484](https://github.com/PrefectHQ/fastmcp/pull/484)\n* Fix flaky timeout test by increasing timeout (#474) by [@davenpi](https://github.com/davenpi) in [#486](https://github.com/PrefectHQ/fastmcp/pull/486)\n* Skipping `test_permission_error` if runner is root. by [@ZiadAmerr](https://github.com/ZiadAmerr) in [#502](https://github.com/PrefectHQ/fastmcp/pull/502)\n* allow passing full uvicorn config by [@zzstoatzz](https://github.com/zzstoatzz) in [#504](https://github.com/PrefectHQ/fastmcp/pull/504)\n* Skip timeout tests on windows by [@jlowin](https://github.com/jlowin) in [#514](https://github.com/PrefectHQ/fastmcp/pull/514)\n\n### New Contributors\n\n* [@rickygenhealth](https://github.com/rickygenhealth) made their first contribution in [#471](https://github.com/PrefectHQ/fastmcp/pull/471)\n* [@Maxi91f](https://github.com/Maxi91f) made their first contribution in [#475](https://github.com/PrefectHQ/fastmcp/pull/475)\n* [@mcw0933](https://github.com/mcw0933) made their first contribution in [#477](https://github.com/PrefectHQ/fastmcp/pull/477)\n* [@mai-nakagawa](https://github.com/mai-nakagawa) made their first contribution in [#479](https://github.com/PrefectHQ/fastmcp/pull/479)\n* [@ZiadAmerr](https://github.com/ZiadAmerr) made their first contribution in [#502](https://github.com/PrefectHQ/fastmcp/pull/502)\n\n**Full Changelog**: [v2.3.4...v2.3.5](https://github.com/PrefectHQ/fastmcp/compare/v2.3.4...v2.3.5)\n</Update>\n\n<Update label=\"v2.3.4\" description=\"2024-05-15\">\n\n## [v2.3.4: Error Today, Gone Tomorrow](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.4)\n\n### New Features 🎉\n\n* logging stack trace for easier debugging by [@jbkoh](https://github.com/jbkoh) in [#413](https://github.com/PrefectHQ/fastmcp/pull/413)\n* add missing StreamableHttpTransport in client exports by [@yihuang](https://github.com/yihuang) in [#408](https://github.com/PrefectHQ/fastmcp/pull/408)\n* Improve error handling for tools and resources by [@jlowin](https://github.com/jlowin) in [#434](https://github.com/PrefectHQ/fastmcp/pull/434)\n* feat: add support for removing tools from server by [@davenpi](https://github.com/davenpi) in [#437](https://github.com/PrefectHQ/fastmcp/pull/437)\n* Prune titles from JSONSchemas by [@jlowin](https://github.com/jlowin) in [#449](https://github.com/PrefectHQ/fastmcp/pull/449)\n* Declare toolsChanged capability for stdio server. by [@davenpi](https://github.com/davenpi) in [#450](https://github.com/PrefectHQ/fastmcp/pull/450)\n* Improve handling of exceptiongroups when raised in clients by [@jlowin](https://github.com/jlowin) in [#452](https://github.com/PrefectHQ/fastmcp/pull/452)\n* Add timeout support to client by [@jlowin](https://github.com/jlowin) in [#455](https://github.com/PrefectHQ/fastmcp/pull/455)\n\n### Fixes 🐞\n\n* Pin to mcp 1.8.1 to resolve callback deadlocks with SHTTP by [@jlowin](https://github.com/jlowin) in [#427](https://github.com/PrefectHQ/fastmcp/pull/427)\n* Add reprs for OpenAPI objects by [@jlowin](https://github.com/jlowin) in [#447](https://github.com/PrefectHQ/fastmcp/pull/447)\n* Ensure openapi defs for structured objects are loaded properly by [@jlowin](https://github.com/jlowin) in [#448](https://github.com/PrefectHQ/fastmcp/pull/448)\n* Ensure tests run against correct python version by [@jlowin](https://github.com/jlowin) in [#454](https://github.com/PrefectHQ/fastmcp/pull/454)\n* Ensure result is only returned if a new key was found by [@jlowin](https://github.com/jlowin) in [#456](https://github.com/PrefectHQ/fastmcp/pull/456)\n\n### Docs 📚\n\n* Add documentation for tool removal by [@jlowin](https://github.com/jlowin) in [#440](https://github.com/PrefectHQ/fastmcp/pull/440)\n\n### Other Changes 🦾\n\n* Deprecate passing settings to the FastMCP instance by [@jlowin](https://github.com/jlowin) in [#424](https://github.com/PrefectHQ/fastmcp/pull/424)\n* Add path prefix to test by [@jlowin](https://github.com/jlowin) in [#432](https://github.com/PrefectHQ/fastmcp/pull/432)\n\n### New Contributors\n\n* [@jbkoh](https://github.com/jbkoh) made their first contribution in [#413](https://github.com/PrefectHQ/fastmcp/pull/413)\n* [@davenpi](https://github.com/davenpi) made their first contribution in [#437](https://github.com/PrefectHQ/fastmcp/pull/437)\n\n**Full Changelog**: [v2.3.3...v2.3.4](https://github.com/PrefectHQ/fastmcp/compare/v2.3.3...v2.3.4)\n</Update>\n\n<Update label=\"v2.3.3\" description=\"2024-05-10\">\n\n## [v2.3.3: SSE you later](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.3)\n\nThis is a hotfix for a bug introduced in 2.3.2 that broke SSE servers\n\n### Fixes 🐞\n\n* Fix bug that sets message path and sse path to same value by [@jlowin](https://github.com/jlowin) in [#405](https://github.com/PrefectHQ/fastmcp/pull/405)\n\n### Docs 📚\n\n* Update composition docs by [@jlowin](https://github.com/jlowin) in [#403](https://github.com/PrefectHQ/fastmcp/pull/403)\n\n### Other Changes 🦾\n\n* Add test for no prefix when importing by [@jlowin](https://github.com/jlowin) in [#404](https://github.com/PrefectHQ/fastmcp/pull/404)\n\n**Full Changelog**: [v2.3.2...v2.3.3](https://github.com/PrefectHQ/fastmcp/compare/v2.3.2...v2.3.3)\n</Update>\n\n<Update label=\"v2.3.2\" description=\"2024-05-10\">\n\n## [v2.3.2: Stuck in the Middleware With You](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.2)\n\n### New Features 🎉\n\n* Allow users to pass middleware to starlette app constructors by [@jlowin](https://github.com/jlowin) in [#398](https://github.com/PrefectHQ/fastmcp/pull/398)\n* Deprecate transport-specific methods on FastMCP server by [@jlowin](https://github.com/jlowin) in [#401](https://github.com/PrefectHQ/fastmcp/pull/401)\n\n### Docs 📚\n\n* Update CLI docs by [@jlowin](https://github.com/jlowin) in [#402](https://github.com/PrefectHQ/fastmcp/pull/402)\n\n### Other Changes 🦾\n\n* Adding 23 tests for CLI by [@didier-durand](https://github.com/didier-durand) in [#394](https://github.com/PrefectHQ/fastmcp/pull/394)\n\n**Full Changelog**: [v2.3.1...v2.3.2](https://github.com/PrefectHQ/fastmcp/compare/v2.3.1...v2.3.2)\n</Update>\n\n<Update label=\"v2.3.1\" description=\"2024-05-09\">\n\n## [v2.3.1: For Good-nests Sake](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.1)\n\nThis release primarily patches a long-standing bug with nested ASGI SSE servers.\n\n### Fixes 🐞\n\n* Fix tool result serialization when the tool returns a list by [@strawgate](https://github.com/strawgate) in [#379](https://github.com/PrefectHQ/fastmcp/pull/379)\n* Ensure FastMCP handles nested SSE and SHTTP apps properly in ASGI frameworks by [@jlowin](https://github.com/jlowin) in [#390](https://github.com/PrefectHQ/fastmcp/pull/390)\n\n### Docs 📚\n\n* Update transport docs by [@jlowin](https://github.com/jlowin) in [#377](https://github.com/PrefectHQ/fastmcp/pull/377)\n* Add llms.txt to docs by [@jlowin](https://github.com/jlowin) in [#384](https://github.com/PrefectHQ/fastmcp/pull/384)\n* Fixing various text typos by [@didier-durand](https://github.com/didier-durand) in [#385](https://github.com/PrefectHQ/fastmcp/pull/385)\n\n### Other Changes 🦾\n\n* Adding a few tests to Image type by [@didier-durand](https://github.com/didier-durand) in [#387](https://github.com/PrefectHQ/fastmcp/pull/387)\n* Adding tests for TimedCache by [@didier-durand](https://github.com/didier-durand) in [#388](https://github.com/PrefectHQ/fastmcp/pull/388)\n\n### New Contributors\n\n* [@didier-durand](https://github.com/didier-durand) made their first contribution in [#385](https://github.com/PrefectHQ/fastmcp/pull/385)\n\n**Full Changelog**: [v2.3.0...v2.3.1](https://github.com/PrefectHQ/fastmcp/compare/v2.3.0...v2.3.1)\n</Update>\n\n<Update label=\"v2.3.0\" description=\"2024-05-08\">\n\n## [v2.3.0: Stream Me Up, Scotty](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.0)\n\n### New Features 🎉\n\n* Add streaming support for HTTP transport by [@jlowin](https://github.com/jlowin) in [#365](https://github.com/PrefectHQ/fastmcp/pull/365)\n* Support streaming HTTP transport in clients by [@jlowin](https://github.com/jlowin) in [#366](https://github.com/PrefectHQ/fastmcp/pull/366)\n* Add streaming support to CLI by [@jlowin](https://github.com/jlowin) in [#367](https://github.com/PrefectHQ/fastmcp/pull/367)\n\n### Fixes 🐞\n\n* Fix streaming transport initialization by [@jlowin](https://github.com/jlowin) in [#368](https://github.com/PrefectHQ/fastmcp/pull/368)\n\n### Docs 📚\n\n* Update transport documentation for streaming support by [@jlowin](https://github.com/jlowin) in [#369](https://github.com/PrefectHQ/fastmcp/pull/369)\n\n**Full Changelog**: [v2.2.10...v2.3.0](https://github.com/PrefectHQ/fastmcp/compare/v2.2.10...v2.3.0)\n</Update>\n\n<Update label=\"v2.2.10\" description=\"2024-05-06\">\n\n## [v2.2.10: That's JSON Bourne](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.10)\n\n### Fixes 🐞\n\n* Disable automatic JSON parsing of tool args by [@jlowin](https://github.com/jlowin) in [#341](https://github.com/PrefectHQ/fastmcp/pull/341)\n* Fix prompt test by [@jlowin](https://github.com/jlowin) in [#342](https://github.com/PrefectHQ/fastmcp/pull/342)\n\n### Other Changes 🦾\n\n* Update docs.json by [@jlowin](https://github.com/jlowin) in [#338](https://github.com/PrefectHQ/fastmcp/pull/338)\n* Add test coverage + tests on 4 examples by [@alainivars](https://github.com/alainivars) in [#306](https://github.com/PrefectHQ/fastmcp/pull/306)\n\n### New Contributors\n\n* [@alainivars](https://github.com/alainivars) made their first contribution in [#306](https://github.com/PrefectHQ/fastmcp/pull/306)\n\n**Full Changelog**: [v2.2.9...v2.2.10](https://github.com/PrefectHQ/fastmcp/compare/v2.2.9...v2.2.10)\n</Update>\n\n<Update label=\"v2.2.9\" description=\"2024-05-06\">\n\n## [v2.2.9: Str-ing the Pot (Hotfix)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.9)\n\nThis release is a hotfix for the issue detailed in #330\n\n### Fixes 🐞\n\n* Prevent invalid resource URIs by [@jlowin](https://github.com/jlowin) in [#336](https://github.com/PrefectHQ/fastmcp/pull/336)\n* Coerce numbers to str by [@jlowin](https://github.com/jlowin) in [#337](https://github.com/PrefectHQ/fastmcp/pull/337)\n\n### Docs 📚\n\n* Add client badge by [@jlowin](https://github.com/jlowin) in [#327](https://github.com/PrefectHQ/fastmcp/pull/327)\n* Update bug.yml by [@jlowin](https://github.com/jlowin) in [#328](https://github.com/PrefectHQ/fastmcp/pull/328)\n\n### Other Changes 🦾\n\n* Update quickstart.mdx example to include import by [@discdiver](https://github.com/discdiver) in [#329](https://github.com/PrefectHQ/fastmcp/pull/329)\n\n### New Contributors\n\n* [@discdiver](https://github.com/discdiver) made their first contribution in [#329](https://github.com/PrefectHQ/fastmcp/pull/329)\n\n**Full Changelog**: [v2.2.8...v2.2.9](https://github.com/PrefectHQ/fastmcp/compare/v2.2.8...v2.2.9)\n</Update>\n\n<Update label=\"v2.2.8\" description=\"2024-05-05\">\n\n## [v2.2.8: Parse and Recreation](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.8)\n\n### New Features 🎉\n\n* Replace custom parsing with TypeAdapter by [@jlowin](https://github.com/jlowin) in [#314](https://github.com/PrefectHQ/fastmcp/pull/314)\n* Handle \\*args/\\*\\*kwargs appropriately for various components by [@jlowin](https://github.com/jlowin) in [#317](https://github.com/PrefectHQ/fastmcp/pull/317)\n* Add timeout-graceful-shutdown as a default config for SSE app by [@jlowin](https://github.com/jlowin) in [#323](https://github.com/PrefectHQ/fastmcp/pull/323)\n* Ensure prompts return descriptions by [@jlowin](https://github.com/jlowin) in [#325](https://github.com/PrefectHQ/fastmcp/pull/325)\n\n### Fixes 🐞\n\n* Ensure that tool serialization has a graceful fallback by [@jlowin](https://github.com/jlowin) in [#310](https://github.com/PrefectHQ/fastmcp/pull/310)\n\n### Docs 📚\n\n* Update docs for clarity by [@jlowin](https://github.com/jlowin) in [#312](https://github.com/PrefectHQ/fastmcp/pull/312)\n\n### Other Changes 🦾\n\n* Remove is\\_async attribute by [@jlowin](https://github.com/jlowin) in [#315](https://github.com/PrefectHQ/fastmcp/pull/315)\n* Dry out retrieving context kwarg by [@jlowin](https://github.com/jlowin) in [#316](https://github.com/PrefectHQ/fastmcp/pull/316)\n\n**Full Changelog**: [v2.2.7...v2.2.8](https://github.com/PrefectHQ/fastmcp/compare/v2.2.7...v2.2.8)\n</Update>\n\n<Update label=\"v2.2.7\" description=\"2024-05-03\">\n\n## [v2.2.7: You Auth to Know Better](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.7)\n\n### New Features 🎉\n\n* use pydantic\\_core.to\\_json by [@jlowin](https://github.com/jlowin) in [#290](https://github.com/PrefectHQ/fastmcp/pull/290)\n* Ensure openapi descriptions are included in tool details by [@jlowin](https://github.com/jlowin) in [#293](https://github.com/PrefectHQ/fastmcp/pull/293)\n* Bump mcp to 1.7.1 by [@jlowin](https://github.com/jlowin) in [#298](https://github.com/PrefectHQ/fastmcp/pull/298)\n* Add support for tool annotations by [@jlowin](https://github.com/jlowin) in [#299](https://github.com/PrefectHQ/fastmcp/pull/299)\n* Add auth support by [@jlowin](https://github.com/jlowin) in [#300](https://github.com/PrefectHQ/fastmcp/pull/300)\n* Add low-level methods to client by [@jlowin](https://github.com/jlowin) in [#301](https://github.com/PrefectHQ/fastmcp/pull/301)\n* Add method for retrieving current starlette request to FastMCP context by [@jlowin](https://github.com/jlowin) in [#302](https://github.com/PrefectHQ/fastmcp/pull/302)\n* get\\_starlette\\_request → get\\_http\\_request by [@jlowin](https://github.com/jlowin) in [#303](https://github.com/PrefectHQ/fastmcp/pull/303)\n* Support custom Serializer for Tools by [@strawgate](https://github.com/strawgate) in [#308](https://github.com/PrefectHQ/fastmcp/pull/308)\n* Support proxy mount by [@jlowin](https://github.com/jlowin) in [#309](https://github.com/PrefectHQ/fastmcp/pull/309)\n\n### Other Changes 🦾\n\n* Improve context injection type checks by [@jlowin](https://github.com/jlowin) in [#291](https://github.com/PrefectHQ/fastmcp/pull/291)\n* add readme to smarthome example by [@zzstoatzz](https://github.com/zzstoatzz) in [#294](https://github.com/PrefectHQ/fastmcp/pull/294)\n\n**Full Changelog**: [v2.2.6...v2.2.7](https://github.com/PrefectHQ/fastmcp/compare/v2.2.6...v2.2.7)\n</Update>\n\n<Update label=\"v2.2.6\" description=\"2024-04-30\">\n\n## [v2.2.6: The REST is History](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.6)\n\n### New Features 🎉\n\n* Added feature : Load MCP server using config by [@sandipan1](https://github.com/sandipan1) in [#260](https://github.com/PrefectHQ/fastmcp/pull/260)\n* small typing fixes by [@zzstoatzz](https://github.com/zzstoatzz) in [#237](https://github.com/PrefectHQ/fastmcp/pull/237)\n* Expose configurable timeout for OpenAPI by [@jlowin](https://github.com/jlowin) in [#279](https://github.com/PrefectHQ/fastmcp/pull/279)\n* Lower websockets pin for compatibility by [@jlowin](https://github.com/jlowin) in [#286](https://github.com/PrefectHQ/fastmcp/pull/286)\n* Improve OpenAPI param handling by [@jlowin](https://github.com/jlowin) in [#287](https://github.com/PrefectHQ/fastmcp/pull/287)\n\n### Fixes 🐞\n\n* Ensure openapi tool responses are properly converted by [@jlowin](https://github.com/jlowin) in [#283](https://github.com/PrefectHQ/fastmcp/pull/283)\n* Fix OpenAPI examples by [@jlowin](https://github.com/jlowin) in [#285](https://github.com/PrefectHQ/fastmcp/pull/285)\n* Fix client docs for advanced features, add tests for logging by [@jlowin](https://github.com/jlowin) in [#284](https://github.com/PrefectHQ/fastmcp/pull/284)\n\n### Other Changes 🦾\n\n* add testing doc by [@jlowin](https://github.com/jlowin) in [#264](https://github.com/PrefectHQ/fastmcp/pull/264)\n* #267 Fix openapi template resource to support multiple path parameters by [@jeger-at](https://github.com/jeger-at) in [#278](https://github.com/PrefectHQ/fastmcp/pull/278)\n\n### New Contributors\n\n* [@sandipan1](https://github.com/sandipan1) made their first contribution in [#260](https://github.com/PrefectHQ/fastmcp/pull/260)\n* [@jeger-at](https://github.com/jeger-at) made their first contribution in [#278](https://github.com/PrefectHQ/fastmcp/pull/278)\n\n**Full Changelog**: [v2.2.5...v2.2.6](https://github.com/PrefectHQ/fastmcp/compare/v2.2.5...v2.2.6)\n</Update>\n\n<Update label=\"v2.2.5\" description=\"2024-04-26\">\n\n## [v2.2.5: Context Switching](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.5)\n\n### New Features 🎉\n\n* Add tests for tool return types; improve serialization behavior by [@jlowin](https://github.com/jlowin) in [#262](https://github.com/PrefectHQ/fastmcp/pull/262)\n* Support context injection in resources, templates, and prompts (like tools) by [@jlowin](https://github.com/jlowin) in [#263](https://github.com/PrefectHQ/fastmcp/pull/263)\n\n### Docs 📚\n\n* Update wildcards to 2.2.4 by [@jlowin](https://github.com/jlowin) in [#257](https://github.com/PrefectHQ/fastmcp/pull/257)\n* Update note in templates docs by [@jlowin](https://github.com/jlowin) in [#258](https://github.com/PrefectHQ/fastmcp/pull/258)\n* Significant documentation and test expansion for tool input types by [@jlowin](https://github.com/jlowin) in [#261](https://github.com/PrefectHQ/fastmcp/pull/261)\n\n**Full Changelog**: [v2.2.4...v2.2.5](https://github.com/PrefectHQ/fastmcp/compare/v2.2.4...v2.2.5)\n</Update>\n\n<Update label=\"v2.2.4\" description=\"2024-04-25\">\n\n## [v2.2.4: The Wild Side, Actually](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.4)\n\nThe wildcard URI templates exposed in v2.2.3 were blocked by a server-level check which is removed in this release.\n\n### New Features 🎉\n\n* Allow customization of inspector proxy port, ui port, and version by [@jlowin](https://github.com/jlowin) in [#253](https://github.com/PrefectHQ/fastmcp/pull/253)\n\n### Fixes 🐞\n\n* fix: unintended type convert by [@cutekibry](https://github.com/cutekibry) in [#252](https://github.com/PrefectHQ/fastmcp/pull/252)\n* Ensure openapi resources return valid responses by [@jlowin](https://github.com/jlowin) in [#254](https://github.com/PrefectHQ/fastmcp/pull/254)\n* Ensure servers expose template wildcards by [@jlowin](https://github.com/jlowin) in [#256](https://github.com/PrefectHQ/fastmcp/pull/256)\n\n### Docs 📚\n\n* Update README.md Grammar error by [@TechWithTy](https://github.com/TechWithTy) in [#249](https://github.com/PrefectHQ/fastmcp/pull/249)\n\n### Other Changes 🦾\n\n* Add resource template tests by [@jlowin](https://github.com/jlowin) in [#255](https://github.com/PrefectHQ/fastmcp/pull/255)\n\n### New Contributors\n\n* [@TechWithTy](https://github.com/TechWithTy) made their first contribution in [#249](https://github.com/PrefectHQ/fastmcp/pull/249)\n* [@cutekibry](https://github.com/cutekibry) made their first contribution in [#252](https://github.com/PrefectHQ/fastmcp/pull/252)\n\n**Full Changelog**: [v2.2.3...v2.2.4](https://github.com/PrefectHQ/fastmcp/compare/v2.2.3...v2.2.4)\n</Update>\n\n<Update label=\"v2.2.3\" description=\"2024-04-25\">\n\n## [v2.2.3: The Wild Side](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.3)\n\n### New Features 🎉\n\n* Add wildcard params for resource templates by [@jlowin](https://github.com/jlowin) in [#246](https://github.com/PrefectHQ/fastmcp/pull/246)\n\n### Docs 📚\n\n* Indicate that Image class is for returns by [@jlowin](https://github.com/jlowin) in [#242](https://github.com/PrefectHQ/fastmcp/pull/242)\n* Update mermaid diagram by [@jlowin](https://github.com/jlowin) in [#243](https://github.com/PrefectHQ/fastmcp/pull/243)\n\n### Other Changes 🦾\n\n* update version badges by [@jlowin](https://github.com/jlowin) in [#248](https://github.com/PrefectHQ/fastmcp/pull/248)\n\n**Full Changelog**: [v2.2.2...v2.2.3](https://github.com/PrefectHQ/fastmcp/compare/v2.2.2...v2.2.3)\n</Update>\n\n<Update label=\"v2.2.2\" description=\"2024-04-24\">\n\n## [v2.2.2: Prompt and Circumstance](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.2)\n\n### New Features 🎉\n\n* Add prompt support by [@jlowin](https://github.com/jlowin) in [#235](https://github.com/PrefectHQ/fastmcp/pull/235)\n\n### Fixes 🐞\n\n* Ensure that resource templates are properly exposed by [@jlowin](https://github.com/jlowin) in [#238](https://github.com/PrefectHQ/fastmcp/pull/238)\n\n### Docs 📚\n\n* Update docs for prompts by [@jlowin](https://github.com/jlowin) in [#236](https://github.com/PrefectHQ/fastmcp/pull/236)\n\n### Other Changes 🦾\n\n* Add prompt tests by [@jlowin](https://github.com/jlowin) in [#239](https://github.com/PrefectHQ/fastmcp/pull/239)\n\n**Full Changelog**: [v2.2.1...v2.2.2](https://github.com/PrefectHQ/fastmcp/compare/v2.2.1...v2.2.2)\n</Update>\n\n<Update label=\"v2.2.1\" description=\"2024-04-23\">\n\n## [v2.2.1: Template for Success](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.1)\n\n### New Features 🎉\n\n* Add resource templates by [@jlowin](https://github.com/jlowin) in [#230](https://github.com/PrefectHQ/fastmcp/pull/230)\n\n### Fixes 🐞\n\n* Ensure that resource templates are properly exposed by [@jlowin](https://github.com/jlowin) in [#231](https://github.com/PrefectHQ/fastmcp/pull/231)\n\n### Docs 📚\n\n* Update docs for resource templates by [@jlowin](https://github.com/jlowin) in [#232](https://github.com/PrefectHQ/fastmcp/pull/232)\n\n### Other Changes 🦾\n\n* Add resource template tests by [@jlowin](https://github.com/jlowin) in [#233](https://github.com/PrefectHQ/fastmcp/pull/233)\n\n**Full Changelog**: [v2.2.0...v2.2.1](https://github.com/PrefectHQ/fastmcp/compare/v2.2.0...v2.2.1)\n</Update>\n\n<Update label=\"v2.2.0\" description=\"2024-04-22\">\n\n## [v2.2.0: Compose Yourself](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.0)\n\n### New Features 🎉\n\n* Add support for mounting FastMCP servers by [@jlowin](https://github.com/jlowin) in [#175](https://github.com/PrefectHQ/fastmcp/pull/175)\n* Add support for duplicate behavior == ignore by [@jlowin](https://github.com/jlowin) in [#169](https://github.com/PrefectHQ/fastmcp/pull/169)\n\n### Breaking Changes 🛫\n\n* Refactor MCP composition by [@jlowin](https://github.com/jlowin) in [#176](https://github.com/PrefectHQ/fastmcp/pull/176)\n\n### Docs 📚\n\n* Improve integration documentation by [@jlowin](https://github.com/jlowin) in [#184](https://github.com/PrefectHQ/fastmcp/pull/184)\n* Improve documentation by [@jlowin](https://github.com/jlowin) in [#185](https://github.com/PrefectHQ/fastmcp/pull/185)\n\n### Other Changes 🦾\n\n* Add transport kwargs for mcp.run() and fastmcp run by [@jlowin](https://github.com/jlowin) in [#161](https://github.com/PrefectHQ/fastmcp/pull/161)\n* Allow resource templates to have optional / excluded arguments by [@jlowin](https://github.com/jlowin) in [#164](https://github.com/PrefectHQ/fastmcp/pull/164)\n* Update resources.mdx by [@jlowin](https://github.com/jlowin) in [#165](https://github.com/PrefectHQ/fastmcp/pull/165)\n\n### New Contributors\n\n* [@kongqi404](https://github.com/kongqi404) made their first contribution in [#181](https://github.com/PrefectHQ/fastmcp/pull/181)\n\n**Full Changelog**: [v2.1.2...v2.2.0](https://github.com/PrefectHQ/fastmcp/compare/v2.1.2...v2.2.0)\n</Update>\n\n<Update label=\"v2.1.2\" description=\"2024-04-14\">\n\n## [v2.1.2: Copy That, Good Buddy](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.2)\n\nThe main improvement in this release is a fix that allows FastAPI / OpenAPI-generated servers to be mounted as sub-servers.\n\n### Fixes 🐞\n\n* Ensure objects are copied properly and test mounting fastapi by [@jlowin](https://github.com/jlowin) in [#153](https://github.com/PrefectHQ/fastmcp/pull/153)\n\n### Docs 📚\n\n* Fix broken links in docs by [@jlowin](https://github.com/jlowin) in [#154](https://github.com/PrefectHQ/fastmcp/pull/154)\n\n### Other Changes 🦾\n\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#149](https://github.com/PrefectHQ/fastmcp/pull/149)\n* Only apply log config to FastMCP loggers by [@jlowin](https://github.com/jlowin) in [#155](https://github.com/PrefectHQ/fastmcp/pull/155)\n* Update pyproject.toml by [@jlowin](https://github.com/jlowin) in [#156](https://github.com/PrefectHQ/fastmcp/pull/156)\n\n**Full Changelog**: [v2.1.1...v2.1.2](https://github.com/PrefectHQ/fastmcp/compare/v2.1.1...v2.1.2)\n</Update>\n\n<Update label=\"v2.1.1\" description=\"2024-04-14\">\n\n## [v2.1.1: Doc Holiday](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.1)\n\nFastMCP's docs are now available at gofastmcp.com.\n\n### Docs 📚\n\n* Add docs by [@jlowin](https://github.com/jlowin) in [#136](https://github.com/PrefectHQ/fastmcp/pull/136)\n* Add docs link to readme by [@jlowin](https://github.com/jlowin) in [#137](https://github.com/PrefectHQ/fastmcp/pull/137)\n* Minor docs updates by [@jlowin](https://github.com/jlowin) in [#138](https://github.com/PrefectHQ/fastmcp/pull/138)\n\n### Fixes 🐞\n\n* fix branch name in example by [@zzstoatzz](https://github.com/zzstoatzz) in [#140](https://github.com/PrefectHQ/fastmcp/pull/140)\n\n### Other Changes 🦾\n\n* smart home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#115](https://github.com/PrefectHQ/fastmcp/pull/115)\n* Remove mac os tests by [@jlowin](https://github.com/jlowin) in [#142](https://github.com/PrefectHQ/fastmcp/pull/142)\n* Expand support for various method interactions by [@jlowin](https://github.com/jlowin) in [#143](https://github.com/PrefectHQ/fastmcp/pull/143)\n* Update docs and add\\_resource\\_fn by [@jlowin](https://github.com/jlowin) in [#144](https://github.com/PrefectHQ/fastmcp/pull/144)\n* Update description by [@jlowin](https://github.com/jlowin) in [#145](https://github.com/PrefectHQ/fastmcp/pull/145)\n* Support openapi 3.0 and 3.1 by [@jlowin](https://github.com/jlowin) in [#147](https://github.com/PrefectHQ/fastmcp/pull/147)\n\n**Full Changelog**: [v2.1.0...v2.1.1](https://github.com/PrefectHQ/fastmcp/compare/v2.1.0...v2.1.1)\n</Update>\n\n<Update label=\"v2.1.0\" description=\"2024-04-13\">\n\n## [v2.1.0: Tag, You're It](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.0)\n\nThe primary motivation for this release is the fix in #128 for Claude desktop compatibility, but the primary new feature of this release is per-object tags. Currently these are for bookkeeping only but will become useful in future releases.\n\n### New Features 🎉\n\n* Add tags for all core MCP objects by [@jlowin](https://github.com/jlowin) in [#121](https://github.com/PrefectHQ/fastmcp/pull/121)\n* Ensure that openapi tags are transferred to MCP objects by [@jlowin](https://github.com/jlowin) in [#124](https://github.com/PrefectHQ/fastmcp/pull/124)\n\n### Fixes 🐞\n\n* Change default mounted tool separator from / to \\_ by [@jlowin](https://github.com/jlowin) in [#128](https://github.com/PrefectHQ/fastmcp/pull/128)\n* Enter mounted app lifespans by [@jlowin](https://github.com/jlowin) in [#129](https://github.com/PrefectHQ/fastmcp/pull/129)\n* Fix CLI that called mcp instead of fastmcp by [@jlowin](https://github.com/jlowin) in [#128](https://github.com/PrefectHQ/fastmcp/pull/128)\n\n### Breaking Changes 🛫\n\n* Changed configuration for duplicate resources/tools/prompts by [@jlowin](https://github.com/jlowin) in [#121](https://github.com/PrefectHQ/fastmcp/pull/121)\n* Improve client return types by [@jlowin](https://github.com/jlowin) in [#123](https://github.com/PrefectHQ/fastmcp/pull/123)\n\n### Other Changes 🦾\n\n* Add tests for tags in server decorators by [@jlowin](https://github.com/jlowin) in [#122](https://github.com/PrefectHQ/fastmcp/pull/122)\n* Clean up server tests by [@jlowin](https://github.com/jlowin) in [#125](https://github.com/PrefectHQ/fastmcp/pull/125)\n\n**Full Changelog**: [v2.0.0...v2.1.0](https://github.com/PrefectHQ/fastmcp/compare/v2.0.0...v2.1.0)\n</Update>\n\n<Update label=\"v2.0.0\" description=\"2024-04-11\">\n\n## [v2.0.0: Second to None](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.0.0)\n\n### New Features 🎉\n\n* Support mounting FastMCP instances as sub-MCPs by [@jlowin](https://github.com/jlowin) in [#99](https://github.com/PrefectHQ/fastmcp/pull/99)\n* Add in-memory client for calling FastMCP servers (and tests) by [@jlowin](https://github.com/jlowin) in [#100](https://github.com/PrefectHQ/fastmcp/pull/100)\n* Add MCP proxy server by [@jlowin](https://github.com/jlowin) in [#105](https://github.com/PrefectHQ/fastmcp/pull/105)\n* Update FastMCP for upstream changes by [@jlowin](https://github.com/jlowin) in [#107](https://github.com/PrefectHQ/fastmcp/pull/107)\n* Generate FastMCP servers from OpenAPI specs and FastAPI by [@jlowin](https://github.com/jlowin) in [#110](https://github.com/PrefectHQ/fastmcp/pull/110)\n* Reorganize all client / transports by [@jlowin](https://github.com/jlowin) in [#111](https://github.com/PrefectHQ/fastmcp/pull/111)\n* Add sampling and roots by [@jlowin](https://github.com/jlowin) in [#117](https://github.com/PrefectHQ/fastmcp/pull/117)\n\n### Fixes 🐞\n\n* Fix bug with tools that return lists by [@jlowin](https://github.com/jlowin) in [#116](https://github.com/PrefectHQ/fastmcp/pull/116)\n\n### Other Changes 🦾\n\n* Add back FastMCP CLI by [@jlowin](https://github.com/jlowin) in [#108](https://github.com/PrefectHQ/fastmcp/pull/108)\n* Update Readme for v2 by [@jlowin](https://github.com/jlowin) in [#112](https://github.com/PrefectHQ/fastmcp/pull/112)\n* fix deprecation warnings by [@zzstoatzz](https://github.com/zzstoatzz) in [#113](https://github.com/PrefectHQ/fastmcp/pull/113)\n* Readme by [@jlowin](https://github.com/jlowin) in [#118](https://github.com/PrefectHQ/fastmcp/pull/118)\n* FastMCP 2.0 by [@jlowin](https://github.com/jlowin) in [#119](https://github.com/PrefectHQ/fastmcp/pull/119)\n\n**Full Changelog**: [v1.0...v2.0.0](https://github.com/PrefectHQ/fastmcp/compare/v1.0...v2.0.0)\n</Update>\n\n<Update label=\"v1.0\" description=\"2024-04-11\">\n\n## [v1.0: It's Official](https://github.com/PrefectHQ/fastmcp/releases/tag/v1.0)\n\nThis release commemorates FastMCP 1.0, which is included in the official Model Context Protocol SDK:\n\n```python\nfrom mcp.server.fastmcp import FastMCP\n```\n\nTo the best of my knowledge, v1 is identical to the upstream version included with `mcp`.\n\n### Docs 📚\n\n* Update readme to redirect to the official SDK by [@jlowin](https://github.com/jlowin) in [#79](https://github.com/PrefectHQ/fastmcp/pull/79)\n\n### Other Changes 🦾\n\n* fix: use Mount instead of Route for SSE message handling by [@samihamine](https://github.com/samihamine) in [#77](https://github.com/PrefectHQ/fastmcp/pull/77)\n\n### New Contributors\n\n* [@samihamine](https://github.com/samihamine) made their first contribution in [#77](https://github.com/PrefectHQ/fastmcp/pull/77)\n\n**Full Changelog**: [v0.4.1...v1.0](https://github.com/PrefectHQ/fastmcp/compare/v0.4.1...v1.0)\n</Update>\n\n<Update label=\"v0.4.1\" description=\"2024-12-09\">\n\n## [v0.4.1: String Theory](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.4.1)\n\n### Fixes 🐞\n\n* fix: handle strings containing numbers correctly by [@sd2k](https://github.com/sd2k) in [#63](https://github.com/PrefectHQ/fastmcp/pull/63)\n\n### Docs 📚\n\n* patch: Update pyproject.toml license by [@leonkozlowski](https://github.com/leonkozlowski) in [#67](https://github.com/PrefectHQ/fastmcp/pull/67)\n\n### Other Changes 🦾\n\n* Avoid new try\\_eval\\_type unavailable with older pydantic by [@jurasofish](https://github.com/jurasofish) in [#57](https://github.com/PrefectHQ/fastmcp/pull/57)\n* Decorator typing by [@jurasofish](https://github.com/jurasofish) in [#56](https://github.com/PrefectHQ/fastmcp/pull/56)\n\n### New Contributors\n\n* [@leonkozlowski](https://github.com/leonkozlowski) made their first contribution in [#67](https://github.com/PrefectHQ/fastmcp/pull/67)\n\n**Full Changelog**: [v0.4.0...v0.4.1](https://github.com/PrefectHQ/fastmcp/compare/v0.4.0...v0.4.1)\n</Update>\n\n<Update label=\"v0.4.0\" description=\"2024-12-05\">\n\n## [v0.4.0: Nice to MIT You](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.4.0)\n\nThis is a relatively small release in terms of features, but the version is bumped to 0.4 to reflect that the code is being relicensed from Apache 2.0 to MIT. This is to facilitate FastMCP's inclusion in the official MCP SDK.\n\n### New Features 🎉\n\n* Add pyright + tests by [@jlowin](https://github.com/jlowin) in [#52](https://github.com/PrefectHQ/fastmcp/pull/52)\n* add pgvector memory example by [@zzstoatzz](https://github.com/zzstoatzz) in [#49](https://github.com/PrefectHQ/fastmcp/pull/49)\n\n### Fixes 🐞\n\n* fix: use stderr for logging by [@sd2k](https://github.com/sd2k) in [#51](https://github.com/PrefectHQ/fastmcp/pull/51)\n\n### Docs 📚\n\n* Update ai-labeler.yml by [@jlowin](https://github.com/jlowin) in [#48](https://github.com/PrefectHQ/fastmcp/pull/48)\n* Relicense from Apache 2.0 to MIT by [@jlowin](https://github.com/jlowin) in [#54](https://github.com/PrefectHQ/fastmcp/pull/54)\n\n### Other Changes 🦾\n\n* fix warning and flake by [@zzstoatzz](https://github.com/zzstoatzz) in [#47](https://github.com/PrefectHQ/fastmcp/pull/47)\n\n### New Contributors\n\n* [@sd2k](https://github.com/sd2k) made their first contribution in [#51](https://github.com/PrefectHQ/fastmcp/pull/51)\n\n**Full Changelog**: [v0.3.5...v0.4.0](https://github.com/PrefectHQ/fastmcp/compare/v0.3.5...v0.4.0)\n</Update>\n\n<Update label=\"v0.3.5\" description=\"2024-12-03\">\n\n## [v0.3.5: Windows of Opportunity](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.5)\n\nThis release is highlighted by the ability to handle complex JSON objects as MCP inputs and improved Windows compatibility.\n\n### New Features 🎉\n\n* Set up multiple os tests by [@jlowin](https://github.com/jlowin) in [#44](https://github.com/PrefectHQ/fastmcp/pull/44)\n* Changes to accommodate windows users. by [@justjoehere](https://github.com/justjoehere) in [#42](https://github.com/PrefectHQ/fastmcp/pull/42)\n* Handle complex inputs by [@jurasofish](https://github.com/jurasofish) in [#31](https://github.com/PrefectHQ/fastmcp/pull/31)\n\n### Docs 📚\n\n* Make AI labeler more conservative by [@jlowin](https://github.com/jlowin) in [#46](https://github.com/PrefectHQ/fastmcp/pull/46)\n\n### Other Changes 🦾\n\n* Additional Windows Fixes for Dev running and for importing modules in a server by [@justjoehere](https://github.com/justjoehere) in [#43](https://github.com/PrefectHQ/fastmcp/pull/43)\n\n### New Contributors\n\n* [@justjoehere](https://github.com/justjoehere) made their first contribution in [#42](https://github.com/PrefectHQ/fastmcp/pull/42)\n* [@jurasofish](https://github.com/jurasofish) made their first contribution in [#31](https://github.com/PrefectHQ/fastmcp/pull/31)\n\n**Full Changelog**: [v0.3.4...v0.3.5](https://github.com/PrefectHQ/fastmcp/compare/v0.3.4...v0.3.5)\n</Update>\n\n<Update label=\"v0.3.4\" description=\"2024-12-02\">\n\n## [v0.3.4: URL's Well That Ends Well](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.4)\n\n### Fixes 🐞\n\n* Handle missing config file when installing by [@jlowin](https://github.com/jlowin) in [#37](https://github.com/PrefectHQ/fastmcp/pull/37)\n* Remove BaseURL reference and use AnyURL by [@jlowin](https://github.com/jlowin) in [#40](https://github.com/PrefectHQ/fastmcp/pull/40)\n\n**Full Changelog**: [v0.3.3...v0.3.4](https://github.com/PrefectHQ/fastmcp/compare/v0.3.3...v0.3.4)\n</Update>\n\n<Update label=\"v0.3.3\" description=\"2024-12-02\">\n\n## [v0.3.3: Dependence Day](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.3)\n\n### New Features 🎉\n\n* Surge example by [@zzstoatzz](https://github.com/zzstoatzz) in [#29](https://github.com/PrefectHQ/fastmcp/pull/29)\n* Support Python dependencies in Server by [@jlowin](https://github.com/jlowin) in [#34](https://github.com/PrefectHQ/fastmcp/pull/34)\n\n### Docs 📚\n\n* add `Contributing` section to README by [@zzstoatzz](https://github.com/zzstoatzz) in [#32](https://github.com/PrefectHQ/fastmcp/pull/32)\n\n**Full Changelog**: [v0.3.2...v0.3.3](https://github.com/PrefectHQ/fastmcp/compare/v0.3.2...v0.3.3)\n</Update>\n\n<Update label=\"v0.3.2\" date=\"2024-12-01\" description=\"Green with ENVy\">\n\n## [v0.3.2: Green with ENVy](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.2)\n\n### New Features 🎉\n\n* Support env vars when installing by [@jlowin](https://github.com/jlowin) in [#27](https://github.com/PrefectHQ/fastmcp/pull/27)\n\n### Docs 📚\n\n* Remove top level env var by [@jlowin](https://github.com/jlowin) in [#28](https://github.com/PrefectHQ/fastmcp/pull/28)\n\n**Full Changelog**: [v0.3.1...v0.3.2](https://github.com/PrefectHQ/fastmcp/compare/v0.3.1...v0.3.2)\n</Update>\n\n<Update label=\"v0.3.1\" description=\"2024-12-01\">\n\n## [v0.3.1](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.1)\n\n### New Features 🎉\n\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#23](https://github.com/PrefectHQ/fastmcp/pull/23)\n* add rich handler and dotenv loading for settings by [@zzstoatzz](https://github.com/zzstoatzz) in [#22](https://github.com/PrefectHQ/fastmcp/pull/22)\n* print exception when server can't start by [@jlowin](https://github.com/jlowin) in [#25](https://github.com/PrefectHQ/fastmcp/pull/25)\n\n### Docs 📚\n\n* Update README.md by [@jlowin](https://github.com/jlowin) in [#24](https://github.com/PrefectHQ/fastmcp/pull/24)\n\n### Other Changes 🦾\n\n* Remove log by [@jlowin](https://github.com/jlowin) in [#26](https://github.com/PrefectHQ/fastmcp/pull/26)\n\n**Full Changelog**: [v0.3.0...v0.3.1](https://github.com/PrefectHQ/fastmcp/compare/v0.3.0...v0.3.1)\n</Update>\n\n<Update label=\"v0.3.0\" description=\"2024-12-01\">\n\n## [v0.3.0: Prompt and Circumstance](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.0)\n\n### New Features 🎉\n\n* Update README by [@jlowin](https://github.com/jlowin) in [#3](https://github.com/PrefectHQ/fastmcp/pull/3)\n* Make log levels strings by [@jlowin](https://github.com/jlowin) in [#4](https://github.com/PrefectHQ/fastmcp/pull/4)\n* Make content method a function by [@jlowin](https://github.com/jlowin) in [#5](https://github.com/PrefectHQ/fastmcp/pull/5)\n* Add template support by [@jlowin](https://github.com/jlowin) in [#6](https://github.com/PrefectHQ/fastmcp/pull/6)\n* Refactor resources module by [@jlowin](https://github.com/jlowin) in [#7](https://github.com/PrefectHQ/fastmcp/pull/7)\n* Clean up cli imports by [@jlowin](https://github.com/jlowin) in [#8](https://github.com/PrefectHQ/fastmcp/pull/8)\n* Prepare to list templates by [@jlowin](https://github.com/jlowin) in [#11](https://github.com/PrefectHQ/fastmcp/pull/11)\n* Move image to separate module by [@jlowin](https://github.com/jlowin) in [#9](https://github.com/PrefectHQ/fastmcp/pull/9)\n* Add support for request context, progress, logging, etc. by [@jlowin](https://github.com/jlowin) in [#12](https://github.com/PrefectHQ/fastmcp/pull/12)\n* Add context tests and better runtime loads by [@jlowin](https://github.com/jlowin) in [#13](https://github.com/PrefectHQ/fastmcp/pull/13)\n* Refactor tools + resourcemanager by [@jlowin](https://github.com/jlowin) in [#14](https://github.com/PrefectHQ/fastmcp/pull/14)\n* func → fn everywhere by [@jlowin](https://github.com/jlowin) in [#15](https://github.com/PrefectHQ/fastmcp/pull/15)\n* Add support for prompts by [@jlowin](https://github.com/jlowin) in [#16](https://github.com/PrefectHQ/fastmcp/pull/16)\n* Create LICENSE by [@jlowin](https://github.com/jlowin) in [#18](https://github.com/PrefectHQ/fastmcp/pull/18)\n* Update cli file spec by [@jlowin](https://github.com/jlowin) in [#19](https://github.com/PrefectHQ/fastmcp/pull/19)\n* Update readmeUpdate README by [@jlowin](https://github.com/jlowin) in [#20](https://github.com/PrefectHQ/fastmcp/pull/20)\n* Use hatchling for version by [@jlowin](https://github.com/jlowin) in [#21](https://github.com/PrefectHQ/fastmcp/pull/21)\n\n### Other Changes 🦾\n\n* Add echo server by [@jlowin](https://github.com/jlowin) in [#1](https://github.com/PrefectHQ/fastmcp/pull/1)\n* Add github workflows by [@jlowin](https://github.com/jlowin) in [#2](https://github.com/PrefectHQ/fastmcp/pull/2)\n* typing updates by [@zzstoatzz](https://github.com/zzstoatzz) in [#17](https://github.com/PrefectHQ/fastmcp/pull/17)\n\n### New Contributors\n\n* [@jlowin](https://github.com/jlowin) made their first contribution in [#1](https://github.com/PrefectHQ/fastmcp/pull/1)\n* [@zzstoatzz](https://github.com/zzstoatzz) made their first contribution in [#17](https://github.com/PrefectHQ/fastmcp/pull/17)\n\n**Full Changelog**: [v0.2.0...v0.3.0](https://github.com/PrefectHQ/fastmcp/compare/v0.2.0...v0.3.0)\n</Update>\n\n<Update label=\"v0.2.0\" description=\"2024-11-30\">\n\n## [v0.2.0](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.2.0)\n\n**Full Changelog**: [v0.1.0...v0.2.0](https://github.com/PrefectHQ/fastmcp/compare/v0.1.0...v0.2.0)\n</Update>\n\n<Update label=\"v0.1.0\" description=\"2024-11-30\">\n\n## [v0.1.0](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.1.0)\n\nThe very first release of FastMCP! 🎉\n\n**Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0)\n</Update>"
  },
  {
    "path": "docs/v2/clients/auth/bearer.mdx",
    "content": "---\ntitle: Bearer Token Authentication\nsidebarTitle: Bearer Auth\ndescription: Authenticate your FastMCP client with a Bearer token.\nicon: key\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.6.0\" />\n\n<Tip>\nBearer Token authentication is only relevant for HTTP-based transports.\n</Tip>\n\nYou can configure your FastMCP client to use **bearer authentication** by supplying a valid access token. This is most appropriate for service accounts, long-lived API keys, CI/CD, applications where authentication is managed separately, or other non-interactive authentication methods.\n\nA Bearer token is a JSON Web Token (JWT) that is used to authenticate a request. It is most commonly used in the `Authorization` header of an HTTP request, using the `Bearer` scheme:\n\n```http\nAuthorization: Bearer <token>\n```\n\n\n## Client Usage\n\nThe most straightforward way to use a pre-existing Bearer token is to provide it as a string to the `auth` parameter of the `fastmcp.Client` or transport instance. FastMCP will automatically format it correctly for the `Authorization` header and bearer scheme.\n\n<Tip>\nIf you're using a string token, do not include the `Bearer` prefix. FastMCP will add it for you.\n</Tip>\n\n```python {5}\nfrom fastmcp import Client\n\nasync with Client(\n    \"https://your-server.fastmcp.app/mcp\", \n    auth=\"<your-token>\",\n) as client:\n    await client.ping()\n```\n\nYou can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:\n\n```python {6}\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\ntransport = StreamableHttpTransport(\n    \"http://your-server.fastmcp.app/mcp\", \n    auth=\"<your-token>\",\n)\n\nasync with Client(transport) as client:\n    await client.ping()\n```\n\n## `BearerAuth` Helper\n\nIf you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface.\n\n```python {6}\nfrom fastmcp import Client\nfrom fastmcp.client.auth import BearerAuth\n\nasync with Client(\n    \"https://your-server.fastmcp.app/mcp\", \n    auth=BearerAuth(token=\"<your-token>\"),\n) as client:\n    await client.ping()\n```\n\n## Custom Headers\n\nIf the MCP server expects a custom header or token scheme, you can manually set the client's `headers` instead of using the `auth` parameter by setting them on your transport:\n\n```python {5}\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\nasync with Client(\n    transport=StreamableHttpTransport(\n        \"https://your-server.fastmcp.app/mcp\", \n        headers={\"X-API-Key\": \"<your-token>\"},\n    ),\n) as client:\n    await client.ping()\n```\n"
  },
  {
    "path": "docs/v2/clients/auth/oauth.mdx",
    "content": "---\ntitle: OAuth Authentication\nsidebarTitle: OAuth\ndescription: Authenticate your FastMCP client via OAuth 2.1.\nicon: window\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.6.0\" />\n\n<Tip>\nOAuth authentication is only relevant for HTTP-based transports and requires user interaction via a web browser.\n</Tip>\n\nWhen your FastMCP client needs to access an MCP server protected by OAuth 2.1, and the process requires user interaction (like logging in and granting consent), you should use the Authorization Code Flow. FastMCP provides the `fastmcp.client.auth.OAuth` helper to simplify this entire process.\n\nThis flow is common for user-facing applications where the application acts on behalf of the user.\n\n## Client Usage\n\n\n### Default Configuration\n\nThe simplest way to use OAuth is to pass the string `\"oauth\"` to the `auth` parameter of the `Client` or transport instance. FastMCP will automatically configure the client to use OAuth with default settings:\n\n```python {4}\nfrom fastmcp import Client\n\n# Uses default OAuth settings\nasync with Client(\"https://your-server.fastmcp.app/mcp\", auth=\"oauth\") as client:\n    await client.ping()\n```\n\n\n### `OAuth` Helper\n\nTo fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx.Auth` interface.\n\n```python {2, 4, 6}\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\n\noauth = OAuth(mcp_url=\"https://your-server.fastmcp.app/mcp\")\n\nasync with Client(\"https://your-server.fastmcp.app/mcp\", auth=oauth) as client:\n    await client.ping()\n```\n\n#### `OAuth` Parameters\n\n- **`mcp_url`** (`str`): The full URL of the target MCP server endpoint. Used to discover OAuth server metadata\n- **`scopes`** (`str | list[str]`, optional): OAuth scopes to request. Can be space-separated string or list of strings\n- **`client_name`** (`str`, optional): Client name for dynamic registration. Defaults to `\"FastMCP Client\"`\n- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options\n- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration\n- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port\n\n\n## OAuth Flow\n\nThe OAuth flow is triggered when you use a FastMCP `Client` configured to use OAuth.\n\n<Steps>\n<Step title=\"Token Check\">\nThe client first checks the configured `token_storage` backend for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client.\n</Step>\n<Step title=\"OAuth Server Discovery\">\nIf no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.\n</Step>\n<Step title=\"Dynamic Client Registration\">\nIf the OAuth server supports it and the client isn't already registered (or credentials aren't cached), the client performs dynamic client registration according to RFC 7591.\n</Step>\n<Step title=\"Local Callback Server\">\nA temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:<port>/callback`) acts as the `redirect_uri` for the OAuth flow.\n</Step>\n<Step title=\"Browser Interaction\">\nThe user's default web browser is automatically opened, directing them to the OAuth server's authorization endpoint. The user logs in and grants (or denies) the requested `scopes`.\n</Step>\n<Step title=\"Authorization Code & Token Exchange\">\nUpon approval, the OAuth server redirects the user's browser to the local callback server with an `authorization_code`. The client captures this code and exchanges it with the OAuth server's token endpoint for an `access_token` (and often a `refresh_token`) using PKCE for security.\n</Step>\n<Step title=\"Token Caching\">\nThe obtained tokens are saved to the configured `token_storage` backend for future use, eliminating the need for repeated browser interactions.\n</Step>\n<Step title=\"Authenticated Requests\">\nThe access token is automatically included in the `Authorization` header for requests to the MCP server.\n</Step>\n<Step title=\"Refresh Token\">\nIf the access token expires, the client will automatically use the refresh token to get a new access token.\n</Step>\n</Steps>\n\n## Token Storage\n\n<VersionBadge version=\"2.13.0\" />\n\nBy default, tokens are stored in memory and lost when your application restarts. For persistent storage, pass an `AsyncKeyValue`-compatible storage backend to the `token_storage` parameter.\n\n<Warning>\n**Security Consideration**: Use encrypted storage for production. MCP clients can accumulate OAuth credentials for many servers over time, and a compromised token store could expose access to multiple services.\n</Warning>\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import OAuth\nfrom key_value.aio.stores.disk import DiskStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\nimport os\n\n# Create encrypted disk storage\nencrypted_storage = FernetEncryptionWrapper(\n    key_value=DiskStore(directory=\"~/.fastmcp/oauth-tokens\"),\n    fernet=Fernet(os.environ[\"OAUTH_STORAGE_ENCRYPTION_KEY\"])\n)\n\noauth = OAuth(\n    mcp_url=\"https://your-server.fastmcp.app/mcp\",\n    token_storage=encrypted_storage\n)\n\nasync with Client(\"https://your-server.fastmcp.app/mcp\", auth=oauth) as client:\n    await client.ping()\n```\n\nYou can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.\n\n<Note>\nWhen selecting a storage backend, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have constraints that affect production suitability.\n</Note>\n"
  },
  {
    "path": "docs/v2/clients/client.mdx",
    "content": "---\ntitle: The FastMCP Client\nsidebarTitle: Overview\ndescription: Programmatic client for interacting with MCP servers through a well-typed, Pythonic interface.\nicon: user-robot\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nThe central piece of MCP client applications is the `fastmcp.Client` class. This class provides a **programmatic interface** for interacting with any Model Context Protocol (MCP) server, handling protocol details and connection management automatically.\n\nThe FastMCP Client is designed for deterministic, controlled interactions rather than autonomous behavior, making it ideal for:\n\n- **Testing MCP servers** during development\n- **Building deterministic applications** that need reliable MCP interactions  \n- **Creating the foundation for agentic or LLM-based clients** with structured, type-safe operations\n\nAll client operations require using the `async with` context manager for proper connection lifecycle management.\n\n\n<Note>\nThis is not an agentic client - it requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems.\n</Note>\n\n## Creating a Client\n\nCreating a client is straightforward. You provide a server source and the client automatically infers the appropriate transport mechanism.\n\n```python\nimport asyncio\nfrom fastmcp import Client, FastMCP\n\n# In-memory server (ideal for testing)\nserver = FastMCP(\"TestServer\")\nclient = Client(server)\n\n# HTTP server\nclient = Client(\"https://example.com/mcp\")\n\n# Local Python script\nclient = Client(\"my_mcp_server.py\")\n\nasync def main():\n    async with client:\n        # Basic server interaction\n        await client.ping()\n        \n        # List available operations\n        tools = await client.list_tools()\n        resources = await client.list_resources()\n        prompts = await client.list_prompts()\n        \n        # Execute operations\n        result = await client.call_tool(\"example_tool\", {\"param\": \"value\"})\n        print(result)\n\nasyncio.run(main())\n```\n\n## Client-Transport Architecture\n\nThe FastMCP Client separates concerns between protocol and connection:\n\n- **`Client`**: Handles MCP protocol operations (tools, resources, prompts) and manages callbacks\n- **`Transport`**: Establishes and maintains the connection (WebSockets, HTTP, Stdio, in-memory)\n\n### Transport Inference\n\nThe client automatically infers the appropriate transport based on the input:\n\n1. **`FastMCP` instance** → In-memory transport (perfect for testing)\n2. **File path ending in `.py`** → Python Stdio transport\n3. **File path ending in `.js`** → Node.js Stdio transport  \n4. **URL starting with `http://` or `https://`** → HTTP transport\n5. **`MCPConfig` dictionary** → Multi-server client\n\n```python\nfrom fastmcp import Client, FastMCP\n\n# Examples of transport inference\nclient_memory = Client(FastMCP(\"TestServer\"))\nclient_script = Client(\"./server.py\") \nclient_http = Client(\"https://api.example.com/mcp\")\n```\n\n<Tip>\nFor testing and development, always prefer the in-memory transport by passing a `FastMCP` server directly to the client. This eliminates network complexity and separate processes.\n</Tip>\n\n## Configuration-Based Clients\n\n<VersionBadge version=\"2.4.0\" />\n\nCreate clients from MCP configuration dictionaries, which can include multiple servers. While there is no official standard for MCP configuration format, FastMCP follows established conventions used by tools like Claude Desktop.\n\n### Configuration Format\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"server_name\": {\n            # Remote HTTP/SSE server\n            \"transport\": \"http\",  # or \"sse\" \n            \"url\": \"https://api.example.com/mcp\",\n            \"headers\": {\"Authorization\": \"Bearer token\"},\n            \"auth\": \"oauth\"  # or bearer token string\n        },\n        \"local_server\": {\n            # Local stdio server\n            \"transport\": \"stdio\",\n            \"command\": \"python\",\n            \"args\": [\"./server.py\", \"--verbose\"],\n            \"env\": {\"DEBUG\": \"true\"},\n            \"cwd\": \"/path/to/server\",\n        }\n    }\n}\n```\n\n### Multi-Server Example\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\"url\": \"https://weather-api.example.com/mcp\"},\n        \"assistant\": {\"command\": \"python\", \"args\": [\"./assistant_server.py\"]}\n    }\n}\n\nclient = Client(config)\n\nasync with client:\n    # Tools are prefixed with server names\n    weather_data = await client.call_tool(\"weather_get_forecast\", {\"city\": \"London\"})\n    response = await client.call_tool(\"assistant_answer_question\", {\"question\": \"What's the capital of France?\"})\n    \n    # Resources use prefixed URIs\n    icons = await client.read_resource(\"weather://weather/icons/sunny\")\n    templates = await client.read_resource(\"resource://assistant/templates/list\")\n```\n\n## Connection Lifecycle\n\nThe client operates asynchronously and uses context managers for connection management:\n\n```python\nasync def example():\n    client = Client(\"my_mcp_server.py\")\n    \n    # Connection established here\n    async with client:\n        print(f\"Connected: {client.is_connected()}\")\n        \n        # Make multiple calls within the same session\n        tools = await client.list_tools()\n        result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        \n    # Connection closed automatically here\n    print(f\"Connected: {client.is_connected()}\")\n```\n\n## Operations\n\nFastMCP clients can interact with several types of server components:\n\n### Tools\n\nTools are server-side functions that the client can execute with arguments.\n\n```python\nasync with client:\n    # List available tools\n    tools = await client.list_tools()\n    \n    # Execute a tool\n    result = await client.call_tool(\"multiply\", {\"a\": 5, \"b\": 3})\n    print(result.data)  # 15\n```\n\nSee [Tools](/v2/clients/tools) for detailed documentation.\n\n### Resources\n\nResources are data sources that the client can read, either static or templated.\n\n```python\nasync with client:\n    # List available resources\n    resources = await client.list_resources()\n    \n    # Read a resource\n    content = await client.read_resource(\"file:///config/settings.json\")\n    print(content[0].text)\n```\n\nSee [Resources](/v2/clients/resources) for detailed documentation.\n\n### Prompts\n\nPrompts are reusable message templates that can accept arguments.\n\n```python\nasync with client:\n    # List available prompts\n    prompts = await client.list_prompts()\n    \n    # Get a rendered prompt\n    messages = await client.get_prompt(\"analyze_data\", {\"data\": [1, 2, 3]})\n    print(messages.messages)\n```\n\nSee [Prompts](/v2/clients/prompts) for detailed documentation.\n\n### Server Connectivity\n\nUse `ping()` to verify the server is reachable:\n\n```python\nasync with client:\n    await client.ping()\n    print(\"Server is reachable\")\n```\n\n### Initialization and Server Information\n\nWhen you enter the client context manager, the client automatically performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions. The result is available through the `initialize_result` property.\n\n```python\nfrom fastmcp import Client, FastMCP\n\nmcp = FastMCP(name=\"MyServer\", instructions=\"Use the greet tool to say hello!\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet a user by name.\"\"\"\n    return f\"Hello, {name}!\"\n\nasync with Client(mcp) as client:\n    # Initialization already happened automatically\n    print(f\"Server: {client.initialize_result.serverInfo.name}\")\n    print(f\"Version: {client.initialize_result.serverInfo.version}\")\n    print(f\"Instructions: {client.initialize_result.instructions}\")\n    print(f\"Capabilities: {client.initialize_result.capabilities.tools}\")\n```\n\n#### Manual Initialization Control\n\nIn advanced scenarios, you might want precise control over when initialization happens. For example, you may need custom error handling, want to defer initialization until after other setup, or need to measure initialization timing separately.\n\nDisable automatic initialization and call `initialize()` manually:\n\n```python\nfrom fastmcp import Client\n\n# Disable automatic initialization\nclient = Client(\"my_mcp_server.py\", auto_initialize=False)\n\nasync with client:\n    # Connection established, but not initialized yet\n    print(f\"Connected: {client.is_connected()}\")\n    print(f\"Initialized: {client.initialize_result is not None}\")  # False\n\n    # Initialize manually with custom timeout\n    result = await client.initialize(timeout=10.0)\n    print(f\"Server: {result.serverInfo.name}\")\n\n    # Now ready for operations\n    tools = await client.list_tools()\n```\n\nThe `initialize()` method is idempotent - calling it multiple times returns the cached result from the first successful call.\n\n## Client Configuration\n\nClients can be configured with additional handlers and settings for specialized use cases.\n\n### Callback Handlers\n\nThe client supports several callback handlers for advanced server interactions:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.logging import LogMessage\n\nasync def log_handler(message: LogMessage):\n    print(f\"Server log: {message.data}\")\n\nasync def progress_handler(progress: float, total: float | None, message: str | None):\n    print(f\"Progress: {progress}/{total} - {message}\")\n\nasync def sampling_handler(messages, params, context):\n    # Integrate with your LLM service here\n    return \"Generated response\"\n\nclient = Client(\n    \"my_mcp_server.py\",\n    log_handler=log_handler,\n    progress_handler=progress_handler,\n    sampling_handler=sampling_handler,\n    timeout=30.0\n)\n```\n\nThe `Client` constructor accepts several configuration options:\n\n- `transport`: Transport instance or source for automatic inference  \n- `log_handler`: Handle server log messages\n- `progress_handler`: Monitor long-running operations\n- `sampling_handler`: Respond to server LLM requests\n- `roots`: Provide local context to servers\n- `timeout`: Default timeout for requests (in seconds)\n\n### Transport Configuration\n\nFor detailed transport configuration (headers, authentication, environment variables), see the [Transports](/v2/clients/transports) documentation.\n\n## Next Steps\n\nExplore the detailed documentation for each operation type:\n\n### Core Operations\n- **[Tools](/v2/clients/tools)** - Execute server-side functions and handle results\n- **[Resources](/v2/clients/resources)** - Access static and templated resources  \n- **[Prompts](/v2/clients/prompts)** - Work with message templates and argument serialization\n\n### Advanced Features\n- **[Logging](/v2/clients/logging)** - Handle server log messages\n- **[Progress](/v2/clients/progress)** - Monitor long-running operations\n- **[Sampling](/v2/clients/sampling)** - Respond to server LLM requests\n- **[Roots](/v2/clients/roots)** - Provide local context to servers\n\n### Connection Details\n- **[Transports](/v2/clients/transports)** - Configure connection methods and parameters\n- **[Authentication](/v2/clients/auth/oauth)** - Set up OAuth and bearer token authentication\n\n<Tip>\nThe FastMCP Client is designed as a foundational tool. Use it directly for deterministic operations, or build higher-level agentic systems on top of its reliable, type-safe interface.\n</Tip>"
  },
  {
    "path": "docs/v2/clients/elicitation.mdx",
    "content": "---\ntitle: User Elicitation\nsidebarTitle: Elicitation\ndescription: Handle server-initiated user input requests with structured schemas.\nicon: message-question\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.10.0\" />\n\n## What is Elicitation?\n\nElicitation allows MCP servers to request structured input from users during tool execution. Instead of requiring all inputs upfront, servers can interactively ask users for information as needed - like prompting for missing parameters, requesting clarification, or gathering additional context.\n\nFor example, a file management tool might ask \"Which directory should I create?\" or a data analysis tool might request \"What date range should I analyze?\"\n\n## How FastMCP Makes Elicitation Easy\n\nFastMCP's client provides a helpful abstraction layer that:\n\n- **Converts JSON schemas to Python types**: The raw MCP protocol uses JSON schemas, but FastMCP automatically converts these to Python dataclasses\n- **Provides structured constructors**: Instead of manually building dictionaries that match the schema, you get dataclass constructors that ensure correct structure\n- **Handles type conversion**: FastMCP takes care of converting between JSON representations and Python objects\n- **Runtime introspection**: You can inspect the generated dataclass fields to understand the expected structure\n\nWhen you implement an elicitation handler, FastMCP gives you a dataclass type that matches the server's schema, making it easy to create properly structured responses without having to manually parse JSON schemas.\n\n## Elicitation Handler\n\nProvide an `elicitation_handler` function when creating the client. FastMCP automatically converts the server's JSON schema into a Python dataclass type, making it easy to construct the response:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.elicitation import ElicitResult\n\nasync def elicitation_handler(message: str, response_type: type, params, context):\n    # Present the message to the user and collect input\n    user_input = input(f\"{message}: \")\n    \n    # Create response using the provided dataclass type\n    # FastMCP converted the JSON schema to this Python type for you\n    response_data = response_type(value=user_input)\n    \n    # You can return data directly - FastMCP will implicitly accept the elicitation\n    return response_data\n    \n    # Or explicitly return an ElicitResult for more control\n    # return ElicitResult(action=\"accept\", content=response_data)\n\nclient = Client(\n    \"my_mcp_server.py\",\n    elicitation_handler=elicitation_handler,\n)\n```\n\n### Handler Parameters\n\nThe elicitation handler receives four parameters:\n\n<Card icon=\"code\" title=\"Elicitation Handler Parameters\">\n<ResponseField name=\"message\" type=\"str\">\n  The prompt message to display to the user\n</ResponseField>\n\n<ResponseField name=\"response_type\" type=\"type\">\n  A Python dataclass type that FastMCP created from the server's JSON schema. Use this to construct your response with proper typing and IDE support. If the server requests an empty object (indicating no response), this will be `None`.\n</ResponseField>\n\n<ResponseField name=\"params\" type=\"ElicitRequestParams\">\n  The original MCP elicitation request parameters, including the raw JSON schema in `params.requestedSchema` if you need it\n</ResponseField>\n\n<ResponseField name=\"context\" type=\"RequestContext\">\n  Request context containing metadata about the elicitation request\n</ResponseField>\n</Card>\n\n### Response Actions\n\nThe handler can return data directly (which implicitly accepts the elicitation) or an `ElicitResult` object for more control over the response action:\n\n<Card icon=\"code\" title=\"ElicitResult Structure\">\n<ResponseField name=\"action\" type=\"Literal['accept', 'decline', 'cancel']\">\n  How the user responded to the elicitation request\n</ResponseField>\n\n<ResponseField name=\"content\" type=\"dataclass instance | dict | None\">\n  The user's input data (required for \"accept\", omitted for \"decline\"/\"cancel\")\n</ResponseField>\n</Card>\n\n**Action Types:**\n- **`accept`**: User provided valid input - include their data in the `content` field\n- **`decline`**: User chose not to provide the requested information - omit `content`  \n- **`cancel`**: User cancelled the entire operation - omit `content`\n\n## Basic Example\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.elicitation import ElicitResult\n\nasync def basic_elicitation_handler(message: str, response_type: type, params, context):\n    print(f\"Server asks: {message}\")\n    \n    # Simple text input for demonstration\n    user_response = input(\"Your response: \")\n    \n    if not user_response:\n        # For non-acceptance, use ElicitResult explicitly\n        return ElicitResult(action=\"decline\")\n    \n    # Use the response_type dataclass to create a properly structured response\n    # FastMCP handles the conversion from JSON schema to Python type\n    # Return data directly - FastMCP will implicitly accept the elicitation\n    return response_type(value=user_response)\n\nclient = Client(\n    \"my_mcp_server.py\", \n    elicitation_handler=basic_elicitation_handler\n)\n```\n\n\n"
  },
  {
    "path": "docs/v2/clients/logging.mdx",
    "content": "---\ntitle: Server Logging\nsidebarTitle: Logging\ndescription: Receive and handle log messages from MCP servers.\nicon: receipt\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nMCP servers can emit log messages to clients. The client can handle these logs through a log handler callback.\n\n## Log Handler\n\nProvide a `log_handler` function when creating the client. For robust logging, the log messages can be integrated with Python's standard `logging` module.\n\n```python\nimport logging\nfrom fastmcp import Client\nfrom fastmcp.client.logging import LogMessage\n\n# In a real app, you might configure this in your main entry point\nlogging.basicConfig(\n    level=logging.INFO,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'\n)\n\n# Get a logger for the module where the client is used\nlogger = logging.getLogger(__name__)\n\n# This mapping is useful for converting MCP level strings to Python's levels\nLOGGING_LEVEL_MAP = logging.getLevelNamesMapping()\n\nasync def log_handler(message: LogMessage):\n    \"\"\"\n    Handles incoming logs from the MCP server and forwards them\n    to the standard Python logging system.\n    \"\"\"\n    msg = message.data.get('msg')\n    extra = message.data.get('extra')\n\n    # Convert the MCP log level to a Python log level\n    level = LOGGING_LEVEL_MAP.get(message.level.upper(), logging.INFO)\n\n    # Log the message using the standard logging library\n    logger.log(level, msg, extra=extra)\n\n\nclient = Client(\n    \"my_mcp_server.py\",\n    log_handler=log_handler,\n)\n```\n\n## Handling Structured Logs\n\nThe `message.data` attribute is a dictionary that contains the log payload from the server. This enables structured logging, allowing you to receive rich, contextual information.\n\nThe dictionary contains two keys:\n- `msg`: The string log message.\n- `extra`: A dictionary containing any extra data sent from the server.\n\nThis structure is preserved even when logs are forwarded through a FastMCP proxy, making it a powerful tool for debugging complex, multi-server applications.\n\n### Handler Parameters\n\nThe `log_handler` is called every time a log message is received. It receives a `LogMessage` object:\n\n<Card icon=\"code\" title=\"Log Handler Parameters\">\n<ResponseField name=\"LogMessage\" type=\"Log Message Object\">\n  <Expandable title=\"attributes\">\n    <ResponseField name=\"level\" type='Literal[\"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\", \"alert\", \"emergency\"]'>\n      The log level\n    </ResponseField>\n\n    <ResponseField name=\"logger\" type=\"str | None\">\n      The logger name (optional, may be None)\n    </ResponseField>\n\n    <ResponseField name=\"data\" type=\"dict\">\n      The log payload, containing `msg` and `extra` keys.\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n```python\nasync def detailed_log_handler(message: LogMessage):\n    msg = message.data.get('msg')\n    extra = message.data.get('extra')\n\n    if message.level == \"error\":\n        print(f\"ERROR: {msg} | Details: {extra}\")\n    elif message.level == \"warning\":\n        print(f\"WARNING: {msg} | Details: {extra}\")\n    else:\n        print(f\"{message.level.upper()}: {msg}\")\n```\n\n## Default Log Handling\n\nIf you don't provide a custom `log_handler`, FastMCP's default handler routes server logs to the appropriate Python logging levels. The MCP levels are mapped as follows: `notice` → INFO; `alert` and `emergency` → CRITICAL. If the server includes a logger name, it is prefixed in the message, and any `extra` data is forwarded via the logging `extra` parameter.\n\n```python\nclient = Client(\"my_mcp_server.py\")\n\nasync with client:\n    # Server logs are forwarded at their proper severity (DEBUG/INFO/WARNING/ERROR/CRITICAL)\n    await client.call_tool(\"some_tool\")\n```\n"
  },
  {
    "path": "docs/v2/clients/messages.mdx",
    "content": "---\ntitle: Message Handling\nsidebarTitle: Messages\ndescription: Handle MCP messages, requests, and notifications with custom message handlers.\nicon: envelope\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.9.1\" />\n\nMCP clients can receive various types of messages from servers, including requests that need responses and notifications that don't. The message handler provides a unified way to process all these messages.\n\n## Function-Based Handler\n\nThe simplest way to handle messages is with a function that receives all messages:\n\n```python\nfrom fastmcp import Client\n\nasync def message_handler(message):\n    \"\"\"Handle all MCP messages from the server.\"\"\"\n    if hasattr(message, 'root'):\n        method = message.root.method\n        print(f\"Received: {method}\")\n        \n        # Handle specific notifications\n        if method == \"notifications/tools/list_changed\":\n            print(\"Tools have changed - might want to refresh tool cache\")\n        elif method == \"notifications/resources/list_changed\":\n            print(\"Resources have changed\")\n\nclient = Client(\n    \"my_mcp_server.py\",\n    message_handler=message_handler,\n)\n```\n\n## Message Handler Class\n\nFor fine-grained targeting, FastMCP provides a `MessageHandler` class you can subclass to take advantage of specific hooks:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.messages import MessageHandler\nimport mcp.types\n\nclass MyMessageHandler(MessageHandler):\n    async def on_tool_list_changed(\n        self, notification: mcp.types.ToolListChangedNotification\n    ) -> None:\n        \"\"\"Handle tool list changes specifically.\"\"\"\n        print(\"Tool list changed - refreshing available tools\")\n\nclient = Client(\n    \"my_mcp_server.py\",\n    message_handler=MyMessageHandler(),\n)\n```\n\n### Available Handler Methods\n\nAll handler methods receive a single argument - the specific message type:\n\n<Card icon=\"code\" title=\"Message Handler Methods\">\n<ResponseField name=\"on_message(message)\" type=\"Any MCP message\">\n  Called for ALL messages (requests and notifications)\n</ResponseField>\n\n<ResponseField name=\"on_request(request)\" type=\"mcp.types.ClientRequest\">\n  Called for requests that expect responses\n</ResponseField>\n\n<ResponseField name=\"on_notification(notification)\" type=\"mcp.types.ServerNotification\">\n  Called for notifications (fire-and-forget)\n</ResponseField>\n\n<ResponseField name=\"on_tool_list_changed(notification)\" type=\"mcp.types.ToolListChangedNotification\">\n  Called when the server's tool list changes\n</ResponseField>\n\n<ResponseField name=\"on_resource_list_changed(notification)\" type=\"mcp.types.ResourceListChangedNotification\">\n  Called when the server's resource list changes\n</ResponseField>\n\n<ResponseField name=\"on_prompt_list_changed(notification)\" type=\"mcp.types.PromptListChangedNotification\">\n  Called when the server's prompt list changes\n</ResponseField>\n\n<ResponseField name=\"on_progress(notification)\" type=\"mcp.types.ProgressNotification\">\n  Called for progress updates during long-running operations\n</ResponseField>\n\n<ResponseField name=\"on_logging_message(notification)\" type=\"mcp.types.LoggingMessageNotification\">\n  Called for log messages from the server\n</ResponseField>\n</Card>\n\n## Example: Handling Tool Changes\n\nHere's a practical example of handling tool list changes:\n\n```python\nfrom fastmcp.client.messages import MessageHandler\nimport mcp.types\n\nclass ToolCacheHandler(MessageHandler):\n    def __init__(self):\n        self.cached_tools = []\n    \n    async def on_tool_list_changed(\n        self, notification: mcp.types.ToolListChangedNotification\n    ) -> None:\n        \"\"\"Clear tool cache when tools change.\"\"\"\n        print(\"Tools changed - clearing cache\")\n        self.cached_tools = []  # Force refresh on next access\n\nclient = Client(\"server.py\", message_handler=ToolCacheHandler())\n```\n\n## Handling Requests\n\nWhile the message handler receives server-initiated requests, for most use cases you should use the dedicated callback parameters instead:\n\n- **Sampling requests**: Use [`sampling_handler`](/v2/clients/sampling)\n- **Progress requests**: Use [`progress_handler`](/v2/clients/progress)  \n- **Log requests**: Use [`log_handler`](/v2/clients/logging)\n\nThe message handler is primarily for monitoring and handling notifications rather than responding to requests."
  },
  {
    "path": "docs/v2/clients/progress.mdx",
    "content": "---\ntitle: Progress Monitoring\nsidebarTitle: Progress\ndescription: Handle progress notifications from long-running server operations.\nicon: bars-progress\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.3.5\" />\n\nMCP servers can report progress during long-running operations. The client can receive these updates through a progress handler.\n\n## Progress Handler\n\nSet a progress handler when creating the client:\n\n```python\nfrom fastmcp import Client\n\nasync def my_progress_handler(\n    progress: float, \n    total: float | None, \n    message: str | None\n) -> None:\n    if total is not None:\n        percentage = (progress / total) * 100\n        print(f\"Progress: {percentage:.1f}% - {message or ''}\")\n    else:\n        print(f\"Progress: {progress} - {message or ''}\")\n\nclient = Client(\n    \"my_mcp_server.py\",\n    progress_handler=my_progress_handler\n)\n```\n\n### Handler Parameters\n\nThe progress handler receives three parameters:\n\n\n<Card icon=\"code\" title=\"Progress Handler Parameters\">\n<ResponseField name=\"progress\" type=\"float\">\n  Current progress value\n</ResponseField>\n\n<ResponseField name=\"total\" type=\"float | None\">\n  Expected total value (may be None)\n</ResponseField>\n\n<ResponseField name=\"message\" type=\"str | None\">\n  Optional status message (may be None)\n</ResponseField>\n</Card>\n\n\n## Per-Call Progress Handler\n\nOverride the progress handler for specific tool calls:\n\n```python\nasync with client:\n    # Override with specific progress handler for this call\n    result = await client.call_tool(\n        \"long_running_task\", \n        {\"param\": \"value\"}, \n        progress_handler=my_progress_handler\n    )\n```\n"
  },
  {
    "path": "docs/v2/clients/prompts.mdx",
    "content": "---\ntitle: Prompts\nsidebarTitle: Prompts\ndescription: Use server-side prompt templates with automatic argument serialization.\nicon: message-lines\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nPrompts are reusable message templates exposed by MCP servers. They can accept arguments to generate personalized message sequences for LLM interactions.\n\n## Listing Prompts\n\nUse `list_prompts()` to retrieve all available prompt templates:\n\n```python\nasync with client:\n    prompts = await client.list_prompts()\n    # prompts -> list[mcp.types.Prompt]\n    \n    for prompt in prompts:\n        print(f\"Prompt: {prompt.name}\")\n        print(f\"Description: {prompt.description}\")\n        if prompt.arguments:\n            print(f\"Arguments: {[arg.name for arg in prompt.arguments]}\")\n        # Access tags and other metadata\n        if hasattr(prompt, '_meta') and prompt._meta:\n            fastmcp_meta = prompt._meta.get('_fastmcp', {})\n            print(f\"Tags: {fastmcp_meta.get('tags', [])}\")\n```\n\n### Filtering by Tags\n\n<VersionBadge version=\"2.11.0\" />\n\nYou can use the `meta` field to filter prompts based on their tags:\n\n```python\nasync with client:\n    prompts = await client.list_prompts()\n    \n    # Filter prompts by tag\n    analysis_prompts = [\n        prompt for prompt in prompts \n        if hasattr(prompt, '_meta') and prompt._meta and\n           prompt._meta.get('_fastmcp', {}) and\n           'analysis' in prompt._meta.get('_fastmcp', {}).get('tags', [])\n    ]\n    \n    print(f\"Found {len(analysis_prompts)} analysis prompts\")\n```\n\n<Note>\nThe `_meta` field is part of the standard MCP specification. FastMCP servers include tags and other metadata within a `_fastmcp` namespace (e.g., `_meta._fastmcp.tags`) to avoid conflicts with user-defined metadata. This behavior can be controlled with the server's `include_fastmcp_meta` setting - when disabled, the `_fastmcp` namespace won't be included. Other MCP server implementations may not provide this metadata structure.\n</Note>\n\n## Using Prompts\n\n### Basic Usage\n\nRequest a rendered prompt using `get_prompt()` with the prompt name and arguments:\n\n```python\nasync with client:\n    # Simple prompt without arguments\n    result = await client.get_prompt(\"welcome_message\")\n    # result -> mcp.types.GetPromptResult\n    \n    # Access the generated messages\n    for message in result.messages:\n        print(f\"Role: {message.role}\")\n        print(f\"Content: {message.content}\")\n```\n\n### Prompts with Arguments\n\nPass arguments as a dictionary to customize the prompt:\n\n```python\nasync with client:\n    # Prompt with simple arguments\n    result = await client.get_prompt(\"user_greeting\", {\n        \"name\": \"Alice\",\n        \"role\": \"administrator\"\n    })\n    \n    # Access the personalized messages\n    for message in result.messages:\n        print(f\"Generated message: {message.content}\")\n```\n\n## Automatic Argument Serialization\n\n<VersionBadge version=\"2.9.0\" />\n\nFastMCP automatically serializes complex arguments to JSON strings as required by the MCP specification. This allows you to pass typed objects directly:\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass UserData:\n    name: str\n    age: int\n\nasync with client:\n    # Complex arguments are automatically serialized\n    result = await client.get_prompt(\"analyze_user\", {\n        \"user\": UserData(name=\"Alice\", age=30),     # Automatically serialized to JSON\n        \"preferences\": {\"theme\": \"dark\"},           # Dict serialized to JSON string\n        \"scores\": [85, 92, 78],                     # List serialized to JSON string\n        \"simple_name\": \"Bob\"                        # Strings passed through unchanged\n    })\n```\n\nThe client handles serialization using `pydantic_core.to_json()` for consistent formatting. FastMCP servers can automatically deserialize these JSON strings back to the expected types.\n\n### Serialization Examples\n\n```python\nasync with client:\n    result = await client.get_prompt(\"data_analysis\", {\n        # These will be automatically serialized to JSON strings:\n        \"config\": {\n            \"format\": \"csv\",\n            \"include_headers\": True,\n            \"delimiter\": \",\"\n        },\n        \"filters\": [\n            {\"field\": \"age\", \"operator\": \">\", \"value\": 18},\n            {\"field\": \"status\", \"operator\": \"==\", \"value\": \"active\"}\n        ],\n        # This remains a string:\n        \"report_title\": \"Monthly Analytics Report\"\n    })\n```\n\n## Working with Prompt Results\n\nThe `get_prompt()` method returns a `GetPromptResult` object containing a list of messages:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"conversation_starter\", {\"topic\": \"climate\"})\n    \n    # Access individual messages\n    for i, message in enumerate(result.messages):\n        print(f\"Message {i + 1}:\")\n        print(f\"  Role: {message.role}\")\n        print(f\"  Content: {message.content.text if hasattr(message.content, 'text') else message.content}\")\n```\n\n## Raw MCP Protocol Access\n\nFor access to the complete MCP protocol objects, use the `*_mcp` methods:\n\n```python\nasync with client:\n    # Raw MCP method returns full protocol object\n    prompts_result = await client.list_prompts_mcp()\n    # prompts_result -> mcp.types.ListPromptsResult\n    \n    prompt_result = await client.get_prompt_mcp(\"example_prompt\", {\"arg\": \"value\"})\n    # prompt_result -> mcp.types.GetPromptResult\n```\n\n## Multi-Server Clients\n\nWhen using multi-server clients, prompts are accessible without prefixing (unlike tools):\n\n```python\nasync with client:  # Multi-server client\n    # Prompts from any server are directly accessible\n    result1 = await client.get_prompt(\"weather_prompt\", {\"city\": \"London\"})\n    result2 = await client.get_prompt(\"assistant_prompt\", {\"query\": \"help\"})\n```\n\n## Common Prompt Patterns\n\n### System Messages\n\nMany prompts generate system messages for LLM configuration:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"system_configuration\", {\n        \"role\": \"helpful assistant\",\n        \"expertise\": \"python programming\"\n    })\n    \n    # Access the returned messages\n    message = result.messages[0]\n    print(f\"Prompt: {message.content}\")\n```\n\n### Conversation Templates\n\nPrompts can generate multi-turn conversation templates:\n\n```python\nasync with client:\n    result = await client.get_prompt(\"interview_template\", {\n        \"candidate_name\": \"Alice\",\n        \"position\": \"Senior Developer\"\n    })\n    \n    # Multiple messages for a conversation flow\n    for message in result.messages:\n        print(f\"{message.role}: {message.content}\")\n```\n\n<Tip>\nPrompt arguments and their expected types depend on the specific prompt implementation. Check the server's documentation or use `list_prompts()` to see available arguments for each prompt.\n</Tip>"
  },
  {
    "path": "docs/v2/clients/resources.mdx",
    "content": "---\ntitle: Resource Operations\nsidebarTitle: Resources\ndescription: Access static and templated resources from MCP servers.\nicon: folder-open\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nResources are data sources exposed by MCP servers. They can be static files or dynamic templates that generate content based on parameters.\n\n## Types of Resources\n\nMCP servers expose two types of resources:\n\n- **Static Resources**: Fixed content accessible via URI (e.g., configuration files, documentation)\n- **Resource Templates**: Dynamic resources that accept parameters to generate content (e.g., API endpoints, database queries)\n\n## Listing Resources\n\n### Static Resources\n\nUse `list_resources()` to retrieve all static resources available on the server:\n\n```python\nasync with client:\n    resources = await client.list_resources()\n    # resources -> list[mcp.types.Resource]\n    \n    for resource in resources:\n        print(f\"Resource URI: {resource.uri}\")\n        print(f\"Name: {resource.name}\")\n        print(f\"Description: {resource.description}\")\n        print(f\"MIME Type: {resource.mimeType}\")\n        # Access tags and other metadata\n        if hasattr(resource, '_meta') and resource._meta:\n            fastmcp_meta = resource._meta.get('_fastmcp', {})\n            print(f\"Tags: {fastmcp_meta.get('tags', [])}\")\n```\n\n### Resource Templates\n\nUse `list_resource_templates()` to retrieve available resource templates:\n\n```python\nasync with client:\n    templates = await client.list_resource_templates()\n    # templates -> list[mcp.types.ResourceTemplate]\n    \n    for template in templates:\n        print(f\"Template URI: {template.uriTemplate}\")\n        print(f\"Name: {template.name}\")\n        print(f\"Description: {template.description}\")\n        # Access tags and other metadata\n        if hasattr(template, '_meta') and template._meta:\n            fastmcp_meta = template._meta.get('_fastmcp', {})\n            print(f\"Tags: {fastmcp_meta.get('tags', [])}\")\n```\n\n### Filtering by Tags\n\n<VersionBadge version=\"2.11.0\" />\n\nYou can use the `meta` field to filter resources based on their tags:\n\n```python\nasync with client:\n    resources = await client.list_resources()\n    \n    # Filter resources by tag\n    config_resources = [\n        resource for resource in resources \n        if hasattr(resource, '_meta') and resource._meta and\n           resource._meta.get('_fastmcp', {}) and\n           'config' in resource._meta.get('_fastmcp', {}).get('tags', [])\n    ]\n    \n    print(f\"Found {len(config_resources)} config resources\")\n```\n\n<Note>\nThe `_meta` field is part of the standard MCP specification. FastMCP servers include tags and other metadata within a `_fastmcp` namespace (e.g., `_meta._fastmcp.tags`) to avoid conflicts with user-defined metadata. This behavior can be controlled with the server's `include_fastmcp_meta` setting - when disabled, the `_fastmcp` namespace won't be included. Other MCP server implementations may not provide this metadata structure.\n</Note>\n\n## Reading Resources\n\n### Static Resources\n\nRead a static resource using its URI:\n\n```python\nasync with client:\n    # Read a static resource\n    content = await client.read_resource(\"file:///path/to/README.md\")\n    # content -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]\n    \n    # Access text content\n    if hasattr(content[0], 'text'):\n        print(content[0].text)\n    \n    # Access binary content\n    if hasattr(content[0], 'blob'):\n        print(f\"Binary data: {len(content[0].blob)} bytes\")\n```\n\n### Resource Templates\n\nRead from a resource template by providing the URI with parameters:\n\n```python\nasync with client:\n    # Read a resource generated from a template\n    # For example, a template like \"weather://{{city}}/current\"\n    weather_content = await client.read_resource(\"weather://london/current\")\n    \n    # Access the generated content\n    print(weather_content[0].text)  # Assuming text JSON response\n```\n\n## Content Types\n\nResources can return different content types:\n\n### Text Resources\n\n```python\nasync with client:\n    content = await client.read_resource(\"resource://config/settings.json\")\n    \n    for item in content:\n        if hasattr(item, 'text'):\n            print(f\"Text content: {item.text}\")\n            print(f\"MIME type: {item.mimeType}\")\n```\n\n### Binary Resources\n\n```python\nasync with client:\n    content = await client.read_resource(\"resource://images/logo.png\")\n    \n    for item in content:\n        if hasattr(item, 'blob'):\n            print(f\"Binary content: {len(item.blob)} bytes\")\n            print(f\"MIME type: {item.mimeType}\")\n            \n            # Save to file\n            with open(\"downloaded_logo.png\", \"wb\") as f:\n                f.write(item.blob)\n```\n\n## Working with Multi-Server Clients\n\nWhen using multi-server clients, resource URIs are automatically prefixed with the server name:\n\n```python\nasync with client:  # Multi-server client\n    # Access resources from different servers\n    weather_icons = await client.read_resource(\"weather://weather/icons/sunny\")\n    templates = await client.read_resource(\"resource://assistant/templates/list\")\n    \n    print(f\"Weather icon: {weather_icons[0].blob}\")\n    print(f\"Templates: {templates[0].text}\")\n```\n\n## Raw MCP Protocol Access\n\nFor access to the complete MCP protocol objects, use the `*_mcp` methods:\n\n```python\nasync with client:\n    # Raw MCP methods return full protocol objects\n    resources_result = await client.list_resources_mcp()\n    # resources_result -> mcp.types.ListResourcesResult\n    \n    templates_result = await client.list_resource_templates_mcp()\n    # templates_result -> mcp.types.ListResourceTemplatesResult\n    \n    content_result = await client.read_resource_mcp(\"resource://example\")\n    # content_result -> mcp.types.ReadResourceResult\n```\n\n## Common Resource URI Patterns\n\nDifferent MCP servers may use various URI schemes:\n\n```python\n# File system resources\n\"file:///path/to/file.txt\"\n\n# Custom protocol resources  \n\"weather://london/current\"\n\"database://users/123\"\n\n# Generic resource protocol\n\"resource://config/settings\"\n\"resource://templates/email\"\n```\n\n<Tip>\nResource URIs and their formats depend on the specific MCP server implementation. Check the server's documentation for available resources and their URI patterns.\n</Tip>"
  },
  {
    "path": "docs/v2/clients/roots.mdx",
    "content": "---\ntitle: Client Roots\nsidebarTitle: Roots\ndescription: Provide local context and resource boundaries to MCP servers.\nicon: folder-tree\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nRoots are a way for clients to inform servers about the resources they have access to. Servers can use this information to adjust behavior or provide more relevant responses.\n\n## Setting Static Roots\n\nProvide a list of roots when creating the client:\n\n<CodeGroup>\n```python Static Roots\nfrom fastmcp import Client\n\nclient = Client(\n    \"my_mcp_server.py\", \n    roots=[\"/path/to/root1\", \"/path/to/root2\"]\n)\n```\n\n```python Dynamic Roots Callback\nfrom fastmcp import Client\nfrom fastmcp.client.roots import RequestContext\n\nasync def roots_callback(context: RequestContext) -> list[str]:\n    print(f\"Server requested roots (Request ID: {context.request_id})\")\n    return [\"/path/to/root1\", \"/path/to/root2\"]\n\nclient = Client(\n    \"my_mcp_server.py\", \n    roots=roots_callback\n)\n```\n</CodeGroup>\n\n"
  },
  {
    "path": "docs/v2/clients/sampling.mdx",
    "content": "---\ntitle: LLM Sampling\nsidebarTitle: Sampling\ndescription: Handle server-initiated LLM sampling requests.\nicon: robot\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.0.0\" />\n\nMCP servers can request LLM completions from clients. The client handles these requests through a sampling handler callback.\n\n## Sampling Handler\n\nProvide a `sampling_handler` function when creating the client:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling import (\n    SamplingMessage,\n    SamplingParams,\n    RequestContext,\n)\n\nasync def sampling_handler(\n    messages: list[SamplingMessage],\n    params: SamplingParams,\n    context: RequestContext\n) -> str:\n    # Your LLM integration logic here\n    # Extract text from messages and generate a response\n    return \"Generated response based on the messages\"\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=sampling_handler,\n)\n```\n\n### Handler Parameters\n\nThe sampling handler receives three parameters:\n\n<Card icon=\"code\" title=\"Sampling Handler Parameters\">\n<ResponseField name=\"SamplingMessage\" type=\"Sampling Message Object\">\n  <Expandable title=\"attributes\">\n    <ResponseField name=\"role\" type='Literal[\"user\", \"assistant\"]'>\n      The role of the message.\n    </ResponseField>\n\n    <ResponseField name=\"content\" type=\"TextContent | ImageContent | AudioContent\">\n      The content of the message.\n\n      TextContent is most common, and has a `.text` attribute.\n    </ResponseField>\n\n  </Expandable>\n</ResponseField>\n<ResponseField name=\"SamplingParams\" type=\"Sampling Parameters Object\">\n  <Expandable title=\"attributes\">\n    <ResponseField name=\"messages\" type=\"list[SamplingMessage]\">\n      The messages to sample from\n    </ResponseField>\n\n    <ResponseField name=\"modelPreferences\" type=\"ModelPreferences | None\">\n      The server's preferences for which model to select. The client MAY ignore\n    these preferences.\n    <Expandable title=\"attributes\">\n      <ResponseField name=\"hints\" type=\"list[ModelHint] | None\">\n        The hints to use for model selection.\n      </ResponseField>\n\n      <ResponseField name=\"costPriority\" type=\"float | None\">\n        The cost priority for model selection.\n      </ResponseField>\n\n      <ResponseField name=\"speedPriority\" type=\"float | None\">\n        The speed priority for model selection.\n      </ResponseField>\n\n      <ResponseField name=\"intelligencePriority\" type=\"float | None\">\n        The intelligence priority for model selection.\n      </ResponseField>\n    </Expandable>\n    </ResponseField>\n\n    <ResponseField name=\"systemPrompt\" type=\"str | None\">\n      An optional system prompt the server wants to use for sampling.\n    </ResponseField>\n\n    <ResponseField name=\"includeContext\" type=\"IncludeContext | None\">\n      A request to include context from one or more MCP servers (including the caller), to\n      be attached to the prompt.\n    </ResponseField>\n\n    <ResponseField name=\"temperature\" type=\"float | None\">\n      The sampling temperature.\n    </ResponseField>\n\n    <ResponseField name=\"maxTokens\" type=\"int\">\n      The maximum number of tokens to sample.\n    </ResponseField>\n\n    <ResponseField name=\"stopSequences\" type=\"list[str] | None\">\n      The stop sequences to use for sampling.\n    </ResponseField>\n\n    <ResponseField name=\"metadata\" type=\"dict[str, Any] | None\">\n      Optional metadata to pass through to the LLM provider.\n    </ResponseField>\n\n    <ResponseField name=\"tools\" type=\"list[Tool] | None\">\n      Optional list of tools the LLM can use during sampling. See [Using the OpenAI Handler](#using-the-openai-handler).\n    </ResponseField>\n\n    <ResponseField name=\"toolChoice\" type=\"ToolChoice | None\">\n      Optional control over tool usage behavior (`auto`, `required`, or `none`).\n    </ResponseField>\n    </Expandable>\n\n</ResponseField>\n<ResponseField name=\"RequestContext\" type=\"Request Context Object\">\n  <Expandable title=\"attributes\">\n    <ResponseField name=\"request_id\" type=\"RequestId\">\n      Unique identifier for the MCP request\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n## Basic Example\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext\n\nasync def basic_sampling_handler(\n    messages: list[SamplingMessage],\n    params: SamplingParams,\n    context: RequestContext\n) -> str:\n    # Extract message content\n    conversation = []\n    for message in messages:\n        content = message.content.text if hasattr(message.content, 'text') else str(message.content)\n        conversation.append(f\"{message.role}: {content}\")\n\n    # Use the system prompt if provided\n    system_prompt = params.systemPrompt or \"You are a helpful assistant.\"\n\n    # Here you would integrate with your preferred LLM service\n    # This is just a placeholder response\n    return f\"Response based on conversation: {' | '.join(conversation)}\"\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=basic_sampling_handler\n)\n```\n\n<Note>\nIf the client doesn't provide a sampling handler, servers can optionally configure a fallback handler. See [Server Sampling](/v2/servers/sampling#sampling-fallback-handler) for details.\n</Note>\n\n## Sampling Capabilities\n\nWhen you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support (for simpler handlers that don't support tools), pass `sampling_capabilities` explicitly:\n\n```python\nfrom mcp.types import SamplingCapability\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=basic_handler,\n    sampling_capabilities=SamplingCapability(),  # No tool support\n)\n```\n\n## Built-in Handlers\n\nFastMCP provides built-in sampling handlers for OpenAI and Anthropic APIs. These handlers support the full sampling API including tool use, handling message conversion and response formatting automatically.\n\n### OpenAI Handler\n\n<VersionBadge version=\"2.11.0\" />\n\nThe OpenAI handler works with OpenAI's API and any OpenAI-compatible provider:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=OpenAISamplingHandler(default_model=\"gpt-4o\"),\n)\n```\n\nFor OpenAI-compatible APIs (like local models), pass a custom client:\n\n```python\nfrom openai import AsyncOpenAI\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=OpenAISamplingHandler(\n        default_model=\"llama-3.1-70b\",\n        client=AsyncOpenAI(base_url=\"http://localhost:8000/v1\"),\n    ),\n)\n```\n\n<Note>\nInstall the OpenAI handler with `pip install fastmcp[openai]`.\n</Note>\n\n### Anthropic Handler\n\n<VersionBadge version=\"2.14.1\" />\n\nThe Anthropic handler uses Claude models via the Anthropic API:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=AnthropicSamplingHandler(default_model=\"claude-sonnet-4-5\"),\n)\n```\n\nYou can pass a custom client for advanced configuration:\n\n```python\nfrom anthropic import AsyncAnthropic\n\nclient = Client(\n    \"my_mcp_server.py\",\n    sampling_handler=AnthropicSamplingHandler(\n        default_model=\"claude-sonnet-4-5\",\n        client=AsyncAnthropic(),  # Uses ANTHROPIC_API_KEY env var\n    ),\n)\n```\n\n<Note>\nInstall the Anthropic handler with `pip install fastmcp[anthropic]`.\n</Note>\n\n### Tool Execution\n\nTool 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.\n\n<Tip>\nTo 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.\n</Tip>"
  },
  {
    "path": "docs/v2/clients/tasks.mdx",
    "content": "---\ntitle: Background Tasks\nsidebarTitle: Background Tasks\ndescription: Execute operations asynchronously and track their progress\nicon: clock\ntag: \"NEW\"\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.14.0\" />\n\nThe [MCP task protocol](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) lets you request operations to run asynchronously. This returns a Task object immediately, letting you track progress, cancel operations, or await results.\n\nSee [Server Background Tasks](/v2/servers/tasks) for how to enable this on the server side.\n\n## Requesting Background Execution\n\nPass `task=True` to run an operation as a background task. The call returns immediately with a Task object while the work executes on the server.\n\n```python\nfrom fastmcp import Client\n\nasync with Client(server) as client:\n    # Start a background task\n    task = await client.call_tool(\"slow_computation\", {\"duration\": 10}, task=True)\n\n    print(f\"Task started: {task.task_id}\")\n\n    # Do other work while it runs...\n\n    # Get the result when ready\n    result = await task.result()\n```\n\nThis works with tools, resources, and prompts:\n\n```python\ntool_task = await client.call_tool(\"my_tool\", args, task=True)\nresource_task = await client.read_resource(\"file://large.txt\", task=True)\nprompt_task = await client.get_prompt(\"my_prompt\", args, task=True)\n```\n\n## Working with Task Objects\n\nAll task types share a common interface for retrieving results, checking status, and receiving updates.\n\nTo get the result, call `await task.result()` or simply `await task`. This blocks until the task completes and returns the result. You can also check status without blocking using `await task.status()`, which returns the current state (`\"working\"`, `\"completed\"`, `\"failed\"`, or `\"cancelled\"`) along with any progress message from the server.\n\n```python\ntask = await client.call_tool(\"analyze\", {\"text\": \"hello\"}, task=True)\n\n# Check current status (non-blocking)\nstatus = await task.status()\nprint(f\"{status.status}: {status.statusMessage}\")\n\n# Wait for result (blocking)\nresult = await task.result()\n```\n\nFor more control over waiting, use `task.wait()` with an optional timeout or target state:\n\n```python\n# Wait up to 30 seconds for completion\nstatus = await task.wait(timeout=30.0)\n\n# Wait for a specific state\nstatus = await task.wait(state=\"completed\", timeout=30.0)\n```\n\nTo cancel a running task, call `await task.cancel()`.\n\n### Real-Time Status Updates\n\nRegister callbacks to receive status updates as the server reports progress. Both sync and async callbacks are supported.\n\n```python\ndef on_status_change(status):\n    print(f\"Task {status.taskId}: {status.status} - {status.statusMessage}\")\n\ntask.on_status_change(on_status_change)\n\n# Async callbacks work too\nasync def on_status_async(status):\n    await log_status(status)\n\ntask.on_status_change(on_status_async)\n```\n\n## Graceful Degradation\n\nYou can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline. The Task API provides a consistent interface either way.\n\n```python\ntask = await client.call_tool(\"my_tool\", args, task=True)\n\nif task.returned_immediately:\n    print(\"Server executed immediately (no background support)\")\nelse:\n    print(\"Running in background\")\n\n# Either way, this works\nresult = await task.result()\n```\n\nThis means you can write task-aware client code without worrying about server capabilities.\n\n## Complete Example\n\n```python\nimport asyncio\nfrom fastmcp import Client\n\nasync def main():\n    async with Client(server) as client:\n        # Start background task\n        task = await client.call_tool(\n            \"slow_computation\",\n            {\"duration\": 10},\n            task=True,\n        )\n\n        # Subscribe to updates\n        def on_update(status):\n            print(f\"Progress: {status.statusMessage}\")\n\n        task.on_status_change(on_update)\n\n        # Do other work while task runs\n        print(\"Doing other work...\")\n        await asyncio.sleep(2)\n\n        # Wait for completion and get result\n        result = await task.result()\n        print(f\"Result: {result.content}\")\n\nasyncio.run(main())\n```\n"
  },
  {
    "path": "docs/v2/clients/tools.mdx",
    "content": "---\ntitle: Tool Operations\nsidebarTitle: Tools\ndescription: Discover and execute server-side tools with the FastMCP client.\nicon: wrench\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nTools are executable functions exposed by MCP servers. The FastMCP client provides methods to discover available tools and execute them with arguments.\n\n## Discovering Tools\n\nUse `list_tools()` to retrieve all tools available on the server:\n\n```python\nasync with client:\n    tools = await client.list_tools()\n    # tools -> list[mcp.types.Tool]\n    \n    for tool in tools:\n        print(f\"Tool: {tool.name}\")\n        print(f\"Description: {tool.description}\")\n        if tool.inputSchema:\n            print(f\"Parameters: {tool.inputSchema}\")\n        # Access tags and other metadata\n        if hasattr(tool, 'meta') and tool.meta:\n            fastmcp_meta = tool.meta.get('_fastmcp', {})\n            print(f\"Tags: {fastmcp_meta.get('tags', [])}\")\n```\n\n### Filtering by Tags\n\n<VersionBadge version=\"2.11.0\" />\n\nYou can use the `meta` field to filter tools based on their tags:\n\n```python\nasync with client:\n    tools = await client.list_tools()\n    \n    # Filter tools by tag\n    analysis_tools = [\n        tool for tool in tools \n        if hasattr(tool, 'meta') and tool.meta and\n           tool.meta.get('_fastmcp', {}) and\n           'analysis' in tool.meta.get('_fastmcp', {}).get('tags', [])\n    ]\n    \n    print(f\"Found {len(analysis_tools)} analysis tools\")\n```\n\n<Note>\nThe `meta` field is part of the standard MCP specification. FastMCP servers include tags and other metadata within a `_fastmcp` namespace (e.g., `meta._fastmcp.tags`) to avoid conflicts with user-defined metadata. This behavior can be controlled with the server's `include_fastmcp_meta` setting - when disabled, the `_fastmcp` namespace won't be included. Other MCP server implementations may not provide this metadata structure.\n</Note>\n\n## Executing Tools\n\n### Basic Execution\n\nExecute a tool using `call_tool()` with the tool name and arguments:\n\n```python\nasync with client:\n    # Simple tool call\n    result = await client.call_tool(\"add\", {\"a\": 5, \"b\": 3})\n    # result -> CallToolResult with structured and unstructured data\n    \n    # Access structured data (automatically deserialized)\n    print(result.data)  # 8 (int) or {\"result\": 8} for primitive types\n    \n    # Access traditional content blocks  \n    print(result.content[0].text)  # \"8\" (TextContent)\n```\n\n### Advanced Execution Options\n\nThe `call_tool()` method supports additional parameters for timeout control and progress monitoring:\n\n```python\nasync with client:\n    # With timeout (aborts if execution takes longer than 2 seconds)\n    result = await client.call_tool(\n        \"long_running_task\", \n        {\"param\": \"value\"}, \n        timeout=2.0\n    )\n    \n    # With progress handler (to track execution progress)\n    result = await client.call_tool(\n        \"long_running_task\",\n        {\"param\": \"value\"},\n        progress_handler=my_progress_handler\n    )\n```\n\n**Parameters:**\n- `name`: The tool name (string)\n- `arguments`: Dictionary of arguments to pass to the tool (optional)\n- `timeout`: Maximum execution time in seconds (optional, overrides client-level timeout)\n- `progress_handler`: Progress callback function (optional, overrides client-level handler)\n- `meta`: Dictionary of metadata to send with the request (optional, see below)\n\n## Sending Metadata\n\n<VersionBadge version=\"2.13.1\" />\n\nThe `meta` parameter sends ancillary information alongside tool calls. This can be used for various purposes like observability, debugging, client identification, or any context the server may need beyond the tool's primary arguments.\n\n```python\nasync with client:\n    result = await client.call_tool(\n        name=\"send_email\",\n        arguments={\n            \"to\": \"user@example.com\",\n            \"subject\": \"Hello\",\n            \"body\": \"Welcome!\"\n        },\n        meta={\n            \"trace_id\": \"abc-123\",\n            \"request_source\": \"mobile_app\"\n        }\n    )\n```\n\nThe structure and usage of `meta` is determined by your application. See [Client Metadata](/v2/servers/context#client-metadata) in the server documentation to learn how to access this data in your tool implementations.\n\n## Handling Results\n\n<VersionBadge version=\"2.10.0\" />\n\nTool execution returns a `CallToolResult` object with both structured and traditional content. FastMCP's standout feature is the `.data` property, which doesn't just provide raw JSON but actually hydrates complete Python objects including complex types like datetimes, UUIDs, and custom classes.\n\n### CallToolResult Properties\n\n<Card icon=\"code\" title=\"CallToolResult Properties\">\n<ResponseField name=\".data\" type=\"Any\">\n  **FastMCP exclusive**: Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). Goes beyond JSON to provide complete object reconstruction from output schemas.\n</ResponseField>\n\n<ResponseField name=\".content\" type=\"list[mcp.types.ContentBlock]\">\n  Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.) available from all MCP servers.\n</ResponseField>\n\n<ResponseField name=\".structured_content\" type=\"dict[str, Any] | None\">\n  Standard MCP structured JSON data as sent by the server, available from all MCP servers that support structured outputs.\n</ResponseField>\n\n<ResponseField name=\".is_error\" type=\"bool\">\n  Boolean indicating if the tool execution failed.\n</ResponseField>\n</Card>\n\n### Structured Data Access\n\nFastMCP's `.data` property provides fully hydrated Python objects, not just JSON dictionaries. This includes complex type reconstruction:\n\n```python\nfrom datetime import datetime\nfrom uuid import UUID\n\nasync with client:\n    result = await client.call_tool(\"get_weather\", {\"city\": \"London\"})\n    \n    # FastMCP reconstructs complete Python objects from the server's output schema\n    weather = result.data  # Server-defined WeatherReport object\n    print(f\"Temperature: {weather.temperature}°C at {weather.timestamp}\")\n    print(f\"Station: {weather.station_id}\")\n    print(f\"Humidity: {weather.humidity}%\")\n    \n    # The timestamp is a real datetime object, not a string!\n    assert isinstance(weather.timestamp, datetime)\n    assert isinstance(weather.station_id, UUID)\n    \n    # Compare with raw structured JSON (standard MCP)\n    print(f\"Raw JSON: {result.structured_content}\")\n    # {\"temperature\": 20, \"timestamp\": \"2024-01-15T14:30:00Z\", \"station_id\": \"123e4567-...\"}\n    \n    # Traditional content blocks (standard MCP)  \n    print(f\"Text content: {result.content[0].text}\")\n```\n\n### Fallback Behavior\n\nFor tools without output schemas or when deserialization fails, `.data` will be `None`:\n\n```python\nasync with client:\n    result = await client.call_tool(\"legacy_tool\", {\"param\": \"value\"})\n    \n    if result.data is not None:\n        # Structured output available and successfully deserialized\n        print(f\"Structured: {result.data}\")\n    else:\n        # No structured output or deserialization failed - use content blocks\n        for content in result.content:\n            if hasattr(content, 'text'):\n                print(f\"Text result: {content.text}\")\n            elif hasattr(content, 'data'):\n                print(f\"Binary data: {len(content.data)} bytes\")\n```\n\n### Primitive Type Unwrapping\n\n<Tip>\nFastMCP servers automatically wrap non-object results (like `int`, `str`, `bool`) in a `{\"result\": value}` structure to create valid structured outputs. FastMCP clients understand this convention and automatically unwrap the value in `.data` for convenience, so you get the original primitive value instead of a wrapper object.\n</Tip>\n\n```python\nasync with client:\n    result = await client.call_tool(\"calculate_sum\", {\"a\": 5, \"b\": 3})\n    \n    # FastMCP client automatically unwraps for convenience\n    print(result.data)  # 8 (int) - the original value\n    \n    # Raw structured content shows the server-side wrapping\n    print(result.structured_content)  # {\"result\": 8}\n    \n    # Other MCP clients would need to manually access [\"result\"]\n    # value = result.structured_content[\"result\"]  # Not needed with FastMCP!\n```\n\n## Error Handling\n\n### Exception-Based Error Handling\n\nBy default, `call_tool()` raises a `ToolError` if the tool execution fails:\n\n```python\nfrom fastmcp.exceptions import ToolError\n\nasync with client:\n    try:\n        result = await client.call_tool(\"potentially_failing_tool\", {\"param\": \"value\"})\n        print(\"Tool succeeded:\", result.data)\n    except ToolError as e:\n        print(f\"Tool failed: {e}\")\n```\n\n### Manual Error Checking\n\nYou can disable automatic error raising and manually check the result:\n\n```python\nasync with client:\n    result = await client.call_tool(\n        \"potentially_failing_tool\", \n        {\"param\": \"value\"}, \n        raise_on_error=False\n    )\n    \n    if result.is_error:\n        print(f\"Tool failed: {result.content[0].text}\")\n    else:\n        print(f\"Tool succeeded: {result.data}\")\n```\n\n### Raw MCP Protocol Access\n\nFor complete control, use `call_tool_mcp()` which returns the raw MCP protocol object:\n\n```python\nasync with client:\n    result = await client.call_tool_mcp(\"potentially_failing_tool\", {\"param\": \"value\"})\n    # result -> mcp.types.CallToolResult\n    \n    if result.isError:\n        print(f\"Tool failed: {result.content}\")\n    else:\n        print(f\"Tool succeeded: {result.content}\")\n        # Note: No automatic deserialization with call_tool_mcp()\n```\n\n## Argument Handling\n\nArguments are passed as a dictionary to the tool:\n\n```python\nasync with client:\n    # Simple arguments\n    result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n    \n    # Complex arguments\n    result = await client.call_tool(\"process_data\", {\n        \"config\": {\"format\": \"json\", \"validate\": True},\n        \"items\": [1, 2, 3, 4, 5],\n        \"metadata\": {\"source\": \"api\", \"version\": \"1.0\"}\n    })\n```\n\n<Tip>\nFor multi-server clients, tool names are automatically prefixed with the server name (e.g., `weather_get_forecast` for a tool named `get_forecast` on the `weather` server).\n</Tip>"
  },
  {
    "path": "docs/v2/clients/transports.mdx",
    "content": "---\ntitle: Client Transports\nsidebarTitle: Transports\ndescription: Configure how FastMCP Clients connect to and communicate with servers.\nicon: link\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.0.0\" />\n\nThe FastMCP `Client` communicates with MCP servers through transport objects that handle the underlying connection mechanics. While the client can automatically select a transport based on what you pass to it, instantiating transports explicitly gives you full control over configuration—environment variables, authentication, session management, and more.\n\nThink of transports as configurable adapters between your client code and MCP servers. Each transport type handles a different communication pattern: subprocesses with pipes, HTTP connections, or direct in-memory calls.\n\n## Choosing the Right Transport\n\n- **Use [STDIO Transport](#stdio-transport)** when you need to run local MCP servers with full control over their environment and lifecycle\n- **Use [Remote Transports](#remote-transports)** when connecting to production services or shared MCP servers running independently  \n- **Use [In-Memory Transport](#in-memory-transport)** for testing FastMCP servers without subprocess or network overhead\n- **Use [MCP JSON Configuration](#mcp-json-configuration-transport)** when you need to connect to multiple servers defined in configuration files\n\n## STDIO Transport\n\nSTDIO (Standard Input/Output) transport communicates with MCP servers through subprocess pipes. This is the standard mechanism used by desktop clients like Claude Desktop and is the primary way to run local MCP servers.\n\n### The Client Runs the Server\n\n<Warning>\n**Critical Concept**: When using STDIO transport, your client actually launches and manages the server process. This is fundamentally different from network transports where you connect to an already-running server. Understanding this relationship is key to using STDIO effectively.\n</Warning>\n\nWith STDIO transport, your client:\n- Starts the server as a subprocess when you connect\n- Manages the server's lifecycle (start, stop, restart)\n- Controls the server's environment and configuration\n- Communicates through stdin/stdout pipes\n\nThis architecture enables powerful local integrations but requires understanding environment isolation and process management.\n\n### Environment Isolation\n\nSTDIO servers run in isolated environments by default. This is a security feature enforced by the MCP protocol to prevent accidental exposure of sensitive data.\n\nWhen your client launches an MCP server:\n- The server does NOT inherit your shell's environment variables\n- API keys, paths, and other configuration must be explicitly passed\n- The working directory and system paths may differ from your shell\n\nTo pass environment variables to your server, use the `env` parameter:\n\n```python\nfrom fastmcp import Client\n\n# If your server needs environment variables (like API keys),\n# you must explicitly pass them:\nclient = Client(\n    \"my_server.py\",\n    env={\"API_KEY\": \"secret\", \"DEBUG\": \"true\"}\n)\n\n# This won't work - the server runs in isolation:\n# export API_KEY=\"secret\"  # in your shell\n# client = Client(\"my_server.py\")  # server can't see API_KEY\n```\n\n### Basic Usage\n\nTo use STDIO transport, you create a transport instance with the command and arguments needed to run your server:\n\n```python\nfrom fastmcp.client.transports import StdioTransport\n\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"my_server.py\"]\n)\nclient = Client(transport)\n```\n\nYou can configure additional settings like environment variables, working directory, or command arguments:\n\n```python\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"my_server.py\", \"--verbose\"],\n    env={\"LOG_LEVEL\": \"DEBUG\"},\n    cwd=\"/path/to/server\"\n)\nclient = Client(transport)\n```\n\nFor convenience, the client can also infer STDIO transport from file paths, but this doesn't allow configuration:\n\n```python\nfrom fastmcp import Client\n\nclient = Client(\"my_server.py\")  # Limited - no configuration options\n```\n\n### Environment Variables\n\nSince STDIO servers don't inherit your environment, you need strategies for passing configuration. Here are two common approaches:\n\n**Selective forwarding** passes only the variables your server actually needs:\n\n```python\nimport os\nfrom fastmcp.client.transports import StdioTransport\n\nrequired_vars = [\"API_KEY\", \"DATABASE_URL\", \"REDIS_HOST\"]\nenv = {\n    var: os.environ[var] \n    for var in required_vars \n    if var in os.environ\n}\n\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"server.py\"],\n    env=env\n)\nclient = Client(transport)\n```\n\n**Loading from .env files** keeps configuration separate from code:\n\n```python\nfrom dotenv import dotenv_values\nfrom fastmcp.client.transports import StdioTransport\n\nenv = dotenv_values(\".env\")\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"server.py\"],\n    env=env\n)\nclient = Client(transport)\n```\n\n### Session Persistence\n\nSTDIO transports maintain sessions across multiple client contexts by default (`keep_alive=True`). This improves performance by reusing the same subprocess for multiple connections, but can be controlled when you need isolation.\n\nBy default, the subprocess persists between connections:\n\n```python\nfrom fastmcp.client.transports import StdioTransport\n\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"server.py\"]\n)\nclient = Client(transport)\n\nasync def efficient_multiple_operations():\n    async with client:\n        await client.ping()\n    \n    async with client:  # Reuses the same subprocess\n        await client.call_tool(\"process_data\", {\"file\": \"data.csv\"})\n```\n\nFor complete isolation between connections, disable session persistence:\n\n```python\ntransport = StdioTransport(\n    command=\"python\",\n    args=[\"server.py\"],\n    keep_alive=False\n)\nclient = Client(transport)\n```\n\nUse `keep_alive=False` when you need complete isolation (e.g., in test suites) or when server state could cause issues between connections.\n\n### Specialized STDIO Transports\n\nFastMCP provides convenience transports that are thin wrappers around `StdioTransport` with pre-configured commands:\n\n- **`PythonStdioTransport`** - Uses `python` command for `.py` files\n- **`NodeStdioTransport`** - Uses `node` command for `.js` files  \n- **`UvStdioTransport`** - Uses `uv` for Python packages (uses `env_vars` parameter)\n- **`UvxStdioTransport`** - Uses `uvx` for Python packages (uses `env_vars` parameter)\n- **`NpxStdioTransport`** - Uses `npx` for Node packages (uses `env_vars` parameter)\n\nFor most use cases, instantiate `StdioTransport` directly with your desired command. These specialized transports are primarily useful for client inference shortcuts.\n\n## Remote Transports\n\nRemote transports connect to MCP servers running as web services. This is a fundamentally different model from STDIO transports—instead of your client launching and managing a server process, you connect to an already-running service that manages its own environment and lifecycle.\n\n### Streamable HTTP Transport\n\n<VersionBadge version=\"2.3.0\" />\n\nStreamable HTTP is the recommended transport for production deployments, providing efficient bidirectional streaming over HTTP connections.\n\n- **Class:** `StreamableHttpTransport`\n- **Server compatibility:** FastMCP servers running with `mcp run --transport http`\n\nThe transport requires a URL and optionally supports custom headers for authentication and configuration:\n\n```python\nfrom fastmcp.client.transports import StreamableHttpTransport\n\n# Basic connection\ntransport = StreamableHttpTransport(url=\"https://api.example.com/mcp\")\nclient = Client(transport)\n\n# With custom headers for authentication\ntransport = StreamableHttpTransport(\n    url=\"https://api.example.com/mcp\",\n    headers={\n        \"Authorization\": \"Bearer your-token-here\",\n        \"X-Custom-Header\": \"value\"\n    }\n)\nclient = Client(transport)\n```\n\nFor convenience, FastMCP also provides authentication helpers:\n\n```python\nfrom fastmcp.client.auth import BearerAuth\n\nclient = Client(\n    \"https://api.example.com/mcp\",\n    auth=BearerAuth(\"your-token-here\")\n)\n```\n\n### SSE Transport (Legacy)\n\nServer-Sent Events transport is maintained for backward compatibility but is superseded by Streamable HTTP for new deployments.\n\n- **Class:** `SSETransport`  \n- **Server compatibility:** FastMCP servers running with `mcp run --transport sse`\n\nSSE transport supports the same configuration options as Streamable HTTP:\n\n```python\nfrom fastmcp.client.transports import SSETransport\n\ntransport = SSETransport(\n    url=\"https://api.example.com/sse\",\n    headers={\"Authorization\": \"Bearer token\"}\n)\nclient = Client(transport)\n```\n\nUse Streamable HTTP for new deployments unless you have specific infrastructure requirements for SSE.\n\n## In-Memory Transport\n\nIn-memory transport connects directly to a FastMCP server instance within the same Python process. This eliminates both subprocess management and network overhead, making it ideal for testing and development.\n\n- **Class:** `FastMCPTransport`\n\n<Note>\nUnlike STDIO transports, in-memory servers have full access to your Python process's environment. They share the same memory space and environment variables as your client code—no isolation or explicit environment passing required.\n</Note>\n\n```python\nfrom fastmcp import FastMCP, Client\nimport os\n\nmcp = FastMCP(\"TestServer\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    prefix = os.environ.get(\"GREETING_PREFIX\", \"Hello\")\n    return f\"{prefix}, {name}!\"\n\nclient = Client(mcp)\n\nasync with client:\n    result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n```\n\n## MCP JSON Configuration Transport\n\n<VersionBadge version=\"2.4.0\" />\n\nThis transport supports the emerging MCP JSON configuration standard for defining multiple servers:\n\n- **Class:** `MCPConfigTransport`\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\"\n        },\n        \"assistant\": {\n            \"command\": \"python\",\n            \"args\": [\"./assistant.py\"],\n            \"env\": {\"LOG_LEVEL\": \"INFO\"}\n        }\n    }\n}\n\nclient = Client(config)\n\nasync with client:\n    # Tools are namespaced by server\n    weather = await client.call_tool(\"weather_get_forecast\", {\"city\": \"NYC\"})\n    answer = await client.call_tool(\"assistant_ask\", {\"question\": \"What?\"})\n```\n\n### Tool Transformation with FastMCP and MCPConfig\n\nFastMCP supports basic tool transformations to be defined alongside the MCP Servers in the MCPConfig file.\n\n```python\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\",\n            \"tools\": { }   #  <--- This is the tool transformation section\n        }\n    }\n}\n```\n\nWith these transformations, you can transform (change) the name, title, description, tags, enablement, and arguments of a tool.\n\nFor each argument the tool takes, you can transform (change) the name, description, default, visibility, whether it's required, and you can provide example values.\n\nIn the following example, we're transforming the `weather_get_forecast` tool to only retrieve the weather for `Miami` and hiding the `city` argument from the client.\n\n```python\ntool_transformations = {\n    \"weather_get_forecast\": {\n        \"name\": \"miami_weather\",\n        \"description\": \"Get the weather for Miami\",\n        \"arguments\": {\n            \"city\": {\n                \"name\": \"city\",\n                \"default\": \"Miami\",\n                \"hide\": True,\n            }\n        }\n    }\n}\n\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\",\n            \"tools\": tool_transformations\n        }\n    }\n}\n```\n\n#### Allowlisting and Blocklisting Tools\n\nTools can be allowlisted or blocklisted from the client by applying `tags` to the tools on the server. In the following example, we're allowlisting only tools marked with the `forecast` tag, all other tools will be unavailable to the client.\n\n```python\ntool_transformations = {\n    \"weather_get_forecast\": {\n        \"enabled\": True,\n        \"tags\": [\"forecast\"]\n    }\n}\n\n\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\",\n            \"tools\": tool_transformations,\n            \"include_tags\": [\"forecast\"]\n        }\n    }\n}\n```"
  },
  {
    "path": "docs/v2/community/showcase.mdx",
    "content": "---\ntitle: 'Community Showcase'\ndescription: 'High-quality projects and examples from the FastMCP community'\nicon: 'users'\n---\n\nimport { YouTubeEmbed } from '/snippets/youtube-embed.mdx'\n\n## Join the Community\n\n<Card title=\"FastMCP Discord\" icon=\"discord\" href=\"https://discord.gg/uu8dJCgttd\">\n  Connect with other FastMCP developers, share your projects, and discuss ideas.\n</Card>\n\n## Featured Projects\n\nDiscover exemplary MCP servers and implementations created by our community. These projects demonstrate best practices and innovative uses of FastMCP.\n\n### Learning Resources\n\n<Card title=\"MCP Dummy Server\" icon=\"graduation-cap\" href=\"https://github.com/WaiYanNyeinNaing/mcp-dummy-server\">\n  A comprehensive educational example demonstrating FastMCP best practices with professional dual-transport server implementation, interactive test client, and detailed documentation.\n</Card>\n\n#### Video Tutorials\n\n**Build Remote MCP Servers w/ Python & FastMCP** - Claude Integrations Tutorial by Greg + Code\n\n<YouTubeEmbed \n  videoId=\"bOYkbXP-GGo\" \n  title=\"Build Remote MCP Servers w/ Python & FastMCP\" \n/>\n\n**FastMCP — the best way to build an MCP server with Python** - Tutorial by ZazenCodes\n\n<YouTubeEmbed \n  videoId=\"rnljvmHorQw\" \n  title=\"FastMCP — the best way to build an MCP server with Python\" \n/>\n\n**Speedrun a MCP server for Claude Desktop (fastmcp)** - Tutorial by Nate from Prefect\n\n<YouTubeEmbed \n  videoId=\"67ZwpkUEtSI\" \n  title=\"Speedrun a MCP server for Claude Desktop (fastmcp)\" \n/>\n\n### Community Examples\n\nHave you built something interesting with FastMCP? We'd love to feature high-quality examples here! Start a [discussion on GitHub](https://github.com/PrefectHQ/fastmcp/discussions) to share your project.\n\n## Contributing\n\nTo get your project featured:\n\n1. Ensure your project demonstrates best practices\n2. Include comprehensive documentation\n3. Add clear usage examples\n4. Open a discussion in our [GitHub Discussions](https://github.com/PrefectHQ/fastmcp/discussions)\n\nWe review submissions regularly and feature projects that provide value to the FastMCP community.\n\n## Further Reading\n\n- [Contrib Modules](/v2/patterns/contrib) - Community-contributed modules that are distributed with FastMCP itself"
  },
  {
    "path": "docs/v2/deployment/http.mdx",
    "content": "---\ntitle: HTTP Deployment\nsidebarTitle: HTTP Deployment\ndescription: Deploy your FastMCP server over HTTP for remote access\nicon: server\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<Tip>\nSTDIO transport is perfect for local development and desktop applications. But to unlock the full potential of MCP—centralized services, multi-client access, and network availability—you need remote HTTP deployment.\n</Tip>\n\nThis guide walks you through deploying your FastMCP server as a remote MCP service that's accessible via a URL. Once deployed, your MCP server will be available over the network, allowing multiple clients to connect simultaneously and enabling integration with cloud-based LLM applications. This guide focuses specifically on remote MCP deployment, not local STDIO servers.\n\n## Choosing Your Approach\n\nFastMCP provides two ways to deploy your server as an HTTP service. Understanding the trade-offs helps you choose the right approach for your needs.\n\nThe **direct HTTP server** approach is simpler and perfect for getting started quickly. You modify your server's `run()` method to use HTTP transport, and FastMCP handles all the web server configuration. This approach works well for standalone deployments where you want your MCP server to be the only service running on a port.\n\nThe **ASGI application** approach gives you more control and flexibility. Instead of running the server directly, you create an ASGI application that can be served by Uvicorn. This approach is better when you need advanced server features like multiple workers, custom middleware, or when you're integrating with existing web applications.\n\n### Direct HTTP Server\n\nThe simplest way to get your MCP server online is to use the built-in `run()` method with HTTP transport. This approach handles all the server configuration for you and is ideal when you want a standalone MCP server without additional complexity.\n\n```python server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\ndef process_data(input: str) -> str:\n    \"\"\"Process data on the server\"\"\"\n    return f\"Processed: {input}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", host=\"0.0.0.0\", port=8000)\n```\n\nRun your server with a simple Python command:\n```bash\npython server.py\n```\n\nYour server is now accessible at `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).\n\nThis approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.\n\n### ASGI Application\n\nFor production deployments, you'll often want more control over how your server runs. FastMCP can create a standard ASGI application that works with any ASGI server like Uvicorn, Gunicorn, or Hypercorn. This approach is particularly useful when you need to configure advanced server options, run multiple workers, or integrate with existing infrastructure.\n\n```python app.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\ndef process_data(input: str) -> str:\n    \"\"\"Process data on the server\"\"\"\n    return f\"Processed: {input}\"\n\n# Create ASGI application\napp = mcp.http_app()\n```\n\nRun with any ASGI server - here's an example with Uvicorn:\n```bash\nuvicorn app:app --host 0.0.0.0 --port 8000\n```\n\nYour server is accessible at the same URL: `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).\n\nThe ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application.\n\n## Configuring Your Server\n\n### Custom Path\n\nBy default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.\n\n```python\n# Option 1: With mcp.run()\nmcp.run(transport=\"http\", host=\"0.0.0.0\", port=8000, path=\"/api/mcp/\")\n\n# Option 2: With ASGI app\napp = mcp.http_app(path=\"/api/mcp/\")\n```\n\nNow your server is accessible at `http://localhost:8000/api/mcp/`.\n\n### Authentication\n\n<Warning>\nAuthentication is **highly recommended** for remote MCP servers. Some LLM clients require authentication for remote servers and will refuse to connect without it.\n</Warning>\n\nFastMCP supports multiple authentication methods to secure your remote server. See the [Authentication Overview](/v2/servers/auth/authentication) for complete configuration options including Bearer tokens, JWT, and OAuth.\n\nIf you're mounting an authenticated server under a path prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) below for important routing considerations.\n\n### Health Checks\n\nHealth check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.\n\n```python\nfrom starlette.responses import JSONResponse\n\n@mcp.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check(request):\n    return JSONResponse({\"status\": \"healthy\", \"service\": \"mcp-server\"})\n```\n\nThis health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.\n\n### Custom Middleware\n\n\n<VersionBadge version=\"2.3.2\" />\n\nAdd custom Starlette middleware to your FastMCP ASGI apps:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.cors import CORSMiddleware\n\n# Create your FastMCP server\nmcp = FastMCP(\"MyServer\")\n\n# Define middleware\nmiddleware = [\n    Middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n]\n\n# Create ASGI app with middleware\nhttp_app = mcp.http_app(middleware=middleware)\n```\n\n### CORS for Browser-Based Clients\n\n<Tip>\nMost MCP clients, including those that you access through a browser like ChatGPT or Claude, don't need CORS configuration. Only enable CORS if you're working with an MCP client that connects directly from a browser, such as debugging tools or inspectors.\n</Tip>\n\nCORS (Cross-Origin Resource Sharing) is needed when JavaScript running in a web browser connects directly to your MCP server. This is different from using an LLM through a browser—in that case, the browser connects to the LLM service, and the LLM service connects to your MCP server (no CORS needed).\n\nBrowser-based MCP clients that need CORS include:\n\n- **MCP Inspector** - Browser-based debugging tool for testing MCP servers\n- **Custom browser-based MCP clients** - If you're building a web app that directly connects to MCP servers\n\nFor these scenarios, add CORS middleware with the specific headers required for MCP protocol:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.cors import CORSMiddleware\n\nmcp = FastMCP(\"MyServer\")\n\n# Configure CORS for browser-based clients\nmiddleware = [\n    Middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],  # Allow all origins; use specific origins for security\n        allow_methods=[\"GET\", \"POST\", \"DELETE\", \"OPTIONS\"],\n        allow_headers=[\n            \"mcp-protocol-version\",\n            \"mcp-session-id\",\n            \"Authorization\",\n            \"Content-Type\",\n        ],\n        expose_headers=[\"mcp-session-id\"],\n    )\n]\n\napp = mcp.http_app(middleware=middleware)\n```\n\n**Key configuration details:**\n\n- **`allow_origins`**: Specify exact origins (e.g., `[\"http://localhost:3000\"]`) rather than `[\"*\"]` for production deployments\n- **`allow_headers`**: Must include `mcp-protocol-version`, `mcp-session-id`, and `Authorization` (for authenticated servers)\n- **`expose_headers`**: Must include `mcp-session-id` so JavaScript can read the session ID from responses and send it in subsequent requests\n\nWithout `expose_headers=[\"mcp-session-id\"]`, browsers will receive the session ID but JavaScript won't be able to access it, causing session management to fail.\n\n<Warning>\n**Production Security**: Never use `allow_origins=[\"*\"]` in production. Specify the exact origins of your browser-based clients. Using wildcards exposes your server to unauthorized access from any website.\n</Warning>\n\n### SSE Polling for Long-Running Operations\n\n<VersionBadge version=\"2.14.0\" />\n\n<Note>\nThis feature only applies to the **StreamableHTTP transport** (the default for `http_app()`). It does not apply to the legacy SSE transport (`transport=\"sse\"`).\n</Note>\n\nWhen running tools that take a long time to complete, you may encounter issues with load balancers or proxies terminating connections that stay idle too long. [SEP-1699](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699) introduces SSE polling to solve this by allowing the server to gracefully close connections and have clients automatically reconnect.\n\nTo enable SSE polling, configure an `EventStore` when creating your HTTP application:\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom fastmcp.server.event_store import EventStore\n\nmcp = FastMCP(\"My Server\")\n\n@mcp.tool\nasync def long_running_task(ctx: Context) -> str:\n    \"\"\"A task that takes several minutes to complete.\"\"\"\n    for i in range(100):\n        await ctx.report_progress(i, 100)\n\n        # Periodically close the connection to avoid load balancer timeouts\n        # Client will automatically reconnect and resume receiving progress\n        if i % 30 == 0 and i > 0:\n            await ctx.close_sse_stream()\n\n        await do_expensive_work()\n\n    return \"Done!\"\n\n# Configure with EventStore for resumability\nevent_store = EventStore()\napp = mcp.http_app(\n    event_store=event_store,\n    retry_interval=2000,  # Client reconnects after 2 seconds\n)\n```\n\n**How it works:**\n\n1. When `event_store` is configured, the server stores all events (progress updates, results) with unique IDs\n2. Calling `ctx.close_sse_stream()` gracefully closes the HTTP connection\n3. The client automatically reconnects with a `Last-Event-ID` header\n4. The server replays any events the client missed during the disconnection\n\nThe `retry_interval` parameter (in milliseconds) controls how long clients wait before reconnecting. Choose a value that balances responsiveness with server load.\n\n<Note>\n`close_sse_stream()` is a no-op if called without an `EventStore` configured, so you can safely include it in tools that may run in different deployment configurations.\n</Note>\n\n#### Custom Storage Backends\n\nBy default, `EventStore` uses in-memory storage. For production deployments with multiple server instances, you can provide a custom storage backend using the `key_value` package:\n\n```python\nfrom fastmcp.server.event_store import EventStore\nfrom key_value.aio.stores.redis import RedisStore\n\n# Use Redis for distributed deployments\nredis_store = RedisStore(url=\"redis://localhost:6379\")\nevent_store = EventStore(\n    storage=redis_store,\n    max_events_per_stream=100,  # Keep last 100 events per stream\n    ttl=3600,  # Events expire after 1 hour\n)\n\napp = mcp.http_app(event_store=event_store)\n```\n\n## Integration with Web Frameworks\n\nIf you already have a web application running, you can add MCP capabilities by mounting a FastMCP server as a sub-application. This allows you to expose MCP tools alongside your existing API endpoints, sharing the same domain and infrastructure. The MCP server becomes just another route in your application, making it easy to manage and deploy.\n\n### Mounting in Starlette\n\nMount your FastMCP server in a Starlette application:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\n# Create your FastMCP server\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef analyze(data: str) -> dict:\n    return {\"result\": f\"Analyzed: {data}\"}\n\n# Create the ASGI app\nmcp_app = mcp.http_app(path='/mcp')\n\n# Create a Starlette app and mount the MCP server\napp = Starlette(\n    routes=[\n        Mount(\"/mcp-server\", app=mcp_app),\n        # Add other routes as needed\n    ],\n    lifespan=mcp_app.lifespan,\n)\n```\n\nThe MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.\n\n<Warning>\nFor Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.\n</Warning>\n\n#### Nested Mounts\n\nYou can create complex routing structures by nesting mounts:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\n# Create your FastMCP server\nmcp = FastMCP(\"MyServer\")\n\n# Create the ASGI app\nmcp_app = mcp.http_app(path='/mcp')\n\n# Create nested application structure\ninner_app = Starlette(routes=[Mount(\"/inner\", app=mcp_app)])\napp = Starlette(\n    routes=[Mount(\"/outer\", app=inner_app)],\n    lifespan=mcp_app.lifespan,\n)\n```\n\nIn this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.\n\n### FastAPI Integration\n\nFor FastAPI-specific integration patterns including both mounting MCP servers into FastAPI apps and generating MCP servers from FastAPI apps, see the [FastAPI Integration guide](/v2/integrations/fastapi).\n\nHere's a quick example showing how to add MCP to an existing FastAPI application:\n\n```python\nfrom fastapi import FastAPI\nfrom fastmcp import FastMCP\n\n# Your existing API\napi = FastAPI()\n\n@api.get(\"/api/status\")\ndef status():\n    return {\"status\": \"ok\"}\n\n# Create your MCP server\nmcp = FastMCP(\"API Tools\")\n\n@mcp.tool\ndef query_database(query: str) -> dict:\n    \"\"\"Run a database query\"\"\"\n    return {\"result\": \"data\"}\n\n# Mount MCP at /mcp\napi.mount(\"/mcp\", mcp.http_app())\n\n# Run with: uvicorn app:api --host 0.0.0.0 --port 8000\n```\n\nYour existing API remains at `http://localhost:8000/api` while MCP is available at `http://localhost:8000/mcp`.\n\n## Mounting Authenticated Servers\n\n<VersionBadge version=\"2.13.0\" />\n\n<Tip>\nThis section only applies if you're **mounting an OAuth-protected FastMCP server under a path prefix** (like `/api`) inside another application using `Mount()`.\n\nIf you're deploying your FastMCP server at root level without any `Mount()` prefix, the well-known routes are automatically included in `mcp.http_app()` and you don't need to do anything special.\n</Tip>\n\nOAuth specifications (RFC 8414 and RFC 9728) require discovery metadata to be accessible at well-known paths under the root level of your domain. When you mount an OAuth-protected FastMCP server under a path prefix like `/api`, this creates a routing challenge: your operational OAuth endpoints move under the prefix, but discovery endpoints must remain at the root.\n\n<Warning>\n**Common Mistakes to Avoid:**\n\n1. **Forgetting to mount `.well-known` routes at root** - FastMCP cannot do this automatically when your server is mounted under a path prefix. You must explicitly mount well-known routes at the root level.\n\n2. **Including mount prefix in both base_url AND mcp_path** - The mount prefix (like `/api`) should only be in `base_url`, not in `mcp_path`. Otherwise you'll get double paths.\n\n   ✅ **Correct:**\n   ```python\n   base_url = \"http://localhost:8000/api\"\n   mcp_path = \"/mcp\"\n   # Result: /api/mcp\n   ```\n\n   ❌ **Wrong:**\n   ```python\n   base_url = \"http://localhost:8000/api\"\n   mcp_path = \"/api/mcp\"\n   # Result: /api/api/mcp (double prefix!)\n   ```\n\nFollow the configuration instructions below to set up mounting correctly.\n</Warning>\n\n<Warning>\n**CORS Middleware Conflicts:**\n\nIf you're integrating FastMCP into an existing application with its own CORS middleware, be aware that layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).\n\nFastMCP and the MCP SDK already handle CORS for OAuth routes. If you need CORS on your own application routes, consider using the sub-app pattern: mount FastMCP and your routes as separate apps, each with their own middleware, rather than adding application-wide CORS middleware.\n</Warning>\n\n### Route Types\n\nOAuth-protected MCP servers expose two categories of routes:\n\n**Operational routes** handle the OAuth flow and MCP protocol:\n- `/authorize` - OAuth authorization endpoint\n- `/token` - Token exchange endpoint\n- `/auth/callback` - OAuth callback handler\n- `/mcp` - MCP protocol endpoint\n\n**Discovery routes** provide metadata for OAuth clients:\n- `/.well-known/oauth-authorization-server` - Authorization server metadata\n- `/.well-known/oauth-protected-resource/*` - Protected resource metadata\n\nWhen you mount your MCP app under a prefix, operational routes move with it, but discovery routes must stay at root level for RFC compliance.\n\n### Configuration Parameters\n\nThree parameters control where routes are located and how they combine:\n\n**`base_url`** tells clients where to find operational endpoints. This includes any Starlette `Mount()` path prefix (e.g., `/api`):\n\n```python\nbase_url=\"http://localhost:8000/api\"  # Includes mount prefix\n```\n\n**`mcp_path`** is the internal FastMCP endpoint path, which gets appended to `base_url`:\n\n```python\nmcp_path=\"/mcp\"  # Internal MCP path, NOT the mount prefix\n```\n\n**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.\n\n```python\n# Usually not needed - just set base_url and it works\nissuer_url=\"http://localhost:8000\"  # Only if you want root-level discovery\n```\n\nWhen `issuer_url` has a path (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.\n\n**Key Invariant:** `base_url + mcp_path = actual externally-accessible MCP URL`\n\nExample:\n- `base_url`: `http://localhost:8000/api` (mount prefix `/api`)\n- `mcp_path`: `/mcp` (internal path)\n- Result: `http://localhost:8000/api/mcp` (final MCP endpoint)\n\nNote that the mount prefix (`/api` from `Mount(\"/api\", ...)`) goes in `base_url`, while `mcp_path` is just the internal MCP route. Don't include the mount prefix in both places or you'll get `/api/api/mcp`.\n\n### Mounting Strategy\n\nWhen mounting an OAuth-protected server under a path prefix, declare your URLs upfront to make the relationships clear:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\n# Define the routing structure\nROOT_URL = \"http://localhost:8000\"\nMOUNT_PREFIX = \"/api\"\nMCP_PATH = \"/mcp\"\n```\n\nCreate the auth provider with `base_url`:\n\n```python\nauth = GitHubProvider(\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n    base_url=f\"{ROOT_URL}{MOUNT_PREFIX}\",  # Operational endpoints under prefix\n    # issuer_url defaults to base_url - path-aware discovery works automatically\n)\n```\n\nCreate the MCP app, which generates operational routes at the specified path:\n\n```python\nmcp = FastMCP(\"Protected Server\", auth=auth)\nmcp_app = mcp.http_app(path=MCP_PATH)\n```\n\nRetrieve the discovery routes from the auth provider. The `mcp_path` argument should match the path used when creating the MCP app:\n\n```python\nwell_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)\n```\n\nFinally, mount everything in the Starlette app with discovery routes at root and the MCP app under the prefix:\n\n```python\napp = Starlette(\n    routes=[\n        *well_known_routes,  # Discovery routes at root level\n        Mount(MOUNT_PREFIX, app=mcp_app),  # Operational routes under prefix\n    ],\n    lifespan=mcp_app.lifespan,\n)\n```\n\nThis configuration produces the following URL structure:\n\n- MCP endpoint: `http://localhost:8000/api/mcp`\n- OAuth authorization: `http://localhost:8000/api/authorize`\n- OAuth callback: `http://localhost:8000/api/auth/callback`\n- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server/api`\n- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp`\n\nBoth discovery endpoints use path-aware URLs per RFC 8414 and RFC 9728, matching the `base_url` path.\n\n### Complete Example\n\nHere's a complete working example showing all the pieces together:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\nimport uvicorn\n\n# Define routing structure\nROOT_URL = \"http://localhost:8000\"\nMOUNT_PREFIX = \"/api\"\nMCP_PATH = \"/mcp\"\n\n# Create OAuth provider\nauth = GitHubProvider(\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n    base_url=f\"{ROOT_URL}{MOUNT_PREFIX}\",\n    # issuer_url defaults to base_url - path-aware discovery works automatically\n)\n\n# Create MCP server\nmcp = FastMCP(\"Protected Server\", auth=auth)\n\n@mcp.tool\ndef analyze(data: str) -> dict:\n    return {\"result\": f\"Analyzed: {data}\"}\n\n# Create MCP app\nmcp_app = mcp.http_app(path=MCP_PATH)\n\n# Get discovery routes for root level\nwell_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)\n\n# Assemble the application\napp = Starlette(\n    routes=[\n        *well_known_routes,\n        Mount(MOUNT_PREFIX, app=mcp_app),\n    ],\n    lifespan=mcp_app.lifespan,\n)\n\nif __name__ == \"__main__\":\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n```\n\nFor more details on OAuth authentication, see the [Authentication guide](/v2/servers/auth).\n\n## Production Deployment\n\n### Running with Uvicorn\n\nWhen deploying to production, you'll want to optimize your server for performance and reliability. Uvicorn provides several options to improve your server's capabilities:\n\n```bash\n# Run with basic configuration\nuvicorn app:app --host 0.0.0.0 --port 8000\n\n# Run with multiple workers for production (requires stateless mode - see below)\nuvicorn app:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n### Horizontal Scaling\n\n<VersionBadge version=\"2.10.2\" />\n\nWhen deploying FastMCP behind a load balancer or running multiple server instances, you need to understand how the HTTP transport handles sessions and configure your server appropriately.\n\n#### Understanding Sessions\n\nBy default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/v2/servers/elicitation) and [sampling](/v2/servers/sampling), where the server needs to maintain context across multiple requests from the same client.\n\nThis works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.\n\n#### Without Stateless Mode\n\nWhen running multiple server instances behind a load balancer (Traefik, nginx, HAProxy, Kubernetes, etc.), requests from the same client may be routed to different instances:\n\n1. Client connects to Instance A → session created on Instance A\n2. Next request routes to Instance B → session doesn't exist → **request fails**\n\nYou might expect sticky sessions (session affinity) to solve this, but they don't work reliably with MCP clients.\n\n<Warning>\n**Why sticky sessions don't work:** Most MCP clients—including Cursor and Claude Code—use `fetch()` internally and don't properly forward `Set-Cookie` headers. Without cookies, load balancers can't identify which instance should handle subsequent requests. This is a limitation in how these clients implement HTTP, not something you can fix with load balancer configuration.\n</Warning>\n\n#### Enabling Stateless Mode\n\nFor horizontally scaled deployments, enable stateless HTTP mode. In stateless mode, each request creates a fresh transport context, eliminating the need for session affinity entirely.\n\n**Option 1: Via constructor**\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My Server\", stateless_http=True)\n\n@mcp.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\napp = mcp.http_app()\n```\n\n**Option 2: Via `run()`**\n\n```python\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", stateless_http=True)\n```\n\n**Option 3: Via environment variable**\n\n```bash\nFASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n### Environment Variables\n\nProduction 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.\n\nHere's an example using bearer token authentication (though OAuth is recommended for production):\n\n```python\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import BearerTokenAuth\n\n# Read configuration from environment\nauth_token = os.environ.get(\"MCP_AUTH_TOKEN\")\nif auth_token:\n    auth = BearerTokenAuth(token=auth_token)\n    mcp = FastMCP(\"Production Server\", auth=auth)\nelse:\n    mcp = FastMCP(\"Production Server\")\n\napp = mcp.http_app()\n```\n\nDeploy with your secrets safely stored in environment variables:\n```bash\nMCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000\n```\n\n### OAuth Token Security\n\n<VersionBadge version=\"2.13.0\" />\n\nIf you're using the [OAuth Proxy](/v2/servers/auth/oauth-proxy), FastMCP issues its own JWT tokens to clients instead of forwarding upstream provider tokens. This maintains proper OAuth 2.0 token boundaries.\n\n**Default Behavior (Development Only):**\n\nBy default, FastMCP automatically manages cryptographic keys:\n- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.\n- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.\n\nThis automatic approach is convenient for development but not suitable for production deployments.\n\n**For Production:**\n\nProduction requires explicit key management to ensure tokens survive restarts and can be shared across multiple server instances. This requires the following two things working together:\n\n1. **Explicit JWT signing key** for signing tokens issued to clients\n3. **Persistent network-accessible storage** for upstream tokens (wrapped in `FernetEncryptionWrapper` to encrypt sensitive data at rest)\n\n**Configuration:**\n\nAdd two parameters to your auth provider:\n\n```python {8-12}\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(host=\"redis.example.com\", port=6379),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    ),\n    base_url=\"https://your-server.com\"  # use HTTPS\n)\n```\n\nBoth parameters are required for production. Without an explicit signing key, keys are signed using a key derived from the client_secret, which will cause invalidation upon rotation of the client secret. Without persistent storage, tokens are local to the server and won't be trusted across hosts. **Wrap your storage backend in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without encryption, tokens are stored in plaintext.\n\nFor more details on the token architecture and key management, see [OAuth Proxy Key and Storage Management](/v2/servers/auth/oauth-proxy#key-and-storage-management).\n\n## Reverse Proxy (nginx)\n\nIn production, you'll typically run your FastMCP server behind a reverse proxy like nginx. A reverse proxy provides TLS termination, domain-based routing, static file serving, and an additional layer of security between the internet and your application.\n\n### Running FastMCP as a Linux Service\n\nBefore configuring nginx, you need your FastMCP server running as a background service. A systemd unit file ensures your server starts automatically and restarts on failure.\n\nCreate a file at `/etc/systemd/system/fastmcp.service`:\n\n```ini\n[Unit]\nDescription=FastMCP Server\nAfter=network.target\n\n[Service]\nUser=www-data\nGroup=www-data\nWorkingDirectory=/opt/fastmcp\nExecStart=/opt/fastmcp/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000\nRestart=always\nRestartSec=5\nEnvironment=\"PATH=/opt/fastmcp/.venv/bin\"\n\n[Install]\nWantedBy=multi-user.target\n```\n\nEnable and start the service:\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable fastmcp\nsudo systemctl start fastmcp\n```\n\nThis assumes your ASGI application is in `/opt/fastmcp/app.py` with a virtual environment at `/opt/fastmcp/.venv`. Adjust paths to match your deployment layout.\n\n### nginx Configuration\n\nFastMCP's Streamable HTTP transport uses Server-Sent Events (SSE) for streaming responses. This requires specific nginx settings to prevent buffering from breaking the event stream.\n\nCreate a site configuration at `/etc/nginx/sites-available/fastmcp`:\n\n```nginx\nserver {\n    listen 80;\n    server_name mcp.example.com;\n\n    # Redirect HTTP to HTTPS\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name mcp.example.com;\n\n    ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;\n\n    location / {\n        proxy_pass http://127.0.0.1:8000;\n        proxy_http_version 1.1;\n        proxy_set_header Connection '';\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # Required for SSE (Server-Sent Events) streaming\n        proxy_buffering off;\n        proxy_cache off;\n\n        # Allow long-lived connections for streaming responses\n        proxy_read_timeout 300s;\n        proxy_send_timeout 300s;\n    }\n}\n```\n\nEnable the site and reload nginx:\n\n```bash\nsudo ln -s /etc/nginx/sites-available/fastmcp /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n```\n\nYour FastMCP server is now accessible at `https://mcp.example.com/mcp`.\n\n<Warning>\n**SSE buffering is the most common issue.** If clients connect but never receive streaming responses (progress updates, tool results), verify that `proxy_buffering off` is set. Without it, nginx buffers the entire SSE stream and delivers it only when the connection closes, which breaks real-time communication.\n</Warning>\n\n### Key Considerations\n\nWhen deploying FastMCP behind a reverse proxy, keep these points in mind:\n\n- **Disable buffering**: SSE requires `proxy_buffering off` so events reach clients immediately. This is the single most important setting.\n- **Increase timeouts**: The default nginx `proxy_read_timeout` is 60 seconds. Long-running MCP tools will cause the connection to drop. Set timeouts to at least 300 seconds, or higher if your tools run longer. For tools that may exceed any timeout, use [SSE Polling](#sse-polling-for-long-running-operations) to gracefully handle proxy disconnections.\n- **Use HTTP/1.1**: Set `proxy_http_version 1.1` and `proxy_set_header Connection ''` to enable keep-alive connections between nginx and your server. Clearing the `Connection` header prevents clients from sending `Connection: close` to your upstream, which would break SSE streams. Both settings are required for proper SSE support.\n- **Forward headers**: Pass `X-Forwarded-For` and `X-Forwarded-Proto` so your FastMCP server can determine the real client IP and protocol. This is important for logging and for OAuth redirect URLs.\n- **TLS termination**: Let nginx handle TLS certificates (e.g., via Let's Encrypt with Certbot). Your FastMCP server can then run on plain HTTP internally.\n\n### Mounting Under a Path Prefix\n\nIf you want your MCP server available at a subpath like `https://example.com/api/mcp` instead of at the root domain, adjust the nginx `location` block:\n\n```nginx\nlocation /api/ {\n    proxy_pass http://127.0.0.1:8000/;\n    proxy_http_version 1.1;\n    proxy_set_header Connection '';\n    proxy_set_header Host $host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n\n    # Required for SSE streaming\n    proxy_buffering off;\n    proxy_cache off;\n    proxy_read_timeout 300s;\n    proxy_send_timeout 300s;\n}\n```\n\nNote the trailing `/` on both `location /api/` and `proxy_pass http://127.0.0.1:8000/` — this ensures nginx strips the `/api` prefix before forwarding to your server. If you're using OAuth authentication with a mount prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) for additional configuration.\n\n## Testing Your Deployment\n\nOnce your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/v2/development/tests) guide.\n\n## Hosting Your Server\n\nThis guide has shown you how to create an HTTP-accessible MCP server, but you'll still need a hosting provider to make it available on the internet. Your FastMCP server can run anywhere that supports Python web applications:\n\n- **Cloud VMs** (AWS EC2, Google Compute Engine, Azure VMs)\n- **Container platforms** (Cloud Run, Container Instances, ECS)  \n- **Platform-as-a-Service** (Railway, Render, Vercel)\n- **Edge platforms** (Cloudflare Workers)\n- **Kubernetes clusters** (self-managed or managed)\n\nThe key requirements are Python 3.10+ support and the ability to expose an HTTP port. Most providers will require you to package your server (requirements.txt, Dockerfile, etc.) according to their deployment format. For managed, zero-configuration deployment, see [Prefect Horizon](/deployment/prefect-horizon).\n"
  },
  {
    "path": "docs/v2/deployment/running-server.mdx",
    "content": "---\ntitle: Running Your Server\nsidebarTitle: Running Your Server\ndescription: Learn how to run your FastMCP server locally for development and testing\nicon: circle-play\n---\n\nFastMCP servers can be run in different ways depending on your needs. This guide focuses on running servers locally for development and testing. For production deployment to a URL, see the [HTTP Deployment](/v2/deployment/http) guide.\n\n## The `run()` Method\n\nEvery FastMCP server needs to be started to accept connections. The simplest way to run a server is by calling the `run()` method on your FastMCP instance. This method starts the server and blocks until it's stopped, handling all the connection management for you.\n\n<Tip>\nFor maximum compatibility, it's best practice to place the `run()` call within an `if __name__ == \"__main__\":` block. This ensures the server starts only when the script is executed directly, not when imported as a module.\n</Tip>\n\n```python {9-10} my_server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\nYou can now run this MCP server by executing `python my_server.py`.\n\n## Transport Protocols\n\nMCP servers communicate with clients through different transport protocols. Think of transports as the \"language\" your server speaks to communicate with clients. FastMCP supports three main transport protocols, each designed for specific use cases and deployment scenarios.\n\nThe choice of transport determines how clients connect to your server, what network capabilities are available, and how many clients can connect simultaneously. Understanding these transports helps you choose the right approach for your application.\n\n### STDIO Transport (Default)\n\nSTDIO (Standard Input/Output) is the default transport for FastMCP servers. When you call `run()` without arguments, your server uses STDIO transport. This transport communicates through standard input and output streams, making it perfect for command-line tools and desktop applications like Claude Desktop.\n\nWith STDIO transport, the client spawns a new server process for each session and manages its lifecycle. The server reads MCP messages from stdin and writes responses to stdout. This is why STDIO servers don't stay running - they're started on-demand by the client.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()  # Uses STDIO transport by default\n```\n\nSTDIO is ideal for:\n- Local development and testing\n- Claude Desktop integration\n- Command-line tools\n- Single-user applications\n\n### HTTP Transport (Streamable)\n\nHTTP transport turns your MCP server into a web service accessible via a URL. This transport uses the Streamable HTTP protocol, which allows clients to connect over the network. Unlike STDIO where each client gets its own process, an HTTP server can handle multiple clients simultaneously.\n\nThe Streamable HTTP protocol provides full bidirectional communication between client and server, supporting all MCP operations including streaming responses. This makes it the recommended choice for network-based deployments.\n\nTo use HTTP transport, specify it in the `run()` method along with networking options:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    # Start an HTTP server on port 8000\n    mcp.run(transport=\"http\", host=\"127.0.0.1\", port=8000)\n```\n\nYour server is now accessible at `http://localhost:8000/mcp`. This URL is the MCP endpoint that clients will connect to. HTTP transport enables:\n- Network accessibility\n- Multiple concurrent clients\n- Integration with web infrastructure\n- Remote deployment capabilities\n\nFor production HTTP deployment with authentication and advanced configuration, see the [HTTP Deployment](/v2/deployment/http) guide.\n\n### SSE Transport (Legacy)\n\nServer-Sent Events (SSE) transport was the original HTTP-based transport for MCP. While still supported for backward compatibility, it has limitations compared to the newer Streamable HTTP transport. SSE only supports server-to-client streaming, making it less efficient for bidirectional communication.\n\n```python\nif __name__ == \"__main__\":\n    # SSE transport - use HTTP instead for new projects\n    mcp.run(transport=\"sse\", host=\"127.0.0.1\", port=8000)\n```\n\nWe recommend using HTTP transport instead of SSE for all new projects. SSE remains available only for compatibility with older clients that haven't upgraded to Streamable HTTP.\n\n### Choosing the Right Transport\n\nEach transport serves different needs. STDIO is perfect when you need simple, local execution - it's what Claude Desktop and most command-line tools expect. HTTP transport is essential when you need network access, want to serve multiple clients, or plan to deploy your server remotely. SSE exists only for backward compatibility and shouldn't be used in new projects.\n\nConsider your deployment scenario: Are you building a tool for local use? STDIO is your best choice. Need a centralized service that multiple clients can access? HTTP transport is the way to go.\n\n## The FastMCP CLI\n\nFastMCP provides a powerful command-line interface for running servers without modifying the source code. The CLI can automatically find and run your server with different transports, manage dependencies, and handle development workflows:\n\n```bash\nfastmcp run server.py\n```\n\nThe CLI automatically finds a FastMCP instance in your file (named `mcp`, `server`, or `app`) and runs it with the specified options. This is particularly useful for testing different transports or configurations without changing your code.\n\n### Dependency Management\n\nThe CLI integrates with `uv` to manage Python environments and dependencies:\n\n```bash\n# Run with a specific Python version\nfastmcp run server.py --python 3.11\n\n# Run with additional packages\nfastmcp run server.py --with pandas --with numpy\n\n# Run with dependencies from a requirements file\nfastmcp run server.py --with-requirements requirements.txt\n\n# Combine multiple options\nfastmcp run server.py --python 3.10 --with httpx --transport http\n\n# Run within a specific project directory\nfastmcp run server.py --project /path/to/project\n```\n\n<Note>\nWhen using `--python`, `--with`, `--project`, or `--with-requirements`, the server runs via `uv run` subprocess instead of using your local environment.\n</Note>\n\n### Passing Arguments to Servers\n\nWhen servers accept command line arguments (using argparse, click, or other libraries), you can pass them after `--`:\n\n```bash\nfastmcp run config_server.py -- --config config.json\nfastmcp run database_server.py -- --database-path /tmp/db.sqlite --debug\n```\n\nThis is useful for servers that need configuration files, database paths, API keys, or other runtime options.\n\nFor more CLI features including development mode with the MCP Inspector, see the [CLI documentation](/v2/patterns/cli).\n\n### Async Usage\n\nFastMCP servers are built on async Python, but the framework provides both synchronous and asynchronous APIs to fit your application's needs. The `run()` method we've been using is actually a synchronous wrapper around the async server implementation.\n\nFor applications that are already running in an async context, FastMCP provides the `run_async()` method:\n\n```python {10-12}\nfrom fastmcp import FastMCP\nimport asyncio\n\nmcp = FastMCP(name=\"MyServer\")\n\n@mcp.tool\ndef hello(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nasync def main():\n    # Use run_async() in async contexts\n    await mcp.run_async(transport=\"http\", port=8000)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n<Warning>\nThe `run()` method cannot be called from inside an async function because it creates its own async event loop internally. If you attempt to call `run()` from inside an async function, you'll get an error about the event loop already running.\n\nAlways use `run_async()` inside async functions and `run()` in synchronous contexts.\n</Warning>\n\nBoth `run()` and `run_async()` accept the same transport arguments, so all the examples above apply to both methods.\n\n## Custom Routes\n\nWhen using HTTP transport, you might want to add custom web endpoints alongside your MCP server. This is useful for health checks, status pages, or simple APIs. FastMCP lets you add custom routes using the `@custom_route` decorator:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.requests import Request\nfrom starlette.responses import PlainTextResponse\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check(request: Request) -> PlainTextResponse:\n    return PlainTextResponse(\"OK\")\n\n@mcp.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")  # Health check at http://localhost:8000/health\n```\n\nCustom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/v2/deployment/http#integration-with-web-frameworks).\n\n## Alternative Initialization Patterns\n\nThe `if __name__ == \"__main__\"` pattern works well for standalone scripts, but some deployment scenarios require different approaches. FastMCP handles these cases automatically.\n\n### CLI-Only Servers\n\nWhen using the FastMCP CLI, you don't need the `if __name__` block at all. The CLI will find your FastMCP instance and run it:\n\n```python\n# server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")  # CLI looks for 'mcp', 'server', or 'app'\n\n@mcp.tool\ndef process(data: str) -> str:\n    return f\"Processed: {data}\"\n\n# No if __name__ block needed - CLI will find and run 'mcp'\n```\n\n### ASGI Applications\n\nFor ASGI deployment (running with Uvicorn or similar), you'll want to create an ASGI application object. This approach is common in production deployments where you need more control over the server configuration:\n\n```python\n# app.py\nfrom fastmcp import FastMCP\n\ndef create_app():\n    mcp = FastMCP(\"MyServer\")\n    \n    @mcp.tool\n    def process(data: str) -> str:\n        return f\"Processed: {data}\"\n    \n    return mcp.http_app()\n\napp = create_app()  # Uvicorn will use this\n```\n\nSee the [HTTP Deployment](/v2/deployment/http) guide for more ASGI deployment patterns."
  },
  {
    "path": "docs/v2/deployment/server-configuration.mdx",
    "content": "---\ntitle: \"Project Configuration\"\nsidebarTitle: \"Project Configuration\"\ndescription: Use fastmcp.json for portable, declarative project configuration\nicon: file-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nFastMCP supports declarative configuration through `fastmcp.json` files. This is the canonical and preferred way to configure FastMCP projects, providing a single source of truth for server settings, dependencies, and deployment options that replaces complex command-line arguments.\n\nThe `fastmcp.json` file is designed to be a portable description of your server configuration that can be shared across environments and teams. When running from a `fastmcp.json` file, you can override any configuration values using CLI arguments.\n\n## Overview\n\nThe `fastmcp.json` configuration file allows you to define all aspects of your FastMCP server in a structured, shareable format. Instead of remembering command-line arguments or writing shell scripts, you declare your server's configuration once and use it everywhere.\n\nWhen you have a `fastmcp.json` file, running your server becomes as simple as:\n\n```bash\n# Run the server using the configuration\nfastmcp run fastmcp.json\n\n# Or if fastmcp.json exists in the current directory\nfastmcp run\n```\n\nThis configuration approach ensures reproducible deployments across different environments, from local development to production servers. It works seamlessly with Claude Desktop, VS Code extensions, and any MCP-compatible client.\n\n## File Structure\n\nThe `fastmcp.json` configuration answers three fundamental questions about your server:\n\n- **Source** = WHERE does your server code live?\n- **Environment** = WHAT environment setup does it require?\n- **Deployment** = HOW should the server run?\n\nThis conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    // WHERE: Location of your server code\n    \"type\": \"filesystem\",  // Optional, defaults to \"filesystem\"\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    // WHAT: Environment setup and dependencies\n    \"type\": \"uv\",  // Optional, defaults to \"uv\"\n    \"python\": \">=3.10\",\n    \"dependencies\": [\"pandas\", \"numpy\"]\n  },\n  \"deployment\": {\n    // HOW: Runtime configuration\n    \"transport\": \"stdio\",\n    \"log_level\": \"INFO\"\n  }\n}\n```\n\nOnly the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.\n\n### JSON Schema Support\n\nFastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  }\n}\n```\n\nTwo schema URLs are available:\n- **Version-specific**: `https://gofastmcp.com/public/schemas/fastmcp.json/v1.json`\n- **Latest version**: `https://gofastmcp.com/public/schemas/fastmcp.json/latest.json`\n\nModern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.\n\n### Source Configuration\n\nThe source configuration determines **WHERE** your server code lives. It tells FastMCP how to find and load your server, whether it's a local Python file, a remote repository, or hosted in the cloud. This section is required and forms the foundation of your configuration.\n\n<Card icon=\"code\" title=\"Source\">\n<ParamField body=\"source\" type=\"object\" required>\n  The server source configuration that determines where your server code lives.\n  \n  <ParamField body=\"type\" type=\"string\" default=\"filesystem\">\n    The source type identifier that determines which implementation to use. Currently supports `\"filesystem\"` for local files. Future releases will add support for `\"git\"` and `\"cloud\"` source types.\n  </ParamField>\n  \n  <Expandable title=\"FileSystemSource\">\n    When `type` is `\"filesystem\"` (or omitted), the source points to a local Python file containing your FastMCP server:\n    \n    <ParamField body=\"path\" type=\"string\" required>\n      Path to the Python file containing your FastMCP server. \n    </ParamField>\n    \n    <ParamField body=\"entrypoint\" type=\"string\">\n      Name of the server instance or factory function within the module:\n      - Can be a FastMCP server instance (e.g., `mcp = FastMCP(\"MyServer\")`)\n      - Can be a function with no arguments that returns a FastMCP server\n      - If not specified, FastMCP searches for common names: `mcp`, `server`, or `app`\n    </ParamField>\n    \n    **Example:**\n    ```json\n    \"source\": {\n      \"type\": \"filesystem\",\n      \"path\": \"src/server.py\",\n      \"entrypoint\": \"mcp\"\n    }\n    ```\n    \n    Note: File paths are resolved relative to the configuration file's location.\n  </Expandable>\n</ParamField>\n</Card>\n\n<Note>\n**Future Source Types**\n\nFuture releases will support additional source types:\n- **Git repositories** (`type: \"git\"`) for loading server code directly from version control\n- **Prefect Horizon** (`type: \"cloud\"`) for hosted servers with automatic scaling and management\n</Note>\n\n### Environment Configuration\n\nThe environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment, ensuring your server runs with the exact Python version and dependencies it requires. This section creates isolated, reproducible environments across different systems.\n\nFastMCP uses an extensible environment system with a base `Environment` class that can be implemented by different environment providers. Currently, FastMCP supports the `UVEnvironment` for Python environment management using `uv`'s powerful dependency resolver.\n\n<Card icon=\"code\" title=\"Environment\">\n<ParamField body=\"environment\" type=\"object\">\n  Optional environment configuration. When specified, FastMCP uses the appropriate environment implementation to set up your server's runtime.\n  \n  <ParamField body=\"type\" type=\"string\" default=\"uv\">\n    The environment type identifier that determines which implementation to use. Currently supports `\"uv\"` for Python environments managed by uv. If omitted, defaults to `\"uv\"`.\n  </ParamField>\n  \n  <Expandable title=\"UVEnvironment\">\n    When `type` is `\"uv\"` (or omitted), the environment uses uv to manage Python dependencies:\n    \n    <ParamField body=\"python\" type=\"string\">\n      Python version constraint. Examples:\n      - Exact version: `\"3.12\"`\n      - Minimum version: `\">=3.10\"`\n      - Version range: `\">=3.10,<3.13\"`\n    </ParamField>\n    \n    <ParamField body=\"dependencies\" type=\"list[str]\">\n      List of pip packages with optional version specifiers (PEP 508 format).\n      ```json\n      \"dependencies\": [\"pandas>=2.0\", \"requests\", \"httpx\"]\n      ```\n    </ParamField>\n    \n    <ParamField body=\"requirements\" type=\"string\">\n      Path to a requirements.txt file, resolved relative to the config file location.\n      ```json\n      \"requirements\": \"requirements.txt\"\n      ```\n    </ParamField>\n    \n    <ParamField body=\"project\" type=\"string\">\n      Path to a project directory containing pyproject.toml for uv project management.\n      ```json\n      \"project\": \".\"\n      ```\n    </ParamField>\n    \n    <ParamField body=\"editable\" type=\"list[string]\">\n      List of paths to packages to install in editable/development mode. Useful for local development when you want changes to be reflected immediately. Supports multiple packages for monorepo setups or shared libraries.\n      ```json\n      \"editable\": [\".\"]\n      ```\n      Or with multiple packages:\n      ```json\n      \"editable\": [\".\", \"../shared-lib\", \"/path/to/another-package\"]\n      ```\n    </ParamField>\n    \n    **Example:**\n    ```json\n    \"environment\": {\n      \"type\": \"uv\",\n      \"python\": \">=3.10\",\n      \"dependencies\": [\"pandas\", \"numpy\"],\n      \"editable\": [\".\"]\n    }\n    ```\n    \n    Note: When any UVEnvironment field is specified, FastMCP automatically creates an isolated environment using `uv` before running your server.\n  </Expandable>\n</ParamField>\n</Card>\n\nWhen environment configuration is provided, FastMCP:\n1. Detects the environment type (defaults to `\"uv\"` if not specified)\n2. Creates an isolated environment using the appropriate provider\n3. Installs the specified dependencies\n4. Runs your server in this clean environment\n\nThis build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.\n\n<Note>\n**Future Environment Types**\n\nSimilar to source types, future releases may support additional environment types for different runtime requirements, such as Docker containers or language-specific environments beyond Python.\n</Note>\n\n### Deployment Configuration\n\nThe deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.\n\nEnvironment variables are included in this section because they're runtime configuration that affects how your server behaves when it executes, not how its environment is built. The deployment configuration is applied every time your server starts, controlling its operational characteristics.\n\n<Card icon=\"code\" title=\"Deployment Fields\">\n<ParamField body=\"deployment\" type=\"object\">\n  Optional runtime configuration for the server.\n  \n  <Expandable title=\"Deployment Fields\">\n    <ParamField body=\"transport\" type=\"string\" default=\"stdio\">\n      Protocol for client communication:\n      - `\"stdio\"`: Standard input/output for desktop clients\n      - `\"http\"`: Network-accessible HTTP server\n      - `\"sse\"`: Server-sent events\n    </ParamField>\n    \n    <ParamField body=\"host\" type=\"string\" default=\"127.0.0.1\">\n      Network interface to bind (HTTP transport only):\n      - `\"127.0.0.1\"`: Local connections only\n      - `\"0.0.0.0\"`: All network interfaces\n    </ParamField>\n    \n    <ParamField body=\"port\" type=\"integer\" default=\"3000\">\n      Port number for HTTP transport.\n    </ParamField>\n    \n    <ParamField body=\"path\" type=\"string\" default=\"/mcp/\">\n      URL path for the MCP endpoint when using HTTP transport.\n    </ParamField>\n    \n    <ParamField body=\"log_level\" type=\"string\" default=\"INFO\">\n      Server logging verbosity. Options:\n      - `\"DEBUG\"`: Detailed debugging information\n      - `\"INFO\"`: General informational messages\n      - `\"WARNING\"`: Warning messages\n      - `\"ERROR\"`: Error messages only\n      - `\"CRITICAL\"`: Critical errors only\n    </ParamField>\n    \n    <ParamField body=\"env\" type=\"object\">\n      Environment variables to set when running the server. Supports `${VAR_NAME}` syntax for runtime interpolation.\n      ```json\n      \"env\": {\n        \"API_KEY\": \"secret-key\",\n        \"DATABASE_URL\": \"postgres://${DB_USER}@${DB_HOST}/mydb\"\n      }\n      ```\n    </ParamField>\n    \n    <ParamField body=\"cwd\" type=\"string\">\n      Working directory for the server process. Relative paths are resolved from the config file location.\n    </ParamField>\n    \n    <ParamField body=\"args\" type=\"list[str]\">\n      Command-line arguments to pass to the server, passed after `--` to the server's argument parser.\n      ```json\n      \"args\": [\"--config\", \"server-config.json\"]\n      ```\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\n#### Environment Variable Interpolation\n\nThe `env` field in deployment configuration supports runtime interpolation of environment variables using `${VAR_NAME}` syntax. This enables dynamic configuration based on your deployment environment:\n\n```json\n{\n  \"deployment\": {\n    \"env\": {\n      \"API_URL\": \"https://api.${ENVIRONMENT}.example.com\",\n      \"DATABASE_URL\": \"postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/myapp\",\n      \"CACHE_KEY\": \"myapp_${ENVIRONMENT}_${VERSION}\"\n    }\n  }\n}\n```\n\nWhen the server starts, FastMCP replaces `${ENVIRONMENT}`, `${DB_USER}`, etc. with values from your system's environment variables. If a variable doesn't exist, the placeholder is preserved as-is.\n\n**Example**: If your system has `ENVIRONMENT=production` and `DB_HOST=db.example.com`:\n```json\n// Configuration\n{\n  \"deployment\": {\n    \"env\": {\n      \"API_URL\": \"https://api.${ENVIRONMENT}.example.com\",\n      \"DB_HOST\": \"${DB_HOST}\"\n    }\n  }\n}\n\n// Result at runtime\n{\n  \"API_URL\": \"https://api.production.example.com\",\n  \"DB_HOST\": \"db.example.com\"\n}\n```\n\nThis feature is particularly useful for:\n- Deploying the same configuration across development, staging, and production\n- Keeping sensitive values out of configuration files\n- Building dynamic URLs and connection strings\n- Creating environment-specific prefixes or suffixes\n\n## Usage with CLI Commands\n\nFastMCP automatically detects and uses a file specifically named `fastmcp.json` in the current directory, making server execution simple and consistent. Files with FastMCP configuration format but different names are not auto-detected and must be specified explicitly:\n\n```bash\n# Auto-detect fastmcp.json in current directory\ncd my-project\nfastmcp run  # No arguments needed!\n\n# Or specify a configuration file explicitly\nfastmcp run prod.fastmcp.json\n\n# Skip environment setup when already in a uv environment\nfastmcp run fastmcp.json --skip-env\n\n# Skip source preparation when source is already prepared\nfastmcp run fastmcp.json --skip-source\n\n# Skip both environment and source preparation\nfastmcp run fastmcp.json --skip-env --skip-source\n```\n\n### Pre-building Environments\n\nYou can use `fastmcp project prepare` to create a persistent uv project with all dependencies pre-installed:\n\n```bash\n# Create a persistent environment\nfastmcp project prepare fastmcp.json --output-dir ./env\n\n# Use the pre-built environment to run the server\nfastmcp run fastmcp.json --project ./env\n```\n\nThis pattern separates environment setup (slow) from server execution (fast), useful for deployment scenarios.\n\n### Using an Existing Environment\n\nBy default, FastMCP creates an isolated environment with `uv` based on your configuration. When you already have a suitable Python environment, use the `--skip-env` flag to skip environment creation:\n\n```bash\nfastmcp run fastmcp.json --skip-env\n```\n\n**When you already have an environment:**\n- You're in an activated virtual environment with all dependencies installed\n- You're inside a Docker container with pre-installed dependencies  \n- You're in a CI/CD pipeline that pre-builds the environment\n- You're using a system-wide installation with all required packages\n- You're in a uv-managed environment (prevents infinite recursion)\n\nThis flag tells FastMCP: \"I already have everything installed, just run the server.\"\n\n### Using an Existing Source\n\nWhen working with source types that require preparation (future support for git repositories or cloud sources), use the `--skip-source` flag when you already have the source code available:\n\n```bash\nfastmcp run fastmcp.json --skip-source\n```\n\n**When you already have the source:**\n- You've previously cloned a git repository and don't need to re-fetch\n- You have a cached copy of a cloud-hosted server\n- You're in a CI/CD pipeline where source checkout is a separate step\n- You're iterating locally on already-downloaded code\n\nThis flag tells FastMCP: \"I already have the source code, skip any download/clone steps.\"\n\nNote: For filesystem sources (local Python files), this flag has no effect since they don't require preparation.\n\nThe configuration file works with all FastMCP commands:\n- **`run`** - Start the server in production mode\n- **`dev`** - Launch with the Inspector UI for development  \n- **`inspect`** - View server capabilities and configuration\n- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients\n\nWhen no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.\n\n### CLI Override Behavior\n\nCommand-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:\n\n```bash\n# Config specifies port 3000, CLI overrides to 8080\nfastmcp run fastmcp.json --port 8080\n\n# Config specifies stdio, CLI overrides to HTTP\nfastmcp run fastmcp.json --transport http\n\n# Add extra dependencies not in config\nfastmcp run fastmcp.json --with requests --with httpx\n```\n\nThis precedence order enables:\n- Quick testing of different settings\n- Environment-specific overrides in deployment scripts\n- Debugging with increased log levels\n- Temporary configuration changes\n\n### Custom Naming Patterns\n\nYou can use different configuration files for different environments:\n\n- `fastmcp.json` - Default configuration\n- `dev.fastmcp.json` - Development settings\n- `prod.fastmcp.json` - Production settings\n- `test_fastmcp.json` - Test configuration\n\nAny file with \"fastmcp.json\" in the name is recognized as a configuration file.\n\n## Examples\n\n<Tabs>\n<Tab title=\"Basic Configuration\">\n\nA minimal configuration for a simple server:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  }\n}\n```\nThis configuration explicitly specifies the server entrypoint (`mcp`), making it clear which server instance or factory function to use. Uses all defaults: STDIO transport, no special dependencies, standard logging.\n</Tab>\n<Tab title=\"Development Configuration\">\n\nA configuration optimized for local development:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  // WHERE does the server live?\n  \"source\": {\n    \"path\": \"src/server.py\",\n    \"entrypoint\": \"app\"\n  },\n  // WHAT dependencies does it need?\n  \"environment\": {\n    \"type\": \"uv\",\n    \"python\": \"3.12\",\n    \"dependencies\": [\"fastmcp[dev]\"],\n    \"editable\": \".\"\n  },\n  // HOW should it run?\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8000,\n    \"log_level\": \"DEBUG\",\n    \"env\": {\n      \"DEBUG\": \"true\",\n      \"ENV\": \"development\"\n    }\n  }\n}\n```\n</Tab>\n<Tab title=\"Production Configuration\">\n\nA production-ready configuration with full dependency management:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  // WHERE does the server live?\n  \"source\": {\n    \"path\": \"app/main.py\",\n    \"entrypoint\": \"mcp_server\"\n  },\n  // WHAT dependencies does it need?\n  \"environment\": {\n    \"python\": \"3.11\",\n    \"requirements\": \"requirements/production.txt\",\n    \"project\": \".\"\n  },\n  // HOW should it run?\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"0.0.0.0\",\n    \"port\": 3000,\n    \"path\": \"/api/mcp/\",\n    \"log_level\": \"INFO\",\n    \"env\": {\n      \"ENV\": \"production\",\n      \"API_BASE_URL\": \"https://api.example.com\",\n      \"DATABASE_URL\": \"postgresql://user:pass@db.example.com/prod\"\n    },\n    \"cwd\": \"/app\",\n    \"args\": [\"--workers\", \"4\"]\n  }\n}\n```\n</Tab>\n<Tab title=\"Data Science Server\">\n\nConfiguration for a data analysis server with scientific packages:\n\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"analysis_server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"python\": \"3.11\",\n    \"dependencies\": [\n      \"pandas>=2.0\",\n      \"numpy\",\n      \"scikit-learn\",\n      \"matplotlib\",\n      \"jupyterlab\"\n    ]\n  },\n  \"deployment\": {\n    \"transport\": \"stdio\",\n    \"env\": {\n      \"MATPLOTLIB_BACKEND\": \"Agg\",\n      \"DATA_PATH\": \"./datasets\"\n    }\n  }\n}\n```\n</Tab>\n<Tab title=\"Multi-Environment Setup\"> \n\nYou can maintain multiple configuration files for different environments:\n\n**dev.fastmcp.json**:\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"log_level\": \"DEBUG\"\n  }\n}\n```\n\n**prod.fastmcp.json**:\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"requirements\": \"requirements/production.txt\"\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"0.0.0.0\",\n    \"log_level\": \"WARNING\"\n  }\n}\n```\n\nRun different configurations:\n```bash\nfastmcp run dev.fastmcp.json   # Development\nfastmcp run prod.fastmcp.json  # Production\n```\n</Tab>\n</Tabs>\n\n## Migrating from CLI Arguments\n\nIf you're currently using command-line arguments or shell scripts, migrating to `fastmcp.json` simplifies your workflow. Here's how common CLI patterns map to configuration:\n\n**CLI Command**:\n```bash\nuv run --with pandas --with requests \\\n  fastmcp run server.py \\\n  --transport http \\\n  --port 8000 \\\n  --log-level INFO\n```\n\n**Equivalent fastmcp.json**:\n```json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"port\": 8000,\n    \"log_level\": \"INFO\"\n  }\n}\n```\n\nNow simply run:\n```bash\nfastmcp run  # Automatically finds and uses fastmcp.json\n```\n\nThe configuration file approach provides better documentation, easier sharing, and consistent execution across different environments while maintaining the flexibility to override settings when needed."
  },
  {
    "path": "docs/v2/development/contributing.mdx",
    "content": "---\ntitle: \"Contributing\"\ndescription: \"Development workflow for FastMCP contributors\"\nicon: code-pull-request\n---\n\nContributing to FastMCP means joining a community that values clean, maintainable code and thoughtful API design. All contributions are valued - from fixing typos in documentation to implementing major features.\n\n## Issues\n\n### Issue First, Code Second\n\n**Every pull request requires a corresponding issue - no exceptions.** This requirement creates a collaborative space where approach, scope, and alignment are established before code is written. Issues serve as design documents where maintainers and contributors discuss implementation strategy, identify potential conflicts with existing patterns, and ensure proposed changes advance FastMCP's vision.\n\n**FastMCP is an opinionated framework, not a kitchen sink.** The maintainers have strong beliefs about what FastMCP should and shouldn't do. Just because something takes N lines of code and you want it in fewer lines doesn't mean FastMCP should take on the maintenance burden or endorse that pattern. This is judged at the maintainers' discretion.\n\nUse issues to understand scope BEFORE opening PRs. The issue discussion determines whether a feature belongs in core, contrib, or not at all.\n\n### Writing Good Issues\n\nFastMCP is an extremely highly-trafficked repository maintained by a very small team. Issues that appear to transfer burden to maintainers without any effort to validate the problem will be closed. Please help the maintainers help you by always providing a minimal reproducible example and clearly describing the problem.\n\n**LLM-generated issues will be closed immediately.** Issues that contain paragraphs of unnecessary explanation, verbose problem descriptions, or obvious LLM authorship patterns obfuscate the actual problem and transfer burden to maintainers.\n\nWrite clear, concise issues that:\n- State the problem directly\n- Provide a minimal reproducible example\n- Skip unnecessary background or context\n- Take responsibility for clear communication\n\nIssues may be labeled \"Invalid\" simply due to confusion caused by verbosity or not adhering to the guidelines outlined here.\n\n## Pull Requests\n\nPRs that deviate from FastMCP's core principles will be rejected regardless of implementation quality. **PRs are NOT for iterating on ideas** - they should only be opened for ideas that already have a bias toward acceptance based on issue discussion.\n\n\n### Development Environment\n\n#### Installation\n\nTo contribute to FastMCP, you'll need to set up a development environment with all necessary tools and dependencies.\n\n```bash\n# Clone the repository\ngit clone https://github.com/PrefectHQ/fastmcp.git\ncd fastmcp\n\n# Install all dependencies including dev tools\nuv sync\n\n# Install prek hooks\nuv run prek install\n```\n\nIn addition, some development commands require [just](https://github.com/casey/just) to be installed.\n\nPrek hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.\n\n### Development Standards\n\n#### Scope\n\nLarge pull requests create review bottlenecks and quality risks. Unless you're fixing a discrete bug or making an incredibly well-scoped change, keep PRs small and focused. \n\nA PR that changes 50 lines across 3 files can be thoroughly reviewed in minutes. A PR that changes 500 lines across 20 files requires hours of careful analysis and often hides subtle issues.\n\nBreaking large features into smaller PRs:\n- Creates better review experiences\n- Makes git history clear\n- Simplifies debugging with bisect\n- Reduces merge conflicts\n- Gets your code merged faster\n\n#### Code Quality\n\nFastMCP values clarity over cleverness. Every line you write will be maintained by someone else - possibly years from now, possibly without context about your decisions.\n\n**PRs can be rejected for two opposing reasons:**\n1. **Insufficient quality** - Code that doesn't meet our standards for clarity, maintainability, or idiomaticity\n2. **Overengineering** - Code that is overbearing, unnecessarily complex, or tries to be too clever\n\nThe focus is on idiomatic, high-quality Python. FastMCP uses patterns like `NotSet` type as an alternative to `None` in certain situations - follow existing patterns.\n\n#### Required Practices\n\n**Full type annotations** on all functions and methods. They catch bugs before runtime and serve as inline documentation.\n\n**Async/await patterns** for all I/O operations. Even if your specific use case doesn't need concurrency, consistency means users can compose features without worrying about blocking operations.\n\n**Descriptive names** make code self-documenting. `auth_token` is clear; `tok` requires mental translation.\n\n**Specific exception types** make error handling predictable. Catching `ValueError` tells readers exactly what error you expect. Never use bare `except` clauses.\n\n#### Anti-Patterns to Avoid\n\n**Complex one-liners** are hard to debug and modify. Break operations into clear steps.\n\n**Mutable default arguments** cause subtle bugs. Use `None` as the default and create the mutable object inside the function.\n\n**Breaking established patterns** confuses readers. If you must deviate, discuss in the issue first.\n\n### Prek Checks\n\n```bash\n# Runs automatically on commit, or manually:\nuv run prek run --all-files\n```\n\nThis runs three critical tools:\n- **Ruff**: Linting and formatting\n- **Prettier**: Code formatting\n- **ty**: Static type checking\n\nPytest runs separately as a distinct workflow step after prek checks pass. CI will reject PRs that fail these checks. Always run them locally first.\n\n### Testing\n\nTests are documentation that shows how features work. Good tests give reviewers confidence and help future maintainers understand intent.\n\n```bash\n# Run specific test directory\nuv run pytest tests/server/ -v\n\n# Run all tests before submitting PR\nuv run pytest\n```\n\nEvery new feature needs tests. See the [Testing Guide](/v2/development/tests) for patterns and requirements.\n\n### Documentation\n\nA feature doesn't exist unless it's documented. Note that FastMCP's hosted documentation always tracks the main branch - users who want historical documentation can clone the repo, checkout a specific tag, and host it themselves.\n\n```bash\n# Preview documentation locally\njust docs\n```\n\nDocumentation requirements:\n- **Explain concepts in prose first** - Code without context is just syntax\n- **Complete, runnable examples** - Every code block should be copy-pasteable\n- **Register in docs.json** - Makes pages appear in navigation\n- **Version badges** - Mark when features were added using `<VersionBadge />`\n\n#### SDK Documentation\n\nFastMCP's SDK documentation is auto-generated from the source code docstrings and type annotations. It is automatically updated on every merge to main by a GitHub Actions workflow, so users are *not* responsible for keeping the documentation up to date. However, to generate it proactively, you can use the following command:\n\n```bash\njust api-ref-all\n```\n\n### Submitting Your PR\n\n#### Before Submitting\n\n1. **Run all checks**: `uv run prek run --all-files && uv run pytest`\n2. **Keep scope small**: One feature or fix per PR\n3. **Write clear description**: Your PR description becomes permanent documentation\n4. **Update docs**: Include documentation for API changes\n\n#### PR Description\n\nWrite PR descriptions that explain:\n- What problem you're solving\n- Why you chose this approach  \n- Any trade-offs or alternatives considered\n- Migration path for breaking changes\n\nFocus on the \"why\" - the code shows the \"what\". Keep it concise but complete.\n\n#### What We Look For\n\n**Framework Philosophy**: FastMCP is NOT trying to do all things or provide all shortcuts. Features are rejected when they don't align with the framework's vision, even if perfectly implemented. The burden of proof is on the PR to demonstrate value.\n\n**Code Quality**: We verify code follows existing patterns. Consistency reduces cognitive load. When every module works similarly, developers understand new code quickly.\n\n**Test Coverage**: Not every line needs testing, but every behavior does. Tests document intent and protect against regressions.\n\n**Breaking Changes**: May be acceptable in minor versions but must be clearly documented. See the [versioning policy](/v2/development/releases#versioning-policy).\n\n## Special Modules\n\n**`contrib`**: Community-maintained patterns and utilities. Original authors maintain their contributions. Not representative of the core framework.\n\n**`experimental`**: Maintainer-developed features that may preview future functionality. Can break or be deleted at any time without notice. Pin your FastMCP version when using these features."
  },
  {
    "path": "docs/v2/development/releases.mdx",
    "content": "---\ntitle: \"Releases\"\ndescription: \"FastMCP versioning and release process\"\nicon: \"truck-fast\"\n---\n\nFastMCP releases frequently to deliver features quickly in the rapidly evolving MCP ecosystem. We use semantic versioning pragmatically - the Model Context Protocol is young, patterns are still emerging, and waiting for perfect stability would mean missing opportunities to empower developers with better tools.\n\n## Versioning Policy\n\n### Semantic Versioning\n\n**Major (x.0.0)**: Complete API redesigns\n\nMajor versions represent fundamental shifts. FastMCP 2.x is entirely different from 1.x in both implementation and design philosophy.\n\n**Minor (2.x.0)**: New features and evolution\n\n<Warning>\nUnlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.\n</Warning>\n\nFastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.\n\n**Patch (2.0.x)**: Bug fixes and refinements\n\nPatch versions contain only bug fixes without breaking changes. These are safe updates you can apply with confidence.\n\n### Breaking Changes\n\nWe permit breaking changes in minor versions because the MCP ecosystem is rapidly evolving. Refusing to break problematic APIs would accumulate design debt that eventually makes the framework unusable. Each breaking change represents a deliberate decision to keep FastMCP aligned with the ecosystem's evolution.\n\nWhen breaking changes occur:\n- They only happen in minor versions (e.g., 2.3.x to 2.4.0)\n- Release notes explain what changed and how to migrate\n- We provide deprecation warnings at least 1 minor version in advance when possible\n- Changes must substantially benefit users to justify disruption\n\nThe public API is what's covered by our compatibility guarantees - these are the parts of FastMCP you can rely on to remain stable within a minor version. The public API consists of:\n- `FastMCP` server class, `Client` class, and FastMCP `Context`\n- Core MCP components: `Tool`, `Prompt`, `Resource`, `ResourceTemplate`, and transports\n- Their public methods and documented behaviors\n\nEverything else (utilities, private methods, internal modules) may change without notice. This boundary lets us refactor internals and improve implementation details without breaking your code. For production stability, pin to specific versions.\n\n<Warning>\nThe `fastmcp.server.auth` module was introduced in 2.12.0 and is exempted from this policy temporarily, meaning it is *expected* to have breaking changes even on patch versions. This is because auth is a rapidly evolving part of the MCP spec and it would be dangerous to be beholden to old decisions. Please pin your FastMCP version if using authentication in production.\n\nWe expect this exemption to last through at least the 2.12.x and 2.13.x release series. \n</Warning>\n\n### Production Use\n\nPin to exact versions:\n```\nfastmcp==2.11.0  # Good\nfastmcp>=2.11.0  # Bad - will install breaking changes\n```\n\n## Creating Releases\n\nOur release process is intentionally simple:\n\n1. Create GitHub release with tag `vMAJOR.MINOR.PATCH` (e.g., `v2.11.0`)\n2. Generate release notes automatically, and curate or add additional editorial information as needed\n3. GitHub releases automatically trigger PyPI deployments\n\nThis automation lets maintainers focus on code quality rather than release mechanics.\n\n### Release Cadence\n\nWe follow a feature-driven release cadence rather than a fixed schedule. Minor versions ship approximately every 3-4 weeks when significant functionality is ready.\n\nPatch releases ship promptly for:\n- Critical bug fixes\n- Security updates (immediate release)\n- Regression fixes\n\nThis approach means you get improvements as soon as they're ready rather than waiting for arbitrary release dates.\n"
  },
  {
    "path": "docs/v2/development/tests.mdx",
    "content": "---\ntitle: \"Tests\"\ndescription: \"Testing patterns and requirements for FastMCP\"\nicon: vial\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nGood tests are the foundation of reliable software. In FastMCP, we treat tests as first-class documentation that demonstrates how features work while protecting against regressions. Every new capability needs comprehensive tests that demonstrate correctness.\n\n## FastMCP Tests\n\n### Running Tests\n\n```bash\n# Run all tests\nuv run pytest\n\n# Run specific test file\nuv run pytest tests/server/test_auth.py\n\n# Run with coverage\nuv run pytest --cov=fastmcp\n\n# Skip integration tests for faster runs\nuv run pytest -m \"not integration\"\n\n# Skip tests that spawn processes\nuv run pytest -m \"not integration and not client_process\"\n```\n\nTests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.\n\n### Test Organization\n\nOur 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.\n\n### Test Markers\n\nWe use pytest markers to categorize tests that require special resources or take longer to run:\n\n```python\n@pytest.mark.integration\nasync def test_github_api_integration():\n    \"\"\"Test GitHub API integration with real service.\"\"\"\n    token = os.getenv(\"FASTMCP_GITHUB_TOKEN\")\n    if not token:\n        pytest.skip(\"FASTMCP_GITHUB_TOKEN not available\")\n    \n    # Test against real GitHub API\n    client = GitHubClient(token)\n    repos = await client.list_repos(\"prefecthq\")\n    assert \"fastmcp\" in [repo.name for repo in repos]\n\n@pytest.mark.client_process\nasync def test_stdio_transport():\n    \"\"\"Test STDIO transport with separate process.\"\"\"\n    # This spawns a subprocess\n    async with Client(\"python examples/simple_echo.py\") as client:\n        result = await client.call_tool(\"echo\", {\"message\": \"test\"})\n        assert result.content[0].text == \"test\"\n```\n\n## Writing Tests\n\n\n### Test Requirements\n\nFollowing these practices creates maintainable, debuggable test suites that serve as both documentation and regression protection.\n\n#### Single Behavior Per Test\n\nEach test should verify exactly one behavior. When it fails, you need to know immediately what broke. A test that checks five things gives you five potential failure points to investigate. A test that checks one thing points directly to the problem.\n\n<CodeGroup>\n\n```python Good: Atomic Test\nasync def test_tool_registration():\n    \"\"\"Test that tools are properly registered with the server.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n    \n    tools = mcp.list_tools()\n    assert len(tools) == 1\n    assert tools[0].name == \"add\"\n```\n\n```python Bad: Multi-Behavior Test\nasync def test_server_functionality():\n    \"\"\"Test multiple server features at once.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    # Tool registration\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n    \n    # Resource creation\n    @mcp.resource(\"config://app\")\n    def get_config():\n        return {\"version\": \"1.0\"}\n    \n    # Authentication setup\n    mcp.auth = BearerTokenProvider({\"token\": \"user\"})\n    \n    # What exactly are we testing? If this fails, what broke?\n    assert mcp.list_tools()\n    assert mcp.list_resources()\n    assert mcp.auth is not None\n```\n\n</CodeGroup>\n\n#### Self-Contained Setup\n\nEvery test must create its own setup. Tests should be runnable in any order, in parallel, or in isolation. When a test fails, you should be able to run just that test to reproduce the issue.\n\n<CodeGroup>\n\n```python Good: Self-Contained\nasync def test_tool_execution_with_error():\n    \"\"\"Test that tool errors are properly handled.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    @mcp.tool\n    def divide(a: int, b: int) -> float:\n        if b == 0:\n            raise ValueError(\"Cannot divide by zero\")\n        return a / b\n    \n    async with Client(mcp) as client:\n        with pytest.raises(Exception):\n            await client.call_tool(\"divide\", {\"a\": 10, \"b\": 0})\n```\n\n```python Bad: Test Dependencies\n# Global state that tests depend on\ntest_server = None\n\ndef test_setup_server():\n    \"\"\"Setup for other tests.\"\"\"\n    global test_server\n    test_server = FastMCP(\"shared-server\")\n\ndef test_server_works():\n    \"\"\"Test server functionality.\"\"\"\n    # Depends on test_setup_server running first\n    assert test_server is not None\n```\n\n</CodeGroup>\n\n#### Clear Intent\n\nTest names and assertions should make the verified behavior obvious. A developer reading your test should understand what feature it validates and how that feature should behave.\n\n```python\nasync def test_authenticated_tool_requires_valid_token():\n    \"\"\"Test that authenticated users can access protected tools.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    mcp.auth = BearerTokenProvider({\"secret-token\": \"test-user\"})\n    \n    @mcp.tool\n    def protected_action() -> str:\n        return \"success\"\n    \n    async with Client(mcp, auth=BearerAuth(\"secret-token\")) as client:\n        result = await client.call_tool(\"protected_action\", {})\n        assert result.content[0].text == \"success\"\n```\n\n#### Using Fixtures\n\nUse fixtures to create reusable data, server configurations, or other resources for your tests. Note that you should **not** open FastMCP clients in your fixtures as it can create hard-to-diagnose issues with event loops.\n\n```python\nimport pytest\nfrom fastmcp import FastMCP, Client\n\n@pytest.fixture\ndef weather_server():\n    server = FastMCP(\"WeatherServer\")\n    \n    @server.tool\n    def get_temperature(city: str) -> dict:\n        temps = {\"NYC\": 72, \"LA\": 85, \"Chicago\": 68}\n        return {\"city\": city, \"temp\": temps.get(city, 70)}\n    \n    return server\n\nasync def test_temperature_tool(weather_server):\n    async with Client(weather_server) as client:\n        result = await client.call_tool(\"get_temperature\", {\"city\": \"LA\"})\n        assert result.data == {\"city\": \"LA\", \"temp\": 85}\n```\n\n#### Effective Assertions\n\nAssertions should be specific and provide context on failure. When a test fails during CI, the assertion message should tell you exactly what went wrong.\n\n```python\n# Basic assertion - minimal context on failure\nassert result.status == \"success\"\n\n# Better - explains what was expected\nassert result.status == \"success\", f\"Expected successful operation, got {result.status}: {result.error}\"\n```\n\nTry not to have too many assertions in a single test unless you truly need to check various aspects of the same behavior. In general, assertions of different behaviors should be in separate tests.\n\n#### Inline Snapshots\n\nFastMCP uses `inline-snapshot` for testing complex data structures. On first run of `pytest --inline-snapshot=create` with an empty `snapshot()`, pytest will auto-populate the expected value. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.\n\n```python\nfrom inline_snapshot import snapshot\n\nasync def test_tool_schema_generation():\n    \"\"\"Test that tool schemas are generated correctly.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    \n    @mcp.tool\n    def calculate_tax(amount: float, rate: float = 0.1) -> dict:\n        \"\"\"Calculate tax on an amount.\"\"\"\n        return {\"amount\": amount, \"tax\": amount * rate, \"total\": amount * (1 + rate)}\n    \n    tools = mcp.list_tools()\n    schema = tools[0].inputSchema\n    \n    # First run: snapshot() is empty, gets auto-populated\n    # Subsequent runs: compares against stored snapshot\n    assert schema == snapshot({\n        \"type\": \"object\", \n        \"properties\": {\n            \"amount\": {\"type\": \"number\"}, \n            \"rate\": {\"type\": \"number\", \"default\": 0.1}\n        }, \n        \"required\": [\"amount\"]\n    })\n```\n\n### In-Memory Testing\n\nFastMCP uses in-memory transport for testing, where servers and clients communicate directly. The majority of functionality can be tested in a deterministic fashion this way. We use more complex setups only when testing transports themselves.\n\nThe in-memory transport runs the real MCP protocol implementation without network overhead. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. Everything runs in the same Python process - you can set breakpoints anywhere and step through with your debugger.\n\n```python\nfrom fastmcp import FastMCP, Client\n\n# Create your server\nserver = FastMCP(\"WeatherServer\")\n\n@server.tool\ndef get_temperature(city: str) -> dict:\n    \"\"\"Get current temperature for a city\"\"\"\n    temps = {\"NYC\": 72, \"LA\": 85, \"Chicago\": 68}\n    return {\"city\": city, \"temp\": temps.get(city, 70)}\n\nasync def test_weather_operations():\n    # Pass server directly - no deployment needed\n    async with Client(server) as client:\n        result = await client.call_tool(\"get_temperature\", {\"city\": \"NYC\"})\n        assert result.data == {\"city\": \"NYC\", \"temp\": 72}\n```\n\nThis pattern makes tests deterministic and fast - typically completing in milliseconds rather than seconds.\n\n### Mocking External Dependencies\n\nFastMCP servers are standard Python objects, so you can mock external dependencies using your preferred approach:\n\n```python\nfrom unittest.mock import AsyncMock\n\nasync def test_database_tool():\n    server = FastMCP(\"DataServer\")\n    \n    # Mock the database\n    mock_db = AsyncMock()\n    mock_db.fetch_users.return_value = [\n        {\"id\": 1, \"name\": \"Alice\"},\n        {\"id\": 2, \"name\": \"Bob\"}\n    ]\n    \n    @server.tool\n    async def list_users() -> list:\n        return await mock_db.fetch_users()\n    \n    async with Client(server) as client:\n        result = await client.call_tool(\"list_users\", {})\n        assert len(result.data) == 2\n        assert result.data[0][\"name\"] == \"Alice\"\n        mock_db.fetch_users.assert_called_once()\n```\n\n### Testing Network Transports\n\nWhile in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports like HTTP or SSE. FastMCP provides two approaches: in-process async servers (preferred), and separate subprocess servers (for special cases).\n\n#### In-Process Network Testing (Preferred)\n\n<VersionBadge version=\"2.13.0\" />\n\nFor most network transport tests, use `run_server_async` as an async context manager. This runs the server as a task in the same process, providing fast, deterministic tests with full debugger support:\n\n```python\nimport pytest\nfrom fastmcp import FastMCP, Client\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.utilities.tests import run_server_async\n\ndef create_test_server() -> FastMCP:\n    \"\"\"Create a test server instance.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    return server\n\n@pytest.fixture\nasync def http_server() -> str:\n    \"\"\"Start server in-process for testing.\"\"\"\n    server = create_test_server()\n    async with run_server_async(server) as url:\n        yield url\n\nasync def test_http_transport(http_server: str):\n    \"\"\"Test actual HTTP transport behavior.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(http_server)\n    ) as client:\n        result = await client.ping()\n        assert result is True\n\n        greeting = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        assert greeting.data == \"Hello, World!\"\n```\n\nThe `run_server_async` context manager automatically handles server lifecycle and cleanup. This approach is faster than subprocess-based testing and provides better error messages.\n\n#### Subprocess Testing (Special Cases)\n\nFor tests that require complete process isolation (like STDIO transport or testing subprocess behavior), use `run_server_in_process`:\n\n```python\nimport pytest\nfrom fastmcp.utilities.tests import run_server_in_process\nfrom fastmcp import FastMCP, Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\ndef run_server(host: str, port: int) -> None:\n    \"\"\"Function to run in subprocess.\"\"\"\n    server = FastMCP(\"TestServer\")\n    \n    @server.tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n    \n    server.run(host=host, port=port)\n\n@pytest.fixture\nasync def http_server():\n    \"\"\"Fixture that runs server in subprocess.\"\"\"\n    with run_server_in_process(run_server, transport=\"http\") as url:\n        yield f\"{url}/mcp\"\n\nasync def test_http_transport(http_server: str):\n    \"\"\"Test actual HTTP transport behavior.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(http_server)\n    ) as client:\n        result = await client.ping()\n        assert result is True\n```\n\nThe `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.\n\n### Documentation Testing\n\nDocumentation requires the same validation as code. The `just docs` command launches a local Mintlify server that renders your documentation exactly as users will see it:\n\n```bash\n# Start local documentation server with hot reload\njust docs\n\n# Or run Mintlify directly\nmintlify dev\n```\n\nThe local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it."
  },
  {
    "path": "docs/v2/development/upgrade-guide.mdx",
    "content": "---\ntitle: Upgrade Guide\nsidebarTitle: Upgrade Guide\ndescription: Migration instructions for upgrading between FastMCP versions\nicon: up\ntag: NEW\n---\n\nThis guide provides migration instructions for breaking changes and major updates when upgrading between FastMCP versions.\n\n## v2.14.0\n\n### OpenAPI Parser Promotion\n\nThe experimental OpenAPI parser is now the standard implementation. The legacy parser has been removed.\n\n**If you were using the legacy parser:** No code changes required. The new parser is a drop-in replacement with improved architecture.\n\n**If you were using the experimental parser:** Update your imports from the experimental module to the standard location:\n\n<CodeGroup>\n```python Before\nfrom fastmcp.experimental.server.openapi import FastMCPOpenAPI, RouteMap, MCPType\n```\n\n```python After\nfrom fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType\n```\n</CodeGroup>\n\nThe experimental imports will continue working temporarily but will show deprecation warnings. The `FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER` environment variable is no longer needed and can be removed.\n\n### Deprecated Features Removed\n\nThe following deprecated features have been removed in v2.14.0:\n\n**BearerAuthProvider** (deprecated in v2.11):\n<CodeGroup>\n```python Before\nfrom fastmcp.server.auth.providers.bearer import BearerAuthProvider\n```\n\n```python After\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n```\n</CodeGroup>\n\n**Context.get_http_request()** (deprecated in v2.2.11):\n<CodeGroup>\n```python Before\nrequest = context.get_http_request()\n```\n\n```python After\nfrom fastmcp.server.dependencies import get_http_request\nrequest = get_http_request()\n```\n</CodeGroup>\n\n**Top-level Image import** (deprecated in v2.8.1):\n<CodeGroup>\n```python Before\nfrom fastmcp import Image\n```\n\n```python After\nfrom fastmcp.utilities.types import Image\n```\n</CodeGroup>\n\n**FastMCP dependencies parameter** (deprecated in v2.11.4):\n<CodeGroup>\n```python Before\nmcp = FastMCP(\"server\", dependencies=[\"requests\", \"pandas\"])\n```\n\n```json After\n{\n  \"environment\": {\n    \"dependencies\": [\"requests\", \"pandas\"]\n  }\n}\n```\n</CodeGroup>\n\n**Legacy resource prefix format**: The `resource_prefix_format` parameter and \"protocol\" format have been removed. Only the \"path\" format is supported (this was already the default).\n\n**FastMCPProxy client parameter**:\n<CodeGroup>\n```python Before\nproxy = FastMCPProxy(client=my_client)\n```\n\n```python After\nproxy = FastMCPProxy(client_factory=lambda: my_client)\n```\n</CodeGroup>\n\n**output_schema=False**:\n<CodeGroup>\n```python Before\n@mcp.tool(output_schema=False)\ndef my_tool() -> str:\n    return \"result\"\n```\n\n```python After\n@mcp.tool(output_schema=None)\ndef my_tool() -> str:\n    return \"result\"\n```\n</CodeGroup>\n\n## v2.13.0\n\n### OAuth Token Key Management\n\nThe OAuth proxy now issues its own JWT tokens to clients instead of forwarding upstream provider tokens. This improves security by maintaining proper token audience boundaries.\n\n**What changed:**\n\nThe OAuth proxy now implements a token factory pattern - it receives tokens from your OAuth provider (GitHub, Google, etc.), encrypts and stores them, then issues its own FastMCP JWT tokens to clients. This requires cryptographic keys for JWT signing and token encryption.\n\n**Default behavior (development):**\n\nBy default, FastMCP automatically manages keys based on your platform:\n- **Mac/Windows**: Keys are auto-managed via system keyring, surviving server restarts with zero configuration. Suitable **only** for development and local testing.\n- **Linux**: Keys are ephemeral (random salt at startup, regenerated on each restart).\n\nThis works fine for development and testing where re-authentication after restart is acceptable.\n\n**For production:**\n\nProduction deployments must provide explicit keys and use persistent storage. Add these three things:\n\n```python\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=\"https://your-server.com\",\n\n    # Explicit keys (required for production)\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n\n    # Persistent network storage (required for production)\n    client_storage=RedisStore(host=\"redis.example.com\", port=6379)\n)\n```\n\n**More information:**\n- [OAuth Token Security](/v2/deployment/http#oauth-token-security) - Complete production setup guide\n- [Key and Storage Management](/v2/servers/auth/oauth-proxy#key-and-storage-management) - Detailed explanation of defaults and production requirements\n- [OAuth Proxy Parameters](/v2/servers/auth/oauth-proxy#configuration-parameters) - Parameter documentation\n"
  },
  {
    "path": "docs/v2/getting-started/installation.mdx",
    "content": "---\ntitle: Installation\nicon: arrow-down-to-line\n---\n## Install FastMCP\n\nWe recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.\n\nIf you plan to use FastMCP in your project, you can add it as a dependency with:\n\n```bash\nuv add fastmcp\n```\n\nAlternatively, you can install it directly with `pip` or `uv pip`:\n<CodeGroup>\n    ```bash uv\n    uv pip install fastmcp\n    ```\n\n    ```bash pip\n    pip install fastmcp\n    ```\n</CodeGroup>\n\n<Warning>\n**FastMCP 3.0** is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`\n</Warning>\n\n### Verify Installation\n\nTo verify that FastMCP is installed correctly, you can run the following command:\n\n```bash\nfastmcp version\n```\n\nYou should see output like the following:\n\n```bash\n$ fastmcp version\n\nFastMCP version:                           2.11.3\nMCP version:                               1.12.4\nPython version:                            3.12.2\nPlatform:            macOS-15.3.1-arm64-arm-64bit\nFastMCP root path:            ~/Developer/fastmcp\n```\n\n### Dependency Licensing\n\n<Info>\nFastMCP depends on Cyclopts for CLI functionality. Cyclopts v4 includes docutils as a transitive dependency, which has complex licensing that may trigger compliance reviews in some organizations.\n\nIf this is a concern, you can install Cyclopts v5 alpha which removes this dependency:\n\n```bash\npip install \"cyclopts>=5.0.0a1\"\n```\n\nAlternatively, wait for the stable v5 release. See [this issue](https://github.com/BrianPugh/cyclopts/issues/672) for details.\n</Info>\n## Upgrading from the Official MCP SDK\n\nUpgrading from the official MCP SDK's FastMCP 1.0 to FastMCP 2.0 is generally straightforward. The core server API is highly compatible, and in many cases, changing your import statement from `from mcp.server.fastmcp import FastMCP` to `from fastmcp import FastMCP` will be sufficient. \n\n\n```python {5}\n# Before\n# from mcp.server.fastmcp import FastMCP\n\n# After\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n```\n\n<Warning>\nPrior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the official 1.0 API. However, as the projects diverge, this can not be guaranteed. You may see deprecation warnings if you attempt to use 1.0 APIs in FastMCP 2.x. Please refer to this documentation for details on new capabilities.\n</Warning>\n\n## Versioning Policy\n\nFastMCP follows semantic versioning with pragmatic adaptations for the rapidly evolving MCP ecosystem. Breaking changes may occur in minor versions (e.g., 2.3.x to 2.4.0) when necessary to stay current with the MCP Protocol.\n\nFor production use, always pin to exact versions:\n```\nfastmcp==2.11.0  # Good\nfastmcp>=2.11.0  # Bad - will install breaking changes\n```\n\nSee the full [versioning and release policy](/v2/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy. \n\n## Contributing to FastMCP\n\nInterested in contributing to FastMCP? See the [Contributing Guide](/v2/development/contributing) for details on:\n- Setting up your development environment\n- Running tests and pre-commit hooks\n- Submitting issues and pull requests\n- Code standards and review process\n"
  },
  {
    "path": "docs/v2/getting-started/quickstart.mdx",
    "content": "---\ntitle: Quickstart\nicon: rocket-launch\n---\n\nWelcome! This guide will help you quickly set up FastMCP, run your first MCP server, and deploy a server to Prefect Horizon.\n\nIf you haven't already installed FastMCP, follow the [installation instructions](/v2/getting-started/installation).\n\n## Create a FastMCP Server\n\nA FastMCP server is a collection of tools, resources, and other MCP components. To create a server, start by instantiating the `FastMCP` class. \n\nCreate a new file called `my_server.py` and add the following code:\n\n```python my_server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n```\n\n\nThat's it! You've created a FastMCP server, albeit a very boring one. Let's add a tool to make it more interesting.\n\n\n## Add a Tool\n\nTo add a tool that returns a simple greeting, write a function and decorate it with `@mcp.tool` to register it with the server:\n\n```python my_server.py {5-7}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n```\n\n\n## Run the Server\n\nThe simplest way to run your FastMCP server is to call its `run()` method. You can choose between different transports, like `stdio` for local servers, or `http` for remote access:\n\n<CodeGroup>\n\n```python my_server.py (stdio) {9, 10}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n```python my_server.py (HTTP) {9, 10}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n</CodeGroup>\n\nThis lets us run the server with `python my_server.py`. The stdio transport is the traditional way to connect MCP servers to clients, while the HTTP transport enables remote connections.\n\n<Tip>\nWhy do we need the `if __name__ == \"__main__\":` block?\n\nThe `__main__` block is recommended for consistency and compatibility, ensuring your server works with all MCP clients that execute your server file as a script. Users who will exclusively run their server with the FastMCP CLI can omit it, as the CLI imports the server object directly.\n</Tip>\n\n### Using the FastMCP CLI\n\nYou can also use the `fastmcp run` command to start your server. Note that the FastMCP CLI **does not** execute the `__main__` block of your server file. Instead, it imports your server object and runs it with whatever transport and options you provide.\n\nFor example, to run this server with the default stdio transport (no matter how you called `mcp.run()`), you can use the following command:\n```bash\nfastmcp run my_server.py:mcp\n```\n\nTo run this server with the HTTP transport, you can use the following command:\n```bash\nfastmcp run my_server.py:mcp --transport http --port 8000\n```\n\n## Call Your Server\n\nOnce your server is running with HTTP transport, you can connect to it with a FastMCP client or any LLM client that supports the MCP protocol:\n\n```python my_client.py\nimport asyncio\nfrom fastmcp import Client\n\nclient = Client(\"http://localhost:8000/mcp\")\n\nasync def call_tool(name: str):\n    async with client:\n        result = await client.call_tool(\"greet\", {\"name\": name})\n        print(result)\n\nasyncio.run(call_tool(\"Ford\"))\n```\n\nNote that:\n- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client\n- We must enter a client context (`async with client:`) before using the client\n- You can make multiple client calls within the same context\n\n## Deploy to Prefect Horizon\n\n[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.\n\n<Info>\nHorizon is **free for personal projects** and offers enterprise governance for teams.\n</Info>\n\nTo deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:\n\n1. Push your `my_server.py` file to a GitHub repository\n2. Sign in to [Prefect Horizon](https://horizon.prefect.io) with your GitHub account\n3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint\n\nThat'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.\n\nFor more details, see the [Prefect Horizon guide](/deployment/prefect-horizon).\n"
  },
  {
    "path": "docs/v2/getting-started/welcome.mdx",
    "content": "---\ntitle: \"Welcome to FastMCP 2.0!\"\nsidebarTitle: \"Welcome!\"\ndescription: The fast, Pythonic way to build MCP servers and clients.\nicon: hand-wave\n---\n\n<img \n  src=\"/assets/brand/f-watercolor-waves.png\" \n  \n  alt=\"'F' logo on a watercolor background\"\n  noZoom\n  className=\"rounded-2xl block dark:hidden\"\n  />\n<img \n  src=\"/assets/brand/f-watercolor-waves-dark.png\" \n  alt=\"'F' logo on a watercolor background\"\n  noZoom\n  className=\"rounded-2xl hidden dark:block\"\n  />\n\n\n**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) provides a standardized way to connect LLMs to tools and data, and FastMCP makes it production-ready with clean, Pythonic code:\n\n```python {1}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Demo 🚀\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Beyond Basic MCP\n\nFastMCP pioneered Python MCP development, and FastMCP 1.0 was incorporated into the [official MCP SDK](https://github.com/modelcontextprotocol/python-sdk) in 2024.\n\n**This is FastMCP 2.0,** the actively maintained version that extends far beyond basic protocol implementation. While the SDK provides core functionality, FastMCP 2.0 delivers everything needed for production: advanced MCP patterns (server composition, proxying, OpenAPI/FastAPI generation, tool transformation), enterprise auth (Google, GitHub, Azure, Auth0, WorkOS, and more), deployment tools, testing frameworks, and comprehensive client libraries.\n\nReady to build? Start with our [installation guide](/v2/getting-started/installation) or jump straight to the [quickstart](/v2/getting-started/quickstart).\n\nFastMCP is made with 💙 by [Prefect](https://www.prefect.io/).\n\n<Warning>\n**FastMCP 3.0** is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`\n</Warning>\n\n## What is MCP?\n\nThe Model Context Protocol lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. It is often described as \"the USB-C port for AI\", providing a uniform way to connect LLMs to resources they can use. It may be easier to think of it as an API, but specifically designed for LLM interactions. MCP servers can:\n\n- Expose data through `Resources` (think of these sort of like GET endpoints; they are used to load information into the LLM's context)\n- Provide functionality through `Tools` (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)\n- Define interaction patterns through `Prompts` (reusable templates for LLM interactions)\n- And more!\n\nFastMCP provides a high-level, Pythonic interface for building, managing, and interacting with these servers.\n\n## Why FastMCP?\n\nFastMCP handles all the complex protocol details so you can focus on building. In most cases, decorating a Python function is all you need — FastMCP handles the rest.\n\n🚀 **Fast**: High-level interface means less code and faster development\n\n🍀 **Simple**: Build MCP servers with minimal boilerplate\n\n🐍 **Pythonic**: Feels natural to Python developers\n\n🔍 **Complete**: Everything for production — enterprise auth (Google, GitHub, Azure, Auth0, WorkOS), deployment tools, testing frameworks, client libraries, and more\n\nFastMCP 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.\n\n<Tip>\n**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.\n</Tip>\n\n## LLM-Friendly Docs\n\nThe FastMCP documentation is available in multiple LLM-friendly formats:\n\n### MCP Server\n\nThe FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`. \n\nIn fact, you can use FastMCP to search the FastMCP docs:\n\n```python\nimport asyncio\nfrom fastmcp import Client\n\nasync def main():\n    async with Client(\"https://gofastmcp.com/mcp\") as client:\n        result = await client.call_tool(\n            name=\"SearchFastMcp\", \n            arguments={\"query\": \"deploy a FastMCP server\"}\n        )\n    print(result)\n\nasyncio.run(main())\n```\n\n### Text Formats\n\nThe docs are also available in [llms.txt format](https://llmstxt.org/):\n- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages\n- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows)\n\nAny page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`.\n\nYou can also copy any page as markdown by pressing \"Cmd+C\" (or \"Ctrl+C\" on Windows) on your keyboard.\n"
  },
  {
    "path": "docs/v2/integrations/anthropic.mdx",
    "content": "---\ntitle: Anthropic API 🤝 FastMCP\nsidebarTitle: Anthropic API\ndescription: Connect FastMCP servers to the Anthropic API\nicon: message-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n\nAnthropic's [Messages API](https://docs.anthropic.com/en/api/messages) supports MCP servers as remote tool sources. This tutorial will show you how to create a FastMCP server and deploy it to a public URL, then how to call it from the Messages API.\n\n<Tip>\nCurrently, the MCP connector only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to Claude. Other MCP features like resources and prompts are not currently supported. You can read more about the MCP connector in the [Anthropic documentation](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector).\n</Tip>\n\n## Create a Server\n\nFirst, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n## Deploy the Server\n\nYour server must be deployed to a public URL in order for Anthropic to access it. The MCP connector supports both SSE and Streamable HTTP transports.\n\nFor development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.\n\nAssuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:\n\n<CodeGroup>\n```bash FastMCP server\npython server.py\n```\n\n```bash ngrok\nngrok http 8000\n```\n</CodeGroup>\n\n<Warning>\nThis exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.\n</Warning>\n\n## Call the Server\n\nTo use the Messages API with MCP servers, you'll need to install the Anthropic Python SDK (not included with FastMCP):\n\n```bash\npip install anthropic\n```\n\nYou'll also need to authenticate with Anthropic. You can do this by setting the `ANTHROPIC_API_KEY` environment variable. Consult the Anthropic SDK documentation for more information.\n\n```bash\nexport ANTHROPIC_API_KEY=\"your-api-key\"\n```\n\nHere is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**\n\n```python {5, 13-22}\nimport anthropic\nfrom rich import print\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\nclient = anthropic.Anthropic()\n\nresponse = client.beta.messages.create(\n    model=\"claude-sonnet-4-20250514\",\n    max_tokens=1000,\n    messages=[{\"role\": \"user\", \"content\": \"Roll a few dice!\"}],\n    mcp_servers=[\n        {\n            \"type\": \"url\",\n            \"url\": f\"{url}/mcp/\",\n            \"name\": \"dice-server\",\n        }\n    ],\n    extra_headers={\n        \"anthropic-beta\": \"mcp-client-2025-04-04\"\n    }\n)\n\nprint(response.content)\n```\n\nIf you run this code, you'll see something like the following output:\n\n```text\nI'll roll some dice for you! Let me use the dice rolling tool.\n\nI rolled 3 dice and got: 4, 2, 6\n\nThe results were 4, 2, and 6. Would you like me to roll again or roll a different number of dice?\n```\n\n\n## Authentication\n\n<VersionBadge version=\"2.6.0\" />\n\nThe MCP connector supports OAuth authentication through authorization tokens, which means you can secure your server while still allowing Anthropic to access it.\n\n### Server Authentication\n\nThe simplest way to add authentication to the server is to use a bearer token scheme. \n\nFor this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/v2/servers/auth/token-verification) documentation. \n\nWe'll start by creating an RSA key pair to sign and verify tokens.\n\n```python\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n```\n\n<Warning>\nFastMCP's `RSAKeyPair` utility is for development and testing only.\n</Warning> \n\nNext, we'll create a `JWTVerifier` to authenticate the server. \n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n```\n\nHere is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**\n\n```python server.py [expandable]\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\nimport random\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    print(f\"\\n---\\n\\n🔑 Dice Roller access token:\\n\\n{access_token}\\n\\n---\\n\")\n    mcp.run(transport=\"http\", port=8000)\n```\n\n### Client Authentication\n\nIf 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.\n\n```python\nError code: 400 - {\n    \"type\": \"error\", \n    \"error\": {\n        \"type\": \"invalid_request_error\", \n        \"message\": \"MCP server 'dice-server' requires authentication. Please provide an authorization_token.\",\n    },\n}\n```\n\nTo authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:\n\n```python {8, 21}\nimport anthropic\nfrom rich import print\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\n# Your access token (replace with your actual token)\naccess_token = 'your-access-token'\n\nclient = anthropic.Anthropic()\n\nresponse = client.beta.messages.create(\n    model=\"claude-sonnet-4-20250514\",\n    max_tokens=1000,\n    messages=[{\"role\": \"user\", \"content\": \"Roll a few dice!\"}],\n    mcp_servers=[\n        {\n            \"type\": \"url\",\n            \"url\": f\"{url}/mcp/\",\n            \"name\": \"dice-server\",\n            \"authorization_token\": access_token\n        }\n    ],\n    extra_headers={\n        \"anthropic-beta\": \"mcp-client-2025-04-04\"\n    }\n)\n\nprint(response.content)\n```\n\nYou should now see the dice roll results in the output.\n"
  },
  {
    "path": "docs/v2/integrations/auth0.mdx",
    "content": "---\ntitle: Auth0 OAuth 🤝 FastMCP\nsidebarTitle: Auth0\ndescription: Secure your FastMCP server with Auth0 OAuth\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.4\" />\n\nThis guide shows you how to secure your FastMCP server using **Auth0 OAuth**. While Auth0 does have support for Dynamic Client Registration, it is not enabled by default so this integration uses the [**OIDC Proxy**](/v2/servers/auth/oidc-proxy) pattern to bridge Auth0's dynamic OIDC configuration with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. An **[Auth0 Account](https://auth0.com/)** with access to create Applications\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create an Auth0 Application\n\nCreate an Application in your Auth0 settings to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to Applications\">\n    Go to **Applications → Applications** in your Auth0 account.\n\n    Click **\"+ Create Application\"** to create a new application.\n</Step>\n\n<Step title=\"Create Your Application\">\n    - **Name**: Choose a name users will recognize (e.g., \"My FastMCP Server\")\n    - **Choose an application type**: Choose \"Single Page Web Applications\"\n    - Click **Create** to create the application\n</Step>\n\n<Step title=\"Configure Your Application\">\n    Select the \"Settings\" tab for your application, then find the \"Application URIs\" section.\n\n    - **Allowed Callback URLs**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    - Click **Save** to save your changes\n\n    <Warning>\n    The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.\n    </Warning>\n\n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0Provider.\n    </Tip>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After creating the app, in the \"Basic Information\" section you'll see:\n\n    - **Client ID**: A public identifier like `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`\n    - **Client Secret**: A private hidden value that should always be stored securely\n\n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n\n<Step title=\"Select Your Audience\">\n  Go to **Applications → APIs** in your Auth0 account.\n\n    - Find the API that you want to use for your application\n    - **API Audience**: A URL that uniquely identifies the API\n\n    <Tip>\n    Store this along with of the credentials above. Never commit this to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `Auth0Provider`.\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\n\n# The Auth0Provider utilizes Auth0 OIDC configuration\nauth_provider = Auth0Provider(\n    config_url=\"https://.../.well-known/openid-configuration\",  # Your Auth0 configuration URL\n    client_id=\"tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB\",               # Your Auth0 application Client ID\n    client_secret=\"vPYqbjemq...\",                               # Your Auth0 application Client Secret\n    audience=\"https://...\",                                     # Your Auth0 API audience\n    base_url=\"http://localhost:8000\",                           # Must match your application configuration\n    # redirect_path=\"/auth/callback\"                            # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"Auth0 Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_token_info() -> dict:\n    \"\"\"Returns information about the Auth0 token.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n\n    token = get_access_token()\n\n    return {\n        \"issuer\": token.claims.get(\"iss\"),\n        \"audience\": token.claims.get(\"aud\"),\n        \"scope\": token.claims.get(\"scope\")\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Auth0 authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Auth0-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Auth0 OAuth flows\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Auth0 login in your browser\n        print(\"✓ Authenticated with Auth0!\")\n\n        # Test the protected tool\n        result = await client.call_tool(\"get_token_info\")\n        print(f\"Auth0 audience: {result['audience']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Auth0's authorization page\n2. After you authorize the app, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = Auth0Provider(\n    config_url=\"https://.../.well-known/openid-configuration\",\n    client_id=\"tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB\",\n    client_secret=\"vPYqbjemq...\",\n    audience=\"https://...\",\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Auth0 App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Environment Variables\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the Auth0 provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.auth0.Auth0Provider` to use Auth0 authentication.\n</ParamField>\n</Card>\n\n### Auth0-Specific Configuration\n\nThese environment variables provide default values for the Auth0 provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL\" required>\nYour Auth0 Application Configuration URL (e.g., `https://.../.well-known/openid-configuration`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID\" required>\nYour Auth0 Application Client ID (e.g., `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET\" required>\nYour Auth0 Application Client Secret (e.g., `vPYqbjemq...`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE\" required>\nYour Auth0 API Audience\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_BASE_URL\" required>\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your Auth0 Application\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES\" default='[\"openid\"]'>\nComma-, space-, or JSON-separated list of required AUth0 scopes (e.g., `openid email` or `[\"openid\",\"email\"]`)\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the Auth0 provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider\n\n# Auth0 configuration and credentials\nFASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration\nFASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB\nFASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET=vPYqbjemq...\nFASTMCP_SERVER_AUTH_AUTH0_AUDIENCE=https://...\nFASTMCP_SERVER_AUTH_AUTH0_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_AUTH0_REQUIRED_SCOPES=openid,email\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"Auth0 Secured App\")\n\n@mcp.tool\nasync def search_logs() -> list[str]:\n    \"\"\"Search the service logs.\"\"\"\n    # Your tool implementation here\n    pass\n```\n"
  },
  {
    "path": "docs/v2/integrations/authkit.mdx",
    "content": "---\ntitle: AuthKit 🤝 FastMCP\nsidebarTitle: AuthKit\ndescription: Secure your FastMCP server with AuthKit by WorkOS\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nThis 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**](/v2/servers/auth/remote-oauth) pattern, where AuthKit handles user login and your FastMCP server validates the tokens.\n\n\n## Configuration\n### Prerequisites\n\nBefore you begin, you will need:\n1.  A **[WorkOS Account](https://workos.com/)** and a new **Project**.\n2.  An **[AuthKit](https://www.authkit.com/)** instance configured within your WorkOS project.\n3.  Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`).\n\n### Step 1: AuthKit Configuration\n\nIn your WorkOS Dashboard, enable AuthKit and configure the following settings:\n\n<Steps>\n<Step title=\"Enable Dynamic Client Registration\">\n    Go to **Applications → Configuration** and enable **Dynamic Client Registration**. This allows MCP clients register with your application automatically.\n\n    ![Enable Dynamic Client Registration](./images/authkit/enable_dcr.png)\n</Step>\n\n<Step title=\"Note Your AuthKit Domain\">\n    Find your **AuthKit Domain** on the configuration page. It will look like `https://your-project-12345.authkit.app`. You'll need this for your FastMCP server configuration.\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server file and use the `AuthKitProvider` to handle all the OAuth integration automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\n# The AuthKitProvider automatically discovers WorkOS endpoints\n# and configures JWT token validation\nauth_provider = AuthKitProvider(\n    authkit_domain=\"https://your-project-12345.authkit.app\",\n    base_url=\"http://localhost:8000\"  # Use your actual server URL\n)\n\nmcp = FastMCP(name=\"AuthKit Secured App\", auth=auth_provider)\n```\n\n## Testing\n\nTo 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:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nNow, you can use a FastMCP client to test that you can reach your server after authenticating:\n\n```python\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        assert await client.ping()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n\n## Environment Variables\n\n<VersionBadge version=\"2.12.1\" />\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the AuthKit provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.workos.AuthKitProvider` to use AuthKit authentication.\n</ParamField>\n</Card>\n\n### AuthKit-Specific Configuration\n\nThese environment variables provide default values for the AuthKit provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN\" required>\nYour AuthKit domain (e.g., `https://your-project-12345.authkit.app`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_BASE_URL\" required>\nPublic URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_REQUIRED_SCOPES\" default=\"[]\">\nComma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid profile email` or `[\"openid\", \"profile\", \"email\"]`)\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the AuthKit provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.AuthKitProvider\n\n# AuthKit configuration\nFASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN=https://your-project-12345.authkit.app\nFASTMCP_SERVER_AUTH_AUTHKITPROVIDER_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_AUTHKITPROVIDER_REQUIRED_SCOPES=openid,profile,email\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"AuthKit Secured App\")\n```"
  },
  {
    "path": "docs/v2/integrations/aws-cognito.mdx",
    "content": "---\ntitle: AWS Cognito OAuth 🤝 FastMCP\nsidebarTitle: AWS Cognito\ndescription: Secure your FastMCP server with AWS Cognito user pools\nicon: aws\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.4\" />\n\nThis guide shows you how to secure your FastMCP server using **AWS Cognito user pools**. Since AWS Cognito doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/v2/servers/auth/oauth-proxy) pattern to bridge AWS Cognito's traditional OAuth with MCP's authentication requirements. It also includes robust JWT token validation, ensuring enterprise-grade authentication.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. An **[AWS Account](https://aws.amazon.com/)** with access to create AWS Cognito user pools\n2. Basic familiarity with AWS Cognito concepts (user pools, app clients)\n3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create an AWS Cognito User Pool and App Client\n\nSet up AWS Cognito user pool with an app client to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to AWS Cognito\">\n    Go to the **[AWS Cognito Console](https://console.aws.amazon.com/cognito/)** and ensure you're in your desired AWS region.\n\n    Select **\"User pools\"** from the side navigation (click on the hamburger icon at the top left in case you don't see any), and click **\"Create user pool\"** to create a new user pool.\n</Step>\n\n<Step title=\"Define Your Application\">\n    AWS Cognito now provides a streamlined setup experience:\n\n    1. **Application type**: Select **\"Traditional web application\"** (this is the correct choice for FastMCP server-side authentication)\n    2. **Name your application**: Enter a descriptive name (e.g., `FastMCP Server`)\n\n    The traditional web application type automatically configures:\n    - Server-side authentication with client secrets\n    - Authorization code grant flow\n    - Appropriate security settings for confidential clients\n\n    <Info>\n    Choose \"Traditional web application\" rather than SPA, Mobile app, or Machine-to-machine options. This ensures proper OAuth 2.0 configuration for FastMCP.\n    </Info>\n</Step>\n\n<Step title=\"Configure Options\">\n    AWS will guide you through configuration options:\n\n    - **Sign-in identifiers**: Choose how users will sign in (email, username, or phone)\n    - **Required attributes**: Select any additional user information you need\n    - **Return URL**: Add your callback URL (e.g., `http://localhost:8000/auth/callback` for development)\n\n    <Tip>\n    The simplified interface handles most OAuth security settings automatically based on your application type selection.\n    </Tip>\n</Step>\n\n<Step title=\"Review and Create\">\n    Review your configuration and click **\"Create user pool\"**.\n\n    After creation, you'll see your user pool details. Save these important values:\n    - **User pool ID** (format: `eu-central-1_XXXXXXXXX`)\n    - **Client ID** (found under → \"Applications\" → \"App clients\" in the side navigation → \\<Your application name, e.g., `FastMCP Server`\\> → \"App client information\")\n    - **Client Secret** (found under → \"Applications\" → \"App clients\" in the side navigation → \\<Your application name, e.g., `FastMCP Server`\\> → \"App client information\")\n\n    <Tip>\n    The user pool ID and app client credentials are all you need for FastMCP configuration.\n    </Tip>\n</Step>\n\n<Step title=\"Configure OAuth Settings\">\n    Under \"Login pages\" in your app client's settings, you can double check and adjust the OAuth configuration:\n\n    - **Allowed callback URLs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    - **Allowed sign-out URLs**: Optional, for logout functionality\n    - **OAuth 2.0 grant types**: Ensure \"Authorization code grant\" is selected\n    - **OpenID Connect scopes**: Select scopes your application needs (e.g., `openid`, `email`, `profile`)\n\n    <Tip>\n    For local development, you can use `http://localhost` URLs. For production, you must use HTTPS.\n    </Tip>\n</Step>\n\n<Step title=\"Configure Resource Server\">\n    AWS Cognito requires a resource server entry to support OAuth with protected resources. Without this, token exchange will fail with an `invalid_grant` error.\n\n    Navigate to **\"Branding\" → \"Domain\"** in the side navigation, then:\n\n    1. Click **\"Create resource server\"**\n    2. **Resource server name**: Enter a descriptive name (e.g., `My MCP Server`)\n    3. **Resource server identifier**: Enter your MCP endpoint URL exactly as it will be accessed (e.g., `http://localhost:8000/mcp` for development, or `https://your-server.com/mcp` for production)\n    4. Click **\"Create resource server\"**\n\n    <Warning>\n    The resource server identifier must exactly match your `base_url + mcp_path`. For the default configuration with `base_url=\"http://localhost:8000\"` and `path=\"/mcp\"`, use `http://localhost:8000/mcp`.\n    </Warning>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After setup, you'll have:\n\n    - **User Pool ID**: Format like `eu-central-1_XXXXXXXXX`\n    - **Client ID**: Your application's client identifier\n    - **Client Secret**: Generated client secret (keep secure)\n    - **AWS Region**: Where Your AWS Cognito user pool is located\n\n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or AWS Secrets Manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cognito's JWT tokens and user claims automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.aws import AWSCognitoProvider\nfrom fastmcp.server.dependencies import get_access_token\n\n# The AWSCognitoProvider handles JWT validation and user claims\nauth_provider = AWSCognitoProvider(\n    user_pool_id=\"eu-central-1_XXXXXXXXX\",   # Your AWS Cognito user pool ID\n    aws_region=\"eu-central-1\",               # AWS region (defaults to eu-central-1)\n    client_id=\"your-app-client-id\",          # Your app client ID\n    client_secret=\"your-app-client-secret\",  # Your app client Secret\n    base_url=\"http://localhost:8000\",        # Must match your callback URL\n    # redirect_path=\"/auth/callback\"         # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"AWS Cognito Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_access_token_claims() -> dict:\n    \"\"\"Get the authenticated user's access token claims.\"\"\"\n    token = get_access_token()\n    return {\n        \"sub\": token.claims.get(\"sub\"),\n        \"username\": token.claims.get(\"username\"),\n        \"cognito:groups\": token.claims.get(\"cognito:groups\", []),\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by AWS Cognito OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with Your AWS Cognito-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle AWS Cognito OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open AWS Cognito login in your browser\n        print(\"✓ Authenticated with AWS Cognito!\")\n\n        # Test the protected tool\n        print(\"Calling protected tool: get_access_token_claims\")\n        result = await client.call_tool(\"get_access_token_claims\")\n        user_data = result.data\n        print(\"Available access token claims:\")\n        print(f\"- sub: {user_data.get('sub', 'N/A')}\")\n        print(f\"- username: {user_data.get('username', 'N/A')}\")\n        print(f\"- cognito:groups: {user_data.get('cognito:groups', [])}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to AWS Cognito's hosted UI login page\n2. After you sign in (or sign up), you'll be redirected back to your MCP server\n3. The client receives the JWT token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.aws import AWSCognitoProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = AWSCognitoProvider(\n    user_pool_id=\"eu-central-1_XXXXXXXXX\",\n    aws_region=\"eu-central-1\",\n    client_id=\"your-app-client-id\",\n    client_secret=\"your-app-client-secret\",\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production AWS Cognito App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Environment Variables\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the AWS Cognito provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.aws.AWSCognitoProvider` to use AWS Cognito authentication.\n</ParamField>\n</Card>\n\n### AWS Cognito-Specific Configuration\n\nThese environment variables provide default values for the AWS Cognito provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID\" required>\nYour AWS Cognito user pool ID (e.g., `eu-central-1_XXXXXXXXX`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION\" default=\"eu-central-1\">\nAWS region where your AWS Cognito user pool is located\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID\" required>\nYour AWS Cognito app client ID\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET\" required>\nYour AWS Cognito app client secret\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL\" default=\"http://localhost:8000\">\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_REDIRECT_PATH\" default=\"/auth/callback\">\nOne of the redirect paths configured in your AWS Cognito app client\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AWS_COGNITO_REQUIRED_SCOPES\" default='[\"openid\"]'>\nComma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid email` or `[\"openid\",\"email\",\"profile\"]`)\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the AWS Cognito provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.aws.AWSCognitoProvider\n\n# AWS Cognito credentials\nFASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID=eu-central-1_XXXXXXXXX\nFASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION=eu-central-1\nFASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID=your-app-client-id\nFASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET=your-app-client-secret\nFASTMCP_SERVER_AUTH_AWS_COGNITO_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_AWS_COGNITO_REQUIRED_SCOPES=openid,email,profile\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_access_token\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"AWS Cognito Secured App\")\n\n@mcp.tool\nasync def get_access_token_claims() -> dict:\n    \"\"\"Get the authenticated user's access token claims.\"\"\"\n    token = get_access_token()\n    return {\n        \"sub\": token.claims.get(\"sub\"),\n        \"username\": token.claims.get(\"username\"),\n        \"cognito:groups\": token.claims.get(\"cognito:groups\", []),\n    }\n```\n\n## Features\n\n### JWT Token Validation\n\nThe AWS Cognito provider includes robust JWT token validation:\n\n- **Signature Verification**: Validates tokens against AWS Cognito's public keys (JWKS)\n- **Expiration Checking**: Automatically rejects expired tokens\n- **Issuer Validation**: Ensures tokens come from your specific AWS Cognito user pool\n- **Scope Enforcement**: Verifies required OAuth scopes are present\n\n### User Claims and Groups\n\nAccess rich user information from AWS Cognito JWT tokens:\n\n```python\nfrom fastmcp.server.dependencies import get_access_token\n\n@mcp.tool\nasync def admin_only_tool() -> str:\n    \"\"\"A tool only available to admin users.\"\"\"\n    token = get_access_token()\n    user_groups = token.claims.get(\"cognito:groups\", [])\n\n    if \"admin\" not in user_groups:\n        raise ValueError(\"This tool requires admin access\")\n\n    return \"Admin access granted!\"\n```\n\n### Enterprise Integration\n\nPerfect for enterprise environments with:\n\n- **Single Sign-On (SSO)**: Integrate with corporate identity providers\n- **Multi-Factor Authentication (MFA)**: Leverage AWS Cognito's built-in MFA\n- **User Groups**: Role-based access control through AWS Cognito groups\n- **Custom Attributes**: Access custom user attributes defined in your AWS Cognito user pool\n- **Compliance**: Meet enterprise security and compliance requirements"
  },
  {
    "path": "docs/v2/integrations/azure.mdx",
    "content": "---\ntitle: Azure (Microsoft Entra ID) OAuth 🤝 FastMCP\nsidebarTitle: Azure (Entra ID)\ndescription: Secure your FastMCP server with Azure/Microsoft Entra OAuth\nicon: microsoft\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nThis guide shows you how to secure your FastMCP server using **Azure OAuth** (Microsoft Entra ID). Since Azure doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/v2/servers/auth/oauth-proxy) pattern to bridge Azure's traditional OAuth with MCP's authentication requirements. FastMCP validates Azure JWTs against your application's client_id.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. An **[Azure Account](https://portal.azure.com/)** with access to create App registrations\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n3. Your Azure tenant ID (found in Azure Portal under Microsoft Entra ID)\n\n### Step 1: Create an Azure App Registration\n\nCreate an App registration in Azure Portal to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to App registrations\">\n    Go to the [Azure Portal](https://portal.azure.com) and navigate to **Microsoft Entra ID → App registrations**.\n    \n    Click **\"New registration\"** to create a new application.\n</Step>\n\n<Step title=\"Configure Your Application\">\n    Fill in the application details:\n    \n    - **Name**: Choose a name users will recognize (e.g., \"My FastMCP Server\")\n    - **Supported account types**: Choose based on your needs:\n      - **Single tenant**: Only users in your organization\n      - **Multitenant**: Users in any Microsoft Entra directory\n      - **Multitenant + personal accounts**: Any Microsoft account\n    - **Redirect URI**: Select \"Web\" and enter your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    \n    <Warning>\n    The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Azure allows `http://localhost` URLs. For production, you must use HTTPS.\n    </Warning>\n    \n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the AzureProvider.\n    </Tip>\n\n    - **Expose an API**: Configure your Application ID URI and define scopes\n      - Go to **Expose an API** in the App registration sidebar.\n      - Click **Set** next to \"Application ID URI\" and choose one of:\n        - Keep the default `api://{client_id}`\n        - Set a custom value, following the supported formats (see [Identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions))\n      - Click **Add a scope** and create a scope your app will require, for example:\n        - Scope name: `read` (or `write`, etc.)\n        - Admin consent display name/description: as appropriate for your org\n        - Who can consent: as needed (Admins only or Admins and users)\n\n    - **Configure Access Token Version**: Ensure your app uses access token v2\n      - Go to **Manifest** in the App registration sidebar.\n      - Find the `requestedAccessTokenVersion` property and set it to `2`:\n        ```json\n        \"api\": {\n            \"requestedAccessTokenVersion\": 2\n        }\n        ```\n      - Click **Save** at the top of the manifest editor.\n\n    <Warning>\n    Access token v2 is required for FastMCP's Azure integration to work correctly. If this is not set, you may encounter authentication errors.\n    </Warning>\n\n    <Note>\n    In FastMCP's `AzureProvider`, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.\n    </Note>\n\n\n</Step>\n\n\n<Step title=\"Create Client Secret\">\n    After registration, navigate to **Certificates & secrets** in your app's settings.\n    \n    - Click **\"New client secret\"**\n    - Add a description (e.g., \"FastMCP Server\")\n    - Choose an expiration period\n    - Click **\"Add\"**\n    \n    <Warning>\n    Copy the secret value immediately - it won't be shown again! You'll need to create a new secret if you lose it.\n    </Warning>\n</Step>\n\n<Step title=\"Note Your Credentials\">\n    From the **Overview** page of your app registration, note:\n    \n    - **Application (client) ID**: A UUID like `835f09b6-0f0f-40cc-85cb-f32c5829a149`\n    - **Directory (tenant) ID**: A UUID like `08541b6e-646d-43de-a0eb-834e6713d6d5`\n    - **Client Secret**: The value you copied in the previous step\n    \n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider\n\n# The AzureProvider handles Azure's token format and validation\nauth_provider = AzureProvider(\n    client_id=\"835f09b6-0f0f-40cc-85cb-f32c5829a149\",  # Your Azure App Client ID\n    client_secret=\"your-client-secret\",                 # Your Azure App Client Secret\n    tenant_id=\"08541b6e-646d-43de-a0eb-834e6713d6d5\", # Your Azure Tenant ID (REQUIRED)\n    base_url=\"http://localhost:8000\",                   # Must match your App registration\n    required_scopes=[\"your-scope\"],                 # At least one scope REQUIRED - name of scope from your App\n    # identifier_uri defaults to api://{client_id}\n    # identifier_uri=\"api://your-api-id\",\n    # Optional: request additional upstream scopes in the authorize request\n    # additional_authorize_scopes=[\"User.Read\", \"offline_access\", \"openid\", \"email\"],\n    # redirect_path=\"/auth/callback\"                  # Default value, customize if needed\n    # base_authority=\"login.microsoftonline.us\"      # For Azure Government (default: login.microsoftonline.com)\n)\n\nmcp = FastMCP(name=\"Azure Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated Azure user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n    \n    token = get_access_token()\n    # The AzureProvider stores user data in token claims\n    return {\n        \"azure_id\": token.claims.get(\"sub\"),\n        \"email\": token.claims.get(\"email\"),\n        \"name\": token.claims.get(\"name\"),\n        \"job_title\": token.claims.get(\"job_title\"),\n        \"office_location\": token.claims.get(\"office_location\")\n    }\n```\n\n<Note>\n**Important**: The `tenant_id` parameter is **REQUIRED**. Azure no longer supports using \"common\" for new applications due to security requirements. You must use one of:\n\n- **Your specific tenant ID**: Found in Azure Portal (e.g., `08541b6e-646d-43de-a0eb-834e6713d6d5`)\n- **\"organizations\"**: For work and school accounts only\n- **\"consumers\"**: For personal Microsoft accounts only\n\nUsing your specific tenant ID is recommended for better security and control.\n</Note>\n\n<Note>\n**Important**: The `required_scopes` parameter is **REQUIRED** and must include at least one scope. Azure's OAuth API requires the `scope` parameter in all authorization requests - you cannot authenticate without specifying at least one scope. Use the unprefixed scope names from your Azure App registration (e.g., `[\"read\", \"write\"]`). These scopes must be created under **Expose an API** in your App registration.\n</Note>\n\n### Scope Handling\n\nFastMCP automatically prefixes `required_scopes` with your `identifier_uri` (e.g., `api://your-client-id`) since these are your custom API scopes. Scopes in `additional_authorize_scopes` are sent as-is since they target external resources like Microsoft Graph.\n\n**`required_scopes`** — Your custom API scopes, defined in Azure \"Expose an API\":\n\n| You write | Sent to Azure | Validated on tokens |\n|-----------|---------------|---------------------|\n| `mcp-read` | `api://xxx/mcp-read` | ✓ |\n| `my.scope` | `api://xxx/my.scope` | ✓ |\n| `openid` | `openid` | ✗ (OIDC scope) |\n| `api://xxx/read` | `api://xxx/read` | ✓ |\n\n**`additional_authorize_scopes`** — External scopes (e.g., Microsoft Graph) for server-side use:\n\n| You write | Sent to Azure | Validated on tokens |\n|-----------|---------------|---------------------|\n| `User.Read` | `User.Read` | ✗ |\n| `Mail.Send` | `Mail.Send` | ✗ |\n\n<Info>\n**Why aren't `additional_authorize_scopes` validated?** Azure issues separate tokens per resource. The access token FastMCP receives is for *your API*—Graph scopes aren't in its `scp` claim. To call Graph APIs, your server uses the upstream Azure token in an on-behalf-of (OBO) flow.\n</Info>\n\n<Note>\nOIDC scopes (`openid`, `profile`, `email`, `offline_access`) are never prefixed and excluded from validation because Azure doesn't include them in access token `scp` claims.\n</Note>\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Azure OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Azure-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Azure OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Azure login in your browser\n        print(\"✓ Authenticated with Azure!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"Azure user: {result['email']}\")\n        print(f\"Name: {result['name']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Microsoft's authorization page\n2. Sign in with your Microsoft account (work, school, or personal based on your tenant configuration)\n3. Grant the requested permissions\n4. After authorization, you'll be redirected back\n5. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = AzureProvider(\n    client_id=\"835f09b6-0f0f-40cc-85cb-f32c5829a149\",\n    client_secret=\"your-client-secret\",\n    tenant_id=\"08541b6e-646d-43de-a0eb-834e6713d6d5\",\n    base_url=\"https://your-production-domain.com\",\n    required_scopes=[\"your-scope\"],\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Azure App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Environment Variables\n\n<VersionBadge version=\"2.12.1\" />\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the Azure provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.azure.AzureProvider` to use Azure authentication.\n</ParamField>\n</Card>\n\n### Azure-Specific Configuration\n\nThese environment variables provide default values for the Azure provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID\" required>\nYour Azure App registration Client ID (e.g., `835f09b6-0f0f-40cc-85cb-f32c5829a149`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET\" required>\nYour Azure App registration Client Secret\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID\" required>\nYour Azure tenant ID (specific ID, \"organizations\", or \"consumers\")\n\n<Note>\nThis is **REQUIRED**. Find your tenant ID in Azure Portal under Microsoft Entra ID → Overview.\n</Note>\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_BASE_URL\" default=\"http://localhost:8000\">\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your Azure App registration\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES\" required>\nComma-, space-, or JSON-separated list of required scopes for your API (at least one scope required). These are validated on tokens and used as defaults if the client does not request specific scopes. Use unprefixed scope names from your Azure App registration (e.g., `read,write`).\n\nYou can include standard OIDC scopes (`openid`, `profile`, `email`, `offline_access`) in `required_scopes`. FastMCP automatically handles them correctly: they're sent to Azure unprefixed and excluded from token validation (since Azure doesn't include OIDC scopes in access token `scp` claims).\n\n<Note>\nAzure's OAuth API requires the `scope` parameter - you must provide at least one scope.\n</Note>\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_ADDITIONAL_AUTHORIZE_SCOPES\" default=\"\">\nComma-, space-, or JSON-separated list of additional scopes to include in the authorization request without prefixing. Use this to request upstream scopes such as Microsoft Graph permissions. These are not used for token validation.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_IDENTIFIER_URI\" default=\"api://{client_id}\">\nApplication ID URI used to prefix scopes during authorization.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY\" default=\"login.microsoftonline.com\">\nAzure authority base URL. Override this to use Azure Government:\n\n- `login.microsoftonline.com` - Azure Public Cloud (default)\n- `login.microsoftonline.us` - Azure Government\n\nThis setting affects all Azure OAuth endpoints (authorization, token, issuer, JWKS).\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the Azure provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.azure.AzureProvider\n\n# Azure OAuth credentials\nFASTMCP_SERVER_AUTH_AZURE_CLIENT_ID=835f09b6-0f0f-40cc-85cb-f32c5829a149\nFASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET=your-client-secret-here\nFASTMCP_SERVER_AUTH_AZURE_TENANT_ID=08541b6e-646d-43de-a0eb-834e6713d6d5\nFASTMCP_SERVER_AUTH_AZURE_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES=read,write\n# Optional custom API configuration\n# FASTMCP_SERVER_AUTH_AZURE_IDENTIFIER_URI=api://your-api-id\n# Request additional upstream scopes (optional)\n# FASTMCP_SERVER_AUTH_AZURE_ADDITIONAL_AUTHORIZE_SCOPES=User.Read,Mail.Read\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"Azure Secured App\")\n\n@mcp.tool\nasync def protected_tool(query: str) -> str:\n    \"\"\"A tool that requires Azure authentication to access.\"\"\"\n    # Your tool implementation here\n    return f\"Processing authenticated request: {query}\"\n```\n\n"
  },
  {
    "path": "docs/v2/integrations/chatgpt.mdx",
    "content": "---\ntitle: ChatGPT 🤝 FastMCP\nsidebarTitle: ChatGPT\ndescription: Connect FastMCP servers to ChatGPT in Chat and Deep Research modes\nicon: message-smile\ntag: NEW\n---\n\nChatGPT supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval.\n\n<Tip>\n**Developer Mode Required for Chat Mode**: To use MCP servers in regular ChatGPT conversations, you must first enable Developer Mode in your ChatGPT settings. This feature is available for ChatGPT Pro, Team, Enterprise, and Edu users.\n</Tip>\n\n<Note>\nOpenAI's official MCP documentation and examples are built with **FastMCP v2**! Learn more from their [MCP documentation](https://platform.openai.com/docs/mcp) and [Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode).\n</Note>\n\n## Build a Server\n\nFirst, let's create a simple FastMCP server:\n\n```python server.py\nfrom fastmcp import FastMCP\nimport random\n\nmcp = FastMCP(\"Demo Server\")\n\n@mcp.tool\ndef roll_dice(sides: int = 6) -> int:\n    \"\"\"Roll a dice with the specified number of sides.\"\"\"\n    return random.randint(1, sides)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n### Deploy Your Server\n\nYour server must be accessible from the internet. For development, use `ngrok`:\n\n<CodeGroup>\n```bash Terminal 1\npython server.py\n```\n\n```bash Terminal 2\nngrok http 8000\n```\n</CodeGroup>\n\nNote your public URL (e.g., `https://abc123.ngrok.io`) for the next steps.\n\n## Chat Mode\n\nChat mode lets you use MCP tools directly in ChatGPT conversations. See [OpenAI's Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode) for the latest requirements.\n\n### Add to ChatGPT\n\n#### 1. Enable Developer Mode\n\n1. Open ChatGPT and go to **Settings** → **Connectors**\n2. Under **Advanced**, toggle **Developer Mode** to enabled\n\n#### 2. Create Connector\n\n1. In **Settings** → **Connectors**, click **Create**\n2. Enter:\n   - **Name**: Your server name\n   - **Server URL**: `https://your-server.ngrok.io/mcp/`\n3. Check **I trust this provider**\n4. Add authentication if needed\n5. Click **Create**\n\n<Note>\n**Without Developer Mode**: If you don't have search/fetch tools, ChatGPT will reject the server. With Developer Mode enabled, you don't need search/fetch tools for Chat mode.\n</Note>\n\n#### 3. Use in Chat\n\n1. Start a new chat\n2. Click the **+** button → **More** → **Developer Mode**\n3. **Enable your MCP server connector** (required - the connector must be explicitly added to each chat)\n4. Now you can use your tools:\n\nExample usage:\n- \"Roll a 20-sided dice\"\n- \"Roll dice\" (uses default 6 sides)\n\n<Tip>\nThe connector must be explicitly enabled in each chat session through Developer Mode. Once added, it remains active for the entire conversation.\n</Tip>\n\n### Skip Confirmations\n\nUse `annotations={\"readOnlyHint\": True}` to skip confirmation prompts for read-only tools:\n\n```python\n@mcp.tool(annotations={\"readOnlyHint\": True})\ndef get_status() -> str:\n    \"\"\"Check system status.\"\"\"\n    return \"All systems operational\"\n\n@mcp.tool()  # No annotation - ChatGPT may ask for confirmation\ndef delete_item(id: str) -> str:\n    \"\"\"Delete an item.\"\"\"\n    return f\"Deleted {id}\"\n```\n\n## Deep Research Mode\n\nDeep Research mode provides systematic information retrieval with citations. See [OpenAI's MCP documentation](https://platform.openai.com/docs/mcp) for the latest Deep Research specifications.\n\n<Warning>\n**Search and Fetch Required**: Without Developer Mode, ChatGPT will reject any server that doesn't have both `search` and `fetch` tools. Even in Developer Mode, Deep Research only uses these two tools.\n</Warning>\n\n### Tool Implementation\n\nDeep Research tools must follow this pattern:\n\n```python\n@mcp.tool()\ndef search(query: str) -> dict:\n    \"\"\"\n    Search for records matching the query.\n    Must return {\"ids\": [list of string IDs]}\n    \"\"\"\n    # Your search logic\n    matching_ids = [\"id1\", \"id2\", \"id3\"]\n    return {\"ids\": matching_ids}\n\n@mcp.tool()\ndef fetch(id: str) -> dict:\n    \"\"\"\n    Fetch a complete record by ID.\n    Return the full record data for ChatGPT to analyze.\n    \"\"\"\n    # Your fetch logic\n    return {\n        \"id\": id,\n        \"title\": \"Record Title\",\n        \"content\": \"Full record content...\",\n        \"metadata\": {\"author\": \"Jane Doe\", \"date\": \"2024\"}\n    }\n```\n\n### Using Deep Research\n\n1. Ensure your server is added to ChatGPT's connectors (same as Chat mode)\n2. Start a new chat\n3. Click **+** → **Deep Research**\n4. Select your MCP server as a source\n5. Ask research questions\n\nChatGPT will use your `search` and `fetch` tools to find and cite relevant information.\n\n"
  },
  {
    "path": "docs/v2/integrations/claude-code.mdx",
    "content": "---\ntitle: Claude Code 🤝 FastMCP\nsidebarTitle: Claude Code\ndescription: Install and use FastMCP servers in Claude Code\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\nClaude Code supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.\n\n## Requirements\n\nThis integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Claude Code's built-in MCP management commands.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.10.3\" />\n\nThe easiest way to install a FastMCP server in Claude Code is using the `fastmcp install claude-code` command. This automatically handles the configuration, dependency management, and calls Claude Code's built-in MCP management system.\n\n```bash\nfastmcp install claude-code server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install claude-code server.py\nfastmcp install claude-code server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install claude-code server.py:my_custom_server\n```\n\nThe command will automatically configure the server with Claude Code's `claude mcp add` command.\n\n#### Dependencies\n\nFastMCP provides flexible dependency management options for your Claude Code servers:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install claude-code server.py --with pandas --with requests\n```\n\n**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:\n\n```bash\nfastmcp install claude-code server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install claude-code server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Configuration\n\nControl the Python environment for your server with these options:\n\n**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:\n\n```bash\nfastmcp install claude-code server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:\n\n```bash\nfastmcp install claude-code server.py --project /path/to/my-project\n```\n\n#### Environment Variables\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install claude-code server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install claude-code server.py --server-name \"Weather Server\" --env-file .env\n```\n\n<Warning>\n**Claude Code must be installed**. The integration looks for the Claude Code CLI at the default installation location (`~/.claude/local/claude`) and uses the `claude mcp add` command to register servers.\n</Warning>\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually use Claude Code's built-in MCP management commands. This gives you direct control over how your server is launched:\n\n```bash\n# Add a server with custom configuration\nclaude mcp add dice-roller -- uv run --with fastmcp fastmcp run server.py\n\n# Add with environment variables\nclaude mcp add weather-server -e API_KEY=secret -e DEBUG=true -- uv run --with fastmcp fastmcp run server.py\n\n# Add with specific scope (local, user, or project)\nclaude mcp add my-server --scope user -- uv run --with fastmcp fastmcp run server.py\n```\n\nYou can also manually specify Python versions and project directories in your Claude Code commands:\n\n```bash\n# With specific Python version\nclaude mcp add ml-server -- uv run --python 3.11 --with fastmcp fastmcp run server.py\n\n# Within a project directory\nclaude mcp add project-server -- uv run --project /path/to/project --with fastmcp fastmcp run server.py\n```\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Claude Code.\n\nTry asking Claude something like:\n\n> \"Roll some dice for me\"\n\nClaude will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:\n\n> I'll roll some dice for you! Here are your results: [4, 2, 6]\n> \n> You rolled three dice and got a 4, a 2, and a 6!\n\nClaude Code can now access all the tools, resources, and prompts you've defined in your FastMCP server. \n\nIf your server provides resources, you can reference them with `@` mentions using the format `@server:protocol://resource/path`. If your server provides prompts, you can use them as slash commands with `/mcp__servername__promptname`."
  },
  {
    "path": "docs/v2/integrations/claude-desktop.mdx",
    "content": "---\ntitle: Claude Desktop 🤝 FastMCP\nsidebarTitle: Claude Desktop\ndescription: Connect FastMCP servers to Claude Desktop\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\nClaude Desktop supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.\n\n<Note>\nRemote MCP server support is currently in beta and available for users on Claude Pro, Max, Team, and Enterprise plans (as of June 2025). Most users will still need to use local STDIO connections.\n</Note>\n\n<Note>\nThis guide focuses specifically on using FastMCP servers with Claude Desktop. For general Claude Desktop MCP setup and official examples, see the [official Claude Desktop quickstart guide](https://modelcontextprotocol.io/quickstart/user).\n</Note>\n\n\n## Requirements\n\nClaude Desktop traditionally requires MCP servers to run locally using STDIO transport, where your server communicates with Claude through standard input/output rather than HTTP. However, users on certain plans now have access to remote server support as well.\n\n<Tip>\nIf you don't have access to remote server support or need to connect to remote servers, you can create a **proxy server** that runs locally via STDIO and forwards requests to remote HTTP servers. See the [Proxy Servers](#proxy-servers) section below.\n</Tip>\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.10.3\" />\n\nThe easiest way to install a FastMCP server in Claude Desktop is using the `fastmcp install claude-desktop` command. This automatically handles the configuration and dependency management.\n\n<Tip>\nPrior to version 2.10.3, Claude Desktop could be managed by running `fastmcp install <path>` without specifying the client.\n</Tip>\n\n```bash\nfastmcp install claude-desktop server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install claude-desktop server.py\nfastmcp install claude-desktop server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install claude-desktop server.py:my_custom_server\n```\n\nAfter installation, restart Claude Desktop completely. You should see a hammer icon (🔨) in the bottom left of the input box, indicating that MCP tools are available.\n\n#### Dependencies\n\nFastMCP provides several ways to manage your server's dependencies when installing in Claude Desktop:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install claude-desktop server.py --with pandas --with requests\n```\n\n**Requirements file**: If you have a `requirements.txt` file listing all your dependencies, use `--with-requirements` to install them all at once:\n\n```bash\nfastmcp install claude-desktop server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: For local packages in development, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install claude-desktop server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Directory\n\nFastMCP allows you to control the Python environment for your server:\n\n**Python version**: Use `--python` to specify which Python version your server should run with. This is particularly useful when your server requires a specific Python version:\n\n```bash\nfastmcp install claude-desktop server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project directory. This ensures that `uv` will discover all `pyproject.toml`, `uv.toml`, and `.python-version` files from that project:\n\n```bash\nfastmcp install claude-desktop server.py --project /path/to/my-project\n```\n\nWhen you specify a project directory, all relative paths in your server will be resolved from that directory, and the project's virtual environment will be used.\n\n#### Environment Variables\n\n<Warning>\nClaude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install claude-desktop server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install claude-desktop server.py --server-name \"Weather Server\" --env-file .env\n```\n<Warning>\n- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.\n- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.\n</Warning>\n\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually edit Claude Desktop's configuration file. You can open the configuration file from Claude's developer settings, or find it in the following locations:\n- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`\n- **Windows**: `%APPDATA%\\Claude\\claude_desktop_config.json`\n\nThe configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/your/server.py\"]\n    }\n  }\n}\n```\n\nAfter updating the configuration file, restart Claude Desktop completely. Look for the hammer icon (🔨) to confirm your server is loaded.\n\n#### Dependencies\n\nIf your server has dependencies, you can use `uv` or another package manager to set up the environment.\n\n\nWhen manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration uses `uv run` to create an isolated environment with your specified packages:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--with\", \"fastmcp\",\n        \"--with\", \"pandas\",\n        \"--with\", \"requests\", \n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nYou can also manually specify Python versions and project directories in your configuration. Add `--python` to use a specific Python version, or `--project` to run within a project directory:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--python\", \"3.11\",\n        \"--project\", \"/path/to/project\",\n        \"--with\", \"fastmcp\",\n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nThe order of arguments matters: Python version and project settings come before package specifications, which come before the actual command to run.\n\n<Warning>\n- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.\n- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.\n</Warning>\n\n#### Environment Variables\n\nYou can also specify environment variables in the configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"weather-server\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/weather_server.py\"],\n      \"env\": {\n        \"API_KEY\": \"your-api-key\",\n        \"DEBUG\": \"true\"\n      }\n    }\n  }\n}\n```\n<Warning>\nClaude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\n\n## Remote Servers\n\n\nUsers on Claude Pro, Max, Team, and Enterprise plans have first-class remote server support via integrations. For other users, or as an alternative approach, FastMCP can create a proxy server that forwards requests to a remote HTTP server. You can install the proxy server in Claude Desktop.\n\nCreate a proxy server that connects to a remote HTTP server:\n\n```python proxy_server.py\nfrom fastmcp import FastMCP\n\n# Create a proxy to a remote server\nproxy = FastMCP.as_proxy(\n    \"https://example.com/mcp/sse\", \n    name=\"Remote Server Proxy\"\n)\n\nif __name__ == \"__main__\":\n    proxy.run()  # Runs via STDIO for Claude Desktop\n```\n\n### Authentication\n\nFor authenticated remote servers, create an authenticated client following the guidance in the [client auth documentation](/v2/clients/auth/bearer) and pass it to the proxy:\n\n```python auth_proxy_server.py {7}\nfrom fastmcp import FastMCP, Client\nfrom fastmcp.client.auth import BearerAuth\n\n# Create authenticated client\nclient = Client(\n    \"https://api.example.com/mcp/sse\",\n    auth=BearerAuth(token=\"your-access-token\")\n)\n\n# Create proxy using the authenticated client\nproxy = FastMCP.as_proxy(client, name=\"Authenticated Proxy\")\n\nif __name__ == \"__main__\":\n    proxy.run()\n```\n\n"
  },
  {
    "path": "docs/v2/integrations/cursor.mdx",
    "content": "---\ntitle: Cursor 🤝 FastMCP\nsidebarTitle: Cursor\ndescription: Install and use FastMCP servers in Cursor\nicon: message-smile\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\nCursor supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers.\n\n## Requirements\n\nThis integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly in Cursor's settings.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.10.3\" />\n\nThe easiest way to install a FastMCP server in Cursor is using the `fastmcp install cursor` command. This automatically handles the configuration, dependency management, and opens Cursor with a deeplink to install the server.\n\n```bash\nfastmcp install cursor server.py\n```\n\n#### Workspace Installation\n<VersionBadge version=\"2.12.0\" />\n\nBy default, FastMCP installs servers globally for Cursor. You can also install servers to project-specific workspaces using the `--workspace` flag:\n\n```bash\n# Install to current directory's .cursor/ folder\nfastmcp install cursor server.py --workspace .\n\n# Install to specific workspace\nfastmcp install cursor server.py --workspace /path/to/project\n```\n\nThis creates a `.cursor/mcp.json` configuration file in the specified workspace directory, allowing different projects to have their own MCP server configurations.\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install cursor server.py\nfastmcp install cursor server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install cursor server.py:my_custom_server\n```\n\nAfter running the command, Cursor will open automatically and prompt you to install the server. The command will be `uv`, which is expected as this is a Python STDIO server. Click \"Install\" to confirm:\n\n![Cursor install prompt](./cursor-install-mcp.png)\n\n#### Dependencies\n\nFastMCP offers multiple ways to manage dependencies for your Cursor servers:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install cursor server.py --with pandas --with requests\n```\n\n**Requirements file**: For projects with a `requirements.txt` file, use `--with-requirements` to install all dependencies at once:\n\n```bash\nfastmcp install cursor server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: When developing local packages, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install cursor server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Configuration\n\nControl your server's Python environment with these options:\n\n**Python version**: Use `--python` to specify which Python version your server should use. This is essential when your server requires specific Python features:\n\n```bash\nfastmcp install cursor server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project context. This ensures `uv` discovers all project configuration files and uses the correct virtual environment:\n\n```bash\nfastmcp install cursor server.py --project /path/to/my-project\n```\n\n#### Environment Variables\n\n<Warning>\nCursor runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install cursor server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install cursor server.py --server-name \"Weather Server\" --env-file .env\n```\n\n<Warning>\n**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.\n</Warning>\n\n### Generate MCP JSON\n\n<Note>\n**Use the first-class integration above for the best experience.** The MCP JSON generation is useful for advanced use cases, manual configuration, or integration with other tools.\n</Note>\n\nYou can generate MCP JSON configuration for manual use:\n\n```bash\n# Generate configuration and output to stdout\nfastmcp install mcp-json server.py --server-name \"Dice Roller\" --with pandas\n\n# Copy configuration to clipboard for easy pasting\nfastmcp install mcp-json server.py --server-name \"Dice Roller\" --copy\n```\n\nThis generates the standard `mcpServers` configuration format that can be used with any MCP-compatible client.\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually edit Cursor's configuration file. The configuration file is located at:\n- **All platforms**: `~/.cursor/mcp.json`\n\nThe configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/your/server.py\"]\n    }\n  }\n}\n```\n\nAfter updating the configuration file, your server should be available in Cursor.\n\n#### Dependencies\n\nIf your server has dependencies, you can use `uv` or another package manager to set up the environment.\n\nWhen manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration should use `uv run` to create an isolated environment with your specified packages:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--with\", \"fastmcp\",\n        \"--with\", \"pandas\",\n        \"--with\", \"requests\", \n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nYou can also manually specify Python versions and project directories in your configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"dice-roller\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--python\", \"3.11\",\n        \"--project\", \"/path/to/project\",\n        \"--with\", \"fastmcp\",\n        \"fastmcp\",\n        \"run\",\n        \"path/to/your/server.py\"\n      ]\n    }\n  }\n}\n```\n\nNote that the order of arguments is important: Python version and project settings should come before package specifications.\n\n<Warning>\n**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.\n</Warning>\n\n#### Environment Variables\n\nYou can also specify environment variables in the configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"weather-server\": {\n      \"command\": \"python\",\n      \"args\": [\"path/to/weather_server.py\"],\n      \"env\": {\n        \"API_KEY\": \"your-api-key\",\n        \"DEBUG\": \"true\"\n      }\n    }\n  }\n}\n```\n\n<Warning>\nCursor runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.\n</Warning>\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Cursor's AI assistant.\n\nTry asking Cursor something like:\n\n> \"Roll some dice for me\"\n\nCursor will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:\n\n> 🎲 Here are your dice rolls: 4, 6, 4\n> \n> You rolled 3 dice with a total of 14! The 6 was a nice high roll there!\n\nThe AI assistant can now access all the tools, resources, and prompts you've defined in your FastMCP server.\n"
  },
  {
    "path": "docs/v2/integrations/descope.mdx",
    "content": "---\ntitle: Descope 🤝 FastMCP\nsidebarTitle: Descope\ndescription: Secure your FastMCP server with Descope\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.12.4\" />\n\nThis guide shows you how to secure your FastMCP server using [**Descope**](https://www.descope.com), a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/v2/servers/auth/remote-oauth) pattern, where Descope handles user login and your FastMCP server validates the tokens.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n\n1. To [sign up](https://www.descope.com/sign-up) for a Free Forever Descope account\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)\n\n### Step 1: Configure Descope\n\n<Steps>\n<Step title=\"Create an MCP Server\">\n    1. Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, and create a new MCP Server.\n    2. Give the MCP server a name and description.\n    3. Ensure that **Dynamic Client Registration (DCR)** is enabled. Then click **Create**.\n    4. Once you've created the MCP Server, note your Well-Known URL.\n    \n    \n    <Warning>\n    DCR is required for FastMCP clients to automatically register with your authentication server.\n    </Warning>\n</Step>\n\n<Step title=\"Note Your Well-Known URL\">\n    Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers):\n    ```\n    Well-Known URL: https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration\n    ```\n</Step>\n</Steps>\n\n### Step 2: Environment Setup\n\nCreate a `.env` file with your Descope configuration:\n\n```bash\nDESCOPE_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration     # Your Descope Well-Known URL\nSERVER_URL=http://localhost:3000     # Your server's base URL\n```\n\n### Step 3: FastMCP Configuration\n\nCreate your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.descope import DescopeProvider\n\n# The DescopeProvider automatically discovers Descope endpoints\n# and configures JWT token validation\nauth_provider = DescopeProvider(\n    config_url=https://.../.well-known/openid-configuration,        # Your MCP Server .well-known URL\n    base_url=SERVER_URL,                  # Your server's public URL\n)\n\n# Create FastMCP server with auth\nmcp = FastMCP(name=\"My Descope Protected Server\", auth=auth_provider)\n\n```\n\n## Testing\n\nTo test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the environment variables with your actual values!), you can run the following command:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nNow, you can use a FastMCP client to test that you can reach your server after authenticating:\n\n```python\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        assert await client.ping()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Environment Variables\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the Descope provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n  <ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\n    Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use\n    Descope authentication.\n  </ParamField>\n</Card>\n\n### Descope-Specific Configuration\n\nThese environment variables provide default values for the Descope provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL\" required>\nYour Well-Known URL from the [Descope Console](https://app.descope.com/mcp-servers)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL\" required>\n  Public URL of your FastMCP server (e.g., `https://your-server.com` or\n  `http://localhost:8000` for development)\n</ParamField>\n</Card>\n\nExample `.env` file:\n\n```bash\n# Use the Descope provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.descope.DescopeProvider\n\n# Descope configuration\nFASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration\nFASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL=https://your-server.com\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"My Descope Protected Server\")\n```\n"
  },
  {
    "path": "docs/v2/integrations/discord.mdx",
    "content": "---\ntitle: Discord OAuth 🤝 FastMCP\nsidebarTitle: Discord\ndescription: Secure your FastMCP server with Discord OAuth\nicon: discord\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.2\" />\n\nThis guide shows you how to secure your FastMCP server using **Discord OAuth**. Since Discord doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/v2/servers/auth/oauth-proxy) pattern to bridge Discord's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[Discord Account](https://discord.com/)** with access to create applications\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a Discord Application\n\nCreate an application in the Discord Developer Portal to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to Discord Developer Portal\">\n    Go to the [Discord Developer Portal](https://discord.com/developers/applications).\n\n    Click **\"New Application\"** and give it a name users will recognize (e.g., \"My FastMCP Server\").\n</Step>\n\n<Step title=\"Configure OAuth2 Settings\">\n    In the left sidebar, click **\"OAuth2\"**.\n\n    In the **Redirects** section, click **\"Add Redirect\"** and enter your callback URL:\n    - For development: `http://localhost:8000/auth/callback`\n    - For production: `https://your-domain.com/auth/callback`\n\n    <Warning>\n    The redirect URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. Discord allows `http://localhost` URLs for development. For production, use HTTPS.\n    </Warning>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    On the same OAuth2 page, you'll find:\n\n    - **Client ID**: A numeric string like `12345`\n    - **Client Secret**: Click \"Reset Secret\" to generate one\n\n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `DiscordProvider`, which handles Discord's OAuth flow automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.discord import DiscordProvider\n\nauth_provider = DiscordProvider(\n    client_id=\"12345\",      # Your Discord Application Client ID\n    client_secret=\"your-client-secret\",    # Your Discord OAuth Client Secret\n    base_url=\"http://localhost:8000\",      # Must match your OAuth configuration\n)\n\nmcp = FastMCP(name=\"Discord Secured App\", auth=auth_provider)\n\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated Discord user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n\n    token = get_access_token()\n    return {\n        \"discord_id\": token.claims.get(\"sub\"),\n        \"username\": token.claims.get(\"username\"),\n        \"avatar\": token.claims.get(\"avatar\"),\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Discord OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Discord-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        print(\"✓ Authenticated with Discord!\")\n\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"Discord user: {result['username']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Discord's authorization page\n2. Sign in with your Discord account and authorize the app\n3. After authorization, you'll be redirected back\n4. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Discord Scopes\n\nDiscord OAuth supports several scopes for accessing different types of user data:\n\n| Scope | Description |\n|-------|-------------|\n| `identify` | Access username, avatar, and discriminator (default) |\n| `email` | Access the user's email address |\n| `guilds` | Access the user's list of servers |\n| `guilds.join` | Ability to add the user to a server |\n\nTo request additional scopes:\n\n```python\nauth_provider = DiscordProvider(\n    client_id=\"...\",\n    client_secret=\"...\",\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"identify\", \"email\"],\n)\n```\n\n## Production Configuration\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.discord import DiscordProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\nauth_provider = DiscordProvider(\n    client_id=\"12345\",\n    client_secret=os.environ[\"DISCORD_CLIENT_SECRET\"],\n    base_url=\"https://your-production-domain.com\",\n\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Discord App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Environment Variables\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the Discord provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.discord.DiscordProvider` to use Discord authentication.\n</ParamField>\n</Card>\n\n### Discord-Specific Configuration\n\nThese environment variables provide default values for the Discord provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID\" required>\nYour Discord Application Client ID (e.g., `12345`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET\" required>\nYour Discord OAuth Client Secret\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_BASE_URL\" default=\"http://localhost:8000\">\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your Discord OAuth settings\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_REQUIRED_SCOPES\" default='[\"identify\"]'>\nComma-, space-, or JSON-separated list of required Discord scopes (e.g., `identify,email` or `[\"identify\",\"email\"]`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_DISCORD_TIMEOUT_SECONDS\" default=\"10\">\nHTTP request timeout for Discord API calls\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.discord.DiscordProvider\n\nFASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID=12345\nFASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET=your-client-secret\nFASTMCP_SERVER_AUTH_DISCORD_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_DISCORD_REQUIRED_SCOPES=identify,email\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Discord Secured App\")\n\n@mcp.tool\nasync def protected_tool(query: str) -> str:\n    \"\"\"A tool that requires Discord authentication to access.\"\"\"\n    return f\"Processing authenticated request: {query}\"\n```\n"
  },
  {
    "path": "docs/v2/integrations/eunomia-authorization.mdx",
    "content": "---\ntitle: Eunomia Authorization 🤝 FastMCP\nsidebarTitle: Eunomia Auth\ndescription: Add policy-based authorization to your FastMCP servers with Eunomia\nicon: shield-check\n---\n\nAdd **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Eunomia][eunomia-github] authorization middleware**.\n\nControl which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic JSON-based policies and obtain a comprehensive audit log of all access attempts and violations.\n\n## How it Works\n\nExploiting FastMCP's [Middleware][fastmcp-middleware], the Eunomia middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks.\n\n### Listing Operations\n\nThe middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant EunomiaMiddleware as Eunomia Middleware\n    participant MCPServer as FastMCP Server\n    participant EunomiaServer as Eunomia Server\n\n    MCPClient->>EunomiaMiddleware: MCP Listing Request (e.g., tools/list)\n    EunomiaMiddleware->>MCPServer: MCP Listing Request\n    MCPServer-->>EunomiaMiddleware: MCP Listing Response\n    EunomiaMiddleware->>EunomiaServer: Authorization Checks\n    EunomiaServer->>EunomiaMiddleware: Authorization Decisions\n    EunomiaMiddleware-->>MCPClient: Filtered MCP Listing Response\n```\n\n### Execution Operations\n\nThe middleware behaves as a firewall for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant EunomiaMiddleware as Eunomia Middleware\n    participant MCPServer as FastMCP Server\n    participant EunomiaServer as Eunomia Server\n\n    MCPClient->>EunomiaMiddleware: MCP Execution Request (e.g., tools/call)\n    EunomiaMiddleware->>EunomiaServer: Authorization Check\n    EunomiaServer->>EunomiaMiddleware: Authorization Decision\n    EunomiaMiddleware-->>MCPClient: MCP Unauthorized Error (if denied)\n    EunomiaMiddleware->>MCPServer: MCP Execution Request (if allowed)\n    MCPServer-->>EunomiaMiddleware: MCP Execution Response (if allowed)\n    EunomiaMiddleware-->>MCPClient: MCP Execution Response (if allowed)\n```\n\n## Add Authorization to Your Server\n\n<Note>\nEunomia is an AI-specific authorization server that handles policy decisions. The server runs embedded within your MCP server by default for a zero-effort configuration, but can alternatively be run remotely for centralized policy decisions.\n\n</Note>\n\n### Create a Server with Authorization\n\nFirst, install the `eunomia-mcp` package:\n\n```bash\npip install eunomia-mcp\n```\n\nThen create a FastMCP server and add the Eunomia middleware in one line:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom eunomia_mcp import create_eunomia_middleware\n\n# Create your FastMCP server\nmcp = FastMCP(\"Secure MCP Server 🔒\")\n\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n# Add middleware to your server\nmiddleware = create_eunomia_middleware(policy_file=\"mcp_policies.json\")\nmcp.add_middleware(middleware)\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Configure Access Policies\n\nUse the `eunomia-mcp` CLI in your terminal to manage your authorization policies:\n\n```bash\n# Create a default policy file\neunomia-mcp init\n\n# Or create a policy file customized for your FastMCP server\neunomia-mcp init --custom-mcp \"app.server:mcp\"\n```\n\nThis creates `mcp_policies.json` file that you can further edit to your access control needs.\n\n```bash\n# Once edited, validate your policy file\neunomia-mcp validate mcp_policies.json\n```\n\n### Run the Server\n\nStart your FastMCP server normally:\n\n```bash\npython server.py\n```\n\nThe middleware will now intercept all MCP requests and check them against your policies. Requests include agent identification through headers like `X-Agent-ID`, `X-User-ID`, `User-Agent`, or `Authorization` and an automatic mapping of MCP methods to authorization resources and actions.\n\n<Tip>\n  For detailed policy configuration, custom authentication, and remote\n  deployments, visit the [Eunomia MCP Middleware\n  repository][eunomia-mcp-github].\n</Tip>\n\n[eunomia-github]: https://github.com/whataboutyou-ai/eunomia\n[eunomia-mcp-github]: https://github.com/whataboutyou-ai/eunomia/tree/main/pkgs/extensions/mcp\n[fastmcp-middleware]: /servers/middleware\n"
  },
  {
    "path": "docs/v2/integrations/fastapi.mdx",
    "content": "---\ntitle: FastAPI 🤝 FastMCP\nsidebarTitle: FastAPI\ndescription: Integrate FastMCP with FastAPI applications\nicon: bolt\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nFastMCP provides two powerful ways to integrate with FastAPI applications:\n\n1. **[Generate an MCP server FROM your FastAPI app](#generating-an-mcp-server)** - Convert existing API endpoints into MCP tools\n2. **[Mount an MCP server INTO your FastAPI app](#mounting-an-mcp-server)** - Add MCP functionality to your web application\n\n\n<Tip>\nGenerating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters.\n\nWe recommend using the FastAPI integration for bootstrapping and prototyping, not for mirroring your API to LLM clients. See the post [Stop Converting Your REST APIs to MCP](https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp) for more details.\n</Tip>\n\n\n<Note>\nFastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.\n</Note>\n\n## Example FastAPI Application\n\nThroughout this guide, we'll use this e-commerce API as our example (click the `Copy` button to copy it for use with other code blocks):\n\n```python [expandable]\n# Copy this FastAPI server into other code blocks in this guide\n\nfrom fastapi import FastAPI, HTTPException\nfrom pydantic import BaseModel\n\n# Models\nclass Product(BaseModel):\n    name: str\n    price: float\n    category: str\n    description: str | None = None\n\nclass ProductResponse(BaseModel):\n    id: int\n    name: str\n    price: float\n    category: str\n    description: str | None = None\n\n# Create FastAPI app\napp = FastAPI(title=\"E-commerce API\", version=\"1.0.0\")\n\n# In-memory database\nproducts_db = {\n    1: ProductResponse(\n        id=1, name=\"Laptop\", price=999.99, category=\"Electronics\"\n    ),\n    2: ProductResponse(\n        id=2, name=\"Mouse\", price=29.99, category=\"Electronics\"\n    ),\n    3: ProductResponse(\n        id=3, name=\"Desk Chair\", price=299.99, category=\"Furniture\"\n    ),\n}\nnext_id = 4\n\n@app.get(\"/products\", response_model=list[ProductResponse])\ndef list_products(\n    category: str | None = None,\n    max_price: float | None = None,\n) -> list[ProductResponse]:\n    \"\"\"List all products with optional filtering.\"\"\"\n    products = list(products_db.values())\n    if category:\n        products = [p for p in products if p.category == category]\n    if max_price:\n        products = [p for p in products if p.price <= max_price]\n    return products\n\n@app.get(\"/products/{product_id}\", response_model=ProductResponse)\ndef get_product(product_id: int):\n    \"\"\"Get a specific product by ID.\"\"\"\n    if product_id not in products_db:\n        raise HTTPException(status_code=404, detail=\"Product not found\")\n    return products_db[product_id]\n\n@app.post(\"/products\", response_model=ProductResponse)\ndef create_product(product: Product):\n    \"\"\"Create a new product.\"\"\"\n    global next_id\n    product_response = ProductResponse(id=next_id, **product.model_dump())\n    products_db[next_id] = product_response\n    next_id += 1\n    return product_response\n\n@app.put(\"/products/{product_id}\", response_model=ProductResponse)\ndef update_product(product_id: int, product: Product):\n    \"\"\"Update an existing product.\"\"\"\n    if product_id not in products_db:\n        raise HTTPException(status_code=404, detail=\"Product not found\")\n    products_db[product_id] = ProductResponse(\n        id=product_id,\n        **product.model_dump(),\n    )\n    return products_db[product_id]\n\n@app.delete(\"/products/{product_id}\")\ndef delete_product(product_id: int):\n    \"\"\"Delete a product.\"\"\"\n    if product_id not in products_db:\n        raise HTTPException(status_code=404, detail=\"Product not found\")\n    del products_db[product_id]\n    return {\"message\": \"Product deleted\"}\n```\n\n<Tip>\nAll subsequent code examples in this guide assume you have the above FastAPI application code already defined. Each example builds upon this base application, `app`.\n</Tip>\n\n## Generating an MCP Server\n\n<VersionBadge version=\"2.0.0\" />\n\nOne of the most common ways to bootstrap an MCP server is to generate it from an existing FastAPI application. FastMCP will expose your FastAPI endpoints as MCP components (tools, by default) in order to expose your API to LLM clients.\n\n\n\n### Basic Conversion\n\nConvert the FastAPI app to an MCP server with a single line:\n\n```python {5}\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\n\n# Convert to MCP server\nmcp = FastMCP.from_fastapi(app=app)\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Adding Components\n\nYour converted MCP server is a full FastMCP instance, meaning you can add new tools, resources, and other components to it just like you would with any other FastMCP instance.\n\n```python {8-11}\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\n\n# Convert to MCP server\nmcp = FastMCP.from_fastapi(app=app)\n\n# Add a new tool\n@mcp.tool\ndef get_product(product_id: int) -> ProductResponse:\n    \"\"\"Get a product by ID.\"\"\"\n    return products_db[product_id]\n\n# Run the MCP server\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n\n\n\n\n### Interacting with the MCP Server\n\nOnce you've converted your FastAPI app to an MCP server, you can interact with it using the FastMCP client to test functionality before deploying it to an LLM-based application.\n\n```python {3, }\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nimport asyncio\n\n# Convert to MCP server\nmcp = FastMCP.from_fastapi(app=app)\n\nasync def demo():\n    async with Client(mcp) as client:\n        # List available tools\n        tools = await client.list_tools()\n        print(f\"Available tools: {[t.name for t in tools]}\")\n        \n        # Create a product\n        result = await client.call_tool(\n            \"create_product_products_post\",\n            {\n                \"name\": \"Wireless Keyboard\",\n                \"price\": 79.99,\n                \"category\": \"Electronics\",\n                \"description\": \"Bluetooth mechanical keyboard\"\n            }\n        )\n        print(f\"Created product: {result.data}\")\n        \n        # List electronics under $100\n        result = await client.call_tool(\n            \"list_products_products_get\",\n            {\"category\": \"Electronics\", \"max_price\": 100}\n        )\n        print(f\"Affordable electronics: {result.data}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(demo())\n```\n\n### Custom Route Mapping\n\nBecause FastMCP's FastAPI integration is based on its [OpenAPI integration](/v2/integrations/openapi), you can customize how endpoints are converted to MCP components in exactly the same way. For example, here we use a `RouteMap` to map all GET requests to MCP resources, and all POST/PUT/DELETE requests to MCP tools:\n\n```python\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\n# Custom mapping rules\nmcp = FastMCP.from_fastapi(\n    app=app,\n    route_maps=[\n        # GET with path params → ResourceTemplates\n        RouteMap(\n            methods=[\"GET\"], \n            pattern=r\".*\\{.*\\}.*\", \n            mcp_type=MCPType.RESOURCE_TEMPLATE\n        ),\n        # Other GETs → Resources\n        RouteMap(\n            methods=[\"GET\"], \n            pattern=r\".*\", \n            mcp_type=MCPType.RESOURCE\n        ),\n        # POST/PUT/DELETE → Tools (default)\n    ],\n)\n\n# Now:\n# - GET /products → Resource\n# - GET /products/{id} → ResourceTemplate\n# - POST/PUT/DELETE → Tools\n```\n\n<Tip>\nTo learn more about customizing the conversion process, see the [OpenAPI Integration guide](/v2/integrations/openapi).\n</Tip>\n\n### Authentication and Headers\n\nYou can configure headers and other client options via the `httpx_client_kwargs` parameter. For example, to add authentication to your FastAPI app, you can pass a `headers` dictionary to the `httpx_client_kwargs` parameter:\n\n```python {27-31}\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\n\n# Add authentication to your FastAPI app\nfrom fastapi import Depends, Header\nfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials\n\nsecurity = HTTPBearer()\n\ndef verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):\n    if credentials.credentials != \"secret-token\":\n        raise HTTPException(status_code=401, detail=\"Invalid authentication\")\n    return credentials.credentials\n\n# Add a protected endpoint\n@app.get(\"/admin/stats\", dependencies=[Depends(verify_token)])\ndef get_admin_stats():\n    return {\n        \"total_products\": len(products_db),\n        \"categories\": list(set(p.category for p in products_db.values()))\n    }\n\n# Create MCP server with authentication headers\nmcp = FastMCP.from_fastapi(\n    app=app,\n    httpx_client_kwargs={\n        \"headers\": {\n            \"Authorization\": \"Bearer secret-token\",\n        }\n    }\n)\n```\n\n## Mounting an MCP Server\n\n<VersionBadge version=\"2.3.1\" />\n\nIn addition to generating servers, FastMCP can facilitate adding MCP servers to your existing FastAPI application. You can do this by mounting the MCP ASGI application.\n\n### Basic Mounting\n\nTo mount an MCP server, you can use the `http_app` method on your FastMCP instance. This will return an ASGI application that can be mounted to your FastAPI application.\n\n```python {23-30}\nfrom fastmcp import FastMCP\nfrom fastapi import FastAPI\n\n# Create MCP server\nmcp = FastMCP(\"Analytics Tools\")\n\n@mcp.tool\ndef analyze_pricing(category: str) -> dict:\n    \"\"\"Analyze pricing for a category.\"\"\"\n    products = [p for p in products_db.values() if p.category == category]\n    if not products:\n        return {\"error\": f\"No products in {category}\"}\n    \n    prices = [p.price for p in products]\n    return {\n        \"category\": category,\n        \"avg_price\": round(sum(prices) / len(prices), 2),\n        \"min\": min(prices),\n        \"max\": max(prices),\n    }\n\n# Create ASGI app from MCP server\nmcp_app = mcp.http_app(path='/mcp')\n\n# Key: Pass lifespan to FastAPI\napp = FastAPI(title=\"E-commerce API\", lifespan=mcp_app.lifespan)\n\n# Mount the MCP server\napp.mount(\"/analytics\", mcp_app)\n\n# Now: API at /products/*, MCP at /analytics/mcp/\n```\n\n## Offering an LLM-Friendly API\n\nA common pattern is to generate an MCP server from your FastAPI app and serve both interfaces from the same application. This provides an LLM-optimized interface alongside your regular API:\n\n```python\n# Assumes the FastAPI app from above is already defined\nfrom fastmcp import FastMCP\nfrom fastapi import FastAPI\n\n# 1. Generate MCP server from your API\nmcp = FastMCP.from_fastapi(app=app, name=\"E-commerce MCP\")\n\n# 2. Create the MCP's ASGI app\nmcp_app = mcp.http_app(path='/mcp')\n\n# 3. Create a new FastAPI app that combines both sets of routes\ncombined_app = FastAPI(\n    title=\"E-commerce API with MCP\",\n    routes=[\n        *mcp_app.routes,  # MCP routes\n        *app.routes,      # Original API routes\n    ],\n    lifespan=mcp_app.lifespan,\n)\n\n# Now you have:\n# - Regular API: http://localhost:8000/products\n# - LLM-friendly MCP: http://localhost:8000/mcp\n# Both served from the same FastAPI application!\n```\n\nThis approach lets you maintain a single codebase while offering both traditional REST endpoints and MCP-compatible endpoints for LLM clients.\n\n## Key Considerations\n\n### Operation IDs\n\nFastAPI operation IDs become MCP component names. Always specify meaningful operation IDs:\n\n```python\n# Good - explicit operation_id\n@app.get(\"/users/{user_id}\", operation_id=\"get_user_by_id\")\ndef get_user(user_id: int):\n    return {\"id\": user_id}\n\n# Less ideal - auto-generated name\n@app.get(\"/users/{user_id}\")\ndef get_user(user_id: int):\n    return {\"id\": user_id}\n```\n\n### Lifespan Management\n\nWhen mounting MCP servers, always pass the lifespan context:\n\n```python\n# Correct - lifespan passed\nmcp_app = mcp.http_app(path='/mcp')\napp = FastAPI(lifespan=mcp_app.lifespan)\napp.mount(\"/mcp\", mcp_app)\n\n# Incorrect - missing lifespan\napp = FastAPI()\napp.mount(\"/mcp\", mcp.http_app())  # Session manager won't initialize\n```\n\nIf you're mounting an authenticated MCP server under a path prefix, see [Mounting Authenticated Servers](/v2/deployment/http#mounting-authenticated-servers) for important OAuth routing considerations.\n\n### CORS Middleware\n\nIf your FastAPI app uses `CORSMiddleware` and you're mounting an OAuth-protected FastMCP server, avoid adding application-wide CORS middleware. FastMCP and the MCP SDK already handle CORS for OAuth routes, and layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).\n\nIf you need CORS on your own FastAPI routes, use the sub-app pattern: mount your API and FastMCP as separate apps, each with their own middleware, rather than adding top-level `CORSMiddleware` to the combined application.\n\n### Combining Lifespans\n\nIf your FastAPI app already has a lifespan (for database connections, startup tasks, etc.), you can't simply replace it with the MCP lifespan. Instead, you need to create a new lifespan function that manages both contexts. This ensures that both your app's initialization logic and the MCP server's session manager run properly:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI\nfrom fastmcp import FastMCP\n\n# Your existing lifespan\n@asynccontextmanager\nasync def app_lifespan(app: FastAPI):\n    # Startup\n    print(\"Starting up the app...\")\n    # Initialize database, cache, etc.\n    yield\n    # Shutdown\n    print(\"Shutting down the app...\")\n\n# Create MCP server\nmcp = FastMCP(\"Tools\")\nmcp_app = mcp.http_app(path='/mcp')\n\n# Combine both lifespans\n@asynccontextmanager\nasync def combined_lifespan(app: FastAPI):\n    # Run both lifespans\n    async with app_lifespan(app):\n        async with mcp_app.lifespan(app):\n            yield\n\n# Use the combined lifespan\napp = FastAPI(lifespan=combined_lifespan)\napp.mount(\"/mcp\", mcp_app)\n```\n\nThis pattern ensures both your app's initialization logic and the MCP server's session manager are properly managed. The key is using nested `async with` statements - the inner context (MCP) will be initialized after the outer context (your app), and cleaned up before it. This maintains the correct initialization and cleanup order for all your resources.\n\n### Performance Tips\n\n1. **Use in-memory transport for testing** - Pass MCP servers directly to clients\n2. **Design purpose-built MCP tools** - Better than auto-converting complex APIs\n3. **Keep tool parameters simple** - LLMs perform better with focused interfaces\n\nFor more details on configuration options, see the [OpenAPI Integration guide](/v2/integrations/openapi)."
  },
  {
    "path": "docs/v2/integrations/gemini-cli.mdx",
    "content": "---\ntitle: Gemini CLI 🤝 FastMCP\nsidebarTitle: Gemini CLI\ndescription: Install and use FastMCP servers in Gemini CLI\nicon: message-smile\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\nimport { LocalFocusTip } from \"/snippets/local-focus.mdx\"\n\n<LocalFocusTip />\n\nGemini CLI supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers.\n\n## Requirements\n\nThis integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Gemini CLI's built-in MCP management commands.\n\n## Create a Server\n\nThe examples in this guide will use the following simple dice-rolling server, saved as `server.py`.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Install the Server\n\n### FastMCP CLI\n<VersionBadge version=\"2.13.0\" />\n\nThe easiest way to install a FastMCP server in Gemini CLI is using the `fastmcp install gemini-cli` command. This automatically handles the configuration, dependency management, and calls Gemini CLI's built-in MCP management system.\n\n```bash\nfastmcp install gemini-cli server.py\n```\n\nThe install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:\n\n```bash\n# These are equivalent if your server object is named 'mcp'\nfastmcp install gemini-cli server.py\nfastmcp install gemini-cli server.py:mcp\n\n# Use explicit object name if your server has a different name\nfastmcp install gemini-cli server.py:my_custom_server\n```\n\nThe command will automatically configure the server with Gemini CLI's `gemini mcp add` command.\n\n#### Dependencies\n\nFastMCP provides flexible dependency management options for your Gemini CLI servers:\n\n**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:\n\n```bash\nfastmcp install gemini-cli server.py --with pandas --with requests\n```\n\n**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:\n\n```bash\nfastmcp install gemini-cli server.py --with-requirements requirements.txt\n```\n\n**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:\n\n```bash\nfastmcp install gemini-cli server.py --with-editable ./my-local-package\n```\n\nAlternatively, you can use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"requests\"]\n  }\n}\n```\n\n\n#### Python Version and Project Configuration\n\nControl the Python environment for your server with these options:\n\n**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:\n\n```bash\nfastmcp install gemini-cli server.py --python 3.11\n```\n\n**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:\n\n```bash\nfastmcp install gemini-cli server.py --project /path/to/my-project\n```\n\n#### Environment Variables\n\nIf your server needs environment variables (like API keys), you must include them:\n\n```bash\nfastmcp install gemini-cli server.py --server-name \"Weather Server\" \\\n  --env API_KEY=your-api-key \\\n  --env DEBUG=true\n```\n\nOr load them from a `.env` file:\n\n```bash\nfastmcp install gemini-cli server.py --server-name \"Weather Server\" --env-file .env\n```\n\n<Warning>\n**Gemini CLI must be installed**. The integration looks for the Gemini CLI and uses the `gemini mcp add` command to register servers.\n</Warning>\n\n### Manual Configuration\n\nFor more control over the configuration, you can manually use Gemini CLI's built-in MCP management commands. This gives you direct control over how your server is launched:\n\n```bash\n# Add a server with custom configuration\ngemini mcp add dice-roller uv -- run --with fastmcp fastmcp run server.py\n\n# Add with environment variables\ngemini mcp add weather-server -e API_KEY=secret -e DEBUG=true uv -- run --with fastmcp fastmcp run server.py\n\n# Add with specific scope (user, or project)\ngemini mcp add my-server --scope user uv -- run --with fastmcp fastmcp run server.py\n```\n\nYou can also manually specify Python versions and project directories in your Gemini CLI commands:\n\n```bash\n# With specific Python version\ngemini mcp add ml-server uv -- run --python 3.11 --with fastmcp fastmcp run server.py\n\n# Within a project directory\ngemini mcp add project-server uv -- run --project /path/to/project --with fastmcp fastmcp run server.py\n```\n\n## Using the Server\n\nOnce your server is installed, you can start using your FastMCP server with Gemini CLI.\n\nTry asking Gemini something like:\n\n> \"Roll some dice for me\"\n\nGemini will automatically detect your `roll_dice` tool and use it to fulfill your request.\n\nGemini CLI can now access all the tools and prompts you've defined in your FastMCP server. \n\nIf your server provides prompts, you can use them as slash commands with `/prompt_name`.\n"
  },
  {
    "path": "docs/v2/integrations/gemini.mdx",
    "content": "---\ntitle: Gemini SDK 🤝 FastMCP\nsidebarTitle: Gemini SDK\ndescription: Connect FastMCP servers to the Google Gemini SDK\nicon: message-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nGoogle's Gemini API includes built-in support for MCP servers in their Python and JavaScript SDKs, allowing you to connect directly to MCP servers and use their tools seamlessly with Gemini models.\n\n## Gemini Python SDK\n\nGoogle's [Gemini Python SDK](https://ai.google.dev/gemini-api/docs) can use FastMCP clients directly.\n\n<Note>\nGoogle's MCP integration is currently experimental and available in the Python and JavaScript SDKs. The API automatically calls MCP tools when needed and can connect to both local and remote MCP servers.\n</Note>\n\n<Tip>\nCurrently, Gemini's MCP support only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to the AI. Other MCP features like resources and prompts are not currently supported.\n</Tip>\n\n### Create a Server\n\nFirst, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Call the Server\n\n\nTo use the Gemini API with MCP, you'll need to install the Google Generative AI SDK:\n\n```bash\npip install google-genai\n```\n\nYou'll also need to authenticate with Google. You can do this by setting the `GEMINI_API_KEY` environment variable. Consult the Gemini SDK documentation for more information.\n\n```bash\nexport GEMINI_API_KEY=\"your-api-key\"\n```\n\nGemini's SDK interacts directly with the MCP client session. To call the server, you'll need to instantiate a FastMCP client, enter its connection context, and pass the client session to the Gemini SDK.\n\n```python {5, 9, 15}\nfrom fastmcp import Client\nfrom google import genai\nimport asyncio\n\nmcp_client = Client(\"server.py\")\ngemini_client = genai.Client()\n\nasync def main():    \n    async with mcp_client:\n        response = await gemini_client.aio.models.generate_content(\n            model=\"gemini-2.0-flash\",\n            contents=\"Roll 3 dice!\",\n            config=genai.types.GenerateContentConfig(\n                temperature=0,\n                tools=[mcp_client.session],  # Pass the FastMCP client session\n            ),\n        )\n        print(response.text)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIf you run this code, you'll see output like:\n\n```text\nOkay, I rolled 3 dice and got a 5, 4, and 1.\n```\n\n### Remote & Authenticated Servers\n\nIn the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/v2/clients/transports) or [auth](/v2/clients/auth) method supported by FastMCP, simply by changing the client configuration.\n\nFor example, to connect to a remote, authenticated server, you can use the following client:\n\n```python\nfrom fastmcp import Client\nfrom fastmcp.client.auth import BearerAuth\n\nmcp_client = Client(\n    \"https://my-server.com/mcp/\",\n    auth=BearerAuth(\"<your-token>\"),\n)\n```\n\nThe rest of the code remains the same.\n\n\n"
  },
  {
    "path": "docs/v2/integrations/github.mdx",
    "content": "---\ntitle: GitHub OAuth 🤝 FastMCP\nsidebarTitle: GitHub\ndescription: Secure your FastMCP server with GitHub OAuth\nicon: github\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nThis guide shows you how to secure your FastMCP server using **GitHub OAuth**. Since GitHub doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/v2/servers/auth/oauth-proxy) pattern to bridge GitHub's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[GitHub Account](https://github.com/)** with access to create OAuth Apps\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a GitHub OAuth App\n\nCreate an OAuth App in your GitHub settings to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to OAuth Apps\">\n    Go to **Settings → Developer settings → OAuth Apps** in your GitHub account, or visit [github.com/settings/developers](https://github.com/settings/developers).\n    \n    Click **\"New OAuth App\"** to create a new application.\n</Step>\n\n<Step title=\"Configure Your OAuth App\">\n    Fill in the application details:\n    \n    - **Application name**: Choose a name users will recognize (e.g., \"My FastMCP Server\")\n    - **Homepage URL**: Your application's homepage or documentation URL\n    - **Authorization callback URL**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    \n    <Warning>\n    The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, GitHub allows `http://localhost` URLs. For production, you must use HTTPS.\n    </Warning>\n    \n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHubProvider.\n    </Tip>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After creating the app, you'll see:\n    \n    - **Client ID**: A public identifier like `Ov23liAbcDefGhiJkLmN`\n    - **Client Secret**: Click \"Generate a new client secret\" and save the value securely\n    \n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `GitHubProvider`, which handles GitHub's OAuth quirks automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\n# The GitHubProvider handles GitHub's token format and validation\nauth_provider = GitHubProvider(\n    client_id=\"Ov23liAbcDefGhiJkLmN\",  # Your GitHub OAuth App Client ID\n    client_secret=\"github_pat_...\",     # Your GitHub OAuth App Client Secret\n    base_url=\"http://localhost:8000\",   # Must match your OAuth App configuration\n    # redirect_path=\"/auth/callback\"   # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"GitHub Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated GitHub user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n    \n    token = get_access_token()\n    # The GitHubProvider stores user data in token claims\n    return {\n        \"github_user\": token.claims.get(\"login\"),\n        \"name\": token.claims.get(\"name\"),\n        \"email\": token.claims.get(\"email\")\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by GitHub OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your GitHub-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle GitHub OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open GitHub login in your browser\n        print(\"✓ Authenticated with GitHub!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"GitHub user: {result['github_user']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to GitHub's authorization page\n2. After you authorize the app, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = GitHubProvider(\n    client_id=\"Ov23liAbcDefGhiJkLmN\",\n    client_secret=\"github_pat_...\",\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production GitHub App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Environment Variables\n\n<VersionBadge version=\"2.12.1\" />\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the GitHub provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.github.GitHubProvider` to use GitHub authentication.\n</ParamField>\n</Card>\n\n### GitHub-Specific Configuration\n\nThese environment variables provide default values for the GitHub provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID\" required>\nYour GitHub OAuth App Client ID (e.g., `Ov23liAbcDefGhiJkLmN`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET\" required>\nYour GitHub OAuth App Client Secret\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_BASE_URL\" default=\"http://localhost:8000\">\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your GitHub OAuth App\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_REQUIRED_SCOPES\" default='[\"user\"]'>\nComma-, space-, or JSON-separated list of required GitHub scopes (e.g., `user repo` or `[\"user\",\"repo\"]`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GITHUB_TIMEOUT_SECONDS\" default=\"10\">\nHTTP request timeout for GitHub API calls\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the GitHub provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider\n\n# GitHub OAuth credentials\nFASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=Ov23liAbcDefGhiJkLmN\nFASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET=github_pat_...\nFASTMCP_SERVER_AUTH_GITHUB_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_GITHUB_REQUIRED_SCOPES=user,repo\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"GitHub Secured App\")\n\n@mcp.tool\nasync def list_repos() -> list[str]:\n    \"\"\"List the authenticated user's repositories.\"\"\"\n    # Your tool implementation here\n    pass\n```\n"
  },
  {
    "path": "docs/v2/integrations/google.mdx",
    "content": "---\ntitle: Google OAuth 🤝 FastMCP\nsidebarTitle: Google\ndescription: Secure your FastMCP server with Google OAuth\nicon: google\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nThis guide shows you how to secure your FastMCP server using **Google OAuth**. Since Google doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/v2/servers/auth/oauth-proxy) pattern to bridge Google's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[Google Cloud Account](https://console.cloud.google.com/)** with access to create OAuth 2.0 Client IDs\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a Google OAuth 2.0 Client ID\n\nCreate an OAuth 2.0 Client ID in your Google Cloud Console to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Navigate to OAuth Consent Screen\">\n    Go to the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) and select your project (or create a new one).\n    \n    First, configure the OAuth consent screen by navigating to **APIs & Services → OAuth consent screen**. Choose \"External\" for testing or \"Internal\" for G Suite organizations.\n</Step>\n\n<Step title=\"Create OAuth 2.0 Client ID\">\n    Navigate to **APIs & Services → Credentials** and click **\"+ CREATE CREDENTIALS\"** → **\"OAuth client ID\"**.\n    \n    Configure your OAuth client:\n    \n    - **Application type**: Web application\n    - **Name**: Choose a descriptive name (e.g., \"FastMCP Server\")\n    - **Authorized JavaScript origins**: Add your server's base URL (e.g., `http://localhost:8000`)\n    - **Authorized redirect URIs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)\n    \n    <Warning>\n    The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Google allows `http://localhost` URLs with various ports. For production, you must use HTTPS.\n    </Warning>\n    \n    <Tip>\n    If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the GoogleProvider.\n    </Tip>\n</Step>\n\n<Step title=\"Save Your Credentials\">\n    After creating the client, you'll receive:\n    \n    - **Client ID**: A string ending in `.apps.googleusercontent.com`\n    - **Client Secret**: A string starting with `GOCSPX-`\n    \n    Download the JSON credentials or copy these values securely.\n    \n    <Tip>\n    Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.\n    </Tip>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `GoogleProvider`, which handles Google's OAuth flow automatically:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.google import GoogleProvider\n\n# The GoogleProvider handles Google's token format and validation\nauth_provider = GoogleProvider(\n    client_id=\"123456789.apps.googleusercontent.com\",  # Your Google OAuth Client ID\n    client_secret=\"GOCSPX-abc123...\",                  # Your Google OAuth Client Secret\n    base_url=\"http://localhost:8000\",                  # Must match your OAuth configuration\n    required_scopes=[                                  # Request user information\n        \"openid\",\n        \"https://www.googleapis.com/auth/userinfo.email\",\n    ],\n    # redirect_path=\"/auth/callback\"                  # Default value, customize if needed\n)\n\nmcp = FastMCP(name=\"Google Secured App\", auth=auth_provider)\n\n# Add a protected tool to test authentication\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Returns information about the authenticated Google user.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n    \n    token = get_access_token()\n    # The GoogleProvider stores user data in token claims\n    return {\n        \"google_id\": token.claims.get(\"sub\"),\n        \"email\": token.claims.get(\"email\"),\n        \"name\": token.claims.get(\"name\"),\n        \"picture\": token.claims.get(\"picture\"),\n        \"locale\": token.claims.get(\"locale\")\n    }\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Google OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Google-protected server:\n\n```python test_client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Google OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Google login in your browser\n        print(\"✓ Authenticated with Google!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"get_user_info\")\n        print(f\"Google user: {result['email']}\")\n        print(f\"Name: {result['name']}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Google's authorization page\n2. Sign in with your Google account and grant the requested permissions\n3. After authorization, you'll be redirected back\n4. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.google import GoogleProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth_provider = GoogleProvider(\n    client_id=\"123456789.apps.googleusercontent.com\",\n    client_secret=\"GOCSPX-abc123...\",\n    base_url=\"https://your-production-domain.com\",\n    required_scopes=[\"openid\", \"https://www.googleapis.com/auth/userinfo.email\"],\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production Google App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Environment Variables\n\n<VersionBadge version=\"2.12.1\" />\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the Google provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.google.GoogleProvider` to use Google authentication.\n</ParamField>\n</Card>\n\n### Google-Specific Configuration\n\nThese environment variables provide default values for the Google provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID\" required>\nYour Google OAuth 2.0 Client ID (e.g., `123456789.apps.googleusercontent.com`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET\" required>\nYour Google OAuth 2.0 Client Secret (e.g., `GOCSPX-abc123...`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_BASE_URL\" default=\"http://localhost:8000\">\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your Google OAuth Client\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES\" default=\"[]\">\nComma-, space-, or JSON-separated list of required Google scopes (e.g., `\"openid,https://www.googleapis.com/auth/userinfo.email\"` or `[\"openid\", \"https://www.googleapis.com/auth/userinfo.email\"]`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_GOOGLE_TIMEOUT_SECONDS\" default=\"10\">\nHTTP request timeout for Google API calls\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the Google provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider\n\n# Google OAuth credentials\nFASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com\nFASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-abc123...\nFASTMCP_SERVER_AUTH_GOOGLE_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_GOOGLE_REQUIRED_SCOPES=openid,https://www.googleapis.com/auth/userinfo.email\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"Google Secured App\")\n\n@mcp.tool\nasync def protected_tool(query: str) -> str:\n    \"\"\"A tool that requires Google authentication to access.\"\"\"\n    # Your tool implementation here\n    return f\"Processing authenticated request: {query}\"\n```"
  },
  {
    "path": "docs/v2/integrations/mcp-json-configuration.mdx",
    "content": "---\ntitle: MCP JSON Configuration 🤝 FastMCP\nsidebarTitle: MCP.json\ndescription: Generate standard MCP configuration files for any compatible client\nicon: brackets-curly\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.10.3\" />\n\nFastMCP can generate standard MCP JSON configuration files that work with any MCP-compatible client including Claude Desktop, VS Code, Cursor, and other applications that support the Model Context Protocol.\n\n## MCP JSON Configuration Standard\n\nThe MCP JSON configuration format is an **emergent standard** that has developed across the MCP ecosystem. This format defines how MCP clients should configure and launch MCP servers, providing a consistent way to specify server commands, arguments, and environment variables.\n\n### Configuration Structure\n\nThe standard uses a `mcpServers` object where each key represents a server name and the value contains the server's configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"server-name\": {\n      \"command\": \"executable\",\n      \"args\": [\"arg1\", \"arg2\"],\n      \"env\": {\n        \"VAR\": \"value\"\n      }\n    }\n  }\n}\n```\n\n### Server Configuration Fields\n\n#### `command` (required)\nThe executable command to run the MCP server. This should be an absolute path or a command available in the system PATH.\n\n```json\n{\n  \"command\": \"python\"\n}\n```\n\n#### `args` (optional)\nAn array of command-line arguments passed to the server executable. Arguments are passed in order.\n\n```json\n{\n  \"args\": [\"server.py\", \"--verbose\", \"--port\", \"8080\"]\n}\n```\n\n#### `env` (optional)\nAn object containing environment variables to set when launching the server. All values must be strings.\n\n```json\n{\n  \"env\": {\n    \"API_KEY\": \"secret-key\",\n    \"DEBUG\": \"true\",\n    \"PORT\": \"8080\"\n  }\n}\n```\n\n### Client Adoption\n\nThis format is widely adopted across the MCP ecosystem:\n\n- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`\n- **Cursor**: Uses `~/.cursor/mcp.json`\n- **VS Code**: Uses workspace `.vscode/mcp.json`\n- **Other clients**: Many MCP-compatible applications follow this standard\n\n## Overview\n\n<Note>\n**For the best experience, use FastMCP's first-class integrations:** [`fastmcp install claude-code`](/v2/integrations/claude-code), [`fastmcp install claude-desktop`](/v2/integrations/claude-desktop), or [`fastmcp install cursor`](/v2/integrations/cursor). Use MCP JSON generation for advanced use cases and unsupported clients.\n</Note>\n\nThe `fastmcp install mcp-json` command generates configuration in the standard `mcpServers` format used across the MCP ecosystem. This is useful when:\n\n- **Working with unsupported clients** - Any MCP client not directly integrated with FastMCP\n- **CI/CD environments** - Automated configuration generation for deployments  \n- **Configuration sharing** - Easy distribution of server setups to team members\n- **Custom tooling** - Integration with your own MCP management tools\n- **Manual setup** - When you prefer to manually configure your MCP client\n\n## Basic Usage\n\nGenerate configuration and output to stdout (useful for piping):\n\n```bash\nfastmcp install mcp-json server.py\n```\n\nThis outputs the server configuration JSON with the server name as the root key:\n\n```json\n{\n  \"My Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\", \n      \"fastmcp\",\n      \"run\",\n      \"/absolute/path/to/server.py\"\n    ]\n  }\n}\n```\n\nTo use this in a client configuration file, add it to the `mcpServers` object in your client's configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"My Server\": {\n      \"command\": \"uv\",\n      \"args\": [\n        \"run\",\n        \"--with\",\n        \"fastmcp\", \n        \"fastmcp\",\n        \"run\",\n        \"/absolute/path/to/server.py\"\n      ]\n    }\n  }\n}\n```\n\n<Note>\nWhen using `--python`, `--project`, or `--with-requirements`, the generated configuration will include these options in the `uv run` command, ensuring your server runs with the correct Python version and dependencies.\n</Note>\n\n<Note>\nDifferent MCP clients may have specific configuration requirements or formatting needs. Always consult your client's documentation to ensure proper integration.\n</Note>\n\n## Configuration Options\n\n### Server Naming\n\n```bash\n# Use server's built-in name (from FastMCP constructor)\nfastmcp install mcp-json server.py\n\n# Override with custom name\nfastmcp install mcp-json server.py --name \"Custom Server Name\"\n```\n\n### Dependencies\n\nAdd Python packages your server needs:\n\n```bash\n# Single package\nfastmcp install mcp-json server.py --with pandas\n\n# Multiple packages  \nfastmcp install mcp-json server.py --with pandas --with requests --with httpx\n\n# Editable local package\nfastmcp install mcp-json server.py --with-editable ./my-package\n\n# From requirements file\nfastmcp install mcp-json server.py --with-requirements requirements.txt\n```\n\nYou can also use a `fastmcp.json` configuration file (recommended):\n\n```json fastmcp.json\n{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"dependencies\": [\"pandas\", \"matplotlib\", \"seaborn\"]\n  }\n}\n```\n\nThen simply install with:\n```bash\nfastmcp install mcp-json fastmcp.json\n```\n\n\n### Environment Variables\n\n```bash\n# Individual environment variables\nfastmcp install mcp-json server.py \\\n  --env API_KEY=your-secret-key \\\n  --env DEBUG=true\n\n# Load from .env file\nfastmcp install mcp-json server.py --env-file .env\n```\n\n### Python Version and Project Directory\n\nSpecify Python version or run within a specific project:\n\n```bash\n# Use specific Python version\nfastmcp install mcp-json server.py --python 3.11\n\n# Run within a project directory\nfastmcp install mcp-json server.py --project /path/to/project\n```\n\n### Server Object Selection\n\nUse the same `file.py:object` notation as other FastMCP commands:\n\n```bash\n# Auto-detects server object (looks for 'mcp', 'server', or 'app')\nfastmcp install mcp-json server.py\n\n# Explicit server object\nfastmcp install mcp-json server.py:my_custom_server\n```\n\n## Clipboard Integration\n\nCopy configuration directly to your clipboard for easy pasting:\n\n```bash\nfastmcp install mcp-json server.py --copy\n```\n\n<Note>\nThe `--copy` flag requires the `pyperclip` Python package. If not installed, you'll see an error message with installation instructions.\n</Note>\n\n## Usage Examples\n\n### Basic Server\n\n```bash\nfastmcp install mcp-json dice_server.py\n```\n\nOutput:\n```json\n{\n  \"Dice Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\",\n      \"fastmcp\", \n      \"run\",\n      \"/home/user/dice_server.py\"\n    ]\n  }\n}\n```\n\n### Production Server with Dependencies\n\n```bash\nfastmcp install mcp-json api_server.py \\\n  --name \"Production API Server\" \\\n  --with requests \\\n  --with python-dotenv \\\n  --env API_BASE_URL=https://api.example.com \\\n  --env TIMEOUT=30\n```\n\n### Advanced Configuration\n\n```bash\nfastmcp install mcp-json ml_server.py \\\n  --name \"ML Analysis Server\" \\\n  --python 3.11 \\\n  --with-requirements requirements.txt \\\n  --project /home/user/ml-project \\\n  --env GPU_DEVICE=0\n```\n\nOutput:\n```json\n{\n  \"Production API Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\",\n      \"--with\",\n      \"python-dotenv\", \n      \"--with\",\n      \"requests\",\n      \"fastmcp\",\n      \"run\", \n      \"/home/user/api_server.py\"\n    ],\n    \"env\": {\n      \"API_BASE_URL\": \"https://api.example.com\",\n      \"TIMEOUT\": \"30\"\n    }\n  }\n}\n```\n\nThe advanced configuration example generates:\n```json\n{\n  \"ML Analysis Server\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--python\",\n      \"3.11\",\n      \"--project\",\n      \"/home/user/ml-project\",\n      \"--with\",\n      \"fastmcp\",\n      \"--with-requirements\",\n      \"requirements.txt\",\n      \"fastmcp\",\n      \"run\",\n      \"/home/user/ml_server.py\"\n    ],\n    \"env\": {\n      \"GPU_DEVICE\": \"0\"\n    }\n  }\n}\n```\n\n### Pipeline Usage\n\nSave configuration to file:\n\n```bash\nfastmcp install mcp-json server.py > mcp-config.json\n```\n\nUse in shell scripts:\n\n```bash\n#!/bin/bash\nCONFIG=$(fastmcp install mcp-json server.py --name \"CI Server\")\necho \"$CONFIG\" | jq '.\"CI Server\".command'\n# Output: \"uv\"\n```\n\n## Integration with MCP Clients\n\nThe generated configuration works with any MCP-compatible application:\n\n### Claude Desktop\n<Note>\n**Prefer [`fastmcp install claude-desktop`](/v2/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.\n</Note>\nCopy the `mcpServers` object into `~/.claude/claude_desktop_config.json`\n\n### Cursor\n<Note>\n**Prefer [`fastmcp install cursor`](/v2/integrations/cursor)** for automatic installation. Use MCP JSON for advanced configuration needs.\n</Note>\nAdd to `~/.cursor/mcp.json`\n\n### VS Code  \nAdd to your workspace's `.vscode/mcp.json` file\n\n### Custom Applications\nUse the JSON configuration with any application that supports the MCP protocol\n\n## Configuration Format\n\nThe generated configuration outputs a server object with the server name as the root key:\n\n```json\n{\n  \"<server-name>\": {\n    \"command\": \"<executable>\",\n    \"args\": [\"<arg1>\", \"<arg2>\", \"...\"],\n    \"env\": {\n      \"<ENV_VAR>\": \"<value>\"\n    }\n  }\n}\n```\n\nTo use this in an MCP client, add it to the client's `mcpServers` configuration object.\n\n**Fields:**\n- `command`: The executable to run (always `uv` for FastMCP servers)\n- `args`: Command-line arguments including dependencies and server path\n- `env`: Environment variables (only included if specified)\n\n<Warning>\n**All file paths in the generated configuration are absolute paths**. This ensures the configuration works regardless of the working directory when the MCP client starts the server.\n</Warning>\n\n## Requirements\n\n- **uv**: Must be installed and available in your system PATH\n- **pyperclip** (optional): Required only for `--copy` functionality\n\nInstall uv if not already available:\n\n```bash\n# macOS\nbrew install uv\n\n# Linux/Windows  \ncurl -LsSf https://astral.sh/uv/install.sh | sh\n```\n"
  },
  {
    "path": "docs/v2/integrations/oci.mdx",
    "content": "---\ntitle: OCI IAM OAuth 🤝 FastMCP\nsidebarTitle: Oracle\ndescription: Secure your FastMCP server with OCI IAM OAuth\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nThis guide shows you how to secure your FastMCP server using **OCI IAM OAuth**. Since OCI IAM doesn't support Dynamic Client Registration, this integration uses the [**OIDC Proxy**](/v2/servers/auth/oidc-proxy) pattern to bridge OCI's traditional OAuth with MCP's authentication requirements.\n\n## Configuration\n\n### Prerequisites\n\n1. An OCI cloud Account with access to create an Integrated Application in an Identity Domain.\n2. Your FastMCP server's URL (For dev environments, it is http://localhost:8000. For PROD environments, it could be https://mcp.${DOMAIN}.com)\n\n### Step 1: Make sure client access is enabled for JWK's URL\n\n<Steps>\n<Step title=\"Navigate to OCI IAM Domain Settings\">\n\n    Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).\n    From \"Identity & Security\" menu, open Domains page.\n    On the Domains list page, select the domain that you are using for MCP Authentication.\n    Open Settings tab. \n    Click on \"Edit Domain Settings\" button.\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ocieditdomainsettingsbutton.png\" alt=\"OCI console showing the Edit Domain Settings button in the IAM Domain settings page\" />\n    </Frame>\n</Step>\n\n<Step title=\"Update Domain Setting\">\n\n    Enable \"Configure client access\" checkbox as shown in the screenshot.\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ocieditdomainsettings.png\" alt=\"OCI IAM Domain Settings\" />\n    </Frame>\n</Step>\n</Steps>\n\n### Step 2: Create OAuth client for MCP server authentication\n\nFollow the Steps as mentioned below to create an OAuth client.\n\n<Steps>\n<Step title=\"Navigate to OCI IAM Integrated Applications\">\n\n    Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).\n    From \"Identity & Security\" menu, open Domains page.\n    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).\n    On the details page, select Integrated applications. A list of applications in the domain is displayed.\n</Step>\n\n<Step title=\"Add an Integrated Application\">\n\n    Select Add application.\n    In the Add application window, select Confidential Application.\n    Select Launch workflow.\n    In the Add application details page, Enter name and description as shown below.\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ociaddapplication.png\" alt=\"Adding a Confidential Integrated Application in OCI IAM Domain\" />\n    </Frame>\n</Step>\n\n<Step title=\"Update OAuth Configuration for an Integrated Application\">\n\n    Once the Integrated Application is created, Click on \"OAuth configuration\" tab.\n    Click on \"Edit OAuth configuration\" button.\n    Configure the application as OAuth client by selecting \"Configure this application as a client now\" radio button.\n    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.\n    For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by \"/oauth/callback\".\n\n    <Frame>\n        <img src=\"/integrations/images/oci/ocioauthconfiguration.png\" alt=\"OAuth Configuration for an Integrated Application in OCI IAM Domain\" />\n    </Frame>\n</Step>\n\n<Step title=\"Activate the Integrated Application\">\n\n    Click on \"Submit\" button to update OAuth configuration for the client application. \n    **Note: You don't need to do any special configuration to support PKCE for the OAuth client.**\n    Make sure to Activate the client application.\n    Note down client ID and client secret for the application. Update .env file and replace FASTMCP_SERVER_AUTH_OCI_CLIENT_ID and FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET values. \n    FASTMCP_SERVER_AUTH_OCI_IAM_GUID in the env file is the Identity domain URL that you chose for the MCP server.\n</Step>\n</Steps>\n\nThis is all you need to implement MCP server authentication against OCI IAM. However, you may want to use an authenticated user token to invoke OCI control plane APIs and propagate identity to the OCI control plane instead of using a service user account. In that case, you need to implement token exchange.\n\n### Step 3: Token Exchange Setup (Only if MCP server needs to talk to OCI Control Plane)\n\nToken exchange helps you exchange a logged-in user's OCI IAM token for an OCI control plane session token, also known as UPST (User Principal Session Token). To learn more about token exchange, refer to my [Workload Identity Federation Blog](https://www.ateam-oracle.com/post/workload-identity-federation)\n\nFor token exchange, we need to configure Identity propagation trust. The blog above discusses setting up the trust using REST APIs. However, you can also use OCI CLI. Before using the CLI command below, ensure that you have created a token exchange OAuth client. In most cases, you can use the same OAuth client that you created above. You will use the client ID of the token exchange OAuth client in the CLI command below and replace it with {FASTMCP_SERVER_AUTH_OCI_CLIENT_ID}. \n\nYou will also need to update the client secret for the token exchange OAuth client in the .env file. It is the FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET parameter. Update FASTMCP_SERVER_AUTH_OCI_IAM_GUID and FASTMCP_SERVER_AUTH_OCI_CLIENT_ID as well for the token exchange OAuth client in the .env file.\n\n```bash\noci identity-domains identity-propagation-trust create \\\n--schemas '[\"urn:ietf:params:scim:schemas:oracle:idcs:IdentityPropagationTrust\"]' \\\n--public-key-endpoint \"https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oraclecloud.com/admin/v1/SigningCert/jwk\" \\\n--name \"For Token Exchange\" --type \"JWT\" \\\n--issuer \"https://identity.oraclecloud.com/\" --active true \\\n--endpoint \"https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oracleclcoud.com\" \\\n--subject-claim-name \"sub\" --allow-impersonation false \\\n--subject-mapping-attribute \"username\" \\\n--subject-type \"User\" --client-claim-name \"iss\" \\\n--client-claim-values '[\"https://identity.oraclecloud.com/\"]' \\\n--oauth-clients '[\"{FASTMCP_SERVER_AUTH_OCI_CLIENT_ID}\"]'\n```\n\nTo exchange access token for OCI token and create a signer object, you need to add below code in MCP server. You can then use the signer object to create any OCI control plane client. \n\n```python\n\nfrom fastmcp.server.dependencies import get_access_token\nfrom fastmcp.utilities.logging import get_logger\nfrom oci.auth.signers import TokenExchangeSigner\nimport os\n\nlogger = get_logger(__name__)\n\n# Load configuration from environment\nFASTMCP_SERVER_AUTH_OCI_IAM_GUID = os.environ[\"FASTMCP_SERVER_AUTH_OCI_IAM_GUID\"]\nFASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ[\"FASTMCP_SERVER_AUTH_OCI_CLIENT_ID\"]\nFASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ[\"FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET\"]\n\n_global_token_cache = {} #In memory cache for OCI session token signer\n    \ndef get_oci_signer() -> TokenExchangeSigner:\n\n    authntoken = get_access_token()\n    tokenID = authntoken.claims.get(\"jti\")\n    token = authntoken.token\n    \n    #Check if the signer exists for the token ID in memory cache\n    cached_signer = _global_token_cache.get(tokenID)\n    logger.debug(f\"Global cached signer: {cached_signer}\")\n    if cached_signer:\n        logger.debug(f\"Using globally cached signer for token ID: {tokenID}\")\n        return cached_signer\n\n    #If the signer is not yet created for the token then create new OCI signer object\n    logger.debug(f\"Creating new signer for token ID: {tokenID}\")\n    signer = TokenExchangeSigner(\n        jwt_or_func=token,\n        oci_domain_id=FASTMCP_SERVER_AUTH_OCI_IAM_GUID.split(\".\")[0],\n        client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID,\n        client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET,\n    )\n    logger.debug(f\"Signer {signer} created for token ID: {tokenID}\")\n        \n    #Cache the signer object in memory cache\n    _global_token_cache[tokenID] = signer\n    logger.debug(f\"Signer cached for token ID: {tokenID}\")\n\n    return signer\n```\n\n## Running MCP server\n\nOnce the setup is complete, to run the MCP server, run the below command.\n```bash\nfastmcp run server.py:mcp --transport http --port 8000\n```\n\nTo run MCP client, run the below command.\n```bash\npython3 client.py\n```\n\nMCP Client sample is as below.\n```python client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle OCI OAuth flows\n    async with Client(\"http://localhost:8000/mcp/\", auth=\"oauth\") as client:\n        # First-time connection will open OCI login in your browser\n        print(\"✓ Authenticated with OCI IAM\")\n\n        tools = await client.list_tools()\n        print(f\"🔧 Available tools ({len(tools)}):\")\n        for tool in tools:\n            print(f\"   - {tool.name}: {tool.description}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to OCI IAM's login page\n2. Sign in with your OCI account and grant the requested consent\n3. After authorization, you'll be redirected back to the redirect path\n4. The client receives the token and can make authenticated requests\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\n\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.oci import OCIProvider\n\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Load configuration from environment\nFASTMCP_SERVER_AUTH_OCI_CONFIG_URL = os.environ[\"FASTMCP_SERVER_AUTH_OCI_CONFIG_URL\"]\nFASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ[\"FASTMCP_SERVER_AUTH_OCI_CLIENT_ID\"]\nFASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ[\"FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET\"]\n\n# Production setup with encrypted persistent token storage\nauth_provider = OCIProvider(\n    config_url=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL,\n    client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID,\n    client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET,\n    base_url=\"https://your-production-domain.com\",\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production OCI App\", auth=auth_provider)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at Rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Environment Variables\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the OCI provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.oci.OCIProvider` to use OCI IAM authentication.\n</ParamField>\n</Card>\n\n### OCI-Specific Configuration\n\nThese environment variables provide default values for the OCI IAM provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_OCI_IAM_GUID\" required>\nYour OCI Application Configuration URL (e.g., `idcs-asdascxasd11......identity.oraclecloud.com`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_OCI_CONFIG_URL\" required>\nYour OCI Application Configuration URL (e.g., `https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oraclecloud.com/.well-known/openid-configuration`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_OCI_CLIENT_ID\" required>\nYour OCI Application Client ID (e.g., `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET\" required>\nYour OCI Application Client Secret (e.g., `idcsssvPYqbjemq...`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_OCI_BASE_URL\" required>\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_OCI_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your OCI IAM Integrated Application\n</ParamField>\n\n</Card>\n\nExample `.env` file:\n```bash\n# Use the OCI IAM provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.oci.OCIProvider\n\n# OCI IAM configuration and credentials\nFASTMCP_SERVER_AUTH_OCI_IAM_GUID=idcs-asaacasd1111.....\nFASTMCP_SERVER_AUTH_OCI_CONFIG_URL=https://{FASTMCP_SERVER_AUTH_OCI_IAM_GUID}.identity.oraclecloud.com/.well-known/openid-configuration\nFASTMCP_SERVER_AUTH_OCI_CLIENT_ID=<your-client-id>\nFASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET=<your-client-secret>\nFASTMCP_SERVER_AUTH_OCI_BASE_URL=https://your-server.com\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_access_token\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"OCI Secured App\")\n\n@mcp.tool\ndef whoami() -> str:\n    \"\"\"The whoami function is to test MCP server without requiring token exchange.\n    This tool can be used to test successful authentication against OCI IAM.\n    It will return logged in user's subject (username from IAM domain).\"\"\"\n    token = get_access_token()\n    user = token.claims.get(\"sub\")\n    return f\"You are User: {user}\"\n```"
  },
  {
    "path": "docs/v2/integrations/openai.mdx",
    "content": "---\ntitle: OpenAI API 🤝 FastMCP\nsidebarTitle: OpenAI API\ndescription: Connect FastMCP servers to the OpenAI API\nicon: message-code\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n\n## Responses API\n\nOpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses) supports [MCP servers](https://platform.openai.com/docs/guides/tools-remote-mcp) as remote tool sources, allowing you to extend AI capabilities with custom functions.\n\n<Note>\nThe Responses API is a distinct API from OpenAI's Completions API or Assistants API. At this time, only the Responses API supports MCP.\n</Note>\n\n<Tip>\nCurrently, the Responses API only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to the AI agent. Other MCP features like resources and prompts are not currently supported.\n</Tip>\n\n\n### Create a Server\n\nFirst, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.\n\n```python server.py\nimport random\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Dice Roller\")\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n### Deploy the Server\n\nYour server must be deployed to a public URL in order for OpenAI to access it.\n\nFor development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.\n\nAssuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:\n\n<CodeGroup>\n```bash FastMCP server\npython server.py\n```\n\n```bash ngrok\nngrok http 8000\n```\n</CodeGroup>\n\n<Warning>\nThis exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.\n</Warning>\n\n### Call the Server\n\nTo use the Responses API, you'll need to install the OpenAI Python SDK (not included with FastMCP):\n\n```bash\npip install openai\n```\n\nYou'll also need to authenticate with OpenAI. You can do this by setting the `OPENAI_API_KEY` environment variable. Consult the OpenAI SDK documentation for more information.\n\n```bash\nexport OPENAI_API_KEY=\"your-api-key\"\n```\n\nHere is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment.\n\n```python {4, 11-16}\nfrom openai import OpenAI\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\nclient = OpenAI()\n\nresp = client.responses.create(\n    model=\"gpt-4.1\",\n    tools=[\n        {\n            \"type\": \"mcp\",\n            \"server_label\": \"dice_server\",\n            \"server_url\": f\"{url}/mcp/\",\n            \"require_approval\": \"never\",\n        },\n    ],\n    input=\"Roll a few dice!\",\n)\n\nprint(resp.output_text)\n```\nIf you run this code, you'll see something like the following output:\n\n```text\nYou rolled 3 dice and got the following results: 6, 4, and 2!\n```\n\n### Authentication\n\n<VersionBadge version=\"2.6.0\" />\n\nThe Responses API can include headers to authenticate the request, which means you don't have to worry about your server being publicly accessible.\n\n#### Server Authentication\n\nThe simplest way to add authentication to the server is to use a bearer token scheme. \n\nFor this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/v2/servers/auth/token-verification) documentation. \n\nWe'll start by creating an RSA key pair to sign and verify tokens.\n\n```python\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n```\n\n<Warning>\nFastMCP's `RSAKeyPair` utility is for development and testing only.\n</Warning> \n\nNext, we'll create a `JWTVerifier` to authenticate the server. \n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n```\n\nHere is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**\n\n```python server.py [expandable]\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import JWTVerifier\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\nimport random\n\nkey_pair = RSAKeyPair.generate()\naccess_token = key_pair.create_token(audience=\"dice-server\")\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    audience=\"dice-server\",\n)\n\nmcp = FastMCP(name=\"Dice Roller\", auth=auth)\n\n@mcp.tool\ndef roll_dice(n_dice: int) -> list[int]:\n    \"\"\"Roll `n_dice` 6-sided dice and return the results.\"\"\"\n    return [random.randint(1, 6) for _ in range(n_dice)]\n\nif __name__ == \"__main__\":\n    print(f\"\\n---\\n\\n🔑 Dice Roller access token:\\n\\n{access_token}\\n\\n---\\n\")\n    mcp.run(transport=\"http\", port=8000)\n```\n\n#### Client Authentication\n\nIf you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:\n\n```python\npythonAPIStatusError: Error code: 424 - {\n    \"error\": {\n        \"message\": \"Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)\",\n        \"type\": \"external_connector_error\",\n        \"param\": \"tools\",\n        \"code\": \"http_error\"\n    }\n}\n```\n\nAs expected, the server is rejecting the request because it's not authenticated.\n\nTo authenticate the client, you can pass the token in the `Authorization` header with the `Bearer` scheme:\n\n\n```python {4, 7, 19-21} [expandable]\nfrom openai import OpenAI\n\n# Your server URL (replace with your actual URL)\nurl = 'https://your-server-url.com'\n\n# Your access token (replace with your actual token)\naccess_token = 'your-access-token'\n\nclient = OpenAI()\n\nresp = client.responses.create(\n    model=\"gpt-4.1\",\n    tools=[\n        {\n            \"type\": \"mcp\",\n            \"server_label\": \"dice_server\",\n            \"server_url\": f\"{url}/mcp/\",\n            \"require_approval\": \"never\",\n            \"headers\": {\n                \"Authorization\": f\"Bearer {access_token}\"\n            }\n        },\n    ],\n    input=\"Roll a few dice!\",\n)\n\nprint(resp.output_text)\n```\n\nYou should now see the dice roll results in the output."
  },
  {
    "path": "docs/v2/integrations/openapi.mdx",
    "content": "---\ntitle: OpenAPI 🤝 FastMCP\nsidebarTitle: OpenAPI\ndescription: Generate MCP servers from any OpenAPI specification\nicon: list-tree\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nFastMCP can automatically generate an MCP server from any OpenAPI specification, allowing AI models to interact with existing APIs through the MCP protocol. Instead of manually creating tools and resources, you provide an OpenAPI spec and FastMCP intelligently converts API endpoints into the appropriate MCP components.\n\n<Tip>\nGenerating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters.\n\nWe recommend using the FastAPI integration for bootstrapping and prototyping, not for mirroring your API to LLM clients. See the post [Stop Converting Your REST APIs to MCP](https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp) for more details.\n</Tip>\n\n## Create a Server\n\nTo convert an OpenAPI specification to an MCP server, use the `FastMCP.from_openapi()` class method:\n\n```python server.py\nimport httpx\nfrom fastmcp import FastMCP\n\n# Create an HTTP client for your API\nclient = httpx.AsyncClient(base_url=\"https://api.example.com\")\n\n# Load your OpenAPI spec \nopenapi_spec = httpx.get(\"https://api.example.com/openapi.json\").json()\n\n# Create the MCP server\nmcp = FastMCP.from_openapi(\n    openapi_spec=openapi_spec,\n    client=client,\n    name=\"My API Server\"\n)\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n### Authentication\n\nIf your API requires authentication, configure it on the HTTP client:\n\n```python\nimport httpx\nfrom fastmcp import FastMCP\n\n# Bearer token authentication\napi_client = httpx.AsyncClient(\n    base_url=\"https://api.example.com\",\n    headers={\"Authorization\": \"Bearer YOUR_TOKEN\"}\n)\n\n# Create MCP server with authenticated client\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec, \n    client=api_client,\n    timeout=30.0  # 30 second timeout for all requests\n)\n```\n\n## Route Mapping\n\nBy default, FastMCP converts **every endpoint** in your OpenAPI specification into an MCP **Tool**. This provides a simple, predictable starting point that ensures all your API's functionality is immediately available to the vast majority of LLM clients which only support MCP tools.\n\nWhile this is a pragmatic default for maximum compatibility, you can easily customize this behavior. Internally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types.\n\nEach `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely.\n\n- **Methods**: HTTP methods to match (e.g. `[\"GET\", \"POST\"]` or `\"*\"` for all)\n- **Pattern**: Regex pattern to match the route path (e.g. `r\"^/users/.*\"` or `r\".*\"` for all)\n- **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.\n- **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)\n- **MCP tags**: A set of custom tags to add to components created from matching routes\n\nHere is FastMCP's default rule:\n\n```python\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nDEFAULT_ROUTE_MAPPINGS = [\n    # All routes become tools\n    RouteMap(mcp_type=MCPType.TOOL),\n]\n```\n\n### Custom Route Maps\n\nWhen creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map.\n\nFor example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `Resource` and `ResourceTemplate` components based on whether they had path parameters. (This was changed solely for client compatibility reasons.) You can restore this behavior by providing custom route maps:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\n# Restore pre-2.8.0 semantic mapping\nsemantic_maps = [\n    # GET requests with path parameters become ResourceTemplates\n    RouteMap(methods=[\"GET\"], pattern=r\".*\\{.*\\}.*\", mcp_type=MCPType.RESOURCE_TEMPLATE),\n    # All other GET requests become Resources\n    RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n]\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=semantic_maps,\n)\n```\n\nWith these maps, `GET` requests are handled semantically, and all other methods (`POST`, `PUT`, etc.) will fall through to the default rule and become `Tool`s.\n\nHere is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged \"internal\". All other routes will be handled by the default rules:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        # Analytics `GET` endpoints are tools\n        RouteMap(\n            methods=[\"GET\"], \n            pattern=r\"^/analytics/.*\", \n            mcp_type=MCPType.TOOL,\n        ),\n\n        # Exclude all admin endpoints\n        RouteMap(\n            pattern=r\"^/admin/.*\", \n            mcp_type=MCPType.EXCLUDE,\n        ),\n\n        # Exclude all routes tagged \"internal\"\n        RouteMap(\n            tags={\"internal\"},\n            mcp_type=MCPType.EXCLUDE,\n        ),\n    ],\n)\n```\n\n<Tip>\nThe default route maps are always applied after your custom maps, so you do not have to create route maps for every possible route.\n</Tip>\n\n### Excluding Routes\n\nTo exclude routes from the MCP server, use a route map to assign them to `MCPType.EXCLUDE`. \n\nYou can use this to remove sensitive or internal routes by targeting them specifically:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        RouteMap(pattern=r\"^/admin/.*\", mcp_type=MCPType.EXCLUDE),\n        RouteMap(tags={\"internal\"}, mcp_type=MCPType.EXCLUDE),\n    ],\n)\n```\n\nOr you can use a catch-all rule to exclude everything that your maps don't handle explicitly:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        # custom mapping logic goes here\n        # ... your specific route maps ...\n        # exclude all remaining routes\n        RouteMap(mcp_type=MCPType.EXCLUDE),\n    ],\n)\n```\n\n<Tip>\nUsing a catch-all exclusion rule will prevent the default route mappings from being applied, since it will match every remaining route. This is useful if you want to explicitly allow-list certain routes.\n</Tip>\n\n### Advanced Route Mapping\n\n<VersionBadge version=\"2.5.0\" />\n\nFor advanced use cases that require more complex logic, you can provide a `route_map_fn` callable. After the route map logic is applied, this function is called on each matched route and its assigned MCP component type. It can optionally return a different component type to override the mapped assignment. If it returns `None`, the assigned type is used.\n\nIn addition to more precise targeting of methods, patterns, and tags, this function can access any additional OpenAPI metadata about the route.\n\n<Tip>\nThe `route_map_fn` is called on all routes, even those that matched `MCPType.EXCLUDE` in your custom maps. This gives you an opportunity to customize the mapping or even override an exclusion.\n</Tip>\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute\n\ndef custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:\n    \"\"\"Advanced route type mapping.\"\"\"\n    # Convert all admin routes to tools regardless of HTTP method\n    if \"/admin/\" in route.path:\n        return MCPType.TOOL\n\n    elif \"internal\" in route.tags:\n        return MCPType.EXCLUDE\n    \n    # Convert user detail routes to templates even if they're POST\n    elif route.path.startswith(\"/users/\") and route.method == \"POST\":\n        return MCPType.RESOURCE_TEMPLATE\n    \n    # Use defaults for all other routes\n    return None\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_map_fn=custom_route_mapper,\n)\n```\n\n## Customization\n\n### Component Names\n\n<VersionBadge version=\"2.5.0\" />\n\nFastMCP automatically generates names for MCP components based on the OpenAPI specification. By default, it uses the `operationId` from your OpenAPI spec, up to the first double underscore (`__`).\n\nAll component names are automatically:\n- **Slugified**: Spaces and special characters are converted to underscores or removed\n- **Truncated**: Limited to 56 characters maximum to ensure compatibility\n- **Unique**: If multiple components have the same name, a number is automatically appended to make them unique\n\nFor more control over component names, you can provide an `mcp_names` dictionary that maps `operationId` values to your desired names. The `operationId` must be exactly as it appears in the OpenAPI spec. The provided name will always be slugified and truncated.\n\n```python\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    mcp_names={\n        \"list_users__with_pagination\": \"user_list\",\n        \"create_user__admin_required\": \"create_user\", \n        \"get_user_details__admin_required\": \"user_detail\",\n    }\n)\n```\n\nAny `operationId` not found in `mcp_names` will use the default strategy (operationId up to the first `__`).\n\n### Tags\n\n<VersionBadge version=\"2.8.0\" />\n\nFastMCP provides several ways to add tags to your MCP components, allowing you to categorize and organize them for better discoverability and filtering. Tags are combined from multiple sources to create the final set of tags on each component.\n\n#### RouteMap Tags\n\nYou 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.\n\n```python\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    route_maps=[\n        # Add custom tags to all POST endpoints\n        RouteMap(\n            methods=[\"POST\"],\n            pattern=r\".*\",\n            mcp_type=MCPType.TOOL,\n            mcp_tags={\"write-operation\", \"api-mutation\"}\n        ),\n        \n        # Add different tags to detail view endpoints\n        RouteMap(\n            methods=[\"GET\"],\n            pattern=r\".*\\{.*\\}.*\",\n            mcp_type=MCPType.RESOURCE_TEMPLATE,\n            mcp_tags={\"detail-view\", \"parameterized\"}\n        ),\n        \n        # Add tags to list endpoints\n        RouteMap(\n            methods=[\"GET\"],\n            pattern=r\".*\",\n            mcp_type=MCPType.RESOURCE,\n            mcp_tags={\"list-data\", \"collection\"}\n        ),\n    ],\n)\n```\n\n#### Global Tags\n\nYou can add tags to **all** components by providing a `tags` parameter when creating your MCP server. These global tags will be applied to every component created from your OpenAPI specification.\n\n```python\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    tags={\"api-v2\", \"production\", \"external\"}\n)\n```\n\n#### OpenAPI Tags in Client Meta\n\nFastMCP automatically includes OpenAPI tags from your specification in the component's metadata. These tags are available to MCP clients through the `_meta._fastmcp.tags` field, allowing clients to filter and organize components based on the original OpenAPI tagging:\n\n<CodeGroup>\n```json {5} OpenAPI spec with tags\n{\n  \"paths\": {\n    \"/users\": {\n      \"get\": {\n        \"tags\": [\"users\", \"public\"],\n        \"operationId\": \"list_users\",\n        \"summary\": \"List all users\"\n      }\n    }\n  }\n}\n```\n```python {6-9} Access OpenAPI tags in MCP client\nasync with client:\n    tools = await client.list_tools()\n    for tool in tools:\n        if hasattr(tool, '_meta') and tool._meta:\n            # OpenAPI tags are now available in _fastmcp namespace!\n            fastmcp_meta = tool._meta.get('_fastmcp', {})\n            openapi_tags = fastmcp_meta.get('tags', [])\n            if 'users' in openapi_tags:\n                print(f\"Found user-related tool: {tool.name}\")\n```\n</CodeGroup>\n\nThis makes it easy for clients to understand and organize API endpoints based on their original OpenAPI categorization.\n\n### Advanced Customization\n\n<VersionBadge version=\"2.5.0\" />\n\nBy default, FastMCP creates MCP components using a variety of metadata from the OpenAPI spec, such as incorporating the OpenAPI description into the MCP component description.\n\nAt times you may want to modify those MCP components in a variety of ways, such as adding LLM-specific instructions or tags. For fine-grained customization, you can provide a `mcp_component_fn` when creating the MCP server. After each MCP component has been created, this function is called on it and has the opportunity to modify it in-place.\n\n<Tip>\nYour `mcp_component_fn` is expected to modify the component in-place, not to return a new component. The result of the function is ignored.\n</Tip>\n\n```python\nfrom fastmcp.server.openapi import (\n    HTTPRoute,\n    OpenAPITool,\n    OpenAPIResource,\n    OpenAPIResourceTemplate,\n)\n\ndef customize_components(\n    route: HTTPRoute, \n    component: OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate,\n) -> None:\n    # Add custom tags to all components\n    component.tags.add(\"openapi\")\n    \n    # Customize based on component type\n    if isinstance(component, OpenAPITool):\n        component.description = f\"🔧 {component.description} (via API)\"\n    \n    if isinstance(component, OpenAPIResource):\n        component.description = f\"📊 {component.description}\"\n        component.tags.add(\"data\")\n\nmcp = FastMCP.from_openapi(\n    openapi_spec=spec,\n    client=client,\n    mcp_component_fn=customize_components,\n)\n```\n\n## Request Parameter Handling\n\nFastMCP intelligently handles different types of parameters in OpenAPI requests:\n\n### Query Parameters\n\nBy default, FastMCP only includes query parameters that have non-empty values. Parameters with `None` values or empty strings are automatically filtered out.\n\n```python\n# When calling this tool...\nawait client.call_tool(\"search_products\", {\n    \"category\": \"electronics\",  # ✅ Included\n    \"min_price\": 100,           # ✅ Included  \n    \"max_price\": None,          # ❌ Excluded\n    \"brand\": \"\",                # ❌ Excluded\n})\n\n# The HTTP request will be: GET /products?category=electronics&min_price=100\n```\n\n### Path Parameters\n\nPath parameters are typically required by REST APIs. FastMCP:\n- Filters out `None` values\n- Validates that all required path parameters are provided\n- Raises clear errors for missing required parameters\n\n```python\n# ✅ This works\nawait client.call_tool(\"get_user\", {\"user_id\": 123})\n\n# ❌ This raises: \"Missing required path parameters: {'user_id'}\"\nawait client.call_tool(\"get_user\", {\"user_id\": None})\n```\n\n### Array Parameters\n\nFastMCP handles array parameters according to OpenAPI specifications:\n\n- **Query arrays**: Serialized based on the `explode` parameter (default: `True`)\n- **Path arrays**: Serialized as comma-separated values (OpenAPI 'simple' style)\n\n```python\n# Query array with explode=true (default)\n# ?tags=red&tags=blue&tags=green\n\n# Query array with explode=false  \n# ?tags=red,blue,green\n\n# Path array (always comma-separated)\n# /items/red,blue,green\n```\n\n### Headers\n\nHeader parameters are automatically converted to strings and included in the HTTP request."
  },
  {
    "path": "docs/v2/integrations/permit.mdx",
    "content": "---\ntitle: Permit.io Authorization 🤝 FastMCP\nsidebarTitle: Permit.io\ndescription: Add fine-grained authorization to your FastMCP servers with Permit.io\nicon: shield-check\n---\n\nAdd **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Permit.io][permit-github] authorization middleware**.\n\nControl which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic policies using Permit.io's powerful RBAC, ABAC, and REBAC capabilities, and obtain comprehensive audit logs of all access attempts and violations.\n\n## How it Works\n\nLeveraging FastMCP's [Middleware][fastmcp-middleware], the Permit.io middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks against your Permit.io policies; covering both server methods and tool execution.\n\n### Policy Mapping\n\nThe middleware automatically maps MCP methods to Permit.io resources and actions:\n\n- **MCP server methods** (e.g., `tools/list`, `resources/read`):\n  - **Resource**: `{server_name}_{component}` (e.g., `myserver_tools`)\n  - **Action**: The method verb (e.g., `list`, `read`)\n- **Tool execution** (method `tools/call`):\n  - **Resource**: `{server_name}` (e.g., `myserver`)\n  - **Action**: The tool name (e.g., `greet`)\n\n![Permit.io Policy Mapping Example](./images/permit/policy_mapping.png)\n\n*Example: In Permit.io, the 'Admin' role is granted permissions on resources and actions as mapped by the middleware. For example, 'greet', 'greet-jwt', and 'login' are actions on the 'mcp_server' resource, and 'list' is an action on the 'mcp_server_tools' resource.*\n\n> **Note:**\n> Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies.\n>\n> ![Permit.io Directory Role Assignment Example](./images/permit/role_assignement.png)\n>\n> *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.*\n\nFor detailed policy mapping examples and configuration, see [Detailed Policy Mapping](https://github.com/permitio/permit-fastmcp/blob/main/docs/policy-mapping.md).\n\n### Listing Operations\n\nThe middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant PermitMiddleware as Permit.io Middleware\n    participant MCPServer as FastMCP Server\n    participant PermitPDP as Permit.io PDP\n\n    MCPClient->>PermitMiddleware: MCP Listing Request (e.g., tools/list)\n    PermitMiddleware->>MCPServer: MCP Listing Request\n    MCPServer-->>PermitMiddleware: MCP Listing Response\n    PermitMiddleware->>PermitPDP: Authorization Checks\n    PermitPDP->>PermitMiddleware: Authorization Decisions\n    PermitMiddleware-->>MCPClient: Filtered MCP Listing Response\n```\n\n### Execution Operations\n\nThe middleware behaves as an enforcement point for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies.\n\n```mermaid\nsequenceDiagram\n    participant MCPClient as MCP Client\n    participant PermitMiddleware as Permit.io Middleware\n    participant MCPServer as FastMCP Server\n    participant PermitPDP as Permit.io PDP\n\n    MCPClient->>PermitMiddleware: MCP Execution Request (e.g., tools/call)\n    PermitMiddleware->>PermitPDP: Authorization Check\n    PermitPDP->>PermitMiddleware: Authorization Decision\n    PermitMiddleware-->>MCPClient: MCP Unauthorized Error (if denied)\n    PermitMiddleware->>MCPServer: MCP Execution Request (if allowed)\n    MCPServer-->>PermitMiddleware: MCP Execution Response (if allowed)\n    PermitMiddleware-->>MCPClient: MCP Execution Response (if allowed)\n```\n\n## Add Authorization to Your Server\n\n<Note>\nPermit.io is a cloud-native authorization service. You need a Permit.io account and a running Policy Decision Point (PDP) for the middleware to function. You can run the PDP locally with Docker or use Permit.io's cloud PDP.\n</Note>\n\n### Prerequisites\n\n1. **Permit.io Account**: Sign up at [permit.io](https://permit.io)\n2. **PDP Setup**: Run the Permit.io PDP locally or use the cloud PDP (RBAC only)\n3. **API Key**: Get your Permit.io API key from the dashboard\n\n### Run the Permit.io PDP\n\nRun the PDP locally with Docker:\n\n```bash\ndocker run -p 7766:7766 permitio/pdp:latest\n```\n\nOr use the cloud PDP URL: `https://cloudpdp.api.permit.io`\n\n### Create a Server with Authorization\n\nFirst, install the `permit-fastmcp` package:\n\n```bash\n# Using UV (recommended)\nuv add permit-fastmcp\n\n# Using pip\npip install permit-fastmcp\n```\n\nThen create a FastMCP server and add the Permit.io middleware:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom permit_fastmcp.middleware.middleware import PermitMcpMiddleware\n\nmcp = FastMCP(\"Secure FastMCP Server 🔒\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet a user by name\"\"\"\n    return f\"Hello, {name}!\"\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n# Add Permit.io authorization middleware\nmcp.add_middleware(PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-permit-api-key\"\n))\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")\n```\n\n### Configure Access Policies\n\nCreate your authorization policies in the Permit.io dashboard:\n\n1. **Create Resources**: Define resources like `mcp_server` and `mcp_server_tools`\n2. **Define Actions**: Add actions like `greet`, `add`, `list`, `read`\n3. **Create Roles**: Define roles like `Admin`, `User`, `Guest`\n4. **Assign Permissions**: Grant roles access to specific resources and actions\n5. **Assign Users**: Assign roles to users in the Permit.io Directory\n\nFor step-by-step setup instructions and troubleshooting, see [Getting Started & FAQ](https://github.com/permitio/permit-fastmcp/blob/main/docs/getting-started.md).\n\n#### Example Policy Configuration\n\nPolicies are defined in the Permit.io dashboard, but you can also use the [Permit.io Terraform provider](https://github.com/permitio/terraform-provider-permitio) to define policies in code.\n\n\n```terraform\n# Resources\nresource \"permitio_resource\" \"mcp_server\" {\n  name = \"mcp_server\"\n  key  = \"mcp_server\"\n  \n  actions = {\n    \"greet\" = { name = \"greet\" }\n    \"add\"   = { name = \"add\" }\n  }\n}\n\nresource \"permitio_resource\" \"mcp_server_tools\" {\n  name = \"mcp_server_tools\"\n  key  = \"mcp_server_tools\"\n  \n  actions = {\n    \"list\" = { name = \"list\" }\n  }\n}\n\n# Roles\nresource \"permitio_role\" \"Admin\" {\n  key         = \"Admin\"\n  name        = \"Admin\"\n  permissions = [\n    \"mcp_server:greet\",\n    \"mcp_server:add\", \n    \"mcp_server_tools:list\"\n  ]\n}\n```\n\nYou can also use the [Permit.io CLI](https://github.com/permitio/permit-cli), [API](https://api.permit.io/scalar) or [SDKs](https://github.com/permitio/permit-python) to manage policies, as well as writing policies directly in REGO (Open Policy Agent's policy language).\n\nFor complete policy examples including ABAC and RBAC configurations, see [Example Policies](https://github.com/permitio/permit-fastmcp/tree/main/docs/example_policies).\n\n### Identity Management\n\nThe middleware supports multiple identity extraction modes:\n\n- **Fixed Identity**: Use a fixed identity for all requests\n- **Header-based**: Extract identity from HTTP headers\n- **JWT-based**: Extract and verify JWT tokens\n- **Source-based**: Use the MCP context source field\n\nFor detailed identity mode configuration and environment variables, see [Identity Modes & Environment Variables](https://github.com/permitio/permit-fastmcp/blob/main/docs/identity-modes.md).\n\n#### JWT Authentication Example\n\n```python\nimport os\n\n# Configure JWT identity extraction\nos.environ[\"PERMIT_MCP_IDENTITY_MODE\"] = \"jwt\"\nos.environ[\"PERMIT_MCP_IDENTITY_JWT_SECRET\"] = \"your-jwt-secret\"\n\nmcp.add_middleware(PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-permit-api-key\"\n))\n```\n\n### ABAC Policies with Tool Arguments\n\nThe middleware supports Attribute-Based Access Control (ABAC) policies that can evaluate tool arguments as attributes. Tool arguments are automatically flattened as individual attributes (e.g., `arg_name`, `arg_number`) for granular policy conditions.\n\n![ABAC Condition Example](./images/permit/abac_condition_example.png)\n\n*Example: Create dynamic resources with conditions like `resource.arg_number greater-than 10` to allow the `conditional-greet` tool only when the number argument exceeds 10.*\n\n#### Example: Conditional Access\n\nCreate a dynamic resource with conditions like `resource.arg_number greater-than 10` to allow the `conditional-greet` tool only when the number argument exceeds 10.\n\n```python\n@mcp.tool\ndef conditional_greet(name: str, number: int) -> str:\n    \"\"\"Greet a user only if number > 10\"\"\"\n    return f\"Hello, {name}! Your number is {number}\"\n```\n\n![ABAC Policy Example](./images/permit/abac_policy_example.png)\n\n*Example: The Admin role is granted access to the \"conditional-greet\" action on the \"Big-greets\" dynamic resource, while other tools like \"greet\", \"greet-jwt\", and \"login\" are granted on the base \"mcp_server\" resource.*\n\nFor comprehensive ABAC configuration and advanced policy examples, see [ABAC Policies with Tool Arguments](https://github.com/permitio/permit-fastmcp/blob/main/docs/policy-mapping.md#abac-policies-with-tool-arguments).\n\n### Run the Server\n\nStart your FastMCP server normally:\n\n```bash\npython server.py\n```\n\nThe middleware will now intercept all MCP requests and check them against your Permit.io policies. Requests include user identification through the configured identity mode and automatic mapping of MCP methods to authorization resources and actions.\n\n## Advanced Configuration\n\n### Environment Variables\n\nConfigure the middleware using environment variables:\n\n```bash\n# Permit.io configuration\nexport PERMIT_MCP_PERMIT_PDP_URL=\"http://localhost:7766\"\nexport PERMIT_MCP_PERMIT_API_KEY=\"your-api-key\"\n\n# Identity configuration\nexport PERMIT_MCP_IDENTITY_MODE=\"jwt\"\nexport PERMIT_MCP_IDENTITY_JWT_SECRET=\"your-jwt-secret\"\n\n# Method configuration\nexport PERMIT_MCP_KNOWN_METHODS='[\"tools/list\",\"tools/call\"]'\nexport PERMIT_MCP_BYPASSED_METHODS='[\"initialize\",\"ping\"]'\n\n# Logging configuration\nexport PERMIT_MCP_ENABLE_AUDIT_LOGGING=\"true\"\n```\n\nFor a complete list of all configuration options and environment variables, see [Configuration Reference](https://github.com/permitio/permit-fastmcp/blob/main/docs/configuration-reference.md).\n\n### Custom Middleware Configuration\n\n```python\nfrom permit_fastmcp.middleware.middleware import PermitMcpMiddleware\n\nmiddleware = PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-api-key\",\n    enable_audit_logging=True,\n    bypass_methods=[\"initialize\", \"ping\", \"health/*\"]\n)\n\nmcp.add_middleware(middleware)\n```\n\nFor advanced configuration options and custom middleware extensions, see [Advanced Configuration](https://github.com/permitio/permit-fastmcp/blob/main/docs/advanced-configuration.md).\n\n## Example: Complete JWT Authentication Server\n\nSee the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/):\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom permit_fastmcp.middleware.middleware import PermitMcpMiddleware\nimport jwt\nimport datetime\n\n# Configure JWT identity extraction\nos.environ[\"PERMIT_MCP_IDENTITY_MODE\"] = \"jwt\"\nos.environ[\"PERMIT_MCP_IDENTITY_JWT_SECRET\"] = \"mysecretkey\"\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool\ndef login(username: str, password: str) -> str:\n    \"\"\"Login to get a JWT token\"\"\"\n    if username == \"admin\" and password == \"password\":\n        token = jwt.encode(\n            {\"sub\": username, \"exp\": datetime.datetime.utcnow() + datetime.timedelta(hours=1)},\n            \"mysecretkey\",\n            algorithm=\"HS256\"\n        )\n        return f\"Bearer {token}\"\n    raise Exception(\"Invalid credentials\")\n\n@mcp.tool\ndef greet_jwt(ctx: Context) -> str:\n    \"\"\"Greet a user by extracting their name from JWT\"\"\"\n    # JWT extraction handled by middleware\n    return \"Hello, authenticated user!\"\n\nmcp.add_middleware(PermitMcpMiddleware(\n    permit_pdp_url=\"http://localhost:7766\",\n    permit_api_key=\"your-permit-api-key\"\n))\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")\n```\n\n<Tip>\n  For detailed policy configuration, custom authentication, and advanced\n  deployment patterns, visit the [Permit.io FastMCP Middleware\n  repository][permit-fastmcp-github]. For troubleshooting common issues, see [Troubleshooting](https://github.com/permitio/permit-fastmcp/blob/main/docs/troubleshooting.md).\n</Tip>\n\n\n[permit.io]: https://www.permit.io\n[permit-github]: https://github.com/permitio\n[permit-fastmcp-github]: https://github.com/permitio/permit-fastmcp\n[Agent.Security]: https://agent.security\n[fastmcp-middleware]: /servers/middleware\n"
  },
  {
    "path": "docs/v2/integrations/scalekit.mdx",
    "content": "---\ntitle: Scalekit 🤝 FastMCP\nsidebarTitle: Scalekit\ndescription: Secure your FastMCP server with Scalekit\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nInstall auth stack to your FastMCP server with [Scalekit](https://scalekit.com) using the [Remote OAuth](/v2/servers/auth/remote-oauth) pattern: Scalekit handles user authentication, and the MCP server validates issued tokens.\n\n### Prerequisites\n\nBefore you begin\n\n1. Get a [Scalekit account](https://app.scalekit.com/) and grab your **Environment URL** from _Dashboard > Settings_ .\n2. Have your FastMCP server's base URL ready (can be localhost for development, e.g., `http://localhost:8000/`)\n\n### Step 1: Configure MCP server in Scalekit environment\n\n<Steps>\n<Step title=\"Register MCP server and set environment\">\n\nIn your Scalekit dashboard:\n    1. Open the **MCP Servers** section, then select **Create new server**\n    2. Enter server details: a name, a resource identifier, and the desired MCP client authentication settings\n    3. Save, then copy the **Resource ID** (for example, res_92015146095)\n\nIn your FastMCP project's `.env`:\n\n```sh\nSCALEKIT_ENVIRONMENT_URL=<YOUR_APP_ENVIRONMENT_URL>\nSCALEKIT_RESOURCE_ID=<YOUR_APP_RESOURCE_ID> # res_926EXAMPLE5878\nBASE_URL=http://localhost:8000/\n# Optional: additional scopes tokens must have\n# SCALEKIT_REQUIRED_SCOPES=read,write\n```\n\n</Step>\n</Steps>\n\n### Step 2: Add auth to FastMCP server\n\nCreate your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:\n\n> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.scalekit import ScalekitProvider\n\n# Discovers Scalekit endpoints and set up JWT token validation\nauth_provider = ScalekitProvider(\n    environment_url=SCALEKIT_ENVIRONMENT_URL,    # Scalekit environment URL\n    resource_id=SCALEKIT_RESOURCE_ID,            # Resource server ID\n    base_url=SERVER_URL,                         # Public MCP endpoint\n    required_scopes=[\"read\"],                    # Optional scope enforcement\n)\n\n# Create FastMCP server with auth\nmcp = FastMCP(name=\"My Scalekit Protected Server\", auth=auth_provider)\n\n@mcp.tool\ndef auth_status() -> dict:\n    \"\"\"Show Scalekit authentication status.\"\"\"\n    # Extract user claims from the JWT\n    return {\n        \"message\": \"This tool requires authentication via Scalekit\",\n        \"authenticated\": True,\n        \"provider\": \"Scalekit\"\n    }\n\n```\n\n<Tip>\nSet `required_scopes` when you need tokens to carry specific permissions. Leave it unset to allow any token issued for the resource.\n</Tip>\n\n## Testing\n\n### Start the MCP server\n\n```sh\nuv run python server.py\n```\n\nUse any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running serve. Verify that authentication succeeds and requests are authorized as expected.\n\n### Provider selection\n\nSetting this environment variable allows the Scalekit provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.scalekit.ScalekitProvider` to use Scalekit authentication.\n</ParamField>\n</Card>\n\n### Scalekit-specific configuration\n\nThese environment variables provide default values for the Scalekit provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL\" required>\nYour Scalekit environment URL from the Admin Portal (e.g., `https://your-env.scalekit.com`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID\" required>\nYour Scalekit resource server ID from the MCP Servers section\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL\" required>\nPublic URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000/` for development)\n</ParamField>\n\nLegacy `FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL` is still recognized for backward compatibility but will be removed soon-rename it to `...BASE_URL`.\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES\" default=\"[]\">\nComma-, space-, or JSON-separated list of scopes that tokens must include to access your server\n</ParamField>\n</Card>\n\nExample `.env`:\n\n```bash\n# Use the Scalekit provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.scalekit.ScalekitProvider\n\n# Scalekit configuration\nFASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL=https://your-env.scalekit.com\nFASTMCP_SERVER_AUTH_SCALEKITPROVIDER_RESOURCE_ID=res_456\nFASTMCP_SERVER_AUTH_SCALEKITPROVIDER_BASE_URL=https://your-server.com/\n# FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_REQUIRED_SCOPES=read,write\n# FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_MCP_URL=https://your-server.com/  # Deprecated\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"My Scalekit Protected Server\")\n\n@mcp.tool\ndef protected_action() -> str:\n    \"\"\"A tool that requires authentication.\"\"\"\n    return \"Access granted via Scalekit!\"\n```\n\n## Capabilities\n\nScalekit supports OAuth 2.1 with Dynamic Client Registration for MCP clients and enterprise SSO, and provides built‑in JWT validation and security controls.\n\n**OAuth 2.1/DCR**: clients self‑register, use PKCE, and work with the Remote OAuth pattern without pre‑provisioned credentials.\n\n**Validation and SSO**: tokens are verified (keys, RS256, issuer, audience, expiry), and SAML, OIDC, OAuth 2.0, ADFS, Azure AD, and Google Workspace are supported; use HTTPS in production and review auth logs as needed.\n\n## Debugging\n\nEnable detailed logging to troubleshoot authentication issues:\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n```\n\n### Token inspection\n\nYou can inspect JWT tokens in your tools to understand the user context:\n\n```python\nfrom fastmcp.server.context import request_ctx\nimport jwt\n\n@mcp.tool\ndef inspect_token() -> dict:\n    \"\"\"Inspect the current JWT token claims.\"\"\"\n    context = request_ctx.get()\n\n    # Extract token from Authorization header\n    if hasattr(context, 'request') and hasattr(context.request, 'headers'):\n        auth_header = context.request.headers.get('authorization', '')\n        if auth_header.startswith('Bearer '):\n            token = auth_header[7:]\n            # Decode without verification (already verified by provider)\n            claims = jwt.decode(token, options={\"verify_signature\": False})\n            return claims\n\n    return {\"error\": \"No token found\"}\n```\n"
  },
  {
    "path": "docs/v2/integrations/supabase.mdx",
    "content": "---\ntitle: Supabase 🤝 FastMCP\nsidebarTitle: Supabase\ndescription: Secure your FastMCP server with Supabase Auth\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nThis guide shows you how to secure your FastMCP server using **Supabase Auth**. This integration uses the [**Remote OAuth**](/v2/servers/auth/remote-oauth) pattern, where Supabase handles user authentication and your FastMCP server validates the tokens.\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[Supabase Account](https://supabase.com/)** with a project or a self-hosted **Supabase Auth** instance\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Get Supabase Project URL\n\nIn your Supabase Dashboard:\n1. Go to **Project Settings**\n2. Copy your **Project URL** (e.g., `https://abc123.supabase.co`)\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `SupabaseProvider`:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.supabase import SupabaseProvider\n\n# Configure Supabase Auth\nauth = SupabaseProvider(\n    project_url=\"https://abc123.supabase.co\",\n    base_url=\"http://localhost:8000\",\n    auth_route=\"/my/auth/route\" # if self-hosting and using custom routes\n)\n\nmcp = FastMCP(\"Supabase Protected Server\", auth=auth)\n\n@mcp.tool\ndef protected_tool(message: str) -> str:\n    \"\"\"This tool requires authentication.\"\"\"\n    return f\"Authenticated user says: {message}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by Supabase authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your Supabase-protected server:\n\n```python client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():\n    # The client will automatically handle Supabase OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open Supabase login in your browser\n        print(\"✓ Authenticated with Supabase!\")\n\n        # Test the protected tool\n        result = await client.call_tool(\"protected_tool\", {\"message\": \"Hello!\"})\n        print(result)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to Supabase's authorization page\n2. After you authorize, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n## Environment Variables\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the Supabase provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.supabase.SupabaseProvider` to use Supabase authentication.\n</ParamField>\n</Card>\n\n### Supabase-Specific Configuration\n\nThese environment variables provide default values for the Supabase provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL\" required>\nYour Supabase project URL (e.g., `https://abc123.supabase.co`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL\" required>\nPublic URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_SUPABASE_AUTH_ROUTE\" default=\"/auth/v1\">\nYour Supabase auth route (e.g., `/auth/v1`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_SUPABASE_REQUIRED_SCOPES\" default=\"[]\">\nComma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid email` or `[\"openid\", \"email\"]`)\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# Use the Supabase provider\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.supabase.SupabaseProvider\n\n# Supabase configuration\nFASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL=https://abc123.supabase.co\nFASTMCP_SERVER_AUTH_SUPABASE_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_SUPABASE_REQUIRED_SCOPES=openid,email\n```\n\nWith environment variables set, your server code simplifies to:\n\n```python server.py\nfrom fastmcp import FastMCP\n\n# Authentication is automatically configured from environment\nmcp = FastMCP(name=\"Supabase Protected Server\")\n```\n"
  },
  {
    "path": "docs/v2/integrations/workos.mdx",
    "content": "---\ntitle: WorkOS 🤝 FastMCP\nsidebarTitle: WorkOS\ndescription: Authenticate FastMCP servers with WorkOS Connect\nicon: shield-check\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.12.0\" />\n\nSecure your FastMCP server with WorkOS Connect authentication. This integration uses the OAuth Proxy pattern to handle authentication through WorkOS Connect while maintaining compatibility with MCP clients.\n\n<Note>\nThis guide covers WorkOS Connect applications. For Dynamic Client Registration (DCR) with AuthKit, see the [AuthKit integration](/v2/integrations/authkit) instead.\n</Note>\n\n## Configuration\n\n### Prerequisites\n\nBefore you begin, you will need:\n1. A **[WorkOS Account](https://workos.com/)** with access to create OAuth Apps\n2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)\n\n### Step 1: Create a WorkOS OAuth App\n\nCreate an OAuth App in your WorkOS dashboard to get the credentials needed for authentication:\n\n<Steps>\n<Step title=\"Create OAuth Application\">\nIn your WorkOS dashboard:\n1. Navigate to **Applications**\n2. Click **Create Application** \n3. Select **OAuth Application**\n4. Name your application\n</Step>\n\n<Step title=\"Get Credentials\">\nIn your OAuth application settings:\n1. Copy your **Client ID** (starts with `client_`)\n2. Click **Generate Client Secret** and save it securely\n3. Copy your **AuthKit Domain** (e.g., `https://your-app.authkit.app`)\n</Step>\n\n<Step title=\"Configure Redirect URI\">\nIn the **Redirect URIs** section:\n- Add: `http://localhost:8000/auth/callback` (for development)\n- For production, add your server's public URL + `/auth/callback`\n\n<Warning>\nThe callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.\n</Warning>\n</Step>\n</Steps>\n\n### Step 2: FastMCP Configuration\n\nCreate your FastMCP server using the `WorkOSProvider`:\n\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import WorkOSProvider\n\n# Configure WorkOS OAuth\nauth = WorkOSProvider(\n    client_id=\"client_YOUR_CLIENT_ID\",\n    client_secret=\"YOUR_CLIENT_SECRET\",\n    authkit_domain=\"https://your-app.authkit.app\",\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"openid\", \"profile\", \"email\"]\n)\n\nmcp = FastMCP(\"WorkOS Protected Server\", auth=auth)\n\n@mcp.tool\ndef protected_tool(message: str) -> str:\n    \"\"\"This tool requires authentication.\"\"\"\n    return f\"Authenticated user says: {message}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\n## Testing\n\n### Running the Server\n\nStart your FastMCP server with HTTP transport to enable OAuth flows:\n\n```bash\nfastmcp run server.py --transport http --port 8000\n```\n\nYour server is now running and protected by WorkOS OAuth authentication.\n\n### Testing with a Client\n\nCreate a test client that authenticates with your WorkOS-protected server:\n\n```python client.py\nfrom fastmcp import Client\nimport asyncio\n\nasync def main():    \n    # The client will automatically handle WorkOS OAuth\n    async with Client(\"http://localhost:8000/mcp\", auth=\"oauth\") as client:\n        # First-time connection will open WorkOS login in your browser\n        print(\"✓ Authenticated with WorkOS!\")\n        \n        # Test the protected tool\n        result = await client.call_tool(\"protected_tool\", {\"message\": \"Hello!\"})\n        print(result)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen you run the client for the first time:\n1. Your browser will open to WorkOS's authorization page\n2. After you authorize the app, you'll be redirected back\n3. The client receives the token and can make authenticated requests\n\n<Info>\nThe client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.\n</Info>\n\n## Production Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:\n\n```python server.py\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import WorkOSProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\n# Production setup with encrypted persistent token storage\nauth = WorkOSProvider(\n    client_id=\"client_YOUR_CLIENT_ID\",\n    client_secret=\"YOUR_CLIENT_SECRET\",\n    authkit_domain=\"https://your-app.authkit.app\",\n    base_url=\"https://your-production-domain.com\",\n    required_scopes=[\"openid\", \"profile\", \"email\"],\n\n    # Production token management\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(\n            host=os.environ[\"REDIS_HOST\"],\n            port=int(os.environ[\"REDIS_PORT\"])\n        ),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n\nmcp = FastMCP(name=\"Production WorkOS App\", auth=auth)\n```\n\n<Note>\nParameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.\n\nFor complete details on these parameters, see the [OAuth Proxy documentation](/v2/servers/auth/oauth-proxy#configuration-parameters).\n</Note>\n\n## Environment Variables\n\n<VersionBadge version=\"2.12.1\" />\n\nFor production deployments, use environment variables instead of hardcoding credentials.\n\n### Provider Selection\n\nSetting this environment variable allows the WorkOS provider to be used automatically without explicitly instantiating it in code.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH\" default=\"Not set\">\nSet to `fastmcp.server.auth.providers.workos.WorkOSProvider` to use WorkOS authentication.\n</ParamField>\n</Card>\n\n### WorkOS-Specific Configuration\n\nThese environment variables provide default values for the WorkOS provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.\n\n<Card>\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID\" required>\nYour WorkOS OAuth App Client ID (e.g., `client_01K33Y6GGS7T3AWMPJWKW42Y3Q`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET\" required>\nYour WorkOS OAuth App Client Secret\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN\" required>\nYour WorkOS AuthKit domain (e.g., `https://your-app.authkit.app`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_BASE_URL\" default=\"http://localhost:8000\">\nPublic URL where OAuth endpoints will be accessible (includes any mount path)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_ISSUER_URL\" default=\"Uses BASE_URL\">\nIssuer URL for OAuth metadata (defaults to `BASE_URL`). Set to root-level URL when mounting under a path prefix to avoid 404 logs. See [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for details.\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_REDIRECT_PATH\" default=\"/auth/callback\">\nRedirect path configured in your WorkOS OAuth App\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES\" default=\"[]\">\nComma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid profile email` or `[\"openid\",\"profile\",\"email\"]`)\n</ParamField>\n\n<ParamField path=\"FASTMCP_SERVER_AUTH_WORKOS_TIMEOUT_SECONDS\" default=\"10\">\nHTTP request timeout for WorkOS API calls\n</ParamField>\n</Card>\n\nExample `.env` file:\n```bash\n# WorkOS OAuth credentials (always used as defaults)\nFASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID=client_01K33Y6GGS7T3AWMPJWKW42Y3Q\nFASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET=your_client_secret\nFASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN=https://your-app.authkit.app\nFASTMCP_SERVER_AUTH_WORKOS_BASE_URL=https://your-server.com\nFASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES=[\"openid\",\"profile\",\"email\"]\n\n# Optional: Automatically provision WorkOS auth for all servers\nFASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.WorkOSProvider\n```\n\nWith environment variables set, you can either:\n\n**Option 1: Manual instantiation (env vars provide defaults)**\n```python server.py\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import WorkOSProvider\n\n# Env vars provide default values for WorkOSProvider()\nauth = WorkOSProvider()  # Uses env var defaults\nmcp = FastMCP(name=\"WorkOS Protected Server\", auth=auth)\n```\n\n**Option 2: Automatic provisioning (requires FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.WorkOSProvider)**\n```python server.py\nfrom fastmcp import FastMCP\n\n# Auth is automatically provisioned from FASTMCP_SERVER_AUTH\nmcp = FastMCP(name=\"WorkOS Protected Server\")\n```\n\n## Configuration Options\n\n<Card>\n<ParamField path=\"client_id\" required>\nWorkOS OAuth application client ID\n</ParamField>\n\n<ParamField path=\"client_secret\" required>\nWorkOS OAuth application client secret\n</ParamField>\n\n<ParamField path=\"authkit_domain\" required>\nYour WorkOS AuthKit domain URL (e.g., `https://your-app.authkit.app`)\n</ParamField>\n\n<ParamField path=\"base_url\" required>\nYour FastMCP server's public URL\n</ParamField>\n\n<ParamField path=\"required_scopes\" default=\"[]\">\nOAuth scopes to request\n</ParamField>\n\n<ParamField path=\"redirect_path\" default=\"/auth/callback\">\nOAuth callback path\n</ParamField>\n\n<ParamField path=\"timeout_seconds\" default=\"10\">\nAPI request timeout\n</ParamField>\n</Card>"
  },
  {
    "path": "docs/v2/patterns/cli.mdx",
    "content": "---\ntitle: FastMCP CLI\nsidebarTitle: CLI\ndescription: Learn how to use the FastMCP command-line interface\nicon: terminal\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n\nFastMCP provides a command-line interface (CLI) that makes it easy to run, develop, and install your MCP servers. The CLI is automatically installed when you install FastMCP.\n\n```bash\nfastmcp --help\n```\n\n## Commands Overview\n\n| Command | Purpose | Dependency Management |\n| ------- | ------- | --------------------- |\n| `run` | Run a FastMCP server directly | **Supports:** Local files, factory functions, URLs, fastmcp.json configs, MCP configs. **Deps:** Uses your local environment directly. With `--python`, `--with`, `--project`, or `--with-requirements`: Runs via `uv run` subprocess. With fastmcp.json: Automatically manages dependencies based on configuration |\n| `dev` | Run a server with the MCP Inspector for testing | **Supports:** Local files and fastmcp.json configs. **Deps:** Always runs via `uv run` subprocess (never uses your local environment); dependencies must be specified or available in a uv-managed project. With fastmcp.json: Uses configured dependencies |\n| `install` | Install a server in MCP client applications | **Supports:** Local files and fastmcp.json configs. **Deps:** Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable`. With fastmcp.json: Uses configured dependencies |\n| `inspect` | Generate a JSON report about a FastMCP server | **Supports:** Local files and fastmcp.json configs. **Deps:** Uses your current environment; you are responsible for ensuring all dependencies are available |\n| `project prepare` | Create a persistent uv project from fastmcp.json environment config | **Supports:** fastmcp.json configs only. **Deps:** Creates a uv project directory with all dependencies pre-installed for reuse with `--project` flag |\n| `version` | Display version information | N/A |\n\n## `fastmcp run`\n\nRun a FastMCP server directly or proxy a remote server.\n\n```bash\nfastmcp run server.py\n```\n\n<Tip>\nBy default, this command runs the server directly in your current Python environment. You are responsible for ensuring all dependencies are available. When using `--python`, `--with`, `--project`, or `--with-requirements` options, it runs the server via `uv run` subprocess instead.\n</Tip>\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Transport | `--transport`, `-t` | Transport protocol to use (`stdio`, `http`, or `sse`) |\n| Host | `--host` | Host to bind to when using http transport (default: 127.0.0.1) |\n| Port | `--port`, `-p` | Port to bind to when using http transport (default: 8000) |\n| Path | `--path` | Path to bind to when using http transport (default: `/mcp/` or `/sse/` for SSE) |\n| Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |\n| No Banner | `--no-banner` | Disable the startup banner display |\n| No Environment | `--skip-env` | Skip environment setup with uv (use when already in a uv environment) |\n| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |\n| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |\n| Project Directory | `--project` | Run the command within the given project directory |\n| Requirements File | `--with-requirements` | Requirements file to install dependencies from |\n\n\n### Entrypoints\n<VersionBadge version=\"2.3.5\" />\n\nThe `fastmcp run` command supports the following entrypoints:\n\n1. **[Inferred server instance](#inferred-server-instance)**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **[Explicit server entrypoint](#explicit-server-entrypoint)**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server**\n5. **[FastMCP configuration file](#fastmcp-configuration)**: `fastmcp.json` - runs servers using FastMCP's declarative configuration format (auto-detects files in current directory)\n6. **MCP configuration file**: `mcp.json` - runs servers defined in a standard MCP configuration file\n\n<Warning>\nNote: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == \"__main__\"` block. This means:\n- Any setup code in `__main__` will NOT run\n- Server configuration in `__main__` is bypassed  \n- `fastmcp run` finds your server entrypoint/factory and runs it with its own transport settings\n\nIf you need setup code to run, use the **factory pattern** instead.\n</Warning>\n\n#### Inferred Server Instance\n\nIf you provide a path to a file, `fastmcp run` will load the file and look for a FastMCP server instance stored as a variable named `mcp`, `server`, or `app`. If no such object is found, it will raise an error.\n\nFor example, if you have a file called `server.py` with the following content:\n\n```python server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n```\n\nYou can run it with:\n\n```bash\nfastmcp run server.py\n```\n\n#### Explicit Server Entrypoint\n\nIf your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server entrypoint:\n\n```bash\nfastmcp run server.py:custom_name\n```\n\nFor example, if you have a file called `server.py` with the following content:\n\n```python\nfrom fastmcp import FastMCP\n\nmy_server = FastMCP(\"CustomServer\")\n\n@my_server.tool\ndef hello() -> str:\n    return \"Hello from custom server!\"\n```\n\nYou can run it with:\n\n```bash\nfastmcp run server.py:my_server\n```\n\n#### Factory Function\n<VersionBadge version=\"2.11.2\" />\n\nSince `fastmcp run` ignores the `if __name__ == \"__main__\"` block, you can use a factory function to run setup code before your server starts. Factory functions are called without any arguments and must return a FastMCP server instance. Both sync and async factory functions are supported.\n\nThe syntax for using a factory function is the same as for an explicit server entrypoint: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance\n\nFor example, if you have a file called `server.py` with the following content:\n\n```python\nfrom fastmcp import FastMCP\n\nasync def create_server() -> FastMCP:\n    mcp = FastMCP(\"MyServer\")\n    \n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        return x + y\n    \n    # Setup that runs with fastmcp run\n    tool = await mcp.get_tool(\"add\")\n    tool.disable()\n    \n    return mcp\n```\n\nYou can run it with:\n\n```bash\nfastmcp run server.py:create_server\n```\n\n#### Remote Server Proxy\n\nFastMCP run can also start a local proxy server that connects to a remote server. This is useful when you want to run a remote server locally for testing or development purposes, or to use with a client that doesn't support direct connections to remote servers.\n\nTo start a local proxy, you can use the following syntax:\n\n```bash\nfastmcp run https://example.com/mcp\n```\n\n#### FastMCP Configuration\n<VersionBadge version=\"2.11.4\" />\n\nFastMCP supports declarative configuration through `fastmcp.json` files. When you run `fastmcp run` without arguments, it automatically looks for a `fastmcp.json` file in the current directory:\n\n```bash\n# Auto-detect fastmcp.json in current directory\nfastmcp run\n\n# Or explicitly specify a configuration file\nfastmcp run my-config.fastmcp.json\n```\n\nThe configuration file handles dependencies, environment variables, and transport settings. Command-line arguments override configuration file values:\n\n```bash\n# Override port from config file\nfastmcp run fastmcp.json --port 8080\n\n# Skip environment setup when already in a uv environment\nfastmcp run fastmcp.json --skip-env\n```\n\n<Note>\nThe `--skip-env` flag is useful when:\n- You're already in an activated virtual environment\n- You're inside a Docker container with pre-installed dependencies  \n- You're in a uv-managed environment (prevents infinite recursion)\n- You want to test the server without environment setup\n</Note>\n\nSee [Server Configuration](/v2/deployment/server-configuration) for detailed documentation on fastmcp.json.\n\n#### MCP Configuration\n\nFastMCP can also run servers defined in a standard MCP configuration file. This is useful when you want to run multiple servers from a single file, or when you want to use a client that doesn't support direct connections to remote servers.\n\nTo run a MCP configuration file, you can use the following syntax:\n\n```bash\nfastmcp run mcp.json\n```\n\nThis will run all the servers defined in the file.\n\n## `fastmcp dev`\n\nRun a MCP server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for testing.\n\n```bash\nfastmcp dev server.py\n```\n\n<Tip> \nThis command always runs your server via `uv run` subprocess (never your local environment) to work with the MCP Inspector. Dependencies can be:\n- Specified using `--with` and/or `--with-editable` options\n- Defined in a `fastmcp.json` configuration file\n- Available in a uv-managed project\n\nWhen using `fastmcp.json`, the dev command automatically uses the configured dependencies.\n</Tip>\n\n<Warning>\nThe `dev` command is a shortcut for testing a server over STDIO only. When the Inspector launches, you may need to:\n1. Select \"STDIO\" from the transport dropdown\n2. Connect manually\n\nThis command does not support HTTP testing. To test a server over Streamable HTTP or SSE:\n1. Start your server manually with the appropriate transport using either the command line:\n   ```bash\n   fastmcp run server.py --transport http\n   ```\n   or by setting the transport in your code:\n   ```bash\n   python server.py  # Assuming your __main__ block sets Streamable HTTP transport\n   ```\n2. Open the MCP Inspector separately and connect to your running server\n</Warning>\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Editable Package | `--with-editable`, `-e` | Directory containing pyproject.toml to install in editable mode |\n| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |\n| Inspector Version | `--inspector-version` | Version of the MCP Inspector to use |\n| UI Port | `--ui-port` | Port for the MCP Inspector UI |\n| Server Port | `--server-port` | Port for the MCP Inspector Proxy server |\n| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |\n| Project Directory | `--project` | Run the command within the given project directory |\n| Requirements File | `--with-requirements` | Requirements file to install dependencies from |\n\n### Entrypoints\n\nThe `dev` command supports local FastMCP server files and configuration:\n\n1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration (auto-detects in current directory)\n\n<Warning>\nThe `dev` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.\n</Warning>\n\n**Examples**\n\n```bash\n# Run dev server with editable mode and additional packages\nfastmcp dev server.py -e . --with pandas --with matplotlib\n\n# Run dev server with fastmcp.json configuration (auto-detects)\nfastmcp dev\n\n# Run dev server with explicit fastmcp.json file\nfastmcp dev dev.fastmcp.json\n\n# Run dev server with specific Python version\nfastmcp dev server.py --python 3.11\n\n# Run dev server with requirements file\nfastmcp dev server.py --with-requirements requirements.txt\n\n# Run dev server within a specific project directory\nfastmcp dev server.py --project /path/to/project\n```\n\n## `fastmcp install`\n<VersionBadge version=\"2.10.3\" />\n\nInstall a MCP server in MCP client applications. FastMCP currently supports the following clients:\n\n- **Claude Code** - Installs via Claude Code's built-in MCP management system\n- **Claude Desktop** - Installs via direct configuration file modification\n- **Cursor** - Installs via deeplink that opens Cursor for user confirmation\n- **MCP JSON** - Generates standard MCP JSON configuration for manual use\n\n```bash\nfastmcp install claude-code server.py\nfastmcp install claude-desktop server.py\nfastmcp install cursor server.py\nfastmcp install mcp-json server.py\n```\n\nNote that for security reasons, MCP clients usually run every server in a completely isolated environment. Therefore, all dependencies must be explicitly specified using the `--with` and/or `--with-editable` options (following `uv` conventions) or by attaching them to your server in code via the `dependencies` parameter. You should not assume that the MCP server will have access to your local environment.\n\n<Warning>\n**`uv` must be installed and available in your system PATH**. Both Claude Desktop and Cursor run in isolated environments and need `uv` to manage dependencies. On macOS, install `uv` globally with Homebrew for Claude Desktop compatibility: `brew install uv`.\n</Warning>\n\n<Note>\n**Python Version Considerations**: The install commands now support the `--python` option to specify a Python version directly. You can also use `--project` to run within a specific project directory or `--with-requirements` to install dependencies from a requirements file.\n</Note>\n\n<Tip>\n**FastMCP `install` commands focus on local server files with STDIO transport.** For remote servers running with HTTP or SSE transport, use your client's native configuration - FastMCP's value is simplifying the complex local setup with dependencies and `uv` commands.\n</Tip>\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Server Name | `--server-name`, `-n` | Custom name for the server (defaults to server's name attribute or file name) |\n| Editable Package | `--with-editable`, `-e` | Directory containing pyproject.toml to install in editable mode |\n| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |\n| Environment Variables | `--env` | Environment variables in KEY=VALUE format (can be used multiple times) |\n| Environment File | `--env-file`, `-f` | Load environment variables from a .env file |\n| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |\n| Project Directory | `--project` | Run the command within the given project directory |\n| Requirements File | `--with-requirements` | Requirements file to install dependencies from |\n\n### Entrypoints\n\nThe `install` command supports local FastMCP server files and configuration:\n\n1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration with dependencies and settings\n\n<Note>\nFactory functions are particularly useful for install commands since they allow setup code to run that would otherwise be ignored when the MCP client runs your server. When using fastmcp.json, dependencies are automatically handled.\n</Note>\n\n<Warning>\nThe `install` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files. For remote servers, use your MCP client's native configuration.\n</Warning>\n\n**Examples**\n\n```bash\n# Auto-detects server entrypoint (looks for 'mcp', 'server', or 'app')\nfastmcp install claude-desktop server.py\n\n# Install with fastmcp.json configuration (auto-detects)\nfastmcp install claude-desktop\n\n# Install with explicit fastmcp.json file\nfastmcp install claude-desktop my-config.fastmcp.json\n\n# Uses specific server entrypoint\nfastmcp install claude-desktop server.py:my_server\n\n# With custom name and dependencies\nfastmcp install claude-desktop server.py:my_server --server-name \"My Analysis Server\" --with pandas\n\n# Install in Claude Code with environment variables\nfastmcp install claude-code server.py --env API_KEY=secret --env DEBUG=true\n\n# Install in Cursor with environment variables\nfastmcp install cursor server.py --env API_KEY=secret --env DEBUG=true\n\n# Install with environment file\nfastmcp install cursor server.py --env-file .env\n\n# Install with specific Python version\nfastmcp install claude-desktop server.py --python 3.11\n\n# Install with requirements file\nfastmcp install claude-code server.py --with-requirements requirements.txt\n\n# Install within a project directory\nfastmcp install cursor server.py --project /path/to/project\n\n# Generate MCP JSON configuration\nfastmcp install mcp-json server.py --name \"My Server\" --with pandas\n\n# Copy JSON configuration to clipboard\nfastmcp install mcp-json server.py --copy\n```\n\n### MCP JSON Generation\n\nThe `mcp-json` subcommand generates standard MCP JSON configuration that can be used with any MCP-compatible client. This is useful when:\n\n- Working with MCP clients not directly supported by FastMCP\n- Creating configuration for CI/CD environments  \n- Sharing server configurations with others\n- Integration with custom tooling\n\nThe generated JSON follows the standard MCP server configuration format used by Claude Desktop, VS Code, Cursor, and other MCP clients, with the server name as the root key:\n\n```json\n{\n  \"server-name\": {\n    \"command\": \"uv\",\n    \"args\": [\n      \"run\",\n      \"--with\",\n      \"fastmcp\",\n      \"fastmcp\",\n      \"run\",\n      \"/path/to/server.py\"\n    ],\n    \"env\": {\n      \"API_KEY\": \"value\"\n    }\n  }\n}\n```\n\n<Note>\nTo use this configuration with your MCP client, you'll typically need to add it to the client's `mcpServers` object. Consult your client's documentation for any specific configuration requirements or formatting needs.\n</Note>\n\n**Options specific to mcp-json:**\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Copy to Clipboard | `--copy` | Copy configuration to clipboard instead of printing to stdout |\n\n## `fastmcp inspect`\n\n<VersionBadge version=\"2.9.0\" />\n\nInspect a FastMCP server to view summary information or generate a detailed JSON report.\n\n```bash\n# Show text summary\nfastmcp inspect server.py\n\n# Output FastMCP JSON to stdout\nfastmcp inspect server.py --format fastmcp\n\n# Save MCP JSON to file (format required with -o)\nfastmcp inspect server.py --format mcp -o manifest.json\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Format | `--format`, `-f` | Output format: `fastmcp` (FastMCP-specific) or `mcp` (MCP protocol). Required when using `-o` |\n| Output File | `--output`, `-o` | Save JSON report to file instead of stdout. Requires `--format` |\n\n### Output Formats\n\n#### FastMCP Format (`--format fastmcp`)\nThe default and most comprehensive format, includes all FastMCP-specific metadata:\n- Server name, instructions, and version\n- FastMCP version and MCP version\n- Tool tags and enabled status\n- Output schemas for tools\n- Annotations and custom metadata\n- Uses snake_case field names\n- **Use this for**: Complete server introspection and debugging FastMCP servers\n\n#### MCP Protocol Format (`--format mcp`)\nShows exactly what MCP clients will see via the protocol:\n- Only includes standard MCP protocol fields\n- Matches output from `client.list_tools()`, `client.list_prompts()`, etc.\n- Uses camelCase field names (e.g., `inputSchema`)\n- Excludes FastMCP-specific fields like tags and enabled status\n- **Use this for**: Debugging client visibility and ensuring MCP compatibility\n\n### Entrypoints\n\nThe `inspect` command supports local FastMCP server files and configuration:\n\n1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.\n2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint  \n3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance\n4. **FastMCP configuration**: `fastmcp.json` - inspects servers defined with FastMCP's declarative configuration\n\n<Warning>\nThe `inspect` command **only supports local files and fastmcp.json** - no URLs, remote servers, or standard MCP configuration files.\n</Warning>\n\n### Examples\n\n```bash\n# Show text summary (no JSON output)\nfastmcp inspect server.py\n# Output: \n# Server: MyServer\n# Instructions: A helpful MCP server\n# Version: 1.0.0\n#\n# Components:\n#   Tools: 5\n#   Prompts: 2\n#   Resources: 3\n#   Templates: 1\n#\n# Environment:\n#   FastMCP: 2.0.0\n#   MCP: 1.0.0\n#\n# Use --format [fastmcp|mcp] for complete JSON output\n\n# Output FastMCP format to stdout\nfastmcp inspect server.py --format fastmcp\n\n# Specify server entrypoint\nfastmcp inspect server.py:my_server\n\n# Output MCP protocol format to stdout\nfastmcp inspect server.py --format mcp\n\n# Save to file (format required)\nfastmcp inspect server.py --format fastmcp -o server-manifest.json\n\n# Save MCP format with custom server object\nfastmcp inspect server.py:my_server --format mcp -o mcp-manifest.json\n\n# Error: format required with output file\nfastmcp inspect server.py -o output.json\n# Error: --format is required when using -o/--output\n```\n\n## `fastmcp project prepare`\n\nCreate a persistent uv project directory from a fastmcp.json file's environment configuration. This allows you to pre-install all dependencies once and reuse them with the `--project` flag.\n\n```bash\nfastmcp project prepare fastmcp.json --output-dir ./env\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Output Directory | `--output-dir` | **Required.** Directory where the persistent uv project will be created |\n\n### Usage Pattern\n\n```bash\n# Step 1: Prepare the environment (installs dependencies)\nfastmcp project prepare fastmcp.json --output-dir ./my-env\n\n# Step 2: Run using the prepared environment (fast, no dependency installation)\nfastmcp run fastmcp.json --project ./my-env\n```\n\nThe prepare command creates a uv project with:\n- A `pyproject.toml` containing all dependencies from the fastmcp.json\n- A `.venv` with all packages pre-installed\n- A `uv.lock` file for reproducible environments\n\nThis is useful when you want to separate environment setup from server execution, such as in deployment scenarios where dependencies are installed once and the server is run multiple times.\n\n## `fastmcp version`\n\nDisplay version information about FastMCP and related components.\n\n```bash\nfastmcp version\n```\n\n### Options\n\n| Option | Flag | Description |\n| ------ | ---- | ----------- |\n| Copy to Clipboard | `--copy` | Copy version information to clipboard |\n"
  },
  {
    "path": "docs/v2/patterns/contrib.mdx",
    "content": "---\ntitle: \"Contrib Modules\"\ndescription: \"Community-contributed modules extending FastMCP\"\nicon: \"cubes\"\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.2.1\" />\n\nFastMCP includes a `contrib` package that holds community-contributed modules. These modules extend FastMCP's functionality but aren't officially maintained by the core team.\n\nContrib 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.\n\nThe available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/contrib).\n\n## Usage\n\nTo use a contrib module, import it from the `fastmcp.contrib` package:\n\n```python\nfrom fastmcp.contrib import my_module\n```\n\n## Important Considerations\n\n- **Stability**: Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.\n- **Compatibility**: Changes to core FastMCP might break modules in `contrib` without explicit warnings in the main changelog.\n- **Dependencies**: Contrib modules may have additional dependencies not required by the core library. These dependencies are typically documented in the module's README or separate requirements files.\n\n## Contributing\n\nWe welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:\n\n1. Create a new directory in `src/fastmcp/contrib/` for your module\n3. Add proper tests for your module in `tests/contrib/`\n2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions\n5. Submit a pull request\n\nThe ideal contrib module:\n- Solves a specific use case or integration need\n- Follows FastMCP coding standards\n- Includes thorough documentation and examples\n- Has comprehensive tests\n- Specifies any additional dependencies\n"
  },
  {
    "path": "docs/v2/patterns/decorating-methods.mdx",
    "content": "---\ntitle: Decorating Methods\nsidebarTitle: Decorating Methods\ndescription: Properly use instance methods, class methods, and static methods with FastMCP decorators.\nicon: at\n---\n\nFastMCP's decorator system is designed to work with functions, but you may see unexpected behavior if you try to decorate an instance or class method. This guide explains the correct approach for using methods with all FastMCP decorators (`@tool`, `@resource`, and `@prompt`).\n\n## Why Are Methods Hard?\n\nWhen you apply a FastMCP decorator like `@tool`, `@resource`, or `@prompt` to a method, the decorator captures the function at decoration time. For instance methods and class methods, this poses a challenge because:\n\n1. For instance methods: The decorator gets the unbound method before any instance exists\n2. For class methods: The decorator gets the function before it's bound to the class\n\nThis means directly decorating these methods doesn't work as expected. In practice, the LLM would see parameters like `self` or `cls` that it cannot provide values for.\n\nAdditionally, **FastMCP decorators return objects (Tool, Resource, or Prompt instances) rather than the original function**. This means that when you decorate a method directly, the method becomes the returned object and is no longer callable by your code:\n\n<Warning>\n**Don't do this!**\n\nThe method will no longer be callable from Python, and the tool won't be callable by LLMs.\n\n```python\n\nfrom fastmcp import FastMCP\nmcp = FastMCP()\n\nclass MyClass:\n    @mcp.tool\n    def my_method(self, x: int) -> int:\n        return x * 2\n\nobj = MyClass()\nobj.my_method(5)  # Fails - my_method is a Tool, not a function\n```\n</Warning>\n\nThis is another important reason to register methods functionally after defining the class.\n\n## Recommended Patterns\n\n### Instance Methods\n\n<Warning>\n**Don't do this!**\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass MyClass:\n    @mcp.tool  # This won't work correctly\n    def add(self, x, y):\n        return x + y\n```\n</Warning>\nWhen the decorator is applied this way, it captures the unbound method. When the LLM later tries to use this component, it will see `self` as a required parameter, but it won't know what to provide for it, causing errors or unexpected behavior.\n\n<Check>\n**Do this instead**:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass MyClass:\n    def add(self, x, y):\n        return x + y\n\n# Create an instance first, then register the bound methods\nobj = MyClass()\nmcp.tool(obj.add)\n\n# Now you can call it without 'self' showing up as a parameter\nawait mcp._mcp_call_tool('add', {'x': 1, 'y': 2})  # Returns 3\n```\n</Check>\n\nThis approach works because:\n1. You first create an instance of the class (`obj`)\n2. When you access the method through the instance (`obj.add`), Python creates a bound method where `self` is already set to that instance\n3. When you register this bound method, the system sees a callable that only expects the appropriate parameters, not `self`\n\n### Class Methods\n\nThe behavior of decorating class methods depends on the order of decorators:\n\n<Warning>\n**Don't do this** (decorator order matters):\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass MyClass:\n    @classmethod\n    @mcp.tool  # This won't work but won't raise an error\n    def from_string_v1(cls, s):\n        return cls(s)\n    \n    @mcp.tool\n    @classmethod  # This will raise a helpful ValueError\n    def from_string_v2(cls, s):\n        return cls(s)\n```\n</Warning>\n\n- If `@classmethod` comes first, then `@mcp.tool`: No error is raised, but it won't work correctly\n- If `@mcp.tool` comes first, then `@classmethod`: FastMCP will detect this and raise a helpful `ValueError` with guidance\n\n<Check>\n**Do this instead**:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass MyClass:\n    @classmethod\n    def from_string(cls, s):\n        return cls(s)\n\n# Register the class method after the class is defined\nmcp.tool(MyClass.from_string)\n```\n</Check>\n\nThis works because:\n1. The `@classmethod` decorator is applied properly during class definition\n2. When you access `MyClass.from_string`, Python provides a special method object that automatically binds the class to the `cls` parameter\n3. When registered, only the appropriate parameters are exposed to the LLM, hiding the implementation detail of the `cls` parameter\n\n### Static Methods\n\nStatic methods \"work\" with FastMCP decorators, but this is not recommended because the FastMCP decorator will not return a callable method. Therefore, you should register static methods the same way as other methods.\n\n<Warning>\n**This is not recommended, though it will work.**\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass MyClass:\n    @mcp.tool\n    @staticmethod\n    def utility(x, y):\n        return x + y\n```\n</Warning>\n\nThis works because `@staticmethod` converts the method to a regular function, which the FastMCP decorator can then properly process. However, this is not recommended because the FastMCP decorator will not return a callable staticmethod. Therefore, you should register static methods the same way as other methods.\n\n<Check>\n**Prefer this pattern:**\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass MyClass:\n    @staticmethod\n    def utility(x, y):\n        return x + y\n\n# This also works\nmcp.tool(MyClass.utility)\n```\n</Check>\n\n## Additional Patterns\n\n### Creating Components at Class Initialization\n\nYou can automatically register instance methods when creating an object:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\nclass ComponentProvider:\n    def __init__(self, mcp_instance):\n        # Register methods\n        mcp_instance.tool(self.tool_method)\n        mcp_instance.resource(\"resource://data\")(self.resource_method)\n    \n    def tool_method(self, x):\n        return x * 2\n    \n    def resource_method(self):\n        return \"Resource data\"\n\n# The methods are automatically registered when creating the instance\nprovider = ComponentProvider(mcp)\n```\n\nThis pattern is useful when:\n- You want to encapsulate registration logic within the class itself\n- You have multiple related components that should be registered together\n- You want to ensure that methods are always properly registered when creating an instance\n\nThe class automatically registers its methods during initialization, ensuring they're properly bound to the instance before registration.\n\n## Summary\n\nThe current behavior of FastMCP decorators with methods is:\n\n- **Static methods**: Can be decorated directly and work perfectly with all FastMCP decorators\n- **Class methods**: Cannot be decorated directly and will raise a helpful `ValueError` with guidance\n- **Instance methods**: Should be registered after creating an instance using the decorator calls\n\nFor class and instance methods, you should register them after creating the instance or class to ensure proper method binding. This ensures that the methods are properly bound before being registered.\n\n\nUnderstanding these patterns allows you to effectively organize your components into classes while maintaining proper method binding, giving you the benefits of object-oriented design without sacrificing the simplicity of FastMCP's decorator system.\n"
  },
  {
    "path": "docs/v2/patterns/testing.mdx",
    "content": "---\ntitle: Testing your FastMCP Server\nsidebarTitle: Testing\ndescription: How to test your FastMCP server.\nicon: vial\n---\n\nThe best way to ensure a reliable and maintainable FastMCP Server is to test it! The FastMCP Client combined with Pytest provides a simple and powerful way to test your FastMCP servers.\n\n## Prerequisites\n\nTesting FastMCP servers requires `pytest-asyncio` to handle async test functions and fixtures. Install it as a development dependency:\n\n```bash\npip install pytest-asyncio\n```\n\nWe recommend configuring pytest to automatically handle async tests by setting the asyncio mode to `auto` in your `pyproject.toml`:\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\n```\n\nThis eliminates the need to decorate every async test with `@pytest.mark.asyncio`.\n\n## Testing with Pytest Fixtures\n\nUsing Pytest Fixtures, you can wrap your FastMCP Server in a Client instance that makes interacting with your server fast and easy. This is especially useful when building your own MCP Servers and enables a tight development loop by allowing you to avoid using a separate tool like MCP Inspector during development:\n\n```python\nimport pytest\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\n\nfrom my_project.main import mcp\n\n@pytest.fixture\nasync def main_mcp_client():\n    async with Client(transport=mcp) as mcp_client:\n        yield mcp_client\n\nasync def test_list_tools(main_mcp_client: Client[FastMCPTransport]):\n    list_tools = await main_mcp_client.list_tools()\n\n    assert len(list_tools) == 5\n```\n\nWe recommend the [inline-snapshot library](https://github.com/15r10nk/inline-snapshot) for asserting complex data structures coming from your MCP Server. This library allows you to write tests that are easy to read and understand, and are also easy to update when the data structure changes. \n\n```python\nfrom inline_snapshot import snapshot\n\nasync def test_list_tools(main_mcp_client: Client[FastMCPTransport]):\n    list_tools = await main_mcp_client.list_tools()\n\n    assert list_tools == snapshot()\n```\n\nSimply run `pytest --inline-snapshot=fix,create` to fill in the `snapshot()` with actual data.\n\n<Tip>\nFor values that change you can leverage the [dirty-equals](https://github.com/samuelcolvin/dirty-equals) library to perform flexible equality assertions on dynamic or non-deterministic values.\n</Tip>\n\nUsing the pytest `parametrize` decorator, you can easily test your tools with a wide variety of inputs.\n\n```python\nimport pytest\nfrom my_project.main import mcp\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\n@pytest.fixture\nasync def main_mcp_client():\n    async with Client(mcp) as client:\n        yield client\n\n\n@pytest.mark.parametrize(\n    \"first_number, second_number, expected\",\n    [\n        (1, 2, 3),\n        (2, 3, 5),\n        (3, 4, 7),\n    ],\n)\nasync def test_add(\n    first_number: int,\n    second_number: int,\n    expected: int,\n    main_mcp_client: Client[FastMCPTransport],\n):\n    result = await main_mcp_client.call_tool(\n        name=\"add\", arguments={\"x\": first_number, \"y\": second_number}\n    )\n    assert result.data is not None\n    assert isinstance(result.data, int)\n    assert result.data == expected\n```\n\n<Tip>\nThe [FastMCP Repository contains thousands of tests](https://github.com/PrefectHQ/fastmcp/tree/main/tests) for the FastMCP Client and Server. Everything from connecting to remote MCP servers, to testing tools, resources, and prompts is covered, take a look for inspiration!\n</Tip>"
  },
  {
    "path": "docs/v2/patterns/tool-transformation.mdx",
    "content": "---\ntitle: Tool Transformation\nsidebarTitle: Tool Transformation\ndescription: Create enhanced tool variants with modified schemas, argument mappings, and custom behavior.\nicon: wand-magic-sparkles\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.8.0\" />\n\nTool transformation allows you to create new, enhanced tools from existing ones. This powerful feature enables you to adapt tools for different contexts, simplify complex interfaces, or add custom logic without duplicating code.\n\n## Why Transform Tools?\n\nOften, an existing tool is *almost* perfect for your use case, but it might have:\n- A confusing description (or no description at all).\n- Argument names or descriptions that are not intuitive for an LLM (e.g., `q` instead of `query`).\n- Unnecessary parameters that you want to hide from the LLM.\n- A need for input validation before the original tool is called.\n- A need to modify or format the tool's output.\n\nInstead of rewriting the tool from scratch, you can **transform** it to fit your needs.\n\n## Basic Transformation\n\nThe primary way to create a transformed tool is with the `Tool.from_tool()` class method. At its simplest, you can use it to change a tool's top-level metadata like its `name`, `description`, or `tags`.\n\nIn the following simple example, we take a generic `search` tool and adjust its name and description to help an LLM client better understand its purpose.\n\n```python {13-21}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\n\nmcp = FastMCP()\n\n# The original, generic tool\n@mcp.tool\ndef search(query: str, category: str = \"all\") -> list[dict]:\n    \"\"\"Searches for items in the database.\"\"\"\n    return database.search(query, category)\n\n# Create a more domain-specific version by changing its metadata\nproduct_search_tool = Tool.from_tool(\n    search,\n    name=\"find_products\",\n    description=\"\"\"\n        Search for products in the e-commerce catalog. \n        Use this when customers ask about finding specific items, \n        checking availability, or browsing product categories.\n        \"\"\",\n)\n\nmcp.add_tool(product_search_tool)\n```\n\n<Tip>\nWhen you transform a tool, the original tool remains registered on the server. To avoid confusing an LLM with two similar tools, you can disable the original one:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\n\nmcp = FastMCP()\n\n# The original, generic tool\n@mcp.tool\ndef search(query: str, category: str = \"all\") -> list[dict]:\n    ...\n\n# Create a more domain-specific version\nproduct_search_tool = Tool.from_tool(search, ...)\nmcp.add_tool(product_search_tool)\n\n# Disable the original tool\nsearch.disable()\n```\n</Tip>\n\nNow, clients see a tool named `find_products` with a clear, domain-specific purpose and relevant tags, even though it still uses the original generic `search` function's logic.\n\n### Parameters\n\nThe `Tool.from_tool()` class method is the primary way to create a transformed tool. It takes the following parameters:\n\n- `tool`: The tool to transform. This is the only required argument.\n- `name`: An optional name for the new tool.\n- `description`: An optional description for the new tool.\n- `transform_args`: A dictionary of `ArgTransform` objects, one for each argument you want to modify.\n- `transform_fn`: An optional function that will be called instead of the parent tool's logic.\n- `output_schema`: Control output schema and structured outputs (see [Output Schema Control](#output-schema-control)).\n- `tags`: An optional set of tags for the new tool.\n- `annotations`: An optional set of `ToolAnnotations` for the new tool.\n- `serializer`: An optional function that will be called to serialize the result of the new tool.\n- `meta`: Control meta information for the tool. Use `None` to remove meta, any dict to set meta, or leave unset to inherit from parent.\n\nThe result is a new `TransformedTool` object that wraps the parent tool and applies the transformations you specify. You can add this tool to your MCP server using its `add_tool()` method.\n\n\n\n## Modifying Arguments\n\nTo modify a tool's parameters, provide a dictionary of `ArgTransform` objects to the `transform_args` parameter of `Tool.from_tool()`. Each key is the name of the *original* argument you want to modify.\n\n<Tip>\nYou only need to provide a `transform_args` entry for arguments you want to modify. All other arguments will be passed through unchanged.\n</Tip>\n\n### The ArgTransform Class\n\nTo modify an argument, you need to create an `ArgTransform` object. This object has the following parameters:\n\n- `name`: The new name for the argument.\n- `description`: The new description for the argument.\n- `default`: The new default value for the argument.\n- `default_factory`: A function that will be called to generate a default value for the argument. This is useful for arguments that need to be generated for each tool call, such as timestamps or unique IDs.\n- `hide`: Whether to hide the argument from the LLM.\n- `required`: Whether the argument is required, usually used to make an optional argument be required instead.\n- `type`: The new type for the argument.\n\n<Tip>\nCertain combinations of parameters are not allowed. For example, you can only use `default_factory` with `hide=True`, because dynamic defaults cannot be represented in a JSON schema for the client. You can only set required=True for arguments that do not declare a default value.\n</Tip>\n\n\n### Descriptions\n\nBy far the most common reason to transform a tool, after its own description, is to improve its argument descriptions. A good description is crucial for helping an LLM understand how to use a parameter correctly. This is especially important when wrapping tools from external APIs, whose argument descriptions may be missing or written for developers, not LLMs.\n\nIn this example, we add a helpful description to the `user_id` argument:\n\n```python {16-19}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\nmcp = FastMCP()\n\n@mcp.tool\ndef find_user(user_id: str):\n    \"\"\"Finds a user by their ID.\"\"\"\n    ...\n\nnew_tool = Tool.from_tool(\n    find_user,\n    transform_args={\n        \"user_id\": ArgTransform(\n            description=(\n                \"The unique identifier for the user, \"\n                \"usually in the format 'usr-xxxxxxxx'.\"\n            )\n        )\n    }\n)\n```\n\n### Names\n\nAt times, you may want to rename an argument to make it more intuitive for an LLM. \n\nFor example, in the following example, we take a generic `q` argument and expand it to `search_query`:\n\n```python {15}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\nmcp = FastMCP()\n\n@mcp.tool\ndef search(q: str):\n    \"\"\"Searches for items in the database.\"\"\"\n    return database.search(q)\n\nnew_tool = Tool.from_tool(\n    search,\n    transform_args={\n        \"q\": ArgTransform(name=\"search_query\")\n    }\n)\n```\n\n### Default Values\n\nYou can update the default value for any argument using the `default` parameter. Here, we change the default value of the `y` argument to 10:\n\n```python{15}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\nmcp = FastMCP()\n\n@mcp.tool\ndef add(x: int, y: int) -> int:\n    \"\"\"Adds two numbers.\"\"\"\n    return x + y\n\nnew_tool = Tool.from_tool(\n    add,\n    transform_args={\n        \"y\": ArgTransform(default=10)\n    }\n)\n```\n\nDefault values are especially useful in combination with hidden arguments.\n\n### Hiding Arguments\n\nSometimes a tool requires arguments that shouldn't be exposed to the LLM, such as API keys, configuration flags, or internal IDs. You can hide these parameters using `hide=True`. Note that you can only hide arguments that have a default value (or for which you provide a new default), because the LLM can't provide a value at call time.\n\n<Tip>\nTo pass a constant value to the parent tool, combine `hide=True` with `default=<value>`.\n</Tip>\n\n```python {19-20}\nimport os\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\nmcp = FastMCP()\n\n@mcp.tool\ndef send_email(to: str, subject: str, body: str, api_key: str):\n    \"\"\"Sends an email.\"\"\"\n    ...\n    \n# Create a simplified version that hides the API key\nnew_tool = Tool.from_tool(\n    send_email,\n    name=\"send_notification\",\n    transform_args={\n        \"api_key\": ArgTransform(\n            hide=True, \n            default=os.environ.get(\"EMAIL_API_KEY\"),\n        )\n    }\n)\n```\nThe LLM now only sees the `to`, `subject`, and `body` parameters. The `api_key` is supplied automatically from an environment variable.\n\nFor values that must be generated for each tool call (like timestamps or unique IDs), use `default_factory`, which is called with no arguments every time the tool is called. For example,\n\n```python {3-4}\ntransform_args = {\n    'timestamp': ArgTransform(\n        hide=True,\n        default_factory=lambda: datetime.now(),\n    )\n}\n```\n\n<Warning>\n`default_factory` can only be used with `hide=True`. This is because visible parameters need static defaults that can be represented in a JSON schema for the client.\n</Warning>\n\n### Meta Information\n\n<VersionBadge version=\"2.11.0\" />\n\nYou can control meta information on transformed tools using the `meta` parameter. Meta information is additional data about the tool that doesn't affect its functionality but can be used by clients for categorization, routing, or other purposes.\n\n```python {15-17}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\n\nmcp = FastMCP()\n\n@mcp.tool\ndef analyze_data(data: str) -> dict:\n    \"\"\"Analyzes the provided data.\"\"\"\n    return {\"result\": f\"Analysis of {data}\"}\n\n# Add custom meta information\nenhanced_tool = Tool.from_tool(\n    analyze_data,\n    name=\"enhanced_analyzer\",\n    meta={\n        \"category\": \"analytics\",\n        \"priority\": \"high\",\n        \"requires_auth\": True\n    }\n)\n\nmcp.add_tool(enhanced_tool)\n```\n\nYou can also remove meta information entirely:\n\n```python {6}\n# Remove meta information from parent tool\nsimplified_tool = Tool.from_tool(\n    analyze_data,\n    name=\"simple_analyzer\", \n    meta=None  # Removes any meta information\n)\n```\n\nIf you don't specify the `meta` parameter, the transformed tool inherits the parent tool's meta information.\n\n### Required Values\n\nIn rare cases where you want to make an optional argument required, you can set `required=True`. This has no effect if the argument was already required.\n\n```python {3}\ntransform_args = {\n    'user_id': ArgTransform(\n        required=True,\n    )\n}\n```\n\n## Modifying Tool Behavior\n\n<Warning>\nWith great power comes great responsibility. Modifying tool behavior is a very advanced feature.\n</Warning>\n\nIn addition to changing a tool's schema, advanced users can also modify its behavior. This is useful for adding validation logic, or for post-processing the tool's output.\n\nThe `from_tool()` method takes a `transform_fn` parameter, which is an async function that replaces the parent tool's logic and gives you complete control over the tool's execution.\n\n### The Transform Function\n\nThe `transform_fn` is an async function that **completely replaces** the parent tool's logic. \n\nCritically, the transform function's arguments are used to determine the new tool's final schema. Any arguments that are not already present in the parent tool schema OR the `transform_args` will be added to the new tool's schema. Note that when `transform_args` and your function have the same argument name, the `transform_args` metadata will take precedence, if provided.\n\n```python\nasync def my_custom_logic(user_input: str, max_length: int = 100) -> str:\n    # Your custom logic here - this completely replaces the parent tool\n    return f\"Custom result for: {user_input[:max_length]}\"\n\nTool.from_tool(transform_fn=my_custom_logic)\n```\n\n<Tip>\nThe name / docstring of the `transform_fn` are ignored. Only its arguments are used to determine the final schema.\n</Tip>\n\n### Calling the Parent Tool\n\nMost of the time, you don't want to completely replace the parent tool's behavior. Instead, you want to add validation, modify inputs, or post-process outputs while still leveraging the parent tool's core functionality. For this, FastMCP provides the special `forward()` and `forward_raw()` functions.\n\nBoth `forward()` and `forward_raw()` are async functions that let you call the parent tool from within your `transform_fn`:\n\n- **`forward()`** (recommended): Automatically handles argument mapping based on your `ArgTransform` configurations. Call it with the transformed argument names.\n- **`forward_raw()`**: Bypasses all transformation and calls the parent tool directly with its original argument names. This is rarely needed unless you're doing complex argument manipulation, perhaps without `arg_transforms`.\n\nThe most common transformation pattern is to validate (potentially renamed) arguments before calling the parent tool. Here's an example that validates that `x` and `y` are positive before calling the parent tool:\n<Tabs>\n<Tab title=\"Using forward()\">\n\nIn the simplest case, your parent tool and your transform function have the same arguments. You can call `forward()` with the same argument names as the parent tool:\n\n```python {15}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import forward\n\nmcp = FastMCP()\n\n@mcp.tool\ndef add(x: int, y: int) -> int:\n    \"\"\"Adds two numbers.\"\"\"\n    return x + y\n\nasync def ensure_positive(x: int, y: int) -> int:\n    if x <= 0 or y <= 0:\n        raise ValueError(\"x and y must be positive\")\n    return await forward(x=x, y=y)\n\nnew_tool = Tool.from_tool(\n    add,\n    transform_fn=ensure_positive,\n)\n\nmcp.add_tool(new_tool)\n```\n</Tab>\n<Tab title=\"Using forward() with renamed args\">\n\nWhen your transformed tool has different argument names than the parent tool, you can call `forward()` with the renamed arguments and it will automatically map the arguments to the parent tool's arguments:\n\n```python {15, 20-23}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import forward\n\nmcp = FastMCP()\n\n@mcp.tool\ndef add(x: int, y: int) -> int:\n    \"\"\"Adds two numbers.\"\"\"\n    return x + y\n\nasync def ensure_positive(a: int, b: int) -> int:\n    if a <= 0 or b <= 0:\n        raise ValueError(\"a and b must be positive\")\n    return await forward(a=a, b=b)\n\nnew_tool = Tool.from_tool(\n    add,\n    transform_fn=ensure_positive,\n    transform_args={\n        \"x\": ArgTransform(name=\"a\"),\n        \"y\": ArgTransform(name=\"b\"),\n    }\n)\n\nmcp.add_tool(new_tool)\n```\n</Tab>\n<Tab title=\"Using forward_raw()\">\nFinally, you can use `forward_raw()` to bypass all argument mapping and call the parent tool directly with its original argument names.\n\n```python {15, 20-23}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import forward\n\nmcp = FastMCP()\n\n@mcp.tool\ndef add(x: int, y: int) -> int:\n    \"\"\"Adds two numbers.\"\"\"\n    return x + y\n\nasync def ensure_positive(a: int, b: int) -> int:\n    if a <= 0 or b <= 0:\n        raise ValueError(\"a and b must be positive\")\n    return await forward_raw(x=a, y=b)\n\nnew_tool = Tool.from_tool(\n    add,\n    transform_fn=ensure_positive,\n    transform_args={\n        \"x\": ArgTransform(name=\"a\"),\n        \"y\": ArgTransform(name=\"b\"),\n    }\n)\n\nmcp.add_tool(new_tool)\n```\n</Tab>\n</Tabs>\n\n### Passing Arguments with **kwargs\n\nIf your `transform_fn` includes `**kwargs` in its signature, it will receive **all arguments from the parent tool after `ArgTransform` configurations have been applied**. This is powerful for creating flexible validation functions that don't require you to add every argument to the function signature.\n\nIn the following example, we wrap a parent tool that accepts two arguments `x` and `y`. These are renamed to `a` and `b` in the transformed tool, and the transform only validates `a`, passing the other argument through as `**kwargs`.\n\n```python {12, 15}\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import forward, ArgTransform\n\nmcp = FastMCP()\n\n@mcp.tool\ndef add(x: int, y: int) -> int:\n    \"\"\"Adds two numbers.\"\"\"\n    return x + y\n\nasync def ensure_a_positive(a: int, **kwargs) -> int:\n    if a <= 0:\n        raise ValueError(\"a must be positive\")\n    return await forward(a=a, **kwargs)\n\nnew_tool = Tool.from_tool(\n    add,\n    transform_fn=ensure_a_positive,\n    transform_args={\n        \"x\": ArgTransform(name=\"a\"),\n        \"y\": ArgTransform(name=\"b\"),\n    }\n)\n\nmcp.add_tool(new_tool)\n```\n\n<Tip>\nIn the above example, `**kwargs` receives the renamed argument `b`, not the original argument `y`. It is therefore recommended to use with `forward()`, not `forward_raw()`.\n</Tip>\n\n## Modifying MCP Tools with MCPConfig\n\nWhen running MCP Servers under FastMCP with `MCPConfig`, you can also apply a subset of tool transformations\ndirectly in the MCPConfig json file.\n\n```json\n{\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather.example.com/mcp\",\n            \"transport\": \"http\",\n            \"tools\": {\n                \"weather_get_forecast\": {\n                    \"name\": \"miami_weather\",\n                    \"description\": \"Get the weather for Miami\",\n                    \"meta\": {\n                        \"category\": \"weather\",\n                        \"location\": \"miami\"\n                    },\n                    \"arguments\": {\n                        \"city\": {\n                            \"name\": \"city\",\n                            \"default\": \"Miami\",\n                            \"hide\": True,\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\nThe `tools` section is a dictionary of tool names to tool configurations. Each tool configuration is a\ndictionary of tool properties.\n\nSee the [MCPConfigTransport](/v2/clients/transports#tool-transformation-with-fastmcp-and-mcpconfig) documentation for more details.\n\n\n## Output Schema Control\n\n<VersionBadge version=\"2.10.0\" />\n\nTransformed tools inherit output schemas from their parent by default, but you can control this behavior:\n\n**Inherit from Parent (Default)**\n```python\nTool.from_tool(parent_tool, name=\"renamed_tool\")\n```\nThe transformed tool automatically uses the parent tool's output schema and structured output behavior.\n\n**Custom Output Schema**\n```python\nTool.from_tool(parent_tool, output_schema={\n    \"type\": \"object\", \n    \"properties\": {\"status\": {\"type\": \"string\"}}\n})\n```\nProvide your own schema that differs from the parent. The tool must return data matching this schema.\n\n**Remove Output Schema**\n```python\nTool.from_tool(parent_tool, output_schema=None)\n```\nRemoves the output schema declaration. Automatic structured content still works for object-like returns (dict, dataclass, Pydantic models) but primitive types won't be structured.\n\n**Full Control with Transform Functions**\n```python\nasync def custom_output(**kwargs) -> ToolResult:\n    result = await forward(**kwargs)\n    return ToolResult(content=[...], structured_content={...})\n\nTool.from_tool(parent_tool, transform_fn=custom_output)\n```\nUse a transform function returning `ToolResult` for complete control over both content blocks and structured outputs.\n\n## Common Patterns\n\nTool transformation is a flexible feature that supports many powerful patterns. Here are a few common use cases to give you ideas.\n\n### Exposing Client Methods as Tools\n\nA powerful use case for tool transformation is exposing methods from existing Python clients (GitHub clients, API clients, database clients, etc.) directly as MCP tools. This pattern eliminates boilerplate wrapper functions and treats tools as annotations around client methods.\n\n**Without Tool Transformation**, you typically create wrapper functions that duplicate annotations:\n\n```python\nasync def get_repository(\n    owner: Annotated[str, \"The owner of the repository.\"],\n    repo: Annotated[str, \"The name of the repository.\"],\n) -> Repository:\n    \"\"\"Get basic information about a GitHub repository.\"\"\"\n    return await github_client.get_repository(owner=owner, repo=repo)\n```\n\n**With Tool Transformation**, you can wrap the client method directly:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\nmcp = FastMCP(\"GitHub Tools\")\n\n# Wrap a client method directly as a tool\nget_repo_tool = Tool.from_tool(\n    tool=Tool.from_function(fn=github_client.get_repository),\n    description=\"Get basic information about a GitHub repository.\",\n    transform_args={\n        \"owner\": ArgTransform(description=\"The owner of the repository.\"),\n        \"repo\": ArgTransform(description=\"The name of the repository.\"),\n    }\n)\n\nmcp.add_tool(get_repo_tool)\n```\n\nThis pattern keeps the implementation in your client and treats the tool as an annotation layer, avoiding duplicate code.\n\n#### Hiding Client-Specific Arguments\n\nClient methods often have internal parameters (debug flags, auth tokens, rate limit settings) that shouldn't be exposed to LLMs. Use `hide=True` with a default value to handle these automatically:\n\n```python\nget_issues_tool = Tool.from_tool(\n    tool=Tool.from_function(fn=github_client.get_issues),\n    description=\"Get issues from a GitHub repository.\",\n    transform_args={\n        \"owner\": ArgTransform(description=\"The owner of the repository.\"),\n        \"repo\": ArgTransform(description=\"The name of the repository.\"),\n        \"limit\": ArgTransform(description=\"Maximum number of issues to return.\"),\n        # Hide internal parameters\n        \"include_debug_info\": ArgTransform(hide=True, default=False),\n        \"error_on_not_found\": ArgTransform(hide=True, default=True),\n    }\n)\n\nmcp.add_tool(get_issues_tool)\n```\n\nThe LLM only sees `owner`, `repo`, and `limit`. Internal parameters are supplied automatically.\n\n#### Reusable Argument Patterns\n\nWhen wrapping multiple client methods, you can define reusable argument transformations. This scales well for larger tool sets and keeps annotations consistent:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\nmcp = FastMCP(\"GitHub Tools\")\n\n# Define reusable argument patterns\nOWNER_ARG = ArgTransform(description=\"The repository owner.\")\nREPO_ARG = ArgTransform(description=\"The repository name.\")\nLIMIT_ARG = ArgTransform(description=\"Maximum number of items to return.\")\nHIDE_ERROR = ArgTransform(hide=True, default=True)\n\ndef create_github_tools(client):\n    \"\"\"Create tools from GitHub client methods with shared argument patterns.\"\"\"\n\n    owner_repo_args = {\n        \"owner\": OWNER_ARG,\n        \"repo\": REPO_ARG,\n    }\n\n    error_args = {\n        \"error_on_not_found\": HIDE_ERROR,\n    }\n\n    return [\n        Tool.from_tool(\n            tool=Tool.from_function(fn=client.get_repository),\n            description=\"Get basic information about a GitHub repository.\",\n            transform_args={**owner_repo_args, **error_args}\n        ),\n        Tool.from_tool(\n            tool=Tool.from_function(fn=client.get_issue),\n            description=\"Get a specific issue from a repository.\",\n            transform_args={\n                **owner_repo_args,\n                \"issue_number\": ArgTransform(description=\"The issue number.\"),\n                \"limit_comments\": LIMIT_ARG,\n                **error_args,\n            }\n        ),\n        Tool.from_tool(\n            tool=Tool.from_function(fn=client.get_pull_request),\n            description=\"Get a specific pull request from a repository.\",\n            transform_args={\n                **owner_repo_args,\n                \"pull_request_number\": ArgTransform(description=\"The PR number.\"),\n                \"limit_comments\": LIMIT_ARG,\n                **error_args,\n            }\n        ),\n    ]\n\n# Add all tools to the server\nfor tool in create_github_tools(github_client):\n    mcp.add_tool(tool)\n```\n\nThis pattern provides several benefits:\n\n- **No duplicate implementation**: Logic stays in the client\n- **Consistent annotations**: Reusable argument patterns ensure consistency\n- **Easy maintenance**: Update the client, not wrapper functions\n- **Scalable**: Easily add new tools by wrapping additional client methods\n\n### Adapting Remote or Generated Tools\nThis is one of the most common reasons to use tool transformation. Tools from remote MCP servers (via a [proxy](/v2/servers/proxy)) or generated from an [OpenAPI spec](/v2/integrations/openapi) are often too generic for direct use by an LLM. You can use transformation to create a simpler, more intuitive version for your specific needs.\n\n### Chaining Transformations\nYou can chain transformations by using an already transformed tool as the parent for a new transformation. This lets you build up complex behaviors in layers, for example, first renaming arguments, and then adding validation logic to the renamed tool.\n\n### Context-Aware Tool Factories\nYou can write functions that act as \"factories,\" generating specialized versions of a tool for different contexts. For example, you could create a `get_my_data` tool that is specific to the currently logged-in user by hiding the `user_id` parameter and providing it automatically.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool, tool\nfrom fastmcp.tools.tool_transform import ArgTransform\n\n# A generic tool that requires a user_id\n@tool\ndef get_user_data(user_id: str, query: str) -> str:\n    \"\"\"Fetch data for a specific user.\"\"\"\n    return f\"Data for user {user_id}: {query}\"\n\n\ndef create_user_tool(user_id: str) -> Tool:\n    \"\"\"Factory that creates a user-specific version of get_user_data.\"\"\"\n    return Tool.from_tool(\n        get_user_data,\n        name=\"get_my_data\",\n        description=\"Fetch your data. No need to specify a user ID.\",\n        transform_args={\n            \"user_id\": ArgTransform(hide=True, default=user_id),\n        },\n    )\n\n\n# Create a server with a tool customized for the current user\nmcp = FastMCP(\"User Server\")\ncurrent_user_id = \"user-123\"  # e.g., from auth context\nmcp.add_tool(create_user_tool(current_user_id))\n\n# Clients see \"get_my_data(query: str)\" — user_id is injected automatically\n```\n"
  },
  {
    "path": "docs/v2/servers/auth/authentication.mdx",
    "content": "---\ntitle: Authentication\nsidebarTitle: Overview\ndescription: Secure your FastMCP server with flexible authentication patterns, from simple API keys to full OAuth 2.1 integration with external identity providers.\nicon: user-shield\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nAuthentication in MCP presents unique challenges that differ from traditional web applications. MCP clients need to discover authentication requirements automatically, negotiate OAuth flows without user intervention, and work seamlessly across different identity providers. FastMCP addresses these challenges by providing authentication patterns that integrate with the MCP protocol while remaining simple to implement and deploy.\n\n<Tip>\nAuthentication applies only to FastMCP's HTTP-based transports (`http` and `sse`). The STDIO transport inherits security from its local execution environment.\n</Tip>\n\n<Warning>\n**Authentication is rapidly evolving in MCP.** The specification and best practices are changing quickly. FastMCP aims to provide stable, secure patterns that adapt to these changes while keeping your code simple and maintainable.\n</Warning>\n\n## MCP Authentication Challenges\n\nTraditional web authentication assumes a human user with a browser who can interact with login forms and consent screens. MCP clients are often automated systems that need to authenticate without human intervention. This creates several unique requirements:\n\n**Automatic Discovery**: MCP clients must discover authentication requirements by examining server metadata rather than encountering login redirects.\n\n**Programmatic OAuth**: OAuth flows must work without human interaction, relying on pre-configured credentials or Dynamic Client Registration.\n\n**Token Management**: Clients need to obtain, refresh, and manage tokens automatically across multiple MCP servers.\n\n**Protocol Integration**: Authentication must integrate cleanly with MCP's transport mechanisms and error handling.\n\nThese challenges mean that not all authentication approaches work well with MCP. The patterns that do work fall into three categories based on the level of authentication responsibility your server assumes.\n\n## Authentication Responsibility\n\nAuthentication responsibility exists on a spectrum. Your MCP server can validate tokens created elsewhere, coordinate with external identity providers, or handle the complete authentication lifecycle internally. Each approach involves different trade-offs between simplicity, security, and control.\n\n### Token Validation\n\nYour server validates tokens but delegates their creation to external systems. This approach treats your MCP server as a pure resource server that trusts tokens signed by known issuers.\n\nToken validation works well when you already have authentication infrastructure that can issue structured tokens like JWTs. Your existing API gateway, microservices platform, or enterprise SSO system becomes the source of truth for user identity, while your MCP server focuses on its core functionality.\n\nThe key insight is that token validation separates authentication (proving who you are) from authorization (determining what you can do). Your MCP server receives proof of identity in the form of a signed token and makes access decisions based on the claims within that token.\n\nThis pattern excels in microservices architectures where multiple services need to validate the same tokens, or when integrating MCP servers into existing systems that already handle user authentication.\n\n### External Identity Providers\n\nYour server coordinates with established identity providers to create seamless authentication experiences for MCP clients. This approach leverages OAuth 2.0 and OpenID Connect protocols to delegate user authentication while maintaining control over authorization decisions.\n\nExternal identity providers handle the complex aspects of authentication: user credential verification, multi-factor authentication, account recovery, and security monitoring. Your MCP server receives tokens from these trusted providers and validates them using the provider's public keys.\n\nThe MCP protocol's support for Dynamic Client Registration makes this pattern particularly powerful. MCP clients can automatically discover your authentication requirements and register themselves with your identity provider without manual configuration.\n\nThis approach works best for production applications that need enterprise-grade authentication features without the complexity of building them from scratch. It scales well across multiple applications and provides consistent user experiences.\n\n### Full OAuth Implementation\n\nYour server implements a complete OAuth 2.0 authorization server, handling everything from user credential verification to token lifecycle management. This approach provides maximum control at the cost of significant complexity.\n\nFull OAuth implementation means building user interfaces for login and consent, implementing secure credential storage, managing token lifecycles, and maintaining ongoing security updates. The complexity extends beyond initial implementation to include threat monitoring, compliance requirements, and keeping pace with evolving security best practices.\n\nThis pattern makes sense only when you need complete control over the authentication process, operate in air-gapped environments, or have specialized requirements that external providers cannot meet.\n\n## FastMCP Authentication Providers\n\nFastMCP translates these authentication responsibility levels into a variety of concrete classes that handle the complexities of MCP protocol integration. You can build on these classes to handle the complexities of MCP protocol integration.\n\n### TokenVerifier\n\n`TokenVerifier` provides pure token validation without OAuth metadata endpoints. This class focuses on the essential task of determining whether a token is valid and extracting authorization information from its claims.\n\nThe implementation handles JWT signature verification, expiration checking, and claim extraction. It validates tokens against known issuers and audiences, ensuring that tokens intended for your server are not accepted by other systems.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nauth = JWTVerifier(\n    jwks_uri=\"https://your-auth-system.com/.well-known/jwks.json\",\n    issuer=\"https://your-auth-system.com\", \n    audience=\"your-mcp-server\"\n)\n\nmcp = FastMCP(name=\"Protected Server\", auth=auth)\n```\n\nThis example configures token validation against a JWT issuer. The `JWTVerifier` will fetch public keys from the JWKS endpoint and validate incoming tokens against those keys. Only tokens with the correct issuer and audience claims will be accepted.\n\n`TokenVerifier` works well when you control both the token issuer and your MCP server, or when integrating with existing JWT-based infrastructure.\n\n→ **Complete guide**: [Token Verification](/v2/servers/auth/token-verification)\n\n### RemoteAuthProvider\n\n`RemoteAuthProvider` enables authentication with identity providers that **support Dynamic Client Registration (DCR)**, such as Descope and WorkOS AuthKit. With DCR, MCP clients can automatically register themselves with the identity provider and obtain credentials without any manual configuration.\n\nThis class combines token validation with OAuth discovery metadata. It extends `TokenVerifier` functionality by adding OAuth 2.0 protected resource endpoints that advertise your authentication requirements. MCP clients examine these endpoints to understand which identity providers you trust and how to obtain valid tokens.\n\nThe key requirement is that your identity provider must support DCR - the ability for clients to dynamically register and obtain credentials. This is what enables the seamless, automated authentication flow that MCP requires.\n\nFor example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully supports DCR:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\nauth = AuthKitProvider(\n    authkit_domain=\"https://your-project.authkit.app\",\n    base_url=\"https://your-fastmcp-server.com\"\n)\n\nmcp = FastMCP(name=\"Enterprise Server\", auth=auth)\n```\n\nThis example uses WorkOS AuthKit as the external identity provider. The `AuthKitProvider` automatically configures token validation against WorkOS and provides the OAuth metadata that MCP clients need for automatic authentication.\n\n`RemoteAuthProvider` is ideal for production applications when your identity provider supports Dynamic Client Registration (DCR). This enables fully automated authentication without manual client configuration.\n\n→ **Complete guide**: [Remote OAuth](/v2/servers/auth/remote-oauth)\n\n### OAuthProxy\n\n<VersionBadge version=\"2.12.0\" />\n\n`OAuthProxy` enables authentication with OAuth providers that **don't support Dynamic Client Registration (DCR)**, such as GitHub, Google, Azure, AWS, and most traditional enterprise identity systems.\n\nWhen identity providers require manual app registration and fixed credentials, `OAuthProxy` bridges the gap. It presents a DCR-compliant interface to MCP clients (accepting any registration request) while using your pre-registered credentials with the upstream provider. The proxy handles the complexity of callback forwarding, enabling dynamic client callbacks to work with providers that require fixed redirect URIs.\n\nThis class solves the fundamental incompatibility between MCP's expectation of dynamic registration and traditional OAuth providers' requirement for manual app registration.\n\nFor example, the built-in `GitHubProvider` extends `OAuthProxy` to work with GitHub's OAuth system:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=\"Ov23li...\",  # Your GitHub OAuth App ID\n    client_secret=\"abc123...\",  # Your GitHub OAuth App Secret\n    base_url=\"https://your-server.com\"\n)\n\nmcp = FastMCP(name=\"GitHub-Protected Server\", auth=auth)\n```\n\nThis example uses the GitHub provider, which extends `OAuthProxy` with GitHub-specific token validation. The proxy handles the complete OAuth flow while making GitHub's non-DCR authentication work seamlessly with MCP clients.\n\n`OAuthProxy` is essential when integrating with OAuth providers that don't support DCR. This includes most established providers like GitHub, Google, and Azure, which require manual app registration through their developer consoles.\n\n→ **Complete guide**: [OAuth Proxy](/v2/servers/auth/oauth-proxy)\n\n### OAuthProvider\n\n`OAuthProvider` implements a complete OAuth 2.0 authorization server within your MCP server. This class handles the full authentication lifecycle from user credential verification to token management.\n\nThe implementation provides all required OAuth endpoints including authorization, token, and discovery endpoints. It manages client registration, user consent, and token lifecycle while integrating with your user storage and authentication logic.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.oauth import MyOAuthProvider\n\nauth = MyOAuthProvider(\n    user_store=your_user_database,\n    client_store=your_client_registry,\n    # Additional configuration...\n)\n\nmcp = FastMCP(name=\"Auth Server\", auth=auth)\n```\n\nThis example shows the basic structure of a custom OAuth provider. The actual implementation requires significant additional configuration for user management, client registration, and security policies.\n\n`OAuthProvider` should be used only when you have specific requirements that external providers cannot meet and the expertise to implement OAuth securely.\n\n→ **Complete guide**: [Full OAuth Server](/v2/servers/auth/full-oauth-server)\n\n## Configuration Approaches\n\nFastMCP supports both programmatic configuration for maximum flexibility and environment-based configuration for deployment simplicity.\n\n### Programmatic Configuration\n\nProgrammatic configuration provides complete control over authentication settings and allows for complex initialization logic. This approach works well during development and when you need to customize authentication behavior based on runtime conditions.\n\nAuthentication providers are instantiated directly in your code with their required parameters. This makes dependencies explicit and allows your IDE to provide helpful autocompletion and type checking.\n\n### Environment Configuration\n\n<VersionBadge version=\"2.12.1\" />\n\nEnvironment-based configuration separates authentication settings from application code, enabling the same codebase to work across different deployment environments without modification.\n\nFastMCP automatically detects authentication configuration from environment variables when no explicit `auth` parameter is provided. The configuration system supports all authentication providers and their various options.\n\n#### Provider Configuration\n\nAuthentication providers are configured by specifying the full module path to the provider class:\n\n<ParamField path=\"FASTMCP_SERVER_AUTH\" type=\"string\">\nThe full module path to the authentication provider class. Examples:\n- `fastmcp.server.auth.providers.github.GitHubProvider` - GitHub OAuth\n- `fastmcp.server.auth.providers.google.GoogleProvider` - Google OAuth\n- `fastmcp.server.auth.providers.jwt.JWTVerifier` - JWT token verification\n- `fastmcp.server.auth.providers.workos.WorkOSProvider` - WorkOS OAuth\n- `fastmcp.server.auth.providers.workos.AuthKitProvider` - WorkOS AuthKit\n- `mycompany.auth.CustomProvider` - Your custom provider class\n</ParamField>\n\nWhen using providers like GitHub or Google, you'll need to set provider-specific environment variables:\n\n```bash\n# GitHub OAuth\nexport FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider\nexport FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=\"Ov23li...\"\nexport FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET=\"github_pat_...\"\n\n# Google OAuth\nexport FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider\nexport FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=\"123456.apps.googleusercontent.com\"\nexport FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET=\"GOCSPX-...\"\n```\n\n#### Provider-Specific Configuration\n\nEach provider has its own configuration options set through environment variables:\n\n```bash\n# JWT Token Verification\nexport FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.jwt.JWTVerifier\nexport FASTMCP_SERVER_AUTH_JWT_JWKS_URI=\"https://auth.example.com/jwks\"\nexport FASTMCP_SERVER_AUTH_JWT_ISSUER=\"https://auth.example.com\"\nexport FASTMCP_SERVER_AUTH_JWT_AUDIENCE=\"mcp-server\"\n\n# Custom Provider\nexport FASTMCP_SERVER_AUTH=mycompany.auth.CustomProvider\n# Plus any environment variables your custom provider expects\n```\n\nWith these environment variables set, creating an authenticated FastMCP server requires no additional configuration:\n\n```python\nfrom fastmcp import FastMCP\n\n# Authentication automatically configured from environment\nmcp = FastMCP(name=\"My Server\")\n```\n\nThis approach simplifies deployment pipelines and follows twelve-factor app principles for configuration management.\n\n## Choosing Your Implementation\n\nThe authentication approach you choose depends on your existing infrastructure, security requirements, and operational constraints.\n\n**For OAuth providers without DCR support (GitHub, Google, Azure, AWS, most enterprise systems), use OAuth Proxy.** These providers require manual app registration through their developer consoles. OAuth Proxy bridges the gap by presenting a DCR-compliant interface to MCP clients while using your fixed credentials with the provider. The proxy's callback forwarding pattern enables dynamic client ports to work with providers that require fixed redirect URIs.\n\n**For identity providers with DCR support (Descope, WorkOS AuthKit, modern auth platforms), use RemoteAuthProvider.** These providers allow clients to dynamically register and obtain credentials without manual configuration. This enables the fully automated authentication flow that MCP is designed for, providing the best user experience and simplest implementation.\n\n**Token validation works well when you already have authentication infrastructure that issues structured tokens.** If your organization already uses JWT-based systems, API gateways, or enterprise SSO that can generate tokens, this approach integrates seamlessly while keeping your MCP server focused on its core functionality. The simplicity comes from leveraging existing investment in authentication infrastructure.\n\n**Full OAuth implementation should be avoided unless you have compelling reasons that external providers cannot address.** Air-gapped environments, specialized compliance requirements, or unique organizational constraints might justify this approach, but it requires significant security expertise and ongoing maintenance commitment. The complexity extends far beyond initial implementation to include threat monitoring, security updates, and keeping pace with evolving attack vectors.\n\nFastMCP's architecture supports migration between these approaches as your requirements evolve. You can integrate with existing token systems initially and migrate to external identity providers as your application scales, or implement custom solutions when your requirements outgrow standard patterns."
  },
  {
    "path": "docs/v2/servers/auth/full-oauth-server.mdx",
    "content": "---\ntitle: Full OAuth Server\nsidebarTitle: Full OAuth Server\ndescription: Build a self-contained authentication system where your FastMCP server manages users, issues tokens, and validates them.\nicon: users-between-lines\n\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\n<Warning>\n**This is an extremely advanced pattern that most users should avoid.** Building a secure OAuth 2.1 server requires deep expertise in authentication protocols, cryptography, and security best practices. The complexity extends far beyond initial implementation to include ongoing security monitoring, threat response, and compliance maintenance.\n\n**Use [Remote OAuth](/v2/servers/auth/remote-oauth) instead** unless you have compelling requirements that external identity providers cannot meet, such as air-gapped environments or specialized compliance needs.\n</Warning>\n\nThe Full OAuth Server pattern exists to support the MCP protocol specification's requirements. Your FastMCP server becomes both an Authorization Server and Resource Server, handling the complete authentication lifecycle from user login to token validation.\n\nThis documentation exists for completeness - the vast majority of applications should use external identity providers instead.\n\n## OAuthProvider\n\nFastMCP provides the `OAuthProvider` abstract class that implements the OAuth 2.1 specification. To use this pattern, you must subclass `OAuthProvider` and implement all required abstract methods.\n\n<Note>\n`OAuthProvider` handles OAuth endpoints, protocol flows, and security requirements, but delegates all storage, user management, and business logic to your implementation of the abstract methods.\n</Note>\n\n## Required Implementation\n\nYou must implement these abstract methods to create a functioning OAuth server:\n\n### Client Management\n\n<Card icon=\"code\" title=\"Client Management Methods\">\n<ParamField body=\"get_client\" type=\"async method\">\n  Retrieve client information by ID from your database.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client_id\" type=\"str\">\n      Client identifier to look up\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"OAuthClientInformationFull | None\" type=\"return type\">\n      Client information object or `None` if client not found\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"register_client\" type=\"async method\">\n  Store new client registration information in your database.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client_info\" type=\"OAuthClientInformationFull\">\n      Complete client registration information to store\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"None\" type=\"return type\">\n      No return value\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\n### Authorization Flow\n\n<Card icon=\"code\" title=\"Authorization Flow Methods\">\n<ParamField body=\"authorize\" type=\"async method\">\n  Handle authorization request and return redirect URL. Must implement user authentication and consent collection.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client making the authorization request\n    </ParamField>\n    <ParamField body=\"params\" type=\"AuthorizationParams\">\n      Authorization request parameters from the client\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"str\" type=\"return type\">\n      Redirect URL to send the client to\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"load_authorization_code\" type=\"async method\">\n  Load authorization code from storage by code string. Return `None` if code is invalid or expired.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client attempting to use the authorization code\n    </ParamField>\n    <ParamField body=\"authorization_code\" type=\"str\">\n      Authorization code string to look up\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"AuthorizationCode | None\" type=\"return type\">\n      Authorization code object or `None` if not found\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\n### Token Management\n\n<Card icon=\"code\" title=\"Token Management Methods\">\n<ParamField body=\"exchange_authorization_code\" type=\"async method\">\n  Exchange authorization code for access and refresh tokens. Must validate code and create new tokens.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client exchanging the authorization code\n    </ParamField>\n    <ParamField body=\"authorization_code\" type=\"AuthorizationCode\">\n      Valid authorization code object to exchange\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"OAuthToken\" type=\"return type\">\n      New OAuth token containing access and refresh tokens\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"load_refresh_token\" type=\"async method\">\n  Load refresh token from storage by token string. Return `None` if token is invalid or expired.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client attempting to use the refresh token\n    </ParamField>\n    <ParamField body=\"refresh_token\" type=\"str\">\n      Refresh token string to look up\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"RefreshToken | None\" type=\"return type\">\n      Refresh token object or `None` if not found\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"exchange_refresh_token\" type=\"async method\">\n  Exchange refresh token for new access/refresh token pair. Must validate scopes and token.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"client\" type=\"OAuthClientInformationFull\">\n      OAuth client using the refresh token\n    </ParamField>\n    <ParamField body=\"refresh_token\" type=\"RefreshToken\">\n      Valid refresh token object to exchange\n    </ParamField>\n    <ParamField body=\"scopes\" type=\"list[str]\">\n      Requested scopes for the new access token\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"OAuthToken\" type=\"return type\">\n      New OAuth token with updated access and refresh tokens\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"load_access_token\" type=\"async method\">\n  Load an access token by its token string.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"token\" type=\"str\">\n      The access token to verify\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"AccessToken | None\" type=\"return type\">\n      The access token object, or `None` if the token is invalid\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"revoke_token\" type=\"async method\">\n  Revoke access or refresh token, marking it as invalid in storage.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"token\" type=\"AccessToken | RefreshToken\">\n      Token object to revoke and mark invalid\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"None\" type=\"return type\">\n      No return value\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"verify_token\" type=\"async method\">\n  Verify bearer token for incoming requests. Return `AccessToken` if valid, `None` if invalid.\n  \n  <Expandable title=\"Parameters\">\n    <ParamField body=\"token\" type=\"str\">\n      Bearer token string from incoming request\n    </ParamField>\n  </Expandable>\n  \n  <Expandable title=\"Returns\">\n    <ParamField body=\"AccessToken | None\" type=\"return type\">\n      Access token object if valid, `None` if invalid or expired\n    </ParamField>\n  </Expandable>\n</ParamField>\n</Card>\n\nEach method must handle storage, validation, security, and error cases according to the OAuth 2.1 specification. The implementation complexity is substantial and requires expertise in OAuth security considerations.\n\n<Warning>\n**Security Notice:** OAuth server implementation involves numerous security considerations including PKCE, state parameters, redirect URI validation, token binding, replay attack prevention, and secure storage requirements. Mistakes can lead to serious security vulnerabilities.\n</Warning>"
  },
  {
    "path": "docs/v2/servers/auth/oauth-proxy.mdx",
    "content": "---\ntitle: OAuth Proxy\nsidebarTitle: OAuth Proxy\ndescription: Bridge traditional OAuth providers to work seamlessly with MCP's authentication flow.\nicon: share\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.12.0\" />\n\nThe OAuth proxy enables FastMCP servers to authenticate with OAuth providers that **don't support Dynamic Client Registration (DCR)**. This includes virtually all traditional OAuth providers: GitHub, Google, Azure, AWS, Discord, Facebook, and most enterprise identity systems. For providers that do support DCR (like Descope and WorkOS AuthKit), use [`RemoteAuthProvider`](/v2/servers/auth/remote-oauth) instead.\n\nMCP clients expect to register automatically and obtain credentials on the fly, but traditional providers require manual app registration through their developer consoles. The OAuth proxy bridges this gap by presenting a DCR-compliant interface to MCP clients while using your pre-registered credentials with the upstream provider. When a client attempts to register, the proxy returns your fixed credentials. When a client initiates authorization, the proxy handles the complexity of callback forwarding—storing the client's dynamic callback URL, using its own fixed callback with the provider, then forwarding back to the client after token exchange.\n\nThis approach enables any MCP client (whether using random localhost ports or fixed URLs like Claude.ai) to authenticate with any traditional OAuth provider, all while maintaining full OAuth 2.1 and PKCE security.\n\n<Note>\n  For providers that support OIDC discovery (Auth0, Google with OIDC\n  configuration, Azure AD), consider using [`OIDC\n  Proxy`](/v2/servers/auth/oidc-proxy) for automatic configuration. OIDC Proxy\n  extends the OAuth proxy to automatically discover endpoints from the provider's\n  `/.well-known/openid-configuration` URL, simplifying setup.\n</Note>\n\n## Implementation\n\n### Provider Setup Requirements\n\nBefore using the OAuth proxy, you need to register your application with your OAuth provider:\n\n1. **Register your application** in the provider's developer console (GitHub Settings, Google Cloud Console, Azure Portal, etc.)\n2. **Configure the redirect URI** as your FastMCP server URL plus your chosen callback path:\n   - Default: `https://your-server.com/auth/callback`\n   - Custom: `https://your-server.com/your/custom/path` (if you set `redirect_path`)\n   - Development: `http://localhost:8000/auth/callback`\n3. **Obtain your credentials**: Client ID and Client Secret\n4. **Note the OAuth endpoints**: Authorization URL and Token URL (usually found in the provider's OAuth documentation)\n\n<Warning>\n  The redirect URI you configure with your provider must exactly match your\n  FastMCP server's URL plus the callback path. If you customize `redirect_path`\n  in the OAuth proxy, update your provider's redirect URI accordingly.\n</Warning>\n\n### Basic Setup\n\nHere's how to implement the OAuth proxy with any provider:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Configure token verification for your provider\n# See the Token Verification guide for provider-specific setups\ntoken_verifier = JWTVerifier(\n    jwks_uri=\"https://your-provider.com/.well-known/jwks.json\",\n    issuer=\"https://your-provider.com\",\n    audience=\"your-app-id\"\n)\n\n# Create the OAuth proxy\nauth = OAuthProxy(\n    # Provider's OAuth endpoints (from their documentation)\n    upstream_authorization_endpoint=\"https://provider.com/oauth/authorize\",\n    upstream_token_endpoint=\"https://provider.com/oauth/token\",\n\n    # Your registered app credentials\n    upstream_client_id=\"your-client-id\",\n    upstream_client_secret=\"your-client-secret\",\n\n    # Token validation (see Token Verification guide)\n    token_verifier=token_verifier,\n\n    # Your FastMCP server's public URL\n    base_url=\"https://your-server.com\",\n\n    # Optional: customize the callback path (default is \"/auth/callback\")\n    # redirect_path=\"/custom/callback\",\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\n### Configuration Parameters\n\n<Card icon=\"code\" title=\"OAuthProxy Parameters\">\n<ParamField body=\"upstream_authorization_endpoint\" type=\"str\" required>\n  URL of your OAuth provider's authorization endpoint (e.g., `https://github.com/login/oauth/authorize`)\n</ParamField>\n\n<ParamField body=\"upstream_token_endpoint\" type=\"str\" required>\n  URL of your OAuth provider's token endpoint (e.g.,\n  `https://github.com/login/oauth/access_token`)\n</ParamField>\n\n<ParamField body=\"upstream_client_id\" type=\"str\" required>\n  Client ID from your registered OAuth application\n</ParamField>\n\n<ParamField body=\"upstream_client_secret\" type=\"str\" required>\n  Client secret from your registered OAuth application\n</ParamField>\n\n<ParamField body=\"token_verifier\" type=\"TokenVerifier\" required>\n  A [`TokenVerifier`](/v2/servers/auth/token-verification) instance to validate the\n  provider's tokens\n</ParamField>\n\n<ParamField body=\"base_url\" type=\"AnyHttpUrl | str\" required>\n  Public URL where OAuth endpoints will be accessible, **including any mount path** (e.g., `https://your-server.com/api`).\n\n  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).\n</ParamField>\n\n<ParamField body=\"redirect_path\" type=\"str\" default=\"/auth/callback\">\n  Path for OAuth callbacks. Must match the redirect URI configured in your OAuth\n  application\n</ParamField>\n\n<ParamField body=\"upstream_revocation_endpoint\" type=\"str | None\">\n  Optional URL of provider's token revocation endpoint\n</ParamField>\n\n<ParamField body=\"issuer_url\" type=\"AnyHttpUrl | str | None\">\n  Issuer URL for OAuth authorization server metadata (defaults to `base_url`).\n\n  When `issuer_url` has a path component (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.\n\n  **Default behavior (recommended for most cases):**\n  ```python\n  auth = GitHubProvider(\n      base_url=\"http://localhost:8000/api\",  # OAuth endpoints under /api\n      # issuer_url defaults to base_url - path-aware discovery works automatically\n  )\n  ```\n\n  **When to set explicitly:**\n  Set `issuer_url` to root level only if you want multiple MCP servers to share a single discovery endpoint:\n  ```python\n  auth = GitHubProvider(\n      base_url=\"http://localhost:8000/api\",\n      issuer_url=\"http://localhost:8000\"  # Shared root-level discovery\n  )\n  ```\n\n  See the [HTTP Deployment guide](/v2/deployment/http#mounting-authenticated-servers) for complete mounting examples.\n</ParamField>\n\n<ParamField body=\"service_documentation_url\" type=\"AnyHttpUrl | str | None\">\n  Optional URL to your service documentation\n</ParamField>\n\n<ParamField body=\"forward_pkce\" type=\"bool\" default=\"True\">\n  Whether to forward PKCE (Proof Key for Code Exchange) to the upstream OAuth\n  provider. When enabled and the client uses PKCE, the proxy generates its own\n  PKCE parameters to send upstream while separately validating the client's\n  PKCE. This ensures end-to-end PKCE security at both layers (client-to-proxy\n  and proxy-to-upstream). - `True` (default): Forward PKCE for providers that\n  support it (Google, Azure, AWS, GitHub, etc.) - `False`: Disable only if upstream\n  provider doesn't support PKCE\n</ParamField>\n\n<ParamField body=\"token_endpoint_auth_method\" type=\"str | None\">\n  Token endpoint authentication method for the upstream OAuth server. Controls\n  how the proxy authenticates when exchanging authorization codes and refresh\n  tokens with the upstream provider. - `\"client_secret_basic\"`: Send credentials\n  in Authorization header (most common) - `\"client_secret_post\"`: Send\n  credentials in request body (required by some providers) - `\"none\"`: No\n  authentication (for public clients) - `None` (default): Uses authlib's default\n  (typically `\"client_secret_basic\"`) Set this if your provider requires a\n  specific authentication method and the default doesn't work.\n</ParamField>\n\n<ParamField body=\"allowed_client_redirect_uris\" type=\"list[str] | None\">\n  List of allowed redirect URI patterns for MCP clients. Patterns support\n  wildcards (e.g., `\"http://localhost:*\"`, `\"https://*.example.com/*\"`). -\n  `None` (default): All redirect URIs allowed (for MCP/DCR compatibility) -\n  Empty list `[]`: No redirect URIs allowed - Custom list: Only matching\n  patterns allowed These patterns apply to MCP client loopback redirects, NOT\n  the upstream OAuth app redirect URI.\n</ParamField>\n\n<ParamField body=\"valid_scopes\" type=\"list[str] | None\">\n  List of all possible valid scopes for the OAuth provider. These are advertised\n  to clients through the `/.well-known` endpoints. Defaults to `required_scopes`\n  from your TokenVerifier if not specified.\n</ParamField>\n\n<ParamField body=\"extra_authorize_params\" type=\"dict[str, str] | None\">\n  Additional parameters to forward to the upstream authorization endpoint. Useful for provider-specific parameters that aren't part of the standard OAuth2 flow.\n  \n  For example, Auth0 requires an `audience` parameter to issue JWT tokens:\n  ```python\n  extra_authorize_params={\"audience\": \"https://api.example.com\"}\n  ```\n  \n  These parameters are added to every authorization request sent to the upstream provider.\n</ParamField>\n\n<ParamField body=\"extra_token_params\" type=\"dict[str, str] | None\">\n  Additional parameters to forward to the upstream token endpoint during code exchange and token refresh. Useful for provider-specific requirements during token operations.\n\nFor example, some providers require additional context during token exchange:\n\n```python\nextra_token_params={\"audience\": \"https://api.example.com\"}\n```\n\nThese parameters are included in all token requests to the upstream provider.\n\n</ParamField>\n\n<ParamField body=\"client_storage\" type=\"AsyncKeyValue | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Storage backend for persisting OAuth client registrations and upstream tokens.\n\n  **Default behavior:**\n  By default, clients are automatically persisted to an encrypted disk store, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. The disk store is encrypted using a key derived from the JWT Signing Key (which is derived from the upstream client secret by default). For client registrations to survive upstream client secret rotation, you should provide a JWT Signing Key or your own client_storage.\n\nFor production deployments with multiple servers or cloud deployments, see [Storage Backends](/v2/servers/storage-backends) for available options.\n\n<Warning>\n  **When providing custom storage**, wrap it in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest:\n\n  ```python\n  from key_value.aio.stores.redis import RedisStore\n  from key_value.aio.wrappers.encryption import FernetEncryptionWrapper\n  from cryptography.fernet import Fernet\n  import os\n\n  auth = OAuthProxy(\n      ...,\n      jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n      client_storage=FernetEncryptionWrapper(\n          key_value=RedisStore(host=\"redis.example.com\", port=6379),\n          fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n      )\n  )\n  ```\n\n  Without encryption, upstream OAuth tokens are stored in plaintext.\n</Warning>\n\nTesting with in-memory storage (unencrypted):\n\n```python\nfrom key_value.aio.stores.memory import MemoryStore\n\n# Use in-memory storage for testing (clients lost on restart)\nauth = OAuthProxy(..., client_storage=MemoryStore())\n```\n\n</ParamField>\n\n<ParamField body=\"jwt_signing_key\" type=\"str | bytes | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.\n\n  **Default behavior (`None`):**\n  Derives a 32-byte key using PBKDF2 from the upstream client secret.\n\n  **For production:**\n  Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the key derived from the upstream client secret. This allows you to manage keys securely in cloud environments, allows keys to work across multiple instances, and allows you to rotate keys without losing client registrations.\n\n  ```python\n  import os\n\n  auth = OAuthProxy(\n      ...,\n      jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],  # Any sufficiently complex string!\n      client_storage=RedisStore(...)  # Persistent storage\n  )\n  ```\n\n  See [HTTP Deployment - OAuth Token Security](/v2/deployment/http#oauth-token-security) for complete production setup.\n</ParamField>\n\n\n<ParamField body=\"require_authorization_consent\" type=\"bool\" default=\"True\">\n  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.\n\n  **Default behavior (True):**\n  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.\n\n  **Disabling consent (False):**\n  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.\n\n  ```python\n  # Development/testing only - skip consent screen\n  auth = OAuthProxy(\n      ...,\n      require_authorization_consent=False  # ⚠️ Security warning: only for local/testing\n  )\n  ```\n\n  <Warning>\n    Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.\n  </Warning>\n</ParamField>\n\n<ParamField body=\"consent_csp_policy\" type=\"str | None\" default=\"None\">\n  Content Security Policy for the consent page.\n\n  - `None` (default): Uses the built-in CSP policy with appropriate directives for form submission\n  - Empty string `\"\"`: Disables CSP entirely (no meta tag rendered)\n  - Custom string: Uses the provided value as the CSP policy\n\n  This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives.\n\n  ```python\n  # Disable CSP entirely (let org CSP policies apply)\n  auth = OAuthProxy(..., consent_csp_policy=\"\")\n\n  # Use custom CSP policy\n  auth = OAuthProxy(..., consent_csp_policy=\"default-src 'self'; style-src 'unsafe-inline'\")\n  ```\n</ParamField>\n</Card>\n\n### Using Built-in Providers\n\nFastMCP includes pre-configured providers for common services:\n\n```python\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=\"your-github-app-id\",\n    client_secret=\"your-github-app-secret\",\n    base_url=\"https://your-server.com\"\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\nAvailable providers include `GitHubProvider`, `GoogleProvider`, and others. These handle token verification automatically.\n\n### Token Verification\n\nThe OAuth proxy requires a compatible `TokenVerifier` to validate tokens from your provider. Different providers use different token formats:\n\n- **JWT tokens** (Google, Azure): Use `JWTVerifier` with the provider's JWKS endpoint\n- **Opaque tokens with RFC 7662 introspection** (Auth0, Okta, WorkOS): Use `IntrospectionTokenVerifier`\n- **Opaque tokens (provider-specific)** (GitHub, Discord): Use provider-specific verifiers like `GitHubTokenVerifier`\n\nSee the [Token Verification guide](/v2/servers/auth/token-verification) for detailed setup instructions for your provider.\n\n### Scope Configuration\n\nOAuth scopes control what permissions your application requests from users. They're configured through your `TokenVerifier` (required for the OAuth proxy to validate tokens from your provider). Set `required_scopes` to automatically request the permissions your application needs:\n\n```python\nJWTVerifier(..., required_scopes = [\"read:user\", \"write:data\"])\n```\n\nDynamic clients created by the proxy will automatically include these scopes in their authorization requests. See the [Token Verification](#token-verification) section below for detailed setup.\n\n### Custom Parameters\n\nSome OAuth providers require additional parameters beyond the standard OAuth2 flow. Use `extra_authorize_params` and `extra_token_params` to pass provider-specific requirements. For example, Auth0 requires an `audience` parameter to issue JWT tokens instead of opaque tokens:\n\n```python\nauth = OAuthProxy(\n    upstream_authorization_endpoint=\"https://your-domain.auth0.com/authorize\",\n    upstream_token_endpoint=\"https://your-domain.auth0.com/oauth/token\",\n    upstream_client_id=\"your-auth0-client-id\",\n    upstream_client_secret=\"your-auth0-client-secret\",\n\n    # Auth0-specific audience parameter\n    extra_authorize_params={\"audience\": \"https://your-api-identifier.com\"},\n    extra_token_params={\"audience\": \"https://your-api-identifier.com\"},\n\n    token_verifier=JWTVerifier(\n        jwks_uri=\"https://your-domain.auth0.com/.well-known/jwks.json\",\n        issuer=\"https://your-domain.auth0.com/\",\n        audience=\"https://your-api-identifier.com\"\n    ),\n    base_url=\"https://your-server.com\"\n)\n```\n\nThe proxy also automatically forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them.\n\n## OAuth Flow\n\n```mermaid\nsequenceDiagram\n    participant Client as MCP Client<br/>(localhost:random)\n    participant User as User\n    participant Proxy as FastMCP OAuth Proxy<br/>(server:8000)\n    participant Provider as OAuth Provider<br/>(GitHub, etc.)\n\n    Note over Client, Proxy: Dynamic Registration (Local)\n    Client->>Proxy: 1. POST /register<br/>redirect_uri: localhost:54321/callback\n    Proxy-->>Client: 2. Returns fixed upstream credentials\n\n    Note over Client, User: Authorization with User Consent\n    Client->>Proxy: 3. GET /authorize<br/>redirect_uri=localhost:54321/callback<br/>code_challenge=CLIENT_CHALLENGE\n    Note over Proxy: Store transaction with client PKCE<br/>Generate proxy PKCE pair\n    Proxy->>User: 4. Show consent page<br/>(client details, redirect URI, scopes)\n    User->>Proxy: 5. Approve/deny consent\n    Proxy->>Provider: 6. Redirect to provider<br/>redirect_uri=server:8000/auth/callback<br/>code_challenge=PROXY_CHALLENGE\n\n    Note over Provider, Proxy: Provider Callback\n    Provider->>Proxy: 7. GET /auth/callback<br/>with authorization code\n    Proxy->>Provider: 8. Exchange code for tokens<br/>code_verifier=PROXY_VERIFIER\n    Provider-->>Proxy: 9. Access & refresh tokens\n\n    Note over Proxy, Client: Client Callback Forwarding\n    Proxy->>Client: 10. Redirect to localhost:54321/callback<br/>with new authorization code\n\n    Note over Client, Proxy: Token Exchange\n    Client->>Proxy: 11. POST /token with code<br/>code_verifier=CLIENT_VERIFIER\n    Proxy-->>Client: 12. Returns stored provider tokens\n```\n\nThe flow diagram above illustrates the complete OAuth proxy pattern. Let's understand each phase:\n\n### Registration Phase\n\nWhen an MCP client calls `/register` with its dynamic callback URL, the proxy responds with your pre-configured upstream credentials. The client stores these credentials believing it has registered a new app. Meanwhile, the proxy records the client's callback URL for later use.\n\n### Authorization Phase\n\nThe client initiates OAuth by redirecting to the proxy's `/authorize` endpoint. The proxy:\n\n1. Stores the client's transaction with its PKCE challenge\n2. Generates its own PKCE parameters for upstream security\n3. Shows the user a consent page with the client's details, redirect URI, and requested scopes\n4. If the user approves (or the client was previously approved), redirects to the upstream provider using the fixed callback URL\n\nThis dual-PKCE approach maintains end-to-end security at both the client-to-proxy and proxy-to-provider layers. The consent step protects against confused deputy attacks by ensuring you explicitly approve each client before it can complete authorization.\n\n### Callback Phase\n\nAfter user authorization, the provider redirects back to the proxy's fixed callback URL. The proxy:\n\n1. Exchanges the authorization code for tokens with the provider\n2. Stores these tokens temporarily\n3. Generates a new authorization code for the client\n4. Redirects to the client's original dynamic callback URL\n\n### Token Exchange Phase\n\nFinally, the client exchanges its authorization code with the proxy to receive the provider's tokens. The proxy validates the client's PKCE verifier before returning the stored tokens.\n\nThis entire flow is transparent to the MCP client—it experiences a standard OAuth flow with dynamic registration, unaware that a proxy is managing the complexity behind the scenes.\n\n### Token Architecture\n\nThe OAuth proxy implements a **token factory pattern**: instead of directly forwarding tokens from the upstream OAuth provider, it issues its own JWT tokens to MCP clients. This maintains proper OAuth 2.0 token audience boundaries and enables better security controls.\n\n**How it works:**\n\nWhen an MCP client completes authorization, the proxy:\n\n1. **Receives upstream tokens** from the OAuth provider (GitHub, Google, etc.)\n2. **Encrypts and stores** these tokens using Fernet encryption (AES-128-CBC + HMAC-SHA256)\n3. **Issues FastMCP JWT tokens** to the client, signed with HS256\n\nThe FastMCP JWT contains minimal claims: issuer, audience, client ID, scopes, expiration, and a unique token identifier (JTI). The JTI acts as a reference linking to the encrypted upstream token.\n\n**Token validation:**\n\nWhen a client makes an MCP request with its FastMCP token:\n\n1. **FastMCP validates the JWT** signature, expiration, issuer, and audience\n2. **Looks up the upstream token** using the JTI from the validated JWT\n3. **Decrypts and validates** the upstream token with the provider\n\nThis two-tier validation ensures that FastMCP tokens can only be used with this server (via audience validation) while maintaining full upstream token security.\n\n**Token expiry alignment:**\n\nFastMCP token lifetimes match the upstream token lifetimes. When the upstream token expires, the FastMCP token also expires, maintaining consistent security boundaries.\n\n**Refresh tokens:**\n\nThe proxy issues its own refresh tokens that map to upstream refresh tokens. When a client uses a FastMCP refresh token, the proxy refreshes the upstream token and issues a new FastMCP access token.\n\n### PKCE Forwarding\n\nThe OAuth proxy automatically handles PKCE (Proof Key for Code Exchange) when working with providers that support or require it. The proxy generates its own PKCE parameters to send upstream while separately validating the client's PKCE, ensuring end-to-end security at both layers.\n\nThis is enabled by default via the `forward_pkce` parameter and works seamlessly with providers like Google, Azure AD, and GitHub. Only disable it for legacy providers that don't support PKCE:\n\n```python\n# Disable PKCE forwarding only if upstream doesn't support it\nauth = OAuthProxy(\n    ...,\n    forward_pkce=False  # Default is True\n)\n```\n\n### Redirect URI Validation\n\nWhile the OAuth proxy accepts all redirect URIs by default (for DCR compatibility), you can restrict which clients can connect by specifying allowed patterns:\n\n```python\n# Allow only localhost clients (common for development)\nauth = OAuthProxy(\n    # ... other parameters ...\n    allowed_client_redirect_uris=[\n        \"http://localhost:*\",\n        \"http://127.0.0.1:*\"\n    ]\n)\n\n# Allow specific known clients\nauth = OAuthProxy(\n    # ... other parameters ...\n    allowed_client_redirect_uris=[\n        \"http://localhost:*\",\n        \"https://claude.ai/api/mcp/auth_callback\",\n        \"https://*.mycompany.com/auth/*\"  # Wildcard patterns supported\n    ]\n)\n```\n\nCheck your server logs for \"Client registered with redirect_uri\" messages to identify what URLs your clients use.\n\n## Security\n\n### Key and Storage Management\n\n<VersionBadge version=\"2.13.0\" />\nThe OAuth proxy requires cryptographic keys for JWT signing and storage encryption, plus persistent storage to maintain valid tokens across server restarts.\n\n**Default behavior (appropriate for development only):**\n- **Mac/Windows**: FastMCP automatically generates keys and stores them in your system keyring. Storage defaults to disk. Tokens survive server restarts. This is **only** suitable for development and local testing.\n- **Linux**: Keys are ephemeral (random salt at startup). Storage defaults to memory. Tokens become invalid on server restart.\n\n**For production:**\nConfigure the following parameters together: provide a unique `jwt_signing_key` (for signing FastMCP JWTs), and a shared `client_storage` backend (for storing tokens). Both are required for production deployments. Use a network-accessible storage backend like Redis or DynamoDB rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** (see the `client_storage` parameter documentation above for examples). The keys accept any secret string and derive proper cryptographic keys using HKDF. See [OAuth Token Security](/v2/deployment/http#oauth-token-security) and [Storage Backends](/v2/servers/storage-backends) for complete production setup.\n\n### Confused Deputy Attacks\n\n<VersionBadge version=\"2.13.0\" />\n\nA confused deputy attack allows a malicious client to steal your authorization by tricking you into granting it access under your identity.\n\nThe OAuth proxy works by bridging DCR clients to traditional auth providers, which means that multiple MCP clients connect through a single upstream OAuth application. An attacker can exploit this shared application by registering a malicious client with their own redirect URI, then sending you an authorization link. When you click it, your browser goes through the OAuth flow—but since you may have already authorized this OAuth app before, the provider might auto-approve the request. The authorization code then gets sent to the attacker's redirect URI instead of a legitimate client, giving them access under your credentials.\n\n#### Mitigation\n\nFastMCP'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.\n\n![](/assets/images/oauth-proxy-consent-screen.png)\n\nThe 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.\n\n\n\n**Learn more:**\n- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance\n- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky\n\n## Environment Configuration\n\n<VersionBadge version=\"2.12.1\" />\n\nFor production deployments, configure the OAuth proxy through environment variables instead of hardcoding credentials:\n\n```bash\n# Specify the provider implementation\nexport FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider\n\n# Provider-specific credentials\nexport FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=\"Ov23li...\"\nexport FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET=\"abc123...\"\nexport FASTMCP_SERVER_AUTH_GITHUB_BASE_URL=\"https://your-production-server.com\"\n```\n\nWith environment configuration, your server code simplifies to:\n\n```python\nfrom fastmcp import FastMCP\n\n# Authentication automatically configured from environment\nmcp = FastMCP(name=\"My Server\")\n\n@mcp.tool\ndef protected_tool(data: str) -> str:\n    \"\"\"This tool is now protected by OAuth.\"\"\"\n    return f\"Processed: {data}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n"
  },
  {
    "path": "docs/v2/servers/auth/oidc-proxy.mdx",
    "content": "---\ntitle: OIDC Proxy\nsidebarTitle: OIDC Proxy\ndescription: Bridge OIDC providers to work seamlessly with MCP's authentication flow.\nicon: share\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.12.4\" />\n\nThe OIDC proxy enables FastMCP servers to authenticate with OIDC providers that **don't support Dynamic Client Registration (DCR)** out of the box. This includes OAuth providers like: Auth0, Google, Azure, AWS, etc. For providers that do support DCR (like WorkOS AuthKit), use [`RemoteAuthProvider`](/v2/servers/auth/remote-oauth) instead.\n\nThe OIDC proxy is built upon [`OAuthProxy`](/v2/servers/auth/oauth-proxy) so it has all the same functionality under the covers.\n\n## Implementation\n\n### Provider Setup Requirements\n\nBefore using the OIDC proxy, you need to register your application with your OAuth provider:\n\n1. **Register your application** in the provider's developer console (Auth0 Applications, Google Cloud Console, Azure Portal, etc.)\n2. **Configure the redirect URI** as your FastMCP server URL plus your chosen callback path:\n   - Default: `https://your-server.com/auth/callback`\n   - Custom: `https://your-server.com/your/custom/path` (if you set `redirect_path`)\n   - Development: `http://localhost:8000/auth/callback`\n3. **Obtain your credentials**: Client ID and Client Secret\n\n<Warning>\n  The redirect URI you configure with your provider must exactly match your\n  FastMCP server's URL plus the callback path. If you customize `redirect_path`\n  in the OIDC proxy, update your provider's redirect URI accordingly.\n</Warning>\n\n### Basic Setup\n\nHere's how to implement the OIDC proxy with any provider:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.oidc_proxy import OIDCProxy\n\n# Create the OIDC proxy\nauth = OIDCProxy(\n    # Provider's configuration URL\n    config_url=\"https://provider.com/.well-known/openid-configuration\",\n\n    # Your registered app credentials\n    client_id=\"your-client-id\",\n    client_secret=\"your-client-secret\",\n\n    # Your FastMCP server's public URL\n    base_url=\"https://your-server.com\",\n\n    # Optional: customize the callback path (default is \"/auth/callback\")\n    # redirect_path=\"/custom/callback\",\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\n### Configuration Parameters\n\n<Card icon=\"code\" title=\"OIDCProxy Parameters\">\n<ParamField body=\"config_url\" type=\"str\" required>\n  URL of your OAuth provider's OIDC configuration\n</ParamField>\n\n<ParamField body=\"client_id\" type=\"str\" required>\n  Client ID from your registered OAuth application\n</ParamField>\n\n<ParamField body=\"client_secret\" type=\"str\" required>\n  Client secret from your registered OAuth application\n</ParamField>\n\n<ParamField body=\"base_url\" type=\"AnyHttpUrl | str\" required>\n  Public URL of your FastMCP server (e.g., `https://your-server.com`)\n</ParamField>\n\n<ParamField body=\"strict\" type=\"bool | None\">\n  Strict flag for configuration validation. When True, requires all OIDC\n  mandatory fields.\n</ParamField>\n\n<ParamField body=\"audience\" type=\"str | None\">\n  Audience parameter for OIDC providers that require it (e.g., Auth0). This is\n  typically your API identifier.\n</ParamField>\n\n<ParamField body=\"timeout_seconds\" type=\"int | None\" default=\"10\">\n  HTTP request timeout in seconds for fetching OIDC configuration\n</ParamField>\n\n<ParamField body=\"token_verifier\" type=\"TokenVerifier | None\">\n\n<VersionBadge version=\"2.13.1\" />\n  Custom token verifier for validating tokens. When provided, FastMCP uses your custom verifier instead of creating a default `JWTVerifier`.\n\n  Cannot be used with `algorithm` or `required_scopes` parameters - configure these on your verifier instead. The verifier's `required_scopes` are automatically loaded and advertised.\n</ParamField>\n\n<ParamField body=\"algorithm\" type=\"str | None\">\n  JWT algorithm to use for token verification (e.g., \"RS256\"). If not specified,\n  uses the provider's default. Only used when `token_verifier` is not provided.\n</ParamField>\n\n<ParamField body=\"required_scopes\" type=\"list[str] | None\">\n  List of OAuth scopes for token validation. These are automatically\n  included in authorization requests. Only used when `token_verifier` is not provided.\n</ParamField>\n\n<ParamField body=\"redirect_path\" type=\"str\" default=\"/auth/callback\">\n  Path for OAuth callbacks. Must match the redirect URI configured in your OAuth\n  application\n</ParamField>\n\n<ParamField body=\"allowed_client_redirect_uris\" type=\"list[str] | None\">\n  List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., `\"http://localhost:*\"`, `\"https://*.example.com/*\"`).\n  - `None` (default): All redirect URIs allowed (for MCP/DCR compatibility)\n  - Empty list `[]`: No redirect URIs allowed\n  - Custom list: Only matching patterns allowed\n\nThese patterns apply to MCP client loopback redirects, NOT the upstream OAuth app redirect URI.\n\n</ParamField>\n\n<ParamField body=\"token_endpoint_auth_method\" type=\"str | None\">\n  Token endpoint authentication method for the upstream OAuth server. Controls how the proxy authenticates when exchanging authorization codes and refresh tokens with the upstream provider.\n  - `\"client_secret_basic\"`: Send credentials in Authorization header (most common)\n  - `\"client_secret_post\"`: Send credentials in request body (required by some providers)\n  - `\"none\"`: No authentication (for public clients)\n  - `None` (default): Uses authlib's default (typically `\"client_secret_basic\"`)\n\nSet this if your provider requires a specific authentication method and the default doesn't work.\n\n</ParamField>\n\n<ParamField body=\"jwt_signing_key\" type=\"str | bytes | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.\n\n  **Default behavior (`None`):**\n  - **Mac/Windows**: Auto-managed via system keyring. Keys are generated once and persisted, surviving server restarts with zero configuration. Keys are automatically derived from server attributes, so this approach, while convenient, is **only** suitable for development and local testing. For production, you must provide an explicit secret.\n  - **Linux**: Ephemeral (random salt at startup). Tokens become invalid on server restart, triggering client re-authentication.\n\n  **For production:**\n  Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the auto-generated one.\n</ParamField>\n\n<ParamField body=\"client_storage\" type=\"AsyncKeyValue | None\">\n\n<VersionBadge version=\"2.13.0\" />\n  Storage backend for persisting OAuth client registrations and upstream tokens.\n\n  **Default behavior:**\n  - **Mac/Windows**: Encrypted DiskStore in your platform's data directory (derived from `platformdirs`)\n  - **Linux**: MemoryStore (ephemeral - clients lost on restart)\n\n  By default on Mac/Windows, clients are automatically persisted to encrypted disk storage, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. On Linux where keyring isn't available, ephemeral storage is used to match the ephemeral key strategy.\n\nFor production deployments with multiple servers or cloud deployments, use a network-accessible storage backend rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.** See [Storage Backends](/v2/servers/storage-backends) for available options.\n\nTesting with in-memory storage (unencrypted):\n\n```python\nfrom key_value.aio.stores.memory import MemoryStore\n\n# Use in-memory storage for testing (clients lost on restart)\nauth = OIDCProxy(..., client_storage=MemoryStore())\n```\n\nProduction with encrypted Redis storage:\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\nimport os\n\nauth = OIDCProxy(\n    ...,\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(host=\"redis.example.com\", port=6379),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n```\n\n</ParamField>\n\n<ParamField body=\"require_authorization_consent\" type=\"bool\" default=\"True\">\n  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.\n</ParamField>\n\n<ParamField body=\"consent_csp_policy\" type=\"str | None\" default=\"None\">\n  Content Security Policy for the consent page.\n\n  - `None` (default): Uses the built-in CSP policy with appropriate directives for form submission\n  - Empty string `\"\"`: Disables CSP entirely (no meta tag rendered)\n  - Custom string: Uses the provided value as the CSP policy\n\n  This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives.\n</ParamField>\n</Card>\n\n### Using Built-in Providers\n\nFastMCP includes pre-configured OIDC providers for common services:\n\n```python\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\n\nauth = Auth0Provider(\n    config_url=\"https://.../.well-known/openid-configuration\",\n    client_id=\"your-auth0-client-id\",\n    client_secret=\"your-auth0-client-secret\",\n    audience=\"https://...\",\n    base_url=\"https://localhost:8000\"\n)\n\nmcp = FastMCP(name=\"My Server\", auth=auth)\n```\n\nAvailable providers include `Auth0Provider` at present.\n\n### Scope Configuration\n\nOAuth scopes are configured with `required_scopes` to automatically request the permissions your application needs.\n\nDynamic clients created by the proxy will automatically include these scopes in their authorization requests.\n\n## Environment Configuration\n\n<VersionBadge version=\"2.13.0\" />\n\nFor production deployments, configure the OIDC proxy through environment variables instead of hardcoding credentials:\n\n```bash\n# Specify the provider implementation\nexport FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider\n\n# Provider-specific credentials\nexport FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration\nexport FASTMCP_SERVER_AUTH_AUTH0_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB\nexport FASTMCP_SERVER_AUTH_AUTH0_CLIENT_SECRET=vPYqbjemq...\nexport FASTMCP_SERVER_AUTH_AUTH0_AUDIENCE=https://...\nexport FASTMCP_SERVER_AUTH_AUTH0_BASE_URL=https://localhost:8000\n```\n\nWith environment configuration, your server code simplifies to:\n\n```python\nfrom fastmcp import FastMCP\n\n# Authentication automatically configured from environment\nmcp = FastMCP(name=\"My Server\")\n\n@mcp.tool\ndef protected_tool(data: str) -> str:\n    \"\"\"This tool is now protected by OAuth.\"\"\"\n    return f\"Processed: {data}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n"
  },
  {
    "path": "docs/v2/servers/auth/remote-oauth.mdx",
    "content": "---\ntitle: Remote OAuth\nsidebarTitle: Remote OAuth\ndescription: Integrate your FastMCP server with external identity providers like Descope, WorkOS, Auth0, and corporate SSO systems.\nicon: camera-cctv\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nRemote OAuth integration allows your FastMCP server to leverage external identity providers that **support Dynamic Client Registration (DCR)**. With DCR, MCP clients can automatically register themselves with the identity provider and obtain credentials without any manual configuration. This provides enterprise-grade authentication with fully automated flows, making it ideal for production applications with modern identity providers.\n\n<Tip>\n**When to use RemoteAuthProvider vs OAuth Proxy:**\n- **RemoteAuthProvider**: For providers WITH Dynamic Client Registration (Descope, WorkOS AuthKit, modern OIDC providers)\n- **OAuth Proxy**: For providers WITHOUT Dynamic Client Registration (GitHub, Google, Azure, AWS, Discord, etc.)\n\nRemoteAuthProvider requires DCR support for fully automated client registration and authentication.\n</Tip>\n\n## DCR-Enabled Providers\n\nRemoteAuthProvider works with identity providers that support **Dynamic Client Registration (DCR)** - a critical capability that enables automated authentication flows:\n\n| Feature | DCR Providers (RemoteAuth) | Non-DCR Providers (OAuth Proxy) |\n|---------|---------------------------|--------------------------------|\n| **Client Registration** | Automatic via API | Manual in provider console |\n| **Credentials** | Dynamic per client | Fixed app credentials |\n| **Configuration** | Zero client config | Pre-shared credentials |\n| **Examples** | Descope, WorkOS AuthKit, modern OIDC | GitHub, Google, Azure |\n| **FastMCP Class** | `RemoteAuthProvider` | [`OAuthProxy`](/v2/servers/auth/oauth-proxy) |\n\nIf your provider doesn't support DCR (most traditional OAuth providers), you'll need to use [`OAuth Proxy`](/v2/servers/auth/oauth-proxy) instead, which bridges the gap between MCP's DCR expectations and fixed OAuth credentials.\n\n## The Remote OAuth Challenge\n\nTraditional OAuth flows assume human users with web browsers who can interact with login forms, consent screens, and redirects. MCP clients operate differently - they're often automated systems that need to authenticate programmatically without human intervention.\n\nThis creates several unique requirements that standard OAuth implementations don't address well:\n\n**Automatic Discovery**: MCP clients must discover authentication requirements by examining server metadata rather than encountering HTTP redirects. They need to know which identity provider to use and how to reach it before making any authenticated requests.\n\n**Programmatic Registration**: Clients need to register themselves with identity providers automatically. Manual client registration doesn't work when clients might be dynamically created tools or services.\n\n**Seamless Token Management**: Clients must obtain, store, and refresh tokens without user interaction. The authentication flow needs to work in headless environments where no human is available to complete OAuth consent flows.\n\n**Protocol Integration**: The authentication process must integrate cleanly with MCP's JSON-RPC transport layer and error handling mechanisms.\n\nThese requirements mean that your MCP server needs to do more than just validate tokens - it needs to provide discovery metadata that enables MCP clients to understand and navigate your authentication requirements automatically.\n\n## MCP Authentication Discovery\n\nMCP authentication discovery relies on well-known endpoints that clients can examine to understand your authentication requirements. Your server becomes a bridge between MCP clients and your chosen identity provider.\n\nThe core discovery endpoint is `/.well-known/oauth-protected-resource`, which tells clients that your server requires OAuth authentication and identifies the authorization servers you trust. This endpoint contains static metadata that points clients to your identity provider without requiring any dynamic lookups.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant FastMCPServer as FastMCP Server\n    participant ExternalIdP as Identity Provider\n\n    Client->>FastMCPServer: 1. GET /.well-known/oauth-protected-resource\n    FastMCPServer-->>Client: 2. \"Use https://my-idp.com for auth\"\n    \n    note over Client, ExternalIdP: Client goes directly to the IdP\n    Client->>ExternalIdP: 3. Authenticate & get token via DCR\n    ExternalIdP-->>Client: 4. Access token\n    \n    Client->>FastMCPServer: 5. MCP request with Bearer token\n    FastMCPServer->>FastMCPServer: 6. Verify token signature\n    FastMCPServer-->>Client: 7. MCP response\n```\n\nThis flow separates concerns cleanly: your MCP server handles resource protection and token validation, while your identity provider handles user authentication and token issuance. The client coordinates between these systems using standardized OAuth discovery mechanisms.\n\n## FastMCP Remote Authentication\n\n<VersionBadge version=\"2.11.1\" />\n\nFastMCP provides `RemoteAuthProvider` to handle the complexities of remote OAuth integration. This class combines token validation capabilities with the OAuth discovery metadata that MCP clients require.\n\n### RemoteAuthProvider\n\n`RemoteAuthProvider` works by composing a [`TokenVerifier`](/v2/servers/auth/token-verification) with authorization server information. A `TokenVerifier` is another FastMCP authentication class that focuses solely on token validation - signature verification, expiration checking, and claim extraction. The `RemoteAuthProvider` takes that token validation capability and adds the OAuth discovery endpoints that enable MCP clients to automatically find and authenticate with your identity provider.\n\nThis composition pattern means you can use any token validation strategy while maintaining consistent OAuth discovery behavior:\n- **JWT tokens**: Use `JWTVerifier` for self-contained tokens\n- **Opaque tokens**: Use `IntrospectionTokenVerifier` for RFC 7662 introspection\n- **Custom validation**: Implement your own `TokenVerifier` subclass\n\nThe separation allows you to change token validation approaches without affecting the client discovery experience.\n\nThe class automatically generates the required OAuth metadata endpoints using the MCP SDK's standardized route creation functions. This ensures compatibility with MCP clients while reducing the implementation complexity for server developers.\n\n### Basic Implementation\n\nMost applications can use `RemoteAuthProvider` directly without subclassing. The implementation requires a `TokenVerifier` instance, a list of trusted authorization servers, and your server's URL for metadata generation.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import RemoteAuthProvider\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom pydantic import AnyHttpUrl\n\n# Configure token validation for your identity provider\ntoken_verifier = JWTVerifier(\n    jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n    issuer=\"https://auth.yourcompany.com\",\n    audience=\"mcp-production-api\"\n)\n\n# Create the remote auth provider\nauth = RemoteAuthProvider(\n    token_verifier=token_verifier,\n    authorization_servers=[AnyHttpUrl(\"https://auth.yourcompany.com\")],\n    base_url=\"https://api.yourcompany.com\",  # Your server base URL\n    # Optional: customize allowed client redirect URIs (defaults to localhost only)\n    allowed_client_redirect_uris=[\"http://localhost:*\", \"http://127.0.0.1:*\"]\n)\n\nmcp = FastMCP(name=\"Company API\", auth=auth)\n```\n\nThis configuration creates a server that accepts tokens issued by `auth.yourcompany.com` and provides the OAuth discovery metadata that MCP clients need. The `JWTVerifier` handles token validation using your identity provider's public keys, while the `RemoteAuthProvider` generates the required OAuth endpoints.\n\nThe `authorization_servers` list tells MCP clients which identity providers you trust. The `base_url` identifies your server in OAuth metadata, enabling proper token audience validation. **Important**: The `base_url` should point to your server base URL - for example, if your MCP server is accessible at `https://api.yourcompany.com/mcp`, use `https://api.yourcompany.com` as the base URL.\n\n### Custom Endpoints\n\nYou can extend `RemoteAuthProvider` to add additional endpoints beyond the standard OAuth protected resource metadata. These don't have to be OAuth-specific - you can add any endpoints your authentication integration requires.\n\n```python\nimport httpx\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nclass CompanyAuthProvider(RemoteAuthProvider):\n    def __init__(self):\n        token_verifier = JWTVerifier(\n            jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n            issuer=\"https://auth.yourcompany.com\",\n            audience=\"mcp-production-api\"\n        )\n        \n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.yourcompany.com\")],\n            base_url=\"https://api.yourcompany.com\"  # Your server base URL\n        )\n    \n    def get_routes(self) -> list[Route]:\n        \"\"\"Add custom endpoints to the standard protected resource routes.\"\"\"\n        \n        # Get the standard OAuth protected resource routes\n        routes = super().get_routes()\n        \n        # Add authorization server metadata forwarding for client convenience\n        async def authorization_server_metadata(request):\n            async with httpx.AsyncClient() as client:\n                response = await client.get(\n                    \"https://auth.yourcompany.com/.well-known/oauth-authorization-server\"\n                )\n                response.raise_for_status()\n                return JSONResponse(response.json())\n        \n        routes.append(\n            Route(\"/.well-known/oauth-authorization-server\", authorization_server_metadata)\n        )\n        \n        return routes\n\nmcp = FastMCP(name=\"Company API\", auth=CompanyAuthProvider())\n```\n\nThis pattern uses `super().get_routes()` to get the standard protected resource routes, then adds additional endpoints as needed. A common use case is providing authorization server metadata forwarding, which allows MCP clients to discover your identity provider's capabilities through your MCP server rather than contacting the identity provider directly.\n\n## WorkOS AuthKit Integration\n\nWorkOS AuthKit provides an excellent example of remote OAuth integration. The `AuthKitProvider` demonstrates how to implement both token validation and OAuth metadata forwarding in a production-ready package.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\nauth = AuthKitProvider(\n    authkit_domain=\"https://your-project.authkit.app\",\n    base_url=\"https://your-mcp-server.com\"\n)\n\nmcp = FastMCP(name=\"Protected Application\", auth=auth)\n```\n\nThe `AuthKitProvider` automatically configures JWT validation against WorkOS's public keys and provides both protected resource metadata and authorization server metadata forwarding. This implementation handles the complete remote OAuth integration with minimal configuration.\n\nWorkOS's support for Dynamic Client Registration makes it particularly well-suited for MCP applications. Clients can automatically register themselves with your WorkOS project and obtain the credentials needed for authentication without manual intervention.\n\n→ **Complete WorkOS tutorial**: [AuthKit Integration Guide](/v2/integrations/authkit)\n\n## Client Redirect URI Security\n\n<Note>\n`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR:\n\n- `None` (default): Only localhost patterns allowed\n- Custom list: Specify allowed patterns with wildcard support\n- Empty list `[]`: Allow all (not recommended)\n\nThis provides defense-in-depth even though DCR providers typically validate redirect URIs themselves.\n</Note>\n\n## Implementation Considerations\n\nRemote OAuth integration requires careful attention to several technical details that affect reliability and security.\n\n**Token Validation Performance**: Your server validates every incoming token by checking signatures against your identity provider's public keys. Consider implementing key caching and rotation handling to minimize latency while maintaining security.\n\n**Error Handling**: Network issues with your identity provider can affect token validation. Implement appropriate timeouts, retry logic, and graceful degradation to maintain service availability during identity provider outages.\n\n**Audience Validation**: Ensure that tokens intended for your server are not accepted by other applications. Proper audience validation prevents token misuse across different services in your ecosystem.\n\n**Scope Management**: Map token scopes to your application's permission model consistently. Consider how scope changes affect existing tokens and plan for smooth permission updates.\n\nThe complexity of these considerations reinforces why external identity providers are recommended over custom OAuth implementations. Established providers handle these technical details with extensive testing and operational experience."
  },
  {
    "path": "docs/v2/servers/auth/token-verification.mdx",
    "content": "---\ntitle: Token Verification\nsidebarTitle: Token Verification\ndescription: Protect your server by validating bearer tokens issued by external systems.\nicon: key\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.11.0\" />\n\nToken verification enables your FastMCP server to validate bearer tokens issued by external systems without participating in user authentication flows. Your server acts as a pure resource server, focusing on token validation and authorization decisions while delegating identity management to other systems in your infrastructure.\n\n<Note>\nToken verification operates somewhat outside the formal MCP authentication flow, which expects OAuth-style discovery. It's best suited for internal systems, microservices architectures, or when you have full control over token generation and distribution.\n</Note>\n\n## Understanding Token Verification\n\nToken verification addresses scenarios where authentication responsibility is distributed across multiple systems. Your MCP server receives structured tokens containing identity and authorization information, validates their authenticity, and makes access control decisions based on their contents.\n\nThis pattern emerges naturally in microservices architectures where a central authentication service issues tokens that multiple downstream services validate independently. It also works well when integrating MCP servers into existing systems that already have established token-based authentication mechanisms.\n\n### The Token Verification Model\n\nToken verification treats your MCP server as a resource server in OAuth terminology. The key insight is that token validation and token issuance are separate concerns that can be handled by different systems.\n\n**Token Issuance**: Another system (API gateway, authentication service, or identity provider) handles user authentication and creates signed tokens containing identity and permission information.\n\n**Token Validation**: Your MCP server receives these tokens, verifies their authenticity using cryptographic signatures, and extracts authorization information from their claims.\n\n**Access Control**: Based on token contents, your server determines what resources, tools, and prompts the client can access.\n\nThis separation allows your MCP server to focus on its core functionality while leveraging existing authentication infrastructure. The token acts as a portable proof of identity that travels with each request.\n\n### Token Security Considerations\n\nToken-based authentication relies on cryptographic signatures to ensure token integrity. Your MCP server validates tokens using public keys corresponding to the private keys used for token creation. This asymmetric approach means your server never needs access to signing secrets.\n\nToken validation must address several security requirements: signature verification ensures tokens haven't been tampered with, expiration checking prevents use of stale tokens, and audience validation ensures tokens intended for your server aren't accepted by other systems.\n\nThe challenge in MCP environments is that clients need to obtain valid tokens before making requests, but the MCP protocol doesn't provide built-in discovery mechanisms for token endpoints. Clients must obtain tokens through separate channels or prior configuration.\n\n\n## TokenVerifier Class\n\nFastMCP provides the `TokenVerifier` class to handle token validation complexity while remaining flexible about token sources and validation strategies.\n\n`TokenVerifier` focuses exclusively on token validation without providing OAuth discovery metadata. This makes it ideal for internal systems where clients already know how to obtain tokens, or for microservices that trust tokens from known issuers.\n\nThe class validates token signatures, checks expiration timestamps, and extracts authorization information from token claims. It supports various token formats and validation strategies while maintaining a consistent interface for authorization decisions.\n\nYou can subclass `TokenVerifier` to implement custom validation logic for specialized token formats or validation requirements. The base class handles common patterns while allowing extension for unique use cases.\n\n## JWT Token Verification\n\nJSON Web Tokens (JWTs) represent the most common token format for modern applications. FastMCP's `JWTVerifier` validates JWTs using industry-standard cryptographic techniques and claim validation.\n\n### JWKS Endpoint Integration\n\nJWKS endpoint integration provides the most flexible approach for production systems. The verifier automatically fetches public keys from a JSON Web Key Set endpoint, enabling automatic key rotation without server configuration changes.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Configure JWT verification against your identity provider\nverifier = JWTVerifier(\n    jwks_uri=\"https://auth.yourcompany.com/.well-known/jwks.json\",\n    issuer=\"https://auth.yourcompany.com\",\n    audience=\"mcp-production-api\"\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThis configuration creates a server that validates JWTs issued by `auth.yourcompany.com`. The verifier periodically fetches public keys from the JWKS endpoint and validates incoming tokens against those keys. Only tokens with the correct issuer and audience claims will be accepted.\n\nThe `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server.\n\n### Symmetric Key Verification (HMAC)\n\nSymmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators.\n\nThis approach is commonly used in microservices architectures where services share a secret key, or when your authentication service and MCP server are both managed by the same organization. The HMAC algorithms (HS256, HS384, HS512) provide strong security when the shared secret is properly managed.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Use a shared secret for symmetric key verification\nverifier = JWTVerifier(\n    public_key=\"your-shared-secret-key-minimum-32-chars\",  # Despite the name, this accepts symmetric secrets\n    issuer=\"internal-auth-service\",\n    audience=\"mcp-internal-api\",\n    algorithm=\"HS256\"  # or HS384, HS512 for stronger security\n)\n\nmcp = FastMCP(name=\"Internal API\", auth=verifier)\n```\n\nThe verifier will validate tokens signed with the same secret using the specified HMAC algorithm. This approach offers several advantages for internal systems:\n\n- **Simplicity**: No key pair management or certificate distribution\n- **Performance**: HMAC operations are typically faster than RSA\n- **Compatibility**: Works well with existing microservice authentication patterns\n\n<Note>\nThe parameter is named `public_key` for backwards compatibility, but when using HMAC algorithms (HS256/384/512), it accepts the symmetric secret string.\n</Note>\n\n<Warning>\n**Security Considerations for Symmetric Keys:**\n- Use a strong, randomly generated secret (minimum 32 characters recommended)\n- Never expose the secret in logs, error messages, or version control\n- Implement secure key distribution and rotation mechanisms\n- Consider using asymmetric keys (RSA/ECDSA) for external-facing APIs\n</Warning>\n\n### Static Public Key Verification\n\nStatic public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n# Use a static public key for token verification\npublic_key_pem = \"\"\"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----\"\"\"\n\nverifier = JWTVerifier(\n    public_key=public_key_pem,\n    issuer=\"https://auth.yourcompany.com\",\n    audience=\"mcp-production-api\"\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThis configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.\n## Opaque Token Verification\n\nMany authorization servers issue opaque tokens rather than self-contained JWTs. Opaque tokens are random strings that carry no information themselves - the authorization server maintains their state and validation requires querying the server. FastMCP supports opaque token validation through OAuth 2.0 Token Introspection (RFC 7662).\n\n### Understanding Opaque Tokens\n\nOpaque tokens differ fundamentally from JWTs in their verification model. Where JWTs carry signed claims that can be validated locally, opaque tokens require network calls to the issuing authorization server for validation. The authorization server maintains token state and can revoke tokens immediately, providing stronger security guarantees for sensitive operations.\n\nThis approach trades performance (network latency on each validation) for security and flexibility. Authorization servers can revoke opaque tokens instantly, implement complex authorization logic, and maintain detailed audit logs of token usage. Many enterprise OAuth providers default to opaque tokens for these security advantages.\n\n### Token Introspection Protocol\n\nRFC 7662 standardizes how resource servers validate opaque tokens. The protocol defines an introspection endpoint where resource servers authenticate using client credentials and receive token metadata including active status, scopes, expiration, and subject identity.\n\nFastMCP implements this protocol through the `IntrospectionTokenVerifier` class, handling authentication, request formatting, and response parsing according to the specification.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\n\n# Configure introspection with your OAuth provider\nverifier = IntrospectionTokenVerifier(\n    introspection_url=\"https://auth.yourcompany.com/oauth/introspect\",\n    client_id=\"mcp-resource-server\",\n    client_secret=\"your-client-secret\",\n    required_scopes=[\"api:read\", \"api:write\"]\n)\n\nmcp = FastMCP(name=\"Protected API\", auth=verifier)\n```\n\nThe verifier authenticates to the introspection endpoint using HTTP Basic Auth with your client credentials. When a request arrives with a bearer token, FastMCP queries the introspection endpoint to determine if the token is active and has sufficient scopes.\n\n## Development and Testing\n\nDevelopment environments often need simpler token management without the complexity of full JWT infrastructure. FastMCP provides tools specifically designed for these scenarios.\n\n### Static Token Verification\n\nStatic token verification enables rapid development by accepting predefined tokens with associated claims. This approach eliminates the need for token generation infrastructure during development and testing.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n# Define development tokens and their associated claims\nverifier = StaticTokenVerifier(\n    tokens={\n        \"dev-alice-token\": {\n            \"client_id\": \"alice@company.com\",\n            \"scopes\": [\"read:data\", \"write:data\", \"admin:users\"]\n        },\n        \"dev-guest-token\": {\n            \"client_id\": \"guest-user\",\n            \"scopes\": [\"read:data\"]\n        }\n    },\n    required_scopes=[\"read:data\"]\n)\n\nmcp = FastMCP(name=\"Development Server\", auth=verifier)\n```\n\nClients can now authenticate using `Authorization: Bearer dev-alice-token` headers. The server will recognize the token and load the associated claims for authorization decisions. This approach enables immediate development without external dependencies.\n\n<Warning>\nStatic token verification stores tokens as plain text and should never be used in production environments. It's designed exclusively for development and testing scenarios.\n</Warning>\n\n\n### Debug/Custom Token Verification\n\n<VersionBadge version=\"2.13.1\" />\n\nThe `DebugTokenVerifier` provides maximum flexibility for testing and special cases where standard token verification isn't applicable. It delegates validation to a user-provided callable, making it useful for prototyping, testing scenarios, or handling opaque tokens without introspection endpoints.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n# Accept all tokens (useful for rapid development)\nverifier = DebugTokenVerifier()\n\nmcp = FastMCP(name=\"Development Server\", auth=verifier)\n```\n\nBy default, `DebugTokenVerifier` accepts any non-empty token as valid. This eliminates authentication barriers during early development, allowing you to focus on core functionality before adding security.\n\nFor more controlled testing, provide custom validation logic:\n\n```python\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n# Synchronous validation - check token prefix\nverifier = DebugTokenVerifier(\n    validate=lambda token: token.startswith(\"dev-\"),\n    client_id=\"development-client\",\n    scopes=[\"read\", \"write\"]\n)\n\nmcp = FastMCP(name=\"Development Server\", auth=verifier)\n```\n\nThe validation callable can also be async, enabling database lookups or external service calls:\n\n```python\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n# Asynchronous validation - check against cache\nasync def validate_token(token: str) -> bool:\n    # Check if token exists in Redis, database, etc.\n    return await redis.exists(f\"valid_tokens:{token}\")\n\nverifier = DebugTokenVerifier(\n    validate=validate_token,\n    client_id=\"api-client\",\n    scopes=[\"api:access\"]\n)\n\nmcp = FastMCP(name=\"Custom API\", auth=verifier)\n```\n\n**Use Cases:**\n\n- **Testing**: Accept any token during integration tests without setting up token infrastructure\n- **Prototyping**: Quickly validate concepts without authentication complexity\n- **Opaque tokens without introspection**: When you have tokens from an IDP that provides no introspection endpoint, and you're willing to accept tokens without validation (validation happens later at the upstream service)\n- **Custom token formats**: Implement validation for non-standard token formats or legacy systems\n\n<Warning>\n`DebugTokenVerifier` bypasses standard security checks. Only use in controlled environments (development, testing) or when you fully understand the security implications. For production, use proper JWT or introspection-based verification.\n</Warning>\n\n### Test Token Generation\n\nTest token generation helps when you need to test JWT verification without setting up complete identity infrastructure. FastMCP includes utilities for generating test key pairs and signed tokens.\n\n```python\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\n# Generate a key pair for testing\nkey_pair = RSAKeyPair.generate()\n\n# Configure your server with the public key\nverifier = JWTVerifier(\n    public_key=key_pair.public_key,\n    issuer=\"https://test.yourcompany.com\",\n    audience=\"test-mcp-server\"\n)\n\n# Generate a test token using the private key\ntest_token = key_pair.create_token(\n    subject=\"test-user-123\",\n    issuer=\"https://test.yourcompany.com\", \n    audience=\"test-mcp-server\",\n    scopes=[\"read\", \"write\", \"admin\"]\n)\n\nprint(f\"Test token: {test_token}\")\n```\n\nThis pattern enables comprehensive testing of JWT validation logic without depending on external token issuers. The generated tokens are cryptographically valid and will pass all standard JWT validation checks.\n\n## Environment Configuration\n\n<VersionBadge version=\"2.12.1\" />\n\nFastMCP supports both programmatic and environment-based configuration for token verification, enabling flexible deployment across different environments.\n\nEnvironment-based configuration separates authentication settings from application code, following twelve-factor app principles and simplifying deployment pipelines.\n\n```bash\n# Enable JWT verification\nexport FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.jwt.JWTVerifier\n\n# For asymmetric verification with JWKS endpoint:\nexport FASTMCP_SERVER_AUTH_JWT_JWKS_URI=\"https://auth.company.com/.well-known/jwks.json\"\nexport FASTMCP_SERVER_AUTH_JWT_ISSUER=\"https://auth.company.com\"\nexport FASTMCP_SERVER_AUTH_JWT_AUDIENCE=\"mcp-production-api\"\nexport FASTMCP_SERVER_AUTH_JWT_REQUIRED_SCOPES=\"read:data,write:data\"\n\n# OR for symmetric key verification (HMAC):\nexport FASTMCP_SERVER_AUTH_JWT_PUBLIC_KEY=\"your-shared-secret-key-minimum-32-chars\"\nexport FASTMCP_SERVER_AUTH_JWT_ALGORITHM=\"HS256\"  # or HS384, HS512\nexport FASTMCP_SERVER_AUTH_JWT_ISSUER=\"internal-auth-service\"\nexport FASTMCP_SERVER_AUTH_JWT_AUDIENCE=\"mcp-internal-api\"\n```\n\nWith these environment variables configured, your FastMCP server automatically enables JWT verification:\n\n```python\nfrom fastmcp import FastMCP\n\n# Authentication automatically configured from environment\nmcp = FastMCP(name=\"Production API\")\n```\n\nThis approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration.\n\n"
  },
  {
    "path": "docs/v2/servers/composition.mdx",
    "content": "---\ntitle: Server Composition\nsidebarTitle: Composition\ndescription: Combine multiple FastMCP servers into a single, larger application using mounting and importing.\nicon: puzzle-piece\n---\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.2.0\" />\n\nAs your MCP applications grow, you might want to organize your tools, resources, and prompts into logical modules or reuse existing server components. FastMCP supports composition through two methods:\n\n- **`import_server`**: For a one-time copy of components with prefixing (static composition).\n- **`mount`**: For creating a live link where the main server delegates requests to the subserver (dynamic composition).\n\n## Why Compose Servers?\n\n-   **Modularity**: Break down large applications into smaller, focused servers (e.g., a `WeatherServer`, a `DatabaseServer`, a `CalendarServer`).\n-   **Reusability**: Create common utility servers (e.g., a `TextProcessingServer`) and mount them wherever needed.\n-   **Teamwork**: Different teams can work on separate FastMCP servers that are later combined.\n-   **Organization**: Keep related functionality grouped together logically.\n\n### Importing vs Mounting\n\nThe choice of importing or mounting depends on your use case and requirements.\n\n| Feature | Importing | Mounting |\n|---------|----------------|---------|\n| **Method** | `FastMCP.import_server(server, prefix=None)` | `FastMCP.mount(server, prefix=None)` |\n| **Composition Type** | One-time copy (static) | Live link (dynamic) |\n| **Updates** | Changes to subserver NOT reflected | Changes to subserver immediately reflected |\n| **Performance** | Fast - no runtime delegation | Slower - affected by slowest mounted server |\n| **Prefix** | Optional - omit for original names | Optional - omit for original names |\n| **Best For** | Bundling finalized components, performance-critical setups | Modular runtime composition |\n\n### Proxy Servers\n\nFastMCP supports [MCP proxying](/v2/servers/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.\n\n<VersionBadge version=\"2.4.0\" />\n\nYou can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/v2/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.\n\nPrefixing rules for tools, prompts, resources, and templates are identical across importing, mounting, and proxies. When prefixes are used, resource URIs are prefixed using path format (since 2.4.0): `resource://prefix/path/to/resource`.\n\n## Importing (Static Composition)\n\nThe `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). An optional `prefix` can be provided to avoid naming conflicts. If no prefix is provided, components are imported without modification. When multiple servers are imported with the same prefix (or no prefix), the most recently imported server's components take precedence.\n\n```python\nfrom fastmcp import FastMCP\nimport asyncio\n\n# Define subservers\nweather_mcp = FastMCP(name=\"WeatherService\")\n\n@weather_mcp.tool\ndef get_forecast(city: str) -> dict:\n    \"\"\"Get weather forecast.\"\"\"\n    return {\"city\": city, \"forecast\": \"Sunny\"}\n\n@weather_mcp.resource(\"data://cities/supported\")\ndef list_supported_cities() -> list[str]:\n    \"\"\"List cities with weather support.\"\"\"\n    return [\"London\", \"Paris\", \"Tokyo\"]\n\n# Define main server\nmain_mcp = FastMCP(name=\"MainApp\")\n\n# Import subserver\nasync def setup():\n    await main_mcp.import_server(weather_mcp, prefix=\"weather\")\n\n# Result: main_mcp now contains prefixed components:\n# - Tool: \"weather_get_forecast\"\n# - Resource: \"data://weather/cities/supported\" \n\nif __name__ == \"__main__\":\n    asyncio.run(setup())\n    main_mcp.run()\n```\n\n### How Importing Works\n\nWhen you call `await main_mcp.import_server(subserver, prefix={whatever})`:\n\n1.  **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.\n    -   `subserver.tool(name=\"my_tool\")` becomes `main_mcp.tool(name=\"{prefix}_my_tool\")`.\n2.  **Resources**: All resources are added with both URIs and names prefixed.\n    -   URI: `subserver.resource(uri=\"data://info\")` becomes `main_mcp.resource(uri=\"data://{prefix}/info\")`.\n    -   Name: `resource.name` becomes `\"{prefix}_{resource.name}\"`.\n3.  **Resource Templates**: Templates are prefixed similarly to resources.\n    -   URI: `subserver.resource(uri=\"data://{id}\")` becomes `main_mcp.resource(uri=\"data://{prefix}/{id}\")`.\n    -   Name: `template.name` becomes `\"{prefix}_{template.name}\"`.\n4.  **Prompts**: All prompts are added with names prefixed using `{prefix}_`.\n    -   `subserver.prompt(name=\"my_prompt\")` becomes `main_mcp.prompt(name=\"{prefix}_my_prompt\")`.\n\nNote that `import_server` performs a **one-time copy** of components. Changes made to the `subserver` *after* importing **will not** be reflected in `main_mcp`. The `subserver`'s `lifespan` context is also **not** executed by the main server.\n\n<Tip>\nThe `prefix` parameter is optional. If omitted, components are imported without modification.\n</Tip>\n\n#### Importing Without Prefixes\n\n<VersionBadge version=\"2.9.0\" />\n\nYou can also import servers without specifying a prefix, which copies components using their original names:\n\n```python\n\nfrom fastmcp import FastMCP\nimport asyncio\n\n# Define subservers\nweather_mcp = FastMCP(name=\"WeatherService\")\n\n@weather_mcp.tool\ndef get_forecast(city: str) -> dict:\n    \"\"\"Get weather forecast.\"\"\"\n    return {\"city\": city, \"forecast\": \"Sunny\"}\n\n@weather_mcp.resource(\"data://cities/supported\")\ndef list_supported_cities() -> list[str]:\n    \"\"\"List cities with weather support.\"\"\"\n    return [\"London\", \"Paris\", \"Tokyo\"]\n\n# Define main server\nmain_mcp = FastMCP(name=\"MainApp\")\n\n# Import subserver\nasync def setup():\n    # Import without prefix - components keep original names\n    await main_mcp.import_server(weather_mcp)\n\n# Result: main_mcp now contains:\n# - Tool: \"get_forecast\" (original name preserved)\n# - Resource: \"data://cities/supported\" (original URI preserved)\n\nif __name__ == \"__main__\":\n    asyncio.run(setup())\n    main_mcp.run()\n```\n\n#### Conflict Resolution\n\n<VersionBadge version=\"2.9.0\" />\n\nWhen importing multiple servers with the same prefix, or no prefix, components from the **most recently imported** server take precedence.\n\n\n\n\n## Mounting (Live Linking)\n\nThe `mount()` method creates a **live link** between the `main_mcp` server and the `subserver`. Instead of copying components, requests for components matching the optional `prefix` are **delegated** to the `subserver` at runtime. If no prefix is provided, the subserver's components are accessible without prefixing. When multiple servers are mounted with the same prefix (or no prefix), the most recently mounted server takes precedence for conflicting component names.\n\n```python\nimport asyncio\nfrom fastmcp import FastMCP, Client\n\n# Define subserver\ndynamic_mcp = FastMCP(name=\"DynamicService\")\n\n@dynamic_mcp.tool\ndef initial_tool():\n    \"\"\"Initial tool demonstration.\"\"\"\n    return \"Initial Tool Exists\"\n\n# Mount subserver (synchronous operation)\nmain_mcp = FastMCP(name=\"MainAppLive\")\nmain_mcp.mount(dynamic_mcp, prefix=\"dynamic\")\n\n# Add a tool AFTER mounting - it will be accessible through main_mcp\n@dynamic_mcp.tool\ndef added_later():\n    \"\"\"Tool added after mounting.\"\"\"\n    return \"Tool Added Dynamically!\"\n\n# Testing access to mounted tools\nasync def test_dynamic_mount():\n    tools = await main_mcp.get_tools()\n    print(\"Available tools:\", list(tools.keys()))\n    # Shows: ['dynamic_initial_tool', 'dynamic_added_later']\n    \n    async with Client(main_mcp) as client:\n        result = await client.call_tool(\"dynamic_added_later\")\n        print(\"Result:\", result.data)\n        # Shows: \"Tool Added Dynamically!\"\n\nif __name__ == \"__main__\":\n    asyncio.run(test_dynamic_mount())\n```\n\n### How Mounting Works\n\nWhen mounting is configured:\n\n1. **Live Link**: The parent server establishes a connection to the mounted server.\n2. **Dynamic Updates**: Changes to the mounted server are immediately reflected when accessed through the parent.\n3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server.\n4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime.\n\nThe same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts. This includes prefixing both the URIs/keys and the names of resources and templates for better identification in multi-server configurations.\n\n<Tip>\n    The `prefix` parameter is optional. If omitted, components are mounted without modification.\n</Tip>\n\n<Note>\nWhen mounting servers, custom HTTP routes defined with `@server.custom_route()` are also forwarded to the parent server, making them accessible through the parent's HTTP application.\n</Note>\n\n#### Performance Considerations\n\nDue to the \"live link\", operations like `list_tools()` on the parent server will be impacted by the speed of the slowest mounted server. In particular, HTTP-based mounted servers can introduce significant latency (300-400ms vs 1-2ms for local tools), and this slowdown affects the whole server, not just interactions with the HTTP-proxied tools. If performance is important, importing tools via [`import_server()`](#importing-static-composition) may be a more appropriate solution as it copies components once at startup rather than delegating requests at runtime.\n\n#### Mounting Without Prefixes\n\n<VersionBadge version=\"2.9.0\" />\n\nYou can also mount servers without specifying a prefix, which makes components accessible without prefixing. This works identically to [importing without prefixes](#importing-without-prefixes), including [conflict resolution](#conflict-resolution).\n\n\n\n\n### Direct vs. Proxy Mounting\n\n<VersionBadge version=\"2.2.7\" />\n\nFastMCP supports two mounting modes:\n\n1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory.\n   - No client lifecycle events occur on the mounted server\n   - The mounted server's lifespan context is not executed\n   - Communication is handled through direct method calls\n   \n2. **Proxy Mounting**: The parent server treats the mounted server as a separate entity and communicates with it through a client interface.\n   - Full client lifecycle events occur on the mounted server\n   - The mounted server's lifespan is executed when a client connects\n   - Communication happens via an in-memory Client transport\n\n```python\n# Direct mounting (default when no custom lifespan)\nmain_mcp.mount(api_server, prefix=\"api\")\n\n# Proxy mounting (preserves full client lifecycle)\nmain_mcp.mount(api_server, prefix=\"api\", as_proxy=True)\n\n# Mounting without a prefix (components accessible without prefixing)\nmain_mcp.mount(api_server)\n```\n\nFastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior with the `as_proxy` parameter.\n\n#### Interaction with Proxy Servers\n\nWhen using `FastMCP.as_proxy()` to create a proxy server, mounting that server will always use proxy mounting:\n\n```python\n# Create a proxy for a remote server\nremote_proxy = FastMCP.as_proxy(Client(\"http://example.com/mcp\"))\n\n# Mount the proxy (always uses proxy mounting)\nmain_server.mount(remote_proxy, prefix=\"remote\")\n```\n\n\n\n## Tag Filtering with Composition\n\n<VersionBadge version=\"2.9.0\" />\n\nWhen using `include_tags` or `exclude_tags` on a parent server, these filters apply **recursively** to all components, including those from mounted or imported servers. This allows you to control which components are exposed at the parent level, regardless of how your application is composed.\n\n```python\nimport asyncio\nfrom fastmcp import FastMCP, Client\n\n# Create a subserver with tools tagged for different environments\napi_server = FastMCP(name=\"APIServer\")\n\n@api_server.tool(tags={\"production\"})\ndef prod_endpoint() -> str:\n    \"\"\"Production-ready endpoint.\"\"\"\n    return \"Production data\"\n\n@api_server.tool(tags={\"development\"})\ndef dev_endpoint() -> str:\n    \"\"\"Development-only endpoint.\"\"\"\n    return \"Debug data\"\n\n# Mount the subserver with production tag filtering at parent level\nprod_app = FastMCP(name=\"ProductionApp\", include_tags={\"production\"})\nprod_app.mount(api_server, prefix=\"api\")\n\n# Test the filtering\nasync def test_filtering():\n    async with Client(prod_app) as client:\n        tools = await client.list_tools()\n        print(\"Available tools:\", [t.name for t in tools])\n        # Shows: ['api_prod_endpoint']\n        # The 'api_dev_endpoint' is filtered out\n\n        # Calling the filtered tool raises an error\n        try:\n            await client.call_tool(\"api_dev_endpoint\")\n        except Exception as e:\n            print(f\"Filtered tool not accessible: {e}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(test_filtering())\n```\n\n### How Recursive Filtering Works\n\nTag filters apply in the following order:\n\n1. **Child Server Filters**: Each mounted/imported server first applies its own `include_tags`/`exclude_tags` to its components.\n2. **Parent Server Filters**: The parent server then applies its own `include_tags`/`exclude_tags` to all components, including those from child servers.\n\nThis ensures that parent server tag policies act as a global policy for everything the parent server exposes, no matter how your application is composed.\n\n<Note>\nThis filtering applies to both **listing** (e.g., `list_tools()`) and **execution** (e.g., `call_tool()`). Filtered components are neither visible nor executable through the parent server.\n</Note>\n"
  },
  {
    "path": "docs/v2/servers/context.mdx",
    "content": "---\ntitle: MCP Context\nsidebarTitle: Context\ndescription: Access MCP capabilities like logging, progress, and resources within your MCP objects.\nicon: rectangle-code\n---\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nWhen defining FastMCP [tools](/v2/servers/tools), [resources](/v2/servers/resources), resource templates, or [prompts](/v2/servers/prompts), your functions might need to interact with the underlying MCP session or access advanced server capabilities. FastMCP provides the `Context` object for this purpose.\n\n<Note>\nFastMCP uses [Docket](https://github.com/chrisguidry/docket)'s dependency injection system for managing runtime dependencies. This page covers Context and the built-in dependencies; see [Custom Dependencies](#custom-dependencies) for creating your own.\n</Note>\n\n## What Is Context?\n\nThe `Context` object provides a clean interface to access MCP features within your functions, including:\n\n- **Logging**: Send debug, info, warning, and error messages back to the client\n- **Progress Reporting**: Update the client on the progress of long-running operations\n- **Resource Access**: List and read data from resources registered with the server\n- **Prompt Access**: List and retrieve prompts registered with the server\n- **LLM Sampling**: Request the client's LLM to generate text based on provided messages\n- **User Elicitation**: Request structured input from users during tool execution\n- **State Management**: Store and share data between middleware and the handler within a single request\n- **Request Information**: Access metadata about the current request\n- **Server Access**: When needed, access the underlying FastMCP server instance\n\n## Accessing the Context\n\n<VersionBadge version=\"2.14\" />\n\nThe preferred way to access context is using the `CurrentContext()` dependency:\n\n```python {1, 6}\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentContext\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(name=\"Context Demo\")\n\n@mcp.tool\nasync def process_file(file_uri: str, ctx: Context = CurrentContext()) -> str:\n    \"\"\"Processes a file, using context for logging and resource access.\"\"\"\n    await ctx.info(f\"Processing {file_uri}\")\n    return \"Processed file\"\n```\n\nThis works with tools, resources, and prompts:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import CurrentContext\nfrom fastmcp.server.context import Context\n\nmcp = FastMCP(name=\"Context Demo\")\n\n@mcp.resource(\"resource://user-data\")\nasync def get_user_data(ctx: Context = CurrentContext()) -> dict:\n    await ctx.debug(\"Fetching user data\")\n    return {\"user_id\": \"example\"}\n\n@mcp.prompt\nasync def data_analysis_request(dataset: str, ctx: Context = CurrentContext()) -> str:\n    return f\"Please analyze the following dataset: {dataset}\"\n```\n\n**Key Points:**\n\n- Dependency parameters are automatically excluded from the MCP schema—clients never see them.\n- Context methods are async, so your function usually needs to be async as well.\n- **Each MCP request receives a new context object.** Context is scoped to a single request; state or data set in one request will not be available in subsequent requests.\n- Context is only available during a request; attempting to use context methods outside a request will raise errors.\n\n### Legacy Type-Hint Injection\n\nFor backwards compatibility, you can still access context by simply adding a parameter with the `Context` type hint. FastMCP will automatically inject the context instance:\n\n```python {1, 6}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"Context Demo\")\n\n@mcp.tool\nasync def process_file(file_uri: str, ctx: Context) -> str:\n    \"\"\"Processes a file, using context for logging and resource access.\"\"\"\n    # Context is injected automatically based on the type hint\n    return \"Processed file\"\n```\n\nThis approach still works for tools, resources, and prompts. The parameter name doesn't matter—only the `Context` type hint is important. The type hint can also be a union (`Context | None`) or use `Annotated[]`.\n\n### Via `get_context()` Function\n\n<VersionBadge version=\"2.2.11\" />\n\nFor code nested deeper within your function calls where passing context through parameters is inconvenient, use `get_context()` to retrieve the active context from anywhere within a request's execution flow:\n\n```python {2,9}\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_context\n\nmcp = FastMCP(name=\"Dependency Demo\")\n\n# Utility function that needs context but doesn't receive it as a parameter\nasync def process_data(data: list[float]) -> dict:\n    # Get the active context - only works when called within a request\n    ctx = get_context()\n    await ctx.info(f\"Processing {len(data)} data points\")\n\n@mcp.tool\nasync def analyze_dataset(dataset_name: str) -> dict:\n    # Call utility function that uses context internally\n    data = load_data(dataset_name)\n    await process_data(data)\n```\n\n**Important Notes:**\n\n- The `get_context()` function should only be used within the context of a server request. Calling it outside of a request will raise a `RuntimeError`.\n- The `get_context()` function is server-only and should not be used in client code.\n\n## Context Capabilities\n\nFastMCP provides several advanced capabilities through the context object. Each capability has dedicated documentation with comprehensive examples and best practices:\n\n### Logging\n\nSend debug, info, warning, and error messages back to the MCP client for visibility into function execution.\n\n```python\nawait ctx.debug(\"Starting analysis\")\nawait ctx.info(f\"Processing {len(data)} items\") \nawait ctx.warning(\"Deprecated parameter used\")\nawait ctx.error(\"Processing failed\")\n```\n\nSee [Server Logging](/v2/servers/logging) for complete documentation and examples.\n### Client Elicitation\n\n<VersionBadge version=\"2.10.0\" />\n\nRequest structured input from clients during tool execution, enabling interactive workflows and progressive disclosure. This is a new feature in the 6/18/2025 MCP spec.\n\n```python\nresult = await ctx.elicit(\"Enter your name:\", response_type=str)\nif result.action == \"accept\":\n    name = result.data\n```\n\nSee [User Elicitation](/v2/servers/elicitation) for detailed examples and supported response types.\n\n### LLM Sampling\n\n<VersionBadge version=\"2.0.0\" />\n\nRequest the client's LLM to generate text based on provided messages, useful for leveraging AI capabilities within your tools.\n\n```python\nresponse = await ctx.sample(\"Analyze this data\", temperature=0.7)\n```\n\nSee [LLM Sampling](/v2/servers/sampling) for comprehensive usage and advanced techniques.\n\n\n### Progress Reporting\n\nUpdate clients on the progress of long-running operations, enabling progress indicators and better user experience.\n\n```python\nawait ctx.report_progress(progress=50, total=100)  # 50% complete\n```\n\nSee [Progress Reporting](/v2/servers/progress) for detailed patterns and examples.\n\n### Resource Access\n\nList and read data from resources registered with your FastMCP server, allowing access to files, configuration, or dynamic content.\n\n```python\n# List available resources\nresources = await ctx.list_resources()\n\n# Read a specific resource\ncontent_list = await ctx.read_resource(\"resource://config\")\ncontent = content_list[0].content\n```\n\n**Method signatures:**\n- **`ctx.list_resources() -> list[MCPResource]`**: <VersionBadge version=\"2.13.0\" /> Returns list of all available resources\n- **`ctx.read_resource(uri: str | AnyUrl) -> list[ReadResourceContents]`**: Returns a list of resource content parts\n\n### Prompt Access\n\n<VersionBadge version=\"2.13.0\" />\n\nList and retrieve prompts registered with your FastMCP server, allowing tools and middleware to discover and use available prompts programmatically.\n\n```python\n# List available prompts\nprompts = await ctx.list_prompts()\n\n# Get a specific prompt with arguments\nresult = await ctx.get_prompt(\"analyze_data\", {\"dataset\": \"users\"})\nmessages = result.messages\n```\n\n**Method signatures:**\n- **`ctx.list_prompts() -> list[MCPPrompt]`**: Returns list of all available prompts\n- **`ctx.get_prompt(name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult`**: Get a specific prompt with optional arguments\n\n### State Management\n\n<VersionBadge version=\"2.11.0\" />\n\nStore and share data between middleware and handlers within a single MCP request. Each MCP request (such as calling a tool, reading a resource, listing tools, or listing resources) receives its own context object with isolated state. Context state is particularly useful for passing information from [middleware](/v2/servers/middleware) to your handlers.\n\nTo store a value in the context state, use `ctx.set_state(key, value)`. To retrieve a value, use `ctx.get_state(key)`.\n\n<Warning>\nContext state is scoped to a single MCP request. Each operation (tool call, resource read, list operation, etc.) receives a new context object. State set during one request will not be available in subsequent requests. For persistent data storage across requests, use external storage mechanisms like databases, files, or in-memory caches.\n</Warning>\n\nThis simplified example shows how to use MCP middleware to store user info in the context state, and how to access that state in a tool:\n\n```python {7-8, 16-17}\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass UserAuthMiddleware(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n\n        # Middleware stores user info in context state\n        context.fastmcp_context.set_state(\"user_id\", \"user_123\")\n        context.fastmcp_context.set_state(\"permissions\", [\"read\", \"write\"])\n\n        return await call_next(context)\n\n@mcp.tool\nasync def secure_operation(data: str, ctx: Context) -> str:\n    \"\"\"Tool can access state set by middleware.\"\"\"\n\n    user_id = ctx.get_state(\"user_id\")  # \"user_123\"\n    permissions = ctx.get_state(\"permissions\")  # [\"read\", \"write\"]\n    \n    if \"write\" not in permissions:\n        return \"Access denied\"\n    \n    return f\"Processing {data} for user {user_id}\"\n```\n\n**Method signatures:**\n- **`ctx.set_state(key: str, value: Any) -> None`**: Store a value in the context state\n- **`ctx.get_state(key: str) -> Any`**: Retrieve a value from the context state (returns None if not found)\n\n**State Inheritance:**\nWhen a new context is created (nested contexts), it inherits a copy of its parent's state. This ensures that:\n- State set on a child context never affects the parent context\n- State set on a parent context after the child context is initialized is not propagated to the child context\n\nThis makes state management predictable and prevents unexpected side effects between nested operations.\n\n### Change Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends list change notifications when components (such as tools, resources, or prompts) are added, removed, enabled, or disabled. In rare cases where you need to manually trigger these notifications, you can use the context methods:\n\n```python\n@mcp.tool\nasync def custom_tool_management(ctx: Context) -> str:\n    \"\"\"Example of manual notification after custom tool changes.\"\"\"\n    # After making custom changes to tools\n    await ctx.send_tool_list_changed()\n    await ctx.send_resource_list_changed()\n    await ctx.send_prompt_list_changed()\n    return \"Notifications sent\"\n```\n\nThese methods are primarily used internally by FastMCP's automatic notification system and most users will not need to invoke them directly.\n\n### FastMCP Server\n\nTo access the underlying FastMCP server instance, you can use the `ctx.fastmcp` property:\n\n```python\n@mcp.tool\nasync def my_tool(ctx: Context) -> None:\n    # Access the FastMCP server instance\n    server_name = ctx.fastmcp.name\n    ...\n```\n\n### MCP Request\n\nAccess metadata about the current request and client.\n\n```python\n@mcp.tool\nasync def request_info(ctx: Context) -> dict:\n    \"\"\"Return information about the current request.\"\"\"\n    return {\n        \"request_id\": ctx.request_id,\n        \"client_id\": ctx.client_id or \"Unknown client\"\n    }\n```\n\n**Available Properties:**\n\n- **`ctx.request_id -> str`**: Get the unique ID for the current MCP request\n- **`ctx.client_id -> str | None`**: Get the ID of the client making the request, if provided during initialization\n- **`ctx.session_id -> str | None`**: Get the MCP session ID for session-based data sharing (HTTP transports only)\n\n#### Request Context Availability\n\n<VersionBadge version=\"2.13.1\" />\n\nThe `ctx.request_context` property provides access to the underlying MCP request context, but returns `None` when the MCP session has not been established yet. This typically occurs:\n\n- During middleware execution in the `on_request` hook before the MCP handshake completes\n- During the initialization phase of client connections\n\nThe MCP request context is distinct from the HTTP request. For HTTP transports, HTTP request data may be available even when the MCP session is not yet established.\n\nTo safely access the request context in situations where it may not be available:\n\n```python\nfrom fastmcp import FastMCP, Context\nfrom fastmcp.server.dependencies import get_http_request\n\nmcp = FastMCP(name=\"Session Aware Demo\")\n\n@mcp.tool\nasync def session_info(ctx: Context) -> dict:\n    \"\"\"Return session information when available.\"\"\"\n\n    # Check if MCP session is available\n    if ctx.request_context:\n        # MCP session available - can access MCP-specific attributes\n        return {\n            \"session_id\": ctx.session_id,\n            \"request_id\": ctx.request_id,\n            \"has_meta\": ctx.request_context.meta is not None\n        }\n    else:\n        # MCP session not available - use HTTP helpers for request data (if using HTTP transport)\n        request = get_http_request()\n        return {\n            \"message\": \"MCP session not available\",\n            \"user_agent\": request.headers.get(\"user-agent\", \"Unknown\")\n        }\n```\n\nFor HTTP request access that works regardless of MCP session availability (when using HTTP transports), use the [HTTP request helpers](#http-requests) like `get_http_request()` and `get_http_headers()`.\n\n#### Client Metadata\n\n<VersionBadge version=\"2.13.1\" />\n\nClients can send contextual information with their requests using the `meta` parameter. This metadata is accessible through `ctx.request_context.meta` and is available for all MCP operations (tools, resources, prompts).\n\nThe `meta` field is `None` when clients don't provide metadata. When provided, metadata is accessible via attribute access (e.g., `meta.user_id`) rather than dictionary access. The structure of metadata is determined by the client making the request.\n\n```python\n@mcp.tool\ndef send_email(to: str, subject: str, body: str, ctx: Context) -> str:\n    \"\"\"Send an email, logging metadata about the request.\"\"\"\n\n    # Access client-provided metadata\n    meta = ctx.request_context.meta\n\n    if meta:\n        # Meta is accessed as an object with attribute access\n        user_id = meta.user_id if hasattr(meta, 'user_id') else None\n        trace_id = meta.trace_id if hasattr(meta, 'trace_id') else None\n\n        # Use metadata for logging, observability, etc.\n        if trace_id:\n            log_with_trace(f\"Sending email for user {user_id}\", trace_id)\n\n    # Send the email...\n    return f\"Email sent to {to}\"\n```\n\n<Warning>\nThe MCP request is part of the low-level MCP SDK and intended for advanced use cases. Most users will not need to use it directly.\n</Warning>\n\n## Runtime Dependencies\n\n### HTTP Requests\n\n<VersionBadge version=\"2.2.11\" />\n\nThe recommended way to access the current HTTP request is through the `get_http_request()` dependency function:\n\n```python {2, 3, 11}\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_http_request\nfrom starlette.requests import Request\n\nmcp = FastMCP(name=\"HTTP Request Demo\")\n\n@mcp.tool\nasync def user_agent_info() -> dict:\n    \"\"\"Return information about the user agent.\"\"\"\n    # Get the HTTP request\n    request: Request = get_http_request()\n    \n    # Access request data\n    user_agent = request.headers.get(\"user-agent\", \"Unknown\")\n    client_ip = request.client.host if request.client else \"Unknown\"\n    \n    return {\n        \"user_agent\": user_agent,\n        \"client_ip\": client_ip,\n        \"path\": request.url.path,\n    }\n```\n\nThis approach works anywhere within a request's execution flow, not just within your MCP function. It's useful when:\n\n1. You need access to HTTP information in helper functions\n2. You're calling nested functions that need HTTP request data\n3. You're working with middleware or other request processing code\n\n### HTTP Headers\n<VersionBadge version=\"2.2.11\" />\n\nIf you only need request headers and want to avoid potential errors, you can use the `get_http_headers()` helper:\n\n```python {2, 10}\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_http_headers\n\nmcp = FastMCP(name=\"Headers Demo\")\n\n@mcp.tool\nasync def safe_header_info() -> dict:\n    \"\"\"Safely get header information without raising errors.\"\"\"\n    # Get headers (returns empty dict if no request context)\n    headers = get_http_headers()\n    \n    # Get authorization header\n    auth_header = headers.get(\"authorization\", \"\")\n    is_bearer = auth_header.startswith(\"Bearer \")\n    \n    return {\n        \"user_agent\": headers.get(\"user-agent\", \"Unknown\"),\n        \"content_type\": headers.get(\"content-type\", \"Unknown\"),\n        \"has_auth\": bool(auth_header),\n        \"auth_type\": \"Bearer\" if is_bearer else \"Other\" if auth_header else \"None\",\n        \"headers_count\": len(headers)\n    }\n```\n\nBy default, `get_http_headers()` excludes problematic headers like `host` and `content-length`. To include all headers, use `get_http_headers(include_all=True)`.\n\n### Access Tokens\n\n<VersionBadge version=\"2.11.0\" />\n\nWhen using authentication with your FastMCP server, you can access the authenticated user's access token information using the `get_access_token()` dependency function:\n\n```python {2, 10}\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_access_token, AccessToken\n\nmcp = FastMCP(name=\"Auth Token Demo\")\n\n@mcp.tool\nasync def get_user_info() -> dict:\n    \"\"\"Get information about the authenticated user.\"\"\"\n    # Get the access token (None if not authenticated)\n    token: AccessToken | None = get_access_token()\n    \n    if token is None:\n        return {\"authenticated\": False}\n    \n    return {\n        \"authenticated\": True,\n        \"client_id\": token.client_id,\n        \"scopes\": token.scopes,\n        \"expires_at\": token.expires_at,\n        \"token_claims\": token.claims,  # JWT claims or custom token data\n    }\n```\n\nThis is particularly useful when you need to:\n\n1. **Access user identification** - Get the `client_id` or subject from token claims\n2. **Check permissions** - Verify scopes or custom claims before performing operations\n3. **Multi-tenant applications** - Extract tenant information from token claims\n4. **Audit logging** - Track which user performed which actions\n\n#### Working with Token Claims\n\nThe `claims` field contains all the data from the original token (JWT claims for JWT tokens, or custom data for other token types):\n\n```python {2, 3, 9, 12, 15}\nfrom fastmcp import FastMCP\nfrom fastmcp.server.dependencies import get_access_token\n\nmcp = FastMCP(name=\"Multi-tenant Demo\")\n\n@mcp.tool\nasync def get_tenant_data(resource_id: str) -> dict:\n    \"\"\"Get tenant-specific data using token claims.\"\"\"\n    token: AccessToken | None = get_access_token()\n    \n    # Extract tenant ID from token claims\n    tenant_id = token.claims.get(\"tenant_id\") if token else None\n    \n    # Extract user ID from standard JWT subject claim\n    user_id = token.claims.get(\"sub\") if token else None\n    \n    # Use tenant and user info to authorize and filter data\n    if not tenant_id:\n        raise ValueError(\"No tenant information in token\")\n    \n    return {\n        \"resource_id\": resource_id,\n        \"tenant_id\": tenant_id,\n        \"user_id\": user_id,\n        \"data\": f\"Tenant-specific data for {tenant_id}\",\n    }\n```\n\n## Custom Dependencies\n\n<VersionBadge version=\"2.14\" />\n\nFastMCP's dependency injection is powered by [Docket](https://github.com/chrisguidry/docket), which provides a flexible system for injecting values into your functions. Beyond the built-in dependencies like `CurrentContext()`, you can create your own.\n\n### Using `Depends()`\n\nThe simplest way to create a custom dependency is with `Depends()`. Pass any callable (sync or async function, or async context manager) and its return value will be injected:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(name=\"Custom Deps Demo\")\n\n# Simple function dependency\ndef get_config() -> dict:\n    return {\"api_url\": \"https://api.example.com\", \"timeout\": 30}\n\n# Async function dependency\nasync def get_user_id() -> int:\n    return 42\n\n@mcp.tool\nasync def fetch_data(\n    query: str,\n    config: dict = Depends(get_config),\n    user_id: int = Depends(get_user_id),\n) -> str:\n    return f\"User {user_id} fetching '{query}' from {config['api_url']}\"\n```\n\nDependencies using `Depends()` are automatically excluded from the MCP schema—clients never see them as parameters.\n\n### Resource Management with Context Managers\n\nFor dependencies that need cleanup (database connections, file handles, etc.), use an async context manager:\n\n```python\nfrom contextlib import asynccontextmanager\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(name=\"Resource Demo\")\n\n@asynccontextmanager\nasync def get_database():\n    db = await connect_to_database()\n    try:\n        yield db\n    finally:\n        await db.close()\n\n@mcp.tool\nasync def query_users(sql: str, db = Depends(get_database)) -> list:\n    return await db.execute(sql)\n```\n\nThe context manager's cleanup code runs after your function completes, even if an error occurs.\n\n### Nested Dependencies\n\nDependencies can depend on other dependencies:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP(name=\"Nested Demo\")\n\ndef get_base_url() -> str:\n    return \"https://api.example.com\"\n\ndef get_api_client(base_url: str = Depends(get_base_url)) -> dict:\n    return {\"base_url\": base_url, \"version\": \"v1\"}\n\n@mcp.tool\nasync def call_api(endpoint: str, client: dict = Depends(get_api_client)) -> str:\n    return f\"Calling {client['base_url']}/{client['version']}/{endpoint}\"\n```\n\n### Advanced: Subclassing `Dependency`\n\nFor more complex dependency patterns—like dependencies that need access to Docket's execution context or require custom lifecycle management—you can subclass Docket's `Dependency` class. See the [Docket documentation on dependencies](https://chrisguidry.github.io/docket/dependencies/) for details.\n"
  },
  {
    "path": "docs/v2/servers/elicitation.mdx",
    "content": "---\ntitle: User Elicitation\nsidebarTitle: Elicitation\ndescription: Request structured input from users during tool execution through the MCP context.\nicon: message-question\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.10.0\" />\n\nUser elicitation allows MCP servers to request structured input from users during tool execution. Instead of requiring all inputs upfront, tools can interactively ask for missing parameters, clarification, or additional context as needed.\n\n<Tip>\nMost of the examples in this document assume you have a FastMCP server instance named `mcp` and show how to use the `ctx.elicit` method to request user input from an `@mcp.tool`-decorated function.\n</Tip>\n\n## What is Elicitation?\n\nElicitation enables tools to pause execution and request specific information from users. This is particularly useful for:\n\n- **Missing parameters**: Ask for required information not provided initially\n- **Clarification requests**: Get user confirmation or choices for ambiguous scenarios  \n- **Progressive disclosure**: Collect complex information step-by-step\n- **Dynamic workflows**: Adapt tool behavior based on user responses\n\nFor example, a file management tool might ask \"Which directory should I create?\" or a data analysis tool might request \"What date range should I analyze?\"\n\n### Basic Usage\n\nUse the `ctx.elicit()` method within any tool function to request user input:\n\n```python {14-17}\nfrom fastmcp import FastMCP, Context\nfrom dataclasses import dataclass\n\nmcp = FastMCP(\"Elicitation Server\")\n\n@dataclass\nclass UserInfo:\n    name: str\n    age: int\n\n@mcp.tool\nasync def collect_user_info(ctx: Context) -> str:\n    \"\"\"Collect user information through interactive prompts.\"\"\"\n    result = await ctx.elicit(\n        message=\"Please provide your information\",\n        response_type=UserInfo\n    )\n    \n    if result.action == \"accept\":\n        user = result.data\n        return f\"Hello {user.name}, you are {user.age} years old\"\n    elif result.action == \"decline\":\n        return \"Information not provided\"\n    else:  # cancel\n        return \"Operation cancelled\"\n```\n\n## Method Signature\n\n<Card icon=\"code\" title=\"Context Elicitation Method\">\n<ResponseField name=\"ctx.elicit\" type=\"async method\">\n  <Expandable title=\"Parameters\">\n    <ResponseField name=\"message\" type=\"str\">\n      The prompt message to display to the user\n    </ResponseField>\n    \n    <ResponseField name=\"response_type\" type=\"type\" default=\"None\">\n      The Python type defining the expected response structure (dataclass, primitive type, etc.) Note that elicitation responses are subject to a restricted subset of JSON Schema types. See [Supported Response Types](#supported-response-types) for more details. \n    </ResponseField>\n  </Expandable>\n  \n  <Expandable title=\"Response\">\n    <ResponseField name=\"ElicitationResult\" type=\"object\">\n      Result object containing the user's response\n      \n      <Expandable title=\"properties\">\n        <ResponseField name=\"action\" type=\"Literal['accept', 'decline', 'cancel']\">\n          How the user responded to the request\n        </ResponseField>\n        \n        <ResponseField name=\"data\" type=\"response_type | None\">\n          The user's input data (only present when action is \"accept\")\n        </ResponseField>\n      </Expandable>\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n## Elicitation Actions\n\nThe elicitation result contains an `action` field indicating how the user responded:\n\n- **`accept`**: User provided valid input - data is available in the `data` field\n- **`decline`**: User chose not to provide the requested information and the data field is `None`\n- **`cancel`**: User cancelled the entire operation and the data field is `None`\n\n```python {5, 7}\n@mcp.tool\nasync def my_tool(ctx: Context) -> str:\n    result = await ctx.elicit(\"Choose an action\")\n\n    if result.action == \"accept\":\n        return \"Accepted!\"\n    elif result.action == \"decline\":\n        return \"Declined!\"\n    else:\n        return \"Cancelled!\"\n```\n\nFastMCP also provides typed result classes for pattern matching on the `action` field:\n\n```python {1-5, 12, 14, 16}\nfrom fastmcp.server.elicitation import (\n    AcceptedElicitation, \n    DeclinedElicitation, \n    CancelledElicitation,\n)\n\n@mcp.tool\nasync def pattern_example(ctx: Context) -> str:\n    result = await ctx.elicit(\"Enter your name:\", response_type=str)\n    \n    match result:\n        case AcceptedElicitation(data=name):\n            return f\"Hello {name}!\"\n        case DeclinedElicitation():\n            return \"No name provided\"\n        case CancelledElicitation():\n            return \"Operation cancelled\"\n```\n\n## Response Types\n\nThe server must send a schema to the client indicating the type of data it expects in response to the elicitation request. If the request is `accept`-ed, the client must send a response that matches the schema.\n\nThe MCP spec only supports a limited subset of JSON Schema types for elicitation responses. Specifically, it only supports JSON  **objects** with **primitive** properties including `string`, `number` (or `integer`), `boolean` and `enum` fields.\n\nFastMCP makes it easy to request a broader range of types, including scalars (e.g. `str`) or no response at all, by automatically wrapping them in MCP-compatible object schemas.\n\n\n### Scalar Types\n\nYou can request simple scalar data types for basic input, such as a string, integer, or boolean.\n\nWhen you request a scalar type, FastMCP automatically wraps it in an object schema for MCP spec compatibility. Clients will see a corresponding schema requesting a single \"value\" field of the requested type. Once clients respond, the provided object is \"unwrapped\" and the scalar value is returned to your tool function as the `data` field of the `ElicitationResult` object.\n\nAs a developer, this means you do not have to worry about creating or accessing a structured object when you only need a scalar value.\n\n<CodeGroup>\n```python {4} title=\"Request a string\"\n@mcp.tool\nasync def get_user_name(ctx: Context) -> str:\n    \"\"\"Get the user's name.\"\"\"\n    result = await ctx.elicit(\"What's your name?\", response_type=str)\n    \n    if result.action == \"accept\":\n        return f\"Hello, {result.data}!\"\n    return \"No name provided\"\n```\n```python {4} title=\"Request an integer\"\n@mcp.tool\nasync def pick_a_number(ctx: Context) -> str:\n    \"\"\"Pick a number.\"\"\"\n    result = await ctx.elicit(\"Pick a number!\", response_type=int)\n    \n    if result.action == \"accept\":\n        return f\"You picked {result.data}\"\n    return \"No number provided\"\n```\n```python {4} title=\"Request a boolean\"\n@mcp.tool\nasync def pick_a_boolean(ctx: Context) -> str:\n    \"\"\"Pick a boolean.\"\"\"\n    result = await ctx.elicit(\"True or false?\", response_type=bool)\n    \n    if result.action == \"accept\":\n        return f\"You picked {result.data}\"\n    return \"No boolean provided\"\n```\n</CodeGroup>\n\n### No Response\n\nSometimes, the goal of an elicitation is to simply get a user to approve or reject an action. In this case, you can pass `None` as the response type to indicate that no response is expected. In order to comply with the MCP spec, the client will see a schema requesting an empty object in response. In this case, the `data` field of the `ElicitationResult` object will be `None` when the user accepts the elicitation.\n\n```python {4} title=\"No response\"\n@mcp.tool\nasync def approve_action(ctx: Context) -> str:\n    \"\"\"Approve an action.\"\"\"\n    result = await ctx.elicit(\"Approve this action?\", response_type=None)\n\n    if result.action == \"accept\":\n        return do_action()\n    else:\n        raise ValueError(\"Action rejected\")\n```\n\n### Constrained Options\n\nOften you'll want to constrain the user's response to a specific set of values. You can do this by using a `Literal` type or a Python enum as the response type, or by passing a list of strings to the `response_type` parameter as a convenient shortcut.\n\n<CodeGroup>\n```python {6} title=\"Using a list of strings\"\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    \"\"\"Set task priority level.\"\"\"\n    result = await ctx.elicit(\n        \"What priority level?\",\n        response_type=[\"low\", \"medium\", \"high\"],\n    )\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data}\"\n```\n```python {1, 8} title=\"Using a Literal type\"\nfrom typing import Literal\n\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    \"\"\"Set task priority level.\"\"\"\n    result = await ctx.elicit(\n        \"What priority level?\",\n        response_type=Literal[\"low\", \"medium\", \"high\"]\n    )\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data}\"\n    return \"No priority set\"\n```\n```python {1, 11} title=\"Using a Python enum\"\nfrom enum import Enum\n\nclass Priority(Enum):\n    LOW = \"low\"\n    MEDIUM = \"medium\"\n    HIGH = \"high\"\n\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    \"\"\"Set task priority level.\"\"\"\n    result = await ctx.elicit(\"What priority level?\", response_type=Priority)\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data.value}\"\n    return \"No priority set\"\n```\n</CodeGroup>\n\n#### Multi-Select\n\n<VersionBadge version=\"2.14.0\" />\n\nEnable multi-select by wrapping your choices in an additional list level. This allows users to select multiple values from the available options.\n\n<CodeGroup>\n```python {6-8} title=\"List of a list of strings\"\n@mcp.tool\nasync def select_tags(ctx: Context) -> str:\n    \"\"\"Select multiple tags.\"\"\"\n    result = await ctx.elicit(\n        \"Choose tags\",\n        response_type=[[\"bug\", \"feature\", \"documentation\"]]  # Note: list of a list\n    )\n\n    if result.action == \"accept\":\n        tags = result.data  # List of selected strings\n        return f\"Selected tags: {', '.join(tags)}\"\n```\n\n```python {1, 3-6, 11-14} title=\"list[Enum] type annotation\"\nfrom enum import Enum\n\nclass Tag(Enum):\n    BUG = \"bug\"\n    FEATURE = \"feature\"\n    DOCS = \"documentation\"\n\n@mcp.tool\nasync def select_tags(ctx: Context) -> str:\n    result = await ctx.elicit(\n        \"Choose tags\",\n        response_type=list[Tag]  # Type annotation for multi-select\n    )\n    if result.action == \"accept\":\n        tags = [tag.value for tag in result.data]\n        return f\"Selected: {', '.join(tags)}\"\n```\n</CodeGroup>\n\nFor titled multi-select, wrap a dict in a list (see [Titled Options](#titled-options) for dict syntax):\n\n```python {6-12}\n@mcp.tool\nasync def select_priorities(ctx: Context) -> str:\n    \"\"\"Select multiple priorities.\"\"\"\n    result = await ctx.elicit(\n        \"Choose priorities\",\n        response_type=[{  # Note: list containing a dict\n            \"low\": {\"title\": \"Low Priority\"},\n            \"medium\": {\"title\": \"Medium Priority\"},\n            \"high\": {\"title\": \"High Priority\"}\n        }]\n    )\n\n    if result.action == \"accept\":\n        priorities = result.data  # List of selected strings\n        return f\"Selected: {', '.join(priorities)}\"\n```\n\n#### Titled Options\n\n<VersionBadge version=\"2.14.0\" />\n\nFor better UI display, you can provide human-readable titles for enum options. FastMCP generates SEP-1330 compliant schemas using the `oneOf` pattern with `const` and `title` fields.\n\nUse a dict to specify titles for enum values:\n\n```python {6-10}\n@mcp.tool\nasync def set_priority(ctx: Context) -> str:\n    \"\"\"Set task priority level.\"\"\"\n    result = await ctx.elicit(\n        \"What priority level?\",\n        response_type={\n            \"low\": {\"title\": \"Low Priority\"},\n            \"medium\": {\"title\": \"Medium Priority\"},\n            \"high\": {\"title\": \"High Priority\"}\n        }\n    )\n\n    if result.action == \"accept\":\n        return f\"Priority set to: {result.data}\"\n```\n\nFor multi-select with titles, wrap the dict in a list:\n\n```python {6-12}\n@mcp.tool\nasync def select_priorities(ctx: Context) -> str:\n    \"\"\"Select multiple priorities.\"\"\"\n    result = await ctx.elicit(\n        \"Choose priorities\",\n        response_type=[{  # List containing a dict for multi-select\n            \"low\": {\"title\": \"Low Priority\"},\n            \"medium\": {\"title\": \"Medium Priority\"},\n            \"high\": {\"title\": \"High Priority\"}\n        }]\n    )\n\n    if result.action == \"accept\":\n        priorities = result.data  # List of selected strings\n        return f\"Selected: {', '.join(priorities)}\"\n```\n\n### Structured Responses\n\nYou can request structured data with multiple fields by using a dataclass, typed dict, or Pydantic model as the response type. Note that the MCP spec only supports shallow objects with scalar (string, number, boolean) or enum properties.\n\n```python {1, 16, 20}\nfrom dataclasses import dataclass\nfrom typing import Literal\n\n@dataclass\nclass TaskDetails:\n    title: str\n    description: str\n    priority: Literal[\"low\", \"medium\", \"high\"]\n    due_date: str\n\n@mcp.tool\nasync def create_task(ctx: Context) -> str:\n    \"\"\"Create a new task with user-provided details.\"\"\"\n    result = await ctx.elicit(\n        \"Please provide task details\",\n        response_type=TaskDetails\n    )\n    \n    if result.action == \"accept\":\n        task = result.data\n        return f\"Created task: {task.title} (Priority: {task.priority})\"\n    return \"Task creation cancelled\"\n```\n\n### Default Values\n\n<VersionBadge version=\"2.14.0\" />\n\nYou can provide default values for elicitation fields using Pydantic's `Field(default=...)`. Clients will pre-populate form fields with these defaults, making it easier for users to provide input.\n\nDefault values are supported for all primitive types:\n- Strings: `Field(default=\"[email protected]\")`\n- Integers: `Field(default=50)`\n- Numbers: `Field(default=3.14)`\n- Booleans: `Field(default=False)`\n- Enums: `Field(default=EnumValue.A)`\n\nFields with default values are automatically marked as optional (not included in the `required` list), so users can accept the default or provide their own value.\n\n```python\nfrom pydantic import BaseModel, Field\nfrom enum import Enum\n\nclass Priority(Enum):\n    LOW = \"low\"\n    MEDIUM = \"medium\"\n    HIGH = \"high\"\n\nclass TaskDetails(BaseModel):\n    title: str = Field(description=\"Task title\")\n    description: str = Field(default=\"\", description=\"Task description\")\n    priority: Priority = Field(default=Priority.MEDIUM, description=\"Task priority\")\n\n@mcp.tool\nasync def create_task(ctx: Context) -> str:\n    result = await ctx.elicit(\"Please provide task details\", response_type=TaskDetails)\n    if result.action == \"accept\":\n        return f\"Created: {result.data.title}\"\n    return \"Task creation cancelled\"\n```\n\n## Multi-Turn Elicitation\n\nTools can make multiple elicitation calls to gather information progressively:\n\n```python {6, 11, 16-19}\n@mcp.tool\nasync def plan_meeting(ctx: Context) -> str:\n    \"\"\"Plan a meeting by gathering details step by step.\"\"\"\n    \n    # Get meeting title\n    title_result = await ctx.elicit(\"What's the meeting title?\", response_type=str)\n    if title_result.action != \"accept\":\n        return \"Meeting planning cancelled\"\n    \n    # Get duration\n    duration_result = await ctx.elicit(\"Duration in minutes?\", response_type=int)\n    if duration_result.action != \"accept\":\n        return \"Meeting planning cancelled\"\n    \n    # Get priority\n    priority_result = await ctx.elicit(\n        \"Is this urgent?\", \n        response_type=Literal[\"yes\", \"no\"]\n    )\n    if priority_result.action != \"accept\":\n        return \"Meeting planning cancelled\"\n    \n    urgent = priority_result.data == \"yes\"\n    return f\"Meeting '{title_result.data}' planned for {duration_result.data} minutes (Urgent: {urgent})\"\n```\n\n\n## Client Requirements\n\nElicitation requires the client to implement an elicitation handler. See [Client Elicitation](/v2/clients/elicitation) for details on how clients can handle these requests.\n\nIf a client doesn't support elicitation, calls to `ctx.elicit()` will raise an error indicating that elicitation is not supported."
  },
  {
    "path": "docs/v2/servers/icons.mdx",
    "content": "---\ntitle: Icons\ndescription: Add visual icons to your servers, tools, resources, and prompts\nicon: image\ntag: NEW\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.13.0\" />\n\nIcons provide visual representations for your MCP servers and components, helping client applications present better user interfaces. When displayed in MCP clients, icons help users quickly identify and navigate your server's capabilities.\n\n## Icon Format\n\nIcons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies:\n\n- **src**: URL or data URI pointing to the icon image\n- **mimeType** (optional): MIME type of the image (e.g., \"image/png\", \"image/svg+xml\")\n- **sizes** (optional): Array of size descriptors (e.g., [\"48x48\"], [\"any\"])\n\n```python\nfrom mcp.types import Icon\n\nicon = Icon(\n    src=\"https://example.com/icon.png\",\n    mimeType=\"image/png\",\n    sizes=[\"48x48\"]\n)\n```\n\n## Server Icons\n\nAdd icons and a website URL to your server for display in client applications:\n\n```python\nfrom fastmcp import FastMCP\nfrom mcp.types import Icon\n\nmcp = FastMCP(\n    name=\"WeatherService\",\n    website_url=\"https://weather.example.com\",\n    icons=[\n        Icon(\n            src=\"https://weather.example.com/icon-48.png\",\n            mimeType=\"image/png\",\n            sizes=[\"48x48\"]\n        ),\n        Icon(\n            src=\"https://weather.example.com/icon-96.png\",\n            mimeType=\"image/png\",\n            sizes=[\"96x96\"]\n        ),\n    ]\n)\n```\n\nServer icons appear in MCP client interfaces to help users identify your server among others they may have installed.\n\n## Component Icons\n\nIcons can be added to individual tools, resources, resource templates, and prompts:\n\n### Tool Icons\n\n```python\nfrom mcp.types import Icon\n\n@mcp.tool(\n    icons=[Icon(src=\"https://example.com/calculator-icon.png\")]\n)\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\n### Resource Icons\n\n```python\n@mcp.resource(\n    \"config://settings\",\n    icons=[Icon(src=\"https://example.com/config-icon.png\")]\n)\ndef get_settings() -> dict:\n    \"\"\"Retrieve application settings.\"\"\"\n    return {\"theme\": \"dark\", \"language\": \"en\"}\n```\n\n### Resource Template Icons\n\n```python\n@mcp.resource(\n    \"user://{user_id}/profile\",\n    icons=[Icon(src=\"https://example.com/user-icon.png\")]\n)\ndef get_user_profile(user_id: str) -> dict:\n    \"\"\"Get a user's profile.\"\"\"\n    return {\"id\": user_id, \"name\": f\"User {user_id}\"}\n```\n\n### Prompt Icons\n\n```python\n@mcp.prompt(\n    icons=[Icon(src=\"https://example.com/prompt-icon.png\")]\n)\ndef analyze_code(code: str):\n    \"\"\"Create a prompt for code analysis.\"\"\"\n    return f\"Please analyze this code:\\n\\n{code}\"\n```\n\n## Using Data URIs\n\nFor small icons or when you want to embed the icon directly, use data URIs:\n\n```python\nfrom mcp.types import Icon\nfrom fastmcp.utilities.types import Image\n\n# SVG icon as data URI\nsvg_icon = Icon(\n    src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+\",\n    mimeType=\"image/svg+xml\"\n)\n\n@mcp.tool(icons=[svg_icon])\ndef my_tool() -> str:\n    \"\"\"A tool with an embedded SVG icon.\"\"\"\n    return \"result\"\n\n# Generating a data URI from a local image file.\nimg = Image(path=\"./assets/brand/favicon.png\")\nicon = Icon(src=img.to_data_uri())\n\n@mcp.tool(icons=[icon])\ndef file_icon_tool() -> str:\n    \"\"\"A tool with an icon generated from a local file.\"\"\"\n    return \"result\"\n```\n"
  },
  {
    "path": "docs/v2/servers/logging.mdx",
    "content": "---\ntitle: Client Logging\nsidebarTitle: Logging\ndescription: Send log messages back to MCP clients through the context.\nicon: receipt\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<Tip>\nThis documentation covers **MCP client logging** - sending messages from your server to MCP clients. For standard server-side logging (e.g., writing to files, console), use `fastmcp.utilities.logging.get_logger()` or Python's built-in `logging` module.\n</Tip>\n\nServer logging allows MCP tools to send debug, info, warning, and error messages back to the client. This provides visibility into function execution and helps with debugging during development and operation.\n\n## Why Use Server Logging?\n\nServer logging is essential for:\n\n- **Debugging**: Send detailed execution information to help diagnose issues\n- **Progress visibility**: Keep users informed about what the tool is doing\n- **Error reporting**: Communicate problems and their context to clients\n- **Audit trails**: Create records of tool execution for compliance or analysis\n\nUnlike standard Python logging, MCP server logging sends messages directly to the client, making them visible in the client's interface or logs.\n\n### Basic Usage\n\nUse the context logging methods within any tool function:\n\n```python {8-9, 13, 17, 21}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"LoggingDemo\")\n\n@mcp.tool\nasync def analyze_data(data: list[float], ctx: Context) -> dict:\n    \"\"\"Analyze numerical data with comprehensive logging.\"\"\"\n    await ctx.debug(\"Starting analysis of numerical data\")\n    await ctx.info(f\"Analyzing {len(data)} data points\")\n    \n    try:\n        if not data:\n            await ctx.warning(\"Empty data list provided\")\n            return {\"error\": \"Empty data list\"}\n        \n        result = sum(data) / len(data)\n        await ctx.info(f\"Analysis complete, average: {result}\")\n        return {\"average\": result, \"count\": len(data)}\n        \n    except Exception as e:\n        await ctx.error(f\"Analysis failed: {str(e)}\")\n        raise\n```\n\n## Structured Logging with `extra`\n\nAll logging methods (`debug`, `info`, `warning`, `error`, `log`) now accept an `extra` parameter, which is a dictionary of arbitrary data. This allows you to send structured data to the client, which is useful for creating rich, queryable logs.\n\n```python\n@mcp.tool\nasync def process_transaction(transaction_id: str, amount: float, ctx: Context):\n    await ctx.info(\n        f\"Processing transaction {transaction_id}\",\n        extra={\n            \"transaction_id\": transaction_id,\n            \"amount\": amount,\n            \"currency\": \"USD\"\n        }\n    )\n    # ... processing logic ...\n```\n\n## Server Logs\n\nClient Logging in the form of `ctx.log()` and its convenience methods (`debug`, `info`, `warning`, `error`) are meant for sending messages to the MCP clients. Messages sent to clients are also logged to the server's log at `DEBUG` level. Enable debug logging on the server or enable debug logging on the `fastmcp.server.context.to_client` logger to see these messages in the server's log.\n\n```python\nimport logging\n\nfrom fastmcp.utilities.logging import get_logger\n\nto_client_logger = get_logger(name=\"fastmcp.server.context.to_client\")\nto_client_logger.setLevel(level=logging.DEBUG)\n```\n\n## Logging Methods\n\n<Card icon=\"code\" title=\"Context Logging Methods\">\n<ResponseField name=\"ctx.debug\" type=\"async method\">\n  Send debug-level messages for detailed execution information\n\n  <Expandable title=\"parameters\">\n    <ResponseField name=\"message\" type=\"str\">\n      The debug message to send to the client\n    </ResponseField>\n    <ResponseField name=\"extra\" type=\"dict | None\" default=\"None\">\n      Optional dictionary for structured logging data\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n\n<ResponseField name=\"ctx.info\" type=\"async method\">\n  Send informational messages about normal execution\n\n  <Expandable title=\"parameters\">\n    <ResponseField name=\"message\" type=\"str\">\n      The information message to send to the client\n    </ResponseField>\n    <ResponseField name=\"extra\" type=\"dict | None\" default=\"None\">\n      Optional dictionary for structured logging data\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n\n<ResponseField name=\"ctx.warning\" type=\"async method\">\n  Send warning messages for potential issues that didn't prevent execution\n\n  <Expandable title=\"parameters\">\n    <ResponseField name=\"message\" type=\"str\">\n      The warning message to send to the client\n    </ResponseField>\n    <ResponseField name=\"extra\" type=\"dict | None\" default=\"None\">\n      Optional dictionary for structured logging data\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n\n<ResponseField name=\"ctx.error\" type=\"async method\">\n  Send error messages for problems that occurred during execution\n\n  <Expandable title=\"parameters\">\n    <ResponseField name=\"message\" type=\"str\">\n      The error message to send to the client\n    </ResponseField>\n    <ResponseField name=\"extra\" type=\"dict | None\" default=\"None\">\n      Optional dictionary for structured logging data\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n\n<ResponseField name=\"ctx.log\" type=\"async method\">\n  Generic logging method with custom level and logger name\n\n  <Expandable title=\"parameters\">\n    <ResponseField name=\"level\" type=\"Literal['debug', 'info', 'warning', 'error']\">\n      The log level for the message\n    </ResponseField>\n\n    <ResponseField name=\"message\" type=\"str\">\n      The message to send to the client\n    </ResponseField>\n\n    <ResponseField name=\"logger_name\" type=\"str | None\" default=\"None\">\n      Optional custom logger name for categorizing messages\n    </ResponseField>\n    <ResponseField name=\"extra\" type=\"dict | None\" default=\"None\">\n      Optional dictionary for structured logging data\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n## Log Levels\n\n### Debug\nUse for detailed information that's typically only useful when diagnosing problems:\n\n```python \n@mcp.tool\nasync def process_file(file_path: str, ctx: Context) -> str:\n    \"\"\"Process a file with detailed debug logging.\"\"\"\n    await ctx.debug(f\"Starting to process file: {file_path}\")\n    await ctx.debug(\"Checking file permissions\")\n    \n    # File processing logic\n    await ctx.debug(\"File processing completed successfully\")\n    return \"File processed\"\n```\n\n### Info\nUse for general information about normal program execution:\n\n```python\n@mcp.tool\nasync def backup_database(ctx: Context) -> str:\n    \"\"\"Backup database with progress information.\"\"\"\n    await ctx.info(\"Starting database backup\")\n    await ctx.info(\"Connecting to database\")\n    await ctx.info(\"Backup completed successfully\")\n    return \"Database backed up\"\n```\n\n### Warning\nUse for potentially harmful situations that don't prevent execution:\n\n```python\n@mcp.tool\nasync def validate_config(config: dict, ctx: Context) -> dict:\n    \"\"\"Validate configuration with warnings for deprecated options.\"\"\"\n    if \"old_api_key\" in config:\n        await ctx.warning(\n            \"Using deprecated 'old_api_key' field. Please use 'api_key' instead\",\n            extra={\"deprecated_field\": \"old_api_key\"}\n        )\n    \n    if config.get(\"timeout\", 30) > 300:\n        await ctx.warning(\n            \"Timeout value is very high (>5 minutes), this may cause issues\",\n            extra={\"timeout_value\": config.get(\"timeout\")}\n        )\n    \n    return {\"status\": \"valid\", \"warnings\": \"see logs\"}\n```\n\n### Error\nUse for error events that might still allow the application to continue:\n\n```python\n@mcp.tool\nasync def batch_process(items: list[str], ctx: Context) -> dict:\n    \"\"\"Process multiple items, logging errors for failed items.\"\"\"\n    successful = 0\n    failed = 0\n    \n    for item in items:\n        try:\n            # Process item\n            successful += 1\n        except Exception as e:\n            await ctx.error(\n                f\"Failed to process item '{item}': {str(e)}\",\n                extra={\"failed_item\": item}\n            )\n            failed += 1\n    \n    return {\"successful\": successful, \"failed\": failed}\n```\n\n\n## Client Handling\n\nLog messages are sent to the client through the MCP protocol. How clients handle these messages depends on their implementation:\n\n- **Development clients**: May display logs in real-time for debugging\n- **Production clients**: May store logs for later analysis or display to users\n- **Integration clients**: May forward logs to external logging systems\n\nSee [Client Logging](/v2/clients/logging) for details on how clients can handle server log messages.\n"
  },
  {
    "path": "docs/v2/servers/middleware.mdx",
    "content": "---\ntitle: MCP Middleware\nsidebarTitle: Middleware\ndescription: Add cross-cutting functionality to your MCP server with middleware that can inspect, modify, and respond to all MCP requests and responses.\nicon: layer-group\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.9.0\" />\n\nMCP middleware is a powerful concept that allows you to add cross-cutting functionality to your FastMCP server. Unlike traditional web middleware, MCP middleware is designed specifically for the Model Context Protocol, providing hooks for different types of MCP operations like tool calls, resource reads, and prompt requests.\n\n<Tip>\nMCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification. This middleware system is designed to work with FastMCP servers and may not be compatible with other MCP implementations.\n</Tip>\n\n<Warning>\nMCP middleware is a brand new concept and may be subject to breaking changes in future versions.\n</Warning>\n\n## What is MCP Middleware?\n\nMCP middleware lets you intercept and modify MCP requests and responses as they flow through your server. Think of it as a pipeline where each piece of middleware can inspect what's happening, make changes, and then pass control to the next middleware in the chain.\n\nCommon use cases for MCP middleware include:\n- **Authentication and Authorization**: Verify client permissions before executing operations\n- **Logging and Monitoring**: Track usage patterns and performance metrics\n- **Rate Limiting**: Control request frequency per client or operation type\n- **Request/Response Transformation**: Modify data before it reaches tools or after it leaves\n- **Caching**: Store frequently requested data to improve performance\n- **Error Handling**: Provide consistent error responses across your server\n\n## How Middleware Works\n\nFastMCP middleware operates on a pipeline model. When a request comes in, it flows through your middleware in the order they were added to the server. Each middleware can:\n\n1. **Inspect the incoming request** and its context\n2. **Modify the request** before passing it to the next middleware or handler\n3. **Execute the next middleware/handler** in the chain by calling `call_next()`\n4. **Inspect and modify the response** before returning it\n5. **Handle errors** that occur during processing\n\nThe key insight is that middleware forms a chain where each piece decides whether to continue processing or stop the chain entirely.\n\nIf you're familiar with ASGI middleware, the basic structure of FastMCP middleware will feel familiar. At its core, middleware is a callable class that receives a context object containing information about the current JSON-RPC message and a handler function to continue the middleware chain.\n\nIt's important to understand that MCP operates on the [JSON-RPC specification](https://spec.modelcontextprotocol.io/specification/basic/transports/). While FastMCP presents requests and responses in a familiar way, these are fundamentally JSON-RPC messages, not HTTP request/response pairs like you might be used to in web applications. FastMCP middleware works with all [transport types](/v2/clients/transports), including local stdio transport and HTTP transports, though not all middleware implementations are compatible across all transports (e.g., middleware that inspects HTTP headers won't work with stdio transport).\n\nThe most fundamental way to implement middleware is by overriding the `__call__` method on the `Middleware` base class:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass RawMiddleware(Middleware):\n    async def __call__(self, context: MiddlewareContext, call_next):\n        # This method receives ALL messages regardless of type\n        print(f\"Raw middleware processing: {context.method}\")\n        result = await call_next(context)\n        print(f\"Raw middleware completed: {context.method}\")\n        return result\n```\n\nThis gives you complete control over every message that flows through your server, but requires you to handle all message types manually.\n\n## Middleware Hooks\n\nTo make it easier for users to target specific types of messages, FastMCP middleware provides a variety of specialized hooks. Instead of implementing the raw `__call__` method, you can override specific hook methods that are called only for certain types of operations, allowing you to target exactly the level of specificity you need for your middleware logic.\n\n### Hook Hierarchy and Execution Order\n\nFastMCP provides multiple hooks that are called with varying levels of specificity. Understanding this hierarchy is crucial for effective middleware design.\n\nWhen a request comes in, **multiple hooks may be called for the same request**, going from general to specific:\n\n1. **`on_message`** - Called for ALL MCP messages (both requests and notifications)\n2. **`on_request` or `on_notification`** - Called based on the message type\n3. **Operation-specific hooks** - Called for specific MCP operations like `on_call_tool`\n\nFor example, when a client calls a tool, your middleware will receive **multiple hook calls**:\n1. `on_message` and `on_request` for any initial tool discovery operations (list_tools)\n2. `on_message` (because it's any MCP message) for the tool call itself\n3. `on_request` (because tool calls expect responses) for the tool call itself\n4. `on_call_tool` (because it's specifically a tool execution) for the tool call itself\n\nNote that the MCP SDK may perform additional operations like listing tools for caching purposes, which will trigger additional middleware calls beyond just the direct tool execution.\n\nThis hierarchy allows you to target your middleware logic with the right level of specificity. Use `on_message` for broad concerns like logging, `on_request` for authentication, and `on_call_tool` for tool-specific logic like performance monitoring.\n\n### Available Hooks\n<VersionBadge version=\"2.9.0\" />\n\n- `on_message`: Called for all MCP messages (requests and notifications)\n- `on_request`: Called specifically for MCP requests (that expect responses)\n- `on_notification`: Called specifically for MCP notifications (fire-and-forget)\n\n- `on_call_tool`: Called when tools are being executed\n- `on_read_resource`: Called when resources are being read\n- `on_get_prompt`: Called when prompts are being retrieved\n- `on_list_tools`: Called when listing available tools\n- `on_list_resources`: Called when listing available resources\n- `on_list_resource_templates`: Called when listing resource templates\n- `on_list_prompts`: Called when listing available prompts\n<VersionBadge version=\"2.13.0\" />\n- `on_initialize`: Called when a client connects and initializes the session (returns `None`)\n<Note>\nThe `on_initialize` hook receives the client's initialization request but **returns `None`** rather than a result. The initialization response is handled internally by the MCP protocol and cannot be modified by middleware. This hook is useful for client detection, logging connections, or initializing session state, but not for modifying the initialization handshake itself.\n</Note>\n\n**Example:**\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom mcp import McpError\nfrom mcp.types import ErrorData\n\nclass InitializationMiddleware(Middleware):\n    async def on_initialize(self, context: MiddlewareContext, call_next):\n        # Check client capabilities before initialization\n        client_info = context.message.params.get(\"clientInfo\", {})\n        client_name = client_info.get(\"name\", \"unknown\")\n\n        # Reject unsupported clients BEFORE call_next\n        if client_name == \"unsupported-client\":\n            raise McpError(ErrorData(code=-32000, message=\"This client is not supported\"))\n\n        # Log successful initialization\n        await call_next(context)\n        print(f\"Client {client_name} initialized successfully\")\n```\n\n<Warning>\nIf you raise `McpError` in `on_initialize` **after** calling `call_next()`, the error will only be logged and will not be sent to the client. The initialization response has already been sent at that point. Always raise `McpError` **before** `call_next()` if you want to reject the initialization.\n</Warning>\n\n### MCP Session Availability in Middleware\n\n<VersionBadge version=\"2.13.1\" />\n\nThe MCP session and request context are not available during certain phases like initialization. When middleware runs during these phases, `context.fastmcp_context.request_context` returns `None` rather than the full MCP request context.\n\nThis typically occurs when:\n- The `on_request` hook fires during client initialization\n- The MCP handshake hasn't completed yet\n\nTo handle this in middleware, check if the MCP request context is available before accessing MCP-specific attributes. Note that the MCP request context is distinct from the HTTP request - for HTTP transports, you can use HTTP helpers to access request data even when the MCP session is not available:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass SessionAwareMiddleware(Middleware):\n    async def on_request(self, context: MiddlewareContext, call_next):\n        ctx = context.fastmcp_context\n\n        if ctx.request_context:\n            # MCP session available - can access session-specific attributes\n            session_id = ctx.session_id\n            request_id = ctx.request_id\n        else:\n            # MCP session not available yet - use HTTP helpers for request data (if using HTTP transport)\n            from fastmcp.server.dependencies import get_http_headers\n            headers = get_http_headers()\n            # Access HTTP data for auth, logging, etc.\n\n        return await call_next(context)\n```\n\nFor HTTP request data (headers, client IP, etc.) when using HTTP transports, use `get_http_request()` or `get_http_headers()` from `fastmcp.server.dependencies`, which work regardless of MCP session availability. See [HTTP Requests](/v2/servers/context#http-requests) for details.\n\n## Component Access in Middleware\n\nUnderstanding how to access component information (tools, resources, prompts) in middleware is crucial for building powerful middleware functionality. The access patterns differ significantly between listing operations and execution operations.\n\n### Listing Operations vs Execution Operations\n\nFastMCP middleware handles two types of operations differently:\n\n**Listing Operations** (`on_list_tools`, `on_list_resources`, `on_list_prompts`, etc.):\n- Middleware receives **FastMCP component objects** with full metadata\n- These objects include FastMCP-specific properties like `tags` that can be accessed directly from the component\n- The result contains complete component information before it's converted to MCP format  \n- Tags are included in the component's `meta` field in the listing response returned to MCP clients\n\n**Execution Operations** (`on_call_tool`, `on_read_resource`, `on_get_prompt`):\n- Middleware runs **before** the component is executed\n- The middleware result is either the execution result or an error if the component wasn't found\n- Component metadata isn't directly available in the hook parameters\n\n### Accessing Component Metadata During Execution\n\nIf you need to check component properties (like tags) during execution operations, use the FastMCP server instance available through the context:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.exceptions import ToolError\n\nclass TagBasedMiddleware(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        # Access the tool object to check its metadata\n        if context.fastmcp_context:\n            try:\n                tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)\n                \n                # Check if this tool has a \"private\" tag\n                if \"private\" in tool.tags:\n                    raise ToolError(\"Access denied: private tool\")\n                    \n                # Check if tool is enabled\n                if not tool.enabled:\n                    raise ToolError(\"Tool is currently disabled\")\n                    \n            except Exception:\n                # Tool not found or other error - let execution continue\n                # and handle the error naturally\n                pass\n        \n        return await call_next(context)\n```\n\nThe same pattern works for resources and prompts:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.exceptions import ResourceError, PromptError\n\nclass ComponentAccessMiddleware(Middleware):\n    async def on_read_resource(self, context: MiddlewareContext, call_next):\n        if context.fastmcp_context:\n            try:\n                resource = await context.fastmcp_context.fastmcp.get_resource(context.message.uri)\n                if \"restricted\" in resource.tags:\n                    raise ResourceError(\"Access denied: restricted resource\")\n            except Exception:\n                pass\n        return await call_next(context)\n    \n    async def on_get_prompt(self, context: MiddlewareContext, call_next):\n        if context.fastmcp_context:\n            try:\n                prompt = await context.fastmcp_context.fastmcp.get_prompt(context.message.name)\n                if not prompt.enabled:\n                    raise PromptError(\"Prompt is currently disabled\")\n            except Exception:\n                pass\n        return await call_next(context)\n```\n\n### Working with Listing Results\n\nFor listing operations, the middleware `call_next` function returns a list of FastMCP components prior to being converted to MCP format. You can filter or modify this list and return it to the client. For example:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass ListingFilterMiddleware(Middleware):\n    async def on_list_tools(self, context: MiddlewareContext, call_next):\n        result = await call_next(context)\n        \n        # Filter out tools with \"private\" tag\n        filtered_tools = [\n            tool for tool in result \n            if \"private\" not in tool.tags\n        ]\n        \n        # Return modified list\n        return filtered_tools\n```\n\nThis filtering happens before the components are converted to MCP format and returned to the client. Tags are accessible both during filtering and are included in the component's `meta` field in the final listing response.\n\n<Tip>\nWhen filtering components in listing operations, ensure you also prevent execution of filtered components in the corresponding execution hooks (`on_call_tool`, `on_read_resource`, `on_get_prompt`) to maintain consistency.\n</Tip>\n\n### Tool Call Denial\n\nYou can deny access to specific tools by raising a `ToolError` in your middleware. This is the correct way to block tool execution, as it integrates properly with the FastMCP error handling system.\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.exceptions import ToolError\n\nclass AuthMiddleware(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        tool_name = context.message.name\n        \n        # Deny access to restricted tools\n        if tool_name.lower() in [\"delete\", \"admin_config\"]:\n            raise ToolError(\"Access denied: tool requires admin privileges\")\n        \n        # Allow other tools to proceed\n        return await call_next(context)\n```\n\n<Warning>\nWhen denying tool calls, always raise `ToolError` rather than returning `ToolResult` objects or other values. `ToolError` ensures proper error propagation through the middleware chain and converts to the correct MCP error response format.\n</Warning>\n\n### Tool Call Modification\n\nFor execution operations like tool calls, you can modify arguments before execution or transform results afterward:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass ToolCallMiddleware(Middleware):\n    async def on_call_tool(self, context: MiddlewareContext, call_next):\n        # Modify arguments before execution\n        if context.message.name == \"calculate\":\n            # Ensure positive inputs\n            if context.message.arguments.get(\"value\", 0) < 0:\n                context.message.arguments[\"value\"] = abs(context.message.arguments[\"value\"])\n        \n        result = await call_next(context)\n        \n        # Transform result after execution\n        if context.message.name == \"get_data\":\n            # Add metadata to result\n            if result.structured_content:\n                result.structured_content[\"processed_at\"] = \"2024-01-01T00:00:00Z\"\n        \n        return result\n```\n\n<Tip>\nFor more complex tool rewriting scenarios, consider using [Tool Transformation](/v2/patterns/tool-transformation) patterns which provide a more structured approach to creating modified tool variants.\n</Tip>\n\n### Anatomy of a Hook\n\nEvery middleware hook follows the same pattern. Let's examine the `on_message` hook to understand the structure:\n\n```python\nasync def on_message(self, context: MiddlewareContext, call_next):\n    # 1. Pre-processing: Inspect and optionally modify the request\n    print(f\"Processing {context.method}\")\n    \n    # 2. Chain continuation: Call the next middleware/handler\n    result = await call_next(context)\n    \n    # 3. Post-processing: Inspect and optionally modify the response\n    print(f\"Completed {context.method}\")\n    \n    # 4. Return the result (potentially modified)\n    return result\n```\n\n### Hook Parameters\n\nEvery hook receives two parameters:\n\n1. **`context: MiddlewareContext`** - Contains information about the current request:\n   - `context.method` - The MCP method name (e.g., \"tools/call\")\n   - `context.source` - Where the request came from (\"client\" or \"server\")\n   - `context.type` - Message type (\"request\" or \"notification\")\n   - `context.message` - The MCP message data\n   - `context.timestamp` - When the request was received\n   - `context.fastmcp_context` - FastMCP Context object (if available)\n\n2. **`call_next`** - A function that continues the middleware chain. You **must** call this to proceed, unless you want to stop processing entirely.\n\n### Control Flow\n\nYou have complete control over the request flow:\n- **Continue processing**: Call `await call_next(context)` to proceed\n- **Modify the request**: Change the context before calling `call_next`\n- **Modify the response**: Change the result after calling `call_next`\n- **Stop the chain**: Don't call `call_next` (rarely needed)\n- **Handle errors**: Wrap `call_next` in try/catch blocks\n\n#### State Management\n\n<VersionBadge version=\"2.11.0\" />\n\nIn addition to modifying the request and response, you can also store state data that your tools can (optionally) access later. To do so, use the FastMCP Context to either `set_state` or `get_state` as appropriate. For more information, see the [Context State Management](/v2/servers/context#state-management) docs.\n\n## Creating Middleware\n\nFastMCP middleware is implemented by subclassing the `Middleware` base class and overriding the hooks you need. You only need to implement the hooks that are relevant to your use case.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass LoggingMiddleware(Middleware):\n    \"\"\"Middleware that logs all MCP operations.\"\"\"\n    \n    async def on_message(self, context: MiddlewareContext, call_next):\n        \"\"\"Called for all MCP messages.\"\"\"\n        print(f\"Processing {context.method} from {context.source}\")\n        \n        result = await call_next(context)\n        \n        print(f\"Completed {context.method}\")\n        return result\n\n# Add middleware to your server\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(LoggingMiddleware())\n```\n\nThis creates a basic logging middleware that will print information about every request that flows through your server.\n\n## Adding Middleware to Your Server\n\n### Single Middleware\n\nAdding middleware to your server is straightforward:\n\n```python\nmcp = FastMCP(\"MyServer\")\nmcp.add_middleware(LoggingMiddleware())\n```\n\n### Multiple Middleware\n\nMiddleware executes in the order it's added to the server. The first middleware added runs first on the way in, and last on the way out:\n\n```python\nmcp = FastMCP(\"MyServer\")\n\nmcp.add_middleware(AuthenticationMiddleware(\"secret-token\"))\nmcp.add_middleware(PerformanceMiddleware())\nmcp.add_middleware(LoggingMiddleware())\n```\n\nThis creates the following execution flow:\n1. AuthenticationMiddleware (pre-processing)\n2. PerformanceMiddleware (pre-processing)  \n3. LoggingMiddleware (pre-processing)\n4. Actual tool/resource handler\n5. LoggingMiddleware (post-processing)\n6. PerformanceMiddleware (post-processing)\n7. AuthenticationMiddleware (post-processing)\n\n## Server Composition and Middleware\n\nWhen using [Server Composition](/v2/servers/composition) with `mount` or `import_server`, middleware behavior follows these rules:\n\n1. **Parent server middleware** runs for all requests, including those routed to mounted servers\n2. **Mounted server middleware** only runs for requests handled by that specific server\n3. **Middleware order** is preserved within each server\n\nThis allows you to create layered middleware architectures where parent servers handle cross-cutting concerns like authentication, while child servers focus on domain-specific middleware.\n\n```python\n# Parent server with middleware\nparent = FastMCP(\"Parent\")\nparent.add_middleware(AuthenticationMiddleware(\"token\"))\n\n# Child server with its own middleware  \nchild = FastMCP(\"Child\")\nchild.add_middleware(LoggingMiddleware())\n\n@child.tool\ndef child_tool() -> str:\n    return \"from child\"\n\n# Mount the child server\nparent.mount(child, prefix=\"child\")\n```\n\nWhen a client calls \"child_tool\", the request will flow through the parent's authentication middleware first, then route to the child server where it will go through the child's logging middleware.\n\n## Built-in Middleware Examples\n\nFastMCP includes several middleware implementations that demonstrate best practices and provide immediately useful functionality. Let's explore how each type works by building simplified versions, then see how to use the full implementations.\n\n### Timing Middleware\n\nPerformance monitoring is essential for understanding your server's behavior and identifying bottlenecks. FastMCP includes timing middleware at `fastmcp.server.middleware.timing`. \n\nHere's an example of how it works:\n\n```python\nimport time\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass SimpleTimingMiddleware(Middleware):\n    async def on_request(self, context: MiddlewareContext, call_next):\n        start_time = time.perf_counter()\n        \n        try:\n            result = await call_next(context)\n            duration_ms = (time.perf_counter() - start_time) * 1000\n            print(f\"Request {context.method} completed in {duration_ms:.2f}ms\")\n            return result\n        except Exception as e:\n            duration_ms = (time.perf_counter() - start_time) * 1000\n            print(f\"Request {context.method} failed after {duration_ms:.2f}ms: {e}\")\n            raise\n```\n\nTo use the full version with proper logging and configuration:\n\n```python\nfrom fastmcp.server.middleware.timing import (\n    TimingMiddleware, \n    DetailedTimingMiddleware\n)\n\n# Basic timing for all requests\nmcp.add_middleware(TimingMiddleware())\n\n# Detailed per-operation timing (tools, resources, prompts)\nmcp.add_middleware(DetailedTimingMiddleware())\n```\n\nThe built-in versions include custom logger support, proper formatting, and **DetailedTimingMiddleware** provides operation-specific hooks like `on_call_tool` and `on_read_resource` for granular timing.\n\n### Tool Injection Middleware\n\nTool injection middleware is a middleware that injects tools into the server during the request lifecycle:\n\n```python\nfrom fastmcp.server.middleware.tool_injection import ToolInjectionMiddleware\n\ndef my_tool_fn(a: int, b: int) -> int:\n    return a + b\n\nmy_tool = Tool.from_function(fn=my_tool_fn, name=\"my_tool\")\n\nmcp.add_middleware(ToolInjectionMiddleware(tools=[my_tool]))\n```\n\n### Prompt Tool Middleware\n\nPrompt tool middleware is a compatibility middleware for clients that are unable to list or get prompts. It provides two tools: `list_prompts` and `get_prompt` which allow clients to list and get prompts respectively using only tool calls.\n\n```python\nfrom fastmcp.server.middleware.tool_injection import PromptToolMiddleware\n\nmcp.add_middleware(PromptToolMiddleware())\n```\n\n### Resource Tool Middleware\n\nResource tool middleware is a compatibility middleware for clients that are unable to list or read resources. It provides two tools: `list_resources` and `read_resource` which allow clients to list and read resources respectively using only tool calls.\n\n```python\nfrom fastmcp.server.middleware.tool_injection import ResourceToolMiddleware\n\nmcp.add_middleware(ResourceToolMiddleware())\n```\n\n### Caching Middleware\n\nCaching middleware is essential for improving performance and reducing server load. FastMCP provides caching middleware at `fastmcp.server.middleware.caching`.\n\nHere's how to use the full version:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\nmcp.add_middleware(ResponseCachingMiddleware())\n```\n\nOut of the box, it caches call/list tool, resources, and prompts to an in-memory cache with TTL-based expiration. Cache entries expire based on their TTL; there is no event-based cache invalidation. List calls are stored under global keys—when sharing a storage backend across multiple servers, consider namespacing collections to prevent conflicts. See [Storage Backends](/v2/servers/storage-backends) for advanced configuration options.\n\nEach method can be configured individually, for example, caching list tools for 30 seconds, limiting caching to specific tools, and disabling caching for resource reads:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware, CallToolSettings, ListToolsSettings, ReadResourceSettings\n\nmcp.add_middleware(ResponseCachingMiddleware(\n    list_tools_settings=ListToolsSettings(\n        ttl=30,\n    ),\n    call_tool_settings=CallToolSettings(\n        included_tools=[\"tool1\"],\n    ),\n    read_resource_settings=ReadResourceSettings(\n        enabled=False\n    )\n))\n```\n\n#### Storage Backends\n\nBy default, caching uses in-memory storage, which is fast but doesn't persist across restarts. For production or persistent caching across server restarts, configure a different storage backend. See [Storage Backends](/v2/servers/storage-backends) for complete options including disk, Redis, DynamoDB, and custom implementations.\n\nDisk-based caching example:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.disk import DiskStore\n\nmcp.add_middleware(ResponseCachingMiddleware(\n    cache_storage=DiskStore(directory=\"cache\"),\n))\n```\n\nRedis for distributed deployments:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.redis import RedisStore\n\nmcp.add_middleware(ResponseCachingMiddleware(\n    cache_storage=RedisStore(host=\"redis.example.com\", port=6379),\n))\n```\n\n#### Cache Statistics\n\nThe caching middleware collects operation statistics (hits, misses, etc.) through the underlying storage layer. Access statistics from the middleware instance:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\nmiddleware = ResponseCachingMiddleware()\nmcp.add_middleware(middleware)\n\n# Later, retrieve statistics\nstats = middleware.statistics()\nprint(f\"Total cache operations: {stats}\")\n```\n\n### Logging Middleware\n\nRequest and response logging is crucial for debugging, monitoring, and understanding usage patterns in your MCP server. FastMCP provides comprehensive logging middleware at `fastmcp.server.middleware.logging`. \n\nHere's an example of how it works:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass SimpleLoggingMiddleware(Middleware):\n    async def on_message(self, context: MiddlewareContext, call_next):\n        print(f\"Processing {context.method} from {context.source}\")\n        \n        try:\n            result = await call_next(context)\n            print(f\"Completed {context.method}\")\n            return result\n        except Exception as e:\n            print(f\"Failed {context.method}: {e}\")\n            raise\n```\n\nTo use the full versions with advanced features:\n\n```python\nfrom fastmcp.server.middleware.logging import (\n    LoggingMiddleware, \n    StructuredLoggingMiddleware\n)\n\n# Human-readable logging with payload support\nmcp.add_middleware(LoggingMiddleware(\n    include_payloads=True,\n    max_payload_length=1000\n))\n\n# JSON-structured logging for log aggregation tools\nmcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True))\n```\n\nThe built-in versions include payload logging, structured JSON output, custom logger support, payload size limits, and operation-specific hooks for granular control.\n\n### Rate Limiting Middleware\n\nRate limiting is essential for protecting your server from abuse, ensuring fair resource usage, and maintaining performance under load. FastMCP includes sophisticated rate limiting middleware at `fastmcp.server.middleware.rate_limiting`. \n\nHere's an example of how it works:\n\n```python\nimport time\nfrom collections import defaultdict\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom mcp import McpError\nfrom mcp.types import ErrorData\n\nclass SimpleRateLimitMiddleware(Middleware):\n    def __init__(self, requests_per_minute: int = 60):\n        self.requests_per_minute = requests_per_minute\n        self.client_requests = defaultdict(list)\n    \n    async def on_request(self, context: MiddlewareContext, call_next):\n        current_time = time.time()\n        client_id = \"default\"  # In practice, extract from headers or context\n        \n        # Clean old requests and check limit\n        cutoff_time = current_time - 60\n        self.client_requests[client_id] = [\n            req_time for req_time in self.client_requests[client_id]\n            if req_time > cutoff_time\n        ]\n        \n        if len(self.client_requests[client_id]) >= self.requests_per_minute:\n            raise McpError(ErrorData(code=-32000, message=\"Rate limit exceeded\"))\n        \n        self.client_requests[client_id].append(current_time)\n        return await call_next(context)\n```\n\nTo use the full versions with advanced algorithms:\n\n```python\nfrom fastmcp.server.middleware.rate_limiting import (\n    RateLimitingMiddleware, \n    SlidingWindowRateLimitingMiddleware\n)\n\n# Token bucket rate limiting (allows controlled bursts)\nmcp.add_middleware(RateLimitingMiddleware(\n    max_requests_per_second=10.0,\n    burst_capacity=20\n))\n\n# Sliding window rate limiting (precise time-based control)\nmcp.add_middleware(SlidingWindowRateLimitingMiddleware(\n    max_requests=100,\n    window_minutes=1\n))\n```\n\nThe built-in versions include token bucket algorithms, per-client identification, global rate limiting, and async-safe implementations with configurable client identification functions.\n\n### Error Handling Middleware\n\nConsistent error handling and recovery is critical for robust MCP servers. FastMCP provides comprehensive error handling middleware at `fastmcp.server.middleware.error_handling`.\n\nHere's an example of how it works:\n\n```python\nimport logging\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass SimpleErrorHandlingMiddleware(Middleware):\n    def __init__(self):\n        self.logger = logging.getLogger(\"errors\")\n        self.error_counts = {}\n    \n    async def on_message(self, context: MiddlewareContext, call_next):\n        try:\n            return await call_next(context)\n        except Exception as error:\n            # Log the error and track statistics\n            error_key = f\"{type(error).__name__}:{context.method}\"\n            self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1\n            \n            self.logger.error(f\"Error in {context.method}: {type(error).__name__}: {error}\")\n            raise\n```\n\nTo use the full versions with advanced features:\n\n```python\nfrom fastmcp.server.middleware.error_handling import (\n    ErrorHandlingMiddleware, \n    RetryMiddleware\n)\n\n# Comprehensive error logging and transformation\nmcp.add_middleware(ErrorHandlingMiddleware(\n    include_traceback=True,\n    transform_errors=True,\n    error_callback=my_error_callback\n))\n\n# Automatic retry with exponential backoff\nmcp.add_middleware(RetryMiddleware(\n    max_retries=3,\n    retry_exceptions=(ConnectionError, TimeoutError)\n))\n```\n\nThe built-in versions include error transformation, custom callbacks, configurable retry logic, and proper MCP error formatting.\n\n### Combining Middleware\n\nThese middleware work together seamlessly:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.timing import TimingMiddleware\nfrom fastmcp.server.middleware.logging import LoggingMiddleware\nfrom fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware\nfrom fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware\n\nmcp = FastMCP(\"Production Server\")\n\n# Add middleware in logical order\nmcp.add_middleware(ErrorHandlingMiddleware())  # Handle errors first\nmcp.add_middleware(RateLimitingMiddleware(max_requests_per_second=50))\nmcp.add_middleware(TimingMiddleware())  # Time actual execution\nmcp.add_middleware(LoggingMiddleware())  # Log everything\n\n@mcp.tool\ndef my_tool(data: str) -> str:\n    return f\"Processed: {data}\"\n```\n\nThis configuration provides comprehensive monitoring, protection, and observability for your MCP server.\n\n### Custom Middleware Example\n\nYou can also create custom middleware by extending the base class:\n\n```python\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\n\nclass CustomHeaderMiddleware(Middleware):\n    async def on_request(self, context: MiddlewareContext, call_next):\n        # Add custom logic here\n        print(f\"Processing {context.method}\")\n        \n        result = await call_next(context)\n        \n        print(f\"Completed {context.method}\")\n        return result\n\nmcp.add_middleware(CustomHeaderMiddleware())\n```\n"
  },
  {
    "path": "docs/v2/servers/progress.mdx",
    "content": "---\ntitle: Progress Reporting\nsidebarTitle: Progress\ndescription: Update clients on the progress of long-running operations through the MCP context.\nicon: chart-line\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nProgress reporting allows MCP tools to notify clients about the progress of long-running operations. This enables clients to display progress indicators and provide better user experience during time-consuming tasks.\n\n## Why Use Progress Reporting?\n\nProgress reporting is valuable for:\n\n- **User experience**: Keep users informed about long-running operations\n- **Progress indicators**: Enable clients to show progress bars or percentages\n- **Timeout prevention**: Demonstrate that operations are actively progressing\n- **Debugging**: Track execution progress for performance analysis\n\n### Basic Usage\n\nUse `ctx.report_progress()` to send progress updates to the client:\n\n```python {14, 21}\nfrom fastmcp import FastMCP, Context\nimport asyncio\n\nmcp = FastMCP(\"ProgressDemo\")\n\n@mcp.tool\nasync def process_items(items: list[str], ctx: Context) -> dict:\n    \"\"\"Process a list of items with progress updates.\"\"\"\n    total = len(items)\n    results = []\n    \n    for i, item in enumerate(items):\n        # Report progress as we process each item\n        await ctx.report_progress(progress=i, total=total)\n        \n        # Simulate processing time\n        await asyncio.sleep(0.1)\n        results.append(item.upper())\n    \n    # Report 100% completion\n    await ctx.report_progress(progress=total, total=total)\n    \n    return {\"processed\": len(results), \"results\": results}\n```\n\n## Method Signature\n\n<Card icon=\"code\" title=\"Context Progress Method\">\n<ResponseField name=\"ctx.report_progress\" type=\"async method\">\n  Report progress to the client for long-running operations\n  \n  <Expandable title=\"Parameters\">\n    <ResponseField name=\"progress\" type=\"float\">\n      Current progress value (e.g., 24, 0.75, 1500)\n    </ResponseField>\n    \n    <ResponseField name=\"total\" type=\"float | None\" default=\"None\">\n      Optional total value (e.g., 100, 1.0, 2000). When provided, clients may interpret this as enabling percentage calculation.\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n## Progress Patterns\n\n### Percentage-Based Progress\n\nReport progress as a percentage (0-100):\n\n```python {13-14}\n@mcp.tool\nasync def download_file(url: str, ctx: Context) -> str:\n    \"\"\"Download a file with percentage progress.\"\"\"\n    total_size = 1000  # KB\n    downloaded = 0\n    \n    while downloaded < total_size:\n        # Download chunk\n        chunk_size = min(50, total_size - downloaded)\n        downloaded += chunk_size\n        \n        # Report percentage progress\n        percentage = (downloaded / total_size) * 100\n        await ctx.report_progress(progress=percentage, total=100)\n        \n        await asyncio.sleep(0.1)  # Simulate download time\n    \n    return f\"Downloaded file from {url}\"\n```\n\n### Absolute Progress\n\nReport progress with absolute values:\n\n```python {10}\n@mcp.tool\nasync def backup_database(ctx: Context) -> str:\n    \"\"\"Backup database tables with absolute progress.\"\"\"\n    tables = [\"users\", \"orders\", \"products\", \"inventory\", \"logs\"]\n    \n    for i, table in enumerate(tables):\n        await ctx.info(f\"Backing up table: {table}\")\n        \n        # Report absolute progress\n        await ctx.report_progress(progress=i + 1, total=len(tables))\n        \n        # Simulate backup time\n        await asyncio.sleep(0.5)\n    \n    return \"Database backup completed\"\n```\n\n### Indeterminate Progress\n\nReport progress without a known total for operations where the endpoint is unknown:\n\n```python {11}\n@mcp.tool\nasync def scan_directory(directory: str, ctx: Context) -> dict:\n    \"\"\"Scan directory with indeterminate progress.\"\"\"\n    files_found = 0\n    \n    # Simulate directory scanning\n    for i in range(10):  # Unknown number of files\n        files_found += 1\n        \n        # Report progress without total for indeterminate operations\n        await ctx.report_progress(progress=files_found)\n        \n        await asyncio.sleep(0.2)\n    \n    return {\"files_found\": files_found, \"directory\": directory}\n```\n\n### Multi-Stage Operations\n\nBreak complex operations into stages with progress for each:\n\n```python\n@mcp.tool\nasync def data_migration(source: str, destination: str, ctx: Context) -> str:\n    \"\"\"Migrate data with multi-stage progress reporting.\"\"\"\n    \n    # Stage 1: Validation (0-25%)\n    await ctx.info(\"Validating source data\")\n    for i in range(5):\n        await ctx.report_progress(progress=i * 5, total=100)\n        await asyncio.sleep(0.1)\n    \n    # Stage 2: Export (25-60%)\n    await ctx.info(\"Exporting data from source\")\n    for i in range(7):\n        progress = 25 + (i * 5)\n        await ctx.report_progress(progress=progress, total=100)\n        await asyncio.sleep(0.1)\n    \n    # Stage 3: Transform (60-80%)\n    await ctx.info(\"Transforming data format\")\n    for i in range(4):\n        progress = 60 + (i * 5)\n        await ctx.report_progress(progress=progress, total=100)\n        await asyncio.sleep(0.1)\n    \n    # Stage 4: Import (80-100%)\n    await ctx.info(\"Importing to destination\")\n    for i in range(4):\n        progress = 80 + (i * 5)\n        await ctx.report_progress(progress=progress, total=100)\n        await asyncio.sleep(0.1)\n    \n    # Final completion\n    await ctx.report_progress(progress=100, total=100)\n    \n    return f\"Migration from {source} to {destination} completed\"\n```\n\n\n## Client Requirements\n\nProgress reporting requires clients to support progress handling:\n\n- Clients must send a `progressToken` in the initial request to receive progress updates\n- If no progress token is provided, progress calls will have no effect (they won't error)\n- See [Client Progress](/v2/clients/progress) for details on implementing client-side progress handling\n"
  },
  {
    "path": "docs/v2/servers/prompts.mdx",
    "content": "---\ntitle: Prompts\nsidebarTitle: Prompts\ndescription: Create reusable, parameterized prompt templates for MCP clients.\nicon: message-lines\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nPrompts are reusable message templates that help LLMs generate structured, purposeful responses. FastMCP simplifies defining these templates, primarily using the `@mcp.prompt` decorator.\n\n## What Are Prompts?\n\nPrompts provide parameterized message templates for LLMs. When a client requests a prompt:\n\n1.  FastMCP finds the corresponding prompt definition.\n2.  If it has parameters, they are validated against your function signature.\n3.  Your function executes with the validated inputs.\n4.  The generated message(s) are returned to the LLM to guide its response.\n\nThis allows you to define consistent, reusable templates that LLMs can use across different clients and contexts.\n\n## Prompts\n\n### The `@prompt` Decorator\n\nThe most common way to define a prompt is by decorating a Python function. The decorator uses the function name as the prompt's identifier.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.prompts.prompt import Message, PromptMessage, TextContent\n\nmcp = FastMCP(name=\"PromptServer\")\n\n# Basic prompt returning a string (converted to user message automatically)\n@mcp.prompt\ndef ask_about_topic(topic: str) -> str:\n    \"\"\"Generates a user message asking for an explanation of a topic.\"\"\"\n    return f\"Can you please explain the concept of '{topic}'?\"\n\n# Prompt returning a specific message type\n@mcp.prompt\ndef generate_code_request(language: str, task_description: str) -> PromptMessage:\n    \"\"\"Generates a user message requesting code generation.\"\"\"\n    content = f\"Write a {language} function that performs the following task: {task_description}\"\n    return PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=content))\n```\n\n**Key Concepts:**\n\n*   **Name:** By default, the prompt name is taken from the function name.\n*   **Parameters:** The function parameters define the inputs needed to generate the prompt.\n*   **Inferred Metadata:** By default:\n    *   Prompt Name: Taken from the function name (`ask_about_topic`).\n    *   Prompt Description: Taken from the function's docstring.\n<Tip>\nFunctions 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.\n</Tip>\n\n#### Decorator Arguments\n\nWhile FastMCP infers the name and description from your function, you can override these and add additional metadata using arguments to the `@mcp.prompt` decorator:\n\n```python\n@mcp.prompt(\n    name=\"analyze_data_request\",          # Custom prompt name\n    description=\"Creates a request to analyze data with specific parameters\",  # Custom description\n    tags={\"analysis\", \"data\"},            # Optional categorization tags\n    meta={\"version\": \"1.1\", \"author\": \"data-team\"}  # Custom metadata\n)\ndef data_analysis_prompt(\n    data_uri: str = Field(description=\"The URI of the resource containing the data.\"),\n    analysis_type: str = Field(default=\"summary\", description=\"Type of analysis.\")\n) -> str:\n    \"\"\"This docstring is ignored when description is provided.\"\"\"\n    return f\"Please perform a '{analysis_type}' analysis on the data found at {data_uri}.\"\n```\n\n<Card icon=\"code\" title=\"@prompt Decorator Arguments\">\n<ParamField body=\"name\" type=\"str | None\">\n  Sets the explicit prompt name exposed via MCP. If not provided, uses the function name\n</ParamField>\n\n<ParamField body=\"title\" type=\"str | None\">\n  A human-readable title for the prompt\n</ParamField>\n\n<ParamField body=\"description\" type=\"str | None\">\n  Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose\n</ParamField>\n\n<ParamField body=\"tags\" type=\"set[str] | None\">\n  A set of strings used to categorize the prompt. These can be used by the server and, in some cases, by clients to filter or group available prompts.\n</ParamField>\n\n<ParamField body=\"enabled\" type=\"bool\" default=\"True\">\n  A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  Optional list of icon representations for this prompt. See [Icons](/v2/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.11.0\" />\n\n  Optional meta information about the prompt. This data is passed through to the MCP client as the `_meta` field of the client-side prompt object and can be used for custom metadata, versioning, or other application-specific purposes.\n</ParamField>\n</Card>\n\n### Argument Types\n\n<VersionBadge version=\"2.9.0\" />\n\nThe MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP:\n\n1. **Automatically converts** string arguments from MCP clients to the expected types\n2. **Generates helpful descriptions** showing the exact JSON string format needed\n3. **Preserves direct usage** - you can still call prompts with properly typed arguments\n\nSince the MCP specification only allows string arguments, clients need to know what string format to use for complex types. FastMCP solves this by automatically enhancing the argument descriptions with JSON schema information, making it clear to both humans and LLMs how to format their arguments.\n\n<CodeGroup>\n\n```python Python Code\n@mcp.prompt\ndef analyze_data(\n    numbers: list[int],\n    metadata: dict[str, str], \n    threshold: float\n) -> str:\n    \"\"\"Analyze numerical data.\"\"\"\n    avg = sum(numbers) / len(numbers)\n    return f\"Average: {avg}, above threshold: {avg > threshold}\"\n```\n\n```json Resulting MCP Prompt\n{\n  \"name\": \"analyze_data\",\n  \"description\": \"Analyze numerical data.\",\n  \"arguments\": [\n    {\n      \"name\": \"numbers\",\n      \"description\": \"Provide as a JSON string matching the following schema: {\\\"items\\\":{\\\"type\\\":\\\"integer\\\"},\\\"type\\\":\\\"array\\\"}\",\n      \"required\": true\n    },\n    {\n      \"name\": \"metadata\", \n      \"description\": \"Provide as a JSON string matching the following schema: {\\\"additionalProperties\\\":{\\\"type\\\":\\\"string\\\"},\\\"type\\\":\\\"object\\\"}\",\n      \"required\": true\n    },\n    {\n      \"name\": \"threshold\",\n      \"description\": \"Provide as a JSON string matching the following schema: {\\\"type\\\":\\\"number\\\"}\",\n      \"required\": true\n    }\n  ]\n}\n```\n\n</CodeGroup>\n\n**MCP clients will call this prompt with string arguments:**\n```json\n{\n  \"numbers\": \"[1, 2, 3, 4, 5]\",\n  \"metadata\": \"{\\\"source\\\": \\\"api\\\", \\\"version\\\": \\\"1.0\\\"}\",\n  \"threshold\": \"2.5\"\n}\n```\n\n**But you can still call it directly with proper types:**\n```python\n# This also works for direct calls\nresult = await prompt.render({\n    \"numbers\": [1, 2, 3, 4, 5],\n    \"metadata\": {\"source\": \"api\", \"version\": \"1.0\"}, \n    \"threshold\": 2.5\n})\n```\n\n<Warning>\nKeep your type annotations simple when using this feature. Complex nested types or custom classes may not convert reliably from JSON strings. The automatically generated schema descriptions are the only guidance users receive about the expected format.\n\nGood choices: `list[int]`, `dict[str, str]`, `float`, `bool`\nAvoid: Complex Pydantic models, deeply nested structures, custom classes\n</Warning>\n\n### Return Values\n\nFastMCP intelligently handles different return types from your prompt function:\n\n-   **`str`**: Automatically converted to a single `PromptMessage`.\n-   **`PromptMessage`**: Used directly as provided. (Note a more user-friendly `Message` constructor is available that can accept raw strings instead of `TextContent` objects.)\n-   **`list[PromptMessage | str]`**: Used as a sequence of messages (a conversation).\n-   **`Any`**: If the return type is not one of the above, the return value is attempted to be converted to a string and used as a `PromptMessage`.\n\n```python\nfrom fastmcp.prompts.prompt import Message, PromptResult\n\n@mcp.prompt\ndef roleplay_scenario(character: str, situation: str) -> PromptResult:\n    \"\"\"Sets up a roleplaying scenario with initial messages.\"\"\"\n    return [\n        Message(f\"Let's roleplay. You are {character}. The situation is: {situation}\"),\n        Message(\"Okay, I understand. I am ready. What happens next?\", role=\"assistant\")\n    ]\n```\n\n\n### Required vs. Optional Parameters\n\nParameters in your function signature are considered **required** unless they have a default value.\n\n```python\n@mcp.prompt\ndef data_analysis_prompt(\n    data_uri: str,                        # Required - no default value\n    analysis_type: str = \"summary\",       # Optional - has default value\n    include_charts: bool = False          # Optional - has default value\n) -> str:\n    \"\"\"Creates a request to analyze data with specific parameters.\"\"\"\n    prompt = f\"Please perform a '{analysis_type}' analysis on the data found at {data_uri}.\"\n    if include_charts:\n        prompt += \" Include relevant charts and visualizations.\"\n    return prompt\n```\n\nIn this example, the client *must* provide `data_uri`. If `analysis_type` or `include_charts` are omitted, their default values will be used.\n\n### Disabling Prompts\n\n<VersionBadge version=\"2.8.0\" />\n\nYou can control the visibility and availability of prompts by enabling or disabling them. Disabled prompts will not appear in the list of available prompts, and attempting to call a disabled prompt will result in an \"Unknown prompt\" error.\n\nBy default, all prompts are enabled. You can disable a prompt upon creation using the `enabled` parameter in the decorator:\n\n```python\n@mcp.prompt(enabled=False)\ndef experimental_prompt():\n    \"\"\"This prompt is not ready for use.\"\"\"\n    return \"This is an experimental prompt.\"\n```\n\nYou can also toggle a prompt's state programmatically after it has been created:\n\n```python\n@mcp.prompt\ndef seasonal_prompt(): return \"Happy Holidays!\"\n\n# Disable and re-enable the prompt\nseasonal_prompt.disable()\nseasonal_prompt.enable()\n```\n\n### Async Prompts\n\nFastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as prompts.\n\n```python\n# Synchronous prompt\n@mcp.prompt\ndef simple_question(question: str) -> str:\n    \"\"\"Generates a simple question to ask the LLM.\"\"\"\n    return f\"Question: {question}\"\n\n# Asynchronous prompt\n@mcp.prompt\nasync def data_based_prompt(data_id: str) -> str:\n    \"\"\"Generates a prompt based on data that needs to be fetched.\"\"\"\n    # In a real scenario, you might fetch data from a database or API\n    async with aiohttp.ClientSession() as session:\n        async with session.get(f\"https://api.example.com/data/{data_id}\") as response:\n            data = await response.json()\n            return f\"Analyze this data: {data['content']}\"\n```\n\nUse `async def` when your prompt function performs I/O operations like network requests, database queries, file I/O, or external service calls.\n\n### Accessing MCP Context\n\n<VersionBadge version=\"2.2.5\" />\n\nPrompts can access additional MCP information and features through the `Context` object. To access it, add a parameter to your prompt function with a type annotation of `Context`:\n\n```python {6}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"PromptServer\")\n\n@mcp.prompt\nasync def generate_report_request(report_type: str, ctx: Context) -> str:\n    \"\"\"Generates a request for a report.\"\"\"\n    return f\"Please create a {report_type} report. Request ID: {ctx.request_id}\"\n```\n\nFor full documentation on the Context object and all its capabilities, see the [Context documentation](/v2/servers/context).\n\n### Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends `notifications/prompts/list_changed` notifications to connected clients when prompts are added, enabled, or disabled. This allows clients to stay up-to-date with the current prompt set without manually polling for changes.\n\n```python\n@mcp.prompt\ndef example_prompt() -> str:\n    return \"Hello!\"\n\n# These operations trigger notifications:\nmcp.add_prompt(example_prompt)  # Sends prompts/list_changed notification\nexample_prompt.disable()        # Sends prompts/list_changed notification  \nexample_prompt.enable()         # Sends prompts/list_changed notification\n```\n\nNotifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.\n\nClients can handle these notifications using a [message handler](/v2/clients/messages) to automatically refresh their prompt lists or update their interfaces.\n\n## Server Behavior\n\n### Duplicate Prompts\n\n<VersionBadge version=\"2.1.0\" />\n\nYou can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\n    name=\"PromptServer\",\n    on_duplicate_prompts=\"error\"  # Raise an error if a prompt name is duplicated\n)\n\n@mcp.prompt\ndef greeting(): return \"Hello, how can I help you today?\"\n\n# This registration attempt will raise a ValueError because\n# \"greeting\" is already registered and the behavior is \"error\".\n# @mcp.prompt\n# def greeting(): return \"Hi there! What can I do for you?\"\n```\n\nThe duplicate behavior options are:\n\n-   `\"warn\"` (default): Logs a warning, and the new prompt replaces the old one.\n-   `\"error\"`: Raises a `ValueError`, preventing the duplicate registration.\n-   `\"replace\"`: Silently replaces the existing prompt with the new one.\n-   `\"ignore\"`: Keeps the original prompt and ignores the new registration attempt. \n"
  },
  {
    "path": "docs/v2/servers/proxy.mdx",
    "content": "---\ntitle: Proxy Servers\nsidebarTitle: Proxy Servers\ndescription: Use FastMCP to act as an intermediary or change transport for other MCP servers.\nicon: arrows-retweet\n---\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\n<VersionBadge version=\"2.0.0\" />\n\nFastMCP provides a powerful proxying capability that allows one FastMCP server instance to act as a frontend for another MCP server (which could be remote, running on a different transport, or even another FastMCP instance). This is achieved using the `FastMCP.as_proxy()` class method.\n\n## What is Proxying?\n\nProxying means setting up a FastMCP server that doesn't implement its own tools or resources directly. Instead, when it receives a request (like `tools/call` or `resources/read`), it forwards that request to a *backend* MCP server, receives the response, and then relays that response back to the original client.\n\n```mermaid\nsequenceDiagram\n    participant ClientApp as Your Client (e.g., Claude Desktop)\n    participant FastMCPProxy as FastMCP Proxy Server\n    participant BackendServer as Backend MCP Server (e.g., remote SSE)\n\n    ClientApp->>FastMCPProxy: MCP Request (e.g. stdio)\n    Note over FastMCPProxy, BackendServer: Proxy forwards the request\n    FastMCPProxy->>BackendServer: MCP Request (e.g. sse)\n    BackendServer-->>FastMCPProxy: MCP Response (e.g. sse)\n    Note over ClientApp, FastMCPProxy: Proxy relays the response\n    FastMCPProxy-->>ClientApp: MCP Response (e.g. stdio)\n```\n\n### Key Benefits\n\n<VersionBadge version=\"2.10.3\" />\n\n- **Session Isolation**: Each request gets its own isolated session, ensuring safe concurrent operations\n- **Transport Bridging**: Expose servers running on one transport via a different transport\n- **Advanced MCP Features**: Automatic forwarding of sampling, elicitation, logging, and progress\n- **Security**: Acts as a controlled gateway to backend servers\n- **Simplicity**: Single endpoint even if backend location or transport changes\n\n### Performance Considerations\n\nWhen using proxy servers, especially those connecting to HTTP-based backend servers, be aware that latency can be significant. Operations like `list_tools()` may take hundreds of milliseconds compared to 1-2ms for local tools. When mounting proxy servers, this latency affects all operations on the parent server, not just interactions with the proxied tools.\n\nIf low latency is a requirement for your use-case, consider using [`import_server()`](/v2/servers/composition#importing-static-composition) to copy tools at startup rather than proxying them at runtime.\n\n## Quick Start\n\n<VersionBadge version=\"2.10.3\" />\n\nThe recommended way to create a proxy is using `ProxyClient`, which provides full MCP feature support with automatic session isolation:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.proxy import ProxyClient\n\n# Create a proxy with full MCP feature support\nproxy = FastMCP.as_proxy(\n    ProxyClient(\"backend_server.py\"),\n    name=\"MyProxy\"\n)\n\n# Run the proxy (e.g., via stdio for Claude Desktop)\nif __name__ == \"__main__\":\n    proxy.run()\n```\n\nThis single setup gives you:\n- Safe concurrent request handling\n- Automatic forwarding of advanced MCP features (sampling, elicitation, etc.)\n- Session isolation to prevent context mixing\n- Full compatibility with all MCP clients\n\nYou can also pass a FastMCP [client transport](/v2/clients/transports) (or parameter that can be inferred to a transport) to `as_proxy()`. This will automatically create a `ProxyClient` instance for you.\n\nFinally, you can pass a regular FastMCP `Client` instance to `as_proxy()`. This will work for many use cases, but may break if advanced MCP features like sampling or elicitation are invoked by the server. \n\n## Session Isolation & Concurrency\n\n<VersionBadge version=\"2.10.3\" />\n\nFastMCP proxies provide session isolation to ensure safe concurrent operations. The session strategy depends on how the proxy is configured:\n\n### Fresh Sessions\n\nWhen you pass a disconnected client (which is the normal case), each request gets its own isolated backend session:\n\n```python\nfrom fastmcp.server.proxy import ProxyClient\n\n# Each request creates a fresh backend session (recommended)\nproxy = FastMCP.as_proxy(ProxyClient(\"backend_server.py\"))\n\n# Multiple clients can use this proxy simultaneously without interference:\n# - Client A calls a tool -> gets isolated backend session\n# - Client B calls a tool -> gets different isolated backend session  \n# - No context mixing between requests\n```\n\n### Session Reuse with Connected Clients\n\nWhen you pass an already-connected client, the proxy will reuse that session for all requests:\n\n```python\nfrom fastmcp import Client\n\n# Create and connect a client\nasync with Client(\"backend_server.py\") as connected_client:\n    # This proxy will reuse the connected session for all requests\n    proxy = FastMCP.as_proxy(connected_client)\n    \n    # ⚠️ Warning: All requests share the same backend session\n    # This may cause context mixing in concurrent scenarios\n```\n\n**Important**: Using shared sessions with concurrent requests from multiple clients may lead to context mixing and race conditions. This approach should only be used in single-threaded scenarios or when you have explicit synchronization.\n\n## Transport Bridging\n\nA common use case is bridging transports - exposing a server running on one transport via a different transport. For example, making a remote SSE server available locally via stdio:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.proxy import ProxyClient\n\n# Bridge remote SSE server to local stdio\nremote_proxy = FastMCP.as_proxy(\n    ProxyClient(\"http://example.com/mcp/sse\"),\n    name=\"Remote-to-Local Bridge\"\n)\n\n# Run locally via stdio for Claude Desktop\nif __name__ == \"__main__\":\n    remote_proxy.run()  # Defaults to stdio transport\n```\n\nOr expose a local server via HTTP for remote access:\n\n```python\n# Bridge local server to HTTP\nlocal_proxy = FastMCP.as_proxy(\n    ProxyClient(\"local_server.py\"),\n    name=\"Local-to-HTTP Bridge\"\n)\n\n# Run via HTTP for remote clients\nif __name__ == \"__main__\":\n    local_proxy.run(transport=\"http\", host=\"0.0.0.0\", port=8080)\n```\n\n\n## Advanced MCP Features\n\n<VersionBadge version=\"2.10.3\" />\n\n`ProxyClient` automatically forwards advanced MCP protocol features between the backend server and clients connected to the proxy, ensuring full MCP compatibility.\n\n### Supported Features\n\n- **Roots**: Forwards filesystem root access requests to the client\n- **Sampling**: Forwards LLM completion requests from backend to client  \n- **Elicitation**: Forwards user input requests to the client\n- **Logging**: Forwards log messages from backend through to client\n- **Progress**: Forwards progress notifications during long operations\n\n```python\nfrom fastmcp.server.proxy import ProxyClient\n\n# ProxyClient automatically handles all these features\nbackend = ProxyClient(\"advanced_backend.py\")\nproxy = FastMCP.as_proxy(backend)\n\n# When the backend server:\n# - Requests LLM sampling -> forwarded to your client\n# - Logs messages -> appear in your client\n# - Reports progress -> shown in your client\n# - Needs user input -> prompts your client\n```\n\n### Customizing Feature Support\n\nYou can selectively disable forwarding by passing `None` for specific handlers:\n\n```python\n# Disable sampling but keep other features\nbackend = ProxyClient(\n    \"backend_server.py\",\n    sampling_handler=None,  # Disable LLM sampling forwarding\n    log_handler=None        # Disable log forwarding\n)\n```\n\nWhen you use a transport string directly with `FastMCP.as_proxy()`, it automatically creates a `ProxyClient` internally to ensure full feature support.\n\n## Configuration-Based Proxies\n\n<VersionBadge version=\"2.4.0\" />\n\nYou can create a proxy directly from a configuration dictionary that follows the MCPConfig schema. This is useful for quickly setting up proxies to remote servers without manually configuring each connection detail.\n\n```python\nfrom fastmcp import FastMCP\n\n# Create a proxy directly from a config dictionary\nconfig = {\n    \"mcpServers\": {\n        \"default\": {  # For single server configs, 'default' is commonly used\n            \"url\": \"https://example.com/mcp\",\n            \"transport\": \"http\"\n        }\n    }\n}\n\n# Create a proxy to the configured server (auto-creates ProxyClient)\nproxy = FastMCP.as_proxy(config, name=\"Config-Based Proxy\")\n\n# Run the proxy with stdio transport for local access\nif __name__ == \"__main__\":\n    proxy.run()\n```\n\n<Note>\nThe MCPConfig format follows an emerging standard for MCP server configuration and may evolve as the specification matures. While FastMCP aims to maintain compatibility with future versions, be aware that field names or structure might change.\n</Note>\n\n### Multi-Server Configurations\n\nYou can create a proxy to multiple servers by specifying multiple entries in the config. They are automatically mounted with their config names as prefixes:\n\n```python\n# Multi-server configuration\nconfig = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"url\": \"https://weather-api.example.com/mcp\",\n            \"transport\": \"http\"\n        },\n        \"calendar\": {\n            \"url\": \"https://calendar-api.example.com/mcp\",\n            \"transport\": \"http\"\n        }\n    }\n}\n\n# Create a unified proxy to multiple servers\ncomposite_proxy = FastMCP.as_proxy(config, name=\"Composite Proxy\")\n\n# Tools, resources, prompts, and templates are accessible with prefixes:\n# - Tools: weather_get_forecast, calendar_add_event\n# - Prompts: weather_daily_summary, calendar_quick_add\n# - Resources: weather://weather/icons/sunny, calendar://calendar/events/today\n# - Templates: weather://weather/locations/{id}, calendar://calendar/events/{date}\n```\n\n## Component Prefixing\n\nWhen proxying one or more servers, component names are prefixed the same way as with mounting and importing:\n\n- Tools: `{prefix}_{tool_name}`\n- Prompts: `{prefix}_{prompt_name}`\n- Resources: `protocol://{prefix}/path/to/resource` (default path format)\n- Resource templates: `protocol://{prefix}/...` and template names are also prefixed\n\nThese rules apply uniformly whether you:\n- Mount a proxy on another server\n- Create a multi-server proxy from an `MCPConfig`\n- Use `FastMCP.as_proxy()` directly\n\n## Mirrored Components\n\n<VersionBadge version=\"2.10.5\" />\n\nWhen you access tools, resources, or prompts from a proxy server, they are \"mirrored\" from the remote server. Mirrored components cannot be modified directly since they reflect the state of the remote server. For example, you can not simply \"disable\" a mirrored component.\n\nHowever, you can create a copy of a mirrored component and store it as a new locally-defined component. Local components always take precedence over mirrored ones because the proxy server will check its own registry before it attempts to engage the remote server.\n\nTherefore, to enable or disable a proxy tool, resource, or prompt, you should first create a local copy and add it to your own server. Here's an example of how to do that for a tool:\n\n```python\n# Create your own server\nmy_server = FastMCP(\"MyServer\")\n\n# Get a proxy server\nproxy = FastMCP.as_proxy(\"backend_server.py\")\n\n# Get mirrored components from proxy\nmirrored_tool = await proxy.get_tool(\"useful_tool\")\n\n# Create a local copy that you can modify\nlocal_tool = mirrored_tool.copy()\n\n# Add the local copy to your server\nmy_server.add_tool(local_tool)\n\n# Now you can disable YOUR copy\nlocal_tool.disable()\n```\n\n\n## `FastMCPProxy` Class\n\nInternally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed for advanced scenarios.\n\n### Direct Usage\n\n```python\nfrom fastmcp.server.proxy import FastMCPProxy, ProxyClient\n\n# Provide a client factory for explicit session control\ndef create_client():\n    return ProxyClient(\"backend_server.py\")\n\nproxy = FastMCPProxy(client_factory=create_client)\n```\n\n### Parameters\n\n- **`client_factory`**: A callable that returns a `Client` instance when called. This gives you full control over session creation and reuse strategies.\n\n### Explicit Session Management\n\n`FastMCPProxy` requires explicit session management - no automatic detection is performed. You must choose your session strategy:\n\n```python\n# Share session across all requests (be careful with concurrency)\nshared_client = ProxyClient(\"backend_server.py\")\ndef shared_session_factory():\n    return shared_client\n\nproxy = FastMCPProxy(client_factory=shared_session_factory)\n\n# Create fresh sessions per request (recommended)\ndef fresh_session_factory():\n    return ProxyClient(\"backend_server.py\")\n\nproxy = FastMCPProxy(client_factory=fresh_session_factory)\n```\n\nFor automatic session strategy selection, use the convenience method `FastMCP.as_proxy()` instead.\n\n```python\n# Custom factory with specific configuration\ndef custom_client_factory():\n    client = ProxyClient(\"backend_server.py\")\n    # Add any custom configuration here\n    return client\n\nproxy = FastMCPProxy(client_factory=custom_client_factory)\n```\n"
  },
  {
    "path": "docs/v2/servers/resources.mdx",
    "content": "---\ntitle: Resources & Templates\nsidebarTitle: Resources\ndescription: Expose data sources and dynamic content generators to your MCP client.\nicon: folder-open\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nResources represent data or files that an MCP client can read, and resource templates extend this concept by allowing clients to request dynamically generated resources based on parameters passed in the URI.\n\nFastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator.\n\n## What Are Resources?\n\nResources provide read-only access to data for the LLM or client application. When a client requests a resource URI:\n\n1.  FastMCP finds the corresponding resource definition.\n2.  If it's dynamic (defined by a function), the function is executed.\n3.  The content (text, JSON, binary data) is returned to the client.\n\nThis allows LLMs to access files, database content, configuration, or dynamically generated information relevant to the conversation.\n\n## Resources\n\n### The `@resource` Decorator\n\nThe most common way to define a resource is by decorating a Python function. The decorator requires the resource's unique URI.\n\n```python\nimport json\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Basic dynamic resource returning a string\n@mcp.resource(\"resource://greeting\")\ndef get_greeting() -> str:\n    \"\"\"Provides a simple greeting message.\"\"\"\n    return \"Hello from FastMCP Resources!\"\n\n# Resource returning JSON data (dict is auto-serialized)\n@mcp.resource(\"data://config\")\ndef get_config() -> dict:\n    \"\"\"Provides application configuration as JSON.\"\"\"\n    return {\n        \"theme\": \"dark\",\n        \"version\": \"1.2.0\",\n        \"features\": [\"tools\", \"resources\"],\n    }\n```\n\n**Key Concepts:**\n\n*   **URI:** The first argument to `@resource` is the unique URI (e.g., `\"resource://greeting\"`) clients use to request this data.\n*   **Lazy Loading:** The decorated function (`get_greeting`, `get_config`) is only executed when a client specifically requests that resource URI via `resources/read`.\n*   **Inferred Metadata:** By default:\n    *   Resource Name: Taken from the function name (`get_greeting`).\n    *   Resource Description: Taken from the function's docstring.\n\n#### Decorator Arguments\n\nYou can customize the resource's properties using arguments in the `@mcp.resource` decorator:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Example specifying metadata\n@mcp.resource(\n    uri=\"data://app-status\",      # Explicit URI (required)\n    name=\"ApplicationStatus\",     # Custom name\n    description=\"Provides the current status of the application.\", # Custom description\n    mime_type=\"application/json\", # Explicit MIME type\n    tags={\"monitoring\", \"status\"}, # Categorization tags\n    meta={\"version\": \"2.1\", \"team\": \"infrastructure\"}  # Custom metadata\n)\ndef get_application_status() -> dict:\n    \"\"\"Internal function description (ignored if description is provided above).\"\"\"\n    return {\"status\": \"ok\", \"uptime\": 12345, \"version\": mcp.settings.version} # Example usage\n```\n\n<Card icon=\"code\" title=\"@resource Decorator Arguments\">\n<ParamField body=\"uri\" type=\"str\" required>\n  The unique identifier for the resource\n</ParamField>\n\n<ParamField body=\"name\" type=\"str | None\">\n  A human-readable name. If not provided, defaults to function name\n</ParamField>\n\n<ParamField body=\"description\" type=\"str | None\">\n  Explanation of the resource. If not provided, defaults to docstring\n</ParamField>\n\n<ParamField body=\"mime_type\" type=\"str | None\">\n  Specifies the content type. FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types\n</ParamField>\n\n<ParamField body=\"tags\" type=\"set[str] | None\">\n  A set of strings used to categorize the resource. These can be used by the server and, in some cases, by clients to filter or group available resources.\n</ParamField>\n\n<ParamField body=\"enabled\" type=\"bool\" default=\"True\">\n  A boolean to enable or disable the resource. See [Disabling Resources](#disabling-resources) for more information\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  Optional list of icon representations for this resource or template. See [Icons](/v2/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"annotations\" type=\"Annotations | dict | None\">\n    An optional `Annotations` object or dictionary to add additional metadata about the resource.\n  <Expandable title=\"Annotations attributes\">\n    <ParamField body=\"readOnlyHint\" type=\"bool | None\">\n      If true, the resource is read-only and does not modify its environment.\n    </ParamField>\n    <ParamField body=\"idempotentHint\" type=\"bool | None\">\n      If true, reading the resource repeatedly will have no additional effect on its environment.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.11.0\" />\n  \n  Optional meta information about the resource. This data is passed through to the MCP client as the `_meta` field of the client-side resource object and can be used for custom metadata, versioning, or other application-specific purposes.\n</ParamField>\n</Card>\n\n### Return Values\n\nFastMCP automatically converts your function's return value into the appropriate MCP resource content:\n\n-   **`str`**: Sent as `TextResourceContents` (with `mime_type=\"text/plain\"` by default).\n-   **`dict`, `list`, `pydantic.BaseModel`**: Automatically serialized to a JSON string and sent as `TextResourceContents` (with `mime_type=\"application/json\"` by default).\n-   **`bytes`**: Base64 encoded and sent as `BlobResourceContents`. You should specify an appropriate `mime_type` (e.g., `\"image/png\"`, `\"application/octet-stream\"`).\n-   **`None`**: Results in an empty resource content list being returned.\n\n### Disabling Resources\n\n<VersionBadge version=\"2.8.0\" />\n\nYou can control the visibility and availability of resources and templates by enabling or disabling them. Disabled resources will not appear in the list of available resources or templates, and attempting to read a disabled resource will result in an \"Unknown resource\" error.\n\nBy default, all resources are enabled. You can disable a resource upon creation using the `enabled` parameter in the decorator:\n\n```python\n@mcp.resource(\"data://secret\", enabled=False)\ndef get_secret_data():\n    \"\"\"This resource is currently disabled.\"\"\"\n    return \"Secret data\"\n```\n\nYou can also toggle a resource's state programmatically after it has been created:\n\n```python\n@mcp.resource(\"data://config\")\ndef get_config(): return {\"version\": 1}\n\n# Disable and re-enable the resource\nget_config.disable()\nget_config.enable()\n```\n\n\n### Accessing MCP Context\n\n<VersionBadge version=\"2.2.5\" />\n\nResources and resource templates can access additional MCP information and features through the `Context` object. To access it, add a parameter to your resource function with a type annotation of `Context`:\n\n```python {6, 14}\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"DataServer\")\n\n@mcp.resource(\"resource://system-status\")\nasync def get_system_status(ctx: Context) -> dict:\n    \"\"\"Provides system status information.\"\"\"\n    return {\n        \"status\": \"operational\",\n        \"request_id\": ctx.request_id\n    }\n\n@mcp.resource(\"resource://{name}/details\")\nasync def get_details(name: str, ctx: Context) -> dict:\n    \"\"\"Get details for a specific name.\"\"\"\n    return {\n        \"name\": name,\n        \"accessed_at\": ctx.request_id\n    }\n```\n\nFor full documentation on the Context object and all its capabilities, see the [Context documentation](/v2/servers/context).\n\n\n### Async Resources\n\nUse `async def` for resource functions that perform I/O operations (e.g., reading from a database or network) to avoid blocking the server.\n\n```python\nimport aiofiles\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n@mcp.resource(\"file:///app/data/important_log.txt\", mime_type=\"text/plain\")\nasync def read_important_log() -> str:\n    \"\"\"Reads content from a specific log file asynchronously.\"\"\"\n    try:\n        async with aiofiles.open(\"/app/data/important_log.txt\", mode=\"r\") as f:\n            content = await f.read()\n        return content\n    except FileNotFoundError:\n        return \"Log file not found.\"\n```\n\n\n### Resource Classes\n\nWhile `@mcp.resource` is ideal for dynamic content, you can directly register pre-defined resources (like static files or simple text) using `mcp.add_resource()` and concrete `Resource` subclasses.\n\n```python\nfrom pathlib import Path\nfrom fastmcp import FastMCP\nfrom fastmcp.resources import FileResource, TextResource, DirectoryResource\n\nmcp = FastMCP(name=\"DataServer\")\n\n# 1. Exposing a static file directly\nreadme_path = Path(\"./README.md\").resolve()\nif readme_path.exists():\n    # Use a file:// URI scheme\n    readme_resource = FileResource(\n        uri=f\"file://{readme_path.as_posix()}\",\n        path=readme_path, # Path to the actual file\n        name=\"README File\",\n        description=\"The project's README.\",\n        mime_type=\"text/markdown\",\n        tags={\"documentation\"}\n    )\n    mcp.add_resource(readme_resource)\n\n# 2. Exposing simple, predefined text\nnotice_resource = TextResource(\n    uri=\"resource://notice\",\n    name=\"Important Notice\",\n    text=\"System maintenance scheduled for Sunday.\",\n    tags={\"notification\"}\n)\nmcp.add_resource(notice_resource)\n\n# 3. Exposing a directory listing\ndata_dir_path = Path(\"./app_data\").resolve()\nif data_dir_path.is_dir():\n    data_listing_resource = DirectoryResource(\n        uri=\"resource://data-files\",\n        path=data_dir_path, # Path to the directory\n        name=\"Data Directory Listing\",\n        description=\"Lists files available in the data directory.\",\n        recursive=False # Set to True to list subdirectories\n    )\n    mcp.add_resource(data_listing_resource) # Returns JSON list of files\n```\n\n**Common Resource Classes:**\n\n-   `TextResource`: For simple string content.\n-   `BinaryResource`: For raw `bytes` content.\n-   `FileResource`: Reads content from a local file path. Handles text/binary modes and lazy reading.\n-   `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`).\n-   `DirectoryResource`: Lists files in a local directory (returns JSON).\n-   (`FunctionResource`: Internal class used by `@mcp.resource`).\n\nUse these when the content is static or sourced directly from a file/URL, bypassing the need for a dedicated Python function.\n\n### Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends `notifications/resources/list_changed` notifications to connected clients when resources or templates are added, enabled, or disabled. This allows clients to stay up-to-date with the current resource set without manually polling for changes.\n\n```python\n@mcp.resource(\"data://example\")\ndef example_resource() -> str:\n    return \"Hello!\"\n\n# These operations trigger notifications:\nmcp.add_resource(example_resource)  # Sends resources/list_changed notification\nexample_resource.disable()          # Sends resources/list_changed notification  \nexample_resource.enable()           # Sends resources/list_changed notification\n```\n\nNotifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.\n\nClients can handle these notifications using a [message handler](/v2/clients/messages) to automatically refresh their resource lists or update their interfaces.\n\n### Annotations\n\n<VersionBadge version=\"2.11.0\" />\n\nFastMCP allows you to add specialized metadata to your resources through annotations. These annotations communicate how resources behave to client applications without consuming token context in LLM prompts.\n\nAnnotations serve several purposes in client applications:\n- Indicating whether resources are read-only or may have side effects\n- Describing the safety profile of resources (idempotent vs. non-idempotent)\n- Helping clients optimize caching and access patterns\n\nYou can add annotations to a resource using the `annotations` parameter in the `@mcp.resource` decorator:\n\n```python\n@mcp.resource(\n    \"data://config\",\n    annotations={\n        \"readOnlyHint\": True,\n        \"idempotentHint\": True\n    }\n)\ndef get_config() -> dict:\n    \"\"\"Get application configuration.\"\"\"\n    return {\"version\": \"1.0\", \"debug\": False}\n```\n\nFastMCP supports these standard annotations:\n\n| Annotation | Type | Default | Purpose |\n| :--------- | :--- | :------ | :------ |\n| `readOnlyHint` | boolean | true | Indicates if the resource only provides data without side effects |\n| `idempotentHint` | boolean | true | Indicates if repeated reads have the same effect as a single read |\n\nRemember that annotations help make better user experiences but should be treated as advisory hints. They help client applications present appropriate UI elements and optimize access patterns, but won't enforce behavior on their own. Always focus on making your annotations accurately represent what your resource actually does.\n\n## Resource Templates\n\nResource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature.\n\nResource templates share most configuration options with regular resources (name, description, mime_type, tags, annotations), but add the ability to define URI parameters that map to function parameters.\n\nResource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `\"user://profile/{name}\"` is registered, MCP clients could request `\"user://profile/ford\"` or `\"user://profile/marvin\"` to retrieve either of those two user profiles as resources, without having to register each resource individually.\n\n<Tip>\nFunctions with `*args` are not supported as resource templates. However, unlike tools and prompts, resource templates do support `**kwargs` because the URI template defines specific parameter names that will be collected and passed as keyword arguments.\n</Tip>\n\nHere is a complete example that shows how to define two resource templates:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Template URI includes {city} placeholder\n@mcp.resource(\"weather://{city}/current\")\ndef get_weather(city: str) -> dict:\n    \"\"\"Provides weather information for a specific city.\"\"\"\n    # In a real implementation, this would call a weather API\n    # Here we're using simplified logic for example purposes\n    return {\n        \"city\": city.capitalize(),\n        \"temperature\": 22,\n        \"condition\": \"Sunny\",\n        \"unit\": \"celsius\"\n    }\n\n# Template with multiple parameters and annotations\n@mcp.resource(\n    \"repos://{owner}/{repo}/info\",\n    annotations={\n        \"readOnlyHint\": True,\n        \"idempotentHint\": True\n    }\n)\ndef get_repo_info(owner: str, repo: str) -> dict:\n    \"\"\"Retrieves information about a GitHub repository.\"\"\"\n    # In a real implementation, this would call the GitHub API\n    return {\n        \"owner\": owner,\n        \"name\": repo,\n        \"full_name\": f\"{owner}/{repo}\",\n        \"stars\": 120,\n        \"forks\": 48\n    }\n```\n\nWith these two templates defined, clients can request a variety of resources:\n- `weather://london/current` → Returns weather for London\n- `weather://paris/current` → Returns weather for Paris\n- `repos://PrefectHQ/fastmcp/info` → Returns info about the PrefectHQ/fastmcp repository\n- `repos://prefecthq/prefect/info` → Returns info about the prefecthq/prefect repository\n\n### RFC 6570 URI Templates\n\n\nFastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/html/rfc6570) for resource templates, providing a standardized way to define parameterized URIs. This includes support for simple expansion, wildcard path parameters, and form-style query parameters.\n\n#### Wildcard Parameters\n\n<VersionBadge version=\"2.2.4\" />\n\nResource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross \"/\" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.\n\n```python {15, 23}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n\n# Standard parameter only matches one segment\n@mcp.resource(\"files://{filename}\")\ndef get_file(filename: str) -> str:\n    \"\"\"Retrieves a file by name.\"\"\"\n    # Will only match files://<single-segment>\n    return f\"File content for: {filename}\"\n\n\n# Wildcard parameter can match multiple segments\n@mcp.resource(\"path://{filepath*}\")\ndef get_path_content(filepath: str) -> str:\n    \"\"\"Retrieves content at a specific path.\"\"\"\n    # Can match path://docs/server/resources.mdx\n    return f\"Content at path: {filepath}\"\n\n\n# Mixing standard and wildcard parameters\n@mcp.resource(\"repo://{owner}/{path*}/template.py\")\ndef get_template_file(owner: str, path: str) -> dict:\n    \"\"\"Retrieves a file from a specific repository and path, but\n    only if the resource ends with `template.py`\"\"\"\n    # Can match repo://PrefectHQ/fastmcp/src/resources/template.py\n    return {\n        \"owner\": owner,\n        \"path\": path + \"/template.py\",\n        \"content\": f\"File at {path}/template.py in {owner}'s repository\"\n    }\n```\n\nWildcard parameters are useful when:\n\n- Working with file paths or hierarchical data\n- Creating APIs that need to capture variable-length path segments\n- Building URL-like patterns similar to REST APIs\n\nNote that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.\n\n#### Query Parameters\n\n<VersionBadge version=\"2.13.0\" />\n\nFastMCP supports RFC 6570 form-style query parameters using the `{?param1,param2}` syntax. Query parameters provide a clean way to pass optional configuration to resources without cluttering the path.\n\nQuery parameters must be optional function parameters (have default values), while path parameters map to required function parameters. This enforces a clear separation: required data goes in the path, optional configuration in query params.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Basic query parameters\n@mcp.resource(\"data://{id}{?format}\")\ndef get_data(id: str, format: str = \"json\") -> str:\n    \"\"\"Retrieve data in specified format.\"\"\"\n    if format == \"xml\":\n        return f\"<data id='{id}' />\"\n    return f'{{\"id\": \"{id}\"}}'\n\n# Multiple query parameters with type coercion\n@mcp.resource(\"api://{endpoint}{?version,limit,offset}\")\ndef call_api(endpoint: str, version: int = 1, limit: int = 10, offset: int = 0) -> dict:\n    \"\"\"Call API endpoint with pagination.\"\"\"\n    return {\n        \"endpoint\": endpoint,\n        \"version\": version,\n        \"limit\": limit,\n        \"offset\": offset,\n        \"results\": fetch_results(endpoint, version, limit, offset)\n    }\n\n# Query parameters with wildcards\n@mcp.resource(\"files://{path*}{?encoding,lines}\")\ndef read_file(path: str, encoding: str = \"utf-8\", lines: int = 100) -> str:\n    \"\"\"Read file with optional encoding and line limit.\"\"\"\n    return read_file_content(path, encoding, lines)\n```\n\n**Example requests:**\n- `data://123` → Uses default format `\"json\"`\n- `data://123?format=xml` → Uses format `\"xml\"`\n- `api://users?version=2&limit=50` → `version=2, limit=50, offset=0`\n- `files://src/main.py?encoding=ascii&lines=50` → Custom encoding and line limit\n\nFastMCP automatically coerces query parameter string values to the correct types based on your function's type hints (`int`, `float`, `bool`, `str`).\n\n**Query parameters vs. hidden defaults:**\n\nQuery parameters expose optional configuration to clients. To hide optional parameters from clients entirely (always use defaults), simply omit them from the URI template:\n\n```python\n# Clients CAN override max_results via query string\n@mcp.resource(\"search://{query}{?max_results}\")\ndef search_configurable(query: str, max_results: int = 10) -> dict:\n    return {\"query\": query, \"limit\": max_results}\n\n# Clients CANNOT override max_results (not in URI template)\n@mcp.resource(\"search://{query}\")\ndef search_fixed(query: str, max_results: int = 10) -> dict:\n    return {\"query\": query, \"limit\": max_results}\n```\n\n### Template Parameter Rules\n\n<VersionBadge version=\"2.2.0\" />\n\nFastMCP enforces these validation rules when creating resource templates:\n\n1. **Required function parameters** (no default values) must appear in the URI path template\n2. **Query parameters** (specified with `{?param}` syntax) must be optional function parameters with default values\n3. **All URI template parameters** (path and query) must exist as function parameters\n\nOptional function parameters (those with default values) can be:\n- Included as query parameters (`{?param}`) - clients can override via query string\n- Omitted from URI template - always uses default value, not exposed to clients\n- Used in alternative path templates - enables multiple ways to access the same resource\n\n**Multiple templates for one function:**\n\nCreate multiple resource templates that expose the same function through different URI patterns by manually applying decorators:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DataServer\")\n\n# Define a user lookup function that can be accessed by different identifiers\ndef lookup_user(name: str | None = None, email: str | None = None) -> dict:\n    \"\"\"Look up a user by either name or email.\"\"\"\n    if email:\n        return find_user_by_email(email)  # pseudocode\n    elif name:\n        return find_user_by_name(name)  # pseudocode\n    else:\n        return {\"error\": \"No lookup parameters provided\"}\n\n# Manually apply multiple decorators to the same function\nmcp.resource(\"users://email/{email}\")(lookup_user)\nmcp.resource(\"users://name/{name}\")(lookup_user)\n```\n\nNow an LLM or client can retrieve user information in two different ways:\n- `users://email/alice@example.com` → Looks up user by email (with name=None)\n- `users://name/Bob` → Looks up user by name (with email=None)\n\nThis approach allows a single function to be registered with multiple URI patterns while keeping the implementation clean and straightforward.\n\nTemplates provide a powerful way to expose parameterized data access points following REST-like principles.\n\n## Error Handling\n\n<VersionBadge version=\"2.4.1\" />\n\nIf your resource function encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ResourceError`.\n\nBy default, all exceptions (including their details) are logged and converted into an MCP error response to be sent back to the client LLM. This helps the LLM understand failures and react appropriately.\n\nIf you want to mask internal error details for security reasons, you can:\n\n1. Use the `mask_error_details=True` parameter when creating your `FastMCP` instance:\n```python\nmcp = FastMCP(name=\"SecureServer\", mask_error_details=True)\n```\n\n2. Or use `ResourceError` to explicitly control what error information is sent to clients:\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import ResourceError\n\nmcp = FastMCP(name=\"DataServer\")\n\n@mcp.resource(\"resource://safe-error\")\ndef fail_with_details() -> str:\n    \"\"\"This resource provides detailed error information.\"\"\"\n    # ResourceError contents are always sent back to clients,\n    # regardless of mask_error_details setting\n    raise ResourceError(\"Unable to retrieve data: file not found\")\n\n@mcp.resource(\"resource://masked-error\")\ndef fail_with_masked_details() -> str:\n    \"\"\"This resource masks internal error details when mask_error_details=True.\"\"\"\n    # This message would be masked if mask_error_details=True\n    raise ValueError(\"Sensitive internal file path: /etc/secrets.conf\")\n\n@mcp.resource(\"data://{id}\")\ndef get_data_by_id(id: str) -> dict:\n    \"\"\"Template resources also support the same error handling pattern.\"\"\"\n    if id == \"secure\":\n        raise ValueError(\"Cannot access secure data\")\n    elif id == \"missing\":\n        raise ResourceError(\"Data ID 'missing' not found in database\")\n    return {\"id\": id, \"value\": \"data\"}\n```\n\nWhen `mask_error_details=True`, only error messages from `ResourceError` will include details, other exceptions will be converted to a generic message.\n\n## Server Behavior\n\n### Duplicate Resources\n\n<VersionBadge version=\"2.1.0\" />\n\nYou can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\n    name=\"ResourceServer\",\n    on_duplicate_resources=\"error\" # Raise error on duplicates\n)\n\n@mcp.resource(\"data://config\")\ndef get_config_v1(): return {\"version\": 1}\n\n# This registration attempt will raise a ValueError because\n# \"data://config\" is already registered and the behavior is \"error\".\n# @mcp.resource(\"data://config\")\n# def get_config_v2(): return {\"version\": 2}\n```\n\nThe duplicate behavior options are:\n\n-   `\"warn\"` (default): Logs a warning, and the new resource/template replaces the old one.\n-   `\"error\"`: Raises a `ValueError`, preventing the duplicate registration.\n-   `\"replace\"`: Silently replaces the existing resource/template with the new one.\n-   `\"ignore\"`: Keeps the original resource/template and ignores the new registration attempt."
  },
  {
    "path": "docs/v2/servers/sampling.mdx",
    "content": "---\ntitle: LLM Sampling\nsidebarTitle: Sampling\ndescription: Request LLM text generation from the client or a configured provider through the MCP context.\nicon: robot\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\";\n\n<VersionBadge version=\"2.0.0\" />\n\nLLM sampling allows your MCP tools to request text generation from an LLM during execution. This enables tools to leverage AI capabilities for analysis, generation, reasoning, and more—without the client needing to orchestrate multiple calls.\n\nBy default, sampling requests are routed to the client's LLM. You can also configure a fallback handler to use a specific provider (like OpenAI) when the client doesn't support sampling, or to always use your own LLM regardless of client capabilities.\n\n## Method Reference\n\n<Card icon=\"code\" title=\"ctx.sample()\">\n<ResponseField name=\"ctx.sample\" type=\"async method\">\n  Request text generation from the LLM, running to completion automatically.\n\n  <Expandable title=\"Parameters\">\n    <ResponseField name=\"messages\" type=\"str | list[str | SamplingMessage]\">\n      The prompt to send. Can be a simple string or a list of messages for multi-turn conversations.\n    </ResponseField>\n\n    <ResponseField name=\"system_prompt\" type=\"str | None\" default=\"None\">\n      Instructions that establish the LLM's role and behavior.\n    </ResponseField>\n\n    <ResponseField name=\"temperature\" type=\"float | None\" default=\"None\">\n      Controls randomness (0.0 = deterministic, 1.0 = creative).\n    </ResponseField>\n\n    <ResponseField name=\"max_tokens\" type=\"int | None\" default=\"512\">\n      Maximum tokens to generate.\n    </ResponseField>\n\n    <ResponseField name=\"model_preferences\" type=\"str | list[str] | None\" default=\"None\">\n      Hints for which model the client should use.\n    </ResponseField>\n\n    <ResponseField name=\"tools\" type=\"list[Callable] | None\" default=\"None\">\n      Functions the LLM can call during sampling.\n    </ResponseField>\n\n    <ResponseField name=\"result_type\" type=\"type[T] | None\" default=\"None\">\n      A type for validated structured output. Supports Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`.\n    </ResponseField>\n\n    <ResponseField name=\"mask_error_details\" type=\"bool | None\" default=\"None\">\n      If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking and provide specific error messages to the LLM.\n    </ResponseField>\n\n  </Expandable>\n\n  <Expandable title=\"Response\">\n    <ResponseField name=\"SamplingResult[T]\" type=\"dataclass\">\n      - `.text`: The raw text response (or JSON for structured output)\n      - `.result`: The typed result—same as `.text` for plain text, or a validated Pydantic object for structured output\n      - `.history`: All messages exchanged during sampling\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n<Card icon=\"code\" title=\"ctx.sample_step()\">\n<ResponseField name=\"ctx.sample_step\" type=\"async method\">\n  Make a single LLM sampling call. Use this for fine-grained control over the sampling loop.\n\n  <Expandable title=\"Parameters\">\n    Same as `sample()`, plus:\n\n    <ResponseField name=\"tool_choice\" type=\"str | None\" default=\"None\">\n      Controls tool usage: `\"auto\"`, `\"required\"`, or `\"none\"`.\n    </ResponseField>\n\n    <ResponseField name=\"execute_tools\" type=\"bool\" default=\"True\">\n      If True, execute tool calls and append results to history. If False, return immediately with tool calls available for manual execution.\n    </ResponseField>\n\n    <ResponseField name=\"mask_error_details\" type=\"bool | None\" default=\"None\">\n      If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking.\n    </ResponseField>\n\n  </Expandable>\n\n  <Expandable title=\"Response\">\n    <ResponseField name=\"SampleStep\" type=\"dataclass\">\n      - `.response`: The raw LLM response\n      - `.history`: Messages including input, assistant response, and tool results\n      - `.is_tool_use`: True if the LLM requested tool execution\n      - `.tool_calls`: List of tool calls (if any)\n      - `.text`: The text content (if any)\n    </ResponseField>\n  </Expandable>\n</ResponseField>\n</Card>\n\n## Basic Sampling\n\nThe simplest use of sampling is passing a prompt string to `ctx.sample()`. The method sends the prompt to the LLM, waits for the complete response, and returns a `SamplingResult`. You can access the generated text through the `.text` attribute.\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def summarize(content: str, ctx: Context) -> str:\n    \"\"\"Generate a summary of the provided content.\"\"\"\n    result = await ctx.sample(f\"Please summarize this:\\n\\n{content}\")\n    return result.text or \"\"\n```\n\nThe `SamplingResult` also provides `.result` (identical to `.text` for plain text responses) and `.history` containing the full message exchange—useful if you need to continue the conversation or debug the interaction.\n\n### System Prompts\n\nSystem prompts let you establish the LLM's role and behavioral guidelines before it processes your request. This is useful for controlling tone, enforcing constraints, or providing context that shouldn't clutter the user-facing prompt.\n\n````python\n@mcp.tool\nasync def generate_code(concept: str, ctx: Context) -> str:\n    \"\"\"Generate a Python code example for a concept.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Write a Python example demonstrating '{concept}'.\",\n        system_prompt=(\n            \"You are an expert Python programmer. \"\n            \"Provide concise, working code without explanations.\"\n        ),\n        temperature=0.7,\n        max_tokens=300\n    )\n    return f\"```python\\n{result.text}\\n```\"\n````\n\nThe `temperature` parameter controls randomness—higher values (up to 1.0) produce more varied outputs, while lower values make responses more deterministic. The `max_tokens` parameter limits response length.\n\n### Model Preferences\n\nModel preferences let you hint at which LLM the client should use for a request. You can pass a single model name or a list of preferences in priority order. These are hints rather than requirements—the actual model used depends on what the client has available.\n\n```python\n@mcp.tool\nasync def technical_analysis(data: str, ctx: Context) -> str:\n    \"\"\"Analyze data using a reasoning-focused model.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Analyze this data:\\n\\n{data}\",\n        model_preferences=[\"claude-opus-4-5\", \"gpt-5-2\"],\n        temperature=0.2,\n    )\n    return result.text or \"\"\n```\n\nUse model preferences when different tasks benefit from different model characteristics. Creative writing might prefer faster models with higher temperature, while complex analysis might benefit from larger reasoning-focused models.\n\n### Multi-Turn Conversations\n\nFor requests that need conversational context, construct a list of `SamplingMessage` objects representing the conversation history. Each message has a `role` (\"user\" or \"assistant\") and `content` (a `TextContent` object).\n\n```python\nfrom mcp.types import SamplingMessage, TextContent\n\n@mcp.tool\nasync def contextual_analysis(query: str, data: str, ctx: Context) -> str:\n    \"\"\"Analyze data with conversational context.\"\"\"\n    messages = [\n        SamplingMessage(\n            role=\"user\",\n            content=TextContent(type=\"text\", text=f\"Here's my data: {data}\"),\n        ),\n        SamplingMessage(\n            role=\"assistant\",\n            content=TextContent(type=\"text\", text=\"I see the data. What would you like to know?\"),\n        ),\n        SamplingMessage(\n            role=\"user\",\n            content=TextContent(type=\"text\", text=query),\n        ),\n    ]\n    result = await ctx.sample(messages=messages)\n    return result.text or \"\"\n```\n\nThe LLM receives the full conversation thread and responds with awareness of the preceding context.\n\n## Structured Output\n\n<VersionBadge version=\"2.14.1\" />\n\nWhen you need validated, typed data instead of free-form text, use the `result_type` parameter. FastMCP ensures the LLM returns data matching your type, handling validation and retries automatically. The `result_type` parameter accepts Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`.\n\n```python\nfrom pydantic import BaseModel\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\nclass SentimentResult(BaseModel):\n    sentiment: str\n    confidence: float\n    reasoning: str\n\n@mcp.tool\nasync def analyze_sentiment(text: str, ctx: Context) -> SentimentResult:\n    \"\"\"Analyze text sentiment with structured output.\"\"\"\n    result = await ctx.sample(\n        messages=f\"Analyze the sentiment of: {text}\",\n        result_type=SentimentResult,\n    )\n    return result.result  # A validated SentimentResult object\n```\n\nWhen you call this tool, the LLM returns a structured response that FastMCP validates against your Pydantic model. You access the validated object through `result.result`, while `result.text` contains the JSON representation.\n\n<Note>\n  When you pass `result_type`, `sample()` automatically creates a\n  `final_response` tool that the LLM calls to provide its response. If\n  validation fails, the error is sent back to the LLM for retry. This automatic\n  handling only applies to `sample()`—with `sample_step()`, you must manage\n  structured output yourself.\n</Note>\n\n## Sampling with Tools\n\n<VersionBadge version=\"2.14.1\" />\n\nSampling with tools enables agentic workflows where the LLM can call functions to gather information before responding. This implements [SEP-1577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577), allowing the LLM to autonomously orchestrate multi-step operations.\n\nPass Python functions to the `tools` parameter, and FastMCP handles the execution loop automatically—calling tools, returning results to the LLM, and continuing until the LLM provides a final response.\n\n### Defining Tools\n\nDefine regular Python functions with type hints and docstrings. FastMCP extracts the function's name, docstring, and parameter types to create tool schemas that the LLM can understand.\n\n```python\nfrom fastmcp import FastMCP, Context\n\ndef search(query: str) -> str:\n    \"\"\"Search the web for information.\"\"\"\n    return f\"Results for: {query}\"\n\ndef get_time() -> str:\n    \"\"\"Get the current time.\"\"\"\n    from datetime import datetime\n    return datetime.now().strftime(\"%H:%M:%S\")\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def research(question: str, ctx: Context) -> str:\n    \"\"\"Answer questions using available tools.\"\"\"\n    result = await ctx.sample(\n        messages=question,\n        tools=[search, get_time],\n    )\n    return result.text or \"\"\n```\n\nThe LLM sees each function's signature and docstring, using this information to decide when and how to call them. Tool errors are caught and sent back to the LLM, allowing it to recover gracefully. An internal safety limit prevents infinite loops.\n\n### Tool Error Handling\n\nBy default, when a sampling tool raises an exception, the error message (including details) is sent back to the LLM so it can attempt recovery. To prevent sensitive information from leaking to the LLM, use the `mask_error_details` parameter:\n\n```python\nresult = await ctx.sample(\n    messages=question,\n    tools=[search],\n    mask_error_details=True,  # Generic error messages only\n)\n```\n\nWhen `mask_error_details=True`, tool errors become generic messages like `\"Error executing tool 'search'\"` instead of exposing stack traces or internal details.\n\nTo intentionally provide specific error messages to the LLM regardless of masking, raise `ToolError`:\n\n```python\nfrom fastmcp.exceptions import ToolError\n\ndef search(query: str) -> str:\n    \"\"\"Search for information.\"\"\"\n    if not query.strip():\n        raise ToolError(\"Search query cannot be empty\")\n    return f\"Results for: {query}\"\n```\n\n`ToolError` messages always pass through to the LLM, making it the escape hatch for errors you want the LLM to see and handle.\n\nFor custom names or descriptions, use `SamplingTool.from_function()`:\n\n```python\nfrom fastmcp.server.sampling import SamplingTool\n\ntool = SamplingTool.from_function(\n    my_func,\n    name=\"custom_name\",\n    description=\"Custom description\"\n)\n\nresult = await ctx.sample(messages=\"...\", tools=[tool])\n```\n\n### Combining with Structured Output\n\nCombine tools with `result_type` for agentic workflows that return validated, structured data. The LLM uses your tools to gather information, then returns a response matching your type.\n\n```python\nresult = await ctx.sample(\n    messages=\"Research Python async patterns\",\n    tools=[search, fetch_url],\n    result_type=ResearchResult,\n)\n```\n\n## Loop Control\n\n<VersionBadge version=\"2.14.1\" />\n\nWhile `sample()` handles the tool execution loop automatically, some scenarios require fine-grained control over each step. The `sample_step()` method makes a single LLM call and returns a `SampleStep` containing the response and updated history.\n\nUnlike `sample()`, `sample_step()` is stateless—it doesn't remember previous calls. You control the conversation by passing the full message history each time. The returned `step.history` includes all messages up through the current response, making it easy to continue the loop.\n\nUse `sample_step()` when you need to:\n\n- Inspect tool calls before they execute\n- Implement custom termination conditions\n- Add logging, metrics, or checkpointing between steps\n- Build custom agentic loops with domain-specific logic\n\n### Using sample_step()\n\nBy default, `sample_step()` executes any tool calls and includes the results in the history. Call it in a loop, passing the updated history each time, until a stop condition is met.\n\n```python\nfrom mcp.types import SamplingMessage\n\n@mcp.tool\nasync def controlled_agent(question: str, ctx: Context) -> str:\n    \"\"\"Agent with manual loop control.\"\"\"\n    messages: list[str | SamplingMessage] = [question]  # strings auto-convert\n\n    while True:\n        step = await ctx.sample_step(\n            messages=messages,\n            tools=[search, get_time],\n        )\n\n        if step.is_tool_use:\n            # Tools already executed (execute_tools=True by default)\n            # Log what was called before continuing\n            for call in step.tool_calls:\n                print(f\"Called tool: {call.name}\")\n\n        if not step.is_tool_use:\n            return step.text or \"\"\n\n        # Continue with updated history\n        messages = step.history\n```\n\n### SampleStep Properties\n\nEach `SampleStep` provides information about what the LLM returned:\n\n- `step.is_tool_use` — True if the LLM requested tool calls\n- `step.tool_calls` — List of tool calls requested (if any)\n- `step.text` — The text content (if any)\n- `step.history` — All messages exchanged so far\n\nThe contents of `step.history` depend on `execute_tools`:\n- **`execute_tools=True`** (default): Includes tool results, ready for the next iteration\n- **`execute_tools=False`**: Includes the assistant's tool request, but you add results yourself\n\n### Manual Tool Execution\n\nSet `execute_tools=False` to handle tool execution yourself. When disabled, `step.history` contains the user message and the assistant's response with tool calls—but no tool results. You execute the tools and append the results as a user message.\n\n```python\nfrom mcp.types import SamplingMessage, ToolResultContent, TextContent\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP()\n\n@mcp.tool\nasync def research(question: str, ctx: Context) -> str:\n    \"\"\"Research with manual tool handling.\"\"\"\n\n    def search(query: str) -> str:\n        return f\"Results for: {query}\"\n\n    def get_time() -> str:\n        return \"12:00 PM\"\n\n    # Map tool names to functions\n    tools = {\"search\": search, \"get_time\": get_time}\n\n    messages: list[SamplingMessage] = [question]  # strings are converted automatically\n\n    while True:\n        step = await ctx.sample_step(\n            messages=messages,\n            tools=list(tools.values()),\n            execute_tools=False,\n        )\n\n        if not step.is_tool_use:\n            return step.text or \"\"\n\n        # Execute tools and collect results\n        tool_results = []\n        for call in step.tool_calls:\n            fn = tools[call.name]\n            result = fn(**call.input)\n            tool_results.append(\n                ToolResultContent(\n                    type=\"tool_result\",\n                    toolUseId=call.id,\n                    content=[TextContent(type=\"text\", text=result)],\n                )\n            )\n\n        messages = list(step.history)\n        messages.append(SamplingMessage(role=\"user\", content=tool_results))\n```\n\n#### Error Handling\n\nTo report an error, set `isError=True`. The LLM will see the error and can decide how to proceed:\n\n```python\ntool_result = ToolResultContent(\n    type=\"tool_result\",\n    toolUseId=call.id,\n    content=[TextContent(type=\"text\", text=\"Permission denied\")],\n    isError=True,\n)\n```\n\n## Fallback Handlers\n\nClient support for sampling is optional—some clients may not implement it. To ensure your tools work regardless of client capabilities, configure a `sampling_handler` that sends requests directly to an LLM provider.\n\nFastMCP provides built-in handlers for [OpenAI and Anthropic APIs](/v2/clients/sampling#built-in-handlers). These handlers support the full sampling API including tools, automatically converting your Python functions to each provider's format.\n\n<Note>\nInstall handlers with `pip install fastmcp[openai]` or `pip install fastmcp[anthropic]`.\n</Note>\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\nserver = FastMCP(\n    name=\"My Server\",\n    sampling_handler=OpenAISamplingHandler(default_model=\"gpt-4o-mini\"),\n    sampling_handler_behavior=\"fallback\",\n)\n```\n\nOr with Anthropic:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\nserver = FastMCP(\n    name=\"My Server\",\n    sampling_handler=AnthropicSamplingHandler(default_model=\"claude-sonnet-4-5\"),\n    sampling_handler_behavior=\"fallback\",\n)\n```\n\n### Behavior Modes\n\nThe `sampling_handler_behavior` parameter controls when the handler is used:\n\n- **`\"fallback\"`** (default): Use the handler only when the client doesn't support sampling. This lets capable clients use their own LLM while ensuring your tools still work with clients that lack sampling support.\n- **`\"always\"`**: Always use the handler, bypassing the client entirely. Use this when you need guaranteed control over which LLM processes requests—for cost control, compliance requirements, or when specific model characteristics are essential.\n\n<Note>\n  Sampling with tools requires the client to advertise the `sampling.tools`\n  capability. FastMCP clients do this automatically. For external clients that\n  don't support tool-enabled sampling, configure a fallback handler with\n  `sampling_handler_behavior=\"always\"`.\n</Note>\n"
  },
  {
    "path": "docs/v2/servers/server.mdx",
    "content": "---\ntitle: The FastMCP Server\nsidebarTitle: Overview\ndescription: The core FastMCP server class for building MCP applications with tools, resources, and prompts.\nicon: server\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\nThe central piece of a FastMCP application is the `FastMCP` server class. This class acts as the main container for your application's tools, resources, and prompts, and manages communication with MCP clients.\n\n## Creating a Server\n\nInstantiating a server is straightforward. You typically provide a name for your server, which helps identify it in client applications or logs.\n\n```python\nfrom fastmcp import FastMCP\n\n# Create a basic server instance\nmcp = FastMCP(name=\"MyAssistantServer\")\n\n# You can also add instructions for how to interact with the server\nmcp_with_instructions = FastMCP(\n    name=\"HelpfulAssistant\",\n    instructions=\"\"\"\n        This server provides data analysis tools.\n        Call get_average() to analyze numerical data.\n    \"\"\",\n)\n```\n\nThe `FastMCP` constructor accepts several arguments:\n\n<Card icon=\"code\" title=\"FastMCP Constructor Parameters\">\n<ParamField body=\"name\" type=\"str\" default=\"FastMCP\">\n  A human-readable name for your server\n</ParamField>\n\n<ParamField body=\"instructions\" type=\"str | None\">\n  Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality\n</ParamField>\n\n<ParamField body=\"version\" type=\"str | None\">\n  Version string for your server. If not provided, defaults to the FastMCP library version\n</ParamField>\n\n<ParamField body=\"website_url\" type=\"str | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  URL to a website with more information about your server. Displayed in client applications\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  List of icon representations for your server. Icons help users visually identify your server in client applications. See [Icons](/v2/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"auth\" type=\"OAuthProvider | TokenVerifier | None\">\n  Authentication provider for securing HTTP-based transports. See [Authentication](/v2/servers/auth/authentication) for configuration options\n</ParamField>\n\n<ParamField body=\"lifespan\" type=\"AsyncContextManager | None\">\n  An async context manager function for server startup and shutdown logic\n</ParamField>\n\n<ParamField body=\"tools\" type=\"list[Tool | Callable] | None\">\n  A list of tools (or functions to convert to tools) to add to the server. In some cases, providing tools programmatically may be more convenient than using the `@mcp.tool` decorator\n</ParamField>\n\n\n<ParamField body=\"include_tags\" type=\"set[str] | None\">\n  Only expose components with at least one matching tag\n</ParamField>\n\n<ParamField body=\"exclude_tags\" type=\"set[str] | None\">\n  Hide components with any matching tag\n</ParamField>\n\n<ParamField body=\"on_duplicate_tools\" type='Literal[\"error\", \"warn\", \"replace\"]' default=\"error\">\n  How to handle duplicate tool registrations\n</ParamField>\n\n<ParamField body=\"on_duplicate_resources\" type='Literal[\"error\", \"warn\", \"replace\"]' default=\"warn\">\n  How to handle duplicate resource registrations\n</ParamField>\n\n<ParamField body=\"on_duplicate_prompts\" type='Literal[\"error\", \"warn\", \"replace\"]' default=\"replace\">\n  How to handle duplicate prompt registrations\n</ParamField>\n\n\n<ParamField body=\"strict_input_validation\" type=\"bool\" default=\"False\">\n  <VersionBadge version=\"2.13.0\" />\n  Controls how tool input parameters are validated. When `False` (default), FastMCP uses Pydantic's flexible validation that coerces compatible inputs (e.g., `\"10\"` → `10` for int parameters). When `True`, uses the MCP SDK's JSON Schema validation to validate inputs against the exact schema before passing them to your function, rejecting any type mismatches. The default mode improves compatibility with LLM clients while maintaining type safety. See [Input Validation Modes](/v2/servers/tools#input-validation-modes) for details\n</ParamField>\n\n<ParamField body=\"include_fastmcp_meta\" type=\"bool\" default=\"True\">\n  <VersionBadge version=\"2.11.0\" />\n\n  Whether to include FastMCP metadata in component responses. When `True`, component tags and other FastMCP-specific metadata are included in the `_fastmcp` namespace within each component's `meta` field. When `False`, this metadata is omitted, resulting in cleaner integration with external systems. Can be overridden globally via `FASTMCP_INCLUDE_FASTMCP_META` environment variable\n</ParamField>\n</Card>\n## Components\n\nFastMCP servers expose several types of components to the client:\n\n### Tools\n\nTools are functions that the client can call to perform actions or access external systems.\n\n```python\n@mcp.tool\ndef multiply(a: float, b: float) -> float:\n    \"\"\"Multiplies two numbers together.\"\"\"\n    return a * b\n```\n\nSee [Tools](/v2/servers/tools) for detailed documentation.\n\n### Resources\n\nResources expose data sources that the client can read.\n\n```python\n@mcp.resource(\"data://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application configuration.\"\"\"\n    return {\"theme\": \"dark\", \"version\": \"1.0\"}\n```\n\nSee [Resources & Templates](/v2/servers/resources) for detailed documentation.\n\n### Resource Templates\n\nResource templates are parameterized resources that allow the client to request specific data.\n\n```python\n@mcp.resource(\"users://{user_id}/profile\")\ndef get_user_profile(user_id: int) -> dict:\n    \"\"\"Retrieves a user's profile by ID.\"\"\"\n    # The {user_id} in the URI is extracted and passed to this function\n    return {\"id\": user_id, \"name\": f\"User {user_id}\", \"status\": \"active\"}\n```\n\nSee [Resources & Templates](/v2/servers/resources) for detailed documentation.\n\n### Prompts\n\nPrompts are reusable message templates for guiding the LLM.\n\n```python\n@mcp.prompt\ndef analyze_data(data_points: list[float]) -> str:\n    \"\"\"Creates a prompt asking for analysis of numerical data.\"\"\"\n    formatted_data = \", \".join(str(point) for point in data_points)\n    return f\"Please analyze these data points: {formatted_data}\"\n```\n\nSee [Prompts](/v2/servers/prompts) for detailed documentation.\n\n## Tag-Based Filtering\n\n<VersionBadge version=\"2.8.0\" />\n\nFastMCP supports tag-based filtering to selectively expose components based on configurable include/exclude tag sets. This is useful for creating different views of your server for different environments or users.\n\nComponents can be tagged when defined using the `tags` parameter:\n\n```python\n@mcp.tool(tags={\"public\", \"utility\"})\ndef public_tool() -> str:\n    return \"This tool is public\"\n\n@mcp.tool(tags={\"internal\", \"admin\"})\ndef admin_tool() -> str:\n    return \"This tool is for admins only\"\n```\n\n\nThe filtering logic works as follows:\n- **Include tags**: If specified, only components with at least one matching tag are exposed\n- **Exclude tags**: Components with any matching tag are filtered out\n- **Precedence**: Exclude tags always take priority over include tags\n\n<Tip>\nTo ensure a component is never exposed, you can set `enabled=False` on the component itself. To learn more, see the component-specific documentation.\n</Tip>\n\nYou configure tag-based filtering when creating your server:\n\n```python\n# Only expose components tagged with \"public\"\nmcp = FastMCP(include_tags={\"public\"})\n\n# Hide components tagged as \"internal\" or \"deprecated\"  \nmcp = FastMCP(exclude_tags={\"internal\", \"deprecated\"})\n\n# Combine both: show admin tools but hide deprecated ones\nmcp = FastMCP(include_tags={\"admin\"}, exclude_tags={\"deprecated\"})\n```\n\nThis filtering applies to all component types (tools, resources, resource templates, and prompts) and affects both listing and access.\n\n## Running the Server\n\nFastMCP servers need a transport mechanism to communicate with clients. You typically start your server by calling the `mcp.run()` method on your `FastMCP` instance, often within an `if __name__ == \"__main__\":` block in your main server script. This pattern ensures compatibility with various MCP clients.\n\n```python\n# my_server.py\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"MyServer\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    \"\"\"Greet a user by name.\"\"\"\n    return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n    # This runs the server, defaulting to STDIO transport\n    mcp.run()\n    \n    # To use a different transport, e.g., HTTP:\n    # mcp.run(transport=\"http\", host=\"127.0.0.1\", port=9000)\n```\n\nFastMCP supports several transport options: \n- STDIO (default, for local tools)\n- HTTP (recommended for web services, uses Streamable HTTP protocol)\n- SSE (legacy web transport, deprecated)\n\nThe server can also be run using the FastMCP CLI.\n\nFor detailed information on each transport, how to configure them (host, port, paths), and when to use which, please refer to the [**Running Your FastMCP Server**](/v2/deployment/running-server) guide.\n\n## Custom Routes\n\nWhen running your server with HTTP transport, you can add custom web routes alongside your MCP endpoint using the `@custom_route` decorator. This is useful for simple endpoints like health checks that need to be served alongside your MCP server:\n\n```python\nfrom fastmcp import FastMCP\nfrom starlette.requests import Request\nfrom starlette.responses import PlainTextResponse\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.custom_route(\"/health\", methods=[\"GET\"])\nasync def health_check(request: Request) -> PlainTextResponse:\n    return PlainTextResponse(\"OK\")\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")  # Health check at http://localhost:8000/health\n```\n\nCustom routes are served alongside your MCP endpoint and are useful for:\n- Health check endpoints for monitoring\n- Simple status or info endpoints\n- Basic webhooks or callbacks\n\nFor more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/v2/deployment/http#integration-with-web-frameworks).\n\n## Composing Servers\n\n<VersionBadge version=\"2.2.0\" />\n\nFastMCP supports composing multiple servers together using `import_server` (static copy) and `mount` (live link). This allows you to organize large applications into modular components or reuse existing servers.\n\nSee the [Server Composition](/v2/servers/composition) guide for full details, best practices, and examples.\n\n```python\n# Example: Importing a subserver\nfrom fastmcp import FastMCP\nimport asyncio\n\nmain = FastMCP(name=\"Main\")\nsub = FastMCP(name=\"Sub\")\n\n@sub.tool\ndef hello(): \n    return \"hi\"\n\n# Mount directly\nmain.mount(sub, prefix=\"sub\")\n```\n\n## Proxying Servers\n\n<VersionBadge version=\"2.0.0\" />\n\nFastMCP can act as a proxy for any MCP server (local or remote) using `FastMCP.as_proxy`, letting you bridge transports or add a frontend to existing servers. For example, you can expose a remote SSE server locally via stdio, or vice versa.\n\nProxies automatically handle concurrent operations safely by creating fresh sessions for each request when using disconnected clients.\n\nSee the [Proxying Servers](/v2/servers/proxy) guide for details and advanced usage.\n\n```python\nfrom fastmcp import FastMCP, Client\n\nbackend = Client(\"http://example.com/mcp/sse\")\nproxy = FastMCP.as_proxy(backend, name=\"ProxyServer\")\n# Now use the proxy like any FastMCP server\n```\n\n## OpenAPI Integration\n\n<VersionBadge version=\"2.0.0\" />\n\nFastMCP can automatically generate servers from OpenAPI specifications or existing FastAPI applications using `FastMCP.from_openapi()` and `FastMCP.from_fastapi()`. This allows you to instantly convert existing APIs into MCP servers without manual tool creation.\n\nSee the [FastAPI Integration](/v2/integrations/fastapi) and [OpenAPI Integration](/v2/integrations/openapi) guides for detailed examples and configuration options.\n\n```python\nimport httpx\nfrom fastmcp import FastMCP\n\n# From OpenAPI spec\nspec = httpx.get(\"https://api.example.com/openapi.json\").json()\nmcp = FastMCP.from_openapi(openapi_spec=spec, client=httpx.AsyncClient())\n\n# From FastAPI app\nfrom fastapi import FastAPI\napp = FastAPI()\nmcp = FastMCP.from_fastapi(app=app)\n```\n\n## Server Configuration\n\nServers can be configured using a combination of initialization arguments, global settings, and transport-specific settings.\n\n### Server-Specific Configuration\n\nServer-specific settings are passed when creating the `FastMCP` instance and control server behavior:\n\n```python\nfrom fastmcp import FastMCP\n\n# Configure server-specific settings\nmcp = FastMCP(\n    name=\"ConfiguredServer\",\n    include_tags={\"public\", \"api\"},              # Only expose these tagged components\n    exclude_tags={\"internal\", \"deprecated\"},     # Hide these tagged components\n    on_duplicate_tools=\"error\",                  # Handle duplicate registrations\n    on_duplicate_resources=\"warn\",\n    on_duplicate_prompts=\"replace\",\n    include_fastmcp_meta=False,                  # Disable FastMCP metadata for cleaner integration\n)\n```\n\n### Global Settings\n\nGlobal settings affect all FastMCP servers and can be configured via environment variables (prefixed with `FASTMCP_`) or in a `.env` file:\n\n```python\nimport fastmcp\n\n# Access global settings\nprint(fastmcp.settings.log_level)        # Default: \"INFO\"\nprint(fastmcp.settings.mask_error_details)  # Default: False\nprint(fastmcp.settings.strict_input_validation)  # Default: False\nprint(fastmcp.settings.include_fastmcp_meta)   # Default: True\n```\n\nCommon global settings include:\n- **`log_level`**: Logging level (\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"), set with `FASTMCP_LOG_LEVEL`\n- **`mask_error_details`**: Whether to hide detailed error information from clients, set with `FASTMCP_MASK_ERROR_DETAILS`\n- **`strict_input_validation`**: Controls tool input validation mode (default: False for flexible coercion), set with `FASTMCP_STRICT_INPUT_VALIDATION`. See [Input Validation Modes](/v2/servers/tools#input-validation-modes)\n- **`include_fastmcp_meta`**: Whether to include FastMCP metadata in component responses (default: True), set with `FASTMCP_INCLUDE_FASTMCP_META`\n- **`env_file`**: Path to the environment file to load settings from (default: \".env\"), set with `FASTMCP_ENV_FILE`. Useful when your project uses a `.env` file with syntax incompatible with python-dotenv\n\n### Transport-Specific Configuration\n\nTransport settings are provided when running the server and control network behavior:\n\n```python\n# Configure transport when running\nmcp.run(\n    transport=\"http\",\n    host=\"0.0.0.0\",           # Bind to all interfaces\n    port=9000,                # Custom port\n    log_level=\"DEBUG\",        # Override global log level\n)\n\n# Or for async usage\nawait mcp.run_async(\n    transport=\"http\", \n    host=\"127.0.0.1\",\n    port=8080,\n)\n```\n\n### Setting Global Configuration\n\nGlobal FastMCP settings can be configured via environment variables (prefixed with `FASTMCP_`):\n\n```bash\n# Configure global FastMCP behavior\nexport FASTMCP_LOG_LEVEL=DEBUG\nexport FASTMCP_MASK_ERROR_DETAILS=True\nexport FASTMCP_STRICT_INPUT_VALIDATION=False\nexport FASTMCP_INCLUDE_FASTMCP_META=False\n```\n\n### Custom Tool Serialization\n\n<VersionBadge version=\"2.2.7\" />\n\nBy default, FastMCP serializes tool return values to JSON when they need to be converted to text. You can customize this behavior by providing a `tool_serializer` function when creating your server:\n\n```python\nimport yaml\nfrom fastmcp import FastMCP\n\n# Define a custom serializer that formats dictionaries as YAML\ndef yaml_serializer(data):\n    return yaml.dump(data, sort_keys=False)\n\n# Create a server with the custom serializer\nmcp = FastMCP(name=\"MyServer\", tool_serializer=yaml_serializer)\n\n@mcp.tool\ndef get_config():\n    \"\"\"Returns configuration in YAML format.\"\"\"\n    return {\"api_key\": \"abc123\", \"debug\": True, \"rate_limit\": 100}\n```\n\nThe serializer function takes any data object and returns a string representation. This is applied to **all non-string return values** from your tools. Tools that already return strings bypass the serializer.\n\nThis customization is useful when you want to:\n- Format data in a specific way (like YAML or custom formats)\n- Control specific serialization options (like indentation or sorting)\n- Add metadata or transform data before sending it to clients\n\n<Tip>\nIf the serializer function raises an exception, the tool will fall back to the default JSON serialization to avoid breaking the server.\n</Tip>\n"
  },
  {
    "path": "docs/v2/servers/storage-backends.mdx",
    "content": "---\ntitle: Storage Backends\nsidebarTitle: Storage Backends\ndescription: Configure persistent and distributed storage for caching and OAuth state management\nicon: database\ntag: NEW\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.13.0\" />\n\nFastMCP uses pluggable storage backends for caching responses and managing OAuth state. By default, all storage is in-memory, which is perfect for development but doesn't persist across restarts. FastMCP includes support for multiple storage backends, and you can easily extend it with custom implementations.\n\n<Tip>\nThe storage layer is powered by **[py-key-value-aio](https://github.com/strawgate/py-key-value)**, an async key-value library maintained by a core FastMCP maintainer. This library provides a unified interface for multiple backends, making it easy to swap implementations based on your deployment needs.\n</Tip>\n\n## Available Backends\n\n### In-Memory Storage\n\n**Best for:** Development, testing, single-process deployments\n\nIn-memory storage is the default for all FastMCP storage needs. It's fast, requires no setup, and is perfect for getting started.\n\n```python\nfrom key_value.aio.stores.memory import MemoryStore\n\n# Used by default - no configuration needed\n# But you can also be explicit:\ncache_store = MemoryStore()\n```\n\n**Characteristics:**\n- ✅ No setup required\n- ✅ Very fast\n- ❌ Data lost on restart\n- ❌ Not suitable for multi-process deployments\n\n### Disk Storage\n\n**Best for:** Single-server production deployments, persistent caching\n\nDisk storage persists data to the filesystem, allowing it to survive server restarts.\n\n```python\nfrom key_value.aio.stores.disk import DiskStore\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\n# Persistent response cache\nmiddleware = ResponseCachingMiddleware(\n    cache_storage=DiskStore(directory=\"/var/cache/fastmcp\")\n)\n```\n\nOr with OAuth token storage:\n\n```python\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.disk import DiskStore\n\nauth = GitHubProvider(\n    client_id=\"your-id\",\n    client_secret=\"your-secret\",\n    base_url=\"https://your-server.com\",\n    client_storage=DiskStore(directory=\"/var/lib/fastmcp/oauth\")\n)\n```\n\n**Characteristics:**\n- ✅ Data persists across restarts\n- ✅ Good performance for moderate load\n- ❌ Not suitable for distributed deployments\n- ❌ Filesystem access required\n\n### Redis\n\n**Best for:** Distributed production deployments, shared caching across multiple servers\n\n<Note>\nRedis support requires an optional dependency: `pip install 'py-key-value-aio[redis]'`\n</Note>\n\nRedis provides distributed caching and state management, ideal for production deployments with multiple server instances.\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\n\n# Distributed response cache\nmiddleware = ResponseCachingMiddleware(\n    cache_storage=RedisStore(host=\"redis.example.com\", port=6379)\n)\n```\n\nWith authentication:\n\n```python\nfrom key_value.aio.stores.redis import RedisStore\n\ncache_store = RedisStore(\n    host=\"redis.example.com\",\n    port=6379,\n    password=\"your-redis-password\"\n)\n```\n\nFor OAuth token storage:\n\n```python\nimport os\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.redis import RedisStore\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=\"https://your-server.com\",\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    client_storage=RedisStore(host=\"redis.example.com\", port=6379)\n)\n```\n\n**Characteristics:**\n- ✅ Distributed and highly available\n- ✅ Fast in-memory performance\n- ✅ Works across multiple server instances\n- ✅ Built-in TTL support\n- ❌ Requires Redis infrastructure\n- ❌ Network latency vs local storage\n\n### Other Backends from py-key-value-aio\n\nThe py-key-value-aio library includes additional implementations for various storage systems:\n\n- **DynamoDB** - AWS distributed database\n- **MongoDB** - NoSQL document store\n- **Elasticsearch** - Distributed search and analytics\n- **Memcached** - Distributed memory caching\n- **RocksDB** - Embedded high-performance key-value store\n- **Valkey** - Redis-compatible server\n\nFor configuration details on these backends, consult the [py-key-value-aio documentation](https://github.com/strawgate/py-key-value).\n\n<Warning>\nBefore using these backends in production, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have specific constraints that make them unsuitable for production use.\n</Warning>\n\n## Use Cases in FastMCP\n\n### Server-Side OAuth Token Storage\n\nThe [OAuth Proxy](/v2/servers/auth/oauth-proxy) and OAuth auth providers use storage for persisting OAuth client registrations and upstream tokens. **By default, storage is automatically encrypted using `FernetEncryptionWrapper`.** When providing custom storage, wrap it in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.\n\n**Development (default behavior):**\n\nBy default, FastMCP automatically manages keys and storage based on your platform:\n- **Mac/Windows**: Keys are auto-managed via system keyring, storage defaults to disk. Suitable **only** for development and local testing.\n- **Linux**: Keys are ephemeral, storage defaults to memory.\n\nNo configuration needed:\n\n```python\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=\"your-id\",\n    client_secret=\"your-secret\",\n    base_url=\"https://your-server.com\"\n)\n```\n\n**Production:**\n\nFor production deployments, configure explicit keys and persistent network-accessible storage with encryption:\n\n```python\nimport os\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom cryptography.fernet import Fernet\n\nauth = GitHubProvider(\n    client_id=os.environ[\"GITHUB_CLIENT_ID\"],\n    client_secret=os.environ[\"GITHUB_CLIENT_SECRET\"],\n    base_url=\"https://your-server.com\",\n    # Explicit JWT signing key (required for production)\n    jwt_signing_key=os.environ[\"JWT_SIGNING_KEY\"],\n    # Encrypted persistent storage (required for production)\n    client_storage=FernetEncryptionWrapper(\n        key_value=RedisStore(host=\"redis.example.com\", port=6379),\n        fernet=Fernet(os.environ[\"STORAGE_ENCRYPTION_KEY\"])\n    )\n)\n```\n\nBoth parameters are required for production. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. See [OAuth Token Security](/v2/deployment/http#oauth-token-security) and [Key and Storage Management](/v2/servers/auth/oauth-proxy#key-and-storage-management) for complete setup details.\n\n### Response Caching Middleware\n\nThe [Response Caching Middleware](/v2/servers/middleware#caching-middleware) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.disk import DiskStore\n\nmcp = FastMCP(\"My Server\")\n\n# Cache to disk instead of memory\nmcp.add_middleware(ResponseCachingMiddleware(\n    cache_storage=DiskStore(directory=\"cache\")\n))\n```\n\nFor multi-server deployments sharing a Redis instance:\n\n```python\nfrom fastmcp.server.middleware.caching import ResponseCachingMiddleware\nfrom key_value.aio.stores.redis import RedisStore\nfrom key_value.aio.wrappers.prefix_collections import PrefixCollectionsWrapper\n\nbase_store = RedisStore(host=\"redis.example.com\")\nnamespaced_store = PrefixCollectionsWrapper(\n    key_value=base_store,\n    prefix=\"my-server\"\n)\n\nmiddleware = ResponseCachingMiddleware(cache_storage=namespaced_store)\n```\n\n### Client-Side OAuth Token Storage\n\nThe [FastMCP Client](/v2/clients/client) uses storage for persisting OAuth tokens locally. By default, tokens are stored in memory:\n\n```python\nfrom fastmcp.client.auth import OAuthClientProvider\nfrom key_value.aio.stores.disk import DiskStore\n\n# Store tokens on disk for persistence across restarts\ntoken_storage = DiskStore(directory=\"~/.local/share/fastmcp/tokens\")\n\noauth_provider = OAuthClientProvider(\n    mcp_url=\"https://your-mcp-server.com/mcp/sse\",\n    token_storage=token_storage\n)\n```\n\nThis allows clients to reconnect without re-authenticating after restarts.\n\n## Choosing a Backend\n\n| Backend | Development | Single Server | Multi-Server | Cloud Native |\n|---------|-------------|---------------|--------------|--------------|\n| Memory | ✅ Best | ⚠️ Limited | ❌ | ❌ |\n| Disk | ✅ Good | ✅ Recommended | ❌ | ⚠️ |\n| Redis | ⚠️ Overkill | ✅ Good | ✅ Best | ✅ Best |\n| DynamoDB | ❌ | ⚠️ | ✅ | ✅ Best (AWS) |\n| MongoDB | ❌ | ⚠️ | ✅ | ✅ Good |\n\n**Decision tree:**\n\n1. **Just starting?** Use **Memory** (default) - no configuration needed\n2. **Single server, needs persistence?** Use **Disk**\n3. **Multiple servers or cloud deployment?** Use **Redis** or **DynamoDB**\n4. **Existing infrastructure?** Look for a matching py-key-value-aio backend\n\n## More Resources\n\n- [py-key-value-aio GitHub](https://github.com/strawgate/py-key-value) - Full library documentation\n- [Response Caching Middleware](/v2/servers/middleware#caching-middleware) - Using storage for caching\n- [OAuth Token Security](/v2/deployment/http#oauth-token-security) - Production OAuth configuration\n- [HTTP Deployment](/v2/deployment/http) - Complete deployment guide\n"
  },
  {
    "path": "docs/v2/servers/tasks.mdx",
    "content": "---\ntitle: Background Tasks\nsidebarTitle: Background Tasks\ndescription: Run long-running operations asynchronously with progress tracking\nicon: clock\ntag: \"NEW\"\n---\n\nimport { VersionBadge } from \"/snippets/version-badge.mdx\"\n\n<VersionBadge version=\"2.14.0\" />\n\nFastMCP implements the MCP background task protocol ([SEP-1686](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)), giving your servers a production-ready distributed task scheduler with a single decorator change.\n\n<Tip>\n**What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service, where it processes millions of concurrent tasks every day. Docket is now open-sourced for the community.\n</Tip>\n\n\n## What Are MCP Background Tasks?\n\nIn MCP, all component interactions are blocking by default. When a client calls a tool, reads a resource, or fetches a prompt, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience.\n\nThe MCP background task protocol solves this by letting clients:\n1. **Start** an operation and receive a task ID immediately\n2. **Track** progress as the operation runs\n3. **Retrieve** the result when ready\n\nFastMCP handles all of this for you. Add `task=True` to your decorator, and your function gains full background execution with progress reporting, distributed processing, and horizontal scaling.\n\n### MCP Background Tasks vs Python Concurrency\n\nYou can always use Python's concurrency primitives (asyncio, threads, multiprocessing) or external task queues in your FastMCP servers. FastMCP is just Python—run code however you like.\n\nMCP background tasks are different: they're **protocol-native**. This means MCP clients that support the task protocol can start operations, receive progress updates, and retrieve results through the standard MCP interface. The coordination happens at the protocol level, not inside your application code.\n\n## Enabling Background Tasks\n\nAdd `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution.\n\n```python {6}\nimport asyncio\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(task=True)\nasync def slow_computation(duration: int) -> str:\n    \"\"\"A long-running operation.\"\"\"\n    for i in range(duration):\n        await asyncio.sleep(1)\n    return f\"Completed in {duration} seconds\"\n```\n\nWhen a client requests background execution, the call returns immediately with a task ID. The work executes in a background worker, and the client can poll for status or wait for the result.\n\n<Warning>\nBackground tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time.\n</Warning>\n\n## Execution Modes\n\nFor fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The MCP task protocol defines three execution modes:\n\n| Mode | Client calls without task | Client calls with task |\n|------|--------------------------|------------------------|\n| `\"forbidden\"` | Executes synchronously | Error: task not supported |\n| `\"optional\"` | Executes synchronously | Executes as background task |\n| `\"required\"` | Error: task required | Executes as background task |\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.tasks import TaskConfig\n\nmcp = FastMCP(\"MyServer\")\n\n# Supports both sync and background execution (default when task=True)\n@mcp.tool(task=TaskConfig(mode=\"optional\"))\nasync def flexible_task() -> str:\n    return \"Works either way\"\n\n# Requires background execution - errors if client doesn't request task\n@mcp.tool(task=TaskConfig(mode=\"required\"))\nasync def must_be_background() -> str:\n    return \"Only runs as a background task\"\n\n# No task support (default when task=False or omitted)\n@mcp.tool(task=TaskConfig(mode=\"forbidden\"))\nasync def sync_only() -> str:\n    return \"Never runs as background task\"\n```\n\nThe boolean shortcuts map to these modes:\n- `task=True` → `TaskConfig(mode=\"optional\")`\n- `task=False` → `TaskConfig(mode=\"forbidden\")`\n\n### Server-Wide Default\n\nTo enable background task support for all components by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`.\n\n```python\nmcp = FastMCP(\"MyServer\", tasks=True)\n```\n\n<Warning>\nIf your server defines any synchronous tools, resources, or prompts, you will need to explicitly set `task=False` on their decorators to avoid an error.\n</Warning>\n\n### Graceful Degradation\n\nWhen a client requests background execution but the component has `mode=\"forbidden\"`, FastMCP executes synchronously and returns the result inline. This follows the SEP-1686 specification for graceful degradation—clients can always request background execution without worrying about server capabilities.\n\nConversely, when a component has `mode=\"required\"` but the client doesn't request background execution, FastMCP returns an error indicating that task execution is required.\n\n### Configuration\n\n| Environment Variable | Default | Description |\n|---------------------|---------|-------------|\n| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |\n\n## Backends\n\nFastMCP supports two backends for task execution, each with different tradeoffs.\n\n### In-Memory Backend (Default)\n\nThe in-memory backend (`memory://`) requires zero configuration and works out of the box.\n\n**Advantages:**\n- No external dependencies\n- Simple single-process deployment\n\n**Disadvantages:**\n- **Ephemeral**: If the server restarts, all pending tasks are lost\n- **Higher latency**: ~250ms task pickup time vs single-digit milliseconds with Redis\n- **No horizontal scaling**: Single process only—you cannot add additional workers\n\n### Redis Backend\n\nFor production deployments, use Redis (or Valkey) as your backend by setting `FASTMCP_DOCKET_URL=redis://localhost:6379`.\n\n**Advantages:**\n- **Persistent**: Tasks survive server restarts\n- **Fast**: Single-digit millisecond task pickup latency\n- **Scalable**: Add workers to distribute load across processes or machines\n\n## Workers\n\nEvery FastMCP server with task-enabled components automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.\n\nTo scale horizontally, add more workers using the CLI:\n\n```bash\nfastmcp tasks worker server.py\n```\n\nEach additional worker pulls tasks from the same queue, distributing load across processes. Configure worker concurrency via environment:\n\n```bash\nexport FASTMCP_DOCKET_CONCURRENCY=20\nfastmcp tasks worker server.py\n```\n\n<Note>\nAdditional workers only work with Redis/Valkey backends. The in-memory backend is single-process only.\n</Note>\n\n## Progress Reporting\n\nThe `Progress` dependency lets you report progress back to clients. Inject it as a parameter with a default value, and FastMCP will provide the active progress reporter.\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Progress\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(task=True)\nasync def process_files(files: list[str], progress: Progress = Progress()) -> str:\n    await progress.set_total(len(files))\n\n    for file in files:\n        await progress.set_message(f\"Processing {file}\")\n        # ... do work ...\n        await progress.increment()\n\n    return f\"Processed {len(files)} files\"\n```\n\nThe progress API:\n- `await progress.set_total(n)` — Set the total number of steps\n- `await progress.increment(amount=1)` — Increment progress\n- `await progress.set_message(text)` — Update the status message\n\nProgress works in both immediate and background execution modes—you can use the same code regardless of how the client invokes your function.\n\n## Docket Dependencies\n\nFastMCP exposes Docket's full dependency injection system within your task-enabled functions. Beyond `Progress`, you can access the Docket instance, worker information, and use advanced features like retries and timeouts.\n\n```python\nfrom docket import Docket, Worker\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker\n\nmcp = FastMCP(\"MyServer\")\n\n@mcp.tool(task=True)\nasync def my_task(\n    progress: Progress = Progress(),\n    docket: Docket = CurrentDocket(),\n    worker: Worker = CurrentWorker(),\n) -> str:\n    # Schedule additional background work\n    await docket.add(another_task, arg1, arg2)\n\n    # Access worker metadata\n    worker_name = worker.name\n\n    return \"Done\"\n```\n\nWith `CurrentDocket()`, you can schedule additional background tasks, chain work together, and coordinate complex workflows. See the [Docket documentation](https://chrisguidry.github.io/docket/) for the complete API, including retry policies, timeouts, and custom dependencies.\n"
  },
  {
    "path": "docs/v2/servers/tools.mdx",
    "content": "---\ntitle: Tools\nsidebarTitle: Tools\ndescription: Expose functions as executable capabilities for your MCP client.\nicon: wrench\n---\n\nimport { VersionBadge } from '/snippets/version-badge.mdx'\n\nTools are the core building blocks that allow your LLM to interact with external systems, execute code, and access data that isn't in its training data. In FastMCP, tools are Python functions exposed to LLMs through the MCP protocol.\n\nTools in FastMCP transform regular Python functions into capabilities that LLMs can invoke during conversations. When an LLM decides to use a tool:\n\n1.  It sends a request with parameters based on the tool's schema.\n2.  FastMCP validates these parameters against your function's signature.\n3.  Your function executes with the validated inputs.\n4.  The result is returned to the LLM, which can use it in its response.\n\nThis allows LLMs to perform tasks like querying databases, calling APIs, making calculations, or accessing files—extending their capabilities beyond what's in their training data.\n\n\n## The `@tool` Decorator\n\nCreating a tool is as simple as decorating a Python function with `@mcp.tool`:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"CalculatorServer\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n```\n\nWhen this tool is registered, FastMCP automatically:\n- Uses the function name (`add`) as the tool name.\n- Uses the function's docstring (`Adds two integer numbers...`) as the tool description.\n- Generates an input schema based on the function's parameters and type annotations.\n- Handles parameter validation and error reporting.\n\nThe way you define your Python function dictates how the tool appears and behaves for the LLM client.\n\n<Tip>\nFunctions with `*args` or `**kwargs` are not supported as tools. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.\n</Tip>\n\n### Decorator Arguments\n\nWhile FastMCP infers the name and description from your function, you can override these and add additional metadata using arguments to the `@mcp.tool` decorator:\n\n```python\n@mcp.tool(\n    name=\"find_products\",           # Custom tool name for the LLM\n    description=\"Search the product catalog with optional category filtering.\", # Custom description\n    tags={\"catalog\", \"search\"},      # Optional tags for organization/filtering\n    meta={\"version\": \"1.2\", \"author\": \"product-team\"}  # Custom metadata\n)\ndef search_products_implementation(query: str, category: str | None = None) -> list[dict]:\n    \"\"\"Internal function description (ignored if description is provided above).\"\"\"\n    # Implementation...\n    print(f\"Searching for '{query}' in category '{category}'\")\n    return [{\"id\": 2, \"name\": \"Another Product\"}]\n```\n\n<Card icon=\"code\" title=\"@tool Decorator Arguments\">\n<ParamField body=\"name\" type=\"str | None\">\n  Sets the explicit tool name exposed via MCP. If not provided, uses the function name\n</ParamField>\n\n<ParamField body=\"description\" type=\"str | None\">\n  Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose\n</ParamField>\n\n<ParamField body=\"tags\" type=\"set[str] | None\">\n  A set of strings used to categorize the tool. These can be used by the server and, in some cases, by clients to filter or group available tools.\n</ParamField>\n\n<ParamField body=\"enabled\" type=\"bool\" default=\"True\">\n  A boolean to enable or disable the tool. See [Disabling Tools](#disabling-tools) for more information\n</ParamField>\n\n<ParamField body=\"icons\" type=\"list[Icon] | None\">\n  <VersionBadge version=\"2.13.0\" />\n\n  Optional list of icon representations for this tool. See [Icons](/v2/servers/icons) for detailed examples\n</ParamField>\n\n<ParamField body=\"annotations\" type=\"ToolAnnotations | dict | None\">\n    An optional `ToolAnnotations` object or dictionary to add additional metadata about the tool.\n  <Expandable title=\"ToolAnnotations attributes\">\n    <ParamField body=\"title\" type=\"str | None\">\n      A human-readable title for the tool.\n    </ParamField>\n    <ParamField body=\"readOnlyHint\" type=\"bool | None\">\n      If true, the tool does not modify its environment.\n    </ParamField>\n    <ParamField body=\"destructiveHint\" type=\"bool | None\">\n      If true, the tool may perform destructive updates to its environment.\n    </ParamField>\n    <ParamField body=\"idempotentHint\" type=\"bool | None\">\n      If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment.\n    </ParamField>\n    <ParamField body=\"openWorldHint\" type=\"bool | None\">\n      If true, this tool may interact with an \"open world\" of external entities. If false, the tool's domain of interaction is closed.\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n<ParamField body=\"meta\" type=\"dict[str, Any] | None\">\n  <VersionBadge version=\"2.11.0\" />\n  \n  Optional meta information about the tool. This data is passed through to the MCP client as the `_meta` field of the client-side tool object and can be used for custom metadata, versioning, or other application-specific purposes.\n</ParamField>\n</Card>\n\n\n### Async Support\n\nFastMCP is an async-first framework that seamlessly supports both asynchronous (`async def`) and synchronous (`def`) functions as tools. Async tools are preferred for I/O-bound operations to keep your server responsive.\n\nWhile synchronous tools work seamlessly in FastMCP, they can block the event loop during execution. For CPU-intensive or potentially blocking synchronous operations, consider alternative strategies. One approach is to use `anyio` (which FastMCP already uses internally) to wrap them as async functions, for example:\n\n```python {1, 13}\nimport anyio\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\ndef cpu_intensive_task(data: str) -> str:\n    # Some heavy computation that could block the event loop\n    return processed_data\n\n@mcp.tool\nasync def wrapped_cpu_task(data: str) -> str:\n    \"\"\"CPU-intensive task wrapped to prevent blocking.\"\"\"\n    return await anyio.to_thread.run_sync(cpu_intensive_task, data)\n```\n\nAlternative approaches include using `asyncio.get_event_loop().run_in_executor()` or other threading techniques to manage blocking operations without impacting server responsiveness. For example, here's a recipe for using the `asyncer` library (not included in FastMCP) to create a decorator that wraps synchronous functions, courtesy of [@hsheth2](https://github.com/PrefectHQ/fastmcp/issues/864#issuecomment-3103678258):\n\n<CodeGroup>\n```python Decorator Recipe\nimport asyncer\nimport functools\nfrom typing import Callable, ParamSpec, TypeVar, Awaitable\n\n_P = ParamSpec(\"_P\")\n_R = TypeVar(\"_R\")\n\ndef make_async_background(fn: Callable[_P, _R]) -> Callable[_P, Awaitable[_R]]:\n    @functools.wraps(fn)\n    async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:\n        return await asyncer.asyncify(fn)(*args, **kwargs)\n\n    return wrapper\n```\n\n```python Using the Decorator {6}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.tool()\n@make_async_background\ndef my_tool() -> None:\n    time.sleep(5)\n```\n</CodeGroup>\n\n## Arguments\n\nBy 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.\n\n<Note>\nFastMCP automatically dereferences `$ref` entries in tool schemas to ensure compatibility with MCP clients that don't fully support JSON Schema references (e.g., VS Code Copilot, Claude Desktop). This means complex Pydantic models with shared types are inlined in the schema rather than using `$defs` references.\n</Note>\n\n### Type Annotations\n\nMCP tools have typed arguments, and FastMCP uses type annotations to determine those types. Therefore, you should use standard Python type annotations for tool arguments:\n\n```python\n@mcp.tool\ndef analyze_text(\n    text: str,\n    max_tokens: int = 100,\n    language: str | None = None\n) -> dict:\n    \"\"\"Analyze the provided text.\"\"\"\n    # Implementation...\n```\n\nFastMCP supports a wide range of type annotations, including all Pydantic types:\n\n| Type Annotation         | Example                       | Description                         |\n| :---------------------- | :---------------------------- | :---------------------------------- |\n| Basic types             | `int`, `float`, `str`, `bool` | Simple scalar values |\n| Binary data             | `bytes`                       | Binary content (raw strings, not auto-decoded base64) |\n| Date and Time           | `datetime`, `date`, `timedelta` | Date and time objects (ISO format strings) |\n| Collection types        | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items |\n| Optional types          | `float \\| None`, `Optional[float]`| Parameters that may be null/omitted |\n| Union types             | `str \\| int`, `Union[str, int]`| Parameters accepting multiple types |\n| Constrained types       | `Literal[\"A\", \"B\"]`, `Enum`   | Parameters with specific allowed values |\n| Paths                   | `Path`                        | File system paths (auto-converted from strings) |\n| UUIDs                   | `UUID`                        | Universally unique identifiers (auto-converted from strings) |\n| Pydantic models         | `UserData`                    | Complex structured data with validation |\n\nFastMCP supports all types that Pydantic supports as fields, including all Pydantic custom types. A few FastMCP-specific behaviors to note:\n\n**Binary Data**: `bytes` parameters accept raw strings without automatic base64 decoding. For base64 data, use `str` and decode manually with `base64.b64decode()`.\n\n**Enums**: Clients send enum values (`\"red\"`), not names (`\"RED\"`). Your function receives the Enum member (`Color.RED`).\n\n**Paths and UUIDs**: String inputs are automatically converted to `Path` and `UUID` objects.\n\n**Pydantic Models**: Must be provided as JSON objects (dicts), not stringified JSON. Even with flexible validation, `{\"user\": {\"name\": \"Alice\"}}` works, but `{\"user\": '{\"name\": \"Alice\"}'}` does not.\n\n### Optional Arguments\n\nFastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.\n\n```python\n@mcp.tool\ndef search_products(\n    query: str,                   # Required - no default value\n    max_results: int = 10,        # Optional - has default value\n    sort_by: str = \"relevance\",   # Optional - has default value\n    category: str | None = None   # Optional - can be None\n) -> list[dict]:\n    \"\"\"Search the product catalog.\"\"\"\n    # Implementation...\n```\n\nIn this example, the LLM must provide a `query` parameter, while `max_results`, `sort_by`, and `category` will use their default values if not explicitly provided.\n\n### Validation Modes\n\n<VersionBadge version=\"2.13.0\" />\n\nBy default, FastMCP uses Pydantic's flexible validation that coerces compatible inputs to match your type annotations. This improves compatibility with LLM clients that may send string representations of values (like `\"10\"` for an integer parameter).\n\nIf you need stricter validation that rejects any type mismatches, you can enable strict input validation. Strict mode uses the MCP SDK's built-in JSON Schema validation to validate inputs against the exact schema before passing them to your function:\n\n```python\n# Enable strict validation for this server\nmcp = FastMCP(\"StrictServer\", strict_input_validation=True)\n\n@mcp.tool\ndef add_numbers(a: int, b: int) -> int:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n\n# With strict_input_validation=True, sending {\"a\": \"10\", \"b\": \"20\"} will fail\n# With strict_input_validation=False (default), it will be coerced to integers\n```\n\n**Validation Behavior Comparison:**\n\n| Input Type | strict_input_validation=False (default) | strict_input_validation=True |\n| :--------- | :-------------------------------------- | :--------------------------- |\n| String integers (`\"10\"` for `int`) | ✅ Coerced to integer | ❌ Validation error |\n| String floats (`\"3.14\"` for `float`) | ✅ Coerced to float | ❌ Validation error |\n| String booleans (`\"true\"` for `bool`) | ✅ Coerced to boolean | ❌ Validation error |\n| Lists with string elements (`[\"1\", \"2\"]` for `list[int]`) | ✅ Elements coerced | ❌ Validation error |\n| Pydantic model fields with type mismatches | ✅ Fields coerced | ❌ Validation error |\n| Invalid values (`\"abc\"` for `int`) | ❌ Validation error | ❌ Validation error |\n\n<Note>\n**Note on Pydantic Models:** Even with `strict_input_validation=False`, Pydantic model parameters must be provided as JSON objects (dicts), not as stringified JSON. For example, `{\"user\": {\"name\": \"Alice\"}}` works, but `{\"user\": '{\"name\": \"Alice\"}'}` does not.\n</Note>\n\nThe default flexible validation mode is recommended for most use cases as it handles common LLM client behaviors gracefully while still providing strong type safety through Pydantic's validation.\n\n### Parameter Metadata\n\nYou can provide additional metadata about parameters in several ways:\n\n#### Simple String Descriptions\n\n<VersionBadge version=\"2.11.0\" />\n\nFor basic parameter descriptions, you can use a convenient shorthand with `Annotated`:\n\n```python \nfrom typing import Annotated\n\n@mcp.tool\ndef process_image(\n    image_url: Annotated[str, \"URL of the image to process\"],\n    resize: Annotated[bool, \"Whether to resize the image\"] = False,\n    width: Annotated[int, \"Target width in pixels\"] = 800,\n    format: Annotated[str, \"Output image format\"] = \"jpeg\"\n) -> dict:\n    \"\"\"Process an image with optional resizing.\"\"\"\n    # Implementation...\n```\n\nThis shorthand syntax is equivalent to using `Field(description=...)` but more concise for simple descriptions.\n\n<Tip>\nThis shorthand syntax is only applied to `Annotated` types with a single string description. \n</Tip>\n\n#### Advanced Metadata with Field\n\nFor validation constraints and advanced metadata, use Pydantic's `Field` class with `Annotated`:\n\n```python\nfrom typing import Annotated\nfrom pydantic import Field\n\n@mcp.tool\ndef process_image(\n    image_url: Annotated[str, Field(description=\"URL of the image to process\")],\n    resize: Annotated[bool, Field(description=\"Whether to resize the image\")] = False,\n    width: Annotated[int, Field(description=\"Target width in pixels\", ge=1, le=2000)] = 800,\n    format: Annotated[\n        Literal[\"jpeg\", \"png\", \"webp\"], \n        Field(description=\"Output image format\")\n    ] = \"jpeg\"\n) -> dict:\n    \"\"\"Process an image with optional resizing.\"\"\"\n    # Implementation...\n```\n\n\nYou can also use the Field as a default value, though the Annotated approach is preferred:\n\n```python\n@mcp.tool\ndef search_database(\n    query: str = Field(description=\"Search query string\"),\n    limit: int = Field(10, description=\"Maximum number of results\", ge=1, le=100)\n) -> list:\n    \"\"\"Search the database with the provided query.\"\"\"\n    # Implementation...\n```\n\nField provides several validation and documentation features:\n- `description`: Human-readable explanation of the parameter (shown to LLMs)\n- `ge`/`gt`/`le`/`lt`: Greater/less than (or equal) constraints\n- `min_length`/`max_length`: String or collection length constraints\n- `pattern`: Regex pattern for string validation\n- `default`: Default value if parameter is omitted\n\n### Hiding Parameters from the LLM\n\n<VersionBadge version=\"2.14.0\" />\n\nTo inject values at runtime without exposing them to the LLM (such as `user_id`, credentials, or database connections), use dependency injection with `Depends()`. Parameters using `Depends()` are automatically excluded from the tool schema:\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Depends\n\nmcp = FastMCP()\n\ndef get_user_id() -> str:\n    return \"user_123\"  # Injected at runtime\n\n@mcp.tool\ndef get_user_details(user_id: str = Depends(get_user_id)) -> str:\n    # user_id is injected by the server, not provided by the LLM\n    return f\"Details for {user_id}\"\n```\n\nSee [Custom Dependencies](/v2/servers/context#custom-dependencies) for more details on dependency injection.\n\n## Return Values\n\n\nFastMCP tools can return data in two complementary formats: **traditional content blocks** (like text and images) and **structured outputs** (machine-readable JSON). When you add return type annotations, FastMCP automatically generates **output schemas** to validate the structured data and enables clients to deserialize results back to Python objects.\n\nUnderstanding how these three concepts work together:\n\n- **Return Values**: What your Python function returns (determines both content blocks and structured data)\n- **Structured Outputs**: JSON data sent alongside traditional content for machine processing  \n- **Output Schemas**: JSON Schema declarations that describe and validate the structured output format\n\nThe following sections explain each concept in detail.\n\n### Content Blocks\n\nFastMCP automatically converts tool return values into appropriate MCP content blocks:\n\n- **`str`**: Sent as `TextContent`\n- **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (within an `EmbeddedResource`)\n- **`fastmcp.utilities.types.Image`**: Sent as `ImageContent`\n- **`fastmcp.utilities.types.Audio`**: Sent as `AudioContent`\n- **`fastmcp.utilities.types.File`**: Sent as base64-encoded `EmbeddedResource`\n- **MCP SDK content blocks**: Sent as-is\n- **A list of any of the above**: Converts each item according to the above rules\n- **`None`**: Results in an empty response\n\n#### Media Helper Classes\n\nFastMCP provides helper classes for returning images, audio, and files. When you return one of these classes, either directly or as part of a list, FastMCP automatically converts it to the appropriate MCP content block. For example, if you return a `fastmcp.utilities.types.Image` object, FastMCP will convert it to an MCP `ImageContent` block with the correct MIME type and base64 encoding.\n\n```python\nfrom fastmcp.utilities.types import Image, Audio, File\n\n@mcp.tool\ndef get_chart() -> Image:\n    \"\"\"Generate a chart image.\"\"\"\n    return Image(path=\"chart.png\")\n\n@mcp.tool\ndef get_multiple_charts() -> list[Image]:\n    \"\"\"Return multiple charts.\"\"\"\n    return [Image(path=\"chart1.png\"), Image(path=\"chart2.png\")]\n```\n\n<Tip>\nHelper classes are only automatically converted to MCP content blocks when returned **directly** or as part of a **list**. For more complex containers like dicts, you can manually convert them to MCP types:\n\n```python\n# ✅ Automatic conversion\nreturn Image(path=\"chart.png\")\nreturn [Image(path=\"chart1.png\"), \"text content\"]\n\n# ❌ Will not be automatically converted\nreturn {\"image\": Image(path=\"chart.png\")}\n\n# ✅ Manual conversion for nested use\nreturn {\"image\": Image(path=\"chart.png\").to_image_content()}\n```\n</Tip>\n\nEach helper class accepts either `path=` or `data=` (mutually exclusive):\n- **`path`**: File path (string or Path object) - MIME type detected from extension\n- **`data`**: Raw bytes - requires `format=` parameter for MIME type\n- **`format`**: Optional format override (e.g., \"png\", \"wav\", \"pdf\")\n- **`name`**: Optional name for `File` when using `data=`\n- **`annotations`**: Optional MCP annotations for the content\n\n### Structured Output\n\n<VersionBadge version=\"2.10.0\" />\n\nThe 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content) structured content, which is a new way to return data from tools. Structured content is a JSON object that is sent alongside traditional content. FastMCP automatically creates structured outputs alongside traditional content when your tool returns data that has a JSON object representation. This provides machine-readable JSON data that clients can deserialize back to Python objects.\n\n**Automatic Structured Content Rules:**\n- **Object-like results** (`dict`, Pydantic models, dataclasses) → Always become structured content (even without output schema)  \n- **Non-object results** (`int`, `str`, `list`) → Only become structured content if there's an output schema to validate/serialize them\n- **All results** → Always become traditional content blocks for backward compatibility\n\n<Note>\nThis automatic behavior enables clients to receive machine-readable data alongside human-readable content without requiring explicit output schemas for object-like returns.\n</Note>\n\n#### Dictionaries and Objects\n\nWhen your tool returns a dictionary, dataclass, or Pydantic model, FastMCP automatically creates structured content from it. The structured content contains the actual object data, making it easy for clients to deserialize back to native objects.\n\n<CodeGroup>\n```python Tool Definition\n@mcp.tool\ndef get_user_data(user_id: str) -> dict:\n    \"\"\"Get user data.\"\"\"\n    return {\"name\": \"Alice\", \"age\": 30, \"active\": True}\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"{\\n  \\\"name\\\": \\\"Alice\\\",\\n  \\\"age\\\": 30,\\n  \\\"active\\\": true\\n}\"\n    }\n  ],\n  \"structuredContent\": {\n    \"name\": \"Alice\",\n    \"age\": 30,\n    \"active\": true\n  }\n}\n```\n</CodeGroup>\n\n#### Primitives and Collections\n\nWhen your tool returns a primitive type (int, str, bool) or a collection (list, set), FastMCP needs a return type annotation to generate structured content. The annotation tells FastMCP how to validate and serialize the result.\n\nWithout a type annotation, the tool only produces `content`:\n\n<CodeGroup>\n```python Tool Definition\n@mcp.tool\ndef calculate_sum(a: int, b: int):\n    \"\"\"Calculate sum without return annotation.\"\"\"\n    return a + b  # Returns 8\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"8\"\n    }\n  ]\n}\n```\n</CodeGroup>\n\nWhen you add a return annotation, such as `-> int`, FastMCP generates `structuredContent` by wrapping the primitive value in a `{\"result\": ...}` object, since JSON schemas require object-type roots for structured output:\n\n<CodeGroup>\n```python Tool Definition\n@mcp.tool\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Calculate sum with return annotation.\"\"\"\n    return a + b  # Returns 8\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"8\"\n    }\n  ],\n  \"structuredContent\": {\n    \"result\": 8\n  }\n}\n```\n</CodeGroup>\n\n#### Typed Models\n\nReturn type annotations work with any type that can be converted to a JSON schema. Dataclasses and Pydantic models are particularly useful because FastMCP extracts their field definitions to create detailed schemas.\n\n<CodeGroup>\n```python Tool Definition\nfrom dataclasses import dataclass\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@dataclass\nclass Person:\n    name: str\n    age: int\n    email: str\n\n@mcp.tool\ndef get_user_profile(user_id: str) -> Person:\n    \"\"\"Get a user's profile information.\"\"\"\n    return Person(\n        name=\"Alice\",\n        age=30,\n        email=\"alice@example.com\",\n    )\n```\n\n```json Generated Output Schema\n{\n  \"properties\": {\n    \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n    \"age\": {\"title\": \"Age\", \"type\": \"integer\"},\n    \"email\": {\"title\": \"Email\", \"type\": \"string\"}\n  },\n  \"required\": [\"name\", \"age\", \"email\"],\n  \"title\": \"Person\",\n  \"type\": \"object\"\n}\n```\n\n```json MCP Result\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"{\\\"name\\\": \\\"Alice\\\", \\\"age\\\": 30, \\\"email\\\": \\\"alice@example.com\\\"}\"\n    }\n  ],\n  \"structuredContent\": {\n    \"name\": \"Alice\",\n    \"age\": 30,\n    \"email\": \"alice@example.com\"\n  }\n}\n```\n</CodeGroup>\n\nThe `Person` dataclass becomes an output schema (second tab) that describes the expected format. When executed, clients receive the result (third tab) with both `content` and `structuredContent` fields.\n\n### Output Schemas\n\n<VersionBadge version=\"2.10.0\" />\n\nThe 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) output schemas, which are a new way to describe the expected output format of a tool. When an output schema is provided, the tool *must* return structured output that matches the schema.\n\nWhen you add return type annotations to your functions, FastMCP automatically generates JSON schemas that describe the expected output format. These schemas help MCP clients understand and validate the structured data they receive.\n\n#### Primitive Type Wrapping\n\nFor primitive return types (like `int`, `str`, `bool`), FastMCP automatically wraps the result under a `\"result\"` key to create valid structured output:\n\n<CodeGroup>\n```python Primitive Return Type\n@mcp.tool\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\n```json Generated Schema (Wrapped)\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"result\": {\"type\": \"integer\"}\n  },\n  \"x-fastmcp-wrap-result\": true\n}\n```\n\n```json Structured Output\n{\n  \"result\": 8\n}\n```\n</CodeGroup>\n\n#### Manual Schema Control\n\nYou can override the automatically generated schema by providing a custom `output_schema`:\n\n```python\n@mcp.tool(output_schema={\n    \"type\": \"object\", \n    \"properties\": {\n        \"data\": {\"type\": \"string\"},\n        \"metadata\": {\"type\": \"object\"}\n    }\n})\ndef custom_schema_tool() -> dict:\n    \"\"\"Tool with custom output schema.\"\"\"\n    return {\"data\": \"Hello\", \"metadata\": {\"version\": \"1.0\"}}\n```\n\nSchema generation works for most common types including basic types, collections, union types, Pydantic models, TypedDict structures, and dataclasses.\n\n<Warning>\n**Important Constraints**: \n- Output schemas must be object types (`\"type\": \"object\"`)\n- If you provide an output schema, your tool **must** return structured output that matches it\n- However, you can provide structured output without an output schema (using `ToolResult`)\n</Warning>\n\n### ToolResult and Metadata\n\nFor complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata.\n\n```python\nfrom fastmcp.tools.tool import ToolResult\nfrom mcp.types import TextContent\n\n@mcp.tool\ndef advanced_tool() -> ToolResult:\n    \"\"\"Tool with full control over output.\"\"\"\n    return ToolResult(\n        content=[TextContent(type=\"text\", text=\"Human-readable summary\")],\n        structured_content={\"data\": \"value\", \"count\": 42},\n        meta={\"execution_time_ms\": 145}\n    )\n```\n\n`ToolResult` accepts three fields:\n\n**`content`** - The traditional MCP content blocks that clients display to users. Can be a string (automatically converted to `TextContent`), a list of MCP content blocks, or any serializable value (converted to JSON string). At least one of `content` or `structured_content` must be provided.\n\n```python\n# Simple string\nToolResult(content=\"Hello, world!\")\n\n# List of content blocks\nToolResult(content=[\n    TextContent(type=\"text\", text=\"Result: 42\"),\n    ImageContent(type=\"image\", data=\"base64...\", mimeType=\"image/png\")\n])\n```\n\n**`structured_content`** - A dictionary containing structured data that matches your tool's output schema. This enables clients to programmatically process the results. If you provide `structured_content`, it must be a dictionary or `None`. If only `structured_content` is provided, it will also be used as `content` (converted to JSON string).\n\n```python\nToolResult(\n    content=\"Found 3 users\",\n    structured_content={\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}\n)\n```\n\n**`meta`** \n<VersionBadge version=\"2.13.1\" />\nRuntime metadata about the tool execution. Use this for performance metrics, debugging information, or any client-specific data that doesn't belong in the content or structured output.\n\n```python\nToolResult(\n    content=\"Analysis complete\",\n    structured_content={\"result\": \"positive\"},\n    meta={\n        \"execution_time_ms\": 145,\n        \"model_version\": \"2.1\",\n        \"confidence\": 0.95\n    }\n)\n```\n\n<Note>\nThe `meta` field in `ToolResult` is for runtime metadata about tool execution (e.g., execution time, performance metrics). This is separate from the `meta` parameter in `@mcp.tool(meta={...})`, which provides static metadata about the tool definition itself.\n</Note>\n\nWhen returning `ToolResult`, you have full control - FastMCP won't automatically wrap or transform your data. `ToolResult` can be returned with or without an output schema.\n\n## Error Handling\n\n<VersionBadge version=\"2.4.1\" />\n\nIf your tool encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ToolError`.\n\nBy default, all exceptions (including their details) are logged and converted into an MCP error response to be sent back to the client LLM. This helps the LLM understand failures and react appropriately.\n\nIf you want to mask internal error details for security reasons, you can:\n\n1. Use the `mask_error_details=True` parameter when creating your `FastMCP` instance:\n```python\nmcp = FastMCP(name=\"SecureServer\", mask_error_details=True)\n```\n\n2. Or use `ToolError` to explicitly control what error information is sent to clients:\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import ToolError\n\n@mcp.tool\ndef divide(a: float, b: float) -> float:\n    \"\"\"Divide a by b.\"\"\"\n\n    if b == 0:\n        # Error messages from ToolError are always sent to clients,\n        # regardless of mask_error_details setting\n        raise ToolError(\"Division by zero is not allowed.\")\n    \n    # If mask_error_details=True, this message would be masked\n    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):\n        raise TypeError(\"Both arguments must be numbers.\")\n        \n    return a / b\n```\n\nWhen `mask_error_details=True`, only error messages from `ToolError` will include details, other exceptions will be converted to a generic message.\n\n## Disabling Tools\n\n<VersionBadge version=\"2.8.0\" />\n\nYou can control the visibility and availability of tools by enabling or disabling them. This is useful for feature flagging, maintenance, or dynamically changing the toolset available to a client. Disabled tools will not appear in the list of available tools returned by `list_tools`, and attempting to call a disabled tool will result in an \"Unknown tool\" error, just as if the tool did not exist.\n\nBy default, all tools are enabled. You can disable a tool upon creation using the `enabled` parameter in the decorator:\n\n```python\n@mcp.tool(enabled=False)\ndef maintenance_tool():\n    \"\"\"This tool is currently under maintenance.\"\"\"\n    return \"This tool is disabled.\"\n```\n\nYou can also toggle a tool's state programmatically after it has been created:\n\n```python\n@mcp.tool\ndef dynamic_tool():\n    return \"I am a dynamic tool.\"\n\n# Disable and re-enable the tool\ndynamic_tool.disable()\ndynamic_tool.enable()\n```\n## MCP Annotations\n\n<VersionBadge version=\"2.2.7\" />\n\nFastMCP allows you to add specialized metadata to your tools through annotations. These annotations communicate how tools behave to client applications without consuming token context in LLM prompts.\n\nAnnotations serve several purposes in client applications:\n- Adding user-friendly titles for display purposes\n- Indicating whether tools modify data or systems\n- Describing the safety profile of tools (destructive vs. non-destructive)\n- Signaling if tools interact with external systems\n\nYou can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:\n\n```python\n@mcp.tool(\n    annotations={\n        \"title\": \"Calculate Sum\",\n        \"readOnlyHint\": True,\n        \"openWorldHint\": False\n    }\n)\ndef calculate_sum(a: float, b: float) -> float:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n```\n\nFastMCP supports these standard annotations:\n\n| Annotation | Type | Default | Purpose |\n| :--------- | :--- | :------ | :------ |\n| `title` | string | - | Display name for user interfaces |\n| `readOnlyHint` | boolean | false | Indicates if the tool only reads without making changes |\n| `destructiveHint` | boolean | true | For non-readonly tools, signals if changes are destructive |\n| `idempotentHint` | boolean | false | Indicates if repeated identical calls have the same effect as a single call |\n| `openWorldHint` | boolean | true | Specifies if the tool interacts with external systems |\n\nRemember that annotations help make better user experiences but should be treated as advisory hints. They help client applications present appropriate UI elements and safety controls, but won't enforce security boundaries on their own. Always focus on making your annotations accurately represent what your tool actually does.\n\n### Using Annotation Hints\n\nMCP clients like Claude and ChatGPT use annotation hints to determine when to skip confirmation prompts and how to present tools to users. The most commonly used hint is `readOnlyHint`, which signals that a tool only reads data without making changes.\n\n**Read-only tools** improve user experience by:\n- Skipping confirmation prompts for safe operations\n- Allowing broader access without security concerns\n- Enabling more aggressive batching and caching\n\nMark a tool as read-only when it retrieves data, performs calculations, or checks status without modifying state:\n\n```python\nfrom fastmcp import FastMCP\nfrom mcp.types import ToolAnnotations\n\nmcp = FastMCP(\"Data Server\")\n\n@mcp.tool(annotations={\"readOnlyHint\": True})\ndef get_user(user_id: str) -> dict:\n    \"\"\"Retrieve user information by ID.\"\"\"\n    return {\"id\": user_id, \"name\": \"Alice\"}\n\n@mcp.tool(\n    annotations=ToolAnnotations(\n        readOnlyHint=True,\n        idempotentHint=True,  # Same result for repeated calls\n        openWorldHint=False   # Only internal data\n    )\n)\ndef search_products(query: str) -> list[dict]:\n    \"\"\"Search the product catalog.\"\"\"\n    return [{\"id\": 1, \"name\": \"Widget\", \"price\": 29.99}]\n\n# Write operations - no readOnlyHint\n@mcp.tool()\ndef update_user(user_id: str, name: str) -> dict:\n    \"\"\"Update user information.\"\"\"\n    return {\"id\": user_id, \"name\": name, \"updated\": True}\n\n@mcp.tool(annotations={\"destructiveHint\": True})\ndef delete_user(user_id: str) -> dict:\n    \"\"\"Permanently delete a user account.\"\"\"\n    return {\"deleted\": user_id}\n```\n\nFor tools that write to databases, send notifications, create/update/delete resources, or trigger workflows, omit `readOnlyHint` or set it to `False`. Use `destructiveHint=True` for operations that cannot be undone.\n\nClient-specific behavior:\n- **ChatGPT**: Skips confirmation prompts for read-only tools in Chat mode (see [ChatGPT integration](/v2/integrations/chatgpt))\n- **Claude**: Uses hints to understand tool safety profiles and make better execution decisions\n\n## Notifications\n\n<VersionBadge version=\"2.9.1\" />\n\nFastMCP automatically sends `notifications/tools/list_changed` notifications to connected clients when tools are added, removed, enabled, or disabled. This allows clients to stay up-to-date with the current tool set without manually polling for changes.\n\n```python\n@mcp.tool\ndef example_tool() -> str:\n    return \"Hello!\"\n\n# These operations trigger notifications:\nmcp.add_tool(example_tool)     # Sends tools/list_changed notification\nexample_tool.disable()         # Sends tools/list_changed notification  \nexample_tool.enable()          # Sends tools/list_changed notification\nmcp.remove_tool(\"example_tool\") # Sends tools/list_changed notification\n```\n\nNotifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.\n\nClients can handle these notifications using a [message handler](/v2/clients/messages) to automatically refresh their tool lists or update their interfaces.\n\n## Accessing the MCP Context\n\nTools can access MCP features like logging, reading resources, or reporting progress through the `Context` object. To use it, add a parameter to your tool function with the type hint `Context`.\n\n```python\nfrom fastmcp import FastMCP, Context\n\nmcp = FastMCP(name=\"ContextDemo\")\n\n@mcp.tool\nasync def process_data(data_uri: str, ctx: Context) -> dict:\n    \"\"\"Process data from a resource with progress reporting.\"\"\"\n    await ctx.info(f\"Processing data from {data_uri}\")\n    \n    # Read a resource\n    resource = await ctx.read_resource(data_uri)\n    data = resource[0].content if resource else \"\"\n    \n    # Report progress\n    await ctx.report_progress(progress=50, total=100)\n    \n    # Example request to the client's LLM for help\n    summary = await ctx.sample(f\"Summarize this in 10 words: {data[:200]}\")\n    \n    await ctx.report_progress(progress=100, total=100)\n    return {\n        \"length\": len(data),\n        \"summary\": summary.text\n    }\n```\n\nThe Context object provides access to:\n\n- **Logging**: `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`\n- **Progress Reporting**: `ctx.report_progress(progress, total)`\n- **Resource Access**: `ctx.read_resource(uri)`\n- **LLM Sampling**: `ctx.sample(...)`\n- **Request Information**: `ctx.request_id`, `ctx.client_id`\n\nFor full documentation on the Context object and all its capabilities, see the [Context documentation](/v2/servers/context).\n\n## Server Behavior\n\n### Duplicate Tools\n\n<VersionBadge version=\"2.1.0\" />\n\nYou can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\n    name=\"StrictServer\",\n    # Configure behavior for duplicate tool names\n    on_duplicate_tools=\"error\"\n)\n\n@mcp.tool\ndef my_tool(): return \"Version 1\"\n\n# This will now raise a ValueError because 'my_tool' already exists\n# and on_duplicate_tools is set to \"error\".\n# @mcp.tool\n# def my_tool(): return \"Version 2\"\n```\n\nThe duplicate behavior options are:\n\n-   `\"warn\"` (default): Logs a warning and the new tool replaces the old one.\n-   `\"error\"`: Raises a `ValueError`, preventing the duplicate registration.\n-   `\"replace\"`: Silently replaces the existing tool with the new one.\n-   `\"ignore\"`: Keeps the original tool and ignores the new registration attempt.\n\n### Removing Tools\n\n<VersionBadge version=\"2.3.4\" />\n\nYou can dynamically remove tools from a server using the `remove_tool` method:\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"DynamicToolServer\")\n\n@mcp.tool\ndef calculate_sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\nmcp.remove_tool(\"calculate_sum\")\n```\n"
  },
  {
    "path": "docs/v2/tutorials/create-mcp-server.mdx",
    "content": "---\ntitle: \"How to Create an MCP Server in Python\"\nsidebarTitle: \"Creating an MCP Server\"\ndescription: \"A step-by-step guide to building a Model Context Protocol (MCP) server using Python and FastMCP, from basic tools to dynamic resources.\"\nicon: server\n---\n\nSo you want to build a Model Context Protocol (MCP) server in Python. The goal is to create a service that can provide tools and data to AI models like Claude, Gemini, or others that support the protocol. While the [MCP specification](https://modelcontextprotocol.io/specification/) is powerful, implementing it from scratch involves a lot of boilerplate: handling JSON-RPC, managing session state, and correctly formatting requests and responses.\n\nThis is where **FastMCP** comes in. It's a high-level framework that handles all the protocol complexities for you, letting you focus on what matters: writing the Python functions that power your server.\n\nThis guide will walk you through creating a fully-featured MCP server from scratch using FastMCP.\n\n<Tip>\nEvery code block in this tutorial is a complete, runnable example. You can copy and paste it into a file and run it, or paste it directly into a Python REPL like IPython to try it out.\n</Tip>\n\n### Prerequisites\n\nMake sure you have FastMCP installed. If not, follow the [installation guide](/v2/getting-started/installation).\n\n```bash\npip install fastmcp\n```\n\n\n## Step 1: Create the Basic Server\n\nEvery FastMCP application starts with an instance of the `FastMCP` class. This object acts as the container for all your tools and resources.\n\nCreate a new file called `my_mcp_server.py`:\n\n```python my_mcp_server.py\nfrom fastmcp import FastMCP\n\n# Create a server instance with a descriptive name\nmcp = FastMCP(name=\"My First MCP Server\")\n```\n\nThat's it! You have a valid (though empty) MCP server. Now, let's add some functionality.\n\n## Step 2: Add a Tool\n\nTools are functions that an LLM can execute. Let's create a simple tool that adds two numbers.\n\nTo do this, simply write a standard Python function and decorate it with `@mcp.tool`.\n\n```python my_mcp_server.py {5-8}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n```\n\nFastMCP automatically handles the rest:\n- **Tool Name:** It uses the function name (`add`) as the tool's name.\n- **Description:** It uses the function's docstring as the tool's description for the LLM.\n- **Schema:** It inspects the type hints (`a: int`, `b: int`) to generate a JSON schema for the inputs.\n\nThis is the core philosophy of FastMCP: **write Python, not protocol boilerplate.**\n\n## Step 3: Expose Data with Resources\n\nResources provide read-only data to the LLM. You can define a resource by decorating a function with `@mcp.resource`, providing a unique URI.\n\nLet's expose a simple configuration dictionary as a resource.\n\n```python my_mcp_server.py {10-13}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n```\n\nWhen a client requests the URI `resource://config`, FastMCP will execute the `get_config` function and return its output (serialized as JSON) to the client. The function is only called when the resource is requested, enabling lazy-loading of data.\n\n## Step 4: Generate Dynamic Content with Resource Templates\n\nSometimes, you need to generate resources based on parameters. This is what **Resource Templates** are for. You define them using the same `@mcp.resource` decorator but with placeholders in the URI.\n\nLet's create a template that provides a personalized greeting.\n\n```python my_mcp_server.py {15-17}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n\n@mcp.resource(\"greetings://{name}\")\ndef personalized_greeting(name: str) -> str:\n    \"\"\"Generates a personalized greeting for the given name.\"\"\"\n    return f\"Hello, {name}! Welcome to the MCP server.\"\n```\n\nNow, clients can request dynamic URIs:\n- `greetings://Ford` will call `personalized_greeting(name=\"Ford\")`.\n- `greetings://Marvin` will call `personalized_greeting(name=\"Marvin\")`.\n\nFastMCP automatically maps the `{name}` placeholder in the URI to the `name` parameter in your function.\n\n## Step 5: Run the Server\n\nTo make your server executable, add a `__main__` block to your script that calls `mcp.run()`.\n\n```python my_mcp_server.py {19-20}\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(name=\"My First MCP Server\")\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n\n@mcp.resource(\"greetings://{name}\")\ndef personalized_greeting(name: str) -> str:\n    \"\"\"Generates a personalized greeting for the given name.\"\"\"\n    return f\"Hello, {name}! Welcome to the MCP server.\"\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\nNow you can run your server from the command line:\n```bash\npython my_mcp_server.py\n```\nThis starts the server using the default **STDIO transport**, which is how clients like Claude Desktop communicate with local servers. To learn about other transports, like HTTP, see the [Running Your Server](/v2/deployment/running-server) guide.\n\n## The Complete Server\n\nHere is the full code for `my_mcp_server.py` (click to expand):\n\n```python my_mcp_server.py [expandable]\nfrom fastmcp import FastMCP\n\n# 1. Create the server\nmcp = FastMCP(name=\"My First MCP Server\")\n\n# 2. Add a tool\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Adds two integer numbers together.\"\"\"\n    return a + b\n\n# 3. Add a static resource\n@mcp.resource(\"resource://config\")\ndef get_config() -> dict:\n    \"\"\"Provides the application's configuration.\"\"\"\n    return {\"version\": \"1.0\", \"author\": \"MyTeam\"}\n\n# 4. Add a resource template for dynamic content\n@mcp.resource(\"greetings://{name}\")\ndef personalized_greeting(name: str) -> str:\n    \"\"\"Generates a personalized greeting for the given name.\"\"\"\n    return f\"Hello, {name}! Welcome to the MCP server.\"\n\n# 5. Make the server runnable\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\n## Next Steps\n\nYou've successfully built an MCP server! From here, you can explore more advanced topics:\n\n-   [**Tools in Depth**](/v2/servers/tools): Learn about asynchronous tools, error handling, and custom return types.\n-   [**Resources & Templates**](/v2/servers/resources): Discover different resource types, including files and HTTP endpoints.\n-   [**Prompts**](/v2/servers/prompts): Create reusable prompt templates for your LLM.\n-   [**Running Your Server**](/v2/deployment/running-server): Deploy your server with different transports like HTTP.\n\n"
  },
  {
    "path": "docs/v2/tutorials/mcp.mdx",
    "content": "---\ntitle: \"What is the Model Context Protocol (MCP)?\"\nsidebarTitle: \"What is MCP?\"\ndescription: \"An introduction to the core concepts of the Model Context Protocol (MCP), explaining what it is, why it's useful, and how it works.\"\nicon: \"diagram-project\"\n---\n\nThe Model Context Protocol (MCP) is an open standard designed to solve a fundamental problem in AI development: how can Large Language Models (LLMs) reliably and securely interact with external tools, data, and services?\n\nIt's the **bridge between the probabilistic, non-deterministic world of AI and the deterministic, reliable world of your code and data.**\n\nWhile you could build a custom REST API for your LLM, MCP provides a specialized, standardized \"port\" for AI-native communication. Think of it as **USB-C for AI**: a single, well-defined interface for connecting any compliant LLM to any compliant tool or data source.\n\nThis guide provides a high-level overview of the protocol itself. We'll use **FastMCP**, the leading Python framework for MCP, to illustrate the concepts with simple code examples.\n\n## Why Do We Need a Protocol?\n\nWith countless APIs already in existence, the most common question is: \"Why do we need another one?\"\n\nThe answer lies in **standardization**. The AI ecosystem is fragmented. Every model provider has its own way of defining and calling tools. MCP's goal is to create a common language that offers several key advantages:\n\n1.  **Interoperability:** Build one MCP server, and it can be used by any MCP-compliant client (Claude, Gemini, OpenAI, custom agents, etc.) without custom integration code. This is the protocol's most important promise.\n2.  **Discoverability:** Clients can dynamically ask a server what it's capable of at runtime. They receive a structured, machine-readable \"menu\" of tools and resources.\n3.  **Security & Safety:** MCP provides a clear, sandboxed boundary. An LLM can't execute arbitrary code on your server; it can only *request* to run the specific, typed, and validated functions you explicitly expose.\n4.  **Composability:** You can build small, specialized MCP servers and combine them to create powerful, complex applications.\n\n## Core MCP Components \n\nAn MCP server exposes its capabilities through three primary components: Tools, Resources, and Prompts.\n\n### Tools: Executable Actions\n\nTools are functions that the LLM can ask the server to execute. They are the action-oriented part of MCP.\n\nIn the spirit of a REST API, you can think of **Tools as being like `POST` requests.** They are used to *perform an action*, *change state*, or *trigger a side effect*, like sending an email, adding a user to a database, or making a calculation.\n\nWith FastMCP, creating a tool is as simple as decorating a Python function.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n# This function is now an MCP tool named \"get_weather\"\n@mcp.tool\ndef get_weather(city: str) -> dict:\n    \"\"\"Gets the current weather for a specific city.\"\"\"\n    # In a real app, this would call a weather API\n    return {\"city\": city, \"temperature\": \"72F\", \"forecast\": \"Sunny\"}\n```\n\n[**Learn more about Tools →**](/v2/servers/tools)\n\n### Resources: Read-Only Data\n\nResources are data sources that the LLM can read. They are used to load information into the LLM's context, providing it with knowledge it doesn't have from its training data.\n\nFollowing the REST API analogy, **Resources are like `GET` requests.** Their purpose is to *retrieve information* idempotently, ideally without causing side effects. A resource can be anything from a static text file to a dynamic piece of data from a database. Each resource is identified by a unique URI.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n# This function provides a resource at the URI \"system://status\"\n@mcp.resource(\"system://status\")\ndef get_system_status() -> dict:\n    \"\"\"Returns the current operational status of the service.\"\"\"\n    return {\"status\": \"all systems normal\"}\n```\n\n#### Resource Templates\n\nYou can also create **Resource Templates** for dynamic data. A client could request `users://42/profile` to get the profile for a specific user.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n# This template provides user data for any given user ID\n@mcp.resource(\"users://{user_id}/profile\")\ndef get_user_profile(user_id: str) -> dict:\n    \"\"\"Returns the profile for a specific user.\"\"\"\n    # Fetch user from a database...\n    return {\"id\": user_id, \"name\": \"Zaphod Beeblebrox\"}\n```\n\n[**Learn more about Resources & Templates →**](/v2/servers/resources)\n\n### Prompts: Reusable Instructions\n\nPrompts are reusable, parameterized message templates. They provide a way to define consistent, structured instructions that a client can request to guide the LLM's behavior for a specific task.\n\n```python\nfrom fastmcp import FastMCP\n\nmcp = FastMCP()\n\n@mcp.prompt\ndef summarize_text(text_to_summarize: str) -> str:\n    \"\"\"Creates a prompt asking the LLM to summarize a piece of text.\"\"\"\n    return f\"\"\"\n        Please provide a concise, one-paragraph summary of the following text:\n        \n        {text_to_summarize}\n        \"\"\"\n```\n\n[**Learn more about Prompts →**](/v2/servers/prompts)\n\n## Advanced Capabilities\n\nBeyond the core components, MCP also supports more advanced interaction patterns, such as a server requesting that the *client's* LLM generate a completion (known as **sampling**), or a server sending asynchronous **notifications** to a client. These features enable more complex, bidirectional workflows and are fully supported by FastMCP.\n\n## Next Steps\n\nNow that you understand the core concepts of the Model Context Protocol, you're ready to start building. The best place to begin is our step-by-step tutorial.\n\n[**Tutorial: How to Create an MCP Server in Python →**](/v2/tutorials/create-mcp-server)\n"
  },
  {
    "path": "docs/v2/tutorials/rest-api.mdx",
    "content": "---\ntitle: \"How to Connect an LLM to a REST API\"\nsidebarTitle: \"Connect LLMs to REST APIs\"\ndescription: \"A step-by-step guide to making any REST API with an OpenAPI spec available to LLMs using FastMCP.\"\nicon: \"plug\"\n---\n\nYou've built a powerful REST API, and now you want your LLM to be able to use it. Manually writing a wrapper function for every single endpoint is tedious, error-prone, and hard to maintain.\n\nThis is where **FastMCP** shines. If your API has an OpenAPI (or Swagger) specification, FastMCP can automatically convert your entire API into a fully-featured MCP server, making every endpoint available as a secure, typed tool for your AI model.\n\nThis guide will walk you through converting a public REST API into an MCP server in just a few lines of code.\n\n<Tip>\nEvery code block in this tutorial is a complete, runnable example. You can copy and paste it into a file and run it, or paste it directly into a Python REPL like IPython to try it out.\n</Tip>\n\n### Prerequisites\n\nMake sure you have FastMCP installed. If not, follow the [installation guide](/v2/getting-started/installation).\n\n```bash\npip install fastmcp\n```\n\n## Step 1: Choose a Target API\n\nFor this tutorial, we'll use the [JSONPlaceholder API](https://jsonplaceholder.typicode.com/), a free, fake online REST API for testing and prototyping. It's perfect because it's simple and has a public OpenAPI specification.\n\n-   **API Base URL:** `https://jsonplaceholder.typicode.com`\n-   **OpenAPI Spec URL:** We'll use a community-provided spec for it.\n\n## Step 2: Create the MCP Server\n\nNow for the magic. We'll use `FastMCP.from_openapi`. This method takes an `httpx.AsyncClient` configured for your API and its OpenAPI specification, and automatically converts **every endpoint** into a callable MCP `Tool`.\n\n<Tip>\nLearn more about working with OpenAPI specs in the [OpenAPI integration docs](/v2/integrations/openapi).\n</Tip>\n\n<Note>\nFor this tutorial, we'll use a simplified OpenAPI spec directly in the code. In a real project, you would typically load the spec from a URL or local file.\n</Note>\n\nCreate a file named `api_server.py`:\n\n```python api_server.py {31-35}\nimport httpx\nfrom fastmcp import FastMCP\n\n# Create an HTTP client for the target API\nclient = httpx.AsyncClient(base_url=\"https://jsonplaceholder.typicode.com\")\n\n# Define a simplified OpenAPI spec for JSONPlaceholder\nopenapi_spec = {\n    \"openapi\": \"3.0.0\",\n    \"info\": {\"title\": \"JSONPlaceholder API\", \"version\": \"1.0\"},\n    \"paths\": {\n        \"/users\": {\n            \"get\": {\n                \"summary\": \"Get all users\",\n                \"operationId\": \"get_users\",\n                \"responses\": {\"200\": {\"description\": \"A list of users.\"}}\n            }\n        },\n        \"/users/{id}\": {\n            \"get\": {\n                \"summary\": \"Get a user by ID\",\n                \"operationId\": \"get_user_by_id\",\n                \"parameters\": [{\"name\": \"id\", \"in\": \"path\", \"required\": True, \"schema\": {\"type\": \"integer\"}}],\n                \"responses\": {\"200\": {\"description\": \"A single user.\"}}\n            }\n        }\n    }\n}\n\n# Create the MCP server from the OpenAPI spec\nmcp = FastMCP.from_openapi(\n    openapi_spec=openapi_spec,\n    client=client,\n    name=\"JSONPlaceholder MCP Server\"\n)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\n\nAnd that's it! With just a few lines of code, you've created an MCP server that exposes the entire JSONPlaceholder API as a collection of tools.\n\n## Step 3: Test the Generated Server\n\nLet's verify that our new MCP server works. We can use the `fastmcp.Client` to connect to it and inspect its tools.\n\n<Tip>\nLearn more about the FastMCP client in the [client docs](/v2/clients/client).\n</Tip>\n\nCreate a separate file, `api_client.py`:\n\n```python api_client.py {2, 6, 9, 16}\nimport asyncio\nfrom fastmcp import Client\n\nasync def main():\n    # Connect to the MCP server we just created\n    async with Client(\"http://127.0.0.1:8000/mcp\") as client:\n        \n        # List the tools that were automatically generated\n        tools = await client.list_tools()\n        print(\"Generated Tools:\")\n        for tool in tools:\n            print(f\"- {tool.name}\")\n            \n        # Call one of the generated tools\n        print(\"\\n\\nCalling tool 'get_user_by_id'...\")\n        user = await client.call_tool(\"get_user_by_id\", {\"id\": 1})\n        print(f\"Result:\\n{user.data}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nFirst, run your server:\n```bash\npython api_server.py\n```\n\nThen, in another terminal, run the client:\n```bash\npython api_client.py\n```\n\nYou should see a list of generated tools (`get_users`, `get_user_by_id`) and the result of calling the `get_user_by_id` tool, which fetches data from the live JSONPlaceholder API.\n\n![](/assets/images/tutorial-rest-api-result.png)\n\n\n## Step 4: Customizing Route Maps\n\nBy default, FastMCP converts every API endpoint into an MCP `Tool`. This ensures maximum compatibility with contemporary LLM clients, many of which **only support the `tools` part of the MCP specification.**\n\nHowever, for clients that support the full MCP spec, representing `GET` requests as `Resources` can be more semantically correct and efficient.\n\nFastMCP allows users to customize this behavior using the concept of \"route maps\". A `RouteMap` is a mapping of an API route to an MCP type. FastMCP checks each API route against your custom maps in order. If a route matches a map, it's converted to the specified `mcp_type`. Any route that doesn't match your custom maps will fall back to the default behavior (becoming a `Tool`).\n\n<Tip>\nLearn more about route maps in the [OpenAPI integration docs](/v2/integrations/openapi#route-mapping).\n</Tip>\n\nHere’s how you can add custom route maps to turn `GET` requests into `Resources` and `ResourceTemplates` (if they have path parameters):\n\n```python api_server_with_resources.py {3, 37-42}\nimport httpx\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import RouteMap, MCPType\n\n\n# Create an HTTP client for the target API\nclient = httpx.AsyncClient(base_url=\"https://jsonplaceholder.typicode.com\")\n\n# Define a simplified OpenAPI spec for JSONPlaceholder\nopenapi_spec = {\n    \"openapi\": \"3.0.0\",\n    \"info\": {\"title\": \"JSONPlaceholder API\", \"version\": \"1.0\"},\n    \"paths\": {\n        \"/users\": {\n            \"get\": {\n                \"summary\": \"Get all users\",\n                \"operationId\": \"get_users\",\n                \"responses\": {\"200\": {\"description\": \"A list of users.\"}}\n            }\n        },\n        \"/users/{id}\": {\n            \"get\": {\n                \"summary\": \"Get a user by ID\",\n                \"operationId\": \"get_user_by_id\",\n                \"parameters\": [{\"name\": \"id\", \"in\": \"path\", \"required\": True, \"schema\": {\"type\": \"integer\"}}],\n                \"responses\": {\"200\": {\"description\": \"A single user.\"}}\n            }\n        }\n    }\n}\n\n# Create the MCP server with custom route mapping\nmcp = FastMCP.from_openapi(\n    openapi_spec=openapi_spec,\n    client=client,\n    name=\"JSONPlaceholder MCP Server\",\n    route_maps=[\n        # Map GET requests with path parameters (e.g., /users/{id}) to ResourceTemplate\n        RouteMap(methods=[\"GET\"], pattern=r\".*\\{.*\\}.*\", mcp_type=MCPType.RESOURCE_TEMPLATE),\n        # Map all other GET requests to Resource\n        RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE),\n    ]\n)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n```\nWith this configuration:\n- `GET /users/{id}` becomes a `ResourceTemplate`.\n- `GET /users` becomes a `Resource`.\n- Any `POST`, `PUT`, etc. endpoints would still become `Tools` by default."
  },
  {
    "path": "docs/v2/updates.mdx",
    "content": "---\ntitle: \"FastMCP Updates\"\nsidebarTitle: \"Updates\"\nicon: \"sparkles\"\ntag: NEW\n---\n\n<Update label=\"FastMCP 2.14.5\" description=\"February 3, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.5: Sealed Docket\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5\"\ncta=\"Read the release notes\"\n>\nFixes a memory leak in the memory:// docket broker where cancelled tasks accumulated instead of being cleaned up. Bumps pydocket to ≥0.17.2.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.4\" description=\"January 22, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.4: Package Deal\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.4\"\ncta=\"Read the release notes\"\n>\nFixes a fresh install bug where the packaging library was missing as a direct dependency, plus backports $ref dereferencing in tool schemas and a task capabilities location fix.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.3\" description=\"January 12, 2026\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.3: Time After Timeout\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.3\"\ncta=\"Read the release notes\"\n>\nSometimes five seconds just isn't enough. This release fixes an HTTP transport bug that was cutting connections short, along with OAuth and Redis fixes, better ASGI support, and CLI update notifications so you never miss a beat.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.2\" description=\"December 31, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.2: Port Authority\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.2\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.14.2 brings a wave of community contributions safely into the 2.x line. A variety of important fixes backported from 3.0 work improve OpenAPI 3.1 compatibility, MCP spec compliance for output schemas and elicitation, and correct a subtle base_url fallback issue. The CLI now gently reminds you that FastMCP 3.0 is on the horizon.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.1\" description=\"December 15, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.1: 'Tis a Gift to Be Sample\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.1\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.14.1 introduces sampling with tools (SEP-1577), enabling servers to pass tools to `ctx.sample()` for agentic workflows where the LLM can automatically execute tool calls in a loop.\n\n🤖 **Sampling with tools** lets servers leverage client LLM capabilities for multi-step agentic workflows. The new `ctx.sample_step()` method provides single LLM calls with tool inspection, while `result_type` enables structured outputs via validated Pydantic models.\n\n🔧 **AnthropicSamplingHandler** joins the existing OpenAI handler, and both are now promoted from experimental to production-ready status with a unified API.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.14.0\" description=\"December 11, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.14.0: Task and You Shall Receive\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.0\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.14 begins adopting the MCP 2025-11-25 specification, introducing protocol-native background tasks that enable long-running operations to report progress without blocking clients.\n\n⏳ **Background Tasks (SEP-1686)** let you add `task=True` to any async tool decorator. Powered by [Docket](https://github.com/chrisguidry/docket) for enterprise task scheduling—in-memory backends work out-of-the-box, Redis enables persistence and horizontal scaling.\n\n🔧 **OpenAPI Parser Promoted** from experimental to standard with improved performance through single-pass schema processing.\n\n📋 **MCP Spec Updates** including SSE polling (SEP-1699), multi-select elicitation (SEP-1330), and tool name validation (SEP-986). Also removes deprecated APIs accumulated across 2.x.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.3\" description=\"December 3, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.3: Pin-ish Line\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.3\"\ncta=\"Read the release notes\"\n>\nPins `mcp<1.23` as a precaution due to MCP SDK changes related to the 11/25/25 protocol update that break certain FastMCP patches and workarounds. FastMCP 2.14 introduces proper support for the updated protocol.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.2\" description=\"December 1, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.2: Refreshing Changes\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.2\"\ncta=\"Read the release notes\"\n>\nPolishes the authentication stack with improvements to token refresh, scope handling, and multi-instance deployments.\n\n🎮 **Discord OAuth provider** added as a built-in authentication option.\n\n🔄 **Token refresh fixes** for Azure and Google providers, plus OAuth proxy improvements for multi-instance deployments.\n\n🎨 **Icon support** added to proxy classes for richer UX.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.1\" description=\"November 15, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.1: Heavy Meta\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.1\"\ncta=\"Read the release notes\"\n>\nIntroduces meta parameter support for `ToolResult`, enabling tools to return supplementary metadata alongside results for patterns like OpenAI's Apps SDK.\n\n🏷️ **Meta parameters** let tools return supplementary metadata alongside results.\n\n🔐 **New auth providers** for OCI and Supabase, plus custom token verifiers with DebugTokenVerifier for development.\n\n🔒 **Security fixes** for CVE-2025-61920 and safer Cursor deeplink URL validation on Windows.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.13.0\" description=\"October 25, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.13.0: Cache Me If You Can\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.0\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.13 \"Cache Me If You Can\" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.\n\n💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more!\n\n🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions, while the OAuth proxy now issues its own tokens with automatic key derivation. RFC 7662 token introspection support enables enterprise auth flows, and path prefix mounting enables OAuth-protected servers to integrate into existing web applications. FastMCP now supports out-of-the-box authentication with [WorkOS](https://gofastmcp.com/integrations/workos) and [AuthKit](https://gofastmcp.com/integrations/authkit), [GitHub](https://gofastmcp.com/integrations/github), [Google](https://gofastmcp.com/integrations/google), [Azure](https://gofastmcp.com/integrations/azure) (Entra ID), [AWS Cognito](https://gofastmcp.com/integrations/aws-cognito), [Auth0](https://gofastmcp.com/integrations/auth0), [Descope](https://gofastmcp.com/integrations/descope), [Scalekit](https://gofastmcp.com/integrations/scalekit), [JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification), and [RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol).\n\n⚡ **Response Caching Middleware** dramatically improves performance for expensive operations, while **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session.\n\n✨ **Developer experience improvements** include Pydantic input validation, icon support, RFC 6570 query parameters for resource templates, improved Context API methods, and async file/directory resources.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.5\" description=\"October 17, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.5: Safety Pin\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.5\"\ncta=\"Read the release notes\"\n>\nPins MCP SDK version below 1.17 to ensure the `.well-known` payload appears in the expected location when using FastMCP auth providers with composite applications.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.4\" description=\"September 26, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.4: OIDC What You Did There\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.4\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.12.4 adds comprehensive OIDC support and expands authentication options with AWS Cognito and Descope providers. The release also includes improvements to logging middleware, URL handling for nested resources, persistent OAuth client registration storage, and various fixes to the experimental OpenAPI parser.\n\n🔐 **OIDC Configuration** brings native support for OpenID Connect, enabling seamless integration with enterprise identity providers.\n\n🏢 **Enterprise Authentication** expands with AWS Cognito and Descope providers, broadening the authentication ecosystem.\n\n🛠️ **Improved Reliability** through enhanced URL handling, persistent OAuth storage, and numerous parser fixes based on community feedback.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.3\" description=\"September 17, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.3: Double Time\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.3\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.12.3 focuses on performance and developer experience improvements. This release includes optimized auth provider imports that reduce server startup time, enhanced OIDC authentication flows, and automatic inline snapshot creation for testing.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.2\" description=\"September 3, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.2: Perchance to Stream\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.2\"\ncta=\"Read the release notes\"\n>\nHotfix for streamable-http transport validation in fastmcp.json configuration files, resolving a parsing error when CLI arguments were merged against the configuration spec.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12.1\" description=\"September 3, 2025\" tags={[\"Releases\"]}>\n<Card\ntitle=\"FastMCP 2.12.1: OAuth to Joy\"\nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.1\"\ncta=\"Read the release notes\"\n>\nFastMCP 2.12.1 strengthens OAuth proxy implementation with improved client storage reliability, PKCE forwarding, configurable token endpoint authentication methods, and expanded scope handling based on extensive community testing.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.12\" description=\"August 31, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.12: Auth to the Races\" \nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.0\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.12 represents one of our most significant releases to date. After extensive testing and iteration with the community, we're shipping major improvements to authentication, configuration, and MCP feature adoption.\n\n🔐 **OAuth Proxy** bridges the gap for providers that don't support Dynamic Client Registration, enabling authentication with GitHub, Google, WorkOS, and Azure through minimal configuration.\n\n📋 **Declarative JSON Configuration** introduces `fastmcp.json` as the single source of truth for server settings, making MCP servers as portable and shareable as container images.\n\n🧠 **Sampling API Fallback** tackles adoption challenges by letting servers generate completions server-side when clients don't support the feature, encouraging innovation while maintaining compatibility.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.11\" description=\"August 1, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.11: Auth to a Good Start\" \nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.0\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.11 brings enterprise-ready authentication and dramatic performance improvements.\n\n🔒 **Comprehensive OAuth 2.1 Support** with WorkOS AuthKit integration, Dynamic Client Registration, and support for separate resource and authorization servers.\n\n⚡ **Experimental OpenAPI Parser** delivers dramatic performance gains through single-pass schema processing and optimized memory usage (enable with environment variable).\n\n💾 **Enhanced State Management** provides persistent state across tool calls with a simple dictionary interface, improving context handling and type annotations.\n\nThis release emphasizes speed and simplicity while setting the foundation for future enterprise features.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.10\" description=\"July 2, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.10: Great Spec-tations\" \nhref=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.0\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.10 achieves full compliance with the 6/18/2025 MCP specification update, introducing powerful new communication patterns.\n\n💬 **Elicitation Support** enables dynamic server-client communication and \"human-in-the-loop\" workflows, allowing servers to request additional information during execution.\n\n📊 **Output Schemas** provide structured outputs for tools, making results more predictable and easier to parse programmatically.\n\n🛠️ **Enhanced HTTP Routing** with OpenAPI extensions support and configurable algorithms for more flexible API integration.\n\nThis release includes a breaking change to `client.call_tool()` return signatures but significantly expands the interaction capabilities of MCP servers.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.9\" description=\"June 23, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card \ntitle=\"FastMCP 2.9: MCP-Native Middleware\" href=\"https://www.jlowin.dev/blog/fastmcp-2-9-middleware\" \nimg=\"https://jlowin.dev/_image?href=%2F_astro%2Fhero.BkVTdeBk.jpg&w=1200&h=630&f=png\" \ncta=\"Read more\"  \n>\nFastMCP 2.9 is a major release that, among other things, introduces two important features that push beyond the basic MCP protocol. \n\n🤝 *MCP Middleware* brings a flexible middleware system for intercepting and controlling server operations - think authentication, logging, rate limiting, and custom business logic without touching core protocol code. \n\n✨ *Server-side type conversion* for prompts solves a major developer pain point: while MCP requires string arguments, your functions can now work with native Python types like lists and dictionaries, with automatic conversion handling the complexity.\n\nThese features transform FastMCP from a simple protocol implementation into a powerful framework for building sophisticated MCP applications. Combined with the new `File` utility for binary data and improvements to authentication and serialization, this release makes FastMCP significantly more flexible and developer-friendly while maintaining full protocol compliance.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.8\" description=\"June 11, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card \ntitle=\"FastMCP 2.8: Transform and Roll Out\" href=\"https://www.jlowin.dev/blog/fastmcp-2-8-tool-transformation\" \nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.su3kspkP.png&w=1000&h=500&f=webp\" \ncta=\"Read more\"  \n>\nFastMCP 2.8 is here, and it's all about taking control of your tools.\n\nThis release is packed with new features for curating the perfect LLM experience:\n\n🛠️ Tool Transformation\n\nThe headline feature lets you wrap any tool—from your own code, a third-party library, or an OpenAPI spec—to create an enhanced, LLM-friendly version. You can rename arguments, rewrite descriptions, and hide parameters without touching the original code.\n\nThis feature was developed in close partnership with Bill Easton. As Bill brilliantly [put it](https://www.linkedin.com/posts/williamseaston_huge-thanks-to-william-easton-for-providing-activity-7338011349525983232-Mw6T?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAAd6d0B3uL9zpCsq9eYWKi3HIvb8eN_r_Q), \"Tool transformation flips Prompt Engineering on its head: stop writing tool-friendly LLM prompts and start providing LLM-friendly tools.\"\n\n🏷️ Component Control\n\nNow that you're transforming tools, you need a way to hide the old ones! In FastMCP 2.8 you can programmatically enable/disable any component, and for everyone who's been asking what FastMCP's tags are for—they finally have a purpose! You can now use tags to declaratively filter which components are exposed to your clients.\n\n🚀 Pragmatic by Default\n\nLastly, to ensure maximum compatibility with the ecosystem, we've made the pragmatic decision to default all OpenAPI routes to Tools, making your entire API immediately accessible to any tool-using agent. When the industry catches up and supports resources, we'll restore the old default -- but no reason you should do extra work before OpenAI, Anthropic, or Google!\n\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.7\" description=\"June 6, 2025\" tags={[\"Releases\"]}>\n<Card \ntitle=\"FastMCP 2.7: Pare Programming\" href=\"https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.0\" \nimg=\"assets/updates/release-2-7.png\" \ncta=\"Read the release notes\"  \n>\nFastMCP 2.7 has been released!\n\nMost notably, it introduces the highly requested (and Pythonic) \"naked\" decorator usage:\n\n```python {3}\nmcp = FastMCP()\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    return a + b\n```\n\nIn addition, decorators now return the objects they create, instead of the decorated function. This is an important usability enhancement.\n\nThe bulk of the update is focused on improving the FastMCP internals, including a few breaking internal changes to private APIs. A number of functions that have clung on since 1.0 are now deprecated.\n</Card>\n</Update>\n\n\n\n<Update label=\"FastMCP 2.6\" description=\"June 2, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card \ntitle=\"FastMCP 2.6: Blast Auth\" href=\"https://www.jlowin.dev/blog/fastmcp-2-6\" \nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.Bsu8afiw.png&w=1000&h=500&f=webp\" \ncta=\"Read more\"  \n>\nFastMCP 2.6 is here!\n\nThis release introduces first-class authentication for MCP servers and clients, including pragmatic Bearer token support and seamless OAuth 2.1 integration. This release aligns with how major AI platforms are adopting MCP today, making it easier than ever to securely connect your tools to real-world AI models. Dive into the update and secure your stack with minimal friction.\n</Card>\n</Update>\n\n<Update description=\"May 21, 2025\" label=\"Vibe-Testing\" tags={[\"Blog Posts\", \"Tutorials\"]}>\n<Card\ntitle=\"Stop Vibe-Testing Your MCP Server\"\nhref=\"https://www.jlowin.dev/blog/stop-vibe-testing-mcp-servers\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.BUPy9I9c.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nYour tests are bad and you should feel bad.\n\nStop vibe-testing your MCP server through LLM guesswork. FastMCP 2.0 introduces in-memory testing for fast, deterministic, and fully Pythonic validation of your MCP logic—no network, no subprocesses, no vibes.\n\n</Card>\n</Update>\n\n\n<Update description=\"May 8, 2025\" label=\"10,000 Stars\" tags={[\"Blog Posts\"]}>\n<Card\ntitle=\"Reflecting on FastMCP at 10k stars 🌟\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-2-10k-stars\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.Cnvci9Q_.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nIn just six weeks since its relaunch, FastMCP has surpassed 10,000 GitHub stars—becoming the fastest-growing OSS project in our orbit. What started as a personal itch has become the backbone of Python-based MCP servers, powering a rapidly expanding ecosystem. While the protocol itself evolves, FastMCP continues to lead with clarity, developer experience, and opinionated tooling. Here’s to what’s next.\n\n</Card>\n</Update>\n\n<Update description=\"May 8, 2025\" label=\"FastMCP 2.3\" tags={[\"Blog Posts\", \"Releases\"]}>\n<Card\ntitle=\"Now Streaming: FastMCP 2.3\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-2-3-streamable-http\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.M_hv6gEB.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nFastMCP 2.3 introduces full support for Streamable HTTP, a modern alternative to SSE that simplifies MCP deployments over the web. It’s efficient, reliable, and now the default HTTP transport. Just run your server with transport=\"http\" and connect clients via a standard URL—FastMCP handles the rest. No special setup required. This release makes deploying MCP servers easier and more portable than ever.\n\n</Card>\n</Update>\n\n<Update description=\"April 23, 2025\" label=\"Proxy Servers\" tags={[\"Blog Posts\", \"Tutorials\"]}>\n<Card\ntitle=\"MCP Proxy Servers with FastMCP 2.0\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-proxy\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Frobot-hero.DpmAqgui.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nEven AI needs a good travel adapter 🔌\n\n\nFastMCP now supports proxying arbitrary MCP servers, letting you run a local FastMCP instance that transparently forwards requests to any remote or third-party server—regardless of transport. This enables transport bridging (e.g., stdio ⇄ SSE), simplified client configuration, and powerful gateway patterns. Proxies are fully composable with other FastMCP servers, letting you mount or import them just like local servers. Use `FastMCP.from_client()` to wrap any backend in a clean, Pythonic proxy.\n</Card>\n</Update>\n\n<Update label=\"FastMCP 2.0\" description=\"April 16, 2025\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card\ntitle=\"Introducing FastMCP 2.0 🚀\"\nhref=\"https://www.jlowin.dev/blog/fastmcp-2\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.DpbmGNrr.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\n\nThis major release reimagines FastMCP as a full ecosystem platform, with powerful new features for composition, integration, and client interaction. You can now compose local and remote servers, proxy arbitrary MCP servers (with transport translation), and generate MCP servers from OpenAPI or FastAPI apps. A new client infrastructure supports advanced workflows like LLM sampling. \n\nFastMCP 2.0 builds on the success of v1 with a cleaner, more flexible foundation—try it out today!\n</Card>\n</Update>\n\n\n\n<Update label=\"Official SDK\" description=\"December 3, 2024\" tags={[\"Announcements\"]}>\n<Card\ntitle=\"FastMCP is joining the official MCP Python SDK!\"\nhref=\"https://bsky.app/profile/jlowin.dev/post/3lch4xk5cf22c\"\nicon=\"sparkles\"\ncta=\"Read the announcement\"\n>\nFastMCP 1.0 will become part of the official MCP Python SDK!\n</Card>\n</Update>\n\n\n\n<Update label=\"FastMCP 1.0\" description=\"December 1, 2024\" tags={[\"Releases\", \"Blog Posts\"]}>\n<Card\ntitle=\"Introducing FastMCP 🚀\"\nhref=\"https://www.jlowin.dev/blog/introducing-fastmcp\"\nimg=\"https://www.jlowin.dev/_image?href=%2F_astro%2Ffastmcp.Bep7YlTw.png&w=1000&h=500&f=webp\"\ncta=\"Read more\"\n>\nBecause life's too short for boilerplate.\n\nThis is where it all started. FastMCP’s launch post introduced a clean, Pythonic way to build MCP servers without the protocol overhead. Just write functions; FastMCP handles the rest. What began as a weekend project quickly became the foundation of a growing ecosystem.\n</Card>\n</Update>\n\n"
  },
  {
    "path": "docs/v2-banner.js",
    "content": "// Add v2 banner inside content-container with negative margins\n(function() {\n  if (typeof window === 'undefined') return;\n  \n  function addBanner() {\n    const isV2 = window.location.pathname.includes('/v2/');\n    const container = document.getElementById('content-container');\n    let banner = document.getElementById('v2-banner');\n    \n    if (isV2 && container) {\n      if (!banner) {\n        banner = document.createElement('div');\n        banner.id = 'v2-banner';\n        banner.innerHTML = 'These are the docs for FastMCP 2.0. <a href=\"/getting-started/welcome\" style=\"color: white; text-decoration: underline; font-weight: 700;\">FastMCP 3.0</a> is now available.';\n        container.insertBefore(banner, container.firstChild);\n      }\n    } else if (!isV2 && banner) {\n      banner.remove();\n    }\n  }\n  \n  function run() {\n    if (document.readyState === 'loading') {\n      document.addEventListener('DOMContentLoaded', addBanner);\n    } else {\n      addBanner();\n    }\n  }\n  \n  run();\n  \n  let lastUrl = location.href;\n  new MutationObserver(() => {\n    if (location.href !== lastUrl) {\n      lastUrl = location.href;\n      setTimeout(addBanner, 100);\n    }\n  }).observe(document.body, {subtree: true, childList: true});\n})();\n"
  },
  {
    "path": "examples/apps/chart_server.py",
    "content": "from prefab_ui.components import Column, Heading, Muted\nfrom prefab_ui.components.charts import BarChart, ChartSeries\n\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Sales Dashboard\")\n\nDATA = [\n    {\"month\": \"Jan\", \"online\": 4200, \"retail\": 2400},\n    {\"month\": \"Feb\", \"online\": 3800, \"retail\": 2100},\n    {\"month\": \"Mar\", \"online\": 5100, \"retail\": 2800},\n    {\"month\": \"Apr\", \"online\": 4600, \"retail\": 3200},\n    {\"month\": \"May\", \"online\": 5800, \"retail\": 3100},\n    {\"month\": \"Jun\", \"online\": 6200, \"retail\": 3500},\n]\n\n\n@mcp.tool(app=True)\ndef sales_chart(stacked: bool = False) -> Column:\n    \"\"\"Show monthly online vs. retail sales as a bar chart.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Monthly Sales\")\n        Muted(\"Online vs. retail — hover bars for details\")\n        BarChart(\n            data=DATA,\n            series=[\n                ChartSeries(data_key=\"online\", label=\"Online\"),\n                ChartSeries(data_key=\"retail\", label=\"Retail\"),\n            ],\n            x_axis=\"month\",\n            stacked=stacked,\n            show_legend=True,\n        )\n    return view\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/apps/contacts/contacts_server.py",
    "content": "\"\"\"Contact manager — a FastMCPApp example with forms and callable tool references.\n\nDemonstrates the full FastMCPApp stack:\n- @app.ui() entry point that the model calls to open the app\n- @app.tool() backend tools that the UI calls via CallTool\n- CallTool(fn) with function references (not strings) that resolve to global keys\n- Form.from_model() for auto-generated Pydantic model forms\n- Manual form construction with the context-manager pattern\n\nUsage:\n    uv run python contacts_server.py               # HTTP (default)\n    uv run python contacts_server.py --stdio        # stdio for MCP clients\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Literal\n\nfrom prefab_ui.actions import SetState, ShowToast\nfrom prefab_ui.actions.mcp import CallTool\nfrom prefab_ui.app import PrefabApp\nfrom prefab_ui.components import (\n    Badge,\n    Button,\n    Column,\n    ForEach,\n    Form,\n    Heading,\n    Input,\n    Muted,\n    Row,\n    Separator,\n    Text,\n)\nfrom prefab_ui.rx import RESULT\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp import FastMCP, FastMCPApp\n\n# ---------------------------------------------------------------------------\n# Data\n# ---------------------------------------------------------------------------\n\n_contacts: list[dict] = [\n    {\n        \"name\": \"Arthur Dent\",\n        \"email\": \"arthur@earth.com\",\n        \"category\": \"Customer\",\n        \"notes\": \"\",\n    },\n    {\n        \"name\": \"Ford Prefect\",\n        \"email\": \"ford@betelgeuse.org\",\n        \"category\": \"Partner\",\n        \"notes\": \"Researcher\",\n    },\n]\n\n\n# ---------------------------------------------------------------------------\n# Pydantic model for auto-generated forms\n# ---------------------------------------------------------------------------\n\n\nclass ContactModel(BaseModel):\n    name: str = Field(title=\"Full Name\", min_length=1)\n    email: str = Field(title=\"Email\")\n    category: Literal[\"Customer\", \"Vendor\", \"Partner\", \"Other\"] = \"Other\"\n    notes: str = Field(\n        default=\"\",\n        title=\"Notes\",\n        json_schema_extra={\"ui\": {\"type\": \"textarea\"}},\n    )\n\n\n# ---------------------------------------------------------------------------\n# App\n# ---------------------------------------------------------------------------\n\napp = FastMCPApp(\"Contacts\")\n\n\n@app.tool()\ndef save_contact(data: ContactModel) -> list[dict]:\n    \"\"\"Save a new contact and return the updated list.\"\"\"\n    _contacts.append(data.model_dump())\n    return list(_contacts)\n\n\n@app.tool()\ndef search_contacts(query: str) -> list[dict]:\n    \"\"\"Filter contacts by name or email.\"\"\"\n    q = query.lower()\n    return [c for c in _contacts if q in c[\"name\"].lower() or q in c[\"email\"].lower()]\n\n\n@app.tool(model=True)\ndef list_contacts() -> list[dict]:\n    \"\"\"Return all contacts. Visible to both the model and the UI.\"\"\"\n    return list(_contacts)\n\n\n@app.ui()\ndef contact_manager() -> PrefabApp:\n    \"\"\"Open the contact manager. The model calls this to launch the app.\"\"\"\n    with Column(gap=6, css_class=\"p-6\") as view:\n        Heading(\"Contacts\")\n\n        with ForEach(\"contacts\") as contact:\n            with Row(gap=2, align=\"center\"):\n                Text(contact.name, css_class=\"font-medium\")\n                Muted(contact.email)\n                Badge(contact.category)\n\n        Separator()\n\n        Heading(\"Add Contact\", level=3)\n        Form.from_model(\n            ContactModel,\n            on_submit=CallTool(\n                save_contact,\n                on_success=[\n                    SetState(\"contacts\", RESULT),\n                    ShowToast(\"Contact saved!\", variant=\"success\"),\n                ],\n                on_error=ShowToast(\"{{ $error }}\", variant=\"error\"),\n            ),\n        )\n\n        Separator()\n\n        Heading(\"Search\", level=3)\n        with Form(\n            on_submit=CallTool(\n                search_contacts,\n                arguments={\"query\": \"{{ query }}\"},\n                on_success=SetState(\"contacts\", RESULT),\n            )\n        ):\n            Input(name=\"query\", placeholder=\"Search by name or email...\")\n            Button(\"Search\")\n\n    return PrefabApp(\n        view=view,\n        state={\"contacts\": list(_contacts)},\n    )\n\n\nmcp = FastMCP(\"Contacts Server\", providers=[app])\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\")\n"
  },
  {
    "path": "examples/apps/datatable_server.py",
    "content": "from prefab_ui.components import Column, Heading, Muted\nfrom prefab_ui.components.data_table import DataTable, DataTableColumn\n\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Team Directory\")\n\nTEAM = [\n    {\n        \"name\": \"Alice Chen\",\n        \"role\": \"Engineering\",\n        \"level\": \"Senior\",\n        \"location\": \"San Francisco\",\n    },\n    {\"name\": \"Bob Martinez\", \"role\": \"Design\", \"level\": \"Lead\", \"location\": \"New York\"},\n    {\n        \"name\": \"Carol Johnson\",\n        \"role\": \"Engineering\",\n        \"level\": \"Staff\",\n        \"location\": \"London\",\n    },\n    {\n        \"name\": \"David Kim\",\n        \"role\": \"Product\",\n        \"level\": \"Senior\",\n        \"location\": \"San Francisco\",\n    },\n    {\"name\": \"Eva Müller\", \"role\": \"Engineering\", \"level\": \"Mid\", \"location\": \"Berlin\"},\n    {\n        \"name\": \"Frank Okafor\",\n        \"role\": \"Data Science\",\n        \"level\": \"Senior\",\n        \"location\": \"Lagos\",\n    },\n    {\n        \"name\": \"Grace Liu\",\n        \"role\": \"Engineering\",\n        \"level\": \"Junior\",\n        \"location\": \"Singapore\",\n    },\n    {\"name\": \"Hassan Ali\", \"role\": \"Design\", \"level\": \"Senior\", \"location\": \"Dubai\"},\n]\n\n\n@mcp.tool(app=True)\ndef team_directory(department: str | None = None) -> Column:\n    \"\"\"Browse the team directory — sortable, searchable, paginated.\"\"\"\n    rows = [p for p in TEAM if not department or p[\"role\"] == department]\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Team Directory\")\n        Muted(f\"{len(rows)} people\")\n        DataTable(\n            columns=[\n                DataTableColumn(key=\"name\", header=\"Name\", sortable=True),\n                DataTableColumn(key=\"role\", header=\"Department\", sortable=True),\n                DataTableColumn(key=\"level\", header=\"Level\", sortable=True),\n                DataTableColumn(key=\"location\", header=\"Location\", sortable=True),\n            ],\n            rows=rows,\n            searchable=True,\n            paginated=True,\n        )\n    return view\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/apps/greet_server.py",
    "content": "\"\"\"Minimal example demonstrating a @app=True tool with arguments.\n\nUsage:\n    uv run python greet_server.py\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Literal\n\nfrom prefab_ui.components import Badge, Column, Heading, Muted\n\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Greeter\")\n\nGREETINGS: dict[str, str] = {\n    \"English\": \"Hello\",\n    \"Spanish\": \"¡Hola\",\n    \"French\": \"Bonjour\",\n    \"Japanese\": \"こんにちは\",\n    \"Arabic\": \"مرحبا\",\n}\n\n\n@mcp.tool(app=True)\ndef greet(\n    name: str,\n    language: Literal[\"English\", \"Spanish\", \"French\", \"Japanese\", \"Arabic\"] = \"English\",\n) -> Column:\n    \"\"\"Greet someone in their language.\"\"\"\n    word = GREETINGS[language]\n    with Column(gap=3, css_class=\"p-8\") as view:\n        Heading(f\"{word}, {name}!\")\n        Muted(\"Greeting rendered by FastMCP\")\n        Badge(language)\n    return view\n\n\nFAREWELLS: dict[str, str] = {\n    \"English\": \"Goodbye\",\n    \"Spanish\": \"Adiós\",\n    \"French\": \"Au revoir\",\n    \"Japanese\": \"さようなら\",\n    \"Arabic\": \"مع السلامة\",\n}\n\n\n@mcp.tool(app=True)\ndef farewell(\n    name: str,\n    language: Literal[\"English\", \"Spanish\", \"French\", \"Japanese\", \"Arabic\"] = \"English\",\n) -> Column:\n    \"\"\"Say farewell in their language.\"\"\"\n    word = FAREWELLS[language]\n    with Column(gap=3, css_class=\"p-8\") as view:\n        Heading(f\"{word}, {name}!\")\n        Muted(\"Farewell rendered by FastMCP\")\n        Badge(language)\n    return view\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/apps/patterns_server.py",
    "content": "\"\"\"Patterns showcase — every Prefab pattern from the docs in one server.\n\nA runnable collection of the patterns from https://gofastmcp.com/apps/patterns.\nEach tool demonstrates a different Prefab UI pattern: charts, tables, forms,\nstatus displays, conditional content, tabs, and accordions.\n\nUsage:\n    uv run python patterns_server.py              # HTTP (port 8000)\n    uv run python patterns_server.py --stdio       # stdio for MCP clients\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom prefab_ui.actions import ShowToast\nfrom prefab_ui.actions.mcp import CallTool\nfrom prefab_ui.app import PrefabApp\nfrom prefab_ui.components import (\n    Accordion,\n    AccordionItem,\n    Alert,\n    AreaChart,\n    Badge,\n    BarChart,\n    Button,\n    Card,\n    CardContent,\n    ChartSeries,\n    Column,\n    DataTable,\n    DataTableColumn,\n    ForEach,\n    Form,\n    Grid,\n    Heading,\n    If,\n    Input,\n    Muted,\n    PieChart,\n    Progress,\n    Row,\n    Select,\n    Separator,\n    Switch,\n    Tab,\n    Tabs,\n    Text,\n    Textarea,\n)\n\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Patterns Showcase\")\n\n\n# ---------------------------------------------------------------------------\n# Data\n# ---------------------------------------------------------------------------\n\nQUARTERLY_DATA = [\n    {\"quarter\": \"Q1\", \"revenue\": 42000, \"costs\": 28000},\n    {\"quarter\": \"Q2\", \"revenue\": 51000, \"costs\": 31000},\n    {\"quarter\": \"Q3\", \"revenue\": 47000, \"costs\": 29000},\n    {\"quarter\": \"Q4\", \"revenue\": 63000, \"costs\": 35000},\n]\n\nDAILY_USAGE = [\n    {\"date\": f\"Feb {d}\", \"requests\": v}\n    for d, v in zip(\n        range(1, 11),\n        [1200, 1350, 980, 1500, 1420, 1680, 1550, 1700, 1450, 1600],\n    )\n]\n\nTICKETS = [\n    {\"category\": \"Bug\", \"count\": 23},\n    {\"category\": \"Feature\", \"count\": 15},\n    {\"category\": \"Docs\", \"count\": 8},\n    {\"category\": \"Infra\", \"count\": 12},\n]\n\nEMPLOYEES = [\n    {\n        \"name\": \"Alice Chen\",\n        \"department\": \"Engineering\",\n        \"role\": \"Staff Engineer\",\n        \"location\": \"San Francisco\",\n    },\n    {\n        \"name\": \"Bob Martinez\",\n        \"department\": \"Design\",\n        \"role\": \"Lead Designer\",\n        \"location\": \"New York\",\n    },\n    {\n        \"name\": \"Carol Johnson\",\n        \"department\": \"Engineering\",\n        \"role\": \"Senior Engineer\",\n        \"location\": \"London\",\n    },\n    {\n        \"name\": \"David Kim\",\n        \"department\": \"Product\",\n        \"role\": \"Product Manager\",\n        \"location\": \"San Francisco\",\n    },\n    {\n        \"name\": \"Eva Müller\",\n        \"department\": \"Engineering\",\n        \"role\": \"Engineer\",\n        \"location\": \"Berlin\",\n    },\n    {\n        \"name\": \"Frank Okafor\",\n        \"department\": \"Data Science\",\n        \"role\": \"Senior Analyst\",\n        \"location\": \"Lagos\",\n    },\n    {\n        \"name\": \"Grace Liu\",\n        \"department\": \"Engineering\",\n        \"role\": \"Junior Engineer\",\n        \"location\": \"Singapore\",\n    },\n    {\n        \"name\": \"Hassan Ali\",\n        \"department\": \"Design\",\n        \"role\": \"Senior Designer\",\n        \"location\": \"Dubai\",\n    },\n]\n\nSERVICES = [\n    {\n        \"name\": \"API Gateway\",\n        \"status\": \"healthy\",\n        \"ok\": True,\n        \"latency_ms\": 12,\n        \"uptime_pct\": 99.9,\n    },\n    {\n        \"name\": \"Database\",\n        \"status\": \"healthy\",\n        \"ok\": True,\n        \"latency_ms\": 3,\n        \"uptime_pct\": 99.99,\n    },\n    {\n        \"name\": \"Cache\",\n        \"status\": \"degraded\",\n        \"ok\": False,\n        \"latency_ms\": 45,\n        \"uptime_pct\": 98.2,\n    },\n    {\n        \"name\": \"Queue\",\n        \"status\": \"healthy\",\n        \"ok\": True,\n        \"latency_ms\": 8,\n        \"uptime_pct\": 99.8,\n    },\n]\n\nENDPOINTS = [\n    {\n        \"path\": \"/api/users\",\n        \"status\": 200,\n        \"healthy\": True,\n        \"avg_ms\": 45,\n        \"p99_ms\": 120,\n        \"uptime_pct\": 99.9,\n    },\n    {\n        \"path\": \"/api/orders\",\n        \"status\": 200,\n        \"healthy\": True,\n        \"avg_ms\": 82,\n        \"p99_ms\": 250,\n        \"uptime_pct\": 99.7,\n    },\n    {\n        \"path\": \"/api/search\",\n        \"status\": 200,\n        \"healthy\": True,\n        \"avg_ms\": 150,\n        \"p99_ms\": 500,\n        \"uptime_pct\": 99.5,\n    },\n    {\n        \"path\": \"/api/webhooks\",\n        \"status\": 503,\n        \"healthy\": False,\n        \"avg_ms\": 2000,\n        \"p99_ms\": 5000,\n        \"uptime_pct\": 95.1,\n    },\n]\n\nPROJECT = {\n    \"name\": \"FastMCP v3\",\n    \"description\": \"Next generation MCP framework with Apps support.\",\n    \"status\": \"Active\",\n    \"created_at\": \"2025-01-15\",\n    \"members\": [\n        {\"name\": \"Alice Chen\", \"role\": \"Lead\"},\n        {\"name\": \"Bob Martinez\", \"role\": \"Design\"},\n        {\"name\": \"Carol Johnson\", \"role\": \"Backend\"},\n    ],\n    \"activity\": [\n        {\n            \"timestamp\": \"2 hours ago\",\n            \"message\": \"Merged PR #342: Add Prefab UI integration\",\n        },\n        {\n            \"timestamp\": \"5 hours ago\",\n            \"message\": \"Opened issue #345: CORS convenience API\",\n        },\n        {\"timestamp\": \"1 day ago\", \"message\": \"Released v3.0.1\"},\n    ],\n}\n\n# In-memory contact store for the form demo\n_contacts: list[dict] = [\n    {\"name\": \"Zaphod Beeblebrox\", \"email\": \"zaphod@galaxy.gov\", \"category\": \"Partner\"},\n]\n\n\n# ---------------------------------------------------------------------------\n# Charts\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef quarterly_revenue(year: int = 2025) -> PrefabApp:\n    \"\"\"Show quarterly revenue as a bar chart.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(f\"{year} Revenue vs Costs\")\n        BarChart(\n            data=QUARTERLY_DATA,\n            series=[\n                ChartSeries(data_key=\"revenue\", label=\"Revenue\"),\n                ChartSeries(data_key=\"costs\", label=\"Costs\"),\n            ],\n            x_axis=\"quarter\",\n            show_legend=True,\n        )\n\n    return PrefabApp(view=view)\n\n\n@mcp.tool(app=True)\ndef usage_trend() -> PrefabApp:\n    \"\"\"Show API usage over time as an area chart.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"API Usage (10 Days)\")\n        AreaChart(\n            data=DAILY_USAGE,\n            series=[ChartSeries(data_key=\"requests\", label=\"Requests\")],\n            x_axis=\"date\",\n            curve=\"smooth\",\n            height=250,\n        )\n\n    return PrefabApp(view=view)\n\n\n@mcp.tool(app=True)\ndef ticket_breakdown() -> PrefabApp:\n    \"\"\"Show open tickets by category as a donut chart.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Open Tickets\")\n        PieChart(\n            data=TICKETS,\n            data_key=\"count\",\n            name_key=\"category\",\n            show_legend=True,\n            inner_radius=60,\n        )\n\n    return PrefabApp(view=view)\n\n\n# ---------------------------------------------------------------------------\n# Data Tables\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef employee_directory() -> PrefabApp:\n    \"\"\"Show a searchable, sortable employee directory.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Employee Directory\")\n        DataTable(\n            columns=[\n                DataTableColumn(key=\"name\", header=\"Name\", sortable=True),\n                DataTableColumn(key=\"department\", header=\"Department\", sortable=True),\n                DataTableColumn(key=\"role\", header=\"Role\"),\n                DataTableColumn(key=\"location\", header=\"Office\", sortable=True),\n            ],\n            rows=EMPLOYEES,\n            searchable=True,\n            paginated=True,\n            page_size=15,\n        )\n\n    return PrefabApp(view=view)\n\n\n# ---------------------------------------------------------------------------\n# Forms\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef contact_form() -> PrefabApp:\n    \"\"\"Show a form to create a new contact, with a live contact list below.\"\"\"\n    with Column(gap=6, css_class=\"p-6\") as view:\n        Heading(\"Contacts\")\n\n        with ForEach(\"contacts\"):\n            with Row(gap=2, align=\"center\"):\n                Text(\"{{ name }}\", css_class=\"font-medium\")\n                Muted(\"{{ email }}\")\n                Badge(\"{{ category }}\")\n\n        Separator()\n\n        Heading(\"Add Contact\", level=3)\n        with Form(\n            on_submit=CallTool(\n                \"save_contact\",\n                result_key=\"contacts\",\n                on_success=ShowToast(\"Contact saved!\", variant=\"success\"),\n                on_error=ShowToast(\"{{ $error }}\", variant=\"error\"),\n            )\n        ):\n            Input(name=\"name\", label=\"Full Name\", required=True)\n            Input(name=\"email\", label=\"Email\", input_type=\"email\", required=True)\n            Select(\n                name=\"category\",\n                label=\"Category\",\n                options=[\"Customer\", \"Vendor\", \"Partner\", \"Other\"],\n            )\n            Textarea(name=\"notes\", label=\"Notes\", placeholder=\"Optional notes...\")\n            Button(\"Save Contact\")\n\n    return PrefabApp(view=view, state={\"contacts\": list(_contacts)})\n\n\n@mcp.tool\ndef save_contact(\n    name: str,\n    email: str,\n    category: str = \"Other\",\n    notes: str = \"\",\n) -> list[dict]:\n    \"\"\"Save a new contact and return the updated list.\"\"\"\n    contact = {\"name\": name, \"email\": email, \"category\": category, \"notes\": notes}\n    _contacts.append(contact)\n    return list(_contacts)\n\n\n# ---------------------------------------------------------------------------\n# Status Displays\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef system_status() -> PrefabApp:\n    \"\"\"Show current system health.\"\"\"\n    all_ok = all(s[\"ok\"] for s in SERVICES)\n\n    with Column(gap=4, css_class=\"p-6\") as view:\n        with Row(gap=2, align=\"center\"):\n            Heading(\"System Status\")\n            Badge(\n                \"All Healthy\" if all_ok else \"Degraded\",\n                variant=\"success\" if all_ok else \"destructive\",\n            )\n\n        Separator()\n\n        with Grid(columns=2, gap=4):\n            for svc in SERVICES:\n                with Card():\n                    with CardContent():\n                        with Row(gap=2, align=\"center\"):\n                            Text(svc[\"name\"], css_class=\"font-medium\")\n                            Badge(\n                                svc[\"status\"],\n                                variant=\"success\" if svc[\"ok\"] else \"destructive\",\n                            )\n                        Muted(f\"Response: {svc['latency_ms']}ms\")\n                        Progress(value=svc[\"uptime_pct\"])\n\n    return PrefabApp(view=view)\n\n\n# ---------------------------------------------------------------------------\n# Conditional Content\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef feature_flags() -> PrefabApp:\n    \"\"\"Toggle feature flags with live preview.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"Feature Flags\")\n\n        Switch(name=\"dark_mode\", label=\"Dark Mode\")\n        Switch(name=\"beta_features\", label=\"Beta Features\")\n\n        Separator()\n\n        with If(\"{{ dark_mode }}\"):\n            Alert(title=\"Dark mode enabled\", description=\"UI will use dark theme.\")\n        with If(\"{{ beta_features }}\"):\n            Alert(\n                title=\"Beta features active\",\n                description=\"Experimental features are now visible.\",\n                variant=\"warning\",\n            )\n\n    return PrefabApp(view=view, state={\"dark_mode\": False, \"beta_features\": False})\n\n\n# ---------------------------------------------------------------------------\n# Tabs\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef project_overview() -> PrefabApp:\n    \"\"\"Show project details organized in tabs.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(PROJECT[\"name\"])\n\n        with Tabs():\n            with Tab(\"Overview\"):\n                Text(PROJECT[\"description\"])\n                with Row(gap=4):\n                    Badge(PROJECT[\"status\"])\n                    Muted(f\"Created: {PROJECT['created_at']}\")\n\n            with Tab(\"Members\"):\n                DataTable(\n                    columns=[\n                        DataTableColumn(key=\"name\", header=\"Name\", sortable=True),\n                        DataTableColumn(key=\"role\", header=\"Role\"),\n                    ],\n                    rows=PROJECT[\"members\"],\n                )\n\n            with Tab(\"Activity\"):\n                with ForEach(\"activity\"):\n                    with Row(gap=2):\n                        Muted(\"{{ timestamp }}\")\n                        Text(\"{{ message }}\")\n\n    return PrefabApp(view=view, state={\"activity\": PROJECT[\"activity\"]})\n\n\n# ---------------------------------------------------------------------------\n# Accordion\n# ---------------------------------------------------------------------------\n\n\n@mcp.tool(app=True)\ndef api_health() -> PrefabApp:\n    \"\"\"Show health details for each API endpoint.\"\"\"\n    with Column(gap=4, css_class=\"p-6\") as view:\n        Heading(\"API Health\")\n\n        with Accordion(multiple=True):\n            for ep in ENDPOINTS:\n                with AccordionItem(ep[\"path\"]):\n                    with Row(gap=4):\n                        Badge(\n                            f\"{ep['status']}\",\n                            variant=\"success\" if ep[\"healthy\"] else \"destructive\",\n                        )\n                        Text(f\"Avg: {ep['avg_ms']}ms\")\n                        Text(f\"P99: {ep['p99_ms']}ms\")\n                    Progress(value=ep[\"uptime_pct\"])\n\n    return PrefabApp(view=view)\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/apps/qr_server/README.md",
    "content": "# QR Code MCP App\n\nAn MCP App server that generates QR codes with an interactive viewer UI. Ported from the [ext-apps QR server example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) to demonstrate FastMCP's MCP Apps support.\n\n## What it demonstrates\n\n- Linking a tool to a `ui://` resource via `AppConfig`\n- Serving embedded HTML with the `@modelcontextprotocol/ext-apps` JS SDK from CDN\n- Declaring CSP resource domains via `ResourceCSP`\n- Returning `ImageContent` (base64 PNG) from a tool\n\n## Setup\n\n```bash\ncd examples/apps/qr_server\nuv sync\n```\n\n## Usage\n\n```bash\nuv run python qr_server.py\n```\n\nOr install it into an MCP client:\n\n```bash\nfastmcp install stdio fastmcp.json\n```\n\n## How it works\n\nThe server registers one tool (`generate_qr`) and one resource (`ui://qr-server/view.html`). The tool generates a QR code as a base64 PNG image. The resource serves an HTML page that uses the MCP Apps JS SDK to receive the tool result and display the image in a sandboxed iframe.\n\nThe HTML loads the ext-apps SDK from unpkg, so the resource declares `csp=ResourceCSP(resource_domains=[\"https://unpkg.com\"])` to allow the host to set the appropriate Content-Security-Policy.\n"
  },
  {
    "path": "examples/apps/qr_server/fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"qr_server.py\"\n  },\n  \"environment\": {\n    \"dependencies\": [\n      \"fastmcp\",\n      \"qrcode[pil]>=8.0\",\n      \"pillow\"\n    ]\n  }\n}\n"
  },
  {
    "path": "examples/apps/qr_server/pyproject.toml",
    "content": "[project]\nname = \"fastmcp-app-examples\"\nversion = \"0.1.0\"\ndescription = \"MCP App examples for FastMCP\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"fastmcp\",\n    \"qrcode[pil]>=8.0\",\n]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n"
  },
  {
    "path": "examples/apps/qr_server/qr_server.py",
    "content": "\"\"\"QR Code MCP App Server — generates QR codes with an interactive view UI.\n\nDemonstrates MCP Apps with FastMCP:\n- Tool linked to a ui:// resource via AppConfig\n- HTML resource with CSP metadata for CDN-loaded dependencies\n- Embedded HTML using the @modelcontextprotocol/ext-apps JS SDK\n- ImageContent return type for binary data\n- Both stdio and HTTP transport modes\n\nBased on https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server\n\nSetup (from examples/apps/):\n    uv sync\n\nUsage:\n    uv run python qr_server.py            # HTTP mode (port 3001)\n    uv run python qr_server.py --stdio     # stdio mode for MCP clients\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport io\n\nimport qrcode  # type: ignore[import-untyped]\nfrom mcp import types\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.apps import AppConfig, ResourceCSP\nfrom fastmcp.tools import ToolResult\n\nVIEW_URI: str = \"ui://qr-server/view.html\"\n\nmcp: FastMCP = FastMCP(\"QR Code Server\")\n\nEMBEDDED_VIEW_HTML: str = \"\"\"\\\n<!DOCTYPE html>\n<html>\n<head>\n  <meta name=\"color-scheme\" content=\"light dark\">\n  <style>\n    html, body {\n      margin: 0;\n      padding: 0;\n      overflow: hidden;\n      background: transparent;\n    }\n    body {\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      height: 340px;\n      width: 340px;\n    }\n    img {\n      width: 300px;\n      height: 300px;\n      border-radius: 8px;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n    }\n  </style>\n</head>\n<body>\n  <div id=\"qr\"></div>\n  <script type=\"module\">\n    import { App } from \"https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps\";\n\n    const app = new App({ name: \"QR View\", version: \"1.0.0\" });\n\n    app.ontoolresult = ({ content }) => {\n      const img = content?.find(c => c.type === 'image');\n      if (img) {\n        const qrDiv = document.getElementById('qr');\n        qrDiv.innerHTML = '';\n\n        const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];\n        const mimeType = allowedTypes.includes(img.mimeType) ? img.mimeType : 'image/png';\n\n        const image = document.createElement('img');\n        image.src = `data:${mimeType};base64,${img.data}`;\n        image.alt = \"QR Code\";\n        qrDiv.appendChild(image);\n      }\n    };\n\n    function handleHostContextChanged(ctx) {\n      if (ctx.safeAreaInsets) {\n        document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`;\n        document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`;\n        document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;\n        document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;\n      }\n    }\n\n    app.onhostcontextchanged = handleHostContextChanged;\n\n    await app.connect();\n    const ctx = app.getHostContext();\n    if (ctx) {\n      handleHostContextChanged(ctx);\n    }\n  </script>\n</body>\n</html>\"\"\"\n\n\n@mcp.tool(app=AppConfig(resource_uri=VIEW_URI))\ndef generate_qr(\n    text: str = \"https://gofastmcp.com\",\n    box_size: int = 10,\n    border: int = 4,\n    error_correction: str = \"M\",\n    fill_color: str = \"black\",\n    back_color: str = \"white\",\n) -> ToolResult:\n    \"\"\"Generate a QR code from text.\n\n    Args:\n        text: The text/URL to encode\n        box_size: Size of each box in pixels (default: 10)\n        border: Border size in boxes (default: 4)\n        error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%)\n        fill_color: Foreground color (hex like #FF0000 or name like red)\n        back_color: Background color (hex like #FFFFFF or name like white)\n    \"\"\"\n    error_levels = {\n        \"L\": qrcode.constants.ERROR_CORRECT_L,\n        \"M\": qrcode.constants.ERROR_CORRECT_M,\n        \"Q\": qrcode.constants.ERROR_CORRECT_Q,\n        \"H\": qrcode.constants.ERROR_CORRECT_H,\n    }\n\n    if box_size <= 0:\n        raise ValueError(\"box_size must be > 0\")\n    if border < 0:\n        raise ValueError(\"border must be >= 0\")\n\n    error_key = error_correction.upper()\n    if error_key not in error_levels:\n        raise ValueError(f\"error_correction must be one of: {', '.join(error_levels)}\")\n\n    qr = qrcode.QRCode(\n        version=1,\n        error_correction=error_levels[error_key],\n        box_size=box_size,\n        border=border,\n    )\n    qr.add_data(text)\n    qr.make(fit=True)\n\n    img = qr.make_image(fill_color=fill_color, back_color=back_color)\n    buffer = io.BytesIO()\n    img.save(buffer, format=\"PNG\")\n    b64 = base64.b64encode(buffer.getvalue()).decode()\n    return ToolResult(\n        content=[types.ImageContent(type=\"image\", data=b64, mimeType=\"image/png\")]\n    )\n\n\n@mcp.resource(\n    VIEW_URI,\n    app=AppConfig(csp=ResourceCSP(resource_domains=[\"https://unpkg.com\"])),\n)\ndef view() -> str:\n    \"\"\"Interactive QR code viewer — renders tool results as images.\"\"\"\n    return EMBEDDED_VIEW_HTML\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/atproto_mcp/README.md",
    "content": "# ATProto MCP Server\n\nThis example demonstrates a FastMCP server that provides tools and resources for interacting with the AT Protocol (Bluesky).\n\n## Features\n\n### Resources (Read-only)\n\n- **atproto://profile/status**: Get connection status and profile information\n- **atproto://timeline**: Retrieve your timeline feed\n- **atproto://notifications**: Get recent notifications\n\n### Tools (Actions)\n\n- **post**: Create posts with rich features (text, images, quotes, replies, links, mentions)\n- **create_thread**: Post multipart threads with automatic linking\n- **search**: Search for posts by query\n- **follow**: Follow users by handle\n- **like**: Like posts by URI\n- **repost**: Share posts by URI\n\n## Setup\n\n1. Create a `.env` file in the root directory with your Bluesky credentials:\n\n```bash\nATPROTO_HANDLE=your.handle@bsky.social\nATPROTO_PASSWORD=your-app-password\nATPROTO_PDS_URL=https://bsky.social  # optional, defaults to bsky.social\n```\n\n2. Install and run the server:\n\n```bash\n# Install dependencies\nuv pip install -e .\n\n# Run the server\nuv run atproto-mcp\n```\n\n## The Unified Post Tool\n\nThe `post` tool is a single, flexible interface for all posting needs:\n\n```python\nasync def post(\n    text: str,                          # Required: Post content\n    images: list[str] = None,           # Optional: Image URLs (max 4)\n    image_alts: list[str] = None,       # Optional: Alt text for images\n    links: list[RichTextLink] = None,   # Optional: Embedded links\n    mentions: list[RichTextMention] = None,  # Optional: User mentions\n    reply_to: str = None,               # Optional: Reply to post URI\n    reply_root: str = None,             # Optional: Thread root URI\n    quote: str = None,                  # Optional: Quote post URI\n)\n```\n\n### Usage Examples\n\n```python\nfrom fastmcp import Client\nfrom atproto_mcp.server import atproto_mcp\n\nasync def demo():\n    async with Client(atproto_mcp) as client:\n        # Simple post\n        await client.call_tool(\"post\", {\n            \"text\": \"Hello from FastMCP!\"\n        })\n        \n        # Post with image\n        await client.call_tool(\"post\", {\n            \"text\": \"Beautiful sunset! 🌅\",\n            \"images\": [\"https://example.com/sunset.jpg\"],\n            \"image_alts\": [\"Sunset over the ocean\"]\n        })\n        \n        # Reply to a post\n        await client.call_tool(\"post\", {\n            \"text\": \"Great point!\",\n            \"reply_to\": \"at://did:plc:xxx/app.bsky.feed.post/yyy\"\n        })\n        \n        # Quote post\n        await client.call_tool(\"post\", {\n            \"text\": \"This is important:\",\n            \"quote\": \"at://did:plc:xxx/app.bsky.feed.post/yyy\"\n        })\n        \n        # Rich text with links and mentions\n        await client.call_tool(\"post\", {\n            \"text\": \"Check out FastMCP by @alternatebuild.dev\",\n            \"links\": [{\"text\": \"FastMCP\", \"url\": \"https://github.com/PrefectHQ/fastmcp\"}],\n            \"mentions\": [{\"handle\": \"alternatebuild.dev\", \"display_text\": \"@alternatebuild.dev\"}]\n        })\n        \n        # Advanced: Quote with image\n        await client.call_tool(\"post\", {\n            \"text\": \"Adding visual context:\",\n            \"quote\": \"at://did:plc:xxx/app.bsky.feed.post/yyy\",\n            \"images\": [\"https://example.com/chart.png\"]\n        })\n        \n        # Advanced: Reply with rich text\n        await client.call_tool(\"post\", {\n            \"text\": \"I agree! See this article for more info\",\n            \"reply_to\": \"at://did:plc:xxx/app.bsky.feed.post/yyy\",\n            \"links\": [{\"text\": \"this article\", \"url\": \"https://example.com/article\"}]\n        })\n        \n        # Create a thread\n        await client.call_tool(\"create_thread\", {\n            \"posts\": [\n                {\"text\": \"Starting a thread about Python 🧵\"},\n                {\"text\": \"Python is great for rapid prototyping\"},\n                {\"text\": \"And the ecosystem is amazing!\", \"images\": [\"https://example.com/python.jpg\"]}\n            ]\n        })\n```\n\n## AI Assistant Use Cases\n\nThe unified API enables natural AI assistant interactions:\n\n- **\"Reply to that post with these findings\"** → Uses `reply_to` with rich text\n- **\"Share this article with commentary\"** → Uses `quote` with the article link\n- **\"Post this chart with explanation\"** → Uses `images` with descriptive text\n- **\"Start a thread about AI safety\"** → Uses `create_thread` for automatic linking\n\n## Architecture\n\nThe server is organized as:\n- `server.py` - Public API with resources and tools\n- `_atproto/` - Private implementation module\n  - `_client.py` - ATProto client management\n  - `_posts.py` - Unified posting logic\n  - `_profile.py` - Profile operations\n  - `_read.py` - Timeline, search, notifications\n  - `_social.py` - Follow, like, repost\n- `types.py` - TypedDict definitions\n- `settings.py` - Configuration management\n\n## Running the Demo\n\n```bash\n# Run demo (read-only)\nuv run python demo.py\n\n# Run demo with posting enabled\nuv run python demo.py --post\n```\n\n## Security Note\n\nStore your Bluesky credentials securely in environment variables. Never commit credentials to version control."
  },
  {
    "path": "examples/atproto_mcp/demo.py",
    "content": "\"\"\"Demo script showing all ATProto MCP server capabilities.\"\"\"\n\nimport argparse\nimport asyncio\nimport json\nfrom typing import cast\n\nfrom atproto_mcp.server import atproto_mcp\nfrom atproto_mcp.types import (\n    NotificationsResult,\n    PostResult,\n    ProfileInfo,\n    SearchResult,\n    TimelineResult,\n)\n\nfrom fastmcp import Client\n\n\nasync def main(enable_posting: bool = False):\n    print(\"🔵 ATProto MCP Server Demo\\n\")\n\n    async with Client(atproto_mcp) as client:\n        # 1. Check connection status (resource)\n        print(\"1. Checking connection status...\")\n        result = await client.read_resource(\"atproto://profile/status\")\n        status: ProfileInfo = (\n            json.loads(result[0].text) if result else cast(ProfileInfo, {})\n        )\n\n        if status.get(\"connected\"):\n            print(f\"✅ Connected as: @{status['handle']}\")\n            print(f\"   Followers: {status['followers']}\")\n            print(f\"   Following: {status['following']}\")\n            print(f\"   Posts: {status['posts']}\")\n        else:\n            print(f\"❌ Connection failed: {status.get('error')}\")\n            return\n\n        # 2. Get timeline\n        print(\"\\n2. Getting timeline...\")\n        result = await client.read_resource(\"atproto://timeline\")\n        timeline: TimelineResult = (\n            json.loads(result[0].text) if result else cast(TimelineResult, {})\n        )\n\n        if timeline.get(\"success\") and timeline[\"posts\"]:\n            print(f\"✅ Found {timeline['count']} posts\")\n            post = timeline[\"posts\"][0]\n            print(f\"   Latest by @{post['author']}: {post['text'][:80]}...\")\n            save_uri = post[\"uri\"]  # Save for later interactions\n        else:\n            print(\"❌ No posts in timeline\")\n            save_uri = None\n\n        # 3. Search for posts\n        print(\"\\n3. Searching for posts about 'Bluesky'...\")\n        result = await client.call_tool(\"search\", {\"query\": \"Bluesky\", \"limit\": 5})\n        search: SearchResult = (\n            json.loads(result[0].text) if result else cast(SearchResult, {})\n        )\n\n        if search.get(\"success\") and search[\"posts\"]:\n            print(f\"✅ Found {search['count']} posts\")\n            print(f\"   Sample: {search['posts'][0]['text'][:80]}...\")\n\n        # 4. Get notifications\n        print(\"\\n4. Checking notifications...\")\n        result = await client.read_resource(\"atproto://notifications\")\n        notifs: NotificationsResult = (\n            json.loads(result[0].text) if result else cast(NotificationsResult, {})\n        )\n\n        if notifs.get(\"success\"):\n            print(f\"✅ You have {notifs['count']} notifications\")\n            unread = sum(1 for n in notifs[\"notifications\"] if not n[\"is_read\"])\n            if unread:\n                print(f\"   ({unread} unread)\")\n\n        # 5. Demo posting capabilities\n        if enable_posting:\n            print(\"\\n5. Demonstrating posting capabilities...\")\n\n            # a. Simple post\n            print(\"\\n   a) Creating a simple post...\")\n            result = await client.call_tool(\n                \"post\",\n                {\"text\": \"🧪 Testing the unified ATProto MCP post tool! #FastMCP\"},\n            )\n            post_result: PostResult = json.loads(result[0].text) if result else {}\n            if post_result.get(\"success\"):\n                print(\"   ✅ Posted successfully!\")\n                simple_uri = post_result[\"uri\"]\n            else:\n                print(f\"   ❌ Failed: {post_result.get('error')}\")\n                simple_uri = None\n\n            # b. Post with rich text (link and mention)\n            print(\"\\n   b) Creating a post with rich text...\")\n            result = await client.call_tool(\n                \"post\",\n                {\n                    \"text\": \"Check out FastMCP and follow @alternatebuild.dev for updates!\",\n                    \"links\": [\n                        {\n                            \"text\": \"FastMCP\",\n                            \"url\": \"https://github.com/PrefectHQ/fastmcp\",\n                        }\n                    ],\n                    \"mentions\": [\n                        {\n                            \"handle\": \"alternatebuild.dev\",\n                            \"display_text\": \"@alternatebuild.dev\",\n                        }\n                    ],\n                },\n            )\n            if json.loads(result[0].text).get(\"success\"):\n                print(\"   ✅ Rich text post created!\")\n\n            # c. Reply to a post\n            if save_uri:\n                print(\"\\n   c) Replying to a post...\")\n                result = await client.call_tool(\n                    \"post\", {\"text\": \"Great post! 👍\", \"reply_to\": save_uri}\n                )\n                if json.loads(result[0].text).get(\"success\"):\n                    print(\"   ✅ Reply posted!\")\n\n            # d. Quote post\n            if simple_uri:\n                print(\"\\n   d) Creating a quote post...\")\n                result = await client.call_tool(\n                    \"post\",\n                    {\n                        \"text\": \"Quoting my own test post for demo purposes 🔄\",\n                        \"quote\": simple_uri,\n                    },\n                )\n                if json.loads(result[0].text).get(\"success\"):\n                    print(\"   ✅ Quote post created!\")\n\n            # e. Post with image\n            print(\"\\n   e) Creating a post with image...\")\n            result = await client.call_tool(\n                \"post\",\n                {\n                    \"text\": \"Here's a test image post! 📸\",\n                    \"images\": [\"https://picsum.photos/400/300\"],\n                    \"image_alts\": [\"Random test image\"],\n                },\n            )\n            if json.loads(result[0].text).get(\"success\"):\n                print(\"   ✅ Image post created!\")\n\n            # f. Quote with image (advanced)\n            if simple_uri:\n                print(\"\\n   f) Creating a quote post with image...\")\n                result = await client.call_tool(\n                    \"post\",\n                    {\n                        \"text\": \"Quote + image combo! 🎨\",\n                        \"quote\": simple_uri,\n                        \"images\": [\"https://picsum.photos/300/200\"],\n                        \"image_alts\": [\"Another test image\"],\n                    },\n                )\n                if json.loads(result[0].text).get(\"success\"):\n                    print(\"   ✅ Quote with image created!\")\n\n            # g. Social actions\n            if save_uri:\n                print(\"\\n   g) Demonstrating social actions...\")\n\n                # Like\n                result = await client.call_tool(\"like\", {\"uri\": save_uri})\n                if json.loads(result[0].text).get(\"success\"):\n                    print(\"   ✅ Liked a post!\")\n\n                # Repost\n                result = await client.call_tool(\"repost\", {\"uri\": save_uri})\n                if json.loads(result[0].text).get(\"success\"):\n                    print(\"   ✅ Reposted!\")\n\n                # Follow\n                result = await client.call_tool(\n                    \"follow\", {\"handle\": \"alternatebuild.dev\"}\n                )\n                if json.loads(result[0].text).get(\"success\"):\n                    print(\"   ✅ Followed @alternatebuild.dev!\")\n\n            # h. Thread creation (new!)\n            print(\"\\n   h) Creating a thread...\")\n            result = await client.call_tool(\n                \"create_thread\",\n                {\n                    \"posts\": [\n                        {\n                            \"text\": \"Let me share some thoughts about the ATProto MCP server 🧵\"\n                        },\n                        {\n                            \"text\": \"First, it makes posting from the terminal incredibly smooth\"\n                        },\n                        {\n                            \"text\": \"The unified post API means one tool handles everything\",\n                            \"links\": [\n                                {\n                                    \"text\": \"everything\",\n                                    \"url\": \"https://github.com/PrefectHQ/fastmcp\",\n                                }\n                            ],\n                        },\n                        {\n                            \"text\": \"And now with create_thread, multi-post threads are trivial!\"\n                        },\n                    ]\n                },\n            )\n            if json.loads(result[0].text).get(\"success\"):\n                thread_result = json.loads(result[0].text)\n                print(f\"   ✅ Thread created with {thread_result['post_count']} posts!\")\n        else:\n            print(\"\\n5. Posting capabilities (not enabled):\")\n            print(\"   To test posting, run with --post flag\")\n            print(\"   Example: python demo.py --post\")\n\n        # 6. Show available capabilities\n        print(\"\\n6. Available capabilities:\")\n        print(\"\\n   Resources (read-only):\")\n        print(\"     - atproto://profile/status\")\n        print(\"     - atproto://timeline\")\n        print(\"     - atproto://notifications\")\n\n        print(\"\\n   Tools (actions):\")\n        print(\"     - post: Unified posting with rich features\")\n        print(\"       • Simple text posts\")\n        print(\"       • Images (up to 4)\")\n        print(\"       • Rich text (links, mentions)\")\n        print(\"       • Replies and threads\")\n        print(\"       • Quote posts\")\n        print(\"       • Combinations (quote + image, reply + rich text, etc.)\")\n        print(\"     - search: Search for posts\")\n        print(\"     - create_thread: Post multi-part threads\")\n        print(\"     - follow: Follow users\")\n        print(\"     - like: Like posts\")\n        print(\"     - repost: Share posts\")\n\n        print(\"\\n✨ Demo complete!\")\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"ATProto MCP Server Demo\")\n    parser.add_argument(\n        \"--post\",\n        action=\"store_true\",\n        help=\"Enable posting test messages to Bluesky\",\n    )\n    args = parser.parse_args()\n\n    asyncio.run(main(enable_posting=args.post))\n"
  },
  {
    "path": "examples/atproto_mcp/fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"src/atproto_mcp/server.py\"\n  },\n  \"environment\": {\n    \"dependencies\": [\n      \"atproto_mcp@git+https://github.com/PrefectHQ/fastmcp.git#subdirectory=examples/atproto_mcp\"\n    ]\n  }\n}"
  },
  {
    "path": "examples/atproto_mcp/pyproject.toml",
    "content": "[project]\nname = \"atproto-mcp\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nauthors = [{ name = \"zzstoatzz\", email = \"thrast36@gmail.com\" }]\nrequires-python = \">=3.10\"\ndependencies = [\n    \"fastmcp>=0.8.0\",\n    \"atproto@git+https://github.com/MarshalX/atproto.git@refs/pull/605/head\",\n    \"pydantic-settings>=2.0.0\",\n    \"websockets>=15.0.1\",\n    \"httpx>=0.27.0\",\n]\n\n[project.scripts]\natproto-mcp = \"atproto_mcp.__main__:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.metadata]\nallow-direct-references = true\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/__init__.py",
    "content": "from atproto_mcp.settings import settings\n\n__all__ = [\"settings\"]\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/__main__.py",
    "content": "from atproto_mcp.server import atproto_mcp\n\n\ndef main():\n    atproto_mcp.run()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/_atproto/__init__.py",
    "content": "\"\"\"Private ATProto implementation module.\"\"\"\n\nfrom ._client import get_client\nfrom ._posts import create_post, create_thread\nfrom ._profile import get_profile_info\nfrom ._read import fetch_notifications, fetch_timeline, search_for_posts\nfrom ._social import follow_user_by_handle, like_post_by_uri, repost_by_uri\n\n__all__ = [\n    \"create_post\",\n    \"create_thread\",\n    \"fetch_notifications\",\n    \"fetch_timeline\",\n    \"follow_user_by_handle\",\n    \"get_client\",\n    \"get_profile_info\",\n    \"like_post_by_uri\",\n    \"repost_by_uri\",\n    \"search_for_posts\",\n]\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py",
    "content": "\"\"\"ATProto client management.\"\"\"\n\nfrom atproto import Client\n\nfrom atproto_mcp.settings import settings\n\n_client: Client | None = None\n\n\ndef get_client() -> Client:\n    \"\"\"Get or create an authenticated ATProto client.\"\"\"\n    global _client\n    if _client is None:\n        _client = Client()\n        _client.login(settings.atproto_handle, settings.atproto_password)\n    return _client\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py",
    "content": "\"\"\"Unified posting functionality.\"\"\"\n\nimport time\nfrom datetime import datetime\n\nfrom atproto import models\n\nfrom atproto_mcp.types import (\n    PostResult,\n    RichTextLink,\n    RichTextMention,\n    ThreadPost,\n    ThreadResult,\n)\n\nfrom ._client import get_client\n\n\ndef create_post(\n    text: str,\n    images: list[str] | None = None,\n    image_alts: list[str] | None = None,\n    links: list[RichTextLink] | None = None,\n    mentions: list[RichTextMention] | None = None,\n    reply_to: str | None = None,\n    reply_root: str | None = None,\n    quote: str | None = None,\n) -> PostResult:\n    \"\"\"Create a unified post with optional features.\n\n    Args:\n        text: Post text (max 300 chars)\n        images: URLs of images to attach (max 4)\n        image_alts: Alt text for images\n        links: Links to embed in rich text\n        mentions: User mentions to embed\n        reply_to: URI of post to reply to\n        reply_root: URI of thread root (defaults to reply_to)\n        quote: URI of post to quote\n    \"\"\"\n    try:\n        client = get_client()\n        facets = []\n        embed = None\n        reply_ref = None\n\n        # Always build facets to handle auto-detected URLs and explicit links/mentions\n        facets = _build_facets(text, links, mentions, client)\n\n        # Handle replies\n        if reply_to:\n            reply_ref = _build_reply_ref(reply_to, reply_root, client)\n\n        # Handle quotes and images\n        if quote and images:\n            # Quote with images - create record with media embed\n            embed = _build_quote_with_images_embed(quote, images, image_alts, client)\n        elif quote:\n            # Quote only\n            embed = _build_quote_embed(quote, client)\n        elif images:\n            # Images only - use send_images for proper handling\n            return _send_images(text, images, image_alts, facets, reply_ref, client)\n\n        # Send the post (always include facets if any were created)\n        post = client.send_post(\n            text=text,\n            facets=facets if facets else None,\n            embed=embed,\n            reply_to=reply_ref,\n        )\n\n        return PostResult(\n            success=True,\n            uri=post.uri,\n            cid=post.cid,\n            text=text,\n            created_at=datetime.now().isoformat(),\n            error=None,\n        )\n    except Exception as e:\n        return PostResult(\n            success=False,\n            uri=None,\n            cid=None,\n            text=None,\n            created_at=None,\n            error=str(e),\n        )\n\n\ndef _build_facets(\n    text: str,\n    links: list[RichTextLink] | None,\n    mentions: list[RichTextMention] | None,\n    client,\n):\n    \"\"\"Build facets for rich text formatting, including auto-detected URLs.\"\"\"\n    import re\n\n    facets = []\n    covered_ranges = []\n\n    # URL regex pattern for auto-detection\n    url_pattern = re.compile(\n        r\"https?://(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&/=]*)\"\n    )\n\n    # Process explicit links first\n    if links:\n        for link in links:\n            start = text.find(link[\"text\"])\n            if start == -1:\n                continue\n            end = start + len(link[\"text\"])\n\n            # Track this range as covered\n            covered_ranges.append((start, end))\n\n            facets.append(\n                models.AppBskyRichtextFacet.Main(\n                    features=[models.AppBskyRichtextFacet.Link(uri=link[\"url\"])],\n                    index=models.AppBskyRichtextFacet.ByteSlice(\n                        byte_start=len(text[:start].encode(\"UTF-8\")),\n                        byte_end=len(text[:end].encode(\"UTF-8\")),\n                    ),\n                )\n            )\n\n    # Auto-detect URLs that aren't already covered by explicit links\n    for match in url_pattern.finditer(text):\n        url = match.group()\n        start = match.start()\n        end = match.end()\n\n        # Check if this URL overlaps with any explicit link\n        overlaps = False\n        for covered_start, covered_end in covered_ranges:\n            if not (end <= covered_start or start >= covered_end):\n                overlaps = True\n                break\n\n        if not overlaps:\n            facets.append(\n                models.AppBskyRichtextFacet.Main(\n                    features=[models.AppBskyRichtextFacet.Link(uri=url)],\n                    index=models.AppBskyRichtextFacet.ByteSlice(\n                        byte_start=len(text[:start].encode(\"UTF-8\")),\n                        byte_end=len(text[:end].encode(\"UTF-8\")),\n                    ),\n                )\n            )\n\n    # Process mentions\n    if mentions:\n        for mention in mentions:\n            display_text = mention.get(\"display_text\") or f\"@{mention['handle']}\"\n            start = text.find(display_text)\n            if start == -1:\n                continue\n            end = start + len(display_text)\n\n            # Resolve handle to DID\n            resolved = client.app.bsky.actor.search_actors(\n                params={\"q\": mention[\"handle\"], \"limit\": 1}\n            )\n            if not resolved.actors:\n                continue\n\n            did = resolved.actors[0].did\n            facets.append(\n                models.AppBskyRichtextFacet.Main(\n                    features=[models.AppBskyRichtextFacet.Mention(did=did)],\n                    index=models.AppBskyRichtextFacet.ByteSlice(\n                        byte_start=len(text[:start].encode(\"UTF-8\")),\n                        byte_end=len(text[:end].encode(\"UTF-8\")),\n                    ),\n                )\n            )\n\n    return facets\n\n\ndef _build_reply_ref(reply_to: str, reply_root: str | None, client):\n    \"\"\"Build reply reference.\"\"\"\n    # Get parent post to extract CID\n    parent_post = client.app.bsky.feed.get_posts(params={\"uris\": [reply_to]})\n    if not parent_post.posts:\n        raise ValueError(\"Parent post not found\")\n\n    parent_cid = parent_post.posts[0].cid\n    parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=reply_to, cid=parent_cid)\n\n    # If no root_uri provided, parent is the root\n    if reply_root is None:\n        root_ref = parent_ref\n    else:\n        # Get root post CID\n        root_post = client.app.bsky.feed.get_posts(params={\"uris\": [reply_root]})\n        if not root_post.posts:\n            raise ValueError(\"Root post not found\")\n        root_cid = root_post.posts[0].cid\n        root_ref = models.ComAtprotoRepoStrongRef.Main(uri=reply_root, cid=root_cid)\n\n    return models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref)\n\n\ndef _build_quote_embed(quote_uri: str, client):\n    \"\"\"Build quote embed.\"\"\"\n    # Get the post to quote\n    quoted_post = client.app.bsky.feed.get_posts(params={\"uris\": [quote_uri]})\n    if not quoted_post.posts:\n        raise ValueError(\"Quoted post not found\")\n\n    # Create strong ref for the quoted post\n    quoted_cid = quoted_post.posts[0].cid\n    quoted_ref = models.ComAtprotoRepoStrongRef.Main(uri=quote_uri, cid=quoted_cid)\n\n    # Create the embed\n    return models.AppBskyEmbedRecord.Main(record=quoted_ref)\n\n\ndef _build_quote_with_images_embed(\n    quote_uri: str, image_urls: list[str], image_alts: list[str] | None, client\n):\n    \"\"\"Build quote embed with images.\"\"\"\n    import httpx\n\n    # Get the quoted post\n    quoted_post = client.app.bsky.feed.get_posts(params={\"uris\": [quote_uri]})\n    if not quoted_post.posts:\n        raise ValueError(\"Quoted post not found\")\n\n    quoted_cid = quoted_post.posts[0].cid\n    quoted_ref = models.ComAtprotoRepoStrongRef.Main(uri=quote_uri, cid=quoted_cid)\n\n    # Download and upload images\n    images = []\n    alts = image_alts or [\"\"] * len(image_urls)\n\n    for i, url in enumerate(image_urls[:4]):\n        response = httpx.get(url, follow_redirects=True)\n        response.raise_for_status()\n\n        # Upload to blob storage\n        upload = client.upload_blob(response.content)\n        images.append(\n            models.AppBskyEmbedImages.Image(\n                alt=alts[i] if i < len(alts) else \"\",\n                image=upload.blob,\n            )\n        )\n\n    # Create record with media embed\n    return models.AppBskyEmbedRecordWithMedia.Main(\n        record=models.AppBskyEmbedRecord.Main(record=quoted_ref),\n        media=models.AppBskyEmbedImages.Main(images=images),\n    )\n\n\ndef _send_images(\n    text: str,\n    image_urls: list[str],\n    image_alts: list[str] | None,\n    facets,\n    reply_ref,\n    client,\n):\n    \"\"\"Send post with images using the client's send_images method.\"\"\"\n    import httpx\n\n    # Ensure alt_texts has same length as images\n    if image_alts is None:\n        image_alts = [\"\"] * len(image_urls)\n    elif len(image_alts) < len(image_urls):\n        image_alts.extend([\"\"] * (len(image_urls) - len(image_alts)))\n\n    image_data = []\n    alts = []\n    for i, url in enumerate(image_urls[:4]):  # Max 4 images\n        # Download image (follow redirects)\n        response = httpx.get(url, follow_redirects=True)\n        response.raise_for_status()\n\n        image_data.append(response.content)\n        alts.append(image_alts[i] if i < len(image_alts) else \"\")\n\n    # Send post with images\n    # Note: send_images doesn't support facets or reply_to directly\n    # So we need to use send_post with manual image upload if we have facets or replies\n    # Since we always create facets now (for URL auto-detection), we'll always use this path\n    if facets or reply_ref:\n        # Manual image upload\n        images = []\n        for i, data in enumerate(image_data):\n            upload = client.upload_blob(data)\n            images.append(\n                models.AppBskyEmbedImages.Image(\n                    alt=alts[i],\n                    image=upload.blob,\n                )\n            )\n\n        embed = models.AppBskyEmbedImages.Main(images=images)\n        post = client.send_post(\n            text=text,\n            facets=facets if facets else None,\n            embed=embed,\n            reply_to=reply_ref,\n        )\n    else:\n        # Use simple send_images\n        post = client.send_images(\n            text=text,\n            images=image_data,\n            image_alts=alts,\n        )\n\n    return PostResult(\n        success=True,\n        uri=post.uri,\n        cid=post.cid,\n        text=text,\n        created_at=datetime.now().isoformat(),\n        error=None,\n    )\n\n\ndef create_thread(posts: list[ThreadPost]) -> ThreadResult:\n    \"\"\"Create a thread of posts with automatic linking.\n\n    Args:\n        posts: List of posts to create as a thread. First post is the root.\n    \"\"\"\n    if not posts:\n        return ThreadResult(\n            success=False,\n            thread_uri=None,\n            post_uris=[],\n            post_count=0,\n            error=\"No posts provided\",\n        )\n\n    try:\n        post_uris = []\n        root_uri = None\n        parent_uri = None\n\n        for i, post_data in enumerate(posts):\n            # First post is the root\n            if i == 0:\n                result = create_post(\n                    text=post_data[\"text\"],\n                    images=post_data.get(\"images\"),\n                    image_alts=post_data.get(\"image_alts\"),\n                    links=post_data.get(\"links\"),\n                    mentions=post_data.get(\"mentions\"),\n                    quote=post_data.get(\"quote\"),\n                )\n\n                if not result[\"success\"]:\n                    return ThreadResult(\n                        success=False,\n                        thread_uri=None,\n                        post_uris=post_uris,\n                        post_count=len(post_uris),\n                        error=f\"Failed to create root post: {result['error']}\",\n                    )\n\n                root_uri = result[\"uri\"]\n                parent_uri = root_uri\n                post_uris.append(root_uri)\n\n                # Small delay to ensure post is indexed\n                time.sleep(0.5)\n            else:\n                # Subsequent posts reply to the previous one\n                result = create_post(\n                    text=post_data[\"text\"],\n                    images=post_data.get(\"images\"),\n                    image_alts=post_data.get(\"image_alts\"),\n                    links=post_data.get(\"links\"),\n                    mentions=post_data.get(\"mentions\"),\n                    quote=post_data.get(\"quote\"),\n                    reply_to=parent_uri,\n                    reply_root=root_uri,\n                )\n\n                if not result[\"success\"]:\n                    return ThreadResult(\n                        success=False,\n                        thread_uri=root_uri,\n                        post_uris=post_uris,\n                        post_count=len(post_uris),\n                        error=f\"Failed to create post {i + 1}: {result['error']}\",\n                    )\n\n                parent_uri = result[\"uri\"]\n                post_uris.append(parent_uri)\n\n                # Small delay between posts\n                if i < len(posts) - 1:\n                    time.sleep(0.5)\n\n        return ThreadResult(\n            success=True,\n            thread_uri=root_uri,\n            post_uris=post_uris,\n            post_count=len(post_uris),\n            error=None,\n        )\n\n    except Exception as e:\n        return ThreadResult(\n            success=False,\n            thread_uri=None,\n            post_uris=post_uris,\n            post_count=len(post_uris),\n            error=str(e),\n        )\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/_atproto/_profile.py",
    "content": "\"\"\"Profile-related operations.\"\"\"\n\nfrom atproto_mcp.types import ProfileInfo\n\nfrom ._client import get_client\n\n\ndef get_profile_info() -> ProfileInfo:\n    \"\"\"Get profile information for the authenticated user.\"\"\"\n    try:\n        client = get_client()\n        profile = client.get_profile(client.me.did)\n        return ProfileInfo(\n            connected=True,\n            handle=profile.handle,\n            display_name=profile.display_name,\n            did=client.me.did,\n            followers=profile.followers_count,\n            following=profile.follows_count,\n            posts=profile.posts_count,\n            error=None,\n        )\n    except Exception as e:\n        return ProfileInfo(\n            connected=False,\n            handle=None,\n            display_name=None,\n            did=None,\n            followers=None,\n            following=None,\n            posts=None,\n            error=str(e),\n        )\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/_atproto/_read.py",
    "content": "\"\"\"Read-only operations for timeline, search, and notifications.\"\"\"\n\nfrom atproto_mcp.types import (\n    Notification,\n    NotificationsResult,\n    Post,\n    SearchResult,\n    TimelineResult,\n)\n\nfrom ._client import get_client\n\n\ndef fetch_timeline(limit: int = 10) -> TimelineResult:\n    \"\"\"Fetch the authenticated user's timeline.\"\"\"\n    try:\n        client = get_client()\n        timeline = client.get_timeline(limit=limit)\n\n        posts = []\n        for feed_view in timeline.feed:\n            post = feed_view.post\n            posts.append(\n                Post(\n                    uri=post.uri,\n                    cid=post.cid,\n                    text=post.record.text if hasattr(post.record, \"text\") else \"\",\n                    author=post.author.handle,\n                    created_at=post.record.created_at,\n                    likes=post.like_count or 0,\n                    reposts=post.repost_count or 0,\n                    replies=post.reply_count or 0,\n                )\n            )\n\n        return TimelineResult(\n            success=True,\n            posts=posts,\n            count=len(posts),\n            error=None,\n        )\n    except Exception as e:\n        return TimelineResult(\n            success=False,\n            posts=[],\n            count=0,\n            error=str(e),\n        )\n\n\ndef search_for_posts(query: str, limit: int = 10) -> SearchResult:\n    \"\"\"Search for posts containing specific text.\"\"\"\n    try:\n        client = get_client()\n        search_results = client.app.bsky.feed.search_posts(\n            params={\"q\": query, \"limit\": limit}\n        )\n\n        posts = []\n        for post in search_results.posts:\n            posts.append(\n                Post(\n                    uri=post.uri,\n                    cid=post.cid,\n                    text=post.record.text if hasattr(post.record, \"text\") else \"\",\n                    author=post.author.handle,\n                    created_at=post.record.created_at,\n                    likes=post.like_count or 0,\n                    reposts=post.repost_count or 0,\n                    replies=post.reply_count or 0,\n                )\n            )\n\n        return SearchResult(\n            success=True,\n            query=query,\n            posts=posts,\n            count=len(posts),\n            error=None,\n        )\n    except Exception as e:\n        return SearchResult(\n            success=False,\n            query=query,\n            posts=[],\n            count=0,\n            error=str(e),\n        )\n\n\ndef fetch_notifications(limit: int = 10) -> NotificationsResult:\n    \"\"\"Fetch recent notifications.\"\"\"\n    try:\n        client = get_client()\n        notifs = client.app.bsky.notification.list_notifications(\n            params={\"limit\": limit}\n        )\n\n        notifications = []\n        for notif in notifs.notifications:\n            notifications.append(\n                Notification(\n                    uri=notif.uri,\n                    cid=notif.cid,\n                    author=notif.author.handle,\n                    reason=notif.reason,\n                    is_read=notif.is_read,\n                    indexed_at=notif.indexed_at,\n                )\n            )\n\n        return NotificationsResult(\n            success=True,\n            notifications=notifications,\n            count=len(notifications),\n            error=None,\n        )\n    except Exception as e:\n        return NotificationsResult(\n            success=False,\n            notifications=[],\n            count=0,\n            error=str(e),\n        )\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/_atproto/_social.py",
    "content": "\"\"\"Social actions like follow, like, and repost.\"\"\"\n\nfrom atproto_mcp.types import FollowResult, LikeResult, RepostResult\n\nfrom ._client import get_client\n\n\ndef follow_user_by_handle(handle: str) -> FollowResult:\n    \"\"\"Follow a user by their handle.\"\"\"\n    try:\n        client = get_client()\n        # Search for the user to get their DID\n        results = client.app.bsky.actor.search_actors(params={\"q\": handle, \"limit\": 1})\n        if not results.actors:\n            return FollowResult(\n                success=False,\n                did=None,\n                handle=None,\n                uri=None,\n                error=f\"User @{handle} not found\",\n            )\n\n        actor = results.actors[0]\n        # Create the follow\n        follow = client.follow(actor.did)\n        return FollowResult(\n            success=True,\n            did=actor.did,\n            handle=actor.handle,\n            uri=follow.uri,\n            error=None,\n        )\n    except Exception as e:\n        return FollowResult(\n            success=False,\n            did=None,\n            handle=None,\n            uri=None,\n            error=str(e),\n        )\n\n\ndef like_post_by_uri(uri: str) -> LikeResult:\n    \"\"\"Like a post by its AT URI.\"\"\"\n    try:\n        client = get_client()\n        # Parse the URI to get the components\n        # URI format: at://did:plc:xxx/app.bsky.feed.post/yyy\n        parts = uri.replace(\"at://\", \"\").split(\"/\")\n        if len(parts) != 3 or parts[1] != \"app.bsky.feed.post\":\n            raise ValueError(\"Invalid post URI format\")\n\n        # Get the post to retrieve its CID\n        post = client.app.bsky.feed.get_posts(params={\"uris\": [uri]})\n        if not post.posts:\n            raise ValueError(\"Post not found\")\n\n        cid = post.posts[0].cid\n\n        # Now like the post with both URI and CID\n        like = client.like(uri, cid)\n        return LikeResult(\n            success=True,\n            liked_uri=uri,\n            like_uri=like.uri,\n            error=None,\n        )\n    except Exception as e:\n        return LikeResult(\n            success=False,\n            liked_uri=None,\n            like_uri=None,\n            error=str(e),\n        )\n\n\ndef repost_by_uri(uri: str) -> RepostResult:\n    \"\"\"Repost a post by its AT URI.\"\"\"\n    try:\n        client = get_client()\n        # Parse the URI to get the components\n        # URI format: at://did:plc:xxx/app.bsky.feed.post/yyy\n        parts = uri.replace(\"at://\", \"\").split(\"/\")\n        if len(parts) != 3 or parts[1] != \"app.bsky.feed.post\":\n            raise ValueError(\"Invalid post URI format\")\n\n        # Get the post to retrieve its CID\n        post = client.app.bsky.feed.get_posts(params={\"uris\": [uri]})\n        if not post.posts:\n            raise ValueError(\"Post not found\")\n\n        cid = post.posts[0].cid\n\n        # Now repost with both URI and CID\n        repost = client.repost(uri, cid)\n        return RepostResult(\n            success=True,\n            reposted_uri=uri,\n            repost_uri=repost.uri,\n            error=None,\n        )\n    except Exception as e:\n        return RepostResult(\n            success=False,\n            reposted_uri=None,\n            repost_uri=None,\n            error=str(e),\n        )\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/py.typed",
    "content": ""
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/server.py",
    "content": "\"\"\"ATProto MCP Server - Public API exposing Bluesky tools and resources.\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import Field\n\nfrom atproto_mcp import _atproto\nfrom atproto_mcp.settings import settings\nfrom atproto_mcp.types import (\n    FollowResult,\n    LikeResult,\n    NotificationsResult,\n    PostResult,\n    ProfileInfo,\n    RepostResult,\n    RichTextLink,\n    RichTextMention,\n    SearchResult,\n    ThreadPost,\n    ThreadResult,\n    TimelineResult,\n)\nfrom fastmcp import FastMCP\n\natproto_mcp = FastMCP(\"ATProto MCP Server\")\n\n\n# Resources - read-only operations\n@atproto_mcp.resource(\"atproto://profile/status\")\ndef atproto_status() -> ProfileInfo:\n    \"\"\"Check the status of the ATProto connection and current user profile.\"\"\"\n    return _atproto.get_profile_info()\n\n\n@atproto_mcp.resource(\"atproto://timeline\")\ndef get_timeline() -> TimelineResult:\n    \"\"\"Get the authenticated user's timeline feed.\"\"\"\n    return _atproto.fetch_timeline(settings.atproto_timeline_default_limit)\n\n\n@atproto_mcp.resource(\"atproto://notifications\")\ndef get_notifications() -> NotificationsResult:\n    \"\"\"Get recent notifications for the authenticated user.\"\"\"\n    return _atproto.fetch_notifications(settings.atproto_notifications_default_limit)\n\n\n# Tools - actions that modify state\n@atproto_mcp.tool\ndef post(\n    text: Annotated[\n        str, Field(max_length=300, description=\"The text content of the post\")\n    ],\n    images: Annotated[\n        list[str] | None,\n        Field(max_length=4, description=\"URLs of images to attach (max 4)\"),\n    ] = None,\n    image_alts: Annotated[\n        list[str] | None, Field(description=\"Alt text for each image\")\n    ] = None,\n    links: Annotated[\n        list[RichTextLink] | None, Field(description=\"Links to embed in the text\")\n    ] = None,\n    mentions: Annotated[\n        list[RichTextMention] | None, Field(description=\"User mentions to embed\")\n    ] = None,\n    reply_to: Annotated[\n        str | None, Field(description=\"AT URI of post to reply to\")\n    ] = None,\n    reply_root: Annotated[\n        str | None, Field(description=\"AT URI of thread root (defaults to reply_to)\")\n    ] = None,\n    quote: Annotated[str | None, Field(description=\"AT URI of post to quote\")] = None,\n) -> PostResult:\n    \"\"\"Create a post with optional rich features like images, quotes, replies, and rich text.\n\n    Examples:\n        - Simple post: post(\"Hello world!\")\n        - With image: post(\"Check this out!\", images=[\"https://example.com/img.jpg\"])\n        - Reply: post(\"I agree!\", reply_to=\"at://did/app.bsky.feed.post/123\")\n        - Quote: post(\"Great point!\", quote=\"at://did/app.bsky.feed.post/456\")\n        - Rich text: post(\"Check out example.com\", links=[{\"text\": \"example.com\", \"url\": \"https://example.com\"}])\n    \"\"\"\n    return _atproto.create_post(\n        text, images, image_alts, links, mentions, reply_to, reply_root, quote\n    )\n\n\n@atproto_mcp.tool\ndef follow(\n    handle: Annotated[\n        str,\n        Field(\n            description=\"The handle of the user to follow (e.g., 'user.bsky.social')\"\n        ),\n    ],\n) -> FollowResult:\n    \"\"\"Follow a user by their handle.\"\"\"\n    return _atproto.follow_user_by_handle(handle)\n\n\n@atproto_mcp.tool\ndef like(\n    uri: Annotated[str, Field(description=\"The AT URI of the post to like\")],\n) -> LikeResult:\n    \"\"\"Like a post by its AT URI.\"\"\"\n    return _atproto.like_post_by_uri(uri)\n\n\n@atproto_mcp.tool\ndef repost(\n    uri: Annotated[str, Field(description=\"The AT URI of the post to repost\")],\n) -> RepostResult:\n    \"\"\"Repost a post by its AT URI.\"\"\"\n    return _atproto.repost_by_uri(uri)\n\n\n@atproto_mcp.tool\ndef search(\n    query: Annotated[str, Field(description=\"Search query for posts\")],\n    limit: Annotated[\n        int, Field(ge=1, le=100, description=\"Number of results to return\")\n    ] = settings.atproto_search_default_limit,\n) -> SearchResult:\n    \"\"\"Search for posts containing specific text.\"\"\"\n    return _atproto.search_for_posts(query, limit)\n\n\n@atproto_mcp.tool\ndef create_thread(\n    posts: Annotated[\n        list[ThreadPost],\n        Field(\n            description=\"List of posts to create as a thread. Each post can have text, images, links, mentions, and quotes.\"\n        ),\n    ],\n) -> ThreadResult:\n    \"\"\"Create a thread of posts with automatic linking.\n\n    The first post becomes the root of the thread, and each subsequent post\n    replies to the previous one, maintaining the thread structure.\n\n    Example:\n        create_thread([\n            {\"text\": \"Starting a thread about Python 🧵\"},\n            {\"text\": \"Python is great for rapid development\"},\n            {\"text\": \"And the ecosystem is amazing!\", \"images\": [\"https://example.com/python.jpg\"]}\n        ])\n    \"\"\"\n    return _atproto.create_thread(posts)\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/settings.py",
    "content": "from pydantic import Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(env_file=[\".env\"], extra=\"ignore\")\n\n    atproto_handle: str = Field(default=...)\n    atproto_password: str = Field(default=...)\n    atproto_pds_url: str = Field(default=\"https://bsky.social\")\n\n    atproto_notifications_default_limit: int = Field(default=10)\n    atproto_timeline_default_limit: int = Field(default=10)\n    atproto_search_default_limit: int = Field(default=10)\n\n\nsettings = Settings()\n"
  },
  {
    "path": "examples/atproto_mcp/src/atproto_mcp/types.py",
    "content": "\"\"\"Type definitions for ATProto MCP server.\"\"\"\n\nfrom typing import TypedDict\n\n\nclass ProfileInfo(TypedDict):\n    \"\"\"Profile information response.\"\"\"\n\n    connected: bool\n    handle: str | None\n    display_name: str | None\n    did: str | None\n    followers: int | None\n    following: int | None\n    posts: int | None\n    error: str | None\n\n\nclass PostResult(TypedDict):\n    \"\"\"Result of creating a post.\"\"\"\n\n    success: bool\n    uri: str | None\n    cid: str | None\n    text: str | None\n    created_at: str | None\n    error: str | None\n\n\nclass Post(TypedDict):\n    \"\"\"A single post.\"\"\"\n\n    author: str\n    text: str | None\n    created_at: str | None\n    likes: int\n    reposts: int\n    replies: int\n    uri: str\n    cid: str\n\n\nclass TimelineResult(TypedDict):\n    \"\"\"Timeline fetch result.\"\"\"\n\n    success: bool\n    count: int\n    posts: list[Post]\n    error: str | None\n\n\nclass SearchResult(TypedDict):\n    \"\"\"Search result.\"\"\"\n\n    success: bool\n    query: str\n    count: int\n    posts: list[Post]\n    error: str | None\n\n\nclass Notification(TypedDict):\n    \"\"\"A single notification.\"\"\"\n\n    reason: str\n    author: str | None\n    is_read: bool\n    indexed_at: str\n    uri: str\n    cid: str\n\n\nclass NotificationsResult(TypedDict):\n    \"\"\"Notifications fetch result.\"\"\"\n\n    success: bool\n    count: int\n    notifications: list[Notification]\n    error: str | None\n\n\nclass FollowResult(TypedDict):\n    \"\"\"Result of following a user.\"\"\"\n\n    success: bool\n    handle: str | None\n    did: str | None\n    uri: str | None\n    error: str | None\n\n\nclass LikeResult(TypedDict):\n    \"\"\"Result of liking a post.\"\"\"\n\n    success: bool\n    liked_uri: str | None\n    like_uri: str | None\n    error: str | None\n\n\nclass RepostResult(TypedDict):\n    \"\"\"Result of reposting.\"\"\"\n\n    success: bool\n    reposted_uri: str | None\n    repost_uri: str | None\n    error: str | None\n\n\nclass RichTextLink(TypedDict):\n    \"\"\"A link in rich text.\"\"\"\n\n    text: str\n    url: str\n\n\nclass RichTextMention(TypedDict):\n    \"\"\"A mention in rich text.\"\"\"\n\n    handle: str\n    display_text: str | None\n\n\nclass ThreadPost(TypedDict, total=False):\n    \"\"\"A post in a thread.\"\"\"\n\n    text: str  # Required\n    images: list[str] | None\n    image_alts: list[str] | None\n    links: list[RichTextLink] | None\n    mentions: list[RichTextMention] | None\n    quote: str | None\n\n\nclass ThreadResult(TypedDict):\n    \"\"\"Result of creating a thread.\"\"\"\n\n    success: bool\n    thread_uri: str | None  # URI of the first post\n    post_uris: list[str]\n    post_count: int\n    error: str | None\n"
  },
  {
    "path": "examples/auth/authkit_dcr/README.md",
    "content": "# AuthKit DCR Example\n\nDemonstrates FastMCP server protection with AuthKit Dynamic Client Registration.\n\n## Setup\n\n1. Set your AuthKit domain:\n\n   ```bash\n   export AUTHKIT_DOMAIN=\"https://your-app.authkit.app\"\n   ```\n\n2. Run the server:\n\n   ```bash\n   python server.py\n   ```\n\n3. In another terminal, run the client:\n\n   ```bash\n   python client.py\n   ```\n\nThe client will open your browser for AuthKit authentication.\n"
  },
  {
    "path": "examples/auth/authkit_dcr/client.py",
    "content": "\"\"\"OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to an OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.auth import OAuth\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    # AuthKit defaults DCR clients to client_secret_basic, which conflicts\n    # with how MCP SDKs send credentials. Force \"none\" to register as a\n    # public client and avoid token exchange errors.\n    auth = OAuth(additional_client_metadata={\"token_endpoint_auth_method\": \"none\"})\n    async with Client(SERVER_URL, auth=auth) as client:\n        assert await client.ping()\n        print(\"Successfully authenticated!\")\n\n        tools = await client.list_tools()\n        print(f\"Available tools ({len(tools)}):\")\n        for tool in tools:\n            print(f\"   - {tool.name}: {tool.description}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/authkit_dcr/server.py",
    "content": "\"\"\"AuthKit DCR server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with AuthKit DCR.\n\nRequired environment variables:\n- FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN: Your AuthKit domain (e.g., \"https://your-app.authkit.app\")\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import AuthKitProvider\n\nauth = AuthKitProvider(\n    authkit_domain=os.getenv(\"AUTHKIT_DOMAIN\") or \"\",\n    base_url=\"http://localhost:8000\",\n)\n\nmcp = FastMCP(\"AuthKit DCR Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/aws_oauth/README.md",
    "content": "# AWS Cognito OAuth Example\n\nDemonstrates FastMCP server protection with AWS Cognito OAuth.\n\n## Setup\n\n1. Create an AWS Cognito User Pool and App Client:\n   - Go to [AWS Cognito Console](https://console.aws.amazon.com/cognito/)\n   - Create a new User Pool or use an existing one\n   - Create an App Client in your User Pool\n   - Configure the App Client settings:\n     - Enable \"Authorization code grant\" flow\n     - Add Callback URL: `http://localhost:8000/auth/callback`\n     - Configure OAuth scopes (at minimum: `openid`)\n   - Note your User Pool ID, App Client ID, Client Secret, and Cognito Domain Prefix\n\n2. Set environment variables:\n\n   ```bash\n   export FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID=\"your-user-pool-id\"\n   export FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION=\"your-aws-region\"\n   export FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID=\"your-app-client-id\"\n   export FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET=\"your-app-client-secret\"\n   ```\n\n   Or create a `.env` file:\n\n   ```env\n   FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID=your-user-pool-id\n   FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION=your-aws-region\n   FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID=your-app-client-id\n   FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET=your-app-client-secret\n   ```\n\n3. Run the server:\n\n   ```bash\n   python server.py\n   ```\n\n4. In another terminal, run the client:\n\n   ```bash\n   python client.py\n   ```\n\nThe client will open your browser for AWS Cognito authentication.\n"
  },
  {
    "path": "examples/auth/aws_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to an OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://localhost:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n\n            # Test the protected tool\n            print(\"🔒 Calling protected tool: get_access_token_claims\")\n            result = await client.call_tool(\"get_access_token_claims\")\n            user_data = result.data\n            print(\"📄 Available access token claims:\")\n            print(f\"   - sub: {user_data.get('sub', 'N/A')}\")\n            print(f\"   - username: {user_data.get('username', 'N/A')}\")\n            print(f\"   - cognito:groups: {user_data.get('cognito:groups', [])}\")\n\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/aws_oauth/requirements.txt",
    "content": "fastmcp\npython-dotenv"
  },
  {
    "path": "examples/auth/aws_oauth/server.py",
    "content": "\"\"\"AWS Cognito OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with AWS Cognito.\n\nRequired environment variables:\n- FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID: Your AWS Cognito User Pool ID\n- FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION: Your AWS region (optional, defaults to eu-central-1)\n- FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID: Your Cognito app client ID\n- FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET: Your Cognito app client secret\n\nTo run:\n    python server.py\n\"\"\"\n\nimport logging\nimport os\n\nfrom dotenv import load_dotenv\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.aws import AWSCognitoProvider\nfrom fastmcp.server.dependencies import get_access_token\n\nlogging.basicConfig(level=logging.DEBUG)\n\nload_dotenv(\".env\", override=True)\n\nauth = AWSCognitoProvider(\n    user_pool_id=os.getenv(\"FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID\") or \"\",\n    aws_region=os.getenv(\"FASTMCP_SERVER_AUTH_AWS_COGNITO_AWS_REGION\")\n    or \"eu-central-1\",\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET\") or \"\",\n    base_url=\"http://localhost:8000\",\n    # redirect_path=\"/custom/callback\"\n)\n\nmcp = FastMCP(\"AWS Cognito OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\n@mcp.tool\nasync def get_access_token_claims() -> dict:\n    \"\"\"Get the authenticated user's access token claims.\"\"\"\n    token = get_access_token()\n    return {\n        \"sub\": token.claims.get(\"sub\"),\n        \"username\": token.claims.get(\"username\"),\n        \"cognito:groups\": token.claims.get(\"cognito:groups\", []),\n    }\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/azure_oauth/README.md",
    "content": "# Azure (Microsoft Entra) OAuth Example\n\nThis example demonstrates how to use the Azure OAuth provider with FastMCP servers.\n\n## Setup\n\n### 1. Azure App Registration\n\n1. Go to [Azure Portal → App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)\n2. Click \"New registration\" and configure:\n   - Name: Your app name\n   - Supported account types: Choose based on your needs\n   - Redirect URI: `http://localhost:8000/auth/callback` (Web platform)\n3. After creation, go to \"Certificates & secrets\" → \"New client secret\"\n4. Note these values from the Overview page:\n   - Application (client) ID\n   - Directory (tenant) ID\n\n### 2. Environment Variables\n\nCreate a `.env` file:\n\n```bash\n# Required\nAZURE_CLIENT_ID=your-application-client-id\nAZURE_CLIENT_SECRET=your-client-secret-value\nAZURE_TENANT_ID=your-tenant-id  # From Azure Portal Overview page\n```\n\n### 3. Run the Example\n\nStart the server:\n\n```bash\nuv run python server.py\n```\n\nTest with client:\n\n```bash\nuv run python client.py\n```\n\n## Tenant Configuration\n\nThe `tenant_id` parameter is **required** and controls which accounts can authenticate:\n\n- **Your tenant ID**: Single organization (most common)\n- **`organizations`**: Any work/school account\n- **`consumers`**: Personal Microsoft accounts only\n\n"
  },
  {
    "path": "examples/auth/azure_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to an OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/azure_oauth/server.py",
    "content": "\"\"\"Azure (Microsoft Entra) OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with Azure/Microsoft OAuth.\n\nRequired environment variables:\n- AZURE_CLIENT_ID: Your Azure application (client) ID\n- AZURE_CLIENT_SECRET: Your Azure client secret\n- AZURE_TENANT_ID: Tenant ID\n  Options: \"organizations\" (work/school), \"consumers\" (personal), or specific tenant ID\n- AZURE_REQUIRED_SCOPES: At least one scope required (e.g., \"read\" or \"read,write\")\n  These must match scope names created under \"Expose an API\" in your Azure App registration\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.azure import AzureProvider\n\nauth = AzureProvider(\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET\") or \"\",\n    tenant_id=os.getenv(\"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID\")\n    or \"\",  # Required for single-tenant apps - get from Azure Portal\n    base_url=\"http://localhost:8000\",\n    required_scopes=[\"read\"],\n    # required_scopes is automatically loaded from FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES\n    # At least one scope is required - use unprefixed scope names from your Azure App (e.g., [\"read\", \"write\"])\n    # redirect_path=\"/auth/callback\",  # Default path - change if using a different callback URL\n)\n\nmcp = FastMCP(\"Azure OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/discord_oauth/README.md",
    "content": "# Discord OAuth Example\n\nDemonstrates FastMCP server protection with Discord OAuth.\n\n## Setup\n\n1. Create a Discord OAuth App:\n   - Go to https://discord.com/developers/applications\n   - Click \"New Application\" and give it a name\n   - Go to OAuth2 in the left sidebar\n   - Add a Redirect URL: `http://localhost:8000/auth/callback`\n   - Copy the Client ID and Client Secret\n\n2. Set environment variables:\n\n   ```bash\n   export FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID=\"your-client-id\"\n   export FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET=\"your-client-secret\"\n   ```\n\n3. Run the server:\n\n   ```bash\n   python server.py\n   ```\n\n4. In another terminal, run the client:\n\n   ```bash\n   python client.py\n   ```\n\nThe client will open your browser for Discord authentication.\n"
  },
  {
    "path": "examples/auth/discord_oauth/client.py",
    "content": "\"\"\"Discord OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to a Discord OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/discord_oauth/server.py",
    "content": "\"\"\"Discord OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with Discord OAuth.\n\nRequired environment variables:\n- FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID: Your Discord OAuth app client ID\n- FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET: Your Discord OAuth app client secret\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.discord import DiscordProvider\n\nauth = DiscordProvider(\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET\") or \"\",\n    base_url=\"http://localhost:8000\",\n    # redirect_path=\"/auth/callback\",  # Default path - change if using a different callback URL\n)\n\nmcp = FastMCP(\"Discord OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/github_oauth/README.md",
    "content": "# GitHub OAuth Example\n\nDemonstrates FastMCP server protection with GitHub OAuth.\n\n## Setup\n\n1. Create a GitHub OAuth App:\n   - Go to GitHub Settings > Developer settings > OAuth Apps\n   - Set Authorization callback URL to: `http://localhost:8000/auth/callback`\n   - Copy the Client ID and Client Secret\n\n2. Set environment variables:\n\n   ```bash\n   export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=\"your-client-id\"\n   export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET=\"your-client-secret\"\n   ```\n\n3. Run the server:\n\n   ```bash\n   python server.py\n   ```\n\n4. In another terminal, run the client:\n\n   ```bash\n   python client.py\n   ```\n\nThe client will open your browser for GitHub authentication.\n"
  },
  {
    "path": "examples/auth/github_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to an OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client, OAuth\n\nSERVER_URL = \"http://localhost:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=OAuth()) as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/github_oauth/server.py",
    "content": "\"\"\"GitHub OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with GitHub OAuth.\n\nRequired environment variables:\n- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID: Your GitHub OAuth app client ID\n- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET: Your GitHub OAuth app client secret\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\n\nauth = GitHubProvider(\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET\") or \"\",\n    base_url=\"http://localhost:8000\",\n    # redirect_path=\"/auth/callback\",  # Default path - change if using a different callback URL\n)\n\nmcp = FastMCP(\"GitHub OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/google_oauth/README.md",
    "content": "# Google OAuth Example\n\nDemonstrates FastMCP server protection with Google OAuth.\n\n## Setup\n\n1. Create a Google OAuth 2.0 Client:\n   - Go to [Google Cloud Console](https://console.cloud.google.com/)\n   - Create or select a project\n   - Go to APIs & Services > Credentials\n   - Create OAuth 2.0 Client ID (Web application)\n   - Add Authorized redirect URI: `http://localhost:8000/auth/callback`\n   - Copy the Client ID and Client Secret\n\n2. Set environment variables:\n\n   ```bash\n   export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=\"your-client-id.apps.googleusercontent.com\"\n   export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET=\"your-client-secret\"\n   ```\n\n3. Run the server:\n\n   ```bash\n   python server.py\n   ```\n\n4. In another terminal, run the client:\n\n   ```bash\n   python client.py\n   ```\n\nThe client will open your browser for Google authentication.\n"
  },
  {
    "path": "examples/auth/google_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to an OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/google_oauth/server.py",
    "content": "\"\"\"Google OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with Google OAuth.\n\nRequired environment variables:\n- FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID: Your Google OAuth client ID\n- FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET: Your Google OAuth client secret\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.google import GoogleProvider\n\nauth = GoogleProvider(\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET\") or \"\",\n    base_url=\"http://localhost:8000\",\n    # redirect_path=\"/auth/callback\",  # Default path - change if using a different callback URL\n    # Optional: specify required scopes\n    # required_scopes=[\"openid\", \"https://www.googleapis.com/auth/userinfo.email\"],\n)\n\nmcp = FastMCP(\"Google OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/mounted/README.md",
    "content": "# Multi-Provider OAuth Example\n\nThis example demonstrates mounting multiple OAuth-protected MCP servers in a single application, each with its own OAuth provider. It showcases RFC 8414 path-aware discovery where each server has its own authorization server metadata endpoint.\n\n## URL Structure\n\n- **GitHub MCP**: `http://localhost:8000/api/mcp/github/mcp`\n- **Google MCP**: `http://localhost:8000/api/mcp/google/mcp`\n\nDiscovery endpoints (RFC 8414 path-aware):\n- **GitHub**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github`\n- **Google**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google`\n\n## Setup\n\nSet environment variables for both providers:\n\n```bash\nexport FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=\"your-github-client-id\"\nexport FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET=\"your-github-client-secret\"\nexport FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=\"your-google-client-id\"\nexport FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET=\"your-google-client-secret\"\n```\n\nConfigure redirect URIs in each provider's developer console (note the `/api/mcp/{provider}` prefix since the servers are mounted):\n- GitHub: `http://localhost:8000/api/mcp/github/auth/callback/github`\n- Google: `http://localhost:8000/api/mcp/google/auth/callback/google`\n\n## Running\n\nStart the server:\n```bash\npython server.py\n```\n\nConnect with the client:\n```bash\npython client.py\n```\n"
  },
  {
    "path": "examples/auth/mounted/client.py",
    "content": "\"\"\"Mounted OAuth servers client example for FastMCP.\n\nThis example demonstrates connecting to multiple mounted OAuth-protected MCP servers.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nGITHUB_URL = \"http://127.0.0.1:8000/api/mcp/github/mcp\"\nGOOGLE_URL = \"http://127.0.0.1:8000/api/mcp/google/mcp\"\n\n\nasync def main():\n    # Connect to GitHub server\n    print(\"\\n--- GitHub Server ---\")\n    try:\n        async with Client(GITHUB_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n    # Connect to Google server\n    print(\"\\n--- Google Server ---\")\n    try:\n        async with Client(GOOGLE_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/mounted/server.py",
    "content": "\"\"\"Mounted OAuth servers example for FastMCP.\n\nThis example demonstrates mounting multiple OAuth-protected MCP servers in a single\napplication, each with its own provider. It showcases RFC 8414 path-aware discovery\nwhere each server has its own authorization server metadata endpoint.\n\nURL structure:\n- GitHub MCP: http://localhost:8000/api/mcp/github/mcp\n- Google MCP: http://localhost:8000/api/mcp/google/mcp\n- GitHub discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github\n- Google discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google\n\nRequired environment variables:\n- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID: Your GitHub OAuth app client ID\n- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET: Your GitHub OAuth app client secret\n- FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID: Your Google OAuth client ID\n- FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET: Your Google OAuth client secret\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nimport uvicorn\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom fastmcp.server.auth.providers.google import GoogleProvider\n\n# Configuration\nROOT_URL = \"http://localhost:8000\"\nAPI_PREFIX = \"/api/mcp\"\n\n# --- GitHub OAuth Server ---\ngithub_auth = GitHubProvider(\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET\") or \"\",\n    base_url=f\"{ROOT_URL}{API_PREFIX}/github\",\n    redirect_path=\"/auth/callback/github\",\n)\n\ngithub_mcp = FastMCP(\"GitHub Server\", auth=github_auth)\n\n\n@github_mcp.tool\ndef github_echo(message: str) -> str:\n    \"\"\"Echo from the GitHub-authenticated server.\"\"\"\n    return f\"[GitHub] {message}\"\n\n\n@github_mcp.tool\ndef github_info() -> str:\n    \"\"\"Get info about the GitHub server.\"\"\"\n    return \"This is the GitHub OAuth protected MCP server\"\n\n\n# --- Google OAuth Server ---\ngoogle_auth = GoogleProvider(\n    client_id=os.getenv(\"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET\") or \"\",\n    base_url=f\"{ROOT_URL}{API_PREFIX}/google\",\n    redirect_path=\"/auth/callback/google\",\n)\n\ngoogle_mcp = FastMCP(\"Google Server\", auth=google_auth)\n\n\n@google_mcp.tool\ndef google_echo(message: str) -> str:\n    \"\"\"Echo from the Google-authenticated server.\"\"\"\n    return f\"[Google] {message}\"\n\n\n@google_mcp.tool\ndef google_info() -> str:\n    \"\"\"Get info about the Google server.\"\"\"\n    return \"This is the Google OAuth protected MCP server\"\n\n\n# --- Create ASGI apps ---\ngithub_app = github_mcp.http_app(path=\"/mcp\")\ngoogle_app = google_mcp.http_app(path=\"/mcp\")\n\n# Get well-known routes for each provider (path-aware per RFC 8414)\ngithub_well_known = github_auth.get_well_known_routes(mcp_path=\"/mcp\")\ngoogle_well_known = google_auth.get_well_known_routes(mcp_path=\"/mcp\")\n\n# --- Combine into single application ---\n# Note: Each provider has its own path-aware discovery endpoint:\n# - /.well-known/oauth-authorization-server/api/mcp/github\n# - /.well-known/oauth-authorization-server/api/mcp/google\napp = Starlette(\n    routes=[\n        # Well-known routes at root level (path-aware)\n        *github_well_known,\n        *google_well_known,\n        # MCP servers under /api/mcp prefix\n        Mount(f\"{API_PREFIX}/github\", app=github_app),\n        Mount(f\"{API_PREFIX}/google\", app=google_app),\n    ],\n    # Use one of the app lifespans (they're functionally equivalent)\n    lifespan=github_app.lifespan,\n)\n\nif __name__ == \"__main__\":\n    print(\"Starting mounted OAuth servers...\")\n    print(f\"  GitHub MCP:     {ROOT_URL}{API_PREFIX}/github/mcp\")\n    print(f\"  Google MCP:     {ROOT_URL}{API_PREFIX}/google/mcp\")\n    print()\n    print(\"Discovery endpoints (RFC 8414 path-aware):\")\n    print(\n        f\"  GitHub:  {ROOT_URL}/.well-known/oauth-authorization-server{API_PREFIX}/github\"\n    )\n    print(\n        f\"  Google:  {ROOT_URL}/.well-known/oauth-authorization-server{API_PREFIX}/google\"\n    )\n    print()\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n"
  },
  {
    "path": "examples/auth/propelauth_oauth/README.md",
    "content": "# PropelAuth OAuth Example\n\nDemonstrates FastMCP server protection with PropelAuth OAuth.\n\n## Setup\n\n### 1. Configure MCP Authentication in PropelAuth\n\n**Create a PropelAuth Account**:\n\n- Go to [PropelAuth Dashboard](https://www.propelauth.com)\n- Navigate to the **MCP** section and click **Enable MCP**\n\n**Configure Allowed MCP Clients**:\n\n- Under **MCP > Allowed MCP Clients**, add redirect URIs for each MCP client you want to allow\n- PropelAuth provides templates for popular clients like Claude, Cursor, and ChatGPT\n\n**Configure Scopes**:\n\n- Under **MCP > Scopes**, define the permissions available to MCP clients (e.g., `read:user_data`)\n\n**Generate Introspection Credentials**:\n\n- Go to **MCP > Request Validation** and click **Create Credentials**\n- Note the **Client ID** and **Client Secret**\n\n**Note Your Auth URL**:\n\n- Find your Auth URL in the **Backend Integration** section (e.g., `https://auth.yourdomain.com`)\n\nCreate a `.env` file:\n\n```bash\n# Required PropelAuth credentials\nPROPELAUTH_AUTH_URL=https://auth.yourdomain.com\nPROPELAUTH_INTROSPECTION_CLIENT_ID=your-client-id\nPROPELAUTH_INTROSPECTION_CLIENT_SECRET=your-client-secret\nBASE_URL=http://localhost:8000/\n# Optional: additional scopes tokens must include (comma-separated)\n# PROPELAUTH_REQUIRED_SCOPES=read:user_data\n```\n\n### 2. Run the Example\n\nStart the server:\n\n```bash\n# From this directory\nuv run python server.py\n```\n\nThe server will start on `http://localhost:8000/mcp` with PropelAuth OAuth authentication enabled.\n\nTest with client:\n\n```bash\nuv run python client.py\n```\n\nThe `client.py` will:\n\n1. Attempt to connect to the server\n2. Detect that OAuth authentication is required\n3. Open a browser for PropelAuth authentication\n4. Complete the OAuth flow and connect to the server\n5. Demonstrate calling authenticated tools (echo and whoami)\n"
  },
  {
    "path": "examples/auth/propelauth_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to PropelAuth-protected FastMCP servers.\n\nThis example demonstrates how to connect to a PropelAuth OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated with PropelAuth!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n\n            # Test calling a tool\n            result = await client.call_tool(\n                \"echo\", {\"message\": \"Hello from PropelAuth!\"}\n            )\n            print(f\"🎯 Echo result: {result}\")\n\n            # Test calling whoami tool\n            whoami = await client.call_tool(\"whoami\", {})\n            print(f\"👤 Who am I: {whoami}\")\n\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/propelauth_oauth/server.py",
    "content": "\"\"\"PropelAuth OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with PropelAuth OAuth.\n\nRequired environment variables:\n- PROPELAUTH_AUTH_URL: Your PropelAuth Auth URL (from Backend Integration page)\n- PROPELAUTH_INTROSPECTION_CLIENT_ID: Introspection Client ID (from MCP > Request Validation)\n- PROPELAUTH_INTROSPECTION_CLIENT_SECRET: Introspection Client Secret (from MCP > Request Validation)\n\nOptional:\n- PROPELAUTH_REQUIRED_SCOPES: Comma-separated scopes tokens must include\n- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom dotenv import load_dotenv\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.propelauth import PropelAuthProvider\nfrom fastmcp.server.dependencies import get_access_token\n\nload_dotenv()\n\nauth = PropelAuthProvider(\n    auth_url=os.environ[\"PROPELAUTH_AUTH_URL\"],\n    introspection_client_id=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_ID\"],\n    introspection_client_secret=os.environ[\"PROPELAUTH_INTROSPECTION_CLIENT_SECRET\"],\n    base_url=os.getenv(\"BASE_URL\", \"http://localhost:8000/\"),\n)\n\nmcp = FastMCP(\"PropelAuth OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\n@mcp.tool\ndef whoami() -> dict:\n    \"\"\"Return the authenticated user's ID.\"\"\"\n    token = get_access_token()\n    if token is None:\n        return {\"error\": \"Not authenticated\"}\n    return {\"user_id\": token.claims.get(\"sub\")}\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/scalekit_oauth/README.md",
    "content": "# Scalekit OAuth Example\n\nDemonstrates FastMCP server protection with Scalekit OAuth.\n\n## Setup\n\n### 1. Configure MCP server in Scalekit environment\n\n**Create a Scalekit Account**:\n\n- Go to [Scalekit Dashboard](https://app.scalekit.com/)\n- Copy your Environment URL from **Developers** → **Settings**\n- Copy Resource ID (res_xxx) from **Developers** → **MCP Servers**\n\n**Register Your MCP Server**:\n\n- Go to **MCP Servers** → **Create New Server**\n- Fill in your MCP server details\n- Note the **Resource ID** (e.g., `res_123`)\n\nCreate a `.env` file:\n\n```bash\n# Required Scalekit credentials\nSCALEKIT_ENVIRONMENT_URL=<YOUR_APP_ENVIRONMENT_URL>\nSCALEKIT_RESOURCE_ID=<YOUR_APP_RESOURCE_ID> # res_926EXAMPLE5878\nBASE_URL=http://localhost:8000/\n# Optional: additional scopes tokens must include (comma-separated)\n# SCALEKIT_REQUIRED_SCOPES=read,write\n```\n\n### 2. Run the Example\n\nStart the server:\n\n```bash\n# From this directory\nuv run python server.py\n```\n\nThe server will start on `http://localhost:8000/mcp` with Scalekit OAuth authentication enabled.\n\nTest with client:\n\n```bash\nuv run python client.py\n```\n\nThe `client.py` will:\n\n1. Attempt to connect to the server\n2. Detect that OAuth authentication is required\n3. Open a browser for Scalekit authentication\n4. Complete the OAuth flow and connect to the server\n5. Demonstrate calling authenticated tools\n"
  },
  {
    "path": "examples/auth/scalekit_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to Scalekit-protected FastMCP servers.\n\nThis example demonstrates how to connect to a Scalekit OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated with Scalekit!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n\n            # Test calling a tool\n            result = await client.call_tool(\"echo\", {\"message\": \"Hello from Scalekit!\"})\n            print(f\"🎯 Echo result: {result}\")\n\n            # Test calling auth status tool\n            auth_status = await client.call_tool(\"auth_status\", {})\n            print(f\"👤 Auth status: {auth_status}\")\n\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/scalekit_oauth/server.py",
    "content": "\"\"\"Scalekit OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with Scalekit OAuth.\n\nRequired environment variables:\n- SCALEKIT_ENVIRONMENT_URL: Your Scalekit environment URL (e.g., \"https://your-env.scalekit.com\")\n- SCALEKIT_RESOURCE_ID: Your Scalekit resource ID\n\nOptional:\n- SCALEKIT_REQUIRED_SCOPES: Comma-separated scopes tokens must include\n- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.scalekit import ScalekitProvider\n\nrequired_scopes_env = os.getenv(\"SCALEKIT_REQUIRED_SCOPES\")\nrequired_scopes = (\n    [scope.strip() for scope in required_scopes_env.split(\",\") if scope.strip()]\n    if required_scopes_env\n    else None\n)\n\nauth = ScalekitProvider(\n    environment_url=os.getenv(\"SCALEKIT_ENVIRONMENT_URL\")\n    or \"https://your-env.scalekit.com\",\n    resource_id=os.getenv(\"SCALEKIT_RESOURCE_ID\") or \"\",\n    base_url=os.getenv(\"BASE_URL\", \"http://localhost:8000/\"),\n    required_scopes=required_scopes,\n)\n\nmcp = FastMCP(\"Scalekit OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\n@mcp.tool\ndef auth_status() -> dict:\n    \"\"\"Show Scalekit authentication status.\"\"\"\n    # In a real implementation, you would extract user info from the JWT token\n    return {\n        \"message\": \"This tool requires authentication via Scalekit\",\n        \"authenticated\": True,\n        \"provider\": \"Scalekit\",\n    }\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/auth/workos_oauth/README.md",
    "content": "# WorkOS OAuth Example\n\nDemonstrates FastMCP server protection with WorkOS OAuth.\n\n## Setup\n\n1. Create a WorkOS application and copy your credentials:\n\n   ```bash\n   export WORKOS_CLIENT_ID=\"your-client-id\"\n   export WORKOS_CLIENT_SECRET=\"your-client-secret\"\n   export WORKOS_AUTHKIT_DOMAIN=\"https://your-app.authkit.app\"\n   ```\n\n2. Run the server:\n\n   ```bash\n   python server.py\n   ```\n\n3. In another terminal, run the client:\n\n   ```bash\n   python client.py\n   ```\n\nThe client will open your browser for WorkOS authentication.\n"
  },
  {
    "path": "examples/auth/workos_oauth/client.py",
    "content": "\"\"\"OAuth client example for connecting to FastMCP servers.\n\nThis example demonstrates how to connect to an OAuth-protected FastMCP server.\n\nTo run:\n    python client.py\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp.client import Client\n\nSERVER_URL = \"http://127.0.0.1:8000/mcp\"\n\n\nasync def main():\n    try:\n        async with Client(SERVER_URL, auth=\"oauth\") as client:\n            assert await client.ping()\n            print(\"✅ Successfully authenticated!\")\n\n            tools = await client.list_tools()\n            print(f\"🔧 Available tools ({len(tools)}):\")\n            for tool in tools:\n                print(f\"   - {tool.name}: {tool.description}\")\n    except Exception as e:\n        print(f\"❌ Authentication failed: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/auth/workos_oauth/server.py",
    "content": "\"\"\"WorkOS OAuth server example for FastMCP.\n\nThis example demonstrates how to protect a FastMCP server with WorkOS OAuth.\n\nRequired environment variables:\n- WORKOS_CLIENT_ID: Your WorkOS Connect application client ID\n- WORKOS_CLIENT_SECRET: Your WorkOS Connect application client secret\n- WORKOS_AUTHKIT_DOMAIN: Your AuthKit domain (e.g., \"https://your-app.authkit.app\")\n\nTo run:\n    python server.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.workos import WorkOSProvider\n\nauth = WorkOSProvider(\n    client_id=os.getenv(\"WORKOS_CLIENT_ID\") or \"\",\n    client_secret=os.getenv(\"WORKOS_CLIENT_SECRET\") or \"\",\n    authkit_domain=os.getenv(\"WORKOS_AUTHKIT_DOMAIN\") or \"https://your-app.authkit.app\",\n    base_url=\"http://localhost:8000\",\n    # redirect_path=\"/auth/callback\",  # Default path - change if using a different callback URL\n)\n\nmcp = FastMCP(\"WorkOS OAuth Example Server\", auth=auth)\n\n\n@mcp.tool\ndef echo(message: str) -> str:\n    \"\"\"Echo the provided message.\"\"\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"http\", port=8000)\n"
  },
  {
    "path": "examples/code_mode/README.md",
    "content": "# Code Mode\n\nCodeMode collapses an entire tool catalog into two meta-tools: `search` (keyword-based discovery) and `execute` (run Python scripts that chain tool calls in a sandbox). Instead of burning context tokens on every intermediate result, the LLM writes a script that runs server-side and returns only the final answer.\n\n## Run\n\n```bash\nuv run python server.py   # in one terminal\nuv run python client.py   # in another\n```\n\n## Example Output\n\n```\n══════════════════ CodeMode Transform ══════════════════\n\n┌────────────── list_tools() ──────────────┐\n│ Tool     Description                     │\n│ search   Search for available tools ...  │\n│ execute  Chain `await call_tool(...)` ... │\n└── 8 backend tools collapsed into 2 ──────┘\n\n┌──── search(query=\"math arithmetic\") ─────┐\n│ #   Tool      Description                │\n│ 1   add       Add two numbers together.  │\n│ 2   multiply  Multiply two numbers.      │\n│ 3   fibonacci Generate the first n ...   │\n└── 3 results ─────────────────────────────┘\n\n┌────────────── execute ───────────────────┐\n│ a = await call_tool(\"add\", {\"a\": 3 ...  │\n│ b = await call_tool(\"multiply\", ...     │\n│ return b                                 │\n└── result: 14.0 ──────────────────────────┘\n```\n\nThe key insight: with standard MCP, each `call_tool` is a round-trip through the LLM. With CodeMode, the LLM writes one script and all the tool calls happen server-side. Intermediate data never touches the context window.\n"
  },
  {
    "path": "examples/code_mode/client.py",
    "content": "\"\"\"Example: Client using CodeMode to discover and chain tools.\n\nCodeMode exposes just two tools: `search` (keyword query) and `execute`\n(run Python code with `call_tool` available). This client demonstrates\nboth: searching for tools, then chaining multiple calls in a single\nexecute block — one round-trip instead of many.\n\nRun with:\n    uv run python examples/code_mode/client.py\n\"\"\"\n\nimport asyncio\nimport json\nfrom typing import Any\n\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.syntax import Syntax\nfrom rich.table import Table\n\nfrom fastmcp.client import Client\n\nconsole = Console()\n\n\ndef _get_result(result) -> Any:\n    \"\"\"Extract the value from a CallToolResult (structured or text).\"\"\"\n    if result.structured_content is not None:\n        data = result.structured_content\n        if isinstance(data, dict) and set(data) == {\"result\"}:\n            return data[\"result\"]\n        return data\n    return result.content[0].text\n\n\ndef _format_params(tool: dict) -> str:\n    \"\"\"Format inputSchema properties as a compact signature.\"\"\"\n    schema = tool.get(\"inputSchema\", {})\n    props = schema.get(\"properties\", {})\n    if not props:\n        return \"()\"\n    parts = []\n    for name, info in props.items():\n        typ = info.get(\"type\", \"\")\n        parts.append(f\"{name}: {typ}\" if typ else name)\n    return f\"({', '.join(parts)})\"\n\n\ndef _tool_table(\n    tools: list[dict], *, ranked: bool = False, show_params: bool = False\n) -> Table:\n    table = Table(show_header=True, show_edge=False, pad_edge=False, expand=True)\n    if ranked:\n        table.add_column(\"#\", style=\"dim\", width=3, justify=\"right\")\n    table.add_column(\"Tool\", style=\"cyan\", no_wrap=True)\n    if show_params:\n        table.add_column(\"Parameters\", style=\"dim\", no_wrap=True)\n    table.add_column(\"Description\", style=\"dim\")\n    for i, tool in enumerate(tools, 1):\n        row = [tool[\"name\"]]\n        if show_params:\n            row.append(_format_params(tool))\n        row.append(tool.get(\"description\", \"\"))\n        if ranked:\n            row.insert(0, str(i))\n        table.add_row(*row)\n    return table\n\n\nasync def main():\n    async with Client(\"examples/code_mode/server.py\") as client:\n        console.print()\n        console.rule(\"[bold]CodeMode[/bold]\")\n        console.print()\n\n        # Step 1: list_tools only returns two synthetic meta-tools\n        console.print(\n            \"The server has 8 tools. CodeMode replaces them with \"\n            \"two synthetic tools — [bold]search[/bold] and [bold]execute[/bold]:\"\n        )\n        console.print()\n        tools = await client.list_tools()\n        visible = [{\"name\": t.name, \"description\": t.description} for t in tools]\n        console.print(\n            Panel(\n                _tool_table(visible),\n                title=\"[bold]list_tools()[/bold]\",\n                title_align=\"left\",\n                border_style=\"blue\",\n            )\n        )\n        console.print()\n\n        # Step 2: search discovers available tools\n        console.print(\"The LLM calls [bold]search[/bold] to discover available tools:\")\n        console.print()\n        result = await client.call_tool(\"search\", {\"query\": \"add multiply numbers\"})\n        found = _get_result(result)\n        if isinstance(found, str):\n            found = json.loads(found)\n        console.print(\n            Panel(\n                _tool_table(found, ranked=True, show_params=True),\n                title='[bold]search[/bold]  [dim]query=\"add multiply numbers\"[/dim]',\n                title_align=\"left\",\n                border_style=\"green\",\n            )\n        )\n        console.print()\n\n        # Step 3: execute chains tool calls in one round-trip\n        console.print(\n            \"Now the LLM writes a Python script that chains \"\n            \"the tools it found. All of it runs server-side in a \"\n            \"sandbox — [bold]one round-trip[/bold], intermediate \"\n            \"data never hits the context window:\"\n        )\n        console.print()\n        code = \"\"\"\\\na = await call_tool(\"add\", {\"a\": 3, \"b\": 4})\nb = await call_tool(\"multiply\", {\"x\": a[\"result\"], \"y\": 2})\nfib = await call_tool(\"fibonacci\", {\"n\": b[\"result\"]})\nreturn {\"sum\": a[\"result\"], \"product\": b[\"result\"], \"fibonacci\": fib[\"result\"]}\n\"\"\"\n        result = await client.call_tool(\"execute\", {\"code\": code})\n        console.print(\n            Panel(\n                Syntax(code.strip(), \"python\", theme=\"monokai\"),\n                title=\"[bold]execute[/bold]\",\n                title_align=\"left\",\n                border_style=\"yellow\",\n            )\n        )\n        console.print()\n\n        # Final result\n        console.print(f\"  Result: [bold green]{_get_result(result)}[/bold green]\")\n        console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/code_mode/server.py",
    "content": "\"\"\"Example: CodeMode transform — search and execute tools via code.\n\nCodeMode replaces the entire tool catalog with two meta-tools: `search`\n(keyword-based tool discovery) and `execute` (run Python code that chains\ntool calls in a sandbox). This dramatically reduces round-trips and\ncontext window usage when an LLM needs to orchestrate many tools.\n\nRequires pydantic-monty for the sandbox:\n    pip install \"fastmcp[code-mode]\"\n\nRun with:\n    uv run python examples/code_mode/server.py\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import CodeMode\n\nmcp = FastMCP(\"CodeMode Demo\")\n\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.tool\ndef multiply(x: float, y: float) -> float:\n    \"\"\"Multiply two numbers.\"\"\"\n    return x * y\n\n\n@mcp.tool\ndef fibonacci(n: int) -> list[int]:\n    \"\"\"Generate the first n Fibonacci numbers.\"\"\"\n    if n <= 0:\n        return []\n    seq = [0, 1]\n    while len(seq) < n:\n        seq.append(seq[-1] + seq[-2])\n    return seq[:n]\n\n\n@mcp.tool\ndef reverse_string(text: str) -> str:\n    \"\"\"Reverse a string.\"\"\"\n    return text[::-1]\n\n\n@mcp.tool\ndef word_count(text: str) -> int:\n    \"\"\"Count the number of words in a text.\"\"\"\n    return len(text.split())\n\n\n@mcp.tool\ndef to_uppercase(text: str) -> str:\n    \"\"\"Convert text to uppercase.\"\"\"\n    return text.upper()\n\n\n@mcp.tool\ndef list_files(directory: str) -> list[str]:\n    \"\"\"List files in a directory.\"\"\"\n    import os\n\n    return os.listdir(directory)\n\n\n@mcp.tool\ndef read_file(path: str) -> str:\n    \"\"\"Read the contents of a file.\"\"\"\n    with open(path) as f:\n        return f.read()\n\n\n# CodeMode collapses all 8 tools into just `search` + `execute`.\n# The LLM discovers tools via keyword search, then writes Python\n# scripts that chain multiple tool calls in a single round-trip.\nmcp.add_transform(CodeMode())\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/complex_inputs.py",
    "content": "\"\"\"\nFastMCP Complex inputs Example\n\nDemonstrates validation via pydantic with complex models.\n\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Shrimp Tank\")\n\n\nclass ShrimpTank(BaseModel):\n    class Shrimp(BaseModel):\n        name: Annotated[str, Field(max_length=10)]\n\n    shrimp: list[Shrimp]\n\n\n@mcp.tool\ndef name_shrimp(\n    tank: ShrimpTank,\n    # You can use pydantic Field in function signatures for validation.\n    extra_names: Annotated[list[str], Field(max_length=10)],\n) -> list[str]:\n    \"\"\"List all shrimp names in the tank\"\"\"\n    return [shrimp.name for shrimp in tank.shrimp] + extra_names\n"
  },
  {
    "path": "examples/config_server.py",
    "content": "\"\"\"\nSimple example showing FastMCP server with command line argument support.\n\nUsage:\n    fastmcp run examples/config_server.py -- --name MyServer --debug\n\"\"\"\n\nimport argparse\n\nfrom fastmcp import FastMCP\n\nparser = argparse.ArgumentParser(description=\"Simple configurable MCP server\")\nparser.add_argument(\n    \"--name\", type=str, default=\"ConfigurableServer\", help=\"Server name\"\n)\nparser.add_argument(\"--debug\", action=\"store_true\", help=\"Enable debug mode\")\n\nargs = parser.parse_args()\n\nserver_name = args.name\nif args.debug:\n    server_name += \" (Debug)\"\n\nmcp = FastMCP(server_name)\n\n\n@mcp.tool\ndef get_status() -> dict[str, str | bool]:\n    \"\"\"Get the current server configuration and status.\"\"\"\n    return {\n        \"server_name\": server_name,\n        \"debug_mode\": args.debug,\n        \"original_name\": args.name,\n    }\n\n\n@mcp.tool\ndef echo_message(message: str) -> str:\n    \"\"\"Echo a message, with debug info if debug mode is enabled.\"\"\"\n    if args.debug:\n        return f\"[DEBUG] Echoing: {message}\"\n    return message\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/custom_tool_serializer_decorator.py",
    "content": "\"\"\"Example of custom tool serialization using ToolResult and a wrapper decorator.\n\nThis pattern provides explicit control over how tool outputs are serialized,\nmaking the serialization visible in each tool's code.\n\"\"\"\n\nimport asyncio\nimport inspect\nfrom collections.abc import Callable\nfrom functools import wraps\nfrom typing import Any\n\nimport yaml\n\nfrom fastmcp import FastMCP\nfrom fastmcp.tools.tool import ToolResult\n\n\ndef with_serializer(serializer: Callable[[Any], str]):\n    \"\"\"Decorator to apply custom serialization to tool output.\"\"\"\n\n    def decorator(fn):\n        @wraps(fn)\n        def wrapper(*args, **kwargs):\n            result = fn(*args, **kwargs)\n            return ToolResult(content=serializer(result), structured_content=result)\n\n        @wraps(fn)\n        async def async_wrapper(*args, **kwargs):\n            result = await fn(*args, **kwargs)\n            return ToolResult(content=serializer(result), structured_content=result)\n\n        return async_wrapper if inspect.iscoroutinefunction(fn) else wrapper\n\n    return decorator\n\n\n# Create reusable serializer decorators\nwith_yaml = with_serializer(lambda d: yaml.dump(d, width=100, sort_keys=False))\n\nserver = FastMCP(name=\"CustomSerializerExample\")\n\n\n@server.tool\n@with_yaml\ndef get_example_data() -> dict:\n    \"\"\"Returns some example data serialized as YAML.\"\"\"\n    return {\"name\": \"Test\", \"value\": 123, \"status\": True}\n\n\n@server.tool\ndef get_json_data() -> dict:\n    \"\"\"Returns data with default JSON serialization.\"\"\"\n    return {\"format\": \"json\", \"data\": [1, 2, 3]}\n\n\nasync def example_usage():\n    # YAML serialized tool\n    yaml_result = await server._call_tool_mcp(\"get_example_data\", {})\n    print(\"YAML Tool Result:\")\n    print(yaml_result)\n    print()\n\n    # Default JSON serialized tool\n    json_result = await server._call_tool_mcp(\"get_json_data\", {})\n    print(\"JSON Tool Result:\")\n    print(json_result)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(example_usage())\n    server.run()\n"
  },
  {
    "path": "examples/desktop.py",
    "content": "\"\"\"\nFastMCP Desktop Example\n\nA simple example that exposes the desktop directory as a resource.\n\"\"\"\n\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\n\n# Create server\nmcp = FastMCP(\"Demo\")\n\n\n@mcp.resource(\"dir://desktop\")\ndef desktop() -> list[str]:\n    \"\"\"List the files in the user's desktop\"\"\"\n    desktop = Path.home() / \"Desktop\"\n    return [str(f) for f in desktop.iterdir()]\n\n\n# Add a dynamic greeting resource\n@mcp.resource(\"greeting://{name}\")\ndef get_greeting(name: str) -> str:\n    \"\"\"Get a personalized greeting\"\"\"\n    return f\"Hello, {name}!\"\n\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n"
  },
  {
    "path": "examples/diagnostics/client_with_tracing.py",
    "content": "#!/usr/bin/env python\n\"\"\"Client script to exercise all diagnostics server components with tracing.\n\nUsage:\n    # First, start the diagnostics server with tracing in one terminal:\n    uv run examples/run_with_tracing.py examples/diagnostics/server.py --transport sse --port 8001\n\n    # Then run this client in another terminal:\n    uv run examples/diagnostics/client_with_tracing.py\n\n    # View traces in otel-desktop-viewer (http://localhost:8000):\n    otel-desktop-viewer\n\nThis script exercises all 8 components:\n- 4 successful: ping, diag://status, diag://echo/{message}, greet prompt\n- 4 error: fail_tool, diag://error, diag://error/{code}, fail prompt\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport os\n\n# Configure OTEL SDK before importing fastmcp\nfrom opentelemetry import trace\nfrom opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter\nfrom opentelemetry.sdk.resources import Resource\nfrom opentelemetry.sdk.trace import TracerProvider\nfrom opentelemetry.sdk.trace.export import BatchSpanProcessor\n\n\ndef setup_tracing():\n    \"\"\"Set up OpenTelemetry tracing with OTLP export.\"\"\"\n    endpoint = os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"http://localhost:4317\")\n    service_name = os.environ.get(\"OTEL_SERVICE_NAME\", \"fastmcp-diagnostics-client\")\n\n    resource = Resource.create({\"service.name\": service_name})\n    provider = TracerProvider(resource=resource)\n    exporter = OTLPSpanExporter(endpoint=endpoint, insecure=True)\n    provider.add_span_processor(BatchSpanProcessor(exporter))\n    trace.set_tracer_provider(provider)\n\n    print(f\"Tracing enabled: OTLP {endpoint}\")\n    print(f\"Service: {service_name}\")\n    print(\"View traces: otel-desktop-viewer (http://localhost:8000)\\n\")\n\n\nasync def main():\n    setup_tracing()\n\n    from fastmcp import Client\n\n    server_url = os.environ.get(\"DIAGNOSTICS_SERVER_URL\", \"http://localhost:8001/sse\")\n    print(f\"Connecting to: {server_url}\\n\")\n\n    async with Client(server_url) as client:\n        # List all available components\n        tools = await client.list_tools()\n        resources = await client.list_resources()\n        prompts = await client.list_prompts()\n\n        print(f\"Found {len(tools)} tools: {[t.name for t in tools]}\")\n        print(f\"Found {len(resources)} resources: {[r.uri for r in resources]}\")\n        print(f\"Found {len(prompts)} prompts: {[p.name for p in prompts]}\\n\")\n\n        # === SUCCESSFUL OPERATIONS ===\n        print(\"=\" * 60)\n        print(\"SUCCESSFUL OPERATIONS\")\n        print(\"=\" * 60)\n\n        # Local successful components\n        print(\"\\n--- Local Tools ---\")\n        result = await client.call_tool(\"ping\", {})\n        print(f\"ping: {result}\")\n\n        print(\"\\n--- Local Resources ---\")\n        result = await client.read_resource(\"diag://status\")\n        print(f\"diag://status: {result}\")\n\n        result = await client.read_resource(\"diag://echo/hello-world\")\n        print(f\"diag://echo/hello-world: {result}\")\n\n        print(\"\\n--- Local Prompts ---\")\n        result = await client.get_prompt(\"greet\", {\"name\": \"Diagnostics\"})\n        print(\n            f\"greet: {result.messages[0].content.text if result.messages else result}\"\n        )\n\n        # Proxied components from echo server\n        print(\"\\n--- Proxied Tools ---\")\n        try:\n            result = await client.call_tool(\n                \"proxied_echo_tool\", {\"text\": \"proxied test\"}\n            )\n            print(f\"proxied_echo_tool: {result}\")\n        except Exception as e:\n            print(f\"proxied_echo_tool: ERROR - {e}\")\n\n        print(\"\\n--- Proxied Resources ---\")\n        try:\n            # Resource: echo://static -> echo://proxied/static\n            result = await client.read_resource(\"echo://proxied/static\")\n            print(f\"echo://proxied/static: {result}\")\n        except Exception as e:\n            print(f\"echo://proxied/static: ERROR - {e}\")\n\n        try:\n            # Template: echo://{text} -> echo://proxied/{text}\n            result = await client.read_resource(\"echo://proxied/test-message\")\n            print(f\"echo://proxied/test-message: {result}\")\n        except Exception as e:\n            print(f\"echo://proxied/test-message: ERROR - {e}\")\n\n        print(\"\\n--- Proxied Prompts ---\")\n        try:\n            result = await client.get_prompt(\"proxied_echo\", {\"text\": \"proxied prompt\"})\n            print(\n                f\"proxied_echo: {result.messages[0].content.text if result.messages else result}\"\n            )\n        except Exception as e:\n            print(f\"proxied_echo: ERROR - {e}\")\n\n        # === ERROR OPERATIONS ===\n        print(\"\\n\" + \"=\" * 60)\n        print(\"ERROR OPERATIONS (expected to fail)\")\n        print(\"=\" * 60)\n\n        print(\"\\n--- Error Tools ---\")\n        try:\n            await client.call_tool(\"fail_tool\", {})\n            print(\"fail_tool: UNEXPECTED SUCCESS\")\n        except Exception as e:\n            print(f\"fail_tool: {type(e).__name__} - {e}\")\n\n        print(\"\\n--- Error Resources ---\")\n        try:\n            await client.read_resource(\"diag://error\")\n            print(\"diag://error: UNEXPECTED SUCCESS\")\n        except Exception as e:\n            print(f\"diag://error: {type(e).__name__} - {e}\")\n\n        try:\n            await client.read_resource(\"diag://error/500\")\n            print(\"diag://error/500: UNEXPECTED SUCCESS\")\n        except Exception as e:\n            print(f\"diag://error/500: {type(e).__name__} - {e}\")\n\n        print(\"\\n--- Error Prompts ---\")\n        try:\n            await client.get_prompt(\"fail\", {})\n            print(\"fail: UNEXPECTED SUCCESS\")\n        except Exception as e:\n            print(f\"fail: {type(e).__name__} - {e}\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"DONE - Check otel-desktop-viewer for traces\")\n    print(\"=\" * 60)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/diagnostics/server.py",
    "content": "\"\"\"FastMCP Diagnostics Server - for testing tracing, errors, and observability.\"\"\"\n\nimport asyncio\nimport os\nimport subprocess\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom pathlib import Path\n\nimport httpx\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server import create_proxy\n\nECHO_SERVER_PORT = 8002\nECHO_SERVER_URL = f\"http://localhost:{ECHO_SERVER_PORT}/sse\"\n\n\n@asynccontextmanager\nasync def lifespan(server: FastMCP) -> AsyncIterator[None]:\n    \"\"\"Start echo server subprocess and mount proxy to it.\"\"\"\n    echo_path = Path(__file__).parent.parent / \"echo.py\"\n\n    # Pass OTEL config to subprocess with different service name\n    env = os.environ.copy()\n    env[\"OTEL_SERVICE_NAME\"] = \"fastmcp-echo-server\"\n\n    # Start echo server as subprocess using run_with_tracing.py for OTEL export\n    run_with_tracing = Path(__file__).parent.parent / \"run_with_tracing.py\"\n    proc = subprocess.Popen(\n        [\n            \"uv\",\n            \"run\",\n            str(run_with_tracing),\n            str(echo_path),\n            \"--transport\",\n            \"sse\",\n            \"--port\",\n            str(ECHO_SERVER_PORT),\n        ],\n        env=env,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n    )\n\n    # Wait for server to be ready (async to avoid blocking event loop)\n    async with httpx.AsyncClient() as client:\n        for _ in range(50):\n            try:\n                await client.get(\n                    f\"http://localhost:{ECHO_SERVER_PORT}/sse\", timeout=0.1\n                )\n                break\n            except Exception:\n                await asyncio.sleep(0.1)\n\n    # Mount proxy to the running echo server\n    echo_proxy = create_proxy(ECHO_SERVER_URL, name=\"Echo Proxy\")\n    server.mount(echo_proxy, namespace=\"proxied\")\n\n    try:\n        yield\n    finally:\n        proc.terminate()\n        try:\n            proc.wait(timeout=5)\n        except subprocess.TimeoutExpired:\n            proc.kill()\n            proc.wait()\n\n\nmcp = FastMCP(\"Diagnostics Server\", lifespan=lifespan)\n\n# === SUCCESSFUL COMPONENTS ===\n\n\n@mcp.tool\ndef ping() -> str:\n    \"\"\"Simple ping tool - always succeeds.\"\"\"\n    return \"pong\"\n\n\n@mcp.resource(\"diag://status\")\ndef status_resource() -> str:\n    \"\"\"Status resource - always succeeds.\"\"\"\n    return \"OK\"\n\n\n@mcp.resource(\"diag://echo/{message}\")\ndef echo_template(message: str) -> str:\n    \"\"\"Echo template - always succeeds.\"\"\"\n    return f\"Echo: {message}\"\n\n\n@mcp.prompt(\"greet\")\ndef greet_prompt(name: str = \"World\") -> str:\n    \"\"\"Greeting prompt - always succeeds.\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# === ERROR COMPONENTS ===\n\n\n@mcp.tool\ndef fail_tool(message: str = \"Intentional tool failure\") -> str:\n    \"\"\"Tool that always raises ValueError - for error tracing.\"\"\"\n    raise ValueError(message)\n\n\n@mcp.resource(\"diag://error\")\ndef error_resource() -> str:\n    \"\"\"Resource that always raises ValueError.\"\"\"\n    raise ValueError(\"Intentional resource failure\")\n\n\n@mcp.resource(\"diag://error/{code}\")\ndef error_template(code: str) -> str:\n    \"\"\"Template that always raises ValueError.\"\"\"\n    raise ValueError(f\"Intentional template failure: {code}\")\n\n\n@mcp.prompt(\"fail\")\ndef fail_prompt() -> str:\n    \"\"\"Prompt that always raises ValueError.\"\"\"\n    raise ValueError(\"Intentional prompt failure\")\n"
  },
  {
    "path": "examples/echo.py",
    "content": "\"\"\"\nFastMCP Echo Server\n\"\"\"\n\nfrom fastmcp import FastMCP\n\n# Create server\nmcp = FastMCP(\"Echo Server\")\n\n\n@mcp.tool\ndef echo_tool(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return text\n\n\n@mcp.resource(\"echo://static\")\ndef echo_resource() -> str:\n    return \"Echo!\"\n\n\n@mcp.resource(\"echo://{text}\")\ndef echo_template(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return f\"Echo: {text}\"\n\n\n@mcp.prompt(\"echo\")\ndef echo_prompt(text: str) -> str:\n    return text\n"
  },
  {
    "path": "examples/elicitation.py",
    "content": "\"\"\"\nFastMCP Elicitation Example\n\nDemonstrates tools that ask users for input during execution.\n\nTry it with the CLI:\n\n    fastmcp list examples/elicitation.py\n    fastmcp call examples/elicitation.py greet\n    fastmcp call examples/elicitation.py survey\n\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom fastmcp import Context, FastMCP\n\nmcp = FastMCP(\"Elicitation Demo\")\n\n\n@mcp.tool\nasync def greet(ctx: Context) -> str:\n    \"\"\"Greet the user by name (asks for their name).\"\"\"\n    result = await ctx.elicit(\"What is your name?\", response_type=str)\n\n    if result.action == \"accept\":\n        return f\"Hello, {result.data}!\"\n    return \"Maybe next time!\"\n\n\n@mcp.tool\nasync def survey(ctx: Context) -> str:\n    \"\"\"Run a short survey collecting structured info.\"\"\"\n\n    @dataclass\n    class SurveyResponse:\n        favorite_color: str\n        lucky_number: int\n\n    result = await ctx.elicit(\n        \"Quick survey — tell us about yourself:\",\n        response_type=SurveyResponse,\n    )\n\n    if result.action == \"accept\":\n        resp = result.data\n        return f\"Got it — you like {resp.favorite_color} and your lucky number is {resp.lucky_number}.\"\n    return \"Survey skipped.\"\n"
  },
  {
    "path": "examples/fastmcp_config/env_interpolation_example.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"entrypoint\": \"src/server.py:app\",\n  \"environment\": {\n    \"python\": \"3.12\",\n    \"dependencies\": [\"fastmcp\", \"httpx\", \"pandas\"]\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"0.0.0.0\",\n    \"port\": 8000,\n    \"env\": {\n      \"API_BASE_URL\": \"https://api.${ENVIRONMENT}.example.com\",\n      \"DATABASE_URL\": \"postgres://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}\",\n      \"CACHE_PREFIX\": \"myapp_${ENVIRONMENT}_v1\",\n      \"LOG_LEVEL\": \"${LOG_LEVEL}\",\n      \"FEATURE_FLAGS\": \"${FEATURE_FLAGS}\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/fastmcp_config/fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/schemas/fastmcp/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\"\n  },\n  \"environment\": {\n    \"python\": \"3.12\",\n    \"dependencies\": [\"requests\"]\n  },\n  \"deployment\": {\n    \"transport\": \"stdio\"\n  }\n}"
  },
  {
    "path": "examples/fastmcp_config/full_example.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\",\n    \"entrypoint\": \"mcp\"\n  },\n  \"environment\": {\n    \"python\": \"3.12\",\n    \"dependencies\": [\"requests>=2.31.0\", \"httpx\"],\n    \"requirements\": null,\n    \"project\": null,\n    \"editable\": null\n  },\n  \"deployment\": {\n    \"transport\": \"http\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8000,\n    \"path\": \"/mcp/\",\n    \"log_level\": \"INFO\",\n    \"env\": {\n      \"DEBUG\": \"false\",\n      \"API_TIMEOUT\": \"30\"\n    },\n    \"cwd\": null,\n    \"args\": null\n  }\n}\n"
  },
  {
    "path": "examples/fastmcp_config/server.py",
    "content": "\"\"\"Example FastMCP server for demonstrating fastmcp.json configuration.\"\"\"\n\nfrom fastmcp import FastMCP\n\n# Create the FastMCP server instance\nmcp = FastMCP(\"Config Example Server\")\n\n\n@mcp.tool\ndef echo(text: str) -> str:\n    \"\"\"Echo the provided text back to the user.\"\"\"\n    return f\"You said: {text}\"\n\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.resource(\"config://example\")\ndef get_example_config() -> str:\n    \"\"\"Return an example configuration.\"\"\"\n    return \"\"\"\n    This server is configured using fastmcp.json.\n    \n    The configuration file specifies:\n    - Python version\n    - Dependencies\n    - Transport settings\n    - Other runtime options\n    \"\"\"\n\n\n# This allows the server to run with: fastmcp run server.py\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(mcp.run_async())\n"
  },
  {
    "path": "examples/fastmcp_config/simple.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/schemas/fastmcp/v1.json\",\n  \"entrypoint\": \"server.py\",\n  \"deployment\": {\n    \"transport\": \"stdio\"\n  }\n}"
  },
  {
    "path": "examples/fastmcp_config_demo/README.md",
    "content": "# FastMCP Configuration Demo\n\nThis example demonstrates the recommended way to configure FastMCP servers using `fastmcp.json`.\n\n## Migration from Dependencies Parameter\n\nPreviously (deprecated as of FastMCP 2.11.4), you would specify dependencies in the Python code:\n\n```python\nmcp = FastMCP(\"Demo Server\", dependencies=[\"pyautogui\", \"Pillow\"])\n```\n\nNow, dependencies are declared in `fastmcp.json`:\n\n```json\n{\n  \"environment\": {\n    \"dependencies\": [\"pyautogui\", \"Pillow\"]\n  }\n}\n```\n\n## Running the Server\n\nWith the configuration file in place, you can run the server in several ways:\n\n```bash\n# Auto-detect fastmcp.json in current directory\ncd examples/mcp_server_config_demo\nfastmcp run\n\n# Or specify the config file explicitly\nfastmcp run examples/mcp_server_config_demo/fastmcp.json\n\n# Or use development mode with the Inspector UI\nfastmcp dev examples/mcp_server_config_demo/fastmcp.json\n```\n\n## Benefits\n\n- **Single source of truth**: All configuration in one place\n- **Environment isolation**: Dependencies are installed in an isolated UV environment\n- **No import-time issues**: Dependencies are installed before the server is imported\n- **IDE support**: JSON schema provides autocomplete and validation\n- **Shareable**: Easy to share complete server configuration with others\n\n## Configuration Structure\n\nThe `fastmcp.json` file supports three main sections:\n\n1. **entrypoint** (required): The Python file containing your server\n2. **environment** (optional): Python version and dependencies\n3. **deployment** (optional): Runtime settings like transport and logging\n\nSee the [full documentation](https://gofastmcp.com/docs/deployment/server-configuration) for more details."
  },
  {
    "path": "examples/fastmcp_config_demo/fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"source\": {\n    \"path\": \"server.py\"\n  },\n  \"environment\": {\n    \"python\": \"3.11\",\n    \"dependencies\": [\"pyautogui\", \"Pillow\"]\n  },\n  \"deployment\": {\n    \"transport\": \"stdio\",\n    \"log_level\": \"INFO\"\n  }\n}\n"
  },
  {
    "path": "examples/fastmcp_config_demo/server.py",
    "content": "\"\"\"\nExample server demonstrating fastmcp.json configuration.\n\nThis server previously would have used the deprecated dependencies parameter:\n    mcp = FastMCP(\"Demo Server\", dependencies=[\"pyautogui\", \"Pillow\"])\n\nNow dependencies are declared in fastmcp.json alongside this file.\n\"\"\"\n\nimport io\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.types import Image\n\n# Create server - dependencies are now in fastmcp.json\nmcp = FastMCP(\"Screenshot Demo\")\n\n\n@mcp.tool\ndef take_screenshot() -> Image:\n    \"\"\"\n    Take a screenshot of the user's screen and return it as an image.\n\n    Use this tool anytime the user wants you to look at something on their screen.\n    \"\"\"\n    import pyautogui\n\n    buffer = io.BytesIO()\n\n    # Capture and compress the screenshot to stay under size limits\n    screenshot = pyautogui.screenshot()\n    screenshot.convert(\"RGB\").save(buffer, format=\"JPEG\", quality=60, optimize=True)\n\n    return Image(data=buffer.getvalue(), format=\"jpeg\")\n\n\n@mcp.tool\ndef analyze_colors() -> dict:\n    \"\"\"\n    Analyze the dominant colors in the current screen.\n\n    Returns a dictionary with color statistics from the screen.\n    \"\"\"\n    import pyautogui\n    from PIL import Image as PILImage\n\n    screenshot = pyautogui.screenshot()\n    # Convert to smaller size for faster analysis\n    small = screenshot.resize((100, 100), PILImage.Resampling.LANCZOS)\n\n    # Get colors\n    colors = small.getcolors(maxcolors=10000)\n    if not colors:\n        return {\"error\": \"Too many colors to analyze\"}\n\n    # Sort by frequency\n    sorted_colors = sorted(colors, key=lambda x: x[0], reverse=True)[:10]\n\n    return {\n        \"top_colors\": [\n            {\"count\": count, \"rgb\": color} for count, color in sorted_colors\n        ],\n        \"total_pixels\": sum(c[0] for c in colors),\n    }\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(mcp.run_async())\n"
  },
  {
    "path": "examples/filesystem-provider/mcp/prompts/assistant.py",
    "content": "\"\"\"Assistant prompts.\"\"\"\n\nfrom fastmcp.prompts import prompt\n\n\n@prompt\ndef code_review(code: str, language: str = \"python\") -> str:\n    \"\"\"Generate a code review prompt.\n\n    Args:\n        code: The code to review.\n        language: Programming language (default: python).\n    \"\"\"\n    return f\"\"\"Please review this {language} code:\n\n```{language}\n{code}\n```\n\nFocus on:\n- Code quality and readability\n- Potential bugs or issues\n- Performance considerations\n- Best practices\"\"\"\n\n\n@prompt(\n    name=\"explain-concept\",\n    description=\"Generate a prompt to explain a technical concept.\",\n    tags={\"education\", \"explanation\"},\n)\ndef explain(topic: str, audience: str = \"developer\") -> str:\n    \"\"\"Generate an explanation prompt.\n\n    Args:\n        topic: The concept to explain.\n        audience: Target audience level.\n    \"\"\"\n    return f\"Explain {topic} to a {audience}. Use clear examples and analogies.\"\n"
  },
  {
    "path": "examples/filesystem-provider/mcp/resources/config.py",
    "content": "\"\"\"Configuration resources - static and templated.\"\"\"\n\nimport json\n\nfrom fastmcp.resources import resource\n\n\n# Static resource - no parameters in URI\n@resource(\"config://app\")\ndef get_app_config() -> str:\n    \"\"\"Get application configuration.\"\"\"\n    return json.dumps(\n        {\n            \"name\": \"FilesystemDemo\",\n            \"version\": \"1.0.0\",\n            \"features\": [\"tools\", \"resources\", \"prompts\"],\n        },\n        indent=2,\n    )\n\n\n# Resource template - {env} is a parameter\n@resource(\"config://env/{env}\")\ndef get_env_config(env: str) -> str:\n    \"\"\"Get environment-specific configuration.\n\n    Args:\n        env: Environment name (dev, staging, prod).\n    \"\"\"\n    configs = {\n        \"dev\": {\"debug\": True, \"log_level\": \"DEBUG\", \"database\": \"localhost\"},\n        \"staging\": {\"debug\": True, \"log_level\": \"INFO\", \"database\": \"staging-db\"},\n        \"prod\": {\"debug\": False, \"log_level\": \"WARNING\", \"database\": \"prod-db\"},\n    }\n    config = configs.get(env, {\"error\": f\"Unknown environment: {env}\"})\n    return json.dumps(config, indent=2)\n\n\n# Resource with custom metadata\n@resource(\n    \"config://features\",\n    name=\"feature-flags\",\n    mime_type=\"application/json\",\n    tags={\"config\", \"features\"},\n)\ndef get_feature_flags() -> str:\n    \"\"\"Get feature flags configuration.\"\"\"\n    return json.dumps(\n        {\n            \"dark_mode\": True,\n            \"beta_features\": False,\n            \"max_upload_size_mb\": 100,\n        },\n        indent=2,\n    )\n"
  },
  {
    "path": "examples/filesystem-provider/mcp/tools/calculator.py",
    "content": "\"\"\"Math tools with custom metadata.\"\"\"\n\nfrom fastmcp.tools import tool\n\n\n@tool(\n    name=\"add-numbers\",  # Custom name (default would be \"add\")\n    description=\"Add two numbers together.\",\n    tags={\"math\", \"arithmetic\"},\n)\ndef add(a: float, b: float) -> float:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n\n\n@tool(tags={\"math\", \"arithmetic\"})\ndef multiply(a: float, b: float) -> float:\n    \"\"\"Multiply two numbers.\n\n    Args:\n        a: First number.\n        b: Second number.\n    \"\"\"\n    return a * b\n"
  },
  {
    "path": "examples/filesystem-provider/mcp/tools/greeting.py",
    "content": "\"\"\"Greeting tools - multiple tools in one file.\"\"\"\n\nfrom fastmcp.tools import tool\n\n\n@tool\ndef greet(name: str) -> str:\n    \"\"\"Greet someone by name.\n\n    Args:\n        name: The person's name.\n    \"\"\"\n    return f\"Hello, {name}!\"\n\n\n@tool\ndef farewell(name: str) -> str:\n    \"\"\"Say goodbye to someone.\n\n    Args:\n        name: The person's name.\n    \"\"\"\n    return f\"Goodbye, {name}!\"\n\n\n# Helper functions without decorators are ignored\ndef _format_message(msg: str) -> str:\n    return msg.strip().capitalize()\n"
  },
  {
    "path": "examples/filesystem-provider/server.py",
    "content": "\"\"\"Filesystem-based MCP server using FileSystemProvider.\n\nThis example demonstrates how to use FileSystemProvider to automatically\ndiscover and register tools, resources, and prompts from the filesystem.\n\nRun:\n    fastmcp run examples/filesystem-provider/server.py\n\nInspect:\n    fastmcp inspect examples/filesystem-provider/server.py\n\nDev mode (re-scan files on every request):\n    Change reload=True below, then modify files while the server runs.\n\"\"\"\n\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers import FileSystemProvider\n\n# The provider scans all .py files in the directory recursively.\n# Functions decorated with @tool, @resource, or @prompt are registered.\n# Directory structure is purely organizational - decorators determine type.\nprovider = FileSystemProvider(\n    root=Path(__file__).parent / \"mcp\",\n    reload=True,  # Set True for dev mode (re-scan on every request)\n)\n\nmcp = FastMCP(\"FilesystemDemo\", providers=[provider])\n"
  },
  {
    "path": "examples/get_file.py",
    "content": "# /// script\n# dependencies = [\"aiohttp\", \"fastmcp\"]\n# ///\n\n# uv pip install aiohttp fastmcp\n\nimport aiohttp\n\nfrom fastmcp.server import FastMCP\nfrom fastmcp.utilities.types import File\n\n\ndef create_server():\n    mcp = FastMCP(name=\"File Demo\", instructions=\"Get files from the server or URL.\")\n\n    @mcp.tool()\n    async def get_test_file_from_server(path: str = \"requirements.txt\") -> File:\n        \"\"\"\n        Get a test file from the server. If the path is not provided, it defaults to 'requirements.txt'.\n        \"\"\"\n        return File(path=path)\n\n    @mcp.tool()\n    async def get_test_pdf_from_url(\n        url: str = \"https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf\",\n    ) -> File:\n        \"\"\"\n        Get a test PDF file from a URL. If the URL is not provided, it defaults to a sample PDF.\n        \"\"\"\n        async with aiohttp.ClientSession() as session:\n            async with session.get(url) as response:\n                pdf_data = await response.read()\n                return File(data=pdf_data, format=\"pdf\")\n\n    return mcp\n\n\nif __name__ == \"__main__\":\n    create_server().run(transport=\"sse\", host=\"0.0.0.0\", port=8001, path=\"/sse\")\n"
  },
  {
    "path": "examples/in_memory_proxy_example.py",
    "content": "\"\"\"\nThis example demonstrates how to set up and use an in-memory FastMCP proxy.\n\nIt illustrates the pattern:\n1. Create an original FastMCP server with some tools.\n2. Create a proxy FastMCP server using ``FastMCP.as_proxy(original_server)``.\n3. Use another Client to connect to the proxy server (in-memory) and interact with the original server's tools through the proxy.\n\"\"\"\n\nimport asyncio\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\nclass EchoService:\n    \"\"\"A simple service to demonstrate with\"\"\"\n\n    def echo(self, message: str) -> str:\n        return f\"Original server echoes: {message}\"\n\n\nasync def main():\n    print(\"--- In-Memory FastMCP Proxy Example ---\")\n    print(\"This example will walk through setting up an in-memory proxy.\")\n    print(\"-----------------------------------------\")\n\n    # 1. Original Server Setup\n    print(\n        \"\\nStep 1: Setting up the Original Server (OriginalEchoServer) with an 'echo' tool...\"\n    )\n    original_server = FastMCP(\"OriginalEchoServer\")\n    original_server.add_tool(EchoService().echo)\n    print(f\"   -> Original Server '{original_server.name}' created.\")\n\n    # 2. Proxy Server Creation\n    print(\"\\nStep 2: Creating the Proxy Server (InMemoryProxy)...\")\n    print(\n        f\"          (Using FastMCP.as_proxy to wrap '{original_server.name}' directly)\"\n    )\n    proxy_server = FastMCP.as_proxy(original_server, name=\"InMemoryProxy\")\n    print(\n        f\"   -> Proxy Server '{proxy_server.name}' created, proxying '{original_server.name}'.\"\n    )\n\n    # 3. Interacting via Proxy\n    print(\"\\nStep 3: Using a new Client to connect to the Proxy Server and interact...\")\n    async with Client(proxy_server) as final_client:\n        print(f\"   -> Successfully connected to proxy '{proxy_server.name}'.\")\n\n        print(\"\\n   Listing tools available via proxy...\")\n        tools = await final_client.list_tools()\n        if tools:\n            print(\"      Available Tools:\")\n            for tool in tools:\n                print(\n                    f\"        - {tool.name} (Description: {tool.description or 'N/A'})\"\n                )\n        else:\n            print(\"      No tools found via proxy.\")\n\n        message_to_echo = \"Hello, simplified proxied world!\"\n        print(f\"\\n   Calling 'echo' tool via proxy with message: '{message_to_echo}'\")\n        try:\n            result = await final_client.call_tool(\"echo\", {\"message\": message_to_echo})\n            if result and isinstance(result[0], TextContent):\n                print(f\"      Result from proxied 'echo' call: '{result[0].text}'\")\n            else:\n                print(\n                    f\"      Error: Unexpected result format from proxied 'echo' call: {result}\"\n                )\n        except Exception as e:\n            print(f\"      Error calling 'echo' tool via proxy: {e}\")\n\n    print(\"\\n-----------------------------------------\")\n    print(\"--- In-Memory Proxy Example Finished ---\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/memory.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"entrypoint\": \"memory.py\",\n  \"environment\": {\n    \"dependencies\": [\"pydantic-ai-slim[openai]\", \"asyncpg\", \"numpy\", \"pgvector\"]\n  }\n}\n"
  },
  {
    "path": "examples/memory.py",
    "content": "# /// script\n# dependencies = [\"pydantic-ai-slim[openai]\", \"asyncpg\", \"numpy\", \"pgvector\", \"fastmcp\"]\n# ///\n\n# uv pip install 'pydantic-ai-slim[openai]' asyncpg numpy pgvector fastmcp\n\n\"\"\"\nRecursive memory system inspired by the human brain's clustering of memories.\nUses OpenAI's 'text-embedding-3-small' model and pgvector for efficient similarity search.\n\"\"\"\n\nimport asyncio\nimport math\nimport os\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom typing import Annotated, Any, Self\n\nimport asyncpg\nimport numpy as np\nfrom openai import AsyncOpenAI\nfrom pgvector.asyncpg import register_vector\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent\n\nimport fastmcp\nfrom fastmcp import FastMCP\n\nMAX_DEPTH = 5\nSIMILARITY_THRESHOLD = 0.7\nDECAY_FACTOR = 0.99\nREINFORCEMENT_FACTOR = 1.1\n\nDEFAULT_LLM_MODEL = \"openai:gpt-4o\"\nDEFAULT_EMBEDDING_MODEL = \"text-embedding-3-small\"\n\n# Dependencies are configured in memory.fastmcp.json\nmcp = FastMCP(\"memory\")\n\nDB_DSN = \"postgresql://postgres:postgres@localhost:54320/memory_db\"\n# reset memory by deleting the profile directory\nPROFILE_DIR = (\n    fastmcp.settings.home / os.environ.get(\"USER\", \"anon\") / \"memory\"\n).resolve()\nPROFILE_DIR.mkdir(parents=True, exist_ok=True)\n\n\ndef cosine_similarity(a: list[float], b: list[float]) -> float:\n    a_array = np.array(a, dtype=np.float64)\n    b_array = np.array(b, dtype=np.float64)\n    return np.dot(a_array, b_array) / (\n        np.linalg.norm(a_array) * np.linalg.norm(b_array)\n    )\n\n\nasync def do_ai(\n    user_prompt: str,\n    system_prompt: str,\n    result_type: type | Annotated,\n    deps=None,\n) -> Any:\n    agent = Agent(\n        DEFAULT_LLM_MODEL,\n        system_prompt=system_prompt,\n        result_type=result_type,\n    )\n    result = await agent.run(user_prompt, deps=deps)\n    return result.data\n\n\n@dataclass\nclass Deps:\n    openai: AsyncOpenAI\n    pool: asyncpg.Pool\n\n\nasync def get_db_pool() -> asyncpg.Pool:\n    async def init(conn):\n        await conn.execute(\"CREATE EXTENSION IF NOT EXISTS vector;\")\n        await register_vector(conn)\n\n    pool = await asyncpg.create_pool(DB_DSN, init=init)\n    return pool\n\n\nclass MemoryNode(BaseModel):\n    id: int | None = None\n    content: str\n    summary: str = \"\"\n    importance: float = 1.0\n    access_count: int = 0\n    timestamp: float = Field(\n        default_factory=lambda: datetime.now(timezone.utc).timestamp()\n    )\n    embedding: list[float]\n\n    @classmethod\n    async def from_content(cls, content: str, deps: Deps):\n        embedding = await get_embedding(content, deps)\n        return cls(content=content, embedding=embedding)\n\n    async def save(self, deps: Deps):\n        async with deps.pool.acquire() as conn:\n            if self.id is None:\n                result = await conn.fetchrow(\n                    \"\"\"\n                    INSERT INTO memories (content, summary, importance, access_count, timestamp, embedding)\n                    VALUES ($1, $2, $3, $4, $5, $6)\n                    RETURNING id\n                    \"\"\",\n                    self.content,\n                    self.summary,\n                    self.importance,\n                    self.access_count,\n                    self.timestamp,\n                    self.embedding,\n                )\n                self.id = result[\"id\"]\n            else:\n                await conn.execute(\n                    \"\"\"\n                    UPDATE memories\n                    SET content = $1, summary = $2, importance = $3,\n                        access_count = $4, timestamp = $5, embedding = $6\n                    WHERE id = $7\n                    \"\"\",\n                    self.content,\n                    self.summary,\n                    self.importance,\n                    self.access_count,\n                    self.timestamp,\n                    self.embedding,\n                    self.id,\n                )\n\n    async def merge_with(self, other: Self, deps: Deps):\n        self.content = await do_ai(\n            f\"{self.content}\\n\\n{other.content}\",\n            \"Combine the following two texts into a single, coherent text.\",\n            str,\n            deps,\n        )\n        self.importance += other.importance\n        self.access_count += other.access_count\n        self.embedding = [\n            (a + b) / 2 for a, b in zip(self.embedding, other.embedding, strict=True)\n        ]\n        self.summary = await do_ai(\n            self.content, \"Summarize the following text concisely.\", str, deps\n        )\n        await self.save(deps)\n        # Delete the merged node from the database\n        if other.id is not None:\n            await delete_memory(other.id, deps)\n\n    def get_effective_importance(self):\n        return self.importance * (1 + math.log(self.access_count + 1))\n\n\nasync def get_embedding(text: str, deps: Deps) -> list[float]:\n    embedding_response = await deps.openai.embeddings.create(\n        input=text,\n        model=DEFAULT_EMBEDDING_MODEL,\n    )\n    return embedding_response.data[0].embedding\n\n\nasync def delete_memory(memory_id: int, deps: Deps):\n    async with deps.pool.acquire() as conn:\n        await conn.execute(\"DELETE FROM memories WHERE id = $1\", memory_id)\n\n\nasync def add_memory(content: str, deps: Deps):\n    new_memory = await MemoryNode.from_content(content, deps)\n    await new_memory.save(deps)\n\n    similar_memories = await find_similar_memories(new_memory.embedding, deps)\n    for memory in similar_memories:\n        if memory.id != new_memory.id:\n            await new_memory.merge_with(memory, deps)\n\n    await update_importance(new_memory.embedding, deps)\n\n    await prune_memories(deps)\n\n    return f\"Remembered: {content}\"\n\n\nasync def find_similar_memories(embedding: list[float], deps: Deps) -> list[MemoryNode]:\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT id, content, summary, importance, access_count, timestamp, embedding\n            FROM memories\n            ORDER BY embedding <-> $1\n            LIMIT 5\n            \"\"\",\n            embedding,\n        )\n    memories = [\n        MemoryNode(\n            id=row[\"id\"],\n            content=row[\"content\"],\n            summary=row[\"summary\"],\n            importance=row[\"importance\"],\n            access_count=row[\"access_count\"],\n            timestamp=row[\"timestamp\"],\n            embedding=row[\"embedding\"],\n        )\n        for row in rows\n    ]\n    return memories\n\n\nasync def update_importance(user_embedding: list[float], deps: Deps):\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"SELECT id, importance, access_count, embedding FROM memories\"\n        )\n        for row in rows:\n            memory_embedding = row[\"embedding\"]\n            similarity = cosine_similarity(user_embedding, memory_embedding)\n            if similarity > SIMILARITY_THRESHOLD:\n                new_importance = row[\"importance\"] * REINFORCEMENT_FACTOR\n                new_access_count = row[\"access_count\"] + 1\n            else:\n                new_importance = row[\"importance\"] * DECAY_FACTOR\n                new_access_count = row[\"access_count\"]\n            await conn.execute(\n                \"\"\"\n                UPDATE memories\n                SET importance = $1, access_count = $2\n                WHERE id = $3\n                \"\"\",\n                new_importance,\n                new_access_count,\n                row[\"id\"],\n            )\n\n\nasync def prune_memories(deps: Deps):\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT id, importance, access_count\n            FROM memories\n            ORDER BY importance DESC\n            OFFSET $1\n            \"\"\",\n            MAX_DEPTH,\n        )\n        for row in rows:\n            await conn.execute(\"DELETE FROM memories WHERE id = $1\", row[\"id\"])\n\n\nasync def display_memory_tree(deps: Deps) -> str:\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT content, summary, importance, access_count\n            FROM memories\n            ORDER BY importance DESC\n            LIMIT $1\n            \"\"\",\n            MAX_DEPTH,\n        )\n    result = \"\"\n    for row in rows:\n        effective_importance = row[\"importance\"] * (\n            1 + math.log(row[\"access_count\"] + 1)\n        )\n        summary = row[\"summary\"] or row[\"content\"]\n        result += f\"- {summary} (Importance: {effective_importance:.2f})\\n\"\n    return result\n\n\n@mcp.tool\nasync def remember(\n    contents: Annotated[\n        list[str], Field(description=\"List of observations or memories to store\")\n    ],\n):\n    deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())\n    try:\n        return \"\\n\".join(\n            await asyncio.gather(*[add_memory(content, deps) for content in contents])\n        )\n    finally:\n        await deps.pool.close()\n\n\n@mcp.tool\nasync def read_profile() -> str:\n    deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())\n    profile = await display_memory_tree(deps)\n    await deps.pool.close()\n    return profile\n\n\nasync def initialize_database():\n    pool = await asyncpg.create_pool(\n        \"postgresql://postgres:postgres@localhost:54320/postgres\"\n    )\n    try:\n        async with pool.acquire() as conn:\n            await conn.execute(\"\"\"\n                SELECT pg_terminate_backend(pg_stat_activity.pid)\n                FROM pg_stat_activity\n                WHERE pg_stat_activity.datname = 'memory_db'\n                AND pid <> pg_backend_pid();\n            \"\"\")\n            await conn.execute(\"DROP DATABASE IF EXISTS memory_db;\")\n            await conn.execute(\"CREATE DATABASE memory_db;\")\n    finally:\n        await pool.close()\n\n    pool = await asyncpg.create_pool(DB_DSN)\n    try:\n        async with pool.acquire() as conn:\n            await conn.execute(\"CREATE EXTENSION IF NOT EXISTS vector;\")\n\n            await register_vector(conn)\n\n            await conn.execute(\"\"\"\n                CREATE TABLE IF NOT EXISTS memories (\n                    id SERIAL PRIMARY KEY,\n                    content TEXT NOT NULL,\n                    summary TEXT,\n                    importance REAL NOT NULL,\n                    access_count INT NOT NULL,\n                    timestamp DOUBLE PRECISION NOT NULL,\n                    embedding vector(1536) NOT NULL\n                );\n                CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw (embedding vector_l2_ops);\n            \"\"\")\n    finally:\n        await pool.close()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(initialize_database())\n"
  },
  {
    "path": "examples/mount_example.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"entrypoint\": \"mount_example.py\"\n}\n"
  },
  {
    "path": "examples/mount_example.py",
    "content": "\"\"\"Example of mounting FastMCP apps together.\n\nThis example demonstrates how to mount FastMCP apps together. It shows how to:\n\n1. Create sub-applications for different domains\n2. Mount those sub-applications to a main application\n3. Access tools with prefixed names and resources with prefixed URIs\n\"\"\"\n\nimport asyncio\nfrom urllib.parse import urlparse\n\nfrom fastmcp import FastMCP\n\n# Weather sub-application\nweather_app = FastMCP(\"Weather App\")\n\n\n@weather_app.tool\ndef get_weather_forecast(location: str) -> str:\n    \"\"\"Get the weather forecast for a location.\"\"\"\n    return f\"Sunny skies for {location} today!\"\n\n\n@weather_app.resource(uri=\"weather://forecast\")\nasync def weather_data():\n    \"\"\"Return current weather data.\"\"\"\n    return {\"temperature\": 72, \"conditions\": \"sunny\", \"humidity\": 45, \"wind_speed\": 5}\n\n\n# News sub-application\nnews_app = FastMCP(\"News App\")\n\n\n@news_app.tool\ndef get_news_headlines() -> list[str]:\n    \"\"\"Get the latest news headlines.\"\"\"\n    return [\n        \"Tech company launches new product\",\n        \"Local team wins championship\",\n        \"Scientists make breakthrough discovery\",\n    ]\n\n\n@news_app.resource(uri=\"news://headlines\")\nasync def news_data():\n    \"\"\"Return latest news data.\"\"\"\n    return {\n        \"top_story\": \"Breaking news: Important event happened\",\n        \"categories\": [\"politics\", \"sports\", \"technology\"],\n        \"sources\": [\"AP\", \"Reuters\", \"Local Sources\"],\n    }\n\n\n# Main application\napp = FastMCP(\"Main App\")\n\n\n@app.tool\ndef check_app_status() -> dict[str, str]:\n    \"\"\"Check the status of the main application.\"\"\"\n    return {\"status\": \"running\", \"version\": \"1.0.0\", \"uptime\": \"3h 24m\"}\n\n\n# Mount sub-applications\napp.mount(server=weather_app, prefix=\"weather\")\n\napp.mount(server=news_app, prefix=\"news\")\n\n\nasync def get_server_details():\n    \"\"\"Print information about mounted resources.\"\"\"\n    # Print available tools\n    tools = await app.list_tools()\n    print(f\"\\nAvailable tools ({len(tools)}):\")\n    for tool in tools:\n        print(f\"  - {tool.name}: {tool.description}\")\n\n    # Print available resources\n    print(\"\\nAvailable resources:\")\n\n    # Distinguish between native and imported resources\n    # Native resources would be those directly in the main app (not prefixed)\n\n    resources = await app.list_resources()\n\n    native_resources = [\n        str(r.uri)\n        for r in resources\n        if urlparse(str(r.uri)).netloc not in (\"weather\", \"news\")\n    ]\n\n    # Imported resources - categorized by source app\n    weather_resources = [\n        str(r.uri) for r in resources if urlparse(str(r.uri)).netloc == \"weather\"\n    ]\n    news_resources = [\n        str(r.uri) for r in resources if urlparse(str(r.uri)).netloc == \"news\"\n    ]\n\n    print(f\"  - Native app resources: {native_resources}\")\n    print(f\"  - Imported from weather app: {weather_resources}\")\n    print(f\"  - Imported from news app: {news_resources}\")\n\n\nif __name__ == \"__main__\":\n    # First run our async function to display info\n    asyncio.run(get_server_details())\n\n    # Then start the server (uncomment to run the server)\n    app.run()\n"
  },
  {
    "path": "examples/namespace_activation/README.md",
    "content": "# Namespace Activation\n\nDemonstrates session-specific visibility control using tags to organize tools into namespaces that can be activated on demand.\n\n## Pattern\n\n1. Tag tools with namespaces: `@server.tool(tags={\"namespace:finance\"})`\n2. Globally disable namespaces: `server.disable(tags={\"namespace:finance\"})`\n3. Provide activation tools that call `ctx.enable_components(tags={\"namespace:finance\"})`\n\nEach session starts with only the activation tools visible. When a session calls an activation tool, that namespace becomes visible **only for that session**.\n\n## Run\n\n```bash\n# Server\nuv run python server.py\n\n# Client (in another terminal)\nuv run python client.py\n```\n\n## Example Output\n\n```\nNamespace Activation Demo\n\n╭─────────────────── Initial Tools ───────────────────╮\n│ activate_finance, activate_admin, deactivate_all    │\n╰─────────────────────────────────────────────────────╯\n\n→ Calling activate_finance()\n  Finance tools activated\n╭─────────────── After Activating Finance ────────────╮\n│ analyze_portfolio, get_market_data, execute_trade,  │\n│ activate_finance, activate_admin, deactivate_all    │\n╰─────────────────────────────────────────────────────╯\n\n→ Calling get_market_data(symbol='AAPL')\n  {'symbol': 'AAPL', 'price': 150.25, 'change': '+2.5%'}\n\n→ Calling activate_admin()\n  Admin tools activated\n╭────────────── After Activating Admin ───────────────╮\n│ analyze_portfolio, get_market_data, execute_trade,  │\n│ list_users, reset_user_password, activate_finance,  │\n│ activate_admin, deactivate_all                      │\n╰─────────────────────────────────────────────────────╯\n\n→ Calling deactivate_all()\n  All namespaces deactivated\n╭────────────── After Deactivating All ───────────────╮\n│ activate_finance, activate_admin, deactivate_all    │\n╰─────────────────────────────────────────────────────╯\n```\n"
  },
  {
    "path": "examples/namespace_activation/client.py",
    "content": "\"\"\"\nNamespace Activation Client\n\nDemonstrates how session-specific visibility works from the client perspective.\n\"\"\"\n\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nfrom rich import print\nfrom rich.panel import Panel\n\nfrom fastmcp import Client\n\n\ndef load_server():\n    \"\"\"Load the example server.\"\"\"\n    examples_dir = Path(__file__).parent\n    if str(examples_dir) not in sys.path:\n        sys.path.insert(0, str(examples_dir))\n\n    import server as server_module\n\n    return server_module.server\n\n\nserver = load_server()\n\n\ndef show_tools(tools: list, title: str) -> None:\n    \"\"\"Display available tools in a panel.\"\"\"\n    tool_names = [f\"[cyan]{t.name}[/]\" for t in tools]\n    print(Panel(\", \".join(tool_names) or \"[dim]No tools[/]\", title=title))\n\n\nasync def main():\n    print(\"\\n[bold]Namespace Activation Demo[/]\\n\")\n\n    async with Client(server) as client:\n        # Initially only activation tools are visible\n        tools = await client.list_tools()\n        show_tools(tools, \"Initial Tools\")\n\n        # Activate finance namespace\n        print(\"\\n[yellow]→ Calling activate_finance()[/]\")\n        result = await client.call_tool(\"activate_finance\", {})\n        print(f\"  [green]{result.data}[/]\")\n\n        tools = await client.list_tools()\n        show_tools(tools, \"After Activating Finance\")\n\n        # Use a finance tool\n        print(\"\\n[yellow]→ Calling get_market_data(symbol='AAPL')[/]\")\n        result = await client.call_tool(\"get_market_data\", {\"symbol\": \"AAPL\"})\n        print(f\"  [green]{result.data}[/]\")\n\n        # Activate admin namespace too\n        print(\"\\n[yellow]→ Calling activate_admin()[/]\")\n        result = await client.call_tool(\"activate_admin\", {})\n        print(f\"  [green]{result.data}[/]\")\n\n        tools = await client.list_tools()\n        show_tools(tools, \"After Activating Admin\")\n\n        # Deactivate all - back to defaults\n        print(\"\\n[yellow]→ Calling deactivate_all()[/]\")\n        result = await client.call_tool(\"deactivate_all\", {})\n        print(f\"  [green]{result.data}[/]\")\n\n        tools = await client.list_tools()\n        show_tools(tools, \"After Deactivating All\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/namespace_activation/server.py",
    "content": "\"\"\"\nNamespace Activation Server\n\nTools are organized into namespaces using tags, globally disabled by default,\nand selectively enabled per-session via activation tools.\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nserver = FastMCP(\"Multi-Domain Assistant\")\n\n\n# Finance namespace\n@server.tool(tags={\"namespace:finance\"})\ndef analyze_portfolio(symbols: list[str]) -> str:\n    \"\"\"Analyze a portfolio of stock symbols.\"\"\"\n    return f\"Portfolio analysis for: {', '.join(symbols)}\"\n\n\n@server.tool(tags={\"namespace:finance\"})\ndef get_market_data(symbol: str) -> dict:\n    \"\"\"Get current market data for a symbol.\"\"\"\n    return {\"symbol\": symbol, \"price\": 150.25, \"change\": \"+2.5%\"}\n\n\n@server.tool(tags={\"namespace:finance\"})\ndef execute_trade(symbol: str, quantity: int, side: str) -> str:\n    \"\"\"Execute a trade (simulated).\"\"\"\n    return f\"Executed {side} order: {quantity} shares of {symbol}\"\n\n\n# Admin namespace\n@server.tool(tags={\"namespace:admin\"})\ndef list_users() -> list[str]:\n    \"\"\"List all system users.\"\"\"\n    return [\"alice\", \"bob\", \"charlie\"]\n\n\n@server.tool(tags={\"namespace:admin\"})\ndef reset_user_password(username: str) -> str:\n    \"\"\"Reset a user's password (simulated).\"\"\"\n    return f\"Password reset for {username}\"\n\n\n# Activation tools - always visible\n@server.tool\nasync def activate_finance(ctx: Context) -> str:\n    \"\"\"Activate finance tools for this session.\"\"\"\n    await ctx.enable_components(tags={\"namespace:finance\"})\n    return \"Finance tools activated\"\n\n\n@server.tool\nasync def activate_admin(ctx: Context) -> str:\n    \"\"\"Activate admin tools for this session.\"\"\"\n    await ctx.enable_components(tags={\"namespace:admin\"})\n    return \"Admin tools activated\"\n\n\n@server.tool\nasync def deactivate_all(ctx: Context) -> str:\n    \"\"\"Deactivate all namespaces, returning to defaults.\"\"\"\n    await ctx.reset_visibility()\n    return \"All namespaces deactivated\"\n\n\n# Globally disable namespace tools by default\nserver.disable(tags={\"namespace:finance\", \"namespace:admin\"})\n\n\nif __name__ == \"__main__\":\n    server.run()\n"
  },
  {
    "path": "examples/persistent_state/README.md",
    "content": "# Persistent Session State\n\nThis example demonstrates session-scoped state that persists across tool calls within the same MCP session.\n\n## What it shows\n\n- State set in one tool call is readable in subsequent calls\n- Different clients have isolated state (same keys, different values)\n- Reconnecting creates a new session with fresh state\n\n## Running\n\n**HTTP transport:**\n\n```bash\n# Terminal 1: Start the server\nuv run python server.py\n\n# Terminal 2: Run the client\nuv run python client.py\n```\n\n**STDIO transport (in-process):**\n\n```bash\nuv run python client_stdio.py\n```\n\n## Example output\n\n```text\nEach line below is a separate tool call\n\nAlice connects\n  session a9f6eaa3\n  set user = Alice\n  set secret = alice-password\n  get user → Alice\n  get secret → alice-password\n\nBob connects (different session)\n  session 0c3bffc5\n  get user → not found\n  get secret → not found\n  set user = Bob\n  get user → Bob\n\nAlice reconnects (new session)\n  session e39640e3\n  get user → not found\n```\n"
  },
  {
    "path": "examples/persistent_state/client.py",
    "content": "\"\"\"Client for testing persistent state.\n\nRun the server first:\n    uv run python examples/persistent_state/server.py\n\nThen run this client:\n    uv run python examples/persistent_state/client.py\n\"\"\"\n\nimport asyncio\n\nfrom rich.console import Console\n\nfrom fastmcp import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\n\nURL = \"http://127.0.0.1:8000/mcp\"\nconsole = Console()\n\n\nasync def main() -> None:\n    console.print()\n    console.print(\"[dim italic]Each line below is a separate tool call[/dim italic]\")\n    console.print()\n\n    # --- Alice's session ---\n    console.print(\"[dim]Alice connects[/dim]\")\n\n    transport1 = StreamableHttpTransport(url=URL)\n    async with Client(transport=transport1) as alice:\n        result = await alice.call_tool(\"list_session_info\", {})\n        console.print(f\"  session [cyan]{result.data['session_id'][:8]}[/cyan]\")\n\n        await alice.call_tool(\"set_value\", {\"key\": \"user\", \"value\": \"Alice\"})\n        console.print(\"  set [white]user[/white] = [green]Alice[/green]\")\n\n        await alice.call_tool(\"set_value\", {\"key\": \"secret\", \"value\": \"alice-password\"})\n        console.print(\"  set [white]secret[/white] = [green]alice-password[/green]\")\n\n        result = await alice.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [green]Alice[/green]\")\n\n        result = await alice.call_tool(\"get_value\", {\"key\": \"secret\"})\n        console.print(\"  get [white]secret[/white] → [green]alice-password[/green]\")\n\n    console.print()\n\n    # --- Bob's session ---\n    console.print(\"[dim]Bob connects (different session)[/dim]\")\n\n    transport2 = StreamableHttpTransport(url=URL)\n    async with Client(transport=transport2) as bob:\n        result = await bob.call_tool(\"list_session_info\", {})\n        console.print(f\"  session [cyan]{result.data['session_id'][:8]}[/cyan]\")\n\n        await bob.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [dim]not found[/dim]\")\n\n        await bob.call_tool(\"get_value\", {\"key\": \"secret\"})\n        console.print(\"  get [white]secret[/white] → [dim]not found[/dim]\")\n\n        await bob.call_tool(\"set_value\", {\"key\": \"user\", \"value\": \"Bob\"})\n        console.print(\"  set [white]user[/white] = [green]Bob[/green]\")\n\n        await bob.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [green]Bob[/green]\")\n\n    console.print()\n\n    # --- Alice reconnects ---\n    console.print(\"[dim]Alice reconnects (new session)[/dim]\")\n\n    transport3 = StreamableHttpTransport(url=URL)\n    async with Client(transport=transport3) as alice_again:\n        result = await alice_again.call_tool(\"list_session_info\", {})\n        console.print(f\"  session [cyan]{result.data['session_id'][:8]}[/cyan]\")\n\n        await alice_again.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [dim]not found[/dim]\")\n\n    console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/persistent_state/client_stdio.py",
    "content": "\"\"\"Client for testing persistent state over STDIO.\n\nRun directly:\n    uv run python examples/persistent_state/client_stdio.py\n\"\"\"\n\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nfrom rich.console import Console\n\nfrom fastmcp import Client, FastMCP\n\n# Add parent directory to path for importing the server module\nexamples_dir: Path = Path(__file__).parent.parent.parent\nif str(examples_dir) not in sys.path:\n    sys.path.insert(0, str(examples_dir))\n\nimport examples.persistent_state.server as server_module  # noqa: E402\n\nserver: FastMCP = server_module.server\n\nconsole: Console = Console()\n\n\nasync def main() -> None:\n    console.print()\n    console.print(\"[dim italic]Each line below is a separate tool call[/dim italic]\")\n    console.print()\n\n    # --- Alice's session ---\n    console.print(\"[dim]Alice connects[/dim]\")\n\n    async with Client(server) as alice:\n        result = await alice.call_tool(\"list_session_info\", {})\n        console.print(f\"  session [cyan]{result.data['session_id'][:8]}[/cyan]\")\n\n        await alice.call_tool(\"set_value\", {\"key\": \"user\", \"value\": \"Alice\"})\n        console.print(\"  set [white]user[/white] = [green]Alice[/green]\")\n\n        await alice.call_tool(\"set_value\", {\"key\": \"secret\", \"value\": \"alice-password\"})\n        console.print(\"  set [white]secret[/white] = [green]alice-password[/green]\")\n\n        await alice.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [green]Alice[/green]\")\n\n        await alice.call_tool(\"get_value\", {\"key\": \"secret\"})\n        console.print(\"  get [white]secret[/white] → [green]alice-password[/green]\")\n\n    console.print()\n\n    # --- Bob's session ---\n    console.print(\"[dim]Bob connects (different session)[/dim]\")\n\n    async with Client(server) as bob:\n        result = await bob.call_tool(\"list_session_info\", {})\n        console.print(f\"  session [cyan]{result.data['session_id'][:8]}[/cyan]\")\n\n        await bob.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [dim]not found[/dim]\")\n\n        await bob.call_tool(\"get_value\", {\"key\": \"secret\"})\n        console.print(\"  get [white]secret[/white] → [dim]not found[/dim]\")\n\n        await bob.call_tool(\"set_value\", {\"key\": \"user\", \"value\": \"Bob\"})\n        console.print(\"  set [white]user[/white] = [green]Bob[/green]\")\n\n        await bob.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [green]Bob[/green]\")\n\n    console.print()\n\n    # --- Alice reconnects ---\n    console.print(\"[dim]Alice reconnects (new session)[/dim]\")\n\n    async with Client(server) as alice_again:\n        result = await alice_again.call_tool(\"list_session_info\", {})\n        console.print(f\"  session [cyan]{result.data['session_id'][:8]}[/cyan]\")\n\n        await alice_again.call_tool(\"get_value\", {\"key\": \"user\"})\n        console.print(\"  get [white]user[/white] → [dim]not found[/dim]\")\n\n    console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/persistent_state/server.py",
    "content": "\"\"\"Example: Persistent session-scoped state.\n\nThis demonstrates using Context.get_state() and set_state() to store\ndata that persists across tool calls within the same MCP session.\n\nRun with:\n    uv run python examples/persistent_state/server.py\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\n\nserver = FastMCP(\"StateExample\")\n\n\n@server.tool\nasync def set_value(key: str, value: str, ctx: Context) -> str:\n    \"\"\"Store a value in session state.\"\"\"\n    await ctx.set_state(key, value)\n    return f\"Stored '{key}' = '{value}'\"\n\n\n@server.tool\nasync def get_value(key: str, ctx: Context) -> str:\n    \"\"\"Retrieve a value from session state.\"\"\"\n    value = await ctx.get_state(key)\n    if value is None:\n        return f\"Key '{key}' not found\"\n    return f\"'{key}' = '{value}'\"\n\n\n@server.tool\nasync def list_session_info(ctx: Context) -> dict[str, str | None]:\n    \"\"\"Get information about the current session.\"\"\"\n    return {\n        \"session_id\": ctx.session_id,\n        \"transport\": ctx.transport,\n    }\n\n\nif __name__ == \"__main__\":\n    server.run(transport=\"streamable-http\")\n"
  },
  {
    "path": "examples/prompts_as_tools/client.py",
    "content": "\"\"\"Example: Client using prompts-as-tools.\n\nThis client demonstrates calling the list_prompts and get_prompt tools\ngenerated by the PromptsAsTools transform.\n\nRun with:\n    uv run python examples/prompts_as_tools/client.py\n\"\"\"\n\nimport asyncio\nimport json\n\nfrom fastmcp.client import Client\n\n\nasync def main():\n    # Connect to the server\n    async with Client(\"examples/prompts_as_tools/server.py\") as client:\n        # List all available tools\n        print(\"=== Available Tools ===\")\n        tools = await client.list_tools()\n        for tool in tools:\n            print(f\"  - {tool.name}: {tool.description}\")\n        print()\n\n        # Use list_prompts tool to see what's available\n        print(\"=== Listing Prompts ===\")\n        result = await client.call_tool(\"list_prompts\", {})\n        prompts = json.loads(result.data)\n\n        for prompt in prompts:\n            print(f\"  {prompt['name']}\")\n            print(f\"    Description: {prompt.get('description', 'N/A')}\")\n            if prompt[\"arguments\"]:\n                print(\"    Arguments:\")\n                for arg in prompt[\"arguments\"]:\n                    required = \"required\" if arg[\"required\"] else \"optional\"\n                    print(\n                        f\"      - {arg['name']} ({required}): {arg.get('description', 'N/A')}\"\n                    )\n            print()\n\n        # Get a prompt without optional arguments\n        print(\"=== Getting Simple Prompt ===\")\n        result = await client.call_tool(\n            \"get_prompt\",\n            {\"name\": \"explain_concept\", \"arguments\": {\"concept\": \"recursion\"}},\n        )\n        response = json.loads(result.data)\n        print(\"Messages:\")\n        for msg in response[\"messages\"]:\n            print(f\"  Role: {msg['role']}\")\n            print(f\"  Content: {msg['content'][:100]}...\")\n        print()\n\n        # Get a prompt with optional arguments\n        print(\"=== Getting Prompt with Optional Arguments ===\")\n        result = await client.call_tool(\n            \"get_prompt\",\n            {\n                \"name\": \"analyze_code\",\n                \"arguments\": {\n                    \"code\": \"def factorial(n):\\n    return n * factorial(n-1)\",\n                    \"language\": \"python\",\n                    \"focus\": \"bugs\",\n                },\n            },\n        )\n        response = json.loads(result.data)\n        print(\"Messages:\")\n        for msg in response[\"messages\"]:\n            print(f\"  Role: {msg['role']}\")\n            print(f\"  Content: {msg['content'][:150]}...\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/prompts_as_tools/server.py",
    "content": "\"\"\"Example: Expose prompts as tools using PromptsAsTools transform.\n\nThis example shows how to use PromptsAsTools to make prompts accessible\nto clients that only support tools (not the prompts protocol).\n\nRun with:\n    uv run python examples/prompts_as_tools/server.py\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import PromptsAsTools\n\nmcp = FastMCP(\"Prompt Tools Demo\")\n\n\n# Simple prompt without arguments\n@mcp.prompt\ndef explain_concept(concept: str) -> str:\n    \"\"\"Explain a programming concept.\"\"\"\n    return f\"\"\"Please explain the following programming concept in simple terms:\n\n{concept}\n\nInclude:\n- A clear definition\n- Common use cases\n- A simple example\n\"\"\"\n\n\n# Prompt with multiple arguments\n@mcp.prompt\ndef analyze_code(code: str, language: str = \"python\", focus: str = \"all\") -> str:\n    \"\"\"Analyze code for potential issues.\"\"\"\n    return f\"\"\"Analyze this {language} code:\n\n```{language}\n{code}\n```\n\nFocus on: {focus}\n\nPlease identify:\n- Potential bugs or errors\n- Performance issues\n- Code style improvements\n- Security concerns\n\"\"\"\n\n\n# Prompt with required and optional arguments\n@mcp.prompt\ndef review_pull_request(\n    title: str, description: str, diff: str, guidelines: str = \"\"\n) -> str:\n    \"\"\"Review a pull request.\"\"\"\n    guidelines_section = (\n        f\"\\n\\nGuidelines to follow:\\n{guidelines}\" if guidelines else \"\"\n    )\n\n    return f\"\"\"Review this pull request:\n\n**Title:** {title}\n\n**Description:**\n{description}\n\n**Diff:**\n```\n{diff}\n```{guidelines_section}\n\nPlease provide:\n- Summary of changes\n- Potential issues or concerns\n- Suggestions for improvement\n- Overall recommendation (approve/request changes)\n\"\"\"\n\n\n# Add the transform - this creates list_prompts and get_prompt tools\nmcp.add_transform(PromptsAsTools(mcp))\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/providers/sqlite/README.md",
    "content": "# Dynamic Tools from SQLite\n\nThis example demonstrates serving MCP tools from a database. Tools can be added, modified, or disabled by updating the database - no server restart required.\n\n## Structure\n\n- `tools.db` - SQLite database with tool configurations (committed for convenience)\n- `setup_db.py` - Script to create/reset the database\n- `server.py` - MCP server that loads tools from the database\n\n## Usage\n\n```bash\n# Reset the database (optional - tools.db is pre-seeded)\nuv run examples/providers/sqlite/setup_db.py\n\n# Run the server\nuv run fastmcp run examples/providers/sqlite/server.py\n```\n\n## How It Works\n\nThe `SQLiteToolProvider` queries the database on every `list_tools` and `call_tool` request:\n\n```python\nclass SQLiteToolProvider(BaseToolProvider):\n    async def list_tools(self) -> list[Tool]:\n        # Query database for enabled tools\n        ...\n\n    async def get_tool(self, name: str) -> Tool | None:\n        # Efficient single-tool lookup\n        ...\n```\n\nTools are defined as `ConfigurableTool` subclasses that combine schema and execution:\n\n```python\nclass ConfigurableTool(Tool):\n    operation: str  # \"add\", \"multiply\", etc.\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        # Execute based on configured operation\n        ...\n```\n\n## Modifying Tools at Runtime\n\nWhile the server is running, you can modify tools in the database:\n\n```bash\n# Add a new tool\nsqlite3 examples/providers/sqlite/tools.db \"INSERT INTO tools VALUES ('subtract_numbers', 'Subtract two numbers', '{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"a\\\":{\\\"type\\\":\\\"number\\\"},\\\"b\\\":{\\\"type\\\":\\\"number\\\"}},\\\"required\\\":[\\\"a\\\",\\\"b\\\"]}', 'subtract', 0, 1)\"\n\n# Disable a tool\nsqlite3 examples/providers/sqlite/tools.db \"UPDATE tools SET enabled = 0 WHERE name = 'divide_numbers'\"\n```\n\nThe next `list_tools` or `call_tool` request will reflect these changes.\n"
  },
  {
    "path": "examples/providers/sqlite/server.py",
    "content": "# /// script\n# dependencies = [\"aiosqlite\", \"fastmcp\"]\n# ///\n\"\"\"\nMCP server with database-configured tools.\n\nTools are loaded from tools.db on each request, so you can add/modify/disable\ntools in the database without restarting the server.\n\nRun with: uv run fastmcp run examples/providers/sqlite/server.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nfrom collections.abc import Sequence\nfrom pathlib import Path\nfrom typing import Any\n\nimport aiosqlite\nfrom rich import print\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.providers import Provider\nfrom fastmcp.tools.tool import Tool, ToolResult\n\nDB_PATH = Path(__file__).parent / \"tools.db\"\n\n\nclass ConfigurableTool(Tool):\n    \"\"\"A tool that performs a configured arithmetic operation.\n\n    This demonstrates the pattern: Tool subclass = schema + execution in one place.\n    \"\"\"\n\n    operation: str  # \"add\", \"multiply\", \"subtract\", \"divide\"\n    default_value: float = 0\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        a = arguments.get(\"a\", self.default_value)\n        b = arguments.get(\"b\", self.default_value)\n\n        if self.operation == \"add\":\n            result = a + b\n        elif self.operation == \"multiply\":\n            result = a * b\n        elif self.operation == \"subtract\":\n            result = a - b\n        elif self.operation == \"divide\":\n            if b == 0:\n                return ToolResult(\n                    structured_content={\n                        \"error\": \"Division by zero\",\n                        \"operation\": self.operation,\n                    }\n                )\n            result = a / b\n        else:\n            result = a + b\n\n        return ToolResult(\n            structured_content={\"result\": result, \"operation\": self.operation}\n        )\n\n\nclass SQLiteToolProvider(Provider):\n    \"\"\"Queries SQLite for tool configurations.\n\n    Called on every list_tools/get_tool request, so database changes\n    are reflected immediately without server restart.\n    \"\"\"\n\n    def __init__(self, db_path: str):\n        super().__init__()\n        self.db_path = db_path\n\n    async def list_tools(self) -> Sequence[Tool]:\n        async with aiosqlite.connect(self.db_path) as db:\n            db.row_factory = aiosqlite.Row\n            async with db.execute(\"SELECT * FROM tools WHERE enabled = 1\") as cursor:\n                rows = await cursor.fetchall()\n                return [self._make_tool(row) for row in rows]\n\n    async def get_tool(self, name: str) -> Tool | None:\n        async with aiosqlite.connect(self.db_path) as db:\n            db.row_factory = aiosqlite.Row\n            async with db.execute(\n                \"SELECT * FROM tools WHERE name = ? AND enabled = 1\", (name,)\n            ) as cursor:\n                row = await cursor.fetchone()\n                return self._make_tool(row) if row else None\n\n    def _make_tool(self, row: aiosqlite.Row) -> ConfigurableTool:\n        return ConfigurableTool(\n            name=row[\"name\"],\n            description=row[\"description\"],\n            parameters=json.loads(row[\"parameters_schema\"]),\n            operation=row[\"operation\"],\n            default_value=row[\"default_value\"] or 0,\n        )\n\n\nprovider = SQLiteToolProvider(db_path=str(DB_PATH))\nmcp = FastMCP(\"DynamicToolsServer\", providers=[provider])\n\n\n@mcp.tool\ndef server_info() -> dict[str, str]:\n    \"\"\"Get information about this server (static tool).\"\"\"\n    return {\n        \"name\": \"DynamicToolsServer\",\n        \"description\": \"A server with database-configured tools\",\n        \"database\": str(DB_PATH),\n    }\n\n\nasync def main():\n    async with Client(mcp) as client:\n        tools = await client.list_tools()\n        print(f\"[bold]Available tools ({len(tools)}):[/bold]\")\n        for tool in tools:\n            print(f\"  • {tool.name}: {tool.description}\")\n\n        print()\n        print(\"[bold]Calling add_numbers(10, 5):[/bold]\")\n        result = await client.call_tool(\"add_numbers\", {\"a\": 10, \"b\": 5})\n        print(f\"  Result: {result.structured_content}\")\n\n        print()\n        print(\"[bold]Calling multiply_numbers(7, 6):[/bold]\")\n        result = await client.call_tool(\"multiply_numbers\", {\"a\": 7, \"b\": 6})\n        print(f\"  Result: {result.structured_content}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/providers/sqlite/setup_db.py",
    "content": "# /// script\n# dependencies = [\"aiosqlite\"]\n# ///\n\"\"\"\nCreates and seeds the tools database.\n\nRun with: uv run examples/providers/sqlite/setup_db.py\n\"\"\"\n\nimport asyncio\nimport json\nfrom pathlib import Path\n\nimport aiosqlite\n\nDB_PATH = Path(__file__).parent / \"tools.db\"\n\n\nasync def setup_database() -> None:\n    \"\"\"Create the tools table and seed with example tools.\"\"\"\n    async with aiosqlite.connect(DB_PATH) as db:\n        await db.execute(\"\"\"\n            CREATE TABLE IF NOT EXISTS tools (\n                name TEXT PRIMARY KEY,\n                description TEXT NOT NULL,\n                parameters_schema TEXT NOT NULL,\n                operation TEXT NOT NULL,\n                default_value REAL,\n                enabled INTEGER DEFAULT 1\n            )\n        \"\"\")\n\n        tools_data = [\n            (\n                \"add_numbers\",\n                \"Add two numbers together\",\n                json.dumps(\n                    {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"a\": {\"type\": \"number\", \"description\": \"First number\"},\n                            \"b\": {\"type\": \"number\", \"description\": \"Second number\"},\n                        },\n                        \"required\": [\"a\", \"b\"],\n                    }\n                ),\n                \"add\",\n                0,\n                1,\n            ),\n            (\n                \"multiply_numbers\",\n                \"Multiply two numbers\",\n                json.dumps(\n                    {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"a\": {\"type\": \"number\", \"description\": \"First number\"},\n                            \"b\": {\"type\": \"number\", \"description\": \"Second number\"},\n                        },\n                        \"required\": [\"a\", \"b\"],\n                    }\n                ),\n                \"multiply\",\n                1,\n                1,\n            ),\n            (\n                \"divide_numbers\",\n                \"Divide two numbers\",\n                json.dumps(\n                    {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"a\": {\"type\": \"number\", \"description\": \"Dividend\"},\n                            \"b\": {\"type\": \"number\", \"description\": \"Divisor\"},\n                        },\n                        \"required\": [\"a\", \"b\"],\n                    }\n                ),\n                \"divide\",\n                0,\n                1,\n            ),\n        ]\n\n        await db.executemany(\n            \"\"\"\n            INSERT OR REPLACE INTO tools\n            (name, description, parameters_schema, operation, default_value, enabled)\n            VALUES (?, ?, ?, ?, ?, ?)\n            \"\"\",\n            tools_data,\n        )\n        await db.commit()\n\n    print(f\"Database created at: {DB_PATH}\")\n    print(\"Seeded 3 tools: add_numbers, multiply_numbers, divide_numbers\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(setup_database())\n"
  },
  {
    "path": "examples/resources_as_tools/client.py",
    "content": "\"\"\"Example: Client using resources-as-tools.\n\nThis client demonstrates calling the list_resources and read_resource tools\ngenerated by the ResourcesAsTools transform.\n\nRun with:\n    uv run python examples/resources_as_tools/client.py\n\"\"\"\n\nimport asyncio\nimport json\n\nfrom fastmcp.client import Client\n\n\nasync def main():\n    # Connect to the server\n    async with Client(\"examples/resources_as_tools/server.py\") as client:\n        # List all available tools\n        print(\"=== Available Tools ===\")\n        tools = await client.list_tools()\n        for tool in tools:\n            print(f\"  - {tool.name}: {tool.description}\")\n        print()\n\n        # Use list_resources tool to see what's available\n        print(\"=== Listing Resources ===\")\n        result = await client.call_tool(\"list_resources\", {})\n        resources = json.loads(result.data)\n        for resource in resources:\n            if \"uri\" in resource:\n                print(f\"  Static: {resource['uri']}\")\n            else:\n                print(f\"  Template: {resource['uri_template']}\")\n            print(f\"    Name: {resource['name']}\")\n            print(f\"    Description: {resource.get('description', 'N/A')}\")\n            print()\n\n        # Read a static resource\n        print(\"=== Reading Static Resource ===\")\n        result = await client.call_tool(\"read_resource\", {\"uri\": \"config://app\"})\n        print(f\"config://app content:\\n{result.data}\")\n        print()\n\n        # Read a templated resource\n        print(\"=== Reading Templated Resource ===\")\n        result = await client.call_tool(\"read_resource\", {\"uri\": \"user://42/profile\"})\n        print(f\"user://42/profile content:\\n{result.data}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/resources_as_tools/server.py",
    "content": "\"\"\"Example: Expose resources as tools using ResourcesAsTools transform.\n\nThis example shows how to use ResourcesAsTools to make resources accessible\nto clients that only support tools (not the resources protocol).\n\nRun with:\n    uv run python examples/resources_as_tools/server.py\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import ResourcesAsTools\n\nmcp = FastMCP(\"Resource Tools Demo\")\n\n\n# Static resource - has a fixed URI\n@mcp.resource(\"config://app\")\ndef app_config() -> str:\n    \"\"\"Application configuration.\"\"\"\n    return \"\"\"\n    {\n        \"app_name\": \"My App\",\n        \"version\": \"1.0.0\",\n        \"debug\": false\n    }\n    \"\"\"\n\n\n# Another static resource\n@mcp.resource(\"readme://main\")\ndef readme() -> str:\n    \"\"\"Project README.\"\"\"\n    return \"\"\"\n    # My Project\n\n    This is an example project demonstrating ResourcesAsTools.\n    \"\"\"\n\n\n# Resource template - URI has placeholders\n@mcp.resource(\"user://{user_id}/profile\")\ndef user_profile(user_id: str) -> str:\n    \"\"\"Get a user's profile by ID.\"\"\"\n    return f\"\"\"\n    {{\n        \"user_id\": \"{user_id}\",\n        \"name\": \"User {user_id}\",\n        \"email\": \"user{user_id}@example.com\"\n    }}\n    \"\"\"\n\n\n# Another template with multiple parameters\n@mcp.resource(\"file://{directory}/{filename}\")\ndef read_file(directory: str, filename: str) -> str:\n    \"\"\"Read a file from a directory.\"\"\"\n    return f\"Contents of {directory}/{filename}\"\n\n\n# Add the transform - this creates list_resources and read_resource tools\nmcp.add_transform(ResourcesAsTools(mcp))\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/run_with_tracing.py",
    "content": "#!/usr/bin/env python\n\"\"\"Run a FastMCP server with OpenTelemetry tracing enabled.\n\nUsage:\n    uv run examples/run_with_tracing.py examples/echo.py --transport sse --port 8001\n\nAll arguments after the script name are passed to `fastmcp run`.\nTraces are exported via OTLP to localhost:4317.\n\nTo view traces, run otel-desktop-viewer in another terminal:\n    otel-desktop-viewer\n    # Trace UI at http://localhost:8000, OTLP receiver on :4317\n\nInstall otel-desktop-viewer:\n    brew install nico-barbas/brew/otel-desktop-viewer\n\"\"\"\n\nimport os\nimport sys\n\n\ndef main() -> None:\n    if len(sys.argv) < 2:\n        print(__doc__)\n        sys.exit(1)\n\n    # Configure OTEL SDK before importing fastmcp\n    from opentelemetry import trace\n    from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter\n    from opentelemetry.sdk.resources import Resource\n    from opentelemetry.sdk.trace import TracerProvider\n    from opentelemetry.sdk.trace.export import BatchSpanProcessor\n\n    endpoint = os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"http://localhost:4317\")\n    service_name = os.environ.get(\n        \"OTEL_SERVICE_NAME\",\n        f\"fastmcp-{os.path.basename(sys.argv[1]).replace('.py', '')}\",\n    )\n\n    # Set up tracer provider with OTLP exporter\n    resource = Resource.create({\"service.name\": service_name})\n    provider = TracerProvider(resource=resource)\n    exporter = OTLPSpanExporter(endpoint=endpoint, insecure=True)\n    provider.add_span_processor(BatchSpanProcessor(exporter))\n    trace.set_tracer_provider(provider)\n\n    print(f\"Tracing enabled → OTLP {endpoint}\", flush=True)\n    print(f\"Service: {service_name}\", flush=True)\n    print(\"View traces: otel-desktop-viewer (http://localhost:8000)\\n\", flush=True)\n\n    # Now run fastmcp CLI\n    from fastmcp.cli.cli import app\n\n    sys.argv = [\"fastmcp\", \"run\", *sys.argv[1:]]\n    app()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/sampling/README.md",
    "content": "# Sampling Examples\n\nThese examples demonstrate FastMCP's sampling API, which allows server tools to request LLM completions from the client.\n\n## Prerequisites\n\n```bash\npip install fastmcp[anthropic]\nexport ANTHROPIC_API_KEY=your-key\n```\n\nOr run directly with `uv`:\n\n```bash\nuv run examples/sampling/text.py\n```\n\n## Examples\n\n### Simple Text Sampling (`text.py`)\n\nBasic sampling flow where a server tool requests an LLM completion:\n\n```bash\nuv run examples/sampling/text.py\n```\n\n### Structured Output (`structured_output.py`)\n\nUses `result_type` to get validated Pydantic models from the LLM:\n\n```bash\nuv run examples/sampling/structured_output.py\n```\n\n### Tool Use (`tool_use.py`)\n\nGives the LLM tools to use during sampling (calculator, time, dice):\n\n```bash\nuv run examples/sampling/tool_use.py\n```\n\n### Server Fallback (`server_fallback.py`)\n\nConfigures a fallback sampling handler on the server, enabling sampling even when clients don't support it:\n\n```bash\nuv run examples/sampling/server_fallback.py\n```\n\n## Using OpenAI Instead\n\nTo use OpenAI instead of Anthropic, change the handler:\n\n```python\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\nhandler = OpenAISamplingHandler(default_model=\"gpt-4o-mini\")\n```\n\nAnd install with `pip install fastmcp[openai]`.\n"
  },
  {
    "path": "examples/sampling/server_fallback.py",
    "content": "# /// script\n# dependencies = [\"anthropic\", \"fastmcp\", \"rich\"]\n# ///\n\"\"\"\nServer-Side Fallback Handler\n\nDemonstrates configuring a sampling handler on the server. This ensures\nsampling works even when the client doesn't provide a handler.\n\nThe server runs as an HTTP server that can be connected to by any MCP client.\n\nRun:\n    uv run examples/sampling/server_fallback.py\n\nThen connect with any MCP client (e.g., Claude Desktop) or test with:\n    curl http://localhost:8000/mcp/\n\"\"\"\n\nimport asyncio\n\nfrom rich.console import Console\nfrom rich.panel import Panel\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\nfrom fastmcp.server.context import Context\n\nconsole = Console()\n\n\n# Create server with a fallback sampling handler\n# This handler is used when the client doesn't support sampling\nmcp = FastMCP(\n    \"Server with Fallback Handler\",\n    sampling_handler=AnthropicSamplingHandler(default_model=\"claude-sonnet-4-5\"),\n    sampling_handler_behavior=\"fallback\",  # Use only if client lacks sampling\n)\n\n\n@mcp.tool\nasync def summarize(text: str, ctx: Context) -> str:\n    \"\"\"Summarize the given text.\"\"\"\n    console.print(f\"[bold cyan]SERVER[/] Summarizing text ({len(text)} chars)...\")\n\n    result = await ctx.sample(\n        messages=f\"Summarize this text in 1-2 sentences:\\n\\n{text}\",\n        system_prompt=\"You are a concise summarizer.\",\n        max_tokens=150,\n    )\n\n    console.print(\"[bold cyan]SERVER[/] Summary complete\")\n    return result.text or \"\"\n\n\n@mcp.tool\nasync def translate(text: str, target_language: str, ctx: Context) -> str:\n    \"\"\"Translate text to the target language.\"\"\"\n    console.print(f\"[bold cyan]SERVER[/] Translating to {target_language}...\")\n\n    result = await ctx.sample(\n        messages=f\"Translate to {target_language}:\\n\\n{text}\",\n        system_prompt=f\"You are a translator. Output only the {target_language} translation.\",\n        max_tokens=500,\n    )\n\n    console.print(\"[bold cyan]SERVER[/] Translation complete\")\n    return result.text or \"\"\n\n\nasync def main():\n    console.print(\n        Panel.fit(\n            \"[bold]Server-Side Fallback Handler Demo[/]\\n\\n\"\n            \"This server has a built-in Anthropic handler that activates\\n\"\n            \"when clients don't provide their own sampling support.\",\n            subtitle=\"server_fallback.py\",\n        )\n    )\n    console.print()\n    console.print(\"[bold yellow]Starting HTTP server on http://localhost:8000[/]\")\n    console.print(\"Connect with an MCP client or press Ctrl+C to stop\")\n    console.print()\n\n    await mcp.run_http_async(host=\"localhost\", port=8000)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/sampling/structured_output.py",
    "content": "# /// script\n# dependencies = [\"anthropic\", \"fastmcp\", \"rich\"]\n# ///\n\"\"\"\nStructured Output Sampling\n\nDemonstrates using `result_type` to get validated Pydantic models from an LLM.\nThe server exposes a sentiment analysis tool that returns structured data.\n\nRun:\n    uv run examples/sampling/structured_output.py\n\"\"\"\n\nimport asyncio\n\nfrom pydantic import BaseModel\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.table import Table\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.sampling import SamplingMessage, SamplingParams\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\nconsole = Console()\n\n\nclass LoggingAnthropicHandler(AnthropicSamplingHandler):\n    async def __call__(\n        self, messages: list[SamplingMessage], params: SamplingParams, context\n    ):  # type: ignore[override]\n        console.print(\"      [bold blue]SAMPLING[/] Calling Claude API...\")\n        result = await super().__call__(messages, params, context)\n        console.print(\"      [bold blue]SAMPLING[/] Response received\")\n        return result\n\n\n# Define a structured output model\nclass SentimentAnalysis(BaseModel):\n    sentiment: str  # \"positive\", \"negative\", or \"neutral\"\n    confidence: float  # 0.0 to 1.0\n    keywords: list[str]  # Keywords that influenced the analysis\n    explanation: str  # Brief explanation of the analysis\n\n\n# Create the MCP server\nmcp = FastMCP(\"Sentiment Analyzer\")\n\n\n@mcp.tool\nasync def analyze_sentiment(text: str, ctx: Context) -> dict:\n    \"\"\"Analyze the sentiment of the given text.\"\"\"\n    console.print(\"    [bold cyan]SERVER[/] Analyzing sentiment...\")\n\n    result = await ctx.sample(\n        messages=f\"Analyze the sentiment of this text:\\n\\n{text}\",\n        system_prompt=\"You are a sentiment analysis expert. Analyze text carefully.\",\n        result_type=SentimentAnalysis,\n    )\n\n    console.print(\"    [bold cyan]SERVER[/] Analysis complete\")\n    return result.result.model_dump()  # type: ignore[attr-defined]\n\n\nasync def main():\n    console.print(\n        Panel.fit(\"[bold]MCP Sampling Flow Demo[/]\", subtitle=\"structured_output.py\")\n    )\n    console.print()\n\n    handler = LoggingAnthropicHandler(default_model=\"claude-sonnet-4-5\")\n\n    async with Client(mcp, sampling_handler=handler) as client:\n        texts = [\n            \"I absolutely love this product! It exceeded all my expectations.\",\n            \"The service was okay, nothing special but got the job done.\",\n            \"This is the worst experience I've ever had. Never again.\",\n        ]\n\n        for text in texts:\n            console.print(f\"[bold green]CLIENT[/] Analyzing: [italic]{text[:50]}...[/]\")\n            console.print()\n\n            result = await client.call_tool(\"analyze_sentiment\", {\"text\": text})\n            data = result.data\n\n            # Display results in a table\n            table = Table(show_header=False, box=None, padding=(0, 2))\n            table.add_column(style=\"bold\")\n            table.add_column()\n\n            sentiment_color = {\n                \"positive\": \"green\",\n                \"negative\": \"red\",\n                \"neutral\": \"yellow\",\n            }.get(\n                data[\"sentiment\"],\n                \"white\",  # type: ignore[union-attr]\n            )\n            table.add_row(\"Sentiment\", f\"[{sentiment_color}]{data['sentiment']}[/]\")  # type: ignore[index]\n            table.add_row(\"Confidence\", f\"{data['confidence']:.0%}\")  # type: ignore[index]\n            table.add_row(\"Keywords\", \", \".join(data[\"keywords\"]))  # type: ignore[index]\n            table.add_row(\"Explanation\", data[\"explanation\"])  # type: ignore[index]\n\n            console.print(Panel(table, border_style=sentiment_color))\n            console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/sampling/text.py",
    "content": "# /// script\n# dependencies = [\"anthropic\", \"fastmcp\", \"rich\"]\n# ///\n\"\"\"\nSimple Text Sampling\n\nDemonstrates the basic MCP sampling flow where a server tool requests\nan LLM completion from the client.\n\nRun:\n    uv run examples/sampling/text.py\n\"\"\"\n\nimport asyncio\n\nfrom rich.console import Console\nfrom rich.panel import Panel\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.sampling import SamplingMessage, SamplingParams\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\nconsole = Console()\n\n\n# Create a wrapper handler that logs when the LLM is called\nclass LoggingAnthropicHandler(AnthropicSamplingHandler):\n    async def __call__(\n        self, messages: list[SamplingMessage], params: SamplingParams, context\n    ):  # type: ignore[override]\n        console.print(\"      [bold blue]SAMPLING[/] Calling Claude API...\")\n        result = await super().__call__(messages, params, context)\n        console.print(\"      [bold blue]SAMPLING[/] Response received\")\n        return result\n\n\n# Create the MCP server\nmcp = FastMCP(\"Haiku Generator\")\n\n\n@mcp.tool\nasync def write_haiku(topic: str, ctx: Context) -> str:\n    \"\"\"Write a haiku about any topic.\"\"\"\n    console.print(\n        f\"    [bold cyan]SERVER[/] Tool 'write_haiku' called with topic: {topic}\"\n    )\n\n    result = await ctx.sample(\n        messages=f\"Write a haiku about: {topic}\",\n        system_prompt=\"You are a poet. Write only the haiku, nothing else.\",\n        max_tokens=100,\n    )\n\n    console.print(\"    [bold cyan]SERVER[/] Returning haiku to client\")\n    return result.text or \"\"\n\n\nasync def main():\n    console.print(Panel.fit(\"[bold]MCP Sampling Flow Demo[/]\", subtitle=\"text.py\"))\n    console.print()\n\n    # Create the sampling handler\n    handler = LoggingAnthropicHandler(default_model=\"claude-sonnet-4-5\")\n\n    # Connect client to server with the sampling handler\n    async with Client(mcp, sampling_handler=handler) as client:\n        console.print(\"[bold green]CLIENT[/] Calling tool 'write_haiku'...\")\n        console.print()\n\n        result = await client.call_tool(\"write_haiku\", {\"topic\": \"Python programming\"})\n\n        console.print()\n        console.print(\"[bold green]CLIENT[/] Received result:\")\n        console.print(Panel(result.data, title=\"Haiku\", border_style=\"green\"))  # type: ignore[arg-type]\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/sampling/tool_use.py",
    "content": "# /// script\n# dependencies = [\"anthropic\", \"fastmcp\", \"rich\"]\n# ///\n\"\"\"\nSampling with Tools\n\nDemonstrates giving an LLM tools to use during sampling. The LLM can call\nhelper functions to gather information before responding.\n\nRun:\n    uv run examples/sampling/tool_use.py\n\"\"\"\n\nimport asyncio\nimport random\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field\nfrom rich.console import Console\nfrom rich.panel import Panel\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.sampling import SamplingMessage, SamplingParams\nfrom fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\nconsole = Console()\n\n\nclass LoggingAnthropicHandler(AnthropicSamplingHandler):\n    async def __call__(\n        self, messages: list[SamplingMessage], params: SamplingParams, context\n    ):  # type: ignore[override]\n        console.print(\"      [bold blue]SAMPLING[/] Calling Claude API...\")\n        result = await super().__call__(messages, params, context)\n        console.print(\"      [bold blue]SAMPLING[/] Response received\")\n        return result\n\n\n# Define tools available to the LLM during sampling\ndef add(a: float, b: float) -> str:\n    \"\"\"Add two numbers together.\"\"\"\n    result = a + b\n    console.print(f\"        [bold magenta]TOOL[/] add({a}, {b}) = {result}\")\n    return str(result)\n\n\ndef multiply(a: float, b: float) -> str:\n    \"\"\"Multiply two numbers together.\"\"\"\n    result = a * b\n    console.print(f\"        [bold magenta]TOOL[/] multiply({a}, {b}) = {result}\")\n    return str(result)\n\n\ndef get_current_time() -> str:\n    \"\"\"Get the current date and time.\"\"\"\n    console.print(\"        [bold magenta]TOOL[/] get_current_time()\")\n    return datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n\n\ndef roll_dice(sides: int = 6) -> str:\n    \"\"\"Roll a die with the specified number of sides.\"\"\"\n    result = random.randint(1, sides)\n    console.print(f\"        [bold magenta]TOOL[/] roll_dice({sides}) = {result}\")\n    return str(result)\n\n\n# Structured output for the response\nclass AssistantResponse(BaseModel):\n    answer: str = Field(description=\"The answer to the user's question\")\n    tools_used: list[str] = Field(description=\"List of tools that were used\")\n    reasoning: str = Field(\n        description=\"Brief explanation of how the answer was determined\"\n    )\n\n\n# Create the MCP server\nmcp = FastMCP(\"Smart Assistant\")\n\n\n@mcp.tool\nasync def ask_assistant(question: str, ctx: Context) -> dict:\n    \"\"\"Ask the assistant a question. It can use tools to help answer.\"\"\"\n    console.print(\"    [bold cyan]SERVER[/] Processing question...\")\n\n    result = await ctx.sample(\n        messages=question,\n        system_prompt=\"You are a helpful assistant with access to tools. Use them when needed to answer questions accurately.\",\n        tools=[add, multiply, get_current_time, roll_dice],\n        result_type=AssistantResponse,\n    )\n\n    console.print(\"    [bold cyan]SERVER[/] Response ready\")\n    return result.result.model_dump()  # type: ignore[attr-defined]\n\n\nasync def main():\n    console.print(Panel.fit(\"[bold]MCP Sampling Flow Demo[/]\", subtitle=\"tool_use.py\"))\n    console.print()\n\n    handler = LoggingAnthropicHandler(default_model=\"claude-sonnet-4-5\")\n\n    async with Client(mcp, sampling_handler=handler) as client:\n        questions = [\n            \"What is 15 times 7, plus 23?\",\n            \"Roll a 20-sided dice for me\",\n            \"What time is it right now?\",\n        ]\n\n        for question in questions:\n            console.print(f\"[bold green]CLIENT[/] Question: {question}\")\n            console.print()\n\n            result = await client.call_tool(\"ask_assistant\", {\"question\": question})\n            data = result.data\n\n            console.print(f\"[bold green]CLIENT[/] Answer: {data['answer']}\")  # type: ignore[index]\n            console.print(\n                f\"         Tools used: {', '.join(data['tools_used']) or 'none'}\"\n            )  # type: ignore[index]\n            console.print(f\"         Reasoning: {data['reasoning']}\")  # type: ignore[index]\n            console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/screenshot.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"entrypoint\": \"screenshot.py\",\n  \"environment\": {\n    \"dependencies\": [\"pyautogui\", \"Pillow\"]\n  }\n}\n"
  },
  {
    "path": "examples/screenshot.py",
    "content": "# /// script\n# dependencies = [\"pyautogui\", \"Pillow\", \"fastmcp\"]\n# ///\n\n\"\"\"\nFastMCP Screenshot Example\n\nGive Claude a tool to capture and view screenshots.\n\"\"\"\n\nimport io\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.types import Image\n\n# Create server\n# Dependencies are configured in screenshot.fastmcp.json\nmcp = FastMCP(\"Screenshot Demo\")\n\n\n@mcp.tool\ndef take_screenshot() -> Image:\n    \"\"\"\n    Take a screenshot of the user's screen and return it as an image. Use\n    this tool anytime the user wants you to look at something they're doing.\n    \"\"\"\n    import pyautogui\n\n    buffer = io.BytesIO()\n\n    # if the file exceeds ~1MB, it will be rejected by Claude\n    screenshot = pyautogui.screenshot()\n    screenshot.convert(\"RGB\").save(buffer, format=\"JPEG\", quality=60, optimize=True)\n    return Image(data=buffer.getvalue(), format=\"jpeg\")\n"
  },
  {
    "path": "examples/search/README.md",
    "content": "# Search Transforms\n\nWhen a server exposes many tools, listing them all at once can overwhelm an LLM's context window. Search transforms collapse the full tool catalog behind a search interface — clients see only `search_tools` and `call_tool`, and discover the real tools on demand.\n\n## Two search strategies\n\n**Regex** (`RegexSearchTransform`) — clients search with regex patterns like `add|multiply` or `text.*`. Fast and precise when you know what you're looking for.\n\n**BM25** (`BM25SearchTransform`) — clients search with natural language like `\"work with numbers\"`. Results are ranked by relevance using BM25 scoring. The index rebuilds automatically when tools change.\n\nBoth strategies respect the full auth pipeline: middleware, visibility transforms, and component-level auth checks all apply to search results.\n\n## Run\n\n```bash\n# Regex\nuv run python examples/search/client_regex.py\n\n# BM25\nuv run python examples/search/client_bm25.py\n```\n"
  },
  {
    "path": "examples/search/client_bm25.py",
    "content": "\"\"\"Example: Client using BM25 search to discover and call tools.\n\nBM25 search accepts natural language queries instead of regex patterns.\nThis client shows how relevance ranking surfaces the best matches.\n\nRun with:\n    uv run python examples/search/client_bm25.py\n\"\"\"\n\nimport asyncio\nimport json\nfrom typing import Any\n\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.table import Table\n\nfrom fastmcp.client import Client\n\nconsole = Console()\n\n\ndef _get_result(result) -> Any:\n    \"\"\"Extract the value from a CallToolResult (structured or text).\"\"\"\n    if result.structured_content is not None:\n        data = result.structured_content\n        if isinstance(data, dict) and set(data) == {\"result\"}:\n            return data[\"result\"]\n        return data\n    return result.content[0].text\n\n\ndef _format_params(tool: dict) -> str:\n    \"\"\"Format inputSchema properties as a compact signature.\"\"\"\n    schema = tool.get(\"inputSchema\", {})\n    props = schema.get(\"properties\", {})\n    if not props:\n        return \"()\"\n    parts = []\n    for name, info in props.items():\n        typ = info.get(\"type\", \"\")\n        parts.append(f\"{name}: {typ}\" if typ else name)\n    return f\"({', '.join(parts)})\"\n\n\ndef _tool_table(\n    tools: list[dict], *, ranked: bool = False, show_params: bool = False\n) -> Table:\n    table = Table(show_header=True, show_edge=False, pad_edge=False, expand=True)\n    if ranked:\n        table.add_column(\"#\", style=\"dim\", width=3, justify=\"right\")\n    table.add_column(\"Tool\", style=\"cyan\", no_wrap=True)\n    if show_params:\n        table.add_column(\"Parameters\", style=\"dim\", no_wrap=True)\n    table.add_column(\"Description\", style=\"dim\")\n    for i, tool in enumerate(tools, 1):\n        row = [tool[\"name\"]]\n        if show_params:\n            row.append(_format_params(tool))\n        row.append(tool.get(\"description\", \"\"))\n        if ranked:\n            row.insert(0, str(i))\n        table.add_row(*row)\n    return table\n\n\nasync def main():\n    async with Client(\"examples/search/server_bm25.py\") as client:\n        console.print()\n        console.rule(\"[bold]BM25 Search Transform[/bold]\")\n        console.print()\n\n        # Step 1: list_tools shows only synthetic tools + pinned tools\n        console.print(\n            \"The server has 8 tools. BM25SearchTransform replaces them with \"\n            \"just [bold]search_tools[/bold] and [bold]call_tool[/bold]. \"\n            \"[bold]list_files[/bold] stays visible via [dim]always_visible[/dim]:\"\n        )\n        console.print()\n        tools = await client.list_tools()\n        visible = [{\"name\": t.name, \"description\": t.description} for t in tools]\n        console.print(\n            Panel(\n                _tool_table(visible),\n                title=\"[bold]list_tools()[/bold]\",\n                title_align=\"left\",\n                border_style=\"blue\",\n            )\n        )\n        console.print()\n\n        # Step 2: natural language search discovers tools by relevance\n        console.print(\n            \"The LLM uses [bold]search_tools[/bold] with natural language \"\n            \"to discover tools ranked by relevance:\"\n        )\n        console.print()\n        result = await client.call_tool(\"search_tools\", {\"query\": \"work with numbers\"})\n        found = _get_result(result)\n        if isinstance(found, str):\n            found = json.loads(found)\n        console.print(\n            Panel(\n                _tool_table(found, ranked=True, show_params=True),\n                title='[bold]search_tools[/bold]  [dim]query=\"work with numbers\"[/dim]',\n                title_align=\"left\",\n                border_style=\"green\",\n            )\n        )\n        console.print()\n\n        result = await client.call_tool(\n            \"search_tools\", {\"query\": \"manipulate text strings\"}\n        )\n        found = _get_result(result)\n        if isinstance(found, str):\n            found = json.loads(found)\n        console.print(\n            Panel(\n                _tool_table(found, ranked=True, show_params=True),\n                title='[bold]search_tools[/bold]  [dim]query=\"manipulate text strings\"[/dim]',\n                title_align=\"left\",\n                border_style=\"green\",\n            )\n        )\n        console.print()\n\n        # Step 3: call a discovered tool\n        console.print(\n            \"Then the LLM calls a discovered tool through [bold]call_tool[/bold]:\"\n        )\n        console.print()\n        result = await client.call_tool(\n            \"call_tool\",\n            {\n                \"name\": \"word_count\",\n                \"arguments\": {\"text\": \"BM25 search makes tool discovery easy\"},\n            },\n        )\n        console.print(\n            Panel(\n                f'call_tool(name=\"word_count\", arguments={{\"text\": \"BM25 search makes tool discovery easy\"}})\\n→ [bold green]{_get_result(result)}[/bold green]',\n                title=\"[bold]call_tool()[/bold]\",\n                title_align=\"left\",\n                border_style=\"magenta\",\n            )\n        )\n        console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/search/client_regex.py",
    "content": "\"\"\"Example: Client using regex search to discover and call tools.\n\nRegex search lets clients find tools by matching patterns against tool names\nand descriptions. Precise when you know what you're looking for.\n\nRun with:\n    uv run python examples/search/client_regex.py\n\"\"\"\n\nimport asyncio\nimport json\nfrom typing import Any\n\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.table import Table\n\nfrom fastmcp.client import Client\n\nconsole = Console()\n\n\ndef _get_result(result) -> Any:\n    \"\"\"Extract the value from a CallToolResult (structured or text).\"\"\"\n    if result.structured_content is not None:\n        data = result.structured_content\n        if isinstance(data, dict) and set(data) == {\"result\"}:\n            return data[\"result\"]\n        return data\n    return result.content[0].text\n\n\ndef _format_params(tool: dict) -> str:\n    \"\"\"Format inputSchema properties as a compact signature.\"\"\"\n    schema = tool.get(\"inputSchema\", {})\n    props = schema.get(\"properties\", {})\n    if not props:\n        return \"()\"\n    parts = []\n    for name, info in props.items():\n        typ = info.get(\"type\", \"\")\n        parts.append(f\"{name}: {typ}\" if typ else name)\n    return f\"({', '.join(parts)})\"\n\n\ndef _tool_table(\n    tools: list[dict], *, ranked: bool = False, show_params: bool = False\n) -> Table:\n    table = Table(show_header=True, show_edge=False, pad_edge=False, expand=True)\n    if ranked:\n        table.add_column(\"#\", style=\"dim\", width=3, justify=\"right\")\n    table.add_column(\"Tool\", style=\"cyan\", no_wrap=True)\n    if show_params:\n        table.add_column(\"Parameters\", style=\"dim\", no_wrap=True)\n    table.add_column(\"Description\", style=\"dim\")\n    for i, tool in enumerate(tools, 1):\n        row = [tool[\"name\"]]\n        if show_params:\n            row.append(_format_params(tool))\n        row.append(tool.get(\"description\", \"\"))\n        if ranked:\n            row.insert(0, str(i))\n        table.add_row(*row)\n    return table\n\n\nasync def main():\n    async with Client(\"examples/search/server_regex.py\") as client:\n        console.print()\n        console.rule(\"[bold]Regex Search Transform[/bold]\")\n        console.print()\n\n        # Step 1: list_tools shows only synthetic tools\n        console.print(\n            \"The server has 6 tools. RegexSearchTransform replaces them with \"\n            \"just [bold]search_tools[/bold] and [bold]call_tool[/bold]:\"\n        )\n        console.print()\n        tools = await client.list_tools()\n        visible = [{\"name\": t.name, \"description\": t.description} for t in tools]\n        console.print(\n            Panel(\n                _tool_table(visible),\n                title=\"[bold]list_tools()[/bold]\",\n                title_align=\"left\",\n                border_style=\"blue\",\n            )\n        )\n        console.print()\n\n        # Step 2: regex patterns discover tools\n        console.print(\n            \"The LLM uses [bold]search_tools[/bold] with regex patterns \"\n            \"to find tools by name:\"\n        )\n        console.print()\n        result = await client.call_tool(\n            \"search_tools\", {\"pattern\": \"add|multiply|fibonacci\"}\n        )\n        found = _get_result(result)\n        if isinstance(found, str):\n            found = json.loads(found)\n        console.print(\n            Panel(\n                _tool_table(found, show_params=True),\n                title='[bold]search_tools[/bold]  [dim]pattern=\"add|multiply|fibonacci\"[/dim]',\n                title_align=\"left\",\n                border_style=\"green\",\n            )\n        )\n        console.print()\n\n        result = await client.call_tool(\"search_tools\", {\"pattern\": \"text|string|word\"})\n        found = _get_result(result)\n        if isinstance(found, str):\n            found = json.loads(found)\n        console.print(\n            Panel(\n                _tool_table(found, show_params=True),\n                title='[bold]search_tools[/bold]  [dim]pattern=\"text|string|word\"[/dim]',\n                title_align=\"left\",\n                border_style=\"green\",\n            )\n        )\n        console.print()\n\n        # Step 3: call discovered tools\n        console.print(\n            \"Then the LLM calls discovered tools through [bold]call_tool[/bold]:\"\n        )\n        console.print()\n        result = await client.call_tool(\n            \"call_tool\", {\"name\": \"add\", \"arguments\": {\"a\": 17, \"b\": 25}}\n        )\n        console.print(\n            Panel(\n                f'call_tool(name=\"add\", arguments={{\"a\": 17, \"b\": 25}})\\n→ [bold green]{_get_result(result)}[/bold green]',\n                title=\"[bold]call_tool()[/bold]\",\n                title_align=\"left\",\n                border_style=\"magenta\",\n            )\n        )\n        console.print()\n\n        result = await client.call_tool(\n            \"call_tool\",\n            {\"name\": \"reverse_string\", \"arguments\": {\"text\": \"hello world\"}},\n        )\n        console.print(\n            Panel(\n                f'call_tool(name=\"reverse_string\", arguments={{\"text\": \"hello world\"}})\\n→ [bold green]{_get_result(result)}[/bold green]',\n                title=\"[bold]call_tool()[/bold]\",\n                title_align=\"left\",\n                border_style=\"magenta\",\n            )\n        )\n        console.print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/search/server_bm25.py",
    "content": "\"\"\"Example: Search transforms with BM25 relevance ranking.\n\nBM25SearchTransform uses term-frequency/inverse-document-frequency scoring\nto rank tools by relevance to a natural language query. Unlike regex search\n(which requires the user to construct a pattern), BM25 handles queries like\n\"work with text\" or \"do math\" and returns the most relevant matches.\n\nThe index is built lazily and rebuilt automatically when the tool catalog\nchanges (e.g. tools added or removed between requests).\n\nRun with:\n    uv run python examples/search/server_bm25.py\n\"\"\"\n\nimport os\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms.search import BM25SearchTransform\n\nmcp = FastMCP(\"BM25 Search Demo\")\n\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.tool\ndef multiply(x: float, y: float) -> float:\n    \"\"\"Multiply two numbers.\"\"\"\n    return x * y\n\n\n@mcp.tool\ndef fibonacci(n: int) -> list[int]:\n    \"\"\"Generate the first n Fibonacci numbers.\"\"\"\n    if n <= 0:\n        return []\n    seq = [0, 1]\n    while len(seq) < n:\n        seq.append(seq[-1] + seq[-2])\n    return seq[:n]\n\n\n@mcp.tool\ndef reverse_string(text: str) -> str:\n    \"\"\"Reverse a string.\"\"\"\n    return text[::-1]\n\n\n@mcp.tool\ndef word_count(text: str) -> int:\n    \"\"\"Count the number of words in a text.\"\"\"\n    return len(text.split())\n\n\n@mcp.tool\ndef to_uppercase(text: str) -> str:\n    \"\"\"Convert text to uppercase.\"\"\"\n    return text.upper()\n\n\n@mcp.tool\ndef list_files(directory: str) -> list[str]:\n    \"\"\"List files in a directory.\"\"\"\n    return os.listdir(directory)\n\n\n@mcp.tool\ndef read_file(path: str) -> str:\n    \"\"\"Read the contents of a file.\"\"\"\n    with open(path) as f:\n        return f.read()\n\n\n# BM25 search with a higher result limit for this larger catalog.\n# The `always_visible` option keeps specific tools in list_tools output\n# alongside the search/call tools — useful for tools the LLM should\n# always know about.\nmcp.add_transform(BM25SearchTransform(max_results=5, always_visible=[\"list_files\"]))\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/search/server_regex.py",
    "content": "\"\"\"Example: Search transforms with regex pattern matching.\n\nWhen a server has many tools, listing them all at once can overwhelm an LLM's\ncontext window. Search transforms collapse the full tool catalog behind a\nsearch interface — clients see only `search_tools` and `call_tool`, and\ndiscover the real tools on demand.\n\nThis example registers a handful of tools and applies RegexSearchTransform.\nClients use `search_tools` with a regex pattern to find relevant tools, then\n`call_tool` to execute them by name.\n\nRun with:\n    uv run python examples/search/server_regex.py\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms.search import RegexSearchTransform\n\nmcp = FastMCP(\"Regex Search Demo\")\n\n\n# Register a variety of tools across different domains.\n# With the search transform active, none of these appear in list_tools —\n# they're only discoverable via search.\n\n\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.tool\ndef multiply(x: float, y: float) -> float:\n    \"\"\"Multiply two numbers.\"\"\"\n    return x * y\n\n\n@mcp.tool\ndef fibonacci(n: int) -> list[int]:\n    \"\"\"Generate the first n Fibonacci numbers.\"\"\"\n    if n <= 0:\n        return []\n    seq = [0, 1]\n    while len(seq) < n:\n        seq.append(seq[-1] + seq[-2])\n    return seq[:n]\n\n\n@mcp.tool\ndef reverse_string(text: str) -> str:\n    \"\"\"Reverse a string.\"\"\"\n    return text[::-1]\n\n\n@mcp.tool\ndef word_count(text: str) -> int:\n    \"\"\"Count the number of words in a text.\"\"\"\n    return len(text.split())\n\n\n@mcp.tool\ndef to_uppercase(text: str) -> str:\n    \"\"\"Convert text to uppercase.\"\"\"\n    return text.upper()\n\n\n# Apply the regex search transform.\n# max_results limits how many tools a single search returns.\nmcp.add_transform(RegexSearchTransform(max_results=3))\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/simple_echo.py",
    "content": "\"\"\"\nFastMCP Echo Server\n\"\"\"\n\nfrom fastmcp import FastMCP\n\n# Create server\nmcp = FastMCP(\"Echo Server\")\n\n\n@mcp.tool\ndef echo(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return text\n"
  },
  {
    "path": "examples/skills/README.md",
    "content": "# Skills Provider Example\n\nThis example demonstrates how to expose agent skills (like Claude Code skills) as MCP resources.\n\n## Structure\n\n```\nskills/\n├── README.md              # This file\n├── server.py              # MCP server that exposes skills\n├── client.py              # Example client that discovers and reads skills\n└── sample_skills/         # Example skills directory\n    ├── pdf-processing/\n    │   ├── SKILL.md       # Main skill file\n    │   └── reference.md   # Supporting documentation\n    └── code-review/\n        └── SKILL.md       # Main skill file\n```\n\n## Running the Example\n\n1. Start the server:\n   ```bash\n   uv run python examples/skills/server.py\n   ```\n\n2. In another terminal, run the client:\n   ```bash\n   uv run python examples/skills/client.py\n   ```\n\n## How It Works\n\nThe skills provider system has a two-layer architecture:\n\n- **`SkillProvider`** - Handles a single skill folder, exposing its files as resources\n- **`SkillsDirectoryProvider`** - Scans a directory, creates a `SkillProvider` per folder\n- **`ClaudeSkillsProvider`** - Convenience subclass for Claude Code skills (~/.claude/skills/)\n\nFor each skill, the provider exposes:\n- A **Resource** for the main file (`skill://{name}/SKILL.md`)\n- A **Resource** for a synthetic manifest (`skill://{name}/_manifest`)\n- Supporting files via **ResourceTemplate** or **Resources** (configurable)\n\n### Progressive Disclosure\n\nWhen a client lists resources, they see skill names and descriptions (from frontmatter) without fetching the full content. This keeps the discovery cost low.\n\nBy default, supporting files are exposed via ResourceTemplate (hidden from `list_resources()`). Set `supporting_files=\"resources\"` to make them visible:\n\n```python\nSkillsDirectoryProvider(roots=skills_dir, supporting_files=\"resources\")\n```\n\n### The Manifest\n\nThe `_manifest` resource provides a JSON listing of all files in a skill:\n\n```json\n{\n  \"skill\": \"pdf-processing\",\n  \"files\": [\n    {\"path\": \"SKILL.md\", \"size\": 1234, \"hash\": \"sha256:abc...\"},\n    {\"path\": \"reference.md\", \"size\": 5678, \"hash\": \"sha256:def...\"}\n  ]\n}\n```\n\nThis enables clients to download entire skills for local use.\n\n## Usage Examples\n\n### Single Skill\n\n```python\nfrom pathlib import Path\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import SkillProvider\n\nmcp = FastMCP(\"My Skill\")\nmcp.add_provider(SkillProvider(Path.home() / \".claude/skills/pdf-processing\"))\nmcp.run()\n```\n\n### All Skills in a Directory\n\n```python\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\nmcp = FastMCP(\"Skills\")\nmcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / \".claude\" / \"skills\"))\nmcp.run()\n```\n\n### Claude Code Skills (default location)\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import ClaudeSkillsProvider\n\nmcp = FastMCP(\"My Skills\")\nmcp.add_provider(ClaudeSkillsProvider())  # Uses ~/.claude/skills/\nmcp.run()\n```\n"
  },
  {
    "path": "examples/skills/client.py",
    "content": "\"\"\"Example: Skills Client\n\nThis example shows how to discover and download skills from a skills server.\n\nRun this client (it starts its own server internally):\n    uv run python examples/skills/client.py\n\"\"\"\n\nimport asyncio\nimport json\nfrom pathlib import Path\n\nfrom fastmcp import Client\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\n\n\nasync def main():\n    # Create a skills provider pointing at our sample skills\n    skills_dir = Path(__file__).parent / \"sample_skills\"\n    provider = SkillsDirectoryProvider(roots=skills_dir)\n\n    # Connect to a FastMCP server with this provider\n    from fastmcp import FastMCP\n\n    mcp = FastMCP(\"Skills Server\")\n    mcp.add_provider(provider)\n\n    async with Client(mcp) as client:\n        print(\"Connected to skills server\\n\")\n\n        # List available resources\n        print(\"=== Available Resources ===\")\n        resources = await client.list_resources()\n        for r in resources:\n            print(f\"  {r.uri}\")\n            if r.description:\n                print(f\"    Description: {r.description}\")\n        print()\n\n        # List resource templates\n        print(\"=== Resource Templates ===\")\n        templates = await client.list_resource_templates()\n        for t in templates:\n            print(f\"  {t.uriTemplate}\")\n        print()\n\n        # Read a skill's main file\n        print(\"=== Reading pdf-processing/SKILL.md ===\")\n        result = await client.read_resource(\"skill://pdf-processing/SKILL.md\")\n        print(result[0].text[:500] + \"...\\n\")\n\n        # Read the manifest to see all files\n        print(\"=== Reading pdf-processing/_manifest ===\")\n        result = await client.read_resource(\"skill://pdf-processing/_manifest\")\n        manifest = json.loads(result[0].text)\n        print(json.dumps(manifest, indent=2))\n        print()\n\n        # Read a supporting file via template\n        print(\"=== Reading pdf-processing/reference.md ===\")\n        result = await client.read_resource(\"skill://pdf-processing/reference.md\")\n        print(result[0].text[:500] + \"...\\n\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/skills/download_skills.py",
    "content": "\"\"\"Example: Downloading skills from an MCP server.\n\nThis example shows how to use the skills client utilities to discover\nand download skills from any MCP server that exposes them via SkillsProvider.\n\nRun this script:\n    uv run python examples/skills/download_skills.py\n\nThis example creates an in-memory server with sample skills. In practice,\nyou would connect to a remote server URL instead.\n\"\"\"\n\nimport asyncio\nimport tempfile\nfrom pathlib import Path\n\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.table import Table\nfrom rich.tree import Tree\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\nfrom fastmcp.utilities.skills import list_skills, sync_skills\n\nconsole = Console()\n\n\nasync def main():\n    # For this example, we'll create an in-memory server with skills.\n    # In practice, you'd connect to a remote server URL.\n    skills_dir = Path(__file__).parent / \"sample_skills\"\n    mcp = FastMCP(\"Skills Server\")\n    mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n\n    async with Client(mcp) as client:\n        # 1. Discover what skills are available on the server\n        console.print()\n        console.print(\n            Panel.fit(\n                \"[bold]Discovering skills on MCP server...[/bold]\",\n                border_style=\"blue\",\n            )\n        )\n\n        skills = await list_skills(client)\n\n        table = Table(title=\"Skills Available on Server\", show_header=True)\n        table.add_column(\"Skill\", style=\"cyan\")\n        table.add_column(\"Description\")\n        for skill in sorted(skills, key=lambda s: s.name):\n            table.add_row(skill.name, skill.description)\n        console.print(table)\n\n        # 2. Download all skills to a local directory\n        console.print()\n        console.print(\n            Panel.fit(\n                \"[bold]Downloading all skills to local directory...[/bold]\",\n                border_style=\"green\",\n            )\n        )\n\n        with tempfile.TemporaryDirectory() as tmp:\n            paths = await sync_skills(client, tmp)\n\n            tree = Tree(f\"[bold]{tmp}[/bold]\")\n            for skill_path in sorted(paths, key=lambda p: p.name):\n                skill_branch = tree.add(f\"[cyan]{skill_path.name}/[/cyan]\")\n                for f in sorted(skill_path.rglob(\"*\")):\n                    if f.is_file():\n                        rel = f.relative_to(skill_path)\n                        skill_branch.add(str(rel))\n\n            console.print(tree)\n            console.print(\n                f\"\\n[green]✓[/green] Downloaded {len(paths)} skills to local directory\"\n            )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/skills/sample_skills/code-review/SKILL.md",
    "content": "---\ndescription: Review code for quality, maintainability, and correctness\nversion: \"1.0.0\"\ntags: [code, review, quality]\n---\n\n# Code Review Skill\n\nThis skill guides you through conducting thorough code reviews.\n\n## Review Checklist\n\nWhen reviewing code, consider:\n\n### Correctness\n- Does the code do what it's supposed to do?\n- Are edge cases handled?\n- Are there any obvious bugs?\n\n### Maintainability\n- Is the code easy to understand?\n- Are variable and function names descriptive?\n- Is there appropriate documentation?\n\n### Performance\n- Are there any obvious performance issues?\n- Are expensive operations cached when appropriate?\n- Are database queries efficient?\n\n### Security\n- Is user input validated?\n- Are there any injection vulnerabilities?\n- Are secrets properly managed?\n\n## Giving Feedback\n\n- Be specific and actionable\n- Explain *why* something should change\n- Suggest alternatives, don't just criticize\n- Acknowledge good work too\n"
  },
  {
    "path": "examples/skills/sample_skills/pdf-processing/SKILL.md",
    "content": "---\ndescription: Extract text from PDFs, fill forms, and merge documents\nversion: \"1.0.0\"\ntags: [document, pdf, extraction]\n---\n\n# PDF Processing Skill\n\nThis skill helps you work with PDF documents.\n\n## Capabilities\n\n- Extract text content from PDF files\n- Fill form fields in PDF documents\n- Merge multiple PDFs into one\n- Split PDFs into separate pages\n\n## Usage\n\nWhen working with PDFs, use the appropriate tools:\n\n1. For text extraction: Read the PDF and parse its content\n2. For forms: Identify form fields and fill them programmatically\n3. For merging: Combine multiple documents in the desired order\n\n## Additional Resources\n\nSee [reference.md](reference.md) for detailed API documentation.\n"
  },
  {
    "path": "examples/skills/sample_skills/pdf-processing/reference.md",
    "content": "# PDF Processing Reference\n\n## Text Extraction\n\nTo extract text from a PDF:\n\n```python\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"document.pdf\")\nfor page in reader.pages:\n    text = page.extract_text()\n    print(text)\n```\n\n## Form Filling\n\nPDF forms can be filled using the form fields API:\n\n```python\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"form.pdf\")\nwriter = PdfWriter()\n\nwriter.append(reader)\nwriter.update_page_form_field_values(\n    writer.pages[0],\n    {\"field_name\": \"field_value\"}\n)\n\nwith open(\"filled_form.pdf\", \"wb\") as output:\n    writer.write(output)\n```\n\n## Merging PDFs\n\n```python\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter()\nwriter.append(\"doc1.pdf\")\nwriter.append(\"doc2.pdf\")\n\nwith open(\"merged.pdf\", \"wb\") as output:\n    writer.write(output)\n```\n"
  },
  {
    "path": "examples/skills/server.py",
    "content": "\"\"\"Example: Skills Provider Server\n\nThis example shows how to expose agent skills as MCP resources.\nSkills can be discovered, browsed, and downloaded by any MCP client.\n\nRun this server:\n    uv run python examples/skills/server.py\n\nThen use the client example to interact with it:\n    uv run python examples/skills/client.py\n\"\"\"\n\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.skills import (\n    SkillsDirectoryProvider,\n)\n\n# Create server\nmcp = FastMCP(\"Skills Server\")\n\n# Option 1: Load a single skill\n# mcp.add_provider(SkillProvider(Path.home() / \".claude/skills/pdf-processing\"))\n\n# Option 2: Load all skills from a custom directory\nskills_dir = Path(__file__).parent / \"sample_skills\"\nmcp.add_provider(SkillsDirectoryProvider(roots=skills_dir, reload=True))\n\n# Option 3: Load skills from a platform's default location\n# mcp.add_provider(ClaudeSkillsProvider())  # ~/.claude/skills/\n\n# Option 4: Load from multiple directories (in precedence order)\n# mcp.add_provider(SkillsDirectoryProvider(roots=[\n#     Path.cwd() / \".claude/skills\",      # Project-level first\n#     Path.home() / \".claude/skills\",     # User-level fallback\n# ]))\n\n# Other vendor providers available:\n# - CursorSkillsProvider()   → ~/.cursor/skills/\n# - VSCodeSkillsProvider()   → ~/.copilot/skills/\n# - CodexSkillsProvider()    → ~/.codex/skills/\n# - GeminiSkillsProvider()   → ~/.gemini/skills/\n# - GooseSkillsProvider()    → ~/.config/agents/skills/\n# - CopilotSkillsProvider()  → ~/.copilot/skills/\n# - OpenCodeSkillsProvider() → ~/.config/opencode/skills/\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/smart_home/README.md",
    "content": "# smart home mcp server\n\n```bash\ncd examples/smart_home\nmcp install src/smart_home/hub.py:hub_mcp -f .env\n```\nwhere `.env` contains the following:\n```\nHUE_BRIDGE_IP=<your hue bridge ip>\nHUE_BRIDGE_USERNAME=<your hue bridge username>\n```\n\n```bash\nopen -a Claude\n```"
  },
  {
    "path": "examples/smart_home/hub.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"entrypoint\": \"src/smart_home/hub.py\",\n  \"environment\": {\n    \"dependencies\": [\n      \"smart_home@git+https://github.com/PrefectHQ/fastmcp.git#subdirectory=examples/smart_home\"\n    ]\n  }\n}"
  },
  {
    "path": "examples/smart_home/lights.fastmcp.json",
    "content": "{\n  \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n  \"entrypoint\": \"src/smart_home/lights/server.py\",\n  \"environment\": {\n    \"dependencies\": [\n      \"smart_home@git+https://github.com/PrefectHQ/fastmcp.git#subdirectory=examples/smart_home\"\n    ]\n  }\n}"
  },
  {
    "path": "examples/smart_home/pyproject.toml",
    "content": "[project]\nname = \"smart-home\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nauthors = [{ name = \"zzstoatzz\", email = \"thrast36@gmail.com\" }]\nrequires-python = \">=3.12\"\ndependencies = [\"fastmcp@git+https://github.com/PrefectHQ/fastmcp.git\", \"phue2\"]\n\n[project.scripts]\nsmart-home = \"smart_home.__main__:main\"\n\n[dependency-groups]\ndev = [\"ruff\", \"ipython\"]\n\n\n[build-system]\nrequires = [\"uv_build\"]\nbuild-backend = \"uv_build\"\n"
  },
  {
    "path": "examples/smart_home/src/smart_home/__init__.py",
    "content": "from smart_home.settings import settings\n\n__all__ = [\"settings\"]\n"
  },
  {
    "path": "examples/smart_home/src/smart_home/__main__.py",
    "content": "from smart_home.hub import hub_mcp\n\n\ndef main():\n    hub_mcp.run()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/smart_home/src/smart_home/hub.py",
    "content": "from mcp.types import ToolAnnotations\nfrom phue import Bridge\n\nfrom fastmcp import FastMCP\nfrom smart_home.lights.server import lights_mcp\nfrom smart_home.settings import settings\n\nhub_mcp = FastMCP(\"Smart Home Hub (phue2)\")\n\n# Mount the lights service under the 'hue' namespace\nhub_mcp.mount(lights_mcp, namespace=\"hue\")\n\n\n# Add a status check for the hub\n@hub_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True))\ndef hub_status() -> str:\n    \"\"\"Checks the status of the main hub and connections.\"\"\"\n    try:\n        bridge = Bridge(\n            ip=settings.hue_bridge_ip,\n            username=settings.hue_bridge_username,\n            save_config=False,\n        )\n        bridge.connect()\n        return \"Hub OK. Hue Bridge Connected (via phue2).\"\n    except Exception as e:\n        return f\"Hub Warning: Hue Bridge connection failed or not attempted: {e}\"\n\n\n# Add mounting points for other services later\n# hub_mcp.mount(\"thermo\", thermostat_mcp)\n"
  },
  {
    "path": "examples/smart_home/src/smart_home/lights/__init__.py",
    "content": ""
  },
  {
    "path": "examples/smart_home/src/smart_home/lights/hue_utils.py",
    "content": "from typing import Any\n\nfrom phue import Bridge\nfrom phue.exceptions import PhueException\n\nfrom smart_home.settings import settings\n\n\ndef _get_bridge() -> Bridge | None:\n    \"\"\"Attempts to connect to the Hue bridge using settings.\"\"\"\n    try:\n        return Bridge(\n            ip=settings.hue_bridge_ip,\n            username=settings.hue_bridge_username,\n            save_config=False,\n        )\n    except Exception:\n        # Broad exception to catch potential connection issues\n        # TODO: Add more specific logging or error handling\n        return None\n\n\ndef handle_phue_error(\n    light_or_group: str, operation: str, error: Exception\n) -> dict[str, Any]:\n    \"\"\"Creates a standardized error response for phue operations.\"\"\"\n    base_info = {\"target\": light_or_group, \"operation\": operation, \"success\": False}\n    if isinstance(error, KeyError):\n        base_info[\"error\"] = f\"Target '{light_or_group}' not found\"\n    elif isinstance(error, PhueException):\n        base_info[\"error\"] = f\"phue error during {operation}: {error}\"\n    else:\n        base_info[\"error\"] = f\"Unexpected error during {operation}: {error}\"\n    return base_info\n"
  },
  {
    "path": "examples/smart_home/src/smart_home/lights/server.py",
    "content": "# /// script\n# dependencies = [\n#     \"smart_home@git+https://github.com/PrefectHQ/fastmcp.git#subdirectory=examples/smart_home\",\n#     \"fastmcp\",\n# ]\n# ///\n\nfrom typing import Annotated, Any, Literal, TypedDict\n\nfrom mcp.types import ToolAnnotations\nfrom phue.exceptions import PhueException\nfrom pydantic import Field\nfrom typing_extensions import NotRequired\n\nfrom fastmcp import FastMCP\nfrom smart_home.lights.hue_utils import _get_bridge, handle_phue_error\n\n\nclass HueAttributes(TypedDict, total=False):\n    \"\"\"TypedDict for optional light attributes.\"\"\"\n\n    on: NotRequired[Annotated[bool, Field(description=\"on/off state\")]]\n    bri: NotRequired[Annotated[int, Field(ge=0, le=254, description=\"brightness\")]]\n    hue: NotRequired[\n        Annotated[\n            int,\n            Field(\n                ge=0,\n                le=65535,\n                description=\"hue (color wheel position)\",\n            ),\n        ]\n    ]\n    sat: NotRequired[\n        Annotated[\n            int,\n            Field(\n                ge=0,\n                le=254,\n                description=\"saturation\",\n            ),\n        ]\n    ]\n    xy: NotRequired[Annotated[list[float], Field(description=\"xy color coordinates\")]]\n    ct: NotRequired[\n        Annotated[\n            int,\n            Field(ge=153, le=500, description=\"color temperature\"),\n        ]\n    ]\n    alert: NotRequired[Literal[\"none\", \"select\", \"lselect\"]]\n    effect: NotRequired[Literal[\"none\", \"colorloop\"]]\n    transitiontime: NotRequired[Annotated[int, Field(description=\"deciseconds\")]]\n\n\n# Dependencies are configured in lights.fastmcp.json\nlights_mcp = FastMCP(\"Hue Lights Service (phue2)\")\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True))\ndef read_all_lights() -> list[str]:\n    \"\"\"Lists the names of all available Hue lights using phue2.\"\"\"\n    if not (bridge := _get_bridge()):\n        return [\"Error: Bridge not connected\"]\n    try:\n        light_dict = bridge.get_light_objects(\"list\")\n        return [light.name for light in light_dict]\n    except (PhueException, Exception) as e:\n        # Simplified error handling for list return type\n        return [f\"Error listing lights: {e}\"]\n\n\n# --- Tools ---\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, openWorldHint=True))\ndef toggle_light(light_name: str, state: bool) -> dict[str, Any]:\n    \"\"\"Turns a specific light on (true) or off (false) using phue2.\"\"\"\n    if not (bridge := _get_bridge()):\n        return {\"error\": \"Bridge not connected\", \"success\": False}\n    try:\n        result = bridge.set_light(light_name, \"on\", state)\n        return {\n            \"light\": light_name,\n            \"set_on_state\": state,\n            \"success\": True,\n            \"phue2_result\": result,\n        }\n    except (KeyError, PhueException, Exception) as e:\n        return handle_phue_error(light_name, \"toggle_light\", e)\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, openWorldHint=True))\ndef set_brightness(light_name: str, brightness: int) -> dict[str, Any]:\n    \"\"\"Sets the brightness of a specific light (0-254) using phue2.\"\"\"\n    if not (bridge := _get_bridge()):\n        return {\"error\": \"Bridge not connected\", \"success\": False}\n    if not 0 <= brightness <= 254:\n        # Keep specific input validation error here\n        return {\n            \"light\": light_name,\n            \"error\": \"Brightness must be between 0 and 254\",\n            \"success\": False,\n        }\n    try:\n        result = bridge.set_light(light_name, \"bri\", brightness)\n        return {\n            \"light\": light_name,\n            \"set_brightness\": brightness,\n            \"success\": True,\n            \"phue2_result\": result,\n        }\n    except (KeyError, PhueException, Exception) as e:\n        return handle_phue_error(light_name, \"set_brightness\", e)\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True))\ndef list_groups() -> list[str]:\n    \"\"\"Lists the names of all available Hue light groups.\"\"\"\n    if not (bridge := _get_bridge()):\n        return [\"Error: Bridge not connected\"]\n    try:\n        # phue2 get_group() returns a dict {id: {details}} including name\n        groups = bridge.get_group()\n        return [group_details[\"name\"] for group_details in groups.values()]\n    except (PhueException, Exception) as e:\n        return [f\"Error listing groups: {e}\"]\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True))\ndef list_scenes() -> dict[str, list[str]] | list[str]:\n    \"\"\"Lists Hue scenes, grouped by the light group they belong to.\n\n    Returns:\n        dict[str, list[str]]: A dictionary mapping group names to a list of scene names within that group.\n        list[str]: An error message list if the bridge connection fails or an error occurs.\n    \"\"\"\n    if not (bridge := _get_bridge()):\n        return [\"Error: Bridge not connected\"]\n    try:\n        scenes_data = bridge.get_scene()  # Returns dict {scene_id: {details...}}\n        groups_data = bridge.get_group()  # Returns dict {group_id: {details...}}\n\n        # Create a lookup for group name by group ID\n        group_id_to_name = {gid: ginfo[\"name\"] for gid, ginfo in groups_data.items()}\n\n        scenes_by_group: dict[str, list[str]] = {}\n        for scene_id, scene_details in scenes_data.items():\n            scene_name = scene_details.get(\"name\")\n            # Scenes might be associated with a group via 'group' key or lights\n            # Using 'group' key if available is more direct for group scenes\n            group_id = scene_details.get(\"group\")\n            if scene_name and group_id and group_id in group_id_to_name:\n                group_name = group_id_to_name[group_id]\n                if group_name not in scenes_by_group:\n                    scenes_by_group[group_name] = []\n                # Avoid duplicate scene names within a group listing (though unlikely)\n                if scene_name not in scenes_by_group[group_name]:\n                    scenes_by_group[group_name].append(scene_name)\n\n        # Sort scenes within each group for consistent output\n        for group_name in scenes_by_group:\n            scenes_by_group[group_name].sort()\n\n        return scenes_by_group\n    except (PhueException, Exception) as e:\n        # Return error as list to match other list-returning tools on error\n        return [f\"Error listing scenes by group: {e}\"]\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, openWorldHint=True))\ndef activate_scene(group_name: str, scene_name: str) -> dict[str, Any]:\n    \"\"\"Activates a specific scene within a specified light group, verifying the scene belongs to the group.\"\"\"\n    if not (bridge := _get_bridge()):\n        return {\"error\": \"Bridge not connected\", \"success\": False}\n    try:\n        # 1. Find the target group ID\n        groups_data = bridge.get_group()\n        target_group_id = None\n        for gid, ginfo in groups_data.items():\n            if ginfo.get(\"name\") == group_name:\n                target_group_id = gid\n                break\n        if not target_group_id:\n            return {\"error\": f\"Group '{group_name}' not found\", \"success\": False}\n\n        # 2. Find the target scene and check its group association\n        scenes_data = bridge.get_scene()\n        scene_found = False\n        scene_in_correct_group = False\n        for sinfo in scenes_data.values():\n            if sinfo.get(\"name\") == scene_name:\n                scene_found = True\n                # Check if this scene is associated with the target group ID\n                if sinfo.get(\"group\") == target_group_id:\n                    scene_in_correct_group = True\n                    break  # Found the scene in the correct group\n\n        if not scene_found:\n            return {\"error\": f\"Scene '{scene_name}' not found\", \"success\": False}\n\n        if not scene_in_correct_group:\n            return {\n                \"error\": f\"Scene '{scene_name}' does not belong to group '{group_name}'\",\n                \"success\": False,\n            }\n\n        # 3. Activate the scene (now that we've verified it)\n        result = bridge.run_scene(group_name=group_name, scene_name=scene_name)\n\n        if result:\n            return {\n                \"group\": group_name,\n                \"activated_scene\": scene_name,\n                \"success\": True,\n                \"phue2_result\": result,\n            }\n        else:\n            # This case might indicate the scene/group exists but activation failed internally\n            return {\n                \"group\": group_name,\n                \"scene\": scene_name,\n                \"error\": \"Scene activation failed (phue2 returned False)\",\n                \"success\": False,\n            }\n\n    except (KeyError, PhueException, Exception) as e:\n        # Handle potential errors during bridge communication or data parsing\n        return handle_phue_error(f\"{group_name}/{scene_name}\", \"activate_scene\", e)\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, openWorldHint=True))\ndef set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str, Any]:\n    \"\"\"Sets multiple attributes (e.g., hue, sat, bri, ct, xy, transitiontime) for a specific light.\"\"\"\n    if not (bridge := _get_bridge()):\n        return {\"error\": \"Bridge not connected\", \"success\": False}\n\n    # Basic validation (more specific validation could be added)\n    if not isinstance(attributes, dict) or not attributes:\n        return {\n            \"error\": \"Attributes must be a non-empty dictionary\",\n            \"success\": False,\n            \"light\": light_name,\n        }\n\n    try:\n        result = bridge.set_light(light_name, dict(attributes))\n        return {\n            \"light\": light_name,\n            \"set_attributes\": attributes,\n            \"success\": True,\n            \"phue2_result\": result,\n        }\n    except (KeyError, PhueException, ValueError, Exception) as e:\n        # ValueError might occur for invalid attribute values\n        return handle_phue_error(light_name, \"set_light_attributes\", e)\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, openWorldHint=True))\ndef set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str, Any]:\n    \"\"\"Sets multiple attributes for all lights within a specific group.\"\"\"\n    if not (bridge := _get_bridge()):\n        return {\"error\": \"Bridge not connected\", \"success\": False}\n\n    if not isinstance(attributes, dict) or not attributes:\n        return {\n            \"error\": \"Attributes must be a non-empty dictionary\",\n            \"success\": False,\n            \"group\": group_name,\n        }\n\n    try:\n        result = bridge.set_group(group_name, dict(attributes))\n        return {\n            \"group\": group_name,\n            \"set_attributes\": attributes,\n            \"success\": True,\n            \"phue2_result\": result,\n        }\n    except (KeyError, PhueException, ValueError, Exception) as e:\n        return handle_phue_error(group_name, \"set_group_attributes\", e)\n\n\n@lights_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True))\ndef list_lights_by_group() -> dict[str, list[str]] | list[str]:\n    \"\"\"Lists Hue lights, grouped by the room/group they belong to.\n\n    Returns:\n        dict[str, list[str]]: A dictionary mapping group names to a list of light names within that group.\n        list[str]: An error message list if the bridge connection fails or an error occurs.\n    \"\"\"\n    if not (bridge := _get_bridge()):\n        return [\"Error: Bridge not connected\"]\n    try:\n        groups_data = bridge.get_group()  # dict {group_id: {details}}\n        lights_data = bridge.get_light_objects(\"id\")  # dict {light_id: {details}}\n\n        lights_by_group: dict[str, list[str]] = {}\n        for group_details in groups_data.values():\n            group_name = group_details.get(\"name\")\n            light_ids = group_details.get(\"lights\", [])\n            if group_name and light_ids:\n                light_names = []\n                for light_id in light_ids:\n                    # phue uses string IDs for lights in group, but int IDs in get_light_objects\n                    light_id_int = int(light_id)\n                    if light_id_int in lights_data:\n                        light_name = lights_data[light_id_int].name\n                        if light_name:\n                            light_names.append(light_name)\n                if light_names:\n                    light_names.sort()\n                    lights_by_group[group_name] = light_names\n\n        return lights_by_group\n\n    except (PhueException, Exception) as e:\n        return [f\"Error listing lights by group: {e}\"]\n"
  },
  {
    "path": "examples/smart_home/src/smart_home/py.typed",
    "content": ""
  },
  {
    "path": "examples/smart_home/src/smart_home/settings.py",
    "content": "from pydantic import Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(env_file=\".env\", extra=\"ignore\")\n\n    hue_bridge_ip: str = Field(default=...)\n    hue_bridge_username: str = Field(default=...)\n\n\nsettings = Settings()\n"
  },
  {
    "path": "examples/tags_example.py",
    "content": "\"\"\"\nExample demonstrating RouteMap tags functionality.\n\nThis example shows how to use the tags parameter in RouteMap\nto selectively route OpenAPI endpoints based on their tags.\n\"\"\"\n\nimport asyncio\n\nfrom fastapi import FastAPI\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.openapi import MCPType, RouteMap\n\n# Create a FastAPI app with tagged endpoints\napp = FastAPI(title=\"Tagged API Example\")\n\n\n@app.get(\"/users\", tags=[\"users\", \"public\"])\nasync def get_users():\n    \"\"\"Get all users - public endpoint\"\"\"\n    return [{\"id\": 1, \"name\": \"Alice\"}, {\"id\": 2, \"name\": \"Bob\"}]\n\n\n@app.post(\"/users\", tags=[\"users\", \"admin\"])\nasync def create_user(name: str):\n    \"\"\"Create a user - admin only\"\"\"\n    return {\"id\": 3, \"name\": name}\n\n\n@app.get(\"/admin/stats\", tags=[\"admin\", \"internal\"])\nasync def get_admin_stats():\n    \"\"\"Get admin statistics - internal use\"\"\"\n    return {\"total_users\": 100, \"active_sessions\": 25}\n\n\n@app.get(\"/health\", tags=[\"public\"])\nasync def health_check():\n    \"\"\"Public health check\"\"\"\n    return {\"status\": \"healthy\"}\n\n\n@app.get(\"/metrics\")\nasync def get_metrics():\n    \"\"\"Metrics endpoint with no tags\"\"\"\n    return {\"requests\": 1000, \"errors\": 5}\n\n\nasync def main():\n    \"\"\"Demonstrate different tag-based routing strategies.\"\"\"\n\n    print(\"=== Example 1: Make admin-tagged routes tools ===\")\n\n    # Strategy 1: Convert admin-tagged routes to tools\n    mcp1 = FastMCP.from_fastapi(\n        app=app,\n        route_maps=[\n            RouteMap(methods=\"*\", pattern=r\".*\", mcp_type=MCPType.TOOL, tags={\"admin\"}),\n            RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n        ],\n    )\n\n    tools = await mcp1.list_tools()\n    resources = await mcp1.list_resources()\n\n    print(f\"Tools ({len(tools)}): {', '.join(t.name for t in tools)}\")\n    print(f\"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}\")\n\n    print(\"\\n=== Example 2: Exclude internal routes ===\")\n\n    # Strategy 2: Exclude internal routes entirely\n    mcp2 = FastMCP.from_fastapi(\n        app=app,\n        route_maps=[\n            RouteMap(\n                methods=\"*\", pattern=r\".*\", mcp_type=MCPType.EXCLUDE, tags={\"internal\"}\n            ),\n            RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n            RouteMap(methods=[\"POST\"], pattern=r\".*\", mcp_type=MCPType.TOOL),\n        ],\n    )\n\n    tools = await mcp2.list_tools()\n    resources = await mcp2.list_resources()\n\n    print(f\"Tools ({len(tools)}): {', '.join(t.name for t in tools)}\")\n    print(f\"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}\")\n\n    print(\"\\n=== Example 3: Pattern + Tags combination ===\")\n\n    # Strategy 3: Routes matching both pattern AND tags\n    mcp3 = FastMCP.from_fastapi(\n        app=app,\n        route_maps=[\n            # Admin routes under /admin path -> tools\n            RouteMap(\n                methods=\"*\",\n                pattern=r\".*/admin/.*\",\n                mcp_type=MCPType.TOOL,\n                tags={\"admin\"},\n            ),\n            # Public routes -> tools\n            RouteMap(\n                methods=\"*\", pattern=r\".*\", mcp_type=MCPType.TOOL, tags={\"public\"}\n            ),\n            RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n        ],\n    )\n\n    tools = await mcp3.list_tools()\n    resources = await mcp3.list_resources()\n\n    print(f\"Tools ({len(tools)}): {', '.join(t.name for t in tools)}\")\n    print(f\"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}\")\n\n    print(\"\\n=== Example 4: Multiple tag AND condition ===\")\n\n    # Strategy 4: Routes must have ALL specified tags\n    mcp4 = FastMCP.from_fastapi(\n        app=app,\n        route_maps=[\n            # Routes with BOTH \"users\" AND \"admin\" tags -> tools\n            RouteMap(\n                methods=\"*\",\n                pattern=r\".*\",\n                mcp_type=MCPType.TOOL,\n                tags={\"users\", \"admin\"},\n            ),\n            RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n        ],\n    )\n\n    tools = await mcp4.list_tools()\n    resources = await mcp4.list_resources()\n\n    print(f\"Tools ({len(tools)}): {', '.join(t.name for t in tools)}\")\n    print(f\"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/task_elicitation.py",
    "content": "\"\"\"\nBackground task elicitation demo.\n\nA background task (Docket) that pauses mid-execution to ask the user a\nquestion, waits for the answer, then resumes and finishes.\n\nWorks with both in-memory and Redis backends:\n\n    # In-memory (single process, no Redis needed)\n    FASTMCP_DOCKET_URL=memory:// uv run python examples/task_elicitation.py\n\n    # Redis (distributed, needs a worker running separately)\n    #   Terminal 1: docker compose -f examples/tasks/docker-compose.yml up -d\n    #   Terminal 2: FASTMCP_DOCKET_URL=redis://localhost:24242/0 \\\n    #               uv run fastmcp tasks worker examples/task_elicitation.py\n    #   Terminal 3: FASTMCP_DOCKET_URL=redis://localhost:24242/0 \\\n    #               uv run python examples/task_elicitation.py\n\nRequires the `docket` extra (included in dev dependencies).\n\"\"\"\n\nimport asyncio\nfrom dataclasses import dataclass\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.elicitation import AcceptedElicitation\n\nmcp = FastMCP(\"Task Elicitation Demo\")\n\n\n@dataclass\nclass DinnerPrefs:\n    cuisine: str\n    vegetarian: bool\n\n\n@mcp.tool(task=True)\nasync def plan_dinner(ctx: Context) -> str:\n    \"\"\"Plan a dinner menu, asking the user what they're in the mood for.\"\"\"\n\n    await ctx.report_progress(0, 2, \"Asking what you'd like...\")\n\n    result = await ctx.elicit(\n        \"What kind of dinner are you in the mood for?\",\n        response_type=DinnerPrefs,\n    )\n\n    if not isinstance(result, AcceptedElicitation):\n        return \"Dinner cancelled!\"\n\n    prefs = result.data\n    await ctx.report_progress(1, 2, \"Planning your menu...\")\n    await asyncio.sleep(1)\n    await ctx.report_progress(2, 2, \"Done!\")\n\n    veg = \"vegetarian \" if prefs.vegetarian else \"\"\n    return f\"Tonight's menu: a lovely {veg}{prefs.cuisine} dinner!\"\n\n\nasync def handle_elicitation(message, response_type, params, context):\n    \"\"\"Handle elicitation requests from background tasks.\"\"\"\n    print(f\"  Server asks: {message}\")\n    print(\"  Responding with: cuisine=Thai, vegetarian=True\")\n    return DinnerPrefs(cuisine=\"Thai\", vegetarian=True)\n\n\nasync def main():\n    async with Client(mcp, elicitation_handler=handle_elicitation) as client:\n        print(\"Starting background task...\")\n        task = await client.call_tool(\"plan_dinner\", {}, task=True)\n        print(f\"  task_id = {task.task_id}\\n\")\n\n        result = await task.result()\n        assert isinstance(result.content[0], TextContent)\n        print(f\"\\nResult: {result.content[0].text}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/tasks/README.md",
    "content": "# FastMCP Tasks Example\n\nDemonstrates background task execution with Docket, including progress tracking, distributed backends, and CLI worker management.\n\n## Setup\n\n```bash\n# From the fastmcp root directory\nuv sync\n\n# Start Redis\ncd examples/tasks\ndocker compose up -d\n\n# Load environment (or source .envrc manually)\ndirenv allow\n\n# Run the server\nfastmcp run server.py\n```\n\nFor single-process mode without Redis, set `FASTMCP_DOCKET_URL=memory://` (note: CLI workers won't work).\n\n## Running the Client\n\n```bash\n# Background execution with progress callbacks\npython examples/tasks/client.py --duration 10\n\n# Immediate execution (blocks)\npython examples/tasks/client.py immediate --duration 5\n```\n\n## Starting Additional Workers\n\nWith Redis, you can run additional workers to process tasks in parallel:\n\n```bash\nfastmcp tasks worker server.py\n\n# Configure via environment:\nexport FASTMCP_DOCKET_CONCURRENCY=20\nfastmcp tasks worker server.py\n```\n\n**Backend options:**\n- `memory://` - Single-process only (default)\n- `redis://` - Distributed, multi-process (Redis or Valkey)\n\n## Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL |\n\n## Learn More\n\n- [FastMCP Tasks Documentation](https://gofastmcp.com/docs/tasks)\n- [Docket Documentation](https://github.com/PrefectHQ/docket)\n- [MCP Task Protocol (SEP-1686)](https://spec.modelcontextprotocol.io/specification/architecture/tasks/)\n"
  },
  {
    "path": "examples/tasks/client.py",
    "content": "\"\"\"\nFastMCP Tasks Example Client\n\nDemonstrates calling tools both immediately and as background tasks,\nwith real-time progress updates via status callbacks.\n\nUsage:\n    # Make sure environment is configured (source .envrc or use direnv)\n    source .envrc\n\n    # Background task execution with progress callbacks (default)\n    python client.py --duration 10\n\n    # Immediate execution (blocks until complete)\n    python client.py immediate --duration 5\n\"\"\"\n\nimport asyncio\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nfrom mcp.types import GetTaskResult, TextContent\nfrom rich.console import Console\n\nfrom fastmcp.client import Client\n\nconsole = Console()\napp = cyclopts.App(name=\"tasks-client\", help=\"FastMCP Tasks Example Client\")\n\n\ndef load_server():\n    \"\"\"Load the example server.\"\"\"\n    examples_dir = Path(__file__).parent.parent.parent\n    if str(examples_dir) not in sys.path:\n        sys.path.insert(0, str(examples_dir))\n\n    import examples.tasks.server as server_module\n\n    return server_module.mcp\n\n\n# Track last message to deduplicate consecutive identical notifications\n# Note: Docket fires separate events for progress.increment() and progress.set_message(),\n# but MCP's statusMessage field only carries the text message (no numerical progress).\n# This means we often get duplicate notifications with identical messages.\n_last_notification_message = None\n\n\ndef print_notification(status: GetTaskResult) -> None:\n    \"\"\"Callback function for push notifications from server.\n\n    This is called automatically when the server sends notifications/tasks/status.\n    Deduplicates identical consecutive messages to keep output clean.\n    \"\"\"\n    global _last_notification_message\n\n    # Skip if this is the same message we just printed\n    if status.statusMessage == _last_notification_message:\n        return\n\n    _last_notification_message = status.statusMessage\n\n    color = {\n        \"working\": \"yellow\",\n        \"completed\": \"green\",\n        \"failed\": \"red\",\n    }.get(status.status, \"yellow\")\n\n    icon = {\n        \"working\": \"🚀\",\n        \"completed\": \"✅\",\n        \"failed\": \"❌\",\n    }.get(status.status, \"⚠️\")\n\n    console.print(\n        f\"[{color}]📢 Notification: {status.status} {icon} - {status.statusMessage}[/{color}]\"\n    )\n\n\n@app.default\nasync def task(\n    duration: Annotated[\n        int,\n        cyclopts.Parameter(help=\"Duration of computation in seconds (1-60)\"),\n    ] = 10,\n):\n    \"\"\"Execute as background task with real-time progress callbacks.\"\"\"\n    if duration < 1 or duration > 60:\n        console.print(\"[red]Error: Duration must be between 1 and 60 seconds[/red]\")\n        sys.exit(1)\n\n    server = load_server()\n\n    console.print(f\"\\n[bold]Calling slow_computation(duration={duration})[/bold]\")\n    console.print(\"Mode: [cyan]Background task[/cyan]\\n\")\n\n    async with Client(server) as client:\n        task_obj = await client.call_tool(\n            \"slow_computation\",\n            arguments={\"duration\": duration},\n            task=True,\n        )\n\n        console.print(f\"Task started: [cyan]{task_obj.task_id}[/cyan]\\n\")\n\n        # Register callback for real-time push notifications\n        task_obj.on_status_change(print_notification)\n\n        console.print(\n            \"[dim]Notifications will appear as the server sends them...[/dim]\\n\"\n        )\n\n        # Do other work while task runs in background\n        for i in range(3):\n            await asyncio.sleep(0.5)\n            console.print(f\"[dim]Client doing other work... ({i + 1}/3)[/dim]\")\n\n        console.print()\n\n        # Wait for task to complete\n        console.print(\"[dim]Waiting for final result...[/dim]\")\n        result = await task_obj.result()\n\n        console.print(\"\\n[bold]Result:[/bold]\")\n        assert isinstance(result.content[0], TextContent)\n        console.print(f\"  {result.content[0].text}\")\n\n\n@app.command\nasync def immediate(\n    duration: Annotated[\n        int,\n        cyclopts.Parameter(help=\"Duration of computation in seconds (1-60)\"),\n    ] = 5,\n):\n    \"\"\"Execute the tool immediately (blocks until complete).\"\"\"\n    if duration < 1 or duration > 60:\n        console.print(\"[red]Error: Duration must be between 1 and 60 seconds[/red]\")\n        sys.exit(1)\n\n    server = load_server()\n\n    console.print(f\"\\n[bold]Calling slow_computation(duration={duration})[/bold]\")\n    console.print(\"Mode: [cyan]Immediate execution[/cyan]\\n\")\n\n    async with Client(server) as client:\n        result = await client.call_tool(\n            \"slow_computation\",\n            arguments={\"duration\": duration},\n        )\n\n        console.print(\"\\n[bold]Result:[/bold]\")\n        assert isinstance(result.content[0], TextContent)\n        console.print(f\"  {result.content[0].text}\")\n\n\nif __name__ == \"__main__\":\n    app()\n"
  },
  {
    "path": "examples/tasks/docker-compose.yml",
    "content": "services:\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"24242:6379\"\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 5s\n      timeout: 3s\n      retries: 5\n"
  },
  {
    "path": "examples/tasks/server.py",
    "content": "\"\"\"\nFastMCP Tasks Example Server\n\nDemonstrates background task execution with progress tracking using Docket.\n\nSetup:\n    1. Start Redis: docker compose up -d\n    2. Load environment: source .envrc\n    3. Run server: fastmcp run server.py\n\nThe example uses Redis by default to demonstrate distributed task execution\nand the fastmcp tasks CLI commands.\n\"\"\"\n\nimport asyncio\nimport logging\nfrom typing import Annotated\n\nfrom docket import Logged\n\nfrom fastmcp import FastMCP\nfrom fastmcp.dependencies import Progress\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n# Create server\nmcp = FastMCP(\"Tasks Example\")\n\n\n@mcp.tool(task=True)\nasync def slow_computation(\n    duration: Annotated[int, Logged],\n    progress: Progress = Progress(),\n) -> str:\n    \"\"\"\n    Perform a slow computation that takes `duration` seconds.\n\n    This tool demonstrates progress tracking with background tasks.\n    It logs progress every 1-2 seconds and reports progress via Docket.\n\n    Args:\n        duration: Number of seconds the computation should take (1-60)\n\n    Returns:\n        A completion message with the total duration\n    \"\"\"\n    if duration < 1 or duration > 60:\n        raise ValueError(\"Duration must be between 1 and 60 seconds\")\n\n    logger.info(f\"Starting slow computation for {duration} seconds\")\n\n    # Set total progress units\n    await progress.set_total(duration)\n\n    # Process each second\n    for i in range(duration):\n        # Sleep for 1 second\n        await asyncio.sleep(1)\n\n        # Update progress\n        elapsed = i + 1\n        remaining = duration - elapsed\n        await progress.increment()\n        await progress.set_message(\n            f\"Working... {elapsed}/{duration}s ({remaining}s remaining)\"\n        )\n\n        # Log every 1-2 seconds\n        if elapsed % 2 == 0 or elapsed == duration:\n            logger.info(f\"Progress: {elapsed}/{duration}s\")\n\n    logger.info(f\"Completed computation in {duration} seconds\")\n    return f\"Computation completed successfully in {duration} seconds!\"\n"
  },
  {
    "path": "examples/testing_demo/README.md",
    "content": "# FastMCP Testing Demo\n\nA comprehensive example demonstrating FastMCP testing patterns with pytest-asyncio.\n\n## Overview\n\nThis example shows how to:\n- Set up pytest-asyncio configuration in `pyproject.toml`\n- Write async test fixtures for MCP clients\n- Test tools, resources, and prompts\n- Use parametrized tests for multiple scenarios\n- Leverage inline-snapshot and dirty-equals for assertions\n\n## Project Structure\n\n```\ntesting_demo/\n├── pyproject.toml          # Project config with pytest-asyncio setup\n├── server.py               # Simple MCP server with tools/resources/prompts\n├── tests/\n│   └── test_server.py      # Comprehensive test suite\n└── README.md               # This file\n```\n\n## Key Features\n\n### pyproject.toml Configuration\n\nThe `pyproject.toml` includes the critical pytest-asyncio configuration:\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\n```\n\nThis eliminates the need for `@pytest.mark.asyncio` decorators on every async test.\n\n### Server Components\n\nThe demo server (`server.py`) includes:\n- **Tools**: `add`, `greet`, `async_multiply`\n- **Resources**: `demo://info`, `demo://greeting/{name}`\n- **Prompts**: `hello`, `explain`\n\n### Test Patterns\n\nThe test suite demonstrates:\n1. **Async fixture pattern**: Proper client fixture using `async with`\n2. **Tool testing**: Calling tools and checking results via `.data` attribute\n3. **Resource testing**: Reading static and templated resources\n4. **Prompt testing**: Getting prompts with different arguments\n5. **Parametrized tests**: Testing multiple scenarios efficiently\n6. **Schema validation**: Verifying tool schemas and structure\n7. **Pattern matching**: Using dirty-equals for flexible assertions\n\n## Running the Tests\n\n```bash\n# Install dependencies\nuv sync\n\n# Run all tests\nuv run pytest\n\n# Run with verbose output\nuv run pytest -v\n\n# Run a specific test\nuv run pytest tests/test_server.py::test_add_tool\n```\n\n## Running the Server\n\n```bash\n# Run the server\nuv run fastmcp run server.py\n\n# Inspect the server\nuv run fastmcp inspect server.py\n```\n\n## Learning More\n\nFor detailed information about testing FastMCP servers, see the [Testing Documentation](../../docs/patterns/testing.mdx).\n"
  },
  {
    "path": "examples/testing_demo/pyproject.toml",
    "content": "[project]\nname = \"testing-demo\"\nversion = \"0.1.0\"\ndescription = \"FastMCP testing example demonstrating pytest-asyncio patterns\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"fastmcp>=2.0.0\",\n    \"pytest>=8.3.3\",\n    \"pytest-asyncio>=1.2.0\",\n    \"dirty-equals>=0.9.0\",\n]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\ntestpaths = [\"tests\"]\npythonpath = [\".\"]\npython_files = [\"test_*.py\"]\n"
  },
  {
    "path": "examples/testing_demo/server.py",
    "content": "\"\"\"\nFastMCP Testing Demo Server\n\nA simple MCP server demonstrating tools, resources, and prompts\nwith comprehensive test coverage.\n\"\"\"\n\nfrom fastmcp import FastMCP\n\n# Create server\nmcp = FastMCP(\"Testing Demo\")\n\n\n# Tools\n@mcp.tool\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers together\"\"\"\n    return a + b\n\n\n@mcp.tool\ndef greet(name: str, greeting: str = \"Hello\") -> str:\n    \"\"\"Greet someone with a customizable greeting\"\"\"\n    return f\"{greeting}, {name}!\"\n\n\n@mcp.tool\nasync def async_multiply(x: float, y: float) -> float:\n    \"\"\"Multiply two numbers (async example)\"\"\"\n    return x * y\n\n\n# Resources\n@mcp.resource(\"demo://info\")\ndef server_info() -> str:\n    \"\"\"Get server information\"\"\"\n    return \"This is the FastMCP Testing Demo server\"\n\n\n@mcp.resource(\"demo://greeting/{name}\")\ndef greeting_resource(name: str) -> str:\n    \"\"\"Get a personalized greeting resource\"\"\"\n    return f\"Welcome to FastMCP, {name}!\"\n\n\n# Prompts\n@mcp.prompt(\"hello\")\ndef hello_prompt(name: str = \"World\") -> str:\n    \"\"\"Generate a hello world prompt\"\"\"\n    return f\"Say hello to {name} in a friendly way.\"\n\n\n@mcp.prompt(\"explain\")\ndef explain_prompt(topic: str, detail_level: str = \"medium\") -> str:\n    \"\"\"Generate a prompt to explain a topic\"\"\"\n    if detail_level == \"simple\":\n        return f\"Explain {topic} in simple terms for beginners.\"\n    elif detail_level == \"detailed\":\n        return f\"Provide a detailed, technical explanation of {topic}.\"\n    else:\n        return f\"Explain {topic} with moderate technical detail.\"\n"
  },
  {
    "path": "examples/testing_demo/tests/test_server.py",
    "content": "\"\"\"\nTests for the Testing Demo server.\n\nDemonstrates pytest-asyncio patterns, fixtures, and testing best practices.\n\"\"\"\n\nimport pytest\nfrom dirty_equals import IsStr\n\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def client():\n    \"\"\"\n    Client fixture for testing.\n\n    Uses async context manager and yields client synchronously.\n    No @pytest.mark.asyncio needed - asyncio_mode = \"auto\" handles it.\n    \"\"\"\n    # Import here to avoid import-time side effects\n    from server import mcp\n\n    async with Client(mcp) as client:\n        yield client\n\n\nasync def test_add_tool(client: Client):\n    \"\"\"Test the add tool with simple addition\"\"\"\n    result = await client.call_tool(\"add\", {\"a\": 2, \"b\": 3})\n    assert result.data == 5\n\n\nasync def test_greet_tool_default(client: Client):\n    \"\"\"Test the greet tool with default greeting\"\"\"\n    result = await client.call_tool(\"greet\", {\"name\": \"Alice\"})\n    assert result.data == \"Hello, Alice!\"\n\n\nasync def test_greet_tool_custom(client: Client):\n    \"\"\"Test the greet tool with custom greeting\"\"\"\n    result = await client.call_tool(\"greet\", {\"name\": \"Bob\", \"greeting\": \"Hi\"})\n    assert result.data == \"Hi, Bob!\"\n\n\nasync def test_async_multiply_tool(client: Client):\n    \"\"\"Test the async multiply tool\"\"\"\n    result = await client.call_tool(\"async_multiply\", {\"x\": 3.5, \"y\": 2.0})\n    assert result.data == 7.0\n\n\n@pytest.mark.parametrize(\n    \"a,b,expected\",\n    [\n        (0, 0, 0),\n        (1, 1, 2),\n        (-1, 1, 0),\n        (100, 200, 300),\n    ],\n)\nasync def test_add_parametrized(client: Client, a: int, b: int, expected: int):\n    \"\"\"Test add tool with multiple parameter combinations\"\"\"\n    result = await client.call_tool(\"add\", {\"a\": a, \"b\": b})\n    assert result.data == expected\n\n\nasync def test_server_info_resource(client: Client):\n    \"\"\"Test the server info resource\"\"\"\n    result = await client.read_resource(\"demo://info\")\n    assert len(result) == 1\n    assert result[0].text == \"This is the FastMCP Testing Demo server\"\n\n\nasync def test_greeting_resource_template(client: Client):\n    \"\"\"Test the greeting resource template\"\"\"\n    result = await client.read_resource(\"demo://greeting/Charlie\")\n    assert len(result) == 1\n    assert result[0].text == \"Welcome to FastMCP, Charlie!\"\n\n\nasync def test_hello_prompt_default(client: Client):\n    \"\"\"Test hello prompt with default parameter\"\"\"\n    result = await client.get_prompt(\"hello\")\n    assert result.messages[0].content.text == \"Say hello to World in a friendly way.\"\n\n\nasync def test_hello_prompt_custom(client: Client):\n    \"\"\"Test hello prompt with custom name\"\"\"\n    result = await client.get_prompt(\"hello\", {\"name\": \"Dave\"})\n    assert result.messages[0].content.text == \"Say hello to Dave in a friendly way.\"\n\n\nasync def test_explain_prompt_levels(client: Client):\n    \"\"\"Test explain prompt with different detail levels\"\"\"\n    # Simple level\n    result = await client.get_prompt(\n        \"explain\", {\"topic\": \"MCP\", \"detail_level\": \"simple\"}\n    )\n    assert \"simple terms\" in result.messages[0].content.text\n    assert \"MCP\" in result.messages[0].content.text\n\n    # Detailed level\n    result = await client.get_prompt(\n        \"explain\", {\"topic\": \"MCP\", \"detail_level\": \"detailed\"}\n    )\n    assert \"detailed\" in result.messages[0].content.text\n    assert \"technical\" in result.messages[0].content.text\n\n\nasync def test_list_tools(client: Client):\n    \"\"\"Test listing available tools\"\"\"\n    tools = await client.list_tools()\n    tool_names = [tool.name for tool in tools]\n\n    assert \"add\" in tool_names\n    assert \"greet\" in tool_names\n    assert \"async_multiply\" in tool_names\n\n\nasync def test_list_resources(client: Client):\n    \"\"\"Test listing available resources\"\"\"\n    resources = await client.list_resources()\n    resource_uris = [str(resource.uri) for resource in resources]\n\n    # Check that we have at least the static resource\n    assert \"demo://info\" in resource_uris\n    # There should be at least one resource listed\n    assert len(resource_uris) >= 1\n\n\nasync def test_list_prompts(client: Client):\n    \"\"\"Test listing available prompts\"\"\"\n    prompts = await client.list_prompts()\n    prompt_names = [prompt.name for prompt in prompts]\n\n    assert \"hello\" in prompt_names\n    assert \"explain\" in prompt_names\n\n\n# Example using dirty-equals for flexible assertions\nasync def test_greet_with_dirty_equals(client: Client):\n    \"\"\"Test greet tool using dirty-equals for pattern matching\"\"\"\n    result = await client.call_tool(\"greet\", {\"name\": \"Eve\"})\n    # Check that result data matches the pattern\n    assert result.data == IsStr(regex=r\"^Hello, \\w+!$\")\n\n\n# Example using inline-snapshot for complex data\nasync def test_tool_schema_structure(client: Client):\n    \"\"\"Test tool schema structure\"\"\"\n    tools = await client.list_tools()\n    add_tool = next(tool for tool in tools if tool.name == \"add\")\n\n    # Verify basic schema structure\n    assert add_tool.name == \"add\"\n    assert add_tool.description == \"Add two numbers together\"\n    assert \"a\" in add_tool.inputSchema[\"properties\"]\n    assert \"b\" in add_tool.inputSchema[\"properties\"]\n    assert add_tool.inputSchema[\"properties\"][\"a\"][\"type\"] == \"integer\"\n    assert add_tool.inputSchema[\"properties\"][\"b\"][\"type\"] == \"integer\"\n"
  },
  {
    "path": "examples/text_me.py",
    "content": "# /// script\n# dependencies = [\"fastmcp\"]\n# ///\n\n\"\"\"\nFastMCP Text Me Server\n--------------------------------\nThis defines a simple FastMCP server that sends a text message to a phone number via https://surgemsg.com/.\n\nTo run this example, create a `.env` file with the following values:\n\nSURGE_API_KEY=...\nSURGE_ACCOUNT_ID=...\nSURGE_MY_PHONE_NUMBER=...\nSURGE_MY_FIRST_NAME=...\nSURGE_MY_LAST_NAME=...\n\nVisit https://surgemsg.com/ and click \"Get Started\" to obtain these values.\n\"\"\"\n\nfrom typing import Annotated\n\nimport httpx\nfrom pydantic import BeforeValidator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nfrom fastmcp import FastMCP\n\n\nclass SurgeSettings(BaseSettings):\n    model_config: SettingsConfigDict = SettingsConfigDict(\n        env_prefix=\"SURGE_\", env_file=\".env\"\n    )\n\n    api_key: str\n    account_id: str\n    my_phone_number: Annotated[\n        str, BeforeValidator(lambda v: \"+\" + v if not v.startswith(\"+\") else v)\n    ]\n    my_first_name: str\n    my_last_name: str\n\n\n# Create server\nmcp = FastMCP(\"Text me\")\nsurge_settings = SurgeSettings()  # type: ignore\n\n\n@mcp.tool(name=\"textme\", description=\"Send a text message to me\")\ndef text_me(text_content: str) -> str:\n    \"\"\"Send a text message to a phone number via https://surgemsg.com/\"\"\"\n    with httpx.Client() as client:\n        response = client.post(\n            \"https://api.surgemsg.com/messages\",\n            headers={\n                \"Authorization\": f\"Bearer {surge_settings.api_key}\",\n                \"Surge-Account\": surge_settings.account_id,\n                \"Content-Type\": \"application/json\",\n            },\n            json={\n                \"body\": text_content,\n                \"conversation\": {\n                    \"contact\": {\n                        \"first_name\": surge_settings.my_first_name,\n                        \"last_name\": surge_settings.my_last_name,\n                        \"phone_number\": surge_settings.my_phone_number,\n                    }\n                },\n            },\n        )\n        response.raise_for_status()\n        return f\"Message sent: {text_content}\"\n"
  },
  {
    "path": "examples/tool_result_echo.py",
    "content": "\"\"\"\nFastMCP Echo Server with Metadata\n\nDemonstrates how to return metadata alongside content and structured data.\nThe meta field can include execution details, versioning, or other information\nthat clients may find useful.\n\"\"\"\n\nimport time\nfrom dataclasses import dataclass\n\nfrom fastmcp import FastMCP\nfrom fastmcp.tools.tool import ToolResult\n\nmcp = FastMCP(\"Echo Server\")\n\n\n@dataclass\nclass EchoData:\n    data: str\n    length: int\n\n\n@mcp.tool\ndef echo(text: str) -> ToolResult:\n    \"\"\"Echo text back with metadata about the operation.\"\"\"\n    start = time.perf_counter()\n\n    result = EchoData(data=text, length=len(text))\n\n    execution_time = (time.perf_counter() - start) * 1000\n\n    return ToolResult(\n        content=f\"Echoed: {text}\",\n        structured_content=result,\n        meta={\n            \"execution_time_ms\": round(execution_time, 2),\n            \"character_count\": len(text),\n            \"word_count\": len(text.split()),\n        },\n    )\n"
  },
  {
    "path": "examples/versioning/client_version_selection.py",
    "content": "\"\"\"\nClient-Side Version Selection\n\nDiscover available versions via metadata and request specific versions\nwhen calling tools, prompts, or resources.\n\nRun: uv run python examples/versioning/client_version_selection.py\n\"\"\"\n\nimport asyncio\n\nfrom rich import print\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.exceptions import ToolError\n\nmcp = FastMCP(\"Payment API\")\n\n\n@mcp.tool(version=\"1.0\")\ndef charge(amount: int, currency: str = \"USD\") -> dict:\n    \"\"\"Charge a payment (v1.0 - basic).\"\"\"\n    return {\"status\": \"charged\", \"amount\": amount, \"currency\": currency}\n\n\n@mcp.tool(version=\"1.1\")\ndef charge(  # noqa: F811\n    amount: int, currency: str = \"USD\", idempotency_key: str | None = None\n) -> dict:\n    \"\"\"Charge a payment (v1.1 - added idempotency).\"\"\"\n    return {\"status\": \"charged\", \"amount\": amount, \"idempotency_key\": idempotency_key}\n\n\n@mcp.tool(version=\"2.0\")\ndef charge(  # noqa: F811\n    amount: int,\n    currency: str = \"USD\",\n    idempotency_key: str | None = None,\n    metadata: dict | None = None,\n) -> dict:\n    \"\"\"Charge a payment (v2.0 - added metadata).\"\"\"\n    return {\"status\": \"charged\", \"amount\": amount, \"metadata\": metadata or {}}\n\n\nasync def main():\n    async with Client(mcp) as client:\n        # Discover versions via metadata\n        tools = await client.list_tools()\n        tool = tools[0]\n        meta = tool.meta.get(\"fastmcp\", {})\n\n        print(f\"[bold]{tool.name}[/]\")\n        print(f\"  Current version: [green]{meta.get('version')}[/]\")\n        print(f\"  All versions:    {meta.get('versions')}\")\n\n        # Call specific versions\n        print(\"\\n[bold]Calling specific versions:[/]\")\n\n        r1 = await client.call_tool(\"charge\", {\"amount\": 100}, version=\"1.0\")\n        print(f\"  v1.0: {r1.data}\")\n\n        r1_1 = await client.call_tool(\n            \"charge\", {\"amount\": 100, \"idempotency_key\": \"abc\"}, version=\"1.1\"\n        )\n        print(f\"  v1.1: {r1_1.data}\")\n\n        r2 = await client.call_tool(\n            \"charge\", {\"amount\": 100, \"metadata\": {\"order\": \"xyz\"}}, version=\"2.0\"\n        )\n        print(f\"  v2.0: {r2.data}\")\n\n        # Handle missing versions\n        print(\"\\n[bold]Missing version:[/]\")\n        try:\n            await client.call_tool(\"charge\", {\"amount\": 100}, version=\"99.0\")\n        except ToolError as e:\n            print(f\"  [red]ToolError:[/] {e}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/versioning/version_filters.py",
    "content": "\"\"\"\nVersion Filters for API Surfaces\n\nUse VersionFilter to create distinct API surfaces from shared components.\nThis lets you serve v1, v2, v3 APIs from a single codebase.\n\nRun: uv run python examples/versioning/version_filters.py\n\"\"\"\n\nimport asyncio\n\nfrom rich import print\nfrom rich.table import Table\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.providers import LocalProvider\nfrom fastmcp.server.transforms import VersionFilter\n\n# Shared component pool with all versions\ncomponents = LocalProvider()\n\n\n@components.tool(version=\"1.0\")\ndef process(data: str) -> str:\n    \"\"\"Process data (v1 - uppercase only).\"\"\"\n    return data.upper()\n\n\n@components.tool(version=\"2.0\")\ndef process(data: str, mode: str = \"upper\") -> str:  # noqa: F811\n    \"\"\"Process data (v2 - with mode selection).\"\"\"\n    return data.lower() if mode == \"lower\" else data.upper()\n\n\n@components.tool(version=\"3.0\")\ndef process(data: str, mode: str = \"upper\", repeat: int = 1) -> str:  # noqa: F811\n    \"\"\"Process data (v3 - with repeat).\"\"\"\n    result = data.lower() if mode == \"lower\" else data.upper()\n    return result * repeat\n\n\n# Unversioned components pass through all filters\n@components.tool()\ndef health() -> str:\n    \"\"\"Health check (always available).\"\"\"\n    return \"ok\"\n\n\n# Create filtered API surfaces\napi_v1 = FastMCP(\"API v1\", providers=[components])\napi_v1.add_transform(VersionFilter(version_lt=\"2.0\"))\n\napi_v2 = FastMCP(\"API v2\", providers=[components])\napi_v2.add_transform(VersionFilter(version_gte=\"2.0\", version_lt=\"3.0\"))\n\napi_v3 = FastMCP(\"API v3\", providers=[components])\napi_v3.add_transform(VersionFilter(version_gte=\"3.0\"))\n\n\nasync def show_surface(name: str, server: FastMCP):\n    \"\"\"Show what's visible through a filtered server.\"\"\"\n    async with Client(server) as client:\n        tools = await client.list_tools()\n\n        table = Table(title=name)\n        table.add_column(\"Tool\")\n        table.add_column(\"Version\", style=\"green\")\n\n        for tool in tools:\n            meta = tool.meta.get(\"fastmcp\", {}) if tool.meta else {}\n            table.add_row(tool.name, meta.get(\"version\", \"(unversioned)\"))\n\n        print(table)\n\n\nasync def main():\n    # Show what each API surface exposes\n    await show_surface(\"API v1 (version_lt='2.0')\", api_v1)\n    await show_surface(\"API v2 (version_gte='2.0', version_lt='3.0')\", api_v2)\n    await show_surface(\"API v3 (version_gte='3.0')\", api_v3)\n\n    # Same tool name, different behavior per API\n    print(\"\\n[bold]Same call through different APIs:[/]\")\n    for name, server in [(\"v1\", api_v1), (\"v2\", api_v2), (\"v3\", api_v3)]:\n        async with Client(server) as client:\n            result = await client.call_tool(\"process\", {\"data\": \"Hello\"})\n            print(f\"  API {name}: process('Hello') -> '{result.data}'\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/versioning/versioned_components.py",
    "content": "\"\"\"\nCreating Versioned Components\n\nRegister multiple versions of the same tool, resource, or prompt.\nClients see the highest version by default but can request specific versions.\n\nRun: uv run python examples/versioning/versioned_components.py\n\"\"\"\n\nimport asyncio\n\nfrom rich import print\nfrom rich.table import Table\n\nfrom fastmcp import Client, FastMCP\n\nmcp = FastMCP(\"Versioned API\")\n\n\n# --- Versioned Tools ---\n# Same name, different versions with different signatures\n\n\n@mcp.tool(version=\"1.0\")\ndef calculate(x: int, y: int) -> int:\n    \"\"\"Add two numbers (v1.0).\"\"\"\n    return x + y\n\n\n@mcp.tool(version=\"2.0\")\ndef calculate(x: int, y: int, z: int = 0) -> int:  # noqa: F811\n    \"\"\"Add two or three numbers (v2.0).\"\"\"\n    return x + y + z\n\n\n# --- Versioned Resources ---\n# Same URI, different content per version\n\n\n@mcp.resource(\"config://app\", version=\"1.0\")\ndef config_v1() -> str:\n    return '{\"format\": \"legacy\"}'\n\n\n@mcp.resource(\"config://app\", version=\"2.0\")\ndef config_v2() -> str:\n    return '{\"format\": \"modern\", \"telemetry\": true}'\n\n\n# --- Versioned Prompts ---\n# Same prompt, different templates per version\n\n\n@mcp.prompt(version=\"1.0\")\ndef summarize(text: str) -> str:\n    return f\"Summarize: {text}\"\n\n\n@mcp.prompt(version=\"2.0\")\ndef summarize(text: str, style: str = \"concise\") -> str:  # noqa: F811\n    return f\"Summarize in a {style} style: {text}\"\n\n\nasync def main():\n    async with Client(mcp) as client:\n        # List components - clients see highest version + all available versions\n        tools = await client.list_tools()\n\n        table = Table(title=\"Components (as seen by clients)\")\n        table.add_column(\"Type\")\n        table.add_column(\"Name\")\n        table.add_column(\"Version\", style=\"green\")\n        table.add_column(\"All Versions\", style=\"dim\")\n\n        for tool in tools:\n            meta = tool.meta.get(\"fastmcp\", {}) if tool.meta else {}\n            table.add_row(\n                \"Tool\",\n                tool.name,\n                meta.get(\"version\"),\n                \", \".join(meta.get(\"versions\", [])),\n            )\n\n        print(table)\n\n        # Call specific versions\n        print(\"\\n[bold]Calling specific versions:[/]\")\n\n        r_default = await client.call_tool(\"calculate\", {\"x\": 5, \"y\": 3})\n        r_v1 = await client.call_tool(\"calculate\", {\"x\": 5, \"y\": 3}, version=\"1.0\")\n        r_v2 = await client.call_tool(\n            \"calculate\", {\"x\": 5, \"y\": 3, \"z\": 2}, version=\"2.0\"\n        )\n\n        print(f\"  calculate(5, 3)          -> {r_default.data}  (default: highest)\")\n        print(f\"  calculate(5, 3) v1.0     -> {r_v1.data}\")\n        print(f\"  calculate(5, 3, 2) v2.0  -> {r_v2.data}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "justfile",
    "content": "# Build the project\nbuild:\n    uv sync\n\n# Run tests\ntest: build\n    uv run --frozen pytest -xvs tests\n\n# Run ty type checker on all files\ntypecheck:\n    uv run --frozen ty check\n\n# Serve documentation locally\ndocs:\n    cd docs && npx --yes mint@latest dev\n\n# Check for broken links in documentation\ndocs-broken-links:\n    cd docs && npx --yes mint@latest broken-links\n\n# Generate API reference documentation for all modules\napi-ref-all:\n    uvx --with-editable . --refresh-package mdxify mdxify@latest --all --root-module fastmcp --anchor-name \"Python SDK\" --exclude fastmcp.contrib\n# Generate API reference for specific modules (e.g., just api-ref prefect.flows prefect.tasks)\napi-ref *MODULES:\n    uvx --with-editable . --refresh-package mdxify mdxify@latest {{MODULES}} --root-module fastmcp --anchor-name \"Python SDK\"\n\n# Clean up API reference documentation\napi-ref-clean:\n    rm -rf docs/python-sdk\n\ncopy-context:\n    uvx --with-editable . --refresh-package copychat copychat@latest src/ docs/ -x changelog.mdx -x python-sdk/ -v"
  },
  {
    "path": "logo.py",
    "content": "LOGO_ASCII_3 = (\n    \" \\x1b[38;2;0;170;255m▄\\x1b[38;2;0;142;255m▀\\x1b[38;2;0;114;255m▀\\x1b[38;2;0;86;255m▀\\x1b[39m\\n\"\n    \" \\x1b[38;2;0;170;255m█\\x1b[38;2;0;142;255m▀\\x1b[38;2;0;114;255m▀\\x1b[39m\\n\"\n    \"\\x1b[38;2;0;170;255m▀\\x1b[39m\\n\"\n    \"\\x1b[0m\"\n)\n\nLOGO_ASCII_4 = (\n    \"\\x1b[38;2;0;198;255m \\x1b[38;2;0;195;255m▄\\x1b[38;2;0;192;255m▀\\x1b[38;2;0;189;255m▀\\x1b[38;2;0;186;255m \\x1b[38;2;0;184;255m▄\\x1b[38;2;0;181;255m▀\\x1b[38;2;0;178;255m█\\x1b[38;2;0;175;255m \"\n    \"\\x1b[38;2;0;172;255m█\\x1b[38;2;0;169;255m▀\\x1b[38;2;0;166;255m▀\\x1b[38;2;0;163;255m \"\n    \"\\x1b[38;2;0;160;255m▀\\x1b[38;2;0;157;255m█\\x1b[38;2;0;155;255m▀\\x1b[38;2;0;152;255m \"\n    \"\\x1b[38;2;0;149;255m█\\x1b[38;2;0;146;255m▀\\x1b[38;2;0;143;255m▄\\x1b[38;2;0;140;255m▀\\x1b[38;2;0;137;255m█\\x1b[38;2;0;134;255m \"\n    \"\\x1b[38;2;0;131;255m█\\x1b[38;2;0;128;255m▀\\x1b[38;2;0;126;255m▀\\x1b[38;2;0;123;255m \"\n    \"\\x1b[38;2;0;120;255m█\\x1b[38;2;0;117;255m▀\\x1b[38;2;0;114;255m█\\x1b[39m\\n\"\n    \"\\x1b[38;2;0;198;255m \\x1b[38;2;0;195;255m█\\x1b[38;2;0;192;255m▀\\x1b[38;2;0;189;255m \\x1b[38;2;0;186;255m \\x1b[38;2;0;184;255m█\\x1b[38;2;0;181;255m▀\\x1b[38;2;0;178;255m█\\x1b[38;2;0;175;255m \"\n    \"\\x1b[38;2;0;172;255m▄\\x1b[38;2;0;169;255m▄\\x1b[38;2;0;166;255m█\\x1b[38;2;0;163;255m \"\n    \"\\x1b[38;2;0;160;255m \\x1b[38;2;0;157;255m█\\x1b[38;2;0;155;255m \\x1b[38;2;0;152;255m \"\n    \"\\x1b[38;2;0;149;255m█\\x1b[38;2;0;146;255m \\x1b[38;2;0;143;255m▀\\x1b[38;2;0;140;255m \\x1b[38;2;0;137;255m█\\x1b[38;2;0;134;255m \"\n    \"\\x1b[38;2;0;131;255m█\\x1b[38;2;0;128;255m▄\\x1b[38;2;0;126;255m▄\\x1b[38;2;0;123;255m \"\n    \"\\x1b[38;2;0;120;255m█\\x1b[38;2;0;117;255m▀\\x1b[38;2;0;114;255m▀\\x1b[39m\\n\"\n    \"\\x1b[38;2;0;198;255m \\x1b[39m\\n\"\n    \"\\x1b[0m\"\n)\n\nprint(LOGO_ASCII_3)\n"
  },
  {
    "path": "loq.toml",
    "content": "# loq configuration - file size enforcement\n# Run `loq baseline` to update when files exceed limits\n\ndefault_max_lines = 1000\nrespect_gitignore = true\n\nexclude = [\"**/uv.lock\", \".git/**\", \".claude/**\", \"docs/**\"]\n\n[[rules]]\npath = \"tests/**\"\nmax_lines = 1000\n\n[[rules]]\npath = \"src/fastmcp/server/context.py\"\nmax_lines = 1272\n\n[[rules]]\npath = \"src/fastmcp/server/server.py\"\nmax_lines = 3250\n\n[[rules]]\npath = \"src/fastmcp/client/client.py\"\nmax_lines = 1885\n\n[[rules]]\npath = \"src/fastmcp/server/auth/oauth_proxy/proxy.py\"\nmax_lines = 1796\n\n[[rules]]\npath = \"src/fastmcp/server/providers/local_provider.py\"\nmax_lines = 1187\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"fastmcp\"\ndynamic = [\"version\"]\ndescription = \"The fast, Pythonic way to build MCP servers and clients.\"\nauthors = [{ name = \"Jeremiah Lowin\" }]\ndependencies = [\n    \"python-dotenv>=1.1.0\",\n    \"exceptiongroup>=1.2.2\",\n    \"httpx>=0.28.1,<1.0\",\n    \"mcp>=1.24.0,<2.0\",\n    \"openapi-pydantic>=0.5.1\",\n    \"opentelemetry-api>=1.20.0\",\n    \"packaging>=24.0\",\n    \"platformdirs>=4.0.0\",\n    \"rich>=13.9.4\",\n    \"cyclopts>=4.0.0\",\n    \"authlib>=1.6.5\",\n    \"pydantic[email]>=2.11.7\",\n    \"pyyaml>=6.0,<7.0\",\n    \"pyperclip>=1.9.0\",\n    \"py-key-value-aio[filetree,keyring,memory]>=0.4.4,<0.5.0\",\n    \"uvicorn>=0.35\",\n    \"websockets>=15.0.1\",\n    \"jsonschema-path>=0.3.4\",\n    \"jsonref>=1.1.0\",\n    \"uncalled-for>=0.2.0\",\n    \"watchfiles>=1.0.0\",\n]\n\nrequires-python = \">=3.10\"\nreadme = \"README.md\"\nlicense = \"Apache-2.0\"\n\nkeywords = [\n    \"mcp\",\n    \"mcp server\",\n    \"mcp client\",\n    \"model context protocol\",\n    \"fastmcp\",\n    \"llm\",\n    \"agent\",\n]\nclassifiers = [\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: Apache Software License\",\n    \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Typing :: Typed\",\n]\n\n[project.optional-dependencies]\nanthropic = [\"anthropic>=0.48.0\"]\napps = [\"prefab-ui>=0.11.2\"]\n# PyJWT floor: transitive via msal; CVE-2026-32597 affects <= 2.11.0\nazure = [\"azure-identity>=1.16.0\", \"PyJWT>=2.12.0\"]\ncode-mode = [\"pydantic-monty==0.0.8\"]\ngemini = [\"google-genai>=1.18.0\"]\nopenai = [\"openai>=1.102.0\"]\ntasks = [\"pydocket>=0.18.0\"]\n\n[dependency-groups]\ndev = [\n    \"dirty-equals>=0.9.0\",\n    \"fastmcp[anthropic,apps,azure,code-mode,gemini,openai,tasks]\",\n    # add optional dependencies for fastmcp dev\n    \"fastapi>=0.115.12\",\n    \"opentelemetry-sdk>=1.20.0\",\n    \"inline-snapshot[dirty-equals]>=0.27.2\",\n    \"ipython>=8.12.3\",\n    \"pdbpp>=0.11.7\",\n    \"psutil>=7.0.0\",\n    \"pyinstrument>=5.0.2\",\n    \"pyperclip>=1.9.0\",\n    \"pytest>=8.3.3\",\n    \"pytest-asyncio>=1.2.0\",\n    \"pytest-cov>=6.1.1\",\n    \"pytest-env>=1.1.5\",\n    \"pytest-flakefinder>=1.1.0\",\n    \"pytest-httpx>=0.35.0\",\n    \"pytest-report>=0.2.1\",\n    \"pytest-retry>=1.7.0\",\n    \"pytest-timeout>=2.4.0\",\n    \"pytest-xdist>=3.6.1\",\n    \"ruff>=0.12.8\",\n    \"ty>=0.0.23\",\n    \"prek>=0.2.12\",\n    \"loq>=0.1.0a3\",\n    \"opentelemetry-exporter-otlp-proto-grpc>=1.39.0\",\n]\n\n[project.scripts]\nfastmcp = \"fastmcp.cli:app\"\n\n[project.urls]\nHomepage = \"https://gofastmcp.com\"\nRepository = \"https://github.com/PrefectHQ/fastmcp\"\nDocumentation = \"https://gofastmcp.com\"\n\n[build-system]\nrequires = [\"hatchling\", \"uv-dynamic-versioning>=0.7.0\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.version]\nsource = \"uv-dynamic-versioning\"\n\n[tool.hatch.metadata]\nallow-direct-references = true\n\n\n[tool.uv-dynamic-versioning]\nvcs = \"git\"\nstyle = \"pep440\"\nbump = true\nfallback-version = \"0.0.0\"\n\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"function\"\n# filterwarnings = [\"error::DeprecationWarning\"]\nfilterwarnings = [\n    # Suppress OAuth in-memory token storage warnings in tests\n    # Tests intentionally use ephemeral storage; this warning is for end users\n    \"ignore:Using in-memory token storage:UserWarning\",\n    # Treat unawaited coroutine warnings as errors - these are almost always bugs\n    \"error:coroutine .* was never awaited:RuntimeWarning\",\n    \"error:Exception ignored in.*coroutine:pytest.PytestUnraisableExceptionWarning\",\n]\ntimeout = 5\nenv = [\n    \"FASTMCP_TEST_MODE=1\",\n    'D:FASTMCP_LOG_LEVEL=DEBUG',\n    'D:FASTMCP_ENABLE_RICH_TRACEBACKS=0',\n\n]\nmarkers = [\n    \"integration: marks tests as integration tests (deselect with '-m \\\"not integration\\\"')\",\n    \"client_process: marks tests that spawn client processes via stdio transport. These can create issues when run in the same CI environment as other subprocess-based tests.\",\n]\n# Automatically mark all tests in integration_tests folder\npythonpath = [\".\"]\ntestpaths = [\"tests\"]\npython_files = [\"test_*.py\", \"*_test.py\"]\npython_classes = [\"Test*\"]\npython_functions = [\"test_*\"]\naddopts = [\"--inline-snapshot=disable\"]\n\n[tool.ty.src]\ninclude = [\"src\", \"tests\"]\nexclude = [\"**/node_modules\", \"**/__pycache__\", \".venv\", \".git\", \"dist\"]\n\n[tool.ty.environment]\npython-version = \"3.10\"\n\n[tool.ty.rules]\n# NOTE: ty currently doesn't support type narrowing with isinstance() on unions\n# See: https://github.com/astral-sh/ty/issues/122 and https://github.com/astral-sh/ty/issues/1113\n# Some code uses `# ty: ignore[invalid-argument-type]` for this limitation.\n# TODO: Remove these ignores once ty supports union narrowing\n\n[tool.ty.terminal]\nerror-on-warning = true\n\n[tool.ruff.lint]\nfixable = [\"ALL\"]\nignore = [\n    \"COM812\",\n    \"PLR0913\", # Too many arguments, MCP Servers have a lot of arguments, OKAY?!\n    \"SIM102\",  # Dont require combining if statements\n]\nextend-select = [\n    \"B\",   # flake8-bugbear: Catches actual bugs like mutable default arguments\n    \"C4\",  # flake8-comprehensions: More efficient/readable comprehensions\n    \"I\",   # flake8-builtins: Catches builtins that are not explicitly imported\n    \"PIE\", # flake8-pie: More idiomatic Python code\n    \"RUF\", # Ruff-specific: Modern best practices unique to Ruff\n    \"SIM\", # flake8-simplify: Simplifies verbose code patterns\n    \"UP\",  # flake8-unused-imports: Catches unused imports\n]\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"fastmcp\"]\n\n[tool.ruff.lint.per-file-ignores]\n\"__init__.py\" = [\"F401\", \"I001\", \"RUF013\"]\n# allow imports not at the top of the file\n\"src/fastmcp/__init__.py\" = [\"E402\"]\n\"!src/**.py\" = [ # Only enforce extended ruff rules for code in src/\n    \"B\",   # flake8-bugbear\n    \"C4\",  # flake8-comprehensions\n    \"PIE\", # flake8-pie\n    \"RUF\", # Ruff-specific\n    \"SIM\", # flake8-simplify\n]\n\n\n[tool.codespell]\nignore-words-list = \"asend,shttp,te\""
  },
  {
    "path": "scripts/auto_close_duplicates.py",
    "content": "#!/usr/bin/env python\n# /// script\n# requires-python = \">=3.10\"\n# dependencies = [\n#     \"httpx\",\n# ]\n# ///\n\"\"\"\nAuto-close duplicate GitHub issues.\n\nThis script runs on a schedule to automatically close issues that have been\nmarked as duplicates and haven't received any preventing activity.\n\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta, timezone\n\nimport httpx\n\n\n@dataclass\nclass Issue:\n    \"\"\"Represents a GitHub issue.\"\"\"\n\n    number: int\n    title: str\n    state: str\n    created_at: str\n    user_id: int\n    user_login: str\n\n\n@dataclass\nclass Comment:\n    \"\"\"Represents a GitHub comment.\"\"\"\n\n    id: int\n    body: str\n    created_at: str\n    user_id: int\n    user_login: str\n    user_type: str\n\n\n@dataclass\nclass Reaction:\n    \"\"\"Represents a reaction on a comment.\"\"\"\n\n    user_id: int\n    user_login: str\n    content: str\n\n\nclass GitHubClient:\n    \"\"\"Client for interacting with GitHub API.\"\"\"\n\n    def __init__(self, token: str, owner: str, repo: str):\n        self.token = token\n        self.owner = owner\n        self.repo = repo\n        self.headers = {\n            \"Authorization\": f\"token {token}\",\n            \"Accept\": \"application/vnd.github.v3+json\",\n        }\n        self.base_url = f\"https://api.github.com/repos/{owner}/{repo}\"\n\n    def get_potential_duplicate_issues(self) -> list[Issue]:\n        \"\"\"Fetch open issues with the potential-duplicate label.\"\"\"\n        url = f\"{self.base_url}/issues\"\n        issues = []\n\n        with httpx.Client() as client:\n            page = 1\n            while page <= 10:  # Safety limit\n                response = client.get(\n                    url,\n                    headers=self.headers,\n                    params={\n                        \"state\": \"open\",\n                        \"labels\": \"potential-duplicate\",\n                        \"per_page\": 100,\n                        \"page\": page,\n                    },\n                )\n\n                if response.status_code != 200:\n                    print(f\"Error fetching issues: {response.status_code}\")\n                    break\n\n                data = response.json()\n                if not data:\n                    break\n\n                for item in data:\n                    # Skip pull requests\n                    if \"pull_request\" in item:\n                        continue\n\n                    issues.append(\n                        Issue(\n                            number=item[\"number\"],\n                            title=item[\"title\"],\n                            state=item[\"state\"],\n                            created_at=item[\"created_at\"],\n                            user_id=item[\"user\"][\"id\"],\n                            user_login=item[\"user\"][\"login\"],\n                        )\n                    )\n\n                page += 1\n\n        return issues\n\n    def get_issue_comments(self, issue_number: int) -> list[Comment]:\n        \"\"\"Fetch all comments for an issue.\"\"\"\n        url = f\"{self.base_url}/issues/{issue_number}/comments\"\n        comments = []\n\n        with httpx.Client() as client:\n            page = 1\n            while True:\n                response = client.get(\n                    url, headers=self.headers, params={\"page\": page, \"per_page\": 100}\n                )\n\n                if response.status_code != 200:\n                    break\n\n                data = response.json()\n                if not data:\n                    break\n\n                for comment_data in data:\n                    comments.append(\n                        Comment(\n                            id=comment_data[\"id\"],\n                            body=comment_data[\"body\"],\n                            created_at=comment_data[\"created_at\"],\n                            user_id=comment_data[\"user\"][\"id\"],\n                            user_login=comment_data[\"user\"][\"login\"],\n                            user_type=comment_data[\"user\"][\"type\"],\n                        )\n                    )\n\n                page += 1\n                if page > 10:  # Safety limit\n                    break\n\n        return comments\n\n    def get_comment_reactions(\n        self, issue_number: int, comment_id: int\n    ) -> list[Reaction]:\n        \"\"\"Fetch reactions for a specific comment.\"\"\"\n        url = f\"{self.base_url}/issues/{issue_number}/comments/{comment_id}/reactions\"\n        reactions = []\n\n        with httpx.Client() as client:\n            response = client.get(url, headers=self.headers)\n\n            if response.status_code != 200:\n                return reactions\n\n            data = response.json()\n            for reaction_data in data:\n                reactions.append(\n                    Reaction(\n                        user_id=reaction_data[\"user\"][\"id\"],\n                        user_login=reaction_data[\"user\"][\"login\"],\n                        content=reaction_data[\"content\"],\n                    )\n                )\n\n        return reactions\n\n    def remove_label(self, issue_number: int, label: str) -> bool:\n        \"\"\"Remove a label from an issue.\"\"\"\n        url = f\"{self.base_url}/issues/{issue_number}/labels/{label}\"\n        with httpx.Client() as client:\n            response = client.delete(url, headers=self.headers)\n            return response.status_code in [200, 204]\n\n    def close_issue(self, issue_number: int, comment: str) -> bool:\n        \"\"\"Close an issue with a comment and add duplicate label.\"\"\"\n        # First add the comment\n        comment_url = f\"{self.base_url}/issues/{issue_number}/comments\"\n        with httpx.Client() as client:\n            response = client.post(\n                comment_url, headers=self.headers, json={\"body\": comment}\n            )\n\n            if response.status_code != 201:\n                print(f\"Failed to add comment to issue #{issue_number}\")\n                return False\n\n        # Swap labels: remove potential-duplicate, add duplicate\n        self.remove_label(issue_number, \"potential-duplicate\")\n\n        labels_url = f\"{self.base_url}/issues/{issue_number}/labels\"\n        with httpx.Client() as client:\n            response = client.post(\n                labels_url, headers=self.headers, json={\"labels\": [\"duplicate\"]}\n            )\n\n            if response.status_code not in [200, 201]:\n                print(f\"Failed to add duplicate label to issue #{issue_number}\")\n\n        # Then close the issue\n        issue_url = f\"{self.base_url}/issues/{issue_number}\"\n        with httpx.Client() as client:\n            response = client.patch(\n                issue_url, headers=self.headers, json={\"state\": \"closed\"}\n            )\n\n            return response.status_code == 200\n\n\ndef find_duplicate_comment(comments: list[Comment]) -> Comment | None:\n    \"\"\"Find a bot comment marking the issue as duplicate.\"\"\"\n    for comment in comments:\n        # Check for the specific duplicate message format from a bot\n        if (\n            comment.user_type == \"Bot\"\n            and \"possible duplicate issues\" in comment.body.lower()\n        ):\n            return comment\n    return None\n\n\ndef was_already_auto_closed(comments: list[Comment]) -> bool:\n    \"\"\"Check if this issue was already auto-closed once.\"\"\"\n    for comment in comments:\n        if (\n            comment.user_type == \"Bot\"\n            and \"closing this issue as a duplicate\" in comment.body.lower()\n        ):\n            return True\n    return False\n\n\ndef is_past_cooldown(duplicate_comment: Comment) -> bool:\n    \"\"\"Check if the 3-day cooldown period has passed.\"\"\"\n    comment_date = datetime.fromisoformat(\n        duplicate_comment.created_at.replace(\"Z\", \"+00:00\")\n    )\n    three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)\n    return comment_date <= three_days_ago\n\n\ndef has_human_activity(\n    issue: Issue,\n    duplicate_comment: Comment,\n    all_comments: list[Comment],\n    reactions: list[Reaction],\n) -> bool:\n    \"\"\"Check if there's human activity that should prevent auto-closure.\"\"\"\n    comment_date = datetime.fromisoformat(\n        duplicate_comment.created_at.replace(\"Z\", \"+00:00\")\n    )\n\n    # Check for preventing reactions (thumbs down)\n    for reaction in reactions:\n        if reaction.content in [\"-1\", \"confused\"]:\n            print(\n                f\"Issue #{issue.number}: Has preventing reaction from {reaction.user_login}\"\n            )\n            return True\n\n    # Check for any human comment after the duplicate marking\n    for comment in all_comments:\n        comment_date_check = datetime.fromisoformat(\n            comment.created_at.replace(\"Z\", \"+00:00\")\n        )\n        if comment_date_check > comment_date:\n            if comment.user_type != \"Bot\":\n                print(\n                    f\"Issue #{issue.number}: {comment.user_login} commented after duplicate marking\"\n                )\n                return True\n\n    return False\n\n\ndef main():\n    \"\"\"Main entry point for auto-closing duplicate issues.\"\"\"\n    print(\"[DEBUG] Starting auto-close duplicates script\")\n\n    # Get environment variables\n    token = os.environ.get(\"GITHUB_TOKEN\")\n    if not token:\n        raise ValueError(\"GITHUB_TOKEN environment variable is required\")\n\n    owner = os.environ.get(\"GITHUB_REPOSITORY_OWNER\", \"prefecthq\")\n    repo = os.environ.get(\"GITHUB_REPOSITORY_NAME\", \"fastmcp\")\n\n    print(f\"[DEBUG] Repository: {owner}/{repo}\")\n\n    # Initialize client\n    client = GitHubClient(token, owner, repo)\n\n    # Only fetch issues with the potential-duplicate label\n    all_issues = client.get_potential_duplicate_issues()\n\n    print(f\"[DEBUG] Found {len(all_issues)} open issues with potential-duplicate label\")\n\n    closed_count = 0\n    cleared_count = 0\n\n    for issue in all_issues:\n        # Get comments for this issue\n        comments = client.get_issue_comments(issue.number)\n\n        # Look for duplicate marking comment\n        duplicate_comment = find_duplicate_comment(comments)\n        if not duplicate_comment:\n            # Label exists but no bot comment - clean up the label\n            print(f\"[DEBUG] Issue #{issue.number} has label but no duplicate comment\")\n            client.remove_label(issue.number, \"potential-duplicate\")\n            cleared_count += 1\n            continue\n\n        # Skip if already auto-closed once (someone reopened it intentionally)\n        if was_already_auto_closed(comments):\n            print(\n                f\"[DEBUG] Issue #{issue.number} was already auto-closed, removing label\"\n            )\n            client.remove_label(issue.number, \"potential-duplicate\")\n            cleared_count += 1\n            continue\n\n        print(f\"[DEBUG] Issue #{issue.number} has duplicate comment\")\n\n        # Still in cooldown period - skip for now, check again later\n        if not is_past_cooldown(duplicate_comment):\n            print(f\"[DEBUG] Issue #{issue.number} still in 3-day cooldown period\")\n            continue\n\n        # Get reactions on the duplicate comment\n        reactions = client.get_comment_reactions(issue.number, duplicate_comment.id)\n\n        # Check for human activity that prevents closure\n        if has_human_activity(issue, duplicate_comment, comments, reactions):\n            print(f\"[DEBUG] Issue #{issue.number} has human activity, removing label\")\n            client.remove_label(issue.number, \"potential-duplicate\")\n            cleared_count += 1\n            continue\n\n        # No human activity after cooldown - close as duplicate\n        close_message = (\n            \"Closing this issue as a duplicate based on the automated analysis above.\\n\\n\"\n            \"The duplicate issues identified contain existing discussions and potential solutions. \"\n            \"Please add your 👍 to those issues if they match your use case.\\n\\n\"\n            \"If this was closed in error, please leave a comment explaining why this is not \"\n            \"a duplicate and we'll reopen it.\"\n        )\n\n        if client.close_issue(issue.number, close_message):\n            print(f\"[SUCCESS] Closed issue #{issue.number} as duplicate\")\n            closed_count += 1\n        else:\n            print(f\"[ERROR] Failed to close issue #{issue.number}\")\n\n    print(\n        f\"[DEBUG] Processing complete. Closed {closed_count} duplicates, \"\n        f\"cleared {cleared_count} from review\"\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/auto_close_needs_mre.py",
    "content": "#!/usr/bin/env python\n# /// script\n# requires-python = \">=3.10\"\n# dependencies = [\n#     \"httpx\",\n# ]\n# ///\n\"\"\"\nAuto-close issues that need MRE (Minimal Reproducible Example).\n\nThis script runs on a schedule to automatically close issues that have been\nmarked as \"needs MRE\" and haven't received activity from the issue author\nwithin 7 days.\n\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta, timezone\n\nimport httpx\n\n\n@dataclass\nclass Issue:\n    \"\"\"Represents a GitHub issue.\"\"\"\n\n    number: int\n    title: str\n    state: str\n    created_at: str\n    user_id: int\n    user_login: str\n    body: str | None\n\n\n@dataclass\nclass Comment:\n    \"\"\"Represents a GitHub comment.\"\"\"\n\n    id: int\n    body: str\n    created_at: str\n    user_id: int\n    user_login: str\n\n\n@dataclass\nclass Event:\n    \"\"\"Represents a GitHub issue event.\"\"\"\n\n    event: str\n    created_at: str\n    label_name: str | None\n\n\nclass GitHubClient:\n    \"\"\"Client for interacting with GitHub API.\"\"\"\n\n    def __init__(self, token: str, owner: str, repo: str):\n        self.token = token\n        self.owner = owner\n        self.repo = repo\n        self.headers = {\n            \"Authorization\": f\"token {token}\",\n            \"Accept\": \"application/vnd.github.v3+json\",\n        }\n        self.base_url = f\"https://api.github.com/repos/{owner}/{repo}\"\n\n    def get_issues_with_label(\n        self, label: str, page: int = 1, per_page: int = 100\n    ) -> list[Issue]:\n        \"\"\"Fetch open issues with a specific label.\"\"\"\n        url = f\"{self.base_url}/issues\"\n        issues = []\n\n        with httpx.Client() as client:\n            response = client.get(\n                url,\n                headers=self.headers,\n                params={\n                    \"state\": \"open\",\n                    \"labels\": label,\n                    \"per_page\": per_page,\n                    \"page\": page,\n                },\n            )\n\n            if response.status_code != 200:\n                print(f\"Error fetching issues: {response.status_code}\")\n                return issues\n\n            data = response.json()\n            for item in data:\n                # Skip pull requests\n                if \"pull_request\" in item:\n                    continue\n\n                issues.append(\n                    Issue(\n                        number=item[\"number\"],\n                        title=item[\"title\"],\n                        state=item[\"state\"],\n                        created_at=item[\"created_at\"],\n                        user_id=item[\"user\"][\"id\"],\n                        user_login=item[\"user\"][\"login\"],\n                        body=item.get(\"body\"),\n                    )\n                )\n\n        return issues\n\n    def get_issue_events(self, issue_number: int) -> list[Event]:\n        \"\"\"Fetch all events for an issue.\"\"\"\n        url = f\"{self.base_url}/issues/{issue_number}/events\"\n        events = []\n\n        with httpx.Client() as client:\n            page = 1\n            while True:\n                response = client.get(\n                    url, headers=self.headers, params={\"page\": page, \"per_page\": 100}\n                )\n\n                if response.status_code != 200:\n                    break\n\n                data = response.json()\n                if not data:\n                    break\n\n                for event_data in data:\n                    label_name = None\n                    if event_data[\"event\"] == \"labeled\" and \"label\" in event_data:\n                        label_name = event_data[\"label\"][\"name\"]\n\n                    events.append(\n                        Event(\n                            event=event_data[\"event\"],\n                            created_at=event_data[\"created_at\"],\n                            label_name=label_name,\n                        )\n                    )\n\n                page += 1\n                if page > 10:  # Safety limit\n                    break\n\n        return events\n\n    def get_issue_comments(self, issue_number: int) -> list[Comment]:\n        \"\"\"Fetch all comments for an issue.\"\"\"\n        url = f\"{self.base_url}/issues/{issue_number}/comments\"\n        comments = []\n\n        with httpx.Client() as client:\n            page = 1\n            while True:\n                response = client.get(\n                    url, headers=self.headers, params={\"page\": page, \"per_page\": 100}\n                )\n\n                if response.status_code != 200:\n                    break\n\n                data = response.json()\n                if not data:\n                    break\n\n                for comment_data in data:\n                    comments.append(\n                        Comment(\n                            id=comment_data[\"id\"],\n                            body=comment_data[\"body\"],\n                            created_at=comment_data[\"created_at\"],\n                            user_id=comment_data[\"user\"][\"id\"],\n                            user_login=comment_data[\"user\"][\"login\"],\n                        )\n                    )\n\n                page += 1\n                if page > 10:  # Safety limit\n                    break\n\n        return comments\n\n    def get_issue_timeline(self, issue_number: int) -> list[dict]:\n        \"\"\"Fetch timeline events for an issue (includes issue edits).\"\"\"\n        url = f\"{self.base_url}/issues/{issue_number}/timeline\"\n        timeline = []\n\n        with httpx.Client() as client:\n            page = 1\n            while True:\n                response = client.get(\n                    url,\n                    headers={\n                        **self.headers,\n                        \"Accept\": \"application/vnd.github.mockingbird-preview+json\",\n                    },\n                    params={\"page\": page, \"per_page\": 100},\n                )\n\n                if response.status_code != 200:\n                    break\n\n                data = response.json()\n                if not data:\n                    break\n\n                timeline.extend(data)\n\n                page += 1\n                if page > 10:  # Safety limit\n                    break\n\n        return timeline\n\n    def close_issue(self, issue_number: int, comment: str) -> tuple[bool, bool]:\n        \"\"\"Close an issue with a comment.\n\n        Closes first, then comments — so a failed comment never leaves\n        a misleading \"closing\" notice on a still-open issue.\n\n        Returns (closed, commented) so the caller can log partial failures.\n        \"\"\"\n        # Close the issue first\n        issue_url = f\"{self.base_url}/issues/{issue_number}\"\n        with httpx.Client() as client:\n            response = client.patch(\n                issue_url, headers=self.headers, json={\"state\": \"closed\"}\n            )\n\n            if response.status_code != 200:\n                print(\n                    f\"Failed to close issue #{issue_number}: \"\n                    f\"{response.status_code} {response.text}\"\n                )\n                return False, False\n\n        # Then add the comment\n        comment_url = f\"{self.base_url}/issues/{issue_number}/comments\"\n        with httpx.Client() as client:\n            response = client.post(\n                comment_url, headers=self.headers, json={\"body\": comment}\n            )\n\n            if response.status_code != 201:\n                print(\n                    f\"Issue #{issue_number} was closed but comment failed: \"\n                    f\"{response.status_code} {response.text}\"\n                )\n                return True, False\n\n        return True, True\n\n\ndef find_label_application_date(\n    events: list[Event], label_name: str\n) -> datetime | None:\n    \"\"\"Find when a specific label was applied to an issue.\"\"\"\n    # Look for the most recent application of this label\n    for event in reversed(events):\n        if event.event == \"labeled\" and event.label_name == label_name:\n            return datetime.fromisoformat(event.created_at.replace(\"Z\", \"+00:00\"))\n    return None\n\n\ndef has_author_activity_after(\n    issue: Issue,\n    comments: list[Comment],\n    timeline: list[dict],\n    after_date: datetime,\n) -> bool:\n    \"\"\"Check if the issue author had any activity after a specific date.\"\"\"\n    # Check for comments from author\n    for comment in comments:\n        if comment.user_id == issue.user_id:\n            comment_date = datetime.fromisoformat(\n                comment.created_at.replace(\"Z\", \"+00:00\")\n            )\n            if comment_date > after_date:\n                print(\n                    f\"Issue #{issue.number}: Author commented after label application\"\n                )\n                return True\n\n    # Check for issue body edits from author\n    for event in timeline:\n        if event.get(\"event\") == \"renamed\" or event.get(\"event\") == \"edited\":\n            if event.get(\"actor\", {}).get(\"id\") == issue.user_id:\n                event_date = datetime.fromisoformat(\n                    event[\"created_at\"].replace(\"Z\", \"+00:00\")\n                )\n                if event_date > after_date:\n                    print(\n                        f\"Issue #{issue.number}: Author edited issue after label application\"\n                    )\n                    return True\n\n    return False\n\n\ndef should_close_as_needs_mre(\n    issue: Issue,\n    label_date: datetime,\n    comments: list[Comment],\n    timeline: list[dict],\n) -> bool:\n    \"\"\"Determine if an issue should be closed for needing an MRE.\"\"\"\n    # Check if label is old enough (7 days)\n    seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)\n\n    if label_date > seven_days_ago:\n        return False\n\n    # Check for author activity after the label was applied\n    if has_author_activity_after(issue, comments, timeline, label_date):\n        return False\n\n    return True\n\n\ndef main():\n    \"\"\"Main entry point for auto-closing needs MRE issues.\"\"\"\n    print(\"[DEBUG] Starting auto-close needs MRE script\")\n\n    # Get environment variables\n    token = os.environ.get(\"GITHUB_TOKEN\")\n    if not token:\n        raise ValueError(\"GITHUB_TOKEN environment variable is required\")\n\n    owner = os.environ.get(\"GITHUB_REPOSITORY_OWNER\", \"prefecthq\")\n    repo = os.environ.get(\"GITHUB_REPOSITORY_NAME\", \"fastmcp\")\n\n    print(f\"[DEBUG] Repository: {owner}/{repo}\")\n\n    # Initialize client\n    client = GitHubClient(token, owner, repo)\n\n    # Get issues with \"needs MRE\" label\n    all_issues = []\n    page = 1\n\n    while page <= 20:  # Safety limit\n        issues = client.get_issues_with_label(\"needs MRE\", page=page)\n        if not issues:\n            break\n        all_issues.extend(issues)\n        page += 1\n\n    print(f\"[DEBUG] Found {len(all_issues)} open issues with 'needs MRE' label\")\n\n    processed_count = 0\n    closed_count = 0\n\n    for issue in all_issues:\n        processed_count += 1\n\n        if processed_count % 10 == 0:\n            print(f\"[DEBUG] Processed {processed_count}/{len(all_issues)} issues\")\n\n        # Get events to find when label was applied\n        events = client.get_issue_events(issue.number)\n        label_date = find_label_application_date(events, \"needs MRE\")\n\n        if not label_date:\n            print(\n                f\"[DEBUG] Issue #{issue.number}: Could not find label application date\"\n            )\n            continue\n\n        print(\n            f\"[DEBUG] Issue #{issue.number}: Label applied on {label_date.isoformat()}\"\n        )\n\n        # Get comments and timeline\n        comments = client.get_issue_comments(issue.number)\n        timeline = client.get_issue_timeline(issue.number)\n\n        # Check if we should close\n        if should_close_as_needs_mre(issue, label_date, comments, timeline):\n            close_message = (\n                \"This issue is being automatically closed because we requested a minimal reproducible example (MRE) \"\n                \"7 days ago and haven't received a response from the issue author.\\n\\n\"\n                \"**If you can provide an MRE**, please add it as a comment and we'll reopen this issue. \"\n                \"An MRE should be a complete, runnable code snippet that demonstrates the problem.\\n\\n\"\n                \"**If this was closed in error**, please leave a comment explaining the situation and we'll reopen it.\"\n            )\n\n            closed, commented = client.close_issue(issue.number, close_message)\n            if closed:\n                closed_count += 1\n                if commented:\n                    print(f\"[SUCCESS] Closed issue #{issue.number} (needs MRE)\")\n                else:\n                    print(\n                        f\"[WARNING] Closed issue #{issue.number} but \"\n                        f\"comment was not posted\"\n                    )\n            else:\n                print(f\"[ERROR] Failed to close issue #{issue.number}\")\n\n    print(f\"[DEBUG] Processing complete. Closed {closed_count} issues needing MRE\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/benchmark_imports.py",
    "content": "#!/usr/bin/env python\n\"\"\"Benchmark import times for fastmcp and its dependency chain.\n\nEach measurement runs in a fresh subprocess so there's no shared module cache.\nIncremental costs are measured by pre-importing dependencies, so we can see\nwhat each module truly adds.\n\nUsage:\n    uv run python scripts/benchmark_imports.py\n    uv run python scripts/benchmark_imports.py --runs 10\n    uv run python scripts/benchmark_imports.py --json\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport subprocess\nimport sys\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass BenchmarkCase:\n    label: str\n    stmt: str\n    prereqs: str = \"\"\n    group: str = \"\"\n\n\nCASES = [\n    # --- Floor ---\n    BenchmarkCase(\"pydantic\", \"import pydantic\", group=\"floor\"),\n    BenchmarkCase(\"mcp\", \"import mcp\", group=\"floor\"),\n    BenchmarkCase(\n        \"mcp (server only)\", \"import mcp.server.lowlevel.server\", group=\"floor\"\n    ),\n    # --- Auth stack (incremental over mcp) ---\n    BenchmarkCase(\n        \"authlib.jose\", \"import authlib.jose\", prereqs=\"import mcp\", group=\"auth\"\n    ),\n    BenchmarkCase(\n        \"cryptography.fernet\",\n        \"from cryptography.fernet import Fernet\",\n        prereqs=\"import mcp\",\n        group=\"auth\",\n    ),\n    BenchmarkCase(\n        \"authlib.integrations.httpx_client\",\n        \"from authlib.integrations.httpx_client import AsyncOAuth2Client\",\n        prereqs=\"import mcp\",\n        group=\"auth\",\n    ),\n    BenchmarkCase(\n        \"key_value.aio\", \"import key_value.aio\", prereqs=\"import mcp\", group=\"auth\"\n    ),\n    BenchmarkCase(\n        \"key_value.aio.stores.filetree\",\n        \"from key_value.aio.stores.filetree import FileTreeStore\",\n        prereqs=\"import mcp\",\n        group=\"auth\",\n    ),\n    BenchmarkCase(\"beartype\", \"import beartype\", prereqs=\"import mcp\", group=\"auth\"),\n    # --- Docket stack (incremental over mcp) ---\n    BenchmarkCase(\"redis\", \"import redis\", prereqs=\"import mcp\", group=\"docket\"),\n    BenchmarkCase(\n        \"opentelemetry.sdk.metrics\",\n        \"import opentelemetry.sdk.metrics\",\n        prereqs=\"import mcp\",\n        group=\"docket\",\n    ),\n    BenchmarkCase(\"docket\", \"import docket\", prereqs=\"import mcp\", group=\"docket\"),\n    BenchmarkCase(\"croniter\", \"import croniter\", prereqs=\"import mcp\", group=\"docket\"),\n    # --- Other deps (incremental over mcp) ---\n    BenchmarkCase(\"httpx\", \"import httpx\", prereqs=\"import mcp\", group=\"other\"),\n    BenchmarkCase(\n        \"starlette\",\n        \"from starlette.applications import Starlette\",\n        prereqs=\"import mcp\",\n        group=\"other\",\n    ),\n    BenchmarkCase(\n        \"pydantic_settings\",\n        \"import pydantic_settings\",\n        prereqs=\"import mcp\",\n        group=\"other\",\n    ),\n    BenchmarkCase(\n        \"rich.console\", \"import rich.console\", prereqs=\"import mcp\", group=\"other\"\n    ),\n    BenchmarkCase(\"jsonref\", \"import jsonref\", prereqs=\"import mcp\", group=\"other\"),\n    BenchmarkCase(\"requests\", \"import requests\", prereqs=\"import mcp\", group=\"other\"),\n    # --- FastMCP (total and incremental) ---\n    BenchmarkCase(\"fastmcp (total)\", \"from fastmcp import FastMCP\", group=\"fastmcp\"),\n    BenchmarkCase(\n        \"fastmcp (over mcp)\",\n        \"from fastmcp import FastMCP\",\n        prereqs=\"import mcp\",\n        group=\"fastmcp\",\n    ),\n    BenchmarkCase(\n        \"fastmcp (over mcp+docket)\",\n        \"from fastmcp import FastMCP\",\n        prereqs=\"import mcp; import docket\",\n        group=\"fastmcp\",\n    ),\n    BenchmarkCase(\n        \"fastmcp (over mcp+docket+auth deps)\",\n        \"from fastmcp import FastMCP\",\n        prereqs=(\n            \"import mcp; import docket; import authlib.jose;\"\n            \" from cryptography.fernet import Fernet;\"\n            \" import key_value.aio\"\n        ),\n        group=\"fastmcp\",\n    ),\n]\n\n\ndef measure_once(stmt: str, prereqs: str) -> float | None:\n    pre = prereqs + \"; \" if prereqs else \"\"\n    code = (\n        f\"{pre}\"\n        \"import time as _t; _s=_t.perf_counter(); \"\n        f\"{stmt}; \"\n        \"print(f'{(_t.perf_counter()-_s)*1000:.2f}')\"\n    )\n    r = subprocess.run(\n        [sys.executable, \"-c\", code],\n        capture_output=True,\n        text=True,\n        timeout=30,\n    )\n    if r.returncode == 0 and r.stdout.strip():\n        return float(r.stdout.strip())\n    return None\n\n\ndef measure(case: BenchmarkCase, runs: int) -> dict[str, float | str | None]:\n    times: list[float] = []\n    for _ in range(runs):\n        t = measure_once(case.stmt, case.prereqs)\n        if t is not None:\n            times.append(t)\n\n    if not times:\n        return {\"label\": case.label, \"group\": case.group, \"median_ms\": None}\n\n    times.sort()\n    median = times[len(times) // 2]\n    return {\n        \"label\": case.label,\n        \"group\": case.group,\n        \"median_ms\": round(median, 1),\n        \"min_ms\": round(times[0], 1),\n        \"max_ms\": round(times[-1], 1),\n        \"runs\": len(times),\n    }\n\n\ndef print_table(results: list[dict[str, float | str | None]]) -> None:\n    current_group = None\n    print(f\"\\n{'Module':<45} {'Median':>8} {'Min':>8} {'Max':>8}\")\n    print(\"-\" * 71)\n    for r in results:\n        if r[\"group\"] != current_group:\n            current_group = r[\"group\"]\n            group_labels = {\n                \"floor\": \"--- Unavoidable floor ---\",\n                \"auth\": \"--- Auth stack (incremental over mcp) ---\",\n                \"docket\": \"--- Docket stack (incremental over mcp) ---\",\n                \"other\": \"--- Other deps (incremental over mcp) ---\",\n                \"fastmcp\": \"--- FastMCP totals ---\",\n            }\n            print(f\"\\n{group_labels.get(current_group, current_group)}\")\n        if r[\"median_ms\"] is not None:\n            print(\n                f\"  {r['label']:<43} {r['median_ms']:>7.1f}ms\"\n                f\" {r['min_ms']:>7.1f}ms {r['max_ms']:>7.1f}ms\"\n            )\n        else:\n            print(f\"  {r['label']:<43}    error\")\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Benchmark fastmcp import times\")\n    parser.add_argument(\n        \"--runs\", type=int, default=5, help=\"Number of runs per measurement (default 5)\"\n    )\n    parser.add_argument(\"--json\", action=\"store_true\", help=\"Output results as JSON\")\n    args = parser.parse_args()\n\n    print(f\"Benchmarking import times ({args.runs} runs each)...\")\n    print(f\"Python: {sys.version.split()[0]}\")\n    print(f\"Executable: {sys.executable}\")\n\n    results = []\n    for case in CASES:\n        r = measure(case, args.runs)\n        results.append(r)\n        if not args.json:\n            ms = f\"{r['median_ms']:.1f}ms\" if r[\"median_ms\"] is not None else \"error\"\n            print(f\"  {case.label}: {ms}\")\n\n    if args.json:\n        print(json.dumps(results, indent=2))\n    else:\n        print_table(results)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/fastmcp-client-cli/SKILL.md",
    "content": "---\nname: fastmcp-client-cli\ndescription: Query and invoke tools on MCP servers using fastmcp list and fastmcp call. Use when you need to discover what tools a server offers, call tools, or integrate MCP servers into workflows.\n---\n\n# FastMCP CLI: List and Call\n\nUse `fastmcp list` and `fastmcp call` to interact with any MCP server from the command line.\n\n## Listing Tools\n\n```bash\n# Remote server\nfastmcp list http://localhost:8000/mcp\n\n# Local Python file (runs via fastmcp run automatically)\nfastmcp list server.py\n\n# MCPConfig with multiple servers\nfastmcp list mcp.json\n\n# Stdio command (npx, uvx, etc.)\nfastmcp list --command 'npx -y @modelcontextprotocol/server-github'\n\n# Include full input/output schemas\nfastmcp list server.py --input-schema --output-schema\n\n# Machine-readable JSON\nfastmcp list server.py --json\n\n# Include resources and prompts\nfastmcp list server.py --resources --prompts\n```\n\nDefault output shows tool signatures and descriptions. Use `--input-schema` or `--output-schema` to include full JSON schemas, `--json` for structured output.\n\n## Calling Tools\n\n```bash\n# Key=value arguments (auto-coerced to correct types)\nfastmcp call server.py greet name=World\nfastmcp call server.py add a=3 b=4\n\n# Single JSON object for complex/nested args\nfastmcp call server.py create_item '{\"name\": \"Widget\", \"tags\": [\"a\", \"b\"]}'\n\n# --input-json with key=value overrides\nfastmcp call server.py search --input-json '{\"query\": \"hello\", \"limit\": 5}' limit=10\n\n# JSON output for scripting\nfastmcp call server.py add a=3 b=4 --json\n```\n\nType coercion is automatic: `limit=5` becomes an integer, `verbose=true` becomes a boolean, based on the tool's input schema.\n\n## Server Targets\n\nAll commands accept the same server targets:\n\n| Target | Example |\n|--------|---------|\n| HTTP/HTTPS URL | `http://localhost:8000/mcp` |\n| Python file | `server.py` |\n| MCPConfig JSON | `mcp.json` (must have `mcpServers` key) |\n| Stdio command | `--command 'npx -y @mcp/server'` |\n| Discovered name | `weather` or `source:name` |\n\nServers configured in editor configs (Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose) or project-level `mcp.json` can be referenced by name. Use `source:name` (e.g. `claude-code:my-server`, `cursor:weather`) to target a specific source. Run `fastmcp discover` to see available names.\n\nFor SSE servers, pass `--transport sse`:\n\n```bash\nfastmcp list http://localhost:8000/mcp --transport sse\n```\n\n## Auth\n\nHTTP targets automatically use OAuth (no-ops if the server doesn't require auth). Disable with `--auth none`:\n\n```bash\nfastmcp call http://server/mcp tool --auth none\n```\n\n## Discovering Configured Servers\n\n```bash\n# See all MCP servers in editor/project configs\nfastmcp discover\n\n# Filter by source\nfastmcp discover --source claude-code\n\n# JSON output\nfastmcp discover --json\n```\n\nScans Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose, and `./mcp.json`. Sources: `claude-desktop`, `claude-code`, `cursor`, `gemini`, `goose`, `project`.\n\n## Workflow Pattern\n\nDiscover tools first, then call them:\n\n```bash\n# 1. See what servers are configured\nfastmcp discover\n\n# 2. See what tools a server has\nfastmcp list weather\n\n# 3. Call a tool\nfastmcp call weather get_forecast city=London\n```\n\nIf you call a nonexistent tool, FastMCP suggests close matches.\n"
  },
  {
    "path": "src/fastmcp/__init__.py",
    "content": "\"\"\"FastMCP - An ergonomic MCP interface.\"\"\"\n\nimport importlib\nimport warnings\nfrom importlib.metadata import version as _version\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.settings import Settings\nfrom fastmcp.utilities.logging import configure_logging as _configure_logging\n\nif TYPE_CHECKING:\n    from fastmcp.client import Client as Client\n    from fastmcp.server.app import FastMCPApp as FastMCPApp\n\nsettings = Settings()\nif settings.log_enabled:\n    _configure_logging(\n        level=settings.log_level,\n        enable_rich_tracebacks=settings.enable_rich_tracebacks,\n    )\n\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.server.context import Context\nimport fastmcp.server\n\n__version__ = _version(\"fastmcp\")\n\n\n# ensure deprecation warnings are displayed by default\nif settings.deprecation_warnings:\n    warnings.simplefilter(\"default\", DeprecationWarning)\n\n\n# --- Lazy imports for performance (see #3292) ---\n# Client and the client submodule are deferred so that server-only users\n# don't pay for the client import chain. Do not convert back to top-level.\n\n\ndef __getattr__(name: str) -> object:\n    if name == \"Client\":\n        from fastmcp.client import Client\n\n        return Client\n    if name == \"FastMCPApp\":\n        from fastmcp.server.app import FastMCPApp\n\n        return FastMCPApp\n    if name == \"client\":\n        return importlib.import_module(\"fastmcp.client\")\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n\n\n__all__ = [\n    \"Client\",\n    \"Context\",\n    \"FastMCP\",\n    \"FastMCPApp\",\n    \"settings\",\n]\n"
  },
  {
    "path": "src/fastmcp/cli/__init__.py",
    "content": "\"\"\"FastMCP CLI package.\"\"\"\n\nfrom .cli import app\n"
  },
  {
    "path": "src/fastmcp/cli/__main__.py",
    "content": "\"\"\"FastMCP CLI as a runnable package\"\"\"\n\nfrom .cli import app\n\napp()\n"
  },
  {
    "path": "src/fastmcp/cli/apps_dev.py",
    "content": "\"\"\"Dev server for previewing FastMCPApp UIs locally.\n\nStarts the user's MCP server on a configurable port, then starts a lightweight\nStarlette dev server that:\n\n  - Serves a Prefab-based tool picker at GET /\n  - Proxies /mcp to the user's server (avoids browser CORS restrictions)\n  - Serves the AppBridge host page at GET /launch\n\nThe host page uses @modelcontextprotocol/ext-apps to connect to the MCP server\nand render the selected UI tool inside an iframe.\n\nStartup sequence\n----------------\n1. Download ext-apps app-bridge.js from npm and patch its bare\n   ``@modelcontextprotocol/sdk/…`` imports to use concrete esm.sh URLs.\n2. Detect the exact Zod v4 module URL that esm.sh serves for that SDK version\n   and build an import-map entry that redirects the broken ``v4.mjs`` (which\n   only re-exports ``{z, default}``) to ``v4/classic/index.mjs`` (which\n   correctly exports every named Zod v4 function).  Import maps apply to the\n   full module graph in the document, including cross-origin esm.sh modules.\n3. Serve both the patched JS and the import-map JSON from the dev server.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport contextlib\nimport io\nimport json\nimport logging\nimport os\nimport re\nimport signal\nimport sys\nimport tarfile\nimport tempfile\nimport urllib.request\nimport webbrowser\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import quote\n\nimport httpcore\nimport httpx\nimport uvicorn\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.responses import HTMLResponse, Response, StreamingResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n_EXT_APPS_VERSION = \"1.0.1\"\n# Pin to the SDK version ext-apps 1.0.1 was compiled against so the client\n# and transport modules are API-compatible with the app-bridge internals.\n_MCP_SDK_VERSION = \"1.25.2\"\n\n# ---------------------------------------------------------------------------\n# Shared AppBridge host shell\n# ---------------------------------------------------------------------------\n\n# Both the picker and the app launcher use the same host-page structure: an\n# iframe that hosts a Prefab renderer, wired to the MCP server via AppBridge.\n# The only differences are (a) which URL loads in the iframe and (b) what\n# oninitialized does.\n#\n# app-bridge.js is served locally (see _fetch_app_bridge_bundle).\n# Client/Transport are loaded from esm.sh.\n# The import map (injected as {import_map_tag}) patches the broken esm.sh\n# Zod v4 module so all Zod named exports are visible to the SDK at runtime.\n\n_HOST_SHELL = \"\"\"\\\n<!doctype html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>{title}</title>\n{import_map_tag}\n  <style>\n    html, body {{ margin: 0; padding: 0; width: 100%; height: 100vh; overflow: hidden; }}\n    #app-frame {{ width: 100%; height: 100%; border: none; display: none; }}\n    #status {{\n      display: flex; align-items: center; justify-content: center; height: 100vh;\n      font-family: system-ui, sans-serif; color: #666; font-size: 1rem;\n    }}\n  </style>\n</head>\n<body>\n  <div id=\"status\" style=\"display:{status_display}\">{status_text}</div>\n  <iframe id=\"app-frame\" style=\"display:{frame_display}\"></iframe>\n  <script type=\"module\">\n    import {{ AppBridge, PostMessageTransport }}\n      from \"/js/app-bridge.js\";\n    import {{ Client }}\n      from \"https://esm.sh/@modelcontextprotocol/sdk@{mcp_sdk_version}/client/index.js\";\n    import {{ StreamableHTTPClientTransport }}\n      from \"https://esm.sh/@modelcontextprotocol/sdk@{mcp_sdk_version}/client/streamableHttp.js\";\n\n    const status = document.getElementById(\"status\");\n    const iframe  = document.getElementById(\"app-frame\");\n\n    async function main() {{\n      const client = new Client({{ name: \"fastmcp-dev\", version: \"1.0.0\" }});\n      await client.connect(\n        new StreamableHTTPClientTransport(new URL(\"/mcp\", window.location.origin))\n      );\n      const serverCaps = client.getServerCapabilities();\n\n      // Set iframe src after adding load listener to avoid race condition\n      const loaded = new Promise(r => iframe.addEventListener(\"load\", r, {{ once: true }}));\n      iframe.src = {iframe_src_json};\n      await loaded;\n\n      const transport = new PostMessageTransport(\n        iframe.contentWindow,\n        iframe.contentWindow,\n      );\n      const bridge = new AppBridge(\n        client,\n        {{ name: \"fastmcp-dev\", version: \"1.0.0\" }},\n        {{\n          openLinks: {{}},\n          serverTools: serverCaps?.tools,\n          serverResources: serverCaps?.resources,\n        }},\n        {{\n          hostContext: {{\n            theme: window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n              ? \"dark\" : \"light\",\n            platform: \"web\",\n            containerDimensions: {{ maxHeight: 8000 }},\n            displayMode: \"inline\",\n            availableDisplayModes: [\"inline\", \"fullscreen\"],\n          }},\n        }},\n      );\n\n      bridge.onmessage = async () => ({{}});\n      {on_open_link}\n      {on_initialized}\n\n      await bridge.connect(transport);\n    }}\n\n    main().catch(err => {{\n      console.error(err);\n      if (status) {{\n        status.style.display = \"flex\";\n        status.textContent = \"Error: \" + err.message;\n      }}\n    }});\n  </script>\n</body>\n</html>\n\"\"\"\n\n# ---------------------------------------------------------------------------\n# Host page HTML\n# ---------------------------------------------------------------------------\n\n_HOST_HTML_TEMPLATE = \"\"\"\\\n<!doctype html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>FastMCP Dev — {tool_name}</title>\n{import_map_tag}\n  <style>\n    html, body {{ margin: 0; padding: 0; width: 100%; height: 100vh; overflow: hidden; }}\n    #app-frame {{ width: 100%; height: 100%; border: none; display: none; }}\n    #status {{\n      display: flex; align-items: center; justify-content: center; height: 100vh;\n      font-family: system-ui, sans-serif; color: #666; font-size: 1rem;\n    }}\n  </style>\n</head>\n<body>\n  <div id=\"status\">Launching {tool_name}…</div>\n  <iframe id=\"app-frame\"></iframe>\n  <script type=\"module\">\n    import {{ AppBridge, PostMessageTransport, getToolUiResourceUri }}\n      from \"/js/app-bridge.js\";\n    import {{ Client }}\n      from \"https://esm.sh/@modelcontextprotocol/sdk@{mcp_sdk_version}/client/index.js\";\n    import {{ StreamableHTTPClientTransport }}\n      from \"https://esm.sh/@modelcontextprotocol/sdk@{mcp_sdk_version}/client/streamableHttp.js\";\n\n    const toolName = {tool_name_json};\n    const toolArgs = {tool_args_json};\n    const status = document.getElementById(\"status\");\n    const iframe  = document.getElementById(\"app-frame\");\n\n    async function main() {{\n      // Connect to the proxied MCP server (same-origin, no CORS needed)\n      const client = new Client({{ name: \"fastmcp-dev\", version: \"1.0.0\" }});\n      await client.connect(\n        new StreamableHTTPClientTransport(new URL(\"/mcp\", window.location.origin))\n      );\n\n      // Find the tool and its UI resource URI\n      const {{ tools }} = await client.listTools();\n      const tool = tools.find(t => t.name === toolName);\n      if (!tool) throw new Error(\"Tool not found: \" + toolName);\n\n      const uiUri = getToolUiResourceUri(tool);\n      if (!uiUri) throw new Error(\"Tool has no UI resource: \" + toolName);\n\n      // The Prefab renderer calls earlyBridge.connect() at module-load time\n      // (synchronously, before React mounts) so it sends its ui/initialize\n      // request very early — potentially before the iframe's load event fires.\n      // Fix: create the AppBridge and call bridge.connect() BEFORE loading the\n      // iframe so our window.addEventListener is registered first.  We pass\n      // null as the PostMessageTransport source so early messages from the\n      // not-yet-known renderer window are not filtered out.  After the iframe\n      // loads we update transport.eventTarget / .eventSource to the real\n      // renderer window; the load-event microtask always runs before the\n      // message macrotask, so the response reaches the correct window.\n      const serverCaps = client.getServerCapabilities();\n      const transport = new PostMessageTransport(iframe.contentWindow, null);\n      const bridge = new AppBridge(\n        client,\n        {{ name: \"fastmcp-dev\", version: \"1.0.0\" }},\n        {{\n          openLinks: {{}},\n          serverTools: serverCaps?.tools,\n          serverResources: serverCaps?.resources,\n        }},\n        {{\n          hostContext: {{\n            theme: window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n              ? \"dark\" : \"light\",\n            platform: \"web\",\n            containerDimensions: {{ maxHeight: 8000 }},\n            displayMode: \"inline\",\n            availableDisplayModes: [\"inline\", \"fullscreen\"],\n          }},\n        }},\n      );\n\n      bridge.onopenlink = async ({{ url }}) => {{\n        window.open(url, \"_blank\", \"noopener,noreferrer\");\n        return {{}};\n      }};\n      bridge.onmessage = async () => ({{}});\n\n      // When the View initializes: send input args, call the tool, send result\n      bridge.oninitialized = async () => {{\n        await bridge.sendToolInput({{ arguments: toolArgs }});\n        const result = await client.callTool({{ name: toolName, arguments: toolArgs }});\n        await bridge.sendToolResult(result);\n        status.style.display = \"none\";\n        iframe.style.display = \"block\";\n      }};\n\n      // Start listening before the iframe loads\n      await bridge.connect(transport);\n\n      // Now load the renderer HTML via the server-side proxy\n      const frameUrl = \"/ui-resource?uri=\" + encodeURIComponent(uiUri);\n      const loaded = new Promise(r => {{ iframe.addEventListener(\"load\", r, {{ once: true }}); }});\n      iframe.src = frameUrl;\n      await loaded;\n\n      // Update transport to the real renderer window.  This microtask runs\n      // before the ui/initialize message macrotask, ensuring the response\n      // is dispatched to the correct window.\n      transport.eventTarget = iframe.contentWindow;\n      transport.eventSource = iframe.contentWindow;\n    }}\n\n    main().catch(err => {{\n      status.textContent = \"Error: \" + err.message;\n      console.error(err);\n    }});\n  </script>\n</body>\n</html>\n\"\"\"\n\n# ---------------------------------------------------------------------------\n# Picker UI (Prefab-based, built in Python)\n# ---------------------------------------------------------------------------\n\n\ndef _has_ui_resource(tool: dict[str, Any]) -> bool:\n    \"\"\"Return True if the tool has a UI resourceUri in its metadata.\"\"\"\n    for key in (\"meta\", \"_meta\"):\n        m = tool.get(key)\n        if isinstance(m, dict):\n            ui = m.get(\"ui\")\n            if isinstance(ui, dict) and ui.get(\"resourceUri\"):\n                return True\n    return False\n\n\ndef _model_from_schema(tool_name: str, input_schema: dict[str, Any]) -> type[Any]:\n    \"\"\"Dynamically create a Pydantic model from a JSON Schema for form generation.\"\"\"\n    import pydantic\n    import pydantic.fields\n\n    properties: dict[str, Any] = input_schema.get(\"properties\") or {}\n    required: list[str] = input_schema.get(\"required\") or []\n\n    field_definitions: dict[str, Any] = {}\n    for prop_name, prop in properties.items():\n        json_type = prop.get(\"type\", \"string\")\n        match json_type:\n            case \"integer\":\n                py_type: type = int\n            case \"number\":\n                py_type = float\n            case \"boolean\":\n                py_type = bool\n            case _:\n                py_type = str\n\n        title = prop.get(\"title\") or prop_name.replace(\"_\", \" \").title()\n        description = prop.get(\"description\")\n        is_required = prop_name in required\n        if is_required:\n            default = pydantic.fields.PydanticUndefined\n        elif \"default\" in prop:\n            default = prop[\"default\"]\n        else:\n            default = None\n            py_type = py_type | None  # type: ignore[assignment]\n\n        extra: dict[str, Any] = {}\n        if prop.get(\"enum\"):\n            from typing import Literal\n\n            py_type = Literal[tuple(prop[\"enum\"])]  # type: ignore[assignment]\n        if prop.get(\"format\") == \"textarea\" or (\n            isinstance(prop.get(\"json_schema_extra\"), dict)\n            and prop[\"json_schema_extra\"].get(\"ui\", {}).get(\"type\") == \"textarea\"\n        ):\n            extra[\"json_schema_extra\"] = {\"ui\": {\"type\": \"textarea\"}}\n\n        field_definitions[prop_name] = (\n            py_type,\n            pydantic.Field(\n                default=default, title=title, description=description, **extra\n            ),\n        )\n\n    return pydantic.create_model(f\"{tool_name.title()}Form\", **field_definitions)\n\n\ndef _build_picker_html(tools: list[dict[str, Any]]) -> str:\n    \"\"\"Build Prefab picker page: dropdown selector with per-tool forms.\"\"\"\n    try:\n        from prefab_ui.actions import Fetch, OpenLink, SetState, ShowToast\n        from prefab_ui.app import PrefabApp\n        from prefab_ui.components import (\n            Button,\n            Column,\n            Heading,\n            Label,\n            Markdown,\n            Muted,\n            Page,\n            Pages,\n            Select,\n            SelectOption,\n        )\n        from prefab_ui.components.form import Form\n        from prefab_ui.rx import RESULT, Rx\n    except ImportError:\n        return \"<html><body><p>prefab-ui not installed. Run: pip install fastmcp[apps]</p></body></html>\"\n\n    if not tools:\n        with Column(gap=4, css_class=\"p-6 max-w-2xl mx-auto\") as view:\n            Heading(\"FastMCP App Preview\")\n            Muted(\n                \"No UI tools found on this server. Use @app.ui() to register entry-point tools.\"\n            )\n        return PrefabApp(title=\"FastMCP App Preview\", view=view).html()\n\n    first_name: str = tools[0][\"name\"]\n\n    def _tool_title(tool: dict[str, Any]) -> str:\n        return tool.get(\"title\") or tool[\"name\"]\n\n    with Column(gap=6, css_class=\"p-8 max-w-lg mx-auto\") as view:\n        Heading(\"FastMCP App Preview\")\n\n        if len(tools) > 1:\n            with Column(gap=1):\n                Label(\"Tool\")\n                with Select(\n                    placeholder=\"Choose a tool…\",\n                    on_change=SetState(\"activeTool\", Rx(\"$event\")),\n                ):\n                    for tool in tools:\n                        SelectOption(\n                            _tool_title(tool),\n                            value=tool[\"name\"],\n                            selected=tool[\"name\"] == first_name,\n                        )\n        else:\n            Heading(_tool_title(tools[0]), level=3)\n\n        with Pages(name=\"activeTool\", default_value=first_name):\n            for tool in tools:\n                name: str = tool[\"name\"]\n                desc: str = tool.get(\"description\") or \"\"\n                input_schema: dict[str, Any] = tool.get(\"inputSchema\") or {}\n                model = _model_from_schema(name, input_schema)\n\n                body: dict[str, Any] = {\"tool\": name}\n                for field_name in model.model_fields:\n                    body[field_name] = Rx(field_name)\n\n                with Page(name, value=name), Column(gap=4):\n                    if desc:\n                        Muted(desc, css_class=\"pb-2\")\n                    with Form(\n                        on_submit=Fetch.post(\n                            \"/api/launch\",\n                            body=body,\n                            on_success=OpenLink(RESULT),\n                            on_error=ShowToast(Rx(\"$error\"), variant=\"error\"),  # type: ignore[arg-type]\n                        ),\n                    ):\n                        Form.from_model(model, fields_only=True)\n                        Button(\n                            \"Launch\",\n                            variant=\"success\",\n                            button_type=\"submit\",\n                        )\n\n        Markdown(\n            \"Generated by [Prefab](https://prefab.prefect.io) 🎨\",\n            css_class=\"text-xs text-muted-foreground text-right\",\n        )\n\n    return PrefabApp(title=\"FastMCP App Preview\", view=view).html()\n\n\n# ---------------------------------------------------------------------------\n# MCP tool listing helper\n# ---------------------------------------------------------------------------\n\n\nasync def _list_tools(mcp_url: str) -> list[dict[str, Any]]:\n    \"\"\"Return raw tool dicts from the MCP server at mcp_url.\"\"\"\n    try:\n        from mcp import ClientSession\n        from mcp.client.streamable_http import streamable_http_client\n    except ImportError:\n        return []\n\n    try:\n        async with streamable_http_client(mcp_url) as (read, write, _):  # noqa: SIM117\n            async with ClientSession(read, write) as session:\n                await session.initialize()\n                result = await session.list_tools()\n                return [t.model_dump() for t in result.tools]\n    except Exception as exc:\n        logger.debug(f\"Could not list tools from {mcp_url}: {exc}\")\n        return []\n\n\nasync def _read_mcp_resource(mcp_url: str, uri: str) -> str | None:\n    \"\"\"Read an MCP resource by URI and return its text content.\"\"\"\n    try:\n        from mcp import ClientSession\n        from mcp.client.streamable_http import streamable_http_client\n        from pydantic import AnyUrl\n    except ImportError:\n        return None\n\n    try:\n        async with streamable_http_client(mcp_url) as (read, write, _):  # noqa: SIM117\n            async with ClientSession(read, write) as session:\n                await session.initialize()\n                result = await session.read_resource(AnyUrl(uri))\n                for content in result.contents:\n                    text = getattr(content, \"text\", None)\n                    if text:\n                        return text\n        return None\n    except Exception as exc:\n        logger.debug(f\"Could not read resource {uri} from {mcp_url}: {exc}\")\n        return None\n\n\n# ---------------------------------------------------------------------------\n# app-bridge.js download, patch, and Zod import-map generation\n# ---------------------------------------------------------------------------\n\n\ndef _fetch_app_bridge_bundle_sync(\n    version: str,\n    sdk_version: str,\n) -> tuple[str, str]:\n    \"\"\"Download app-bridge.js and build an import-map that fixes Zod v4 on esm.sh.\n\n    Returns ``(app_bridge_js, import_map_json)`` where *import_map_json* is a\n    JSON string ready to embed in a ``<script type=\"importmap\">`` tag.\n\n    Background\n    ----------\n    esm.sh's ``zod@x.y.z/es2022/v4.mjs`` only re-exports ``{z, default}``,\n    losing all individual named exports (``custom``, ``string``, etc.).  The\n    MCP SDK does ``import * as t from \"zod/v4\"`` and calls ``t.custom(…)``\n    which fails.  ``zod@x.y.z/es2022/v4/classic/index.mjs`` exports everything\n    correctly.  An import-map that remaps the broken URL to the working one\n    fixes all modules in the page's graph, including those loaded cross-origin\n    from esm.sh.\n\n    ext-apps app-bridge.js imports the SDK via bare specifiers\n    (``@modelcontextprotocol/sdk/types.js`` etc.) that the browser cannot\n    resolve.  We rewrite them to concrete esm.sh URLs before serving.\n    \"\"\"\n    cache_path = (\n        Path(tempfile.gettempdir())\n        / f\"fastmcp-ext-apps-{version}-sdk-{sdk_version}-bundle.json\"\n    )\n    if cache_path.exists():\n        cached = json.loads(cache_path.read_text())\n        return cached[\"app_bridge_js\"], cached[\"import_map_json\"]\n\n    # -- Download and patch app-bridge.js -----------------------------------\n    npm_url = f\"https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-{version}.tgz\"\n    with urllib.request.urlopen(npm_url) as resp:\n        data = resp.read()\n\n    with tarfile.open(fileobj=io.BytesIO(data), mode=\"r:gz\") as tar:\n        member = tar.extractfile(\"package/dist/src/app-bridge.js\")\n        if member is None:\n            raise RuntimeError(\"app-bridge.js not found in ext-apps tarball\")\n        app_bridge_js = member.read().decode()\n\n    # Rewrite bare SDK module specifiers to concrete esm.sh URLs\n    sdk_base = f\"https://esm.sh/@modelcontextprotocol/sdk@{sdk_version}\"\n    for sdk_path in (\"types.js\", \"shared/protocol.js\"):\n        app_bridge_js = app_bridge_js.replace(\n            f'from\"@modelcontextprotocol/sdk/{sdk_path}\"',\n            f'from\"{sdk_base}/{sdk_path}\"',\n        )\n\n    # -- Detect the broken Zod v4.mjs URL -----------------------------------\n    # The SDK's types module imports zod/v4 via a version-range URL like\n    # /zod@^4.3.5/v4?target=es2022.  That wrapper re-exports from the\n    # version-specific v4.mjs (e.g. /zod@4.3.6/es2022/v4.mjs) which is\n    # broken.  We fetch the wrapper to discover the exact version.\n    types_url = f\"{sdk_base}/types.js\"\n    with urllib.request.urlopen(types_url) as resp:\n        types_content = resp.read().decode()\n\n    # Extract the zod/v4?target=es2022 path from the types.js redirect\n    zod_wrapper_match = re.search(r'import \"(/zod@[^\"]*v4[^\"]*)\"', types_content)\n    if not zod_wrapper_match:\n        raise RuntimeError(\n            f\"Could not find zod/v4 import in {types_url}:\\n{types_content[:500]}\"\n        )\n    zod_wrapper_path = zod_wrapper_match.group(1)  # e.g. /zod@^4.3.5/v4?target=es2022\n\n    zod_wrapper_url = f\"https://esm.sh{zod_wrapper_path}\"\n    with urllib.request.urlopen(zod_wrapper_url) as resp:\n        wrapper_content = resp.read().decode()\n\n    # The wrapper does: export * from \"/zod@4.3.6/es2022/v4.mjs\"\n    broken_match = re.search(\n        r'export \\* from \"(/zod@[\\d.]+/es2022/v4\\.mjs)\"', wrapper_content\n    )\n    if not broken_match:\n        raise RuntimeError(\n            f\"Could not find v4.mjs re-export in {zod_wrapper_url}:\\n{wrapper_content[:500]}\"\n        )\n    broken_path = broken_match.group(1)  # e.g. /zod@4.3.6/es2022/v4.mjs\n    zod_version = broken_path.split(\"@\")[1].split(\"/\")[0]  # e.g. 4.3.6\n\n    broken_url = f\"https://esm.sh{broken_path}\"\n    fixed_url = f\"https://esm.sh/zod@{zod_version}/es2022/v4/classic/index.mjs\"\n\n    import_map_json = json.dumps({\"imports\": {broken_url: fixed_url}})\n\n    # -- Cache and return ----------------------------------------------------\n    cache_path.write_text(\n        json.dumps({\"app_bridge_js\": app_bridge_js, \"import_map_json\": import_map_json})\n    )\n    return app_bridge_js, import_map_json\n\n\nasync def _fetch_app_bridge_bundle(\n    version: str,\n    sdk_version: str,\n) -> tuple[str, str]:\n    \"\"\"Async wrapper around _fetch_app_bridge_bundle_sync.\"\"\"\n    loop = asyncio.get_running_loop()\n    return await loop.run_in_executor(\n        None, _fetch_app_bridge_bundle_sync, version, sdk_version\n    )\n\n\n# ---------------------------------------------------------------------------\n# FastAPI dev server\n# ---------------------------------------------------------------------------\n\n\ndef _make_dev_app(\n    mcp_url: str,\n    app_bridge_js: str,\n    import_map_tag: str,\n) -> Starlette:\n    \"\"\"Build the Starlette dev server application.\"\"\"\n\n    async def picker(request: Request) -> HTMLResponse:\n        \"\"\"AppBridge host page — loads the picker app in an iframe and wires the bridge.\"\"\"\n        host_html = _HOST_SHELL.format(\n            title=\"FastMCP App Preview\",\n            import_map_tag=import_map_tag,\n            status_text=\"\",\n            status_display=\"none\",\n            frame_display=\"block\",\n            mcp_sdk_version=_MCP_SDK_VERSION,\n            iframe_src_json=json.dumps(\"/picker-app\"),\n            on_open_link=\"bridge.onopenlink = async ({ url }) => { window.location.href = url; return {}; };\",\n            on_initialized=\"bridge.oninitialized = async () => {};\",\n        )\n        return HTMLResponse(host_html)\n\n    async def picker_app(request: Request) -> HTMLResponse:\n        \"\"\"Prefab picker UI — tool list with one tab per UI tool.\"\"\"\n        try:\n            raw_tools = await _list_tools(mcp_url)\n            ui_tools = [t for t in raw_tools if _has_ui_resource(t)]\n            html = _build_picker_html(ui_tools)\n        except Exception as exc:\n            logger.exception(\"Error building picker UI\")\n            html = f\"<pre style='padding:2rem;color:red'>Error: {exc}</pre>\"\n        return HTMLResponse(html)\n\n    async def launch(request: Request) -> HTMLResponse:\n        \"\"\"Host page: GET /launch?tool=name&args={...}\"\"\"\n        tool = request.query_params.get(\"tool\", \"\")\n        args_raw = request.query_params.get(\"args\", \"{}\")\n        tool_args = json.loads(args_raw)\n        host_html = _HOST_HTML_TEMPLATE.format(\n            tool_name=tool,\n            import_map_tag=import_map_tag,\n            tool_name_json=json.dumps(tool),\n            tool_args_json=json.dumps(tool_args),\n            mcp_sdk_version=_MCP_SDK_VERSION,\n        )\n        return HTMLResponse(host_html)\n\n    async def api_launch(request: Request) -> Response:\n        \"\"\"Picker form submits here; returns a /launch URL string for OpenLink.\"\"\"\n        data = await request.json()\n        tool = data.pop(\"tool\", \"\")\n        # Remaining keys are tool arguments; pass all including empty optionals\n        tool_args = dict(data)\n        args_json = quote(json.dumps(tool_args))\n        url = f\"/launch?tool={tool}&args={args_json}\"\n        return Response(\n            content=json.dumps(url),\n            media_type=\"application/json\",\n        )\n\n    async def ui_resource(request: Request) -> Response:\n        \"\"\"Fetch an MCP resource server-side and return it as HTML.\n\n        Used by the launch page to load the renderer via iframe.src rather\n        than iframe.srcdoc — avoids a race condition where the Prefab renderer\n        sends its MCP initialize message before the AppBridge transport is\n        listening (srcdoc parses and runs module scripts synchronously, while\n        iframe.src load adds the network-roundtrip gap needed).\n        \"\"\"\n        uri = request.query_params.get(\"uri\", \"\")\n        if not uri:\n            return Response(\"Missing uri parameter\", status_code=400)\n        html = await _read_mcp_resource(mcp_url, uri)\n        if html is None:\n            return Response(f\"Could not read MCP resource: {uri}\", status_code=502)\n        return HTMLResponse(html)\n\n    async def serve_app_bridge_js(request: Request) -> Response:\n        \"\"\"Serve the locally patched app-bridge.js.\"\"\"\n        return Response(\n            content=app_bridge_js,\n            media_type=\"application/javascript\",\n        )\n\n    async def proxy_mcp(request: Request) -> Response:\n        \"\"\"Proxy all MCP requests to the user's server (avoids browser CORS).\"\"\"\n        body = await request.body()\n        headers = {\n            k: v\n            for k, v in request.headers.items()\n            if k.lower() not in (\"host\", \"content-length\")\n        }\n\n        client = httpx.AsyncClient(timeout=None)\n\n        async def _stream_and_cleanup(resp: httpx.Response) -> Any:\n            try:\n                async for chunk in resp.aiter_bytes():\n                    yield chunk\n            except (\n                httpx.RemoteProtocolError,\n                httpx.ReadError,\n                httpcore.RemoteProtocolError,\n            ):\n                pass  # Connection closed during shutdown — not an error\n            finally:\n                with contextlib.suppress(Exception):\n                    await resp.aclose()\n                with contextlib.suppress(Exception):\n                    await client.aclose()\n\n        try:\n            req = client.build_request(\n                method=request.method,\n                url=mcp_url,\n                content=body,\n                headers=headers,\n                params=dict(request.query_params),\n            )\n            resp = await client.send(req, stream=True)\n            content_type = resp.headers.get(\"content-type\", \"\")\n            # Strip hop-by-hop headers that shouldn't be forwarded\n            fwd_headers = {\n                k: v\n                for k, v in resp.headers.items()\n                if k.lower()\n                not in (\n                    \"transfer-encoding\",\n                    \"connection\",\n                    \"keep-alive\",\n                    \"content-encoding\",\n                )\n            }\n            return StreamingResponse(\n                _stream_and_cleanup(resp),\n                status_code=resp.status_code,\n                headers=fwd_headers,\n                media_type=content_type or \"application/octet-stream\",\n            )\n        except httpx.ConnectError:\n            await client.aclose()\n            return Response(\n                content=json.dumps({\"error\": \"MCP server not reachable\"}).encode(),\n                status_code=503,\n                media_type=\"application/json\",\n            )\n\n    return Starlette(\n        routes=[\n            Route(\"/\", picker),\n            Route(\"/picker-app\", picker_app),\n            Route(\"/launch\", launch),\n            Route(\"/api/launch\", api_launch, methods=[\"POST\"]),\n            Route(\"/ui-resource\", ui_resource),\n            Route(\"/js/app-bridge.js\", serve_app_bridge_js),\n            Route(\n                \"/mcp\",\n                proxy_mcp,\n                methods=[\"GET\", \"POST\", \"DELETE\", \"PUT\", \"PATCH\", \"OPTIONS\"],\n            ),\n        ]\n    )\n\n\n# ---------------------------------------------------------------------------\n# Launch helpers\n# ---------------------------------------------------------------------------\n\n\nasync def _start_user_server(\n    server_spec: str,\n    mcp_port: int,\n    *,\n    reload: bool = True,\n) -> asyncio.subprocess.Process:\n    \"\"\"Start the user's MCP server as a subprocess on mcp_port.\"\"\"\n    cmd = [\n        sys.executable,\n        \"-m\",\n        \"fastmcp.cli\",\n        \"run\",\n        server_spec,\n        \"--transport\",\n        \"http\",\n        \"--port\",\n        str(mcp_port),\n        \"--no-banner\",\n    ]\n    if reload:\n        cmd.append(\"--reload\")\n    else:\n        cmd.append(\"--no-reload\")\n    env = {**os.environ, \"FASTMCP_LOG_LEVEL\": \"WARNING\"}\n    process = await asyncio.create_subprocess_exec(\n        *cmd,\n        env=env,\n        start_new_session=sys.platform != \"win32\",\n    )\n    return process\n\n\nasync def _wait_for_server(url: str, timeout: float = 15.0) -> bool:\n    \"\"\"Poll until the server is accepting connections.\"\"\"\n    loop = asyncio.get_running_loop()\n    deadline = loop.time() + timeout\n    async with httpx.AsyncClient() as client:\n        while loop.time() < deadline:\n            try:\n                await client.get(url, timeout=1.0)\n                return True\n            except (\n                httpx.ConnectError,\n                httpx.RemoteProtocolError,\n                httpx.TimeoutException,\n            ):\n                await asyncio.sleep(0.25)\n    return False\n\n\nasync def run_dev_apps(\n    server_spec: str,\n    *,\n    mcp_port: int = 8000,\n    dev_port: int = 8080,\n    reload: bool = True,\n) -> None:\n    \"\"\"Start the full dev environment for a FastMCPApp server.\n\n    Starts the user's MCP server on *mcp_port*, starts the Prefab dev UI\n    on *dev_port* (with an /mcp proxy to the user's server), then opens\n    the browser.\n    \"\"\"\n    mcp_url = f\"http://localhost:{mcp_port}/mcp\"\n    dev_url = f\"http://localhost:{dev_port}\"\n\n    user_proc: asyncio.subprocess.Process | None = None\n\n    async def _body() -> None:\n        nonlocal user_proc\n\n        logger.info(f\"Starting user server on port {mcp_port}…\")\n        logger.info(\"Fetching app-bridge.js from npm…\")\n\n        # Start the server first so user_proc is assigned before anything\n        # that might fail (e.g. npm fetch).  This ensures the finally\n        # cleanup can kill the subprocess even if the bundle fetch raises.\n        user_proc = await _start_user_server(server_spec, mcp_port, reload=reload)\n        app_bridge_js, import_map_json = await _fetch_app_bridge_bundle(\n            _EXT_APPS_VERSION, _MCP_SDK_VERSION\n        )\n\n        import_map_tag = (\n            f'  <script type=\"importmap\">\\n  {import_map_json}\\n  </script>'\n        )\n\n        ready = await _wait_for_server(mcp_url, timeout=15.0)\n        if not ready:\n            raise RuntimeError(f\"User server did not start on port {mcp_port}\")\n\n        logger.info(f\"FastMCP dev UI at {dev_url}\")\n\n        dev_app = _make_dev_app(mcp_url, app_bridge_js, import_map_tag)\n        config = uvicorn.Config(\n            dev_app,\n            host=\"localhost\",\n            port=dev_port,\n            log_level=\"warning\",\n            ws=\"websockets-sansio\",\n        )\n        server = uvicorn.Server(config)\n        # Suppress uvicorn's own signal handlers — they use signal.signal() which\n        # conflicts with asyncio and causes hangs.  We cancel the task instead.\n        server.install_signal_handlers = lambda: None  # type: ignore[method-assign]\n\n        async def _open_browser() -> None:\n            await asyncio.sleep(0.8)\n            webbrowser.open(dev_url)\n\n        await asyncio.gather(server.serve(), _open_browser())\n\n    # Register signal handlers before any work starts so that Ctrl+C during\n    # startup (server spawn, npm fetch, server-ready poll) is handled the same\n    # way as Ctrl+C during the running phase — both cancel the body task and\n    # fall through to the cleanup finally block.\n    loop = asyncio.get_running_loop()\n    task = asyncio.ensure_future(_body())\n\n    def _on_signal() -> None:\n        # Silence uvicorn's error logger before cancelling so that the\n        # CancelledError propagating through uvicorn doesn't get logged as\n        # an ERROR during the forced shutdown.\n        logging.getLogger(\"uvicorn.error\").setLevel(logging.CRITICAL)\n        task.cancel()\n\n    if sys.platform != \"win32\":\n        loop.add_signal_handler(signal.SIGINT, _on_signal)\n        loop.add_signal_handler(signal.SIGTERM, _on_signal)\n\n    try:\n        await task\n    except asyncio.CancelledError:\n        pass\n    finally:\n        if sys.platform != \"win32\":\n            loop.remove_signal_handler(signal.SIGINT)\n            loop.remove_signal_handler(signal.SIGTERM)\n        if user_proc is not None and user_proc.returncode is None:\n            # Kill the entire process group (not just the top-level process)\n            # because --reload creates a watcher that spawns child processes.\n            # Killing only the watcher leaves the actual server holding the port.\n            try:\n                if sys.platform != \"win32\":\n                    os.killpg(os.getpgid(user_proc.pid), signal.SIGTERM)\n                else:\n                    user_proc.kill()\n            except (ProcessLookupError, PermissionError):\n                user_proc.kill()\n            await user_proc.wait()\n"
  },
  {
    "path": "src/fastmcp/cli/auth.py",
    "content": "\"\"\"Authentication-related CLI commands.\"\"\"\n\nimport cyclopts\n\nfrom fastmcp.cli.cimd import cimd_app\n\nauth_app = cyclopts.App(\n    name=\"auth\",\n    help=\"Authentication-related utilities and configuration.\",\n)\n\n# Nest CIMD commands under auth\nauth_app.command(cimd_app)\n"
  },
  {
    "path": "src/fastmcp/cli/cimd.py",
    "content": "\"\"\"CIMD (Client ID Metadata Document) CLI commands.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nfrom rich.console import Console\n\nfrom fastmcp.server.auth.cimd import (\n    CIMDFetcher,\n    CIMDFetchError,\n    CIMDValidationError,\n)\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(\"cli.cimd\")\nconsole = Console()\n\n\ncimd_app = cyclopts.App(\n    name=\"cimd\",\n    help=\"CIMD (Client ID Metadata Document) utilities for OAuth authentication.\",\n)\n\n\n@cimd_app.command(name=\"create\")\ndef create_command(\n    *,\n    name: Annotated[\n        str,\n        cyclopts.Parameter(help=\"Human-readable name of the client application\"),\n    ],\n    redirect_uri: Annotated[\n        list[str],\n        cyclopts.Parameter(\n            name=[\"--redirect-uri\", \"-r\"],\n            help=\"Allowed redirect URIs (can specify multiple)\",\n        ),\n    ],\n    client_id: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=\"--client-id\",\n            help=\"The URL where this document will be hosted (sets client_id directly)\",\n        ),\n    ] = None,\n    client_uri: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=\"--client-uri\",\n            help=\"URL of the client's home page\",\n        ),\n    ] = None,\n    logo_uri: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=\"--logo-uri\",\n            help=\"URL of the client's logo image\",\n        ),\n    ] = None,\n    scope: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=\"--scope\",\n            help=\"Space-separated list of scopes the client may request\",\n        ),\n    ] = None,\n    output: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--output\", \"-o\"],\n            help=\"Output file path (default: stdout)\",\n        ),\n    ] = None,\n    pretty: Annotated[\n        bool,\n        cyclopts.Parameter(\n            help=\"Pretty-print JSON output\",\n        ),\n    ] = True,\n) -> None:\n    \"\"\"Generate a CIMD document for hosting.\n\n    Create a Client ID Metadata Document that you can host at an HTTPS URL.\n    The URL where you host this document becomes your client_id.\n\n    Example:\n        fastmcp cimd create --name \"My App\" -r \"http://localhost:*/callback\"\n\n    After creating the document, host it at an HTTPS URL with a non-root path,\n    for example: https://myapp.example.com/oauth/client.json\n    \"\"\"\n    # Build the document\n    doc = {\n        \"client_id\": client_id or \"https://YOUR-DOMAIN.com/path/to/client.json\",\n        \"client_name\": name,\n        \"redirect_uris\": redirect_uri,\n        \"token_endpoint_auth_method\": \"none\",\n        \"grant_types\": [\"authorization_code\"],\n        \"response_types\": [\"code\"],\n    }\n\n    # Add optional fields\n    if client_uri:\n        doc[\"client_uri\"] = client_uri\n    if logo_uri:\n        doc[\"logo_uri\"] = logo_uri\n    if scope:\n        doc[\"scope\"] = scope\n\n    # Format output\n    json_output = json.dumps(doc, indent=2) if pretty else json.dumps(doc)\n\n    # Write output\n    if output:\n        output_path = Path(output).expanduser().resolve()\n        output_path.parent.mkdir(parents=True, exist_ok=True)\n        with open(output_path, \"w\") as f:\n            f.write(json_output)\n            f.write(\"\\n\")\n        console.print(f\"[green]✓[/green] CIMD document written to {output}\")\n        if not client_id:\n            console.print(\n                \"\\n[yellow]Important:[/yellow] client_id is a placeholder. Update it to the URL where you will host this document, or re-run with --client-id.\"\n            )\n    else:\n        print(json_output)\n        if not client_id:\n            # Print instructions to stderr so they don't interfere with piping\n            stderr_console = Console(stderr=True)\n            stderr_console.print(\n                \"\\n[yellow]Important:[/yellow] client_id is a placeholder.\"\n                \" Update it to the URL where you will host this document,\"\n                \" or re-run with --client-id.\"\n            )\n\n\n@cimd_app.command(name=\"validate\")\ndef validate_command(\n    url: Annotated[\n        str,\n        cyclopts.Parameter(help=\"URL of the CIMD document to validate\"),\n    ],\n    *,\n    timeout: Annotated[\n        float,\n        cyclopts.Parameter(\n            name=[\"--timeout\", \"-t\"],\n            help=\"HTTP request timeout in seconds\",\n        ),\n    ] = 10.0,\n) -> None:\n    \"\"\"Validate a hosted CIMD document.\n\n    Fetches the document from the given URL and validates:\n    - URL is valid CIMD URL (HTTPS, non-root path)\n    - Document is valid JSON\n    - Document conforms to CIMD schema\n    - client_id in document matches the URL\n\n    Example:\n        fastmcp cimd validate https://myapp.example.com/oauth/client.json\n    \"\"\"\n\n    async def _validate() -> bool:\n        fetcher = CIMDFetcher(timeout=timeout)\n\n        # Check URL format first\n        if not fetcher.is_cimd_client_id(url):\n            console.print(f\"[red]✗[/red] Invalid CIMD URL: {url}\")\n            console.print()\n            console.print(\"CIMD URLs must:\")\n            console.print(\"  • Use HTTPS (not HTTP)\")\n            console.print(\"  • Have a non-root path (e.g., /client.json, not just /)\")\n            return False\n\n        console.print(f\"[blue]→[/blue] Fetching {url}...\")\n\n        try:\n            doc = await fetcher.fetch(url)\n        except CIMDFetchError as e:\n            console.print(f\"[red]✗[/red] Failed to fetch document: {e}\")\n            return False\n        except CIMDValidationError as e:\n            console.print(f\"[red]✗[/red] Validation error: {e}\")\n            return False\n\n        # Success - show document details\n        console.print(\"[green]✓[/green] Valid CIMD document\")\n        console.print()\n        console.print(\"[bold]Document details:[/bold]\")\n        console.print(f\"  client_id: {doc.client_id}\")\n        console.print(f\"  client_name: {doc.client_name or '(not set)'}\")\n        console.print(f\"  token_endpoint_auth_method: {doc.token_endpoint_auth_method}\")\n\n        if doc.redirect_uris:\n            console.print(\"  redirect_uris:\")\n            for uri in doc.redirect_uris:\n                console.print(f\"    • {uri}\")\n        else:\n            console.print(\"  redirect_uris: (none)\")\n\n        if doc.scope:\n            console.print(f\"  scope: {doc.scope}\")\n\n        if doc.client_uri:\n            console.print(f\"  client_uri: {doc.client_uri}\")\n\n        return True\n\n    success = asyncio.run(_validate())\n    if not success:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/cli.py",
    "content": "\"\"\"FastMCP CLI tools using Cyclopts.\"\"\"\n\nimport importlib.metadata\nimport importlib.util\nimport json\nimport os\nimport platform\nimport subprocess\nimport sys\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import Annotated, Literal\n\nimport cyclopts\nimport pyperclip\nfrom cyclopts import Parameter\nfrom rich.console import Console\nfrom rich.table import Table\n\nimport fastmcp\nfrom fastmcp.cli import run as run_module\nfrom fastmcp.cli.auth import auth_app\nfrom fastmcp.cli.client import call_command, discover_command, list_command\nfrom fastmcp.cli.generate import generate_cli_command\nfrom fastmcp.cli.install import install_app\nfrom fastmcp.cli.tasks import tasks_app\nfrom fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config\nfrom fastmcp.utilities.inspect import (\n    InspectFormat,\n    format_info,\n    inspect_fastmcp,\n)\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config import MCPServerConfig\nfrom fastmcp.utilities.version_check import check_for_newer_version\n\nlogger = get_logger(\"cli\")\nconsole = Console()\n\napp = cyclopts.App(\n    name=\"fastmcp\",\n    help=\"FastMCP - The fast, Pythonic way to build MCP servers and clients.\",\n    version=fastmcp.__version__,\n    # Disable automatic negative parameters by default\n    default_parameter=Parameter(negative=()),\n)\n\n\ndef _get_npx_command():\n    \"\"\"Get the correct npx command for the current platform.\"\"\"\n    if sys.platform == \"win32\":\n        # Try both npx.cmd and npx.exe on Windows\n        for cmd in [\"npx.cmd\", \"npx.exe\", \"npx\"]:\n            try:\n                subprocess.run([cmd, \"--version\"], check=True, capture_output=True)\n                return cmd\n            except (subprocess.CalledProcessError, FileNotFoundError):\n                continue\n        return None\n    return \"npx\"  # On Unix-like systems, just use npx\n\n\ndef _parse_env_var(env_var: str) -> tuple[str, str]:\n    \"\"\"Parse environment variable string in format KEY=VALUE.\"\"\"\n    if \"=\" not in env_var:\n        logger.error(\"Invalid environment variable format. Must be KEY=VALUE\")\n        sys.exit(1)\n    key, value = env_var.split(\"=\", 1)\n    return key.strip(), value.strip()\n\n\n@contextmanager\ndef with_argv(args: list[str] | None):\n    \"\"\"Temporarily replace sys.argv if args provided.\n\n    This context manager is used at the CLI boundary to inject\n    server arguments when needed, without mutating sys.argv deep\n    in the source loading logic.\n\n    Args are provided without the script name, so we preserve sys.argv[0]\n    and replace the rest.\n    \"\"\"\n    if args is not None:\n        original = sys.argv[:]\n        try:\n            # Preserve the script name (sys.argv[0]) and replace the rest\n            sys.argv = [sys.argv[0], *args]\n            yield\n        finally:\n            sys.argv = original\n    else:\n        yield\n\n\n@app.command\ndef version(\n    *,\n    copy: Annotated[\n        bool,\n        cyclopts.Parameter(\"--copy\", help=\"Copy version information to clipboard\"),\n    ] = False,\n):\n    \"\"\"Display version information and platform details.\"\"\"\n    info = {\n        \"FastMCP version\": fastmcp.__version__,\n        \"MCP version\": importlib.metadata.version(\"mcp\"),\n        \"Python version\": platform.python_version(),\n        \"Platform\": platform.platform(),\n        \"FastMCP root path\": Path(fastmcp.__file__ or \".\").resolve().parents[1],\n    }\n\n    g = Table.grid(padding=(0, 1))\n    g.add_column(style=\"bold\", justify=\"left\")\n    g.add_column(style=\"cyan\", justify=\"right\")\n    for k, v in info.items():\n        g.add_row(k + \":\", str(v).replace(\"\\n\", \" \"))\n\n    if copy:\n        # Use Rich's plain text rendering for copying\n        plain_console = Console(file=None, force_terminal=False, legacy_windows=False)\n        with plain_console.capture() as capture:\n            plain_console.print(g)\n        pyperclip.copy(capture.get())\n        console.print(\"[green]✓[/green] Version information copied to clipboard\")\n    else:\n        console.print(g)\n\n        # Check for updates (not included in --copy output)\n        if newer_version := check_for_newer_version():\n            console.print()\n            console.print(\n                f\"[bold]🎉 FastMCP update available:[/bold] [green]{newer_version}[/green]\"\n            )\n            console.print(\"[dim]Run: pip install --upgrade fastmcp[/dim]\")\n\n\n# Create dev subcommand group\ndev_app = cyclopts.App(name=\"dev\", help=\"Development tools for MCP servers\")\n\n\n@dev_app.command\nasync def inspector(\n    server_spec: str | None = None,\n    *,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory containing pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    inspector_version: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--inspector-version\",\n            help=\"Version of the MCP Inspector to use\",\n        ),\n    ] = None,\n    ui_port: Annotated[\n        int | None,\n        cyclopts.Parameter(\n            \"--ui-port\",\n            help=\"Port for the MCP Inspector UI\",\n        ),\n    ] = None,\n    server_port: Annotated[\n        int | None,\n        cyclopts.Parameter(\n            \"--server-port\",\n            help=\"Port for the MCP Inspector Proxy server\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n    reload: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--reload\",\n            help=\"Enable auto-reload on file changes (enabled by default)\",\n            negative=\"--no-reload\",\n        ),\n    ] = True,\n    reload_dir: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--reload-dir\",\n            help=\"Directories to watch for changes (default: current directory)\",\n        ),\n    ] = None,\n    module: Annotated[\n        bool,\n        cyclopts.Parameter(\n            name=[\"--module\", \"-m\"],\n            help=\"Run a Python module (python -m <module>) instead of importing a server object\",\n        ),\n    ] = False,\n) -> None:\n    \"\"\"Run an MCP server with the MCP Inspector for development.\n\n    Args:\n        server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json\n    \"\"\"\n\n    try:\n        # Load config and apply CLI overrides\n        config, server_spec = load_and_merge_config(\n            server_spec,\n            python=python,\n            with_packages=with_packages or [],\n            with_requirements=with_requirements,\n            project=project,\n            editable=[str(p) for p in with_editable] if with_editable else None,\n            port=server_port,  # Use deployment config for server port\n        )\n\n        # Get server port from config if not specified via CLI\n        if not server_port:\n            server_port = config.deployment.port\n\n    except FileNotFoundError:\n        sys.exit(1)\n\n    logger.debug(\n        \"Starting dev server\",\n        extra={\n            \"server_spec\": server_spec,\n            \"with_editable\": config.environment.editable,\n            \"with_packages\": config.environment.dependencies,\n            \"ui_port\": ui_port,\n            \"server_port\": server_port,\n        },\n    )\n\n    try:\n        if not config:\n            logger.error(\"No configuration available\")\n            sys.exit(1)\n        assert config is not None  # For type checker\n\n        # Skip server-object validation in module mode — the module\n        # manages its own startup and may not expose an importable server.\n        if not module:\n            await config.source.load_server()\n\n        env_vars = {}\n        if ui_port:\n            env_vars[\"CLIENT_PORT\"] = str(ui_port)\n        if server_port:\n            env_vars[\"SERVER_PORT\"] = str(server_port)\n\n        # Get the correct npx command\n        npx_cmd = _get_npx_command()\n        if not npx_cmd:\n            logger.error(\n                \"npx not found. Please ensure Node.js and npm are properly installed \"\n                \"and added to your system PATH.\"\n            )\n            sys.exit(1)\n\n        inspector_cmd = \"@modelcontextprotocol/inspector\"\n        if inspector_version:\n            inspector_cmd += f\"@{inspector_version}\"\n\n        # Build the fastmcp run command\n        fastmcp_cmd = [\"fastmcp\", \"run\", server_spec, \"--no-banner\"]\n\n        # Forward module mode flag\n        if module:\n            fastmcp_cmd.append(\"--module\")\n\n        # Add reload flags if enabled - the server will handle reloading\n        if reload:\n            fastmcp_cmd.append(\"--reload\")\n            if reload_dir:\n                for dir_path in reload_dir:\n                    fastmcp_cmd.extend([\"--reload-dir\", str(dir_path)])\n\n        # Use the environment from config (already has CLI overrides applied)\n        uv_cmd = config.environment.build_command(fastmcp_cmd)\n\n        # Set marker to prevent infinite loops when subprocess calls FastMCP\n        env = dict(os.environ.items()) | env_vars | {\"FASTMCP_UV_SPAWNED\": \"1\"}\n\n        # Run the MCP Inspector command\n        process = subprocess.run(\n            [npx_cmd, inspector_cmd, *uv_cmd],\n            check=True,\n            env=env,\n        )\n        sys.exit(process.returncode)\n    except subprocess.CalledProcessError as e:\n        logger.error(\n            \"Dev server failed\",\n            extra={\n                \"file\": str(server_spec),\n                \"error\": str(e),\n                \"returncode\": e.returncode,\n            },\n        )\n        sys.exit(e.returncode)\n    except FileNotFoundError:\n        logger.error(\n            \"npx not found. Please ensure Node.js and npm are properly installed \"\n            \"and added to your system PATH. You may need to restart your terminal \"\n            \"after installation.\",\n            extra={\"file\": str(server_spec)},\n        )\n        sys.exit(1)\n\n\n@dev_app.command\nasync def apps(\n    server_spec: str,\n    *,\n    mcp_port: Annotated[\n        int,\n        cyclopts.Parameter(\n            \"--mcp-port\",\n            help=\"Port for the user's MCP server\",\n        ),\n    ] = 8000,\n    dev_port: Annotated[\n        int,\n        cyclopts.Parameter(\n            \"--dev-port\",\n            help=\"Port for the FastMCP dev UI\",\n        ),\n    ] = 8080,\n    reload: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--reload\",\n            negative=\"--no-reload\",\n            help=\"Auto-reload the MCP server on file changes\",\n        ),\n    ] = True,\n) -> None:\n    \"\"\"Preview a FastMCPApp UI in the browser.\n\n    Starts the MCP server from SERVER_SPEC on --mcp-port, launches a local\n    dev UI on --dev-port with a tool picker and AppBridge host, then opens\n    the browser automatically.\n\n    Requires fastmcp[apps] to be installed (prefab-ui).\n    \"\"\"\n    try:\n        import prefab_ui  # noqa: F401\n    except ImportError:\n        logger.error(\n            \"fastmcp dev apps requires prefab-ui. Install with: pip install 'fastmcp[apps]'\"\n        )\n        sys.exit(1)\n\n    from fastmcp.cli.apps_dev import run_dev_apps\n\n    await run_dev_apps(server_spec, mcp_port=mcp_port, dev_port=dev_port, reload=reload)\n\n\n@app.command\nasync def run(\n    server_spec: str | None = None,\n    *server_args: str,\n    transport: Annotated[\n        run_module.TransportType | None,\n        cyclopts.Parameter(\n            name=[\"--transport\", \"-t\"],\n            help=\"Transport protocol to use\",\n        ),\n    ] = None,\n    host: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--host\",\n            help=\"Host to bind to when using http transport (default: 127.0.0.1)\",\n        ),\n    ] = None,\n    port: Annotated[\n        int | None,\n        cyclopts.Parameter(\n            name=[\"--port\", \"-p\"],\n            help=\"Port to bind to when using http transport (default: 8000)\",\n        ),\n    ] = None,\n    path: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--path\",\n            help=\"The route path for the server (default: /mcp/ for http transport, /sse/ for sse transport)\",\n        ),\n    ] = None,\n    log_level: Annotated[\n        Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] | None,\n        cyclopts.Parameter(\n            name=[\"--log-level\", \"-l\"],\n            help=\"Log level\",\n        ),\n    ] = None,\n    no_banner: Annotated[\n        bool,\n        cyclopts.Parameter(\"--no-banner\", help=\"Don't show the server banner\"),\n    ] = False,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    skip_source: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--skip-source\",\n            help=\"Skip source preparation step (use when source is already prepared)\",\n        ),\n    ] = False,\n    skip_env: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--skip-env\",\n            help=\"Skip environment configuration (for internal use when already in a uv environment)\",\n        ),\n    ] = False,\n    reload: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--reload\",\n            negative=\"--no-reload\",\n            help=\"Enable auto-reload on file changes (development mode)\",\n        ),\n    ] = False,\n    reload_dir: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--reload-dir\",\n            help=\"Directories to watch for changes (default: current directory)\",\n        ),\n    ] = None,\n    stateless: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--stateless\",\n            help=\"Run in stateless mode (no session, used internally for reload)\",\n        ),\n    ] = False,\n    module: Annotated[\n        bool,\n        cyclopts.Parameter(\n            name=[\"--module\", \"-m\"],\n            help=\"Run a Python module (python -m <module>) instead of importing a server object\",\n        ),\n    ] = False,\n) -> None:\n    \"\"\"Run an MCP server or connect to a remote one.\n\n    The server can be specified in several ways:\n    1. Module approach: \"server.py\" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'\n    2. Import approach: \"server.py:app\" - imports and runs the specified server object\n    3. URL approach: \"http://server-url\" - connects to a remote server and creates a proxy\n    4. MCPConfig file: \"mcp.json\" - runs as a proxy server for the MCP Servers in the MCPConfig file\n    5. FastMCP config: \"fastmcp.json\" - runs server using FastMCP configuration\n    6. No argument: looks for fastmcp.json in current directory\n    7. Module mode: \"-m my_module\" - runs the module directly via python -m\n\n    Server arguments can be passed after -- :\n    fastmcp run server.py -- --config config.json --debug\n\n    Args:\n        server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect\n    \"\"\"\n\n    # --- Module mode: delegate to python -m and exit early ---\n    if module:\n        if server_spec is None:\n            logger.error(\"A module name is required when using --module / -m\")\n            sys.exit(1)\n\n        # Warn about options that are ignored in module mode\n        ignored_options: list[str] = []\n        if transport:\n            ignored_options.append(\"--transport\")\n        if host:\n            ignored_options.append(\"--host\")\n        if port:\n            ignored_options.append(\"--port\")\n        if path:\n            ignored_options.append(\"--path\")\n        if ignored_options:\n            logger.warning(\n                f\"Options {', '.join(ignored_options)} are ignored in module mode \"\n                f\"(-m). The module manages its own server startup.\"\n            )\n\n        # Build environment wrapper if needed\n        env_builder = None\n        if not skip_env and not is_already_in_uv_subprocess():\n            from fastmcp.utilities.mcp_server_config.v1.environments.uv import (\n                UVEnvironment,\n            )\n\n            env = UVEnvironment(\n                python=python,\n                dependencies=with_packages or None,\n                requirements=with_requirements,\n                project=project,\n            )\n            test_cmd = [\"test\"]\n            if env.build_command(test_cmd) != test_cmd:\n                env_builder = env.build_command\n\n        if reload:\n            # Build a fastmcp run command for the reload watcher to restart\n            reload_cmd = [\"fastmcp\", \"run\", server_spec, \"--module\", \"--no-reload\"]\n            if log_level:\n                reload_cmd.extend([\"--log-level\", log_level])\n            if no_banner:\n                reload_cmd.append(\"--no-banner\")\n            if env_builder is not None:\n                reload_cmd.append(\"--skip-env\")\n            if server_args:\n                reload_cmd.append(\"--\")\n                reload_cmd.extend(server_args)\n            if env_builder is not None:\n                reload_cmd = env_builder(reload_cmd)\n            await run_module.run_with_reload(\n                reload_cmd, reload_dirs=reload_dir, is_stdio=True\n            )\n            return\n\n        run_module.run_module_command(\n            server_spec,\n            env_command_builder=env_builder,\n            extra_args=list(server_args) if server_args else None,\n        )\n        return\n\n    # Check if we were spawned by uv (or user explicitly set --skip-env)\n    if skip_env or is_already_in_uv_subprocess():\n        skip_env = True\n\n    try:\n        # Load config and apply CLI overrides\n        config, server_spec = load_and_merge_config(\n            server_spec,\n            python=python,\n            with_packages=with_packages or [],\n            with_requirements=with_requirements,\n            project=project,\n            transport=transport,\n            host=host,\n            port=port,\n            path=path,\n            log_level=log_level,\n            server_args=list(server_args) if server_args else None,\n        )\n    except FileNotFoundError:\n        sys.exit(1)\n\n    # Get effective values (CLI overrides take precedence)\n    final_transport = transport or config.deployment.transport\n    final_host = host or config.deployment.host\n    final_port = port or config.deployment.port\n    final_path = path or config.deployment.path\n    final_log_level = log_level or config.deployment.log_level\n    final_server_args = server_args or config.deployment.args\n    # Use CLI override if provided, otherwise use settings\n    # no_banner CLI flag overrides the show_server_banner setting\n    final_no_banner = (\n        no_banner if no_banner else not fastmcp.settings.show_server_banner\n    )\n\n    logger.debug(\n        \"Running server or client\",\n        extra={\n            \"server_spec\": server_spec,\n            \"transport\": final_transport,\n            \"host\": final_host,\n            \"port\": final_port,\n            \"path\": final_path,\n            \"log_level\": final_log_level,\n            \"server_args\": list(final_server_args) if final_server_args else [],\n        },\n    )\n\n    # Handle reload mode\n    if reload:\n        # SSE is incompatible with reload (no stateless mode exists)\n        if final_transport == \"sse\":\n            logger.warning(\n                \"--reload is not supported with SSE transport (sessions are lost on restart). \"\n                \"Use streamable-http transport instead, or use --no-reload. \"\n                \"Running without reload.\"\n            )\n            # Fall through to normal execution\n        else:\n            # Build command for subprocess (with --no-reload to prevent infinite spawning)\n            reload_cmd = [\"fastmcp\", \"run\", server_spec]\n            if final_transport:\n                reload_cmd.extend([\"--transport\", final_transport])\n            if final_transport != \"stdio\":\n                if final_host:\n                    reload_cmd.extend([\"--host\", final_host])\n                if final_port:\n                    reload_cmd.extend([\"--port\", str(final_port)])\n                if final_path:\n                    reload_cmd.extend([\"--path\", final_path])\n            if final_log_level:\n                reload_cmd.extend([\"--log-level\", final_log_level])\n            if final_no_banner:\n                reload_cmd.append(\"--no-banner\")\n            reload_cmd.append(\"--no-reload\")  # Prevent infinite spawning\n            reload_cmd.append(\"--stateless\")  # Stateless mode for reload compatibility\n\n            # If environment setup is needed, wrap with uv\n            test_cmd = [\"test\"]\n            needs_uv = (\n                config.environment.build_command(test_cmd) != test_cmd and not skip_env\n            )\n            if needs_uv:\n                # Add --skip-env to prevent nested uv runs (child would spawn another uv)\n                reload_cmd.append(\"--skip-env\")\n\n            if final_server_args:\n                reload_cmd.append(\"--\")\n                reload_cmd.extend(final_server_args)\n\n            if needs_uv:\n                reload_cmd = config.environment.build_command(reload_cmd)\n\n            is_stdio = final_transport in (\"stdio\", None)\n            await run_module.run_with_reload(\n                reload_cmd, reload_dirs=reload_dir, is_stdio=is_stdio\n            )\n            return\n\n    # Check if we need to use uv run (but skip if we're already in uv or user said to skip)\n    # We check if the environment would modify the command\n    test_cmd = [\"test\"]\n    needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env\n\n    if needs_uv:\n        # Build the inner fastmcp command\n        inner_cmd = [\"fastmcp\", \"run\", server_spec]\n\n        # Add transport options to the inner command\n        if final_transport:\n            inner_cmd.extend([\"--transport\", final_transport])\n        # Only add HTTP-specific options for non-stdio transports\n        if final_transport != \"stdio\":\n            if final_host:\n                inner_cmd.extend([\"--host\", final_host])\n            if final_port:\n                inner_cmd.extend([\"--port\", str(final_port)])\n            if final_path:\n                inner_cmd.extend([\"--path\", final_path])\n        if final_log_level:\n            inner_cmd.extend([\"--log-level\", final_log_level])\n        if final_no_banner:\n            inner_cmd.append(\"--no-banner\")\n        # Add skip-env flag to prevent infinite recursion\n        inner_cmd.append(\"--skip-env\")\n\n        # Add server args if any\n        if final_server_args:\n            inner_cmd.append(\"--\")\n            inner_cmd.extend(final_server_args)\n\n        # Build the full uv command using the config's environment\n        cmd = config.environment.build_command(inner_cmd)\n\n        # Set marker to prevent infinite loops when subprocess calls FastMCP again\n        env = os.environ | {\"FASTMCP_UV_SPAWNED\": \"1\"}\n\n        # Run the command\n        logger.debug(f\"Running command: {' '.join(cmd)}\")\n        try:\n            process = subprocess.run(cmd, check=True, env=env)\n            sys.exit(process.returncode)\n        except subprocess.CalledProcessError as e:\n            logger.exception(\n                f\"Failed to run: {e}\",\n                extra={\n                    \"server_spec\": server_spec,\n                    \"error\": str(e),\n                    \"returncode\": e.returncode,\n                },\n            )\n            sys.exit(e.returncode)\n    else:\n        # Use direct import for backwards compatibility\n        try:\n            await run_module.run_command(\n                server_spec=server_spec,\n                transport=final_transport,\n                host=final_host,\n                port=final_port,\n                path=final_path,\n                log_level=final_log_level,\n                server_args=list(final_server_args) if final_server_args else [],\n                show_banner=not final_no_banner,\n                skip_source=skip_source,\n                stateless=stateless,\n            )\n        except Exception as e:\n            logger.exception(\n                f\"Failed to run: {e}\",\n                extra={\n                    \"server_spec\": server_spec,\n                    \"error\": str(e),\n                },\n            )\n            sys.exit(1)\n\n\n@app.command\nasync def inspect(\n    server_spec: str | None = None,\n    *,\n    format: Annotated[\n        InspectFormat | None,\n        cyclopts.Parameter(\n            name=[\"--format\", \"-f\"],\n            help=\"Output format: fastmcp (FastMCP-specific) or mcp (MCP protocol). Required when using -o.\",\n        ),\n    ] = None,\n    output: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            name=[\"--output\", \"-o\"],\n            help=\"Output file path for the JSON report. If not specified, outputs to stdout when format is provided.\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    skip_env: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--skip-env\",\n            help=\"Skip environment configuration (for internal use when already in a uv environment)\",\n        ),\n    ] = False,\n) -> None:\n    \"\"\"Inspect an MCP server and display information or generate a JSON report.\n\n    This command analyzes an MCP server. Without flags, it displays a text summary.\n    Use --format to output complete JSON data.\n\n    Examples:\n        # Show text summary\n        fastmcp inspect server.py\n\n        # Output FastMCP format JSON to stdout\n        fastmcp inspect server.py --format fastmcp\n\n        # Save MCP protocol format to file (format required with -o)\n        fastmcp inspect server.py --format mcp -o manifest.json\n\n        # Inspect from fastmcp.json configuration\n        fastmcp inspect fastmcp.json\n        fastmcp inspect  # auto-detect fastmcp.json\n\n    Args:\n        server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json\n    \"\"\"\n\n    # Check if we were spawned by uv (or user explicitly set --skip-env)\n    if skip_env or is_already_in_uv_subprocess():\n        skip_env = True\n\n    try:\n        # Load config and apply CLI overrides\n        config, server_spec = load_and_merge_config(\n            server_spec,\n            python=python,\n            with_packages=with_packages or [],\n            with_requirements=with_requirements,\n            project=project,\n        )\n\n        # Check if it's an MCPConfig (which inspect doesn't support)\n        if server_spec.endswith(\".json\") and config is None:\n            # This might be an MCPConfig, check the file\n            try:\n                with open(Path(server_spec)) as f:\n                    data = json.load(f)\n                if \"mcpServers\" in data:\n                    logger.error(\"MCPConfig files are not supported by inspect command\")\n                    sys.exit(1)\n            except (json.JSONDecodeError, FileNotFoundError):\n                pass\n\n    except FileNotFoundError:\n        sys.exit(1)\n\n    # Check if we need to use uv run (but skip if we're already in uv or user said to skip)\n    # We check if the environment would modify the command\n    test_cmd = [\"test\"]\n    needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env\n\n    if needs_uv:\n        # Build and run uv command\n        # The environment is already configured in the config object\n        inspect_command = [\n            \"fastmcp\",\n            \"inspect\",\n            server_spec,\n            \"--skip-env\",  # Prevent infinite recursion\n        ]\n\n        # Add format and output flags if specified\n        if format:\n            inspect_command.extend([\"--format\", format.value])\n        if output:\n            inspect_command.extend([\"--output\", str(output)])\n\n        # Run the command using subprocess\n        import subprocess\n\n        cmd = config.environment.build_command(inspect_command)\n        env = os.environ | {\"FASTMCP_UV_SPAWNED\": \"1\"}\n        process = subprocess.run(cmd, check=True, env=env)\n        sys.exit(process.returncode)\n\n    logger.debug(\n        \"Inspecting server\",\n        extra={\n            \"server_spec\": server_spec,\n            \"format\": format,\n            \"output\": str(output) if output else None,\n        },\n    )\n\n    try:\n        # Load the server using the config\n        if not config:\n            logger.error(\"No configuration available\")\n            sys.exit(1)\n        assert config is not None  # For type checker\n        server = await config.source.load_server()\n\n        # Get basic server information\n        info = await inspect_fastmcp(server)\n\n        # Check for invalid combination\n        if output and not format:\n            console.print(\n                \"[bold red]Error:[/bold red] --format is required when using -o/--output\"\n            )\n            console.print(\n                \"[dim]Use --format fastmcp or --format mcp to specify the output format[/dim]\"\n            )\n            sys.exit(1)\n\n        # If no format specified, show text summary\n        if format is None:\n            # Display text summary\n            console.print()\n\n            # Server section\n            console.print(\"[bold]Server[/bold]\")\n            console.print(f\"  Name:         {info.name}\")\n            if info.version:\n                console.print(f\"  Version:      {info.version}\")\n            if info.website_url:\n                console.print(f\"  Website:      {info.website_url}\")\n            if info.icons:\n                console.print(f\"  Icons:        {len(info.icons)}\")\n            console.print(f\"  Generation:   {info.server_generation}\")\n            if info.instructions:\n                console.print(f\"  Instructions: {info.instructions}\")\n            console.print()\n\n            # Components section\n            console.print(\"[bold]Components[/bold]\")\n            console.print(f\"  Tools:        {len(info.tools)}\")\n            console.print(f\"  Prompts:      {len(info.prompts)}\")\n            console.print(f\"  Resources:    {len(info.resources)}\")\n            console.print(f\"  Templates:    {len(info.templates)}\")\n            console.print()\n\n            # Environment section\n            console.print(\"[bold]Environment[/bold]\")\n            console.print(f\"  FastMCP:      {info.fastmcp_version}\")\n            console.print(f\"  MCP:          {info.mcp_version}\")\n            console.print()\n\n            console.print(\n                \"[dim]Use --format \\\\[fastmcp|mcp] for complete JSON output[/dim]\"\n            )\n            return\n\n        # Generate formatted JSON output\n        formatted_json = await format_info(server, format, info)\n\n        # Output to file or stdout\n        if output:\n            # Ensure output directory exists\n            output.parent.mkdir(parents=True, exist_ok=True)\n\n            # Write JSON report\n            with output.open(\"wb\") as f:\n                f.write(formatted_json)\n\n            logger.info(f\"Server inspection complete. Report saved to {output}\")\n\n            # Print confirmation to console\n            console.print(\n                f\"[bold green]✓[/bold green] Server inspection saved to: [cyan]{output}[/cyan]\"\n            )\n            console.print(f\"  Server: [bold]{info.name}[/bold]\")\n            console.print(f\"  Format: {format.value}\")\n        else:\n            # Output JSON to stdout\n            console.print(formatted_json.decode(\"utf-8\"))\n\n    except Exception as e:\n        logger.exception(\n            f\"Failed to inspect server: {e}\",\n            extra={\n                \"server_spec\": server_spec,\n                \"error\": str(e),\n            },\n        )\n        console.print(f\"[bold red]✗[/bold red] Failed to inspect server: {e}\")\n        sys.exit(1)\n\n\n# Create project subcommand group\nproject_app = cyclopts.App(name=\"project\", help=\"Manage FastMCP projects\")\n\n\n@project_app.command\nasync def prepare(\n    config_path: Annotated[\n        str | None,\n        cyclopts.Parameter(help=\"Path to fastmcp.json configuration file\"),\n    ] = None,\n    output_dir: Annotated[\n        str | None,\n        cyclopts.Parameter(help=\"Directory to create the persistent environment in\"),\n    ] = None,\n    skip_source: Annotated[\n        bool,\n        cyclopts.Parameter(help=\"Skip source preparation (e.g., git clone)\"),\n    ] = False,\n) -> None:\n    \"\"\"Prepare a FastMCP project by creating a persistent uv environment.\n\n    This command creates a persistent uv project with all dependencies installed:\n    - Creates a pyproject.toml with dependencies from the config\n    - Installs all Python packages into a .venv\n    - Prepares the source (git clone, download, etc.) unless --skip-source\n\n    After running this command, you can use:\n    fastmcp run <config> --project <output-dir>\n\n    This is useful for:\n    - CI/CD pipelines with separate build and run stages\n    - Docker images where you prepare during build\n    - Production deployments where you want fast startup times\n\n    Example:\n        fastmcp project prepare myserver.json --output-dir ./prepared-env\n        fastmcp run myserver.json --project ./prepared-env\n    \"\"\"\n    from pathlib import Path\n\n    # Require output-dir\n    if output_dir is None:\n        logger.error(\n            \"The --output-dir parameter is required.\\n\"\n            \"Please specify where to create the persistent environment.\"\n        )\n        sys.exit(1)\n\n    # Auto-detect fastmcp.json if not provided\n    if config_path is None:\n        found_config = MCPServerConfig.find_config()\n        if found_config:\n            config_path = str(found_config)\n            logger.info(f\"Using configuration from {config_path}\")\n        else:\n            logger.error(\n                \"No configuration file specified and no fastmcp.json found.\\n\"\n                \"Please specify a configuration file or create a fastmcp.json.\"\n            )\n            sys.exit(1)\n\n    assert config_path is not None\n    config_file = Path(config_path)\n    if not config_file.exists():\n        logger.error(f\"Configuration file not found: {config_path}\")\n        sys.exit(1)\n\n    assert output_dir is not None\n    output_path = Path(output_dir)\n\n    try:\n        # Load the configuration\n        config = MCPServerConfig.from_file(config_file)\n\n        # Prepare environment and source\n        await config.prepare(\n            skip_source=skip_source,\n            output_dir=output_path,\n        )\n\n        console.print(\n            f\"[bold green]✓[/bold green] Project prepared successfully in {output_path}!\\n\"\n            f\"You can now run the server with:\\n\"\n            f\"  [cyan]fastmcp run {config_path} --project {output_dir}[/cyan]\"\n        )\n\n    except Exception as e:\n        logger.error(f\"Failed to prepare project: {e}\")\n        console.print(f\"[bold red]✗[/bold red] Failed to prepare project: {e}\")\n        sys.exit(1)\n\n\n# Add dev subcommand group\napp.command(dev_app)\n\n# Add project subcommand group\napp.command(project_app)\n\n# Add install subcommands using proper Cyclopts pattern\napp.command(install_app)\n\n# Add tasks subcommand group\napp.command(tasks_app)\n\n# Add client query commands\napp.command(list_command, name=\"list\")\napp.command(call_command, name=\"call\")\napp.command(discover_command, name=\"discover\")\napp.command(generate_cli_command, name=\"generate-cli\")\n\n# Add auth subcommand group (includes CIMD commands)\napp.command(auth_app)\n\n\nif __name__ == \"__main__\":\n    app()\n"
  },
  {
    "path": "src/fastmcp/cli/client.py",
    "content": "\"\"\"Client-side CLI commands for querying and invoking MCP servers.\"\"\"\n\nimport difflib\nimport json\nimport shlex\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated, Any, Literal\n\nimport cyclopts\nimport mcp.types\nfrom rich.console import Console\nfrom rich.markup import escape as escape_rich_markup\n\nfrom fastmcp.cli.discovery import DiscoveredServer, discover_servers, resolve_name\nfrom fastmcp.client.client import CallToolResult, Client\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.client.transports.base import ClientTransport\nfrom fastmcp.client.transports.http import StreamableHttpTransport\nfrom fastmcp.client.transports.sse import SSETransport\nfrom fastmcp.client.transports.stdio import StdioTransport\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(\"cli.client\")\nconsole = Console()\n\n\n# ---------------------------------------------------------------------------\n# Server spec resolution\n# ---------------------------------------------------------------------------\n\n_JSON_SCHEMA_TYPE_MAP: dict[str, str] = {\n    \"string\": \"str\",\n    \"integer\": \"int\",\n    \"number\": \"float\",\n    \"boolean\": \"bool\",\n    \"array\": \"list\",\n    \"object\": \"dict\",\n    \"null\": \"None\",\n}\n\n\ndef resolve_server_spec(\n    server_spec: str | None,\n    *,\n    command: str | None = None,\n    transport: str | None = None,\n) -> str | dict[str, Any] | ClientTransport:\n    \"\"\"Turn CLI inputs into something ``Client()`` accepts.\n\n    Exactly one of ``server_spec`` or ``command`` should be provided.\n\n    Resolution order for ``server_spec``:\n    1. URLs (``http://``, ``https://``) — passed through as-is.\n       If ``--transport`` is ``sse``, the URL is rewritten to end with ``/sse``\n       so ``infer_transport`` picks the right transport.\n    2. Existing file paths, or strings ending in ``.py``/``.js``/``.json``.\n    3. Anything else — name-based resolution via ``resolve_name``.\n\n    When ``command`` is provided, the string is shell-split into a\n    ``StdioTransport(command, args)``.\n    \"\"\"\n\n    if command is not None and server_spec is not None:\n        console.print(\n            \"[bold red]Error:[/bold red] Cannot use both a server spec and --command\"\n        )\n        sys.exit(1)\n\n    if command is not None:\n        return _build_stdio_from_command(command)\n\n    if server_spec is None:\n        console.print(\n            \"[bold red]Error:[/bold red] Provide a server spec or use --command\"\n        )\n        sys.exit(1)\n\n    assert isinstance(server_spec, str)\n    spec: str = server_spec\n\n    # 1. URL\n    if spec.startswith((\"http://\", \"https://\")):\n        if transport == \"sse\" and not spec.rstrip(\"/\").endswith(\"/sse\"):\n            spec = spec.rstrip(\"/\") + \"/sse\"\n        return spec\n\n    # 2. File path (must be a file, not a directory)\n    path = Path(spec)\n    is_file = path.is_file() or (\n        not path.is_dir() and spec.endswith((\".py\", \".js\", \".json\"))\n    )\n\n    if is_file:\n        if spec.endswith(\".json\"):\n            return _resolve_json_spec(path)\n        if spec.endswith(\".py\"):\n            # Run via `fastmcp run` so scripts don't need mcp.run()\n            resolved_path = path.resolve()\n            return StdioTransport(\n                command=\"fastmcp\",\n                args=[\"run\", str(resolved_path), \"--no-banner\"],\n            )\n        # .js — pass through for Client's infer_transport\n        return spec\n\n    # 3. Name-based resolution (bare name or source:name)\n    try:\n        return resolve_name(spec)\n    except ValueError as exc:\n        console.print(f\"[bold red]Error:[/bold red] {exc}\")\n        sys.exit(1)\n\n\ndef _build_stdio_from_command(command_str: str) -> StdioTransport:\n    \"\"\"Shell-split a command string into a ``StdioTransport``.\"\"\"\n    try:\n        parts = shlex.split(command_str)\n    except ValueError as exc:\n        console.print(f\"[bold red]Error:[/bold red] Invalid command: {exc}\")\n        sys.exit(1)\n\n    if not parts:\n        console.print(\"[bold red]Error:[/bold red] Empty --command\")\n        sys.exit(1)\n\n    return StdioTransport(command=parts[0], args=parts[1:])\n\n\ndef _resolve_json_spec(path: Path) -> str | dict[str, Any]:\n    \"\"\"Disambiguate a ``.json`` server spec.\"\"\"\n\n    if not path.exists():\n        console.print(\n            f\"[bold red]Error:[/bold red] File not found: [cyan]{path}[/cyan]\"\n        )\n        sys.exit(1)\n\n    try:\n        data = json.loads(path.read_text())\n    except json.JSONDecodeError as exc:\n        console.print(f\"[bold red]Error:[/bold red] Invalid JSON in {path}: {exc}\")\n        sys.exit(1)\n\n    if isinstance(data, dict) and \"mcpServers\" in data:\n        return data\n\n    # Likely a fastmcp.json (MCPServerConfig) — not directly usable as a client target.\n    console.print(\n        f\"[bold red]Error:[/bold red] [cyan]{path}[/cyan] is a FastMCP server config, not an MCPConfig.\\n\"\n        f\"Start the server first, then query it:\\n\\n\"\n        f\"  fastmcp run {path}\\n\"\n        f\"  fastmcp list http://localhost:8000/mcp\\n\"\n    )\n    sys.exit(1)\n\n\ndef _is_http_target(resolved: str | dict[str, Any] | ClientTransport) -> bool:\n    \"\"\"Return True if the resolved target will use an HTTP-based transport.\n\n    MCPConfig dicts are excluded because ``MCPConfigTransport`` manages\n    individual server transports internally and does not support top-level auth.\n    \"\"\"\n    if isinstance(resolved, str):\n        return resolved.startswith((\"http://\", \"https://\"))\n    return isinstance(resolved, (StreamableHttpTransport, SSETransport))\n\n\nasync def _terminal_elicitation_handler(\n    message: str,\n    response_type: type[Any] | None,\n    params: Any,\n    context: Any,\n) -> ElicitResult[dict[str, Any]]:\n    \"\"\"Prompt the user on the terminal for elicitation responses.\n\n    Prints the server's message and prompts for each field in the schema.\n    The user can type 'decline' or 'cancel' instead of a value to abort.\n    \"\"\"\n    from mcp.types import ElicitRequestFormParams\n\n    console.print(f\"\\n[bold yellow]Server asks:[/bold yellow] {message}\")\n\n    if not isinstance(params, ElicitRequestFormParams):\n        answer = console.input(\n            \"[dim](press Enter to accept, or type 'decline'):[/dim] \"\n        )\n        if answer.strip().lower() == \"decline\":\n            return ElicitResult(action=\"decline\")\n        if answer.strip().lower() == \"cancel\":\n            return ElicitResult(action=\"cancel\")\n        return ElicitResult(action=\"accept\", content={})\n\n    schema = params.requestedSchema\n    properties = schema.get(\"properties\", {})\n    required = set(schema.get(\"required\", []))\n\n    if not properties:\n        answer = console.input(\n            \"[dim](press Enter to accept, or type 'decline'):[/dim] \"\n        )\n        if answer.strip().lower() == \"decline\":\n            return ElicitResult(action=\"decline\")\n        if answer.strip().lower() == \"cancel\":\n            return ElicitResult(action=\"cancel\")\n        return ElicitResult(action=\"accept\", content={})\n\n    result: dict[str, Any] = {}\n    for field_name, field_schema in properties.items():\n        type_hint = field_schema.get(\"type\", \"string\")\n        req_marker = \" [red]*[/red]\" if field_name in required else \"\"\n        prompt_text = f\"  [cyan]{field_name}[/cyan] ({type_hint}){req_marker}: \"\n\n        raw = console.input(prompt_text)\n        if raw.strip().lower() == \"decline\":\n            return ElicitResult(action=\"decline\")\n        if raw.strip().lower() == \"cancel\":\n            return ElicitResult(action=\"cancel\")\n\n        if raw == \"\" and field_name not in required:\n            continue\n\n        result[field_name] = coerce_value(raw, field_schema)\n\n    return ElicitResult(action=\"accept\", content=result)\n\n\ndef _build_client(\n    resolved: str | dict[str, Any] | ClientTransport,\n    *,\n    timeout: float | None = None,\n    auth: str | None = None,\n) -> Client:\n    \"\"\"Build a ``Client`` from a resolved server spec.\n\n    Applies ``auth='oauth'`` automatically for HTTP-based targets unless\n    the caller explicitly passes ``--auth none`` to disable it.\n\n    ``auth=None`` means \"not specified\" (use default), ``auth=\"none\"``\n    means \"explicitly disabled\".\n    \"\"\"\n    if auth == \"none\":\n        effective_auth: str | None = None\n    elif auth is not None:\n        effective_auth = auth\n    elif _is_http_target(resolved):\n        effective_auth = \"oauth\"\n    else:\n        effective_auth = None\n\n    return Client(\n        resolved,\n        timeout=timeout,\n        auth=effective_auth,\n        elicitation_handler=_terminal_elicitation_handler,\n    )\n\n\n# ---------------------------------------------------------------------------\n# Argument coercion\n# ---------------------------------------------------------------------------\n\n\ndef coerce_value(raw: str, schema: dict[str, Any]) -> Any:\n    \"\"\"Coerce a string CLI value according to a JSON-Schema type hint.\"\"\"\n\n    schema_type = schema.get(\"type\", \"string\")\n\n    if schema_type == \"integer\":\n        try:\n            return int(raw)\n        except ValueError:\n            raise ValueError(f\"Expected integer, got {raw!r}\") from None\n\n    if schema_type == \"number\":\n        try:\n            return float(raw)\n        except ValueError:\n            raise ValueError(f\"Expected number, got {raw!r}\") from None\n\n    if schema_type == \"boolean\":\n        if raw.lower() in (\"true\", \"1\", \"yes\"):\n            return True\n        if raw.lower() in (\"false\", \"0\", \"no\"):\n            return False\n        raise ValueError(f\"Expected boolean, got {raw!r}\")\n\n    if schema_type in (\"array\", \"object\"):\n        try:\n            return json.loads(raw)\n        except json.JSONDecodeError:\n            raise ValueError(f\"Expected JSON {schema_type}, got {raw!r}\") from None\n\n    # Default: treat as string\n    return raw\n\n\ndef parse_tool_arguments(\n    raw_args: tuple[str, ...],\n    input_json: str | None,\n    input_schema: dict[str, Any],\n) -> dict[str, Any]:\n    \"\"\"Build a tool-call argument dict from CLI inputs.\n\n    A single JSON object argument is treated as the full argument dict.\n    ``--input-json`` provides the base dict; ``key=value`` pairs override.\n    Values are coerced using the tool's ``inputSchema``.\n    \"\"\"\n\n    # A single positional arg that looks like JSON → treat as input-json\n    if len(raw_args) == 1 and raw_args[0].startswith(\"{\") and input_json is None:\n        input_json = raw_args[0]\n        raw_args = ()\n\n    result: dict[str, Any] = {}\n\n    if input_json is not None:\n        try:\n            parsed = json.loads(input_json)\n        except json.JSONDecodeError as exc:\n            console.print(f\"[bold red]Error:[/bold red] Invalid --input-json: {exc}\")\n            sys.exit(1)\n        if not isinstance(parsed, dict):\n            console.print(\n                \"[bold red]Error:[/bold red] --input-json must be a JSON object\"\n            )\n            sys.exit(1)\n        result.update(parsed)\n\n    properties = input_schema.get(\"properties\", {})\n\n    for arg in raw_args:\n        if \"=\" not in arg:\n            console.print(\n                f\"[bold red]Error:[/bold red] Invalid argument [cyan]{arg}[/cyan] — expected key=value\"\n            )\n            sys.exit(1)\n        key, value = arg.split(\"=\", 1)\n        prop_schema = properties.get(key, {})\n        try:\n            result[key] = coerce_value(value, prop_schema)\n        except ValueError as exc:\n            console.print(\n                f\"[bold red]Error:[/bold red] Argument [cyan]{key}[/cyan]: {exc}\"\n            )\n            sys.exit(1)\n\n    return result\n\n\n# ---------------------------------------------------------------------------\n# Tool signature formatting\n# ---------------------------------------------------------------------------\n\n\ndef _json_schema_type_to_str(schema: dict[str, Any]) -> str:\n    \"\"\"Produce a short Python-style type string from a JSON-Schema fragment.\"\"\"\n\n    if \"anyOf\" in schema:\n        parts = [_json_schema_type_to_str(s) for s in schema[\"anyOf\"]]\n        return \" | \".join(parts)\n\n    schema_type = schema.get(\"type\", \"any\")\n    if isinstance(schema_type, list):\n        return \" | \".join(_JSON_SCHEMA_TYPE_MAP.get(t, t) for t in schema_type)\n\n    return _JSON_SCHEMA_TYPE_MAP.get(schema_type, schema_type)\n\n\ndef format_tool_signature(tool: mcp.types.Tool) -> str:\n    \"\"\"Build ``name(param: type, ...) -> return_type`` from a tool's JSON schemas.\"\"\"\n\n    params: list[str] = []\n    schema = tool.inputSchema\n    properties = schema.get(\"properties\", {})\n    required = set(schema.get(\"required\", []))\n\n    for prop_name, prop_schema in properties.items():\n        type_str = _json_schema_type_to_str(prop_schema)\n        if prop_name in required:\n            params.append(f\"{prop_name}: {type_str}\")\n        else:\n            default = prop_schema.get(\"default\")\n            default_repr = repr(default) if default is not None else \"...\"\n            params.append(f\"{prop_name}: {type_str} = {default_repr}\")\n\n    sig = f\"{tool.name}({', '.join(params)})\"\n\n    if tool.outputSchema:\n        ret = _json_schema_type_to_str(tool.outputSchema)\n        sig += f\" -> {ret}\"\n\n    return sig\n\n\n# ---------------------------------------------------------------------------\n# Output formatting\n# ---------------------------------------------------------------------------\n\n\ndef _print_schema(label: str, schema: dict[str, Any]) -> None:\n    \"\"\"Print a JSON schema with a label.\"\"\"\n    properties = schema.get(\"properties\", {})\n    if not properties:\n        return\n    console.print(f\"    [dim]{label}: {json.dumps(schema)}[/dim]\")\n\n\ndef _sanitize_untrusted_text(value: str) -> str:\n    \"\"\"Escape rich markup and encode control chars for terminal-safe output.\"\"\"\n    sanitized = escape_rich_markup(value)\n    return \"\".join(\n        ch\n        if ch in {\"\\n\", \"\\t\"} or (0x20 <= ord(ch) < 0x7F) or ord(ch) > 0x9F\n        else f\"\\\\x{ord(ch):02x}\"\n        for ch in sanitized\n    )\n\n\ndef _format_call_result_text(result: CallToolResult) -> None:\n    \"\"\"Pretty-print a tool call result to the console.\"\"\"\n\n    if result.is_error:\n        for block in result.content:\n            if isinstance(block, mcp.types.TextContent):\n                console.print(\n                    f\"[bold red]Error:[/bold red] {_sanitize_untrusted_text(block.text)}\"\n                )\n            else:\n                console.print(\n                    f\"[bold red]Error:[/bold red] {_sanitize_untrusted_text(str(block))}\"\n                )\n        return\n\n    if result.structured_content is not None:\n        console.print_json(json.dumps(result.structured_content))\n        return\n\n    for block in result.content:\n        if isinstance(block, mcp.types.TextContent):\n            console.print(_sanitize_untrusted_text(block.text))\n        elif isinstance(block, mcp.types.ImageContent):\n            size = len(block.data) * 3 // 4  # rough decoded size\n            console.print(f\"[dim][Image: {block.mimeType}, ~{size} bytes][/dim]\")\n        elif isinstance(block, mcp.types.AudioContent):\n            size = len(block.data) * 3 // 4\n            console.print(f\"[dim][Audio: {block.mimeType}, ~{size} bytes][/dim]\")\n        else:\n            console.print(_sanitize_untrusted_text(str(block)))\n\n\ndef _content_block_to_dict(block: mcp.types.ContentBlock) -> dict[str, Any]:\n    \"\"\"Serialize a single content block to a JSON-safe dict.\"\"\"\n    if isinstance(block, mcp.types.TextContent):\n        return {\"type\": \"text\", \"text\": block.text}\n    if isinstance(block, mcp.types.ImageContent):\n        return {\"type\": \"image\", \"mimeType\": block.mimeType, \"data\": block.data}\n    if isinstance(block, mcp.types.AudioContent):\n        return {\"type\": \"audio\", \"mimeType\": block.mimeType, \"data\": block.data}\n    return {\"type\": \"unknown\", \"value\": str(block)}\n\n\ndef _call_result_to_dict(result: CallToolResult) -> dict[str, Any]:\n    \"\"\"Serialize a ``CallToolResult`` to a JSON-safe dict.\"\"\"\n\n    content_list = [_content_block_to_dict(block) for block in result.content]\n    out: dict[str, Any] = {\"content\": content_list, \"is_error\": result.is_error}\n    if result.structured_content is not None:\n        out[\"structured_content\"] = result.structured_content\n    return out\n\n\ndef _tools_to_json(tools: list[mcp.types.Tool]) -> list[dict[str, Any]]:\n    \"\"\"Serialize a list of tools to JSON-safe dicts.\"\"\"\n\n    return [\n        {\n            \"name\": t.name,\n            \"description\": t.description,\n            \"inputSchema\": t.inputSchema,\n            **({\"outputSchema\": t.outputSchema} if t.outputSchema else {}),\n        }\n        for t in tools\n    ]\n\n\n# ---------------------------------------------------------------------------\n# Call handlers (tool, resource, prompt)\n# ---------------------------------------------------------------------------\n\n\nasync def _handle_tool_call(\n    client: Client,\n    tool_name: str,\n    arguments: tuple[str, ...],\n    input_json: str | None,\n    json_output: bool,\n) -> None:\n    \"\"\"Handle a tool call within an open client session.\"\"\"\n    tools = await client.list_tools()\n    tool_map = {t.name: t for t in tools}\n\n    if tool_name not in tool_map:\n        close_matches = difflib.get_close_matches(\n            tool_name, tool_map.keys(), n=3, cutoff=0.5\n        )\n        msg = f\"Tool [cyan]{tool_name}[/cyan] not found.\"\n        if close_matches:\n            suggestions = \", \".join(f\"[cyan]{m}[/cyan]\" for m in close_matches)\n            msg += f\" Did you mean: {suggestions}?\"\n        console.print(f\"[bold red]Error:[/bold red] {msg}\")\n        sys.exit(1)\n\n    tool = tool_map[tool_name]\n    parsed_args = parse_tool_arguments(arguments, input_json, tool.inputSchema)\n\n    required = set(tool.inputSchema.get(\"required\", []))\n    provided = set(parsed_args.keys())\n    missing = required - provided\n    if missing:\n        missing_str = \", \".join(f\"[cyan]{m}[/cyan]\" for m in sorted(missing))\n        console.print(\n            f\"[bold red]Error:[/bold red] Missing required arguments: {missing_str}\"\n        )\n        console.print()\n        sig = format_tool_signature(tool)\n        console.print(f\"  [dim]{sig}[/dim]\")\n        sys.exit(1)\n\n    result = await client.call_tool(tool_name, parsed_args, raise_on_error=False)\n\n    if json_output:\n        console.print_json(json.dumps(_call_result_to_dict(result)))\n    else:\n        _format_call_result_text(result)\n\n    if result.is_error:\n        sys.exit(1)\n\n\nasync def _handle_resource(\n    client: Client,\n    uri: str,\n    json_output: bool,\n) -> None:\n    \"\"\"Handle a resource read within an open client session.\"\"\"\n    contents = await client.read_resource(uri)\n\n    if json_output:\n        data = []\n        for block in contents:\n            if isinstance(block, mcp.types.TextResourceContents):\n                data.append(\n                    {\n                        \"uri\": str(block.uri),\n                        \"mimeType\": block.mimeType,\n                        \"text\": block.text,\n                    }\n                )\n            elif isinstance(block, mcp.types.BlobResourceContents):\n                data.append(\n                    {\n                        \"uri\": str(block.uri),\n                        \"mimeType\": block.mimeType,\n                        \"blob\": block.blob,\n                    }\n                )\n        console.print_json(json.dumps(data))\n        return\n\n    for block in contents:\n        if isinstance(block, mcp.types.TextResourceContents):\n            console.print(_sanitize_untrusted_text(block.text))\n        elif isinstance(block, mcp.types.BlobResourceContents):\n            size = len(block.blob) * 3 // 4\n            console.print(f\"[dim][Blob: {block.mimeType}, ~{size} bytes][/dim]\")\n\n\nasync def _handle_prompt(\n    client: Client,\n    prompt_name: str,\n    arguments: tuple[str, ...],\n    input_json: str | None,\n    json_output: bool,\n) -> None:\n    \"\"\"Handle a prompt get within an open client session.\"\"\"\n    # Prompt arguments are always string->string, but we reuse\n    # parse_tool_arguments for the key=value / --input-json parsing.\n    # Pass an empty schema so values stay as strings.\n    parsed_args = parse_tool_arguments(arguments, input_json, {\"type\": \"object\"})\n\n    prompts = await client.list_prompts()\n    prompt_map = {p.name: p for p in prompts}\n\n    if prompt_name not in prompt_map:\n        close_matches = difflib.get_close_matches(\n            prompt_name, prompt_map.keys(), n=3, cutoff=0.5\n        )\n        msg = f\"Prompt [cyan]{prompt_name}[/cyan] not found.\"\n        if close_matches:\n            suggestions = \", \".join(f\"[cyan]{m}[/cyan]\" for m in close_matches)\n            msg += f\" Did you mean: {suggestions}?\"\n        console.print(f\"[bold red]Error:[/bold red] {msg}\")\n        sys.exit(1)\n\n    result = await client.get_prompt(prompt_name, parsed_args or None)\n\n    if json_output:\n        data: dict[str, Any] = {}\n        if result.description:\n            data[\"description\"] = result.description\n        data[\"messages\"] = [\n            {\n                \"role\": msg.role,\n                \"content\": _content_block_to_dict(msg.content),\n            }\n            for msg in result.messages\n        ]\n        console.print_json(json.dumps(data))\n        return\n\n    for msg in result.messages:\n        console.print(f\"[bold]{_sanitize_untrusted_text(msg.role)}:[/bold]\")\n        if isinstance(msg.content, mcp.types.TextContent):\n            console.print(f\"  {_sanitize_untrusted_text(msg.content.text)}\")\n        elif isinstance(msg.content, mcp.types.ImageContent):\n            size = len(msg.content.data) * 3 // 4\n            console.print(\n                f\"  [dim][Image: {msg.content.mimeType}, ~{size} bytes][/dim]\"\n            )\n        else:\n            console.print(f\"  {_sanitize_untrusted_text(str(msg.content))}\")\n        console.print()\n\n\n# ---------------------------------------------------------------------------\n# Commands\n# ---------------------------------------------------------------------------\n\n\nasync def list_command(\n    server_spec: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            help=\"Server URL, Python file, MCPConfig JSON, or .js file\",\n        ),\n    ] = None,\n    *,\n    command: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--command\",\n            help=\"Stdio command to connect to (e.g. 'npx -y @mcp/server')\",\n        ),\n    ] = None,\n    transport: Annotated[\n        Literal[\"http\", \"sse\"] | None,\n        cyclopts.Parameter(\n            name=[\"--transport\", \"-t\"],\n            help=\"Force transport type for URL targets (http or sse)\",\n        ),\n    ] = None,\n    resources: Annotated[\n        bool,\n        cyclopts.Parameter(\"--resources\", help=\"Also list resources\"),\n    ] = False,\n    prompts: Annotated[\n        bool,\n        cyclopts.Parameter(\"--prompts\", help=\"Also list prompts\"),\n    ] = False,\n    input_schema: Annotated[\n        bool,\n        cyclopts.Parameter(\"--input-schema\", help=\"Show full input schemas\"),\n    ] = False,\n    output_schema: Annotated[\n        bool,\n        cyclopts.Parameter(\"--output-schema\", help=\"Show full output schemas\"),\n    ] = False,\n    json_output: Annotated[\n        bool,\n        cyclopts.Parameter(\"--json\", help=\"Output as JSON\"),\n    ] = False,\n    timeout: Annotated[\n        float | None,\n        cyclopts.Parameter(\"--timeout\", help=\"Connection timeout in seconds\"),\n    ] = None,\n    auth: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--auth\",\n            help=\"Auth method: 'oauth', a bearer token string, or 'none' to disable\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"List tools available on an MCP server.\n\n    Examples:\n        fastmcp list http://localhost:8000/mcp\n        fastmcp list server.py\n        fastmcp list mcp.json --json\n        fastmcp list --command 'npx -y @mcp/server' --resources\n        fastmcp list http://server/mcp --transport sse\n    \"\"\"\n\n    resolved = resolve_server_spec(server_spec, command=command, transport=transport)\n    client = _build_client(resolved, timeout=timeout, auth=auth)\n\n    try:\n        async with client:\n            tools = await client.list_tools()\n\n            if json_output:\n                data: dict[str, Any] = {\"tools\": _tools_to_json(tools)}\n                if resources:\n                    res = await client.list_resources()\n                    data[\"resources\"] = [\n                        {\n                            \"uri\": str(r.uri),\n                            \"name\": r.name,\n                            \"description\": r.description,\n                            \"mimeType\": r.mimeType,\n                        }\n                        for r in res\n                    ]\n                if prompts:\n                    prm = await client.list_prompts()\n                    data[\"prompts\"] = [\n                        {\n                            \"name\": p.name,\n                            \"description\": p.description,\n                            \"arguments\": [a.model_dump() for a in (p.arguments or [])],\n                        }\n                        for p in prm\n                    ]\n                console.print_json(json.dumps(data))\n                return\n\n            # Text output\n            if not tools:\n                console.print(\"[dim]No tools found.[/dim]\")\n            else:\n                console.print(f\"[bold]Tools ({len(tools)})[/bold]\")\n                console.print()\n                for tool in tools:\n                    sig = format_tool_signature(tool)\n                    console.print(f\"  [cyan]{_sanitize_untrusted_text(sig)}[/cyan]\")\n                    if tool.description:\n                        console.print(\n                            f\"    {_sanitize_untrusted_text(tool.description)}\"\n                        )\n                    if input_schema:\n                        _print_schema(\"Input\", tool.inputSchema)\n                    if output_schema and tool.outputSchema:\n                        _print_schema(\"Output\", tool.outputSchema)\n                    console.print()\n\n            if resources:\n                res = await client.list_resources()\n                console.print(f\"[bold]Resources ({len(res)})[/bold]\")\n                console.print()\n                if not res:\n                    console.print(\"  [dim]No resources found.[/dim]\")\n                for r in res:\n                    console.print(\n                        f\"  [cyan]{_sanitize_untrusted_text(str(r.uri))}[/cyan]\"\n                    )\n                    desc_parts = [r.name or \"\", r.description or \"\"]\n                    desc = \" — \".join(p for p in desc_parts if p)\n                    if desc:\n                        console.print(f\"    {_sanitize_untrusted_text(desc)}\")\n                console.print()\n\n            if prompts:\n                prm = await client.list_prompts()\n                console.print(f\"[bold]Prompts ({len(prm)})[/bold]\")\n                console.print()\n                if not prm:\n                    console.print(\"  [dim]No prompts found.[/dim]\")\n                for p in prm:\n                    args_str = \"\"\n                    if p.arguments:\n                        parts = [a.name for a in p.arguments]\n                        args_str = f\"({', '.join(parts)})\"\n                    console.print(\n                        f\"  [cyan]{_sanitize_untrusted_text(p.name + args_str)}[/cyan]\"\n                    )\n                    if p.description:\n                        console.print(f\"    {_sanitize_untrusted_text(p.description)}\")\n                console.print()\n\n    except Exception as exc:\n        console.print(f\"[bold red]Error:[/bold red] {exc}\")\n        sys.exit(1)\n\n\nasync def call_command(\n    server_spec: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            help=\"Server URL, Python file, MCPConfig JSON, or .js file\",\n        ),\n    ] = None,\n    target: Annotated[\n        str,\n        cyclopts.Parameter(\n            help=\"Tool name, resource URI, or prompt name (with --prompt)\",\n        ),\n    ] = \"\",\n    *arguments: str,\n    command: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--command\",\n            help=\"Stdio command to connect to (e.g. 'npx -y @mcp/server')\",\n        ),\n    ] = None,\n    transport: Annotated[\n        Literal[\"http\", \"sse\"] | None,\n        cyclopts.Parameter(\n            name=[\"--transport\", \"-t\"],\n            help=\"Force transport type for URL targets (http or sse)\",\n        ),\n    ] = None,\n    prompt: Annotated[\n        bool,\n        cyclopts.Parameter(\"--prompt\", help=\"Treat target as a prompt name\"),\n    ] = False,\n    input_json: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--input-json\",\n            help=\"JSON string of arguments (merged with key=value args)\",\n        ),\n    ] = None,\n    json_output: Annotated[\n        bool,\n        cyclopts.Parameter(\"--json\", help=\"Output raw JSON result\"),\n    ] = False,\n    timeout: Annotated[\n        float | None,\n        cyclopts.Parameter(\"--timeout\", help=\"Connection timeout in seconds\"),\n    ] = None,\n    auth: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--auth\",\n            help=\"Auth method: 'oauth', a bearer token string, or 'none' to disable\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Call a tool, read a resource, or get a prompt on an MCP server.\n\n    By default the target is treated as a tool name. If the target\n    contains ``://`` it is treated as a resource URI. Pass ``--prompt``\n    to treat it as a prompt name.\n\n    Arguments are passed as key=value pairs. Use --input-json for complex\n    or nested arguments.\n\n    Examples:\n        ```\n        fastmcp call server.py greet name=World\n        fastmcp call server.py resource://docs/readme\n        fastmcp call server.py analyze --prompt data='[1,2,3]'\n        fastmcp call http://server/mcp create --input-json '{\"tags\": [\"a\",\"b\"]}'\n        ```\n    \"\"\"\n\n    if not target:\n        console.print(\n            \"[bold red]Error:[/bold red] Missing target.\\n\\n\"\n            \"Usage: fastmcp call <server> <target> [key=value ...]\\n\\n\"\n            \"  target can be a tool name, a resource URI, or a prompt name (with --prompt).\\n\\n\"\n            \"Use [cyan]fastmcp list <server>[/cyan] to see available tools.\"\n        )\n        sys.exit(1)\n\n    resolved = resolve_server_spec(server_spec, command=command, transport=transport)\n    client = _build_client(resolved, timeout=timeout, auth=auth)\n\n    try:\n        async with client:\n            if prompt:\n                await _handle_prompt(client, target, arguments, input_json, json_output)\n            elif \"://\" in target:\n                await _handle_resource(client, target, json_output)\n            else:\n                await _handle_tool_call(\n                    client, target, arguments, input_json, json_output\n                )\n\n    except Exception as exc:\n        console.print(f\"[bold red]Error:[/bold red] {exc}\")\n        sys.exit(1)\n\n\nasync def discover_command(\n    *,\n    source: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--source\",\n            help=\"Only show servers from these sources (e.g. claude-code, cursor, gemini)\",\n        ),\n    ] = None,\n    json_output: Annotated[\n        bool,\n        cyclopts.Parameter(\"--json\", help=\"Output as JSON\"),\n    ] = False,\n) -> None:\n    \"\"\"Discover MCP servers configured in editor and project configs.\n\n    Scans Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose, and\n    project-level mcp.json files for MCP server definitions.\n\n    Discovered server names can be used directly with ``fastmcp list``\n    and ``fastmcp call`` instead of specifying a URL or file path.\n\n    Examples:\n        fastmcp discover\n        fastmcp discover --source claude-code\n        fastmcp discover --source cursor --source gemini --json\n        fastmcp list weather\n        fastmcp call cursor:weather get_forecast city=London\n    \"\"\"\n\n    servers = discover_servers()\n\n    if source:\n        servers = [s for s in servers if s.source in source]\n\n    if json_output:\n        data: list[dict[str, Any]] = [\n            {\n                \"name\": s.name,\n                \"source\": s.source,\n                \"qualified_name\": s.qualified_name,\n                \"transport_summary\": s.transport_summary,\n                \"config_path\": str(s.config_path),\n            }\n            for s in servers\n        ]\n        console.print_json(json.dumps(data))\n        return\n\n    if not servers:\n        console.print(\"[dim]No MCP servers found.[/dim]\")\n        console.print()\n        console.print(\"Searched:\")\n        console.print(\"  • Claude Desktop config\")\n        console.print(\"  • ~/.claude.json (Claude Code)\")\n        console.print(\"  • .cursor/mcp.json (walked up from cwd)\")\n        console.print(\"  • ~/.gemini/settings.json (Gemini CLI)\")\n        console.print(\"  • ~/.config/goose/config.yaml (Goose)\")\n        console.print(\"  • ./mcp.json\")\n        return\n\n    from rich.table import Table\n\n    # Group by source\n    by_source: dict[str, list[DiscoveredServer]] = {}\n    for s in servers:\n        by_source.setdefault(s.source, []).append(s)\n\n    for source_name, group in by_source.items():\n        console.print()\n        console.print(f\"[bold]Source:[/bold]  {source_name}\")\n        console.print(f\"[bold]Config:[/bold]  [dim]{group[0].config_path}[/dim]\")\n        console.print()\n\n        table = Table(\n            show_header=True,\n            header_style=\"bold\",\n            show_edge=False,\n            pad_edge=False,\n            box=None,\n            padding=(0, 2),\n        )\n        table.add_column(\"Server\", style=\"cyan\")\n        table.add_column(\"Transport\", style=\"dim\")\n\n        for s in group:\n            table.add_row(s.name, s.transport_summary)\n\n        console.print(table)\n        console.print()\n"
  },
  {
    "path": "src/fastmcp/cli/discovery.py",
    "content": "\"\"\"Discover MCP servers configured in editor config files.\n\nScans filesystem-readable config files from editors like Claude Desktop,\nClaude Code, Cursor, Gemini CLI, and Goose, as well as project-level\n``mcp.json`` files. Each discovered server can be resolved by name\n(or ``source:name``) so the CLI can connect without requiring a URL\nor file path.\n\"\"\"\n\nimport json\nimport os\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nimport yaml\n\nfrom fastmcp.client.transports.base import ClientTransport\nfrom fastmcp.mcp_config import (\n    MCPConfig,\n    MCPServerTypes,\n    RemoteMCPServer,\n    StdioMCPServer,\n)\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(\"cli.discovery\")\n\n\n# ---------------------------------------------------------------------------\n# Data model\n# ---------------------------------------------------------------------------\n\n\n@dataclass(frozen=True)\nclass DiscoveredServer:\n    \"\"\"A single MCP server found in an editor or project config.\"\"\"\n\n    name: str\n    source: str\n    config: MCPServerTypes\n    config_path: Path\n\n    @property\n    def qualified_name(self) -> str:\n        \"\"\"Fully qualified ``source:name`` identifier.\"\"\"\n        return f\"{self.source}:{self.name}\"\n\n    @property\n    def transport_summary(self) -> str:\n        \"\"\"Human-readable one-liner describing the transport.\"\"\"\n        cfg = self.config\n        if isinstance(cfg, StdioMCPServer):\n            parts = [cfg.command, *cfg.args]\n            return f\"stdio: {' '.join(parts)}\"\n        if isinstance(cfg, RemoteMCPServer):\n            transport = cfg.transport or \"http\"\n            return f\"{transport}: {cfg.url}\"\n        return str(type(cfg).__name__)\n\n\n# ---------------------------------------------------------------------------\n# Scanners — one per config source\n# ---------------------------------------------------------------------------\n\n\ndef _normalize_server_entry(entry: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Normalize editor-specific server config fields to MCPConfig format.\n\n    Handles two known differences:\n    - Claude Code uses ``type`` where MCPConfig uses ``transport`` for\n      remote servers.\n    - Gemini CLI uses ``httpUrl`` where MCPConfig uses ``url``.\n    \"\"\"\n    # Gemini: httpUrl → url\n    if \"httpUrl\" in entry and \"url\" not in entry:\n        entry = {**entry, \"url\": entry[\"httpUrl\"]}\n        del entry[\"httpUrl\"]\n\n    # Claude Code / others: type → transport (for url-based entries only)\n    if \"url\" in entry and \"type\" in entry and \"transport\" not in entry:\n        transport = entry[\"type\"]\n        entry = {k: v for k, v in entry.items() if k != \"type\"}\n        entry[\"transport\"] = transport\n\n    return entry\n\n\ndef _parse_mcp_servers(\n    servers_dict: dict[str, Any],\n    *,\n    source: str,\n    config_path: Path,\n) -> list[DiscoveredServer]:\n    \"\"\"Parse an ``mcpServers``-style dict into discovered servers.\"\"\"\n    if not servers_dict:\n        return []\n\n    normalized = {\n        name: _normalize_server_entry(entry)\n        for name, entry in servers_dict.items()\n        if isinstance(entry, dict)\n    }\n\n    try:\n        config = MCPConfig.from_dict({\"mcpServers\": normalized})\n    except Exception as exc:\n        logger.warning(\"Could not parse MCP servers from %s: %s\", config_path, exc)\n        return []\n\n    return [\n        DiscoveredServer(\n            name=name, source=source, config=server, config_path=config_path\n        )\n        for name, server in config.mcpServers.items()\n    ]\n\n\ndef _parse_mcp_config(path: Path, source: str) -> list[DiscoveredServer]:\n    \"\"\"Parse an mcpServers-style JSON file into discovered servers.\"\"\"\n    try:\n        text = path.read_text()\n    except OSError as exc:\n        logger.debug(\"Could not read %s: %s\", path, exc)\n        return []\n\n    try:\n        data: dict[str, Any] = json.loads(text)\n    except json.JSONDecodeError as exc:\n        logger.warning(\"Invalid JSON in %s: %s\", path, exc)\n        return []\n\n    if not isinstance(data, dict) or \"mcpServers\" not in data:\n        return []\n\n    return _parse_mcp_servers(data[\"mcpServers\"], source=source, config_path=path)\n\n\ndef _scan_claude_desktop() -> list[DiscoveredServer]:\n    \"\"\"Scan the Claude Desktop config file.\"\"\"\n    if sys.platform == \"win32\":\n        config_dir = Path(Path.home(), \"AppData\", \"Roaming\", \"Claude\")\n    elif sys.platform == \"darwin\":\n        config_dir = Path(Path.home(), \"Library\", \"Application Support\", \"Claude\")\n    elif sys.platform.startswith(\"linux\"):\n        config_dir = Path(\n            os.environ.get(\"XDG_CONFIG_HOME\", Path.home() / \".config\"), \"Claude\"\n        )\n    else:\n        return []\n\n    path = config_dir / \"claude_desktop_config.json\"\n    return _parse_mcp_config(path, \"claude-desktop\")\n\n\ndef _scan_claude_code(start_dir: Path) -> list[DiscoveredServer]:\n    \"\"\"Scan ``~/.claude.json`` for global and project-scoped MCP servers.\"\"\"\n    path = Path.home() / \".claude.json\"\n    try:\n        text = path.read_text()\n    except OSError:\n        return []\n\n    try:\n        data: dict[str, Any] = json.loads(text)\n    except json.JSONDecodeError as exc:\n        logger.warning(\"Invalid JSON in %s: %s\", path, exc)\n        return []\n\n    if not isinstance(data, dict):\n        return []\n\n    results: list[DiscoveredServer] = []\n\n    # Global servers\n    if global_servers := data.get(\"mcpServers\"):\n        if isinstance(global_servers, dict):\n            results.extend(\n                _parse_mcp_servers(\n                    global_servers, source=\"claude-code\", config_path=path\n                )\n            )\n\n    # Project-scoped servers matching start_dir\n    resolved_dir = str(start_dir.resolve())\n    projects = data.get(\"projects\", {})\n    if isinstance(projects, dict):\n        project_data = projects.get(resolved_dir, {})\n        if isinstance(project_data, dict):\n            if project_servers := project_data.get(\"mcpServers\"):\n                if isinstance(project_servers, dict):\n                    results.extend(\n                        _parse_mcp_servers(\n                            project_servers,\n                            source=\"claude-code\",\n                            config_path=path,\n                        )\n                    )\n\n    return results\n\n\ndef _scan_cursor_workspace(start_dir: Path) -> list[DiscoveredServer]:\n    \"\"\"Walk up from *start_dir* looking for ``.cursor/mcp.json``.\"\"\"\n    current = start_dir.resolve()\n    home = Path.home().resolve()\n\n    while True:\n        candidate = current / \".cursor\" / \"mcp.json\"\n        if candidate.is_file():\n            return _parse_mcp_config(candidate, \"cursor\")\n\n        parent = current.parent\n        # Stop at filesystem root or home directory\n        if parent == current or current == home:\n            break\n        current = parent\n\n    return []\n\n\ndef _scan_project_mcp_json(start_dir: Path) -> list[DiscoveredServer]:\n    \"\"\"Check for ``mcp.json`` in *start_dir*.\"\"\"\n    candidate = start_dir.resolve() / \"mcp.json\"\n    if candidate.is_file():\n        return _parse_mcp_config(candidate, \"project\")\n    return []\n\n\ndef _scan_gemini(start_dir: Path) -> list[DiscoveredServer]:\n    \"\"\"Scan Gemini CLI settings for MCP servers.\n\n    Checks both user-level ``~/.gemini/settings.json`` and project-level\n    ``.gemini/settings.json``.\n    \"\"\"\n    results: list[DiscoveredServer] = []\n\n    # User-level\n    user_path = Path.home() / \".gemini\" / \"settings.json\"\n    results.extend(_parse_mcp_config(user_path, \"gemini\"))\n\n    # Project-level\n    project_path = start_dir.resolve() / \".gemini\" / \"settings.json\"\n    if project_path != user_path:\n        results.extend(_parse_mcp_config(project_path, \"gemini\"))\n\n    return results\n\n\ndef _scan_goose() -> list[DiscoveredServer]:\n    \"\"\"Scan Goose config for MCP server extensions.\n\n    Goose uses YAML (``~/.config/goose/config.yaml``) with a different\n    schema — MCP servers are defined as ``extensions`` with ``type: stdio``.\n    \"\"\"\n    if sys.platform == \"win32\":\n        config_dir = Path(\n            os.environ.get(\"APPDATA\", Path.home() / \"AppData\" / \"Roaming\"),\n            \"Block\",\n            \"goose\",\n            \"config\",\n        )\n    else:\n        config_dir = Path(\n            os.environ.get(\"XDG_CONFIG_HOME\", Path.home() / \".config\"),\n            \"goose\",\n        )\n\n    path = config_dir / \"config.yaml\"\n    try:\n        text = path.read_text()\n    except OSError:\n        return []\n\n    try:\n        data = yaml.safe_load(text)\n    except yaml.YAMLError as exc:\n        logger.warning(\"Invalid YAML in %s: %s\", path, exc)\n        return []\n\n    if not isinstance(data, dict):\n        return []\n\n    extensions = data.get(\"extensions\", {})\n    if not isinstance(extensions, dict):\n        return []\n\n    # Convert Goose extensions to mcpServers format\n    servers: dict[str, Any] = {}\n    for name, ext in extensions.items():\n        if not isinstance(ext, dict):\n            continue\n        if not ext.get(\"enabled\", True):\n            continue\n        ext_type = ext.get(\"type\", \"\")\n        if ext_type == \"stdio\" and \"cmd\" in ext:\n            servers[name] = {\n                \"command\": ext[\"cmd\"],\n                \"args\": ext.get(\"args\", []),\n                \"env\": ext.get(\"envs\", {}),\n            }\n        elif ext_type == \"sse\" and \"uri\" in ext:\n            servers[name] = {\"url\": ext[\"uri\"], \"transport\": \"sse\"}\n\n    return _parse_mcp_servers(servers, source=\"goose\", config_path=path)\n\n\n# ---------------------------------------------------------------------------\n# Public API\n# ---------------------------------------------------------------------------\n\n\ndef discover_servers(start_dir: Path | None = None) -> list[DiscoveredServer]:\n    \"\"\"Run all scanners and return the combined results.\n\n    Duplicate names across sources are preserved — callers can\n    use :pyattr:`DiscoveredServer.qualified_name` to disambiguate.\n    \"\"\"\n    cwd = start_dir or Path.cwd()\n    results: list[DiscoveredServer] = []\n    results.extend(_scan_claude_desktop())\n    results.extend(_scan_claude_code(cwd))\n    results.extend(_scan_cursor_workspace(cwd))\n    results.extend(_scan_gemini(cwd))\n    results.extend(_scan_goose())\n    results.extend(_scan_project_mcp_json(cwd))\n    return results\n\n\ndef resolve_name(name: str, start_dir: Path | None = None) -> ClientTransport:\n    \"\"\"Resolve a server name (or ``source:name``) to a transport.\n\n    Raises :class:`ValueError` when the name is not found or is ambiguous.\n    \"\"\"\n    servers = discover_servers(start_dir)\n\n    # Qualified form: \"cursor:weather\"\n    if \":\" in name:\n        source, server_name = name.split(\":\", 1)\n        matches = [s for s in servers if s.source == source and s.name == server_name]\n        if not matches:\n            raise ValueError(\n                f\"No server named '{server_name}' found in source '{source}'.\"\n            )\n        return matches[0].config.to_transport()\n\n    # Bare name: \"weather\"\n    matches = [s for s in servers if s.name == name]\n\n    if not matches:\n        if servers:\n            available = \", \".join(sorted({s.name for s in servers}))\n            raise ValueError(f\"No server named '{name}' found. Available: {available}\")\n        locations = [\n            \"Claude Desktop config\",\n            \"~/.claude.json (Claude Code)\",\n            \".cursor/mcp.json (walked up from cwd)\",\n            \"~/.gemini/settings.json (Gemini CLI)\",\n            \"~/.config/goose/config.yaml (Goose)\",\n            \"./mcp.json\",\n        ]\n        raise ValueError(\n            f\"No server named '{name}' found. Searched: {', '.join(locations)}\"\n        )\n\n    if len(matches) == 1:\n        return matches[0].config.to_transport()\n\n    # Ambiguous — list qualified alternatives\n    alternatives = \", \".join(f\"'{m.qualified_name}'\" for m in matches)\n    raise ValueError(\n        f\"Ambiguous server name '{name}' — found in multiple sources. \"\n        f\"Use a qualified name: {alternatives}\"\n    )\n"
  },
  {
    "path": "src/fastmcp/cli/generate.py",
    "content": "\"\"\"Generate a standalone CLI script and agent skill from an MCP server.\"\"\"\n\nimport keyword\nimport re\nimport sys\nimport textwrap\nfrom pathlib import Path\nfrom typing import Annotated, Any\nfrom urllib.parse import urlparse\n\nimport cyclopts\nimport mcp.types\nimport pydantic_core\nfrom mcp import McpError\nfrom rich.console import Console\n\nfrom fastmcp.cli.client import _build_client, resolve_server_spec\nfrom fastmcp.client.transports.base import ClientTransport\nfrom fastmcp.client.transports.stdio import StdioTransport\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(\"cli.generate\")\nconsole = Console()\n\n# ---------------------------------------------------------------------------\n# JSON Schema type → Python type string\n# ---------------------------------------------------------------------------\n\n_SIMPLE_TYPES = {\"string\", \"integer\", \"number\", \"boolean\", \"null\"}\n\n\ndef _is_simple_type(schema: dict[str, Any]) -> bool:\n    \"\"\"Check if a schema represents a simple (non-complex) type.\"\"\"\n    schema_type = schema.get(\"type\")\n    if isinstance(schema_type, list):\n        # Union of types - simple only if all are simple\n        return all(t in _SIMPLE_TYPES for t in schema_type)\n    return schema_type in _SIMPLE_TYPES\n\n\ndef _is_simple_array(schema: dict[str, Any]) -> tuple[bool, str | None]:\n    \"\"\"Check if schema is an array of simple types.\n\n    Returns (is_simple_array, item_type_str).\n    \"\"\"\n    if schema.get(\"type\") != \"array\":\n        return False, None\n\n    items = schema.get(\"items\", {})\n    if not _is_simple_type(items):\n        return False, None\n\n    # Map JSON Schema type to Python type\n    item_type = items.get(\"type\", \"string\")\n    if isinstance(item_type, list):\n        return False, None\n    type_map = {\n        \"string\": \"str\",\n        \"integer\": \"int\",\n        \"number\": \"float\",\n        \"boolean\": \"bool\",\n    }\n    py_type = type_map.get(item_type)\n    if py_type is None:\n        return False, None\n    return True, py_type\n\n\ndef _schema_to_python_type(schema: dict[str, Any]) -> tuple[str, bool]:\n    \"\"\"Convert a JSON Schema to a Python type annotation.\n\n    Returns (type_annotation, needs_json_parsing).\n    \"\"\"\n    # Check for simple array first\n    is_simple_arr, item_type = _is_simple_array(schema)\n    if is_simple_arr:\n        return f\"list[{item_type}]\", False\n\n    # Check for simple type\n    if _is_simple_type(schema):\n        schema_type = schema.get(\"type\", \"string\")\n        if isinstance(schema_type, list):\n            # Union of simple types\n            type_map = {\n                \"string\": \"str\",\n                \"integer\": \"int\",\n                \"number\": \"float\",\n                \"boolean\": \"bool\",\n                \"null\": \"None\",\n            }\n            parts = [type_map.get(t, \"str\") for t in schema_type]\n            return \" | \".join(parts), False\n\n        type_map = {\n            \"string\": \"str\",\n            \"integer\": \"int\",\n            \"number\": \"float\",\n            \"boolean\": \"bool\",\n            \"null\": \"None\",\n        }\n        return type_map.get(schema_type, \"str\"), False\n\n    # Complex type - needs JSON parsing\n    return \"str\", True\n\n\ndef _format_schema_for_help(schema: dict[str, Any]) -> str:\n    \"\"\"Format a JSON schema for display in help text.\"\"\"\n    # Pretty print the schema, indented for help text\n    schema_str = pydantic_core.to_json(schema, indent=2).decode()\n    # Indent each line for help text alignment\n    lines = schema_str.split(\"\\n\")\n    indented = \"\\n                          \".join(lines)\n    return f\"JSON Schema: {indented}\"\n\n\n# ---------------------------------------------------------------------------\n# Transport serialization\n# ---------------------------------------------------------------------------\n\n\ndef serialize_transport(\n    resolved: str | dict[str, Any] | ClientTransport,\n) -> tuple[str, set[str]]:\n    \"\"\"Serialize a resolved transport to a Python expression string.\n\n    Returns ``(expression, extra_imports)`` where *extra_imports* is a set of\n    import lines needed by the expression.\n    \"\"\"\n    if isinstance(resolved, str):\n        return repr(resolved), set()\n\n    if isinstance(resolved, StdioTransport):\n        parts = [f\"command={resolved.command!r}\", f\"args={resolved.args!r}\"]\n        if resolved.env:\n            parts.append(f\"env={resolved.env!r}\")\n        if resolved.cwd:\n            parts.append(f\"cwd={resolved.cwd!r}\")\n        expr = f\"StdioTransport({', '.join(parts)})\"\n        imports = {\"from fastmcp.client.transports import StdioTransport\"}\n        return expr, imports\n\n    if isinstance(resolved, dict):\n        return repr(resolved), set()\n\n    # Fallback: try repr\n    return repr(resolved), set()\n\n\n# ---------------------------------------------------------------------------\n# Per-tool code generation\n# ---------------------------------------------------------------------------\n\n\ndef _to_python_identifier(name: str) -> str:\n    \"\"\"Sanitize a string into a valid Python identifier.\"\"\"\n    safe = re.sub(r\"[^a-zA-Z0-9_]\", \"_\", name)\n    if safe and safe[0].isdigit():\n        safe = f\"_{safe}\"\n    safe = safe or \"_unnamed\"\n    if keyword.iskeyword(safe):\n        safe = f\"{safe}_\"\n    return safe\n\n\ndef _tool_function_source(tool: mcp.types.Tool) -> str:\n    \"\"\"Generate the source for a single ``@call_tool_app.command`` function.\"\"\"\n    schema = tool.inputSchema\n    properties: dict[str, Any] = schema.get(\"properties\", {})\n    required = set(schema.get(\"required\", []))\n\n    # Build parameter lines and track which need JSON parsing\n    param_lines: list[str] = []\n    call_args: list[str] = []\n    json_params: list[tuple[str, str]] = []  # (prop_name, safe_name)\n    seen_names: dict[str, str] = {}  # safe_name -> original prop_name\n\n    for prop_name, prop_schema in properties.items():\n        py_type, needs_json = _schema_to_python_type(prop_schema)\n        help_text = prop_schema.get(\"description\", \"\")\n        is_required = prop_name in required\n        safe_name = _to_python_identifier(prop_name)\n\n        # Check for name collisions after sanitization\n        if safe_name in seen_names:\n            raise ValueError(\n                f\"Parameter name collision: '{prop_name}' and '{seen_names[safe_name]}' \"\n                f\"both sanitize to '{safe_name}'\"\n            )\n        seen_names[safe_name] = prop_name\n\n        # For complex types, add schema to help text\n        if needs_json:\n            schema_help = _format_schema_for_help(prop_schema)\n            help_text = f\"{help_text}\\\\n{schema_help}\" if help_text else schema_help\n            json_params.append((prop_name, safe_name))\n\n        # Escape special characters in help text\n        help_escaped = (\n            help_text.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"').replace(\"\\n\", \"\\\\n\")\n        )\n\n        # Build parameter annotation\n        if is_required:\n            annotation = (\n                f'Annotated[{py_type}, cyclopts.Parameter(help=\"{help_escaped}\")]'\n            )\n            param_lines.append(f\"    {safe_name}: {annotation},\")\n        else:\n            default = prop_schema.get(\"default\")\n            if default is not None:\n                # For complex types with defaults, serialize to JSON string\n                if needs_json:\n                    default_str = pydantic_core.to_json(default, fallback=str).decode()\n                    annotation = f'Annotated[{py_type}, cyclopts.Parameter(help=\"{help_escaped}\")]'\n                    param_lines.append(\n                        f\"    {safe_name}: {annotation} = {default_str!r},\"\n                    )\n                else:\n                    annotation = f'Annotated[{py_type}, cyclopts.Parameter(help=\"{help_escaped}\")]'\n                    param_lines.append(f\"    {safe_name}: {annotation} = {default!r},\")\n            else:\n                # For list types, default to empty list; others default to None\n                if py_type.startswith(\"list[\"):\n                    annotation = f'Annotated[{py_type}, cyclopts.Parameter(help=\"{help_escaped}\")]'\n                    param_lines.append(f\"    {safe_name}: {annotation} = [],\")\n                else:\n                    annotation = f'Annotated[{py_type} | None, cyclopts.Parameter(help=\"{help_escaped}\")]'\n                    param_lines.append(f\"    {safe_name}: {annotation} = None,\")\n\n        call_args.append(f\"{prop_name!r}: {safe_name}\")\n\n    # Function name: sanitize to valid Python identifier\n    fn_name = _to_python_identifier(tool.name)\n\n    # Docstring - use single-quoted docstrings to avoid triple-quote escaping issues\n    description = (tool.description or \"\").replace(\"\\\\\", \"\\\\\\\\\").replace(\"'\", \"\\\\'\")\n\n    lines = []\n    lines.append(\"\")\n    # Always pass name= to preserve the original tool name (cyclopts\n    # would otherwise convert underscores to hyphens).\n    lines.append(f\"@call_tool_app.command(name={tool.name!r})\")\n    lines.append(f\"async def {fn_name}(\")\n\n    if param_lines:\n        lines.append(\"    *,\")\n        lines.extend(param_lines)\n\n    lines.append(\") -> None:\")\n    lines.append(f\"    '''{description}'''\")\n\n    # Add JSON parsing for complex parameters\n    if json_params:\n        lines.append(\"    # Parse JSON parameters\")\n        for _prop_name, safe_name in json_params:\n            lines.append(\n                f\"    {safe_name}_parsed = json.loads({safe_name}) if isinstance({safe_name}, str) else {safe_name}\"\n            )\n        lines.append(\"\")\n\n    # Build call arguments, using parsed versions for JSON params\n    call_arg_parts = []\n    for prop_name, _ in properties.items():\n        safe_name = _to_python_identifier(prop_name)\n        if any(pn == prop_name for pn, _ in json_params):\n            call_arg_parts.append(f\"{prop_name!r}: {safe_name}_parsed\")\n        else:\n            call_arg_parts.append(f\"{prop_name!r}: {safe_name}\")\n\n    dict_items = \", \".join(call_arg_parts)\n    lines.append(f\"    await _call_tool({tool.name!r}, {{{dict_items}}})\")\n    lines.append(\"\")\n\n    return \"\\n\".join(lines)\n\n\n# ---------------------------------------------------------------------------\n# Full script generation\n# ---------------------------------------------------------------------------\n\n\ndef generate_cli_script(\n    server_name: str,\n    server_spec: str,\n    transport_code: str,\n    extra_imports: set[str],\n    tools: list[mcp.types.Tool],\n) -> str:\n    \"\"\"Generate the full CLI script source code.\"\"\"\n\n    # Determine app name from server_name - sanitize for use in string literal\n    app_name = (\n        server_name.replace(\" \", \"-\").lower().replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"')\n    )\n\n    # --- Header ---\n    lines: list[str] = []\n    lines.append(\"#!/usr/bin/env python3\")\n    lines.append(f'\"\"\"CLI for {server_name} MCP server.')\n    lines.append(\"\")\n    lines.append(f\"Generated by: fastmcp generate-cli {server_spec}\")\n    lines.append('\"\"\"')\n    lines.append(\"\")\n\n    # --- Imports ---\n    lines.append(\"import json\")\n    lines.append(\"import sys\")\n    lines.append(\"from typing import Annotated\")\n    lines.append(\"\")\n    lines.append(\"import cyclopts\")\n    lines.append(\"import mcp.types\")\n    lines.append(\"from rich.console import Console\")\n    lines.append(\"\")\n    lines.append(\"from fastmcp import Client\")\n    for imp in sorted(extra_imports):\n        lines.append(imp)\n    lines.append(\"\")\n\n    # --- Transport config ---\n    lines.append(\"# Modify this to change how the CLI connects to the MCP server.\")\n    lines.append(f\"CLIENT_SPEC = {transport_code}\")\n    lines.append(\"\")\n\n    # --- App setup ---\n    server_name_escaped = server_name.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"')\n    lines.append(\n        f'app = cyclopts.App(name=\"{app_name}\", help=\"CLI for {server_name_escaped} MCP server\")'\n    )\n    lines.append(\n        'call_tool_app = cyclopts.App(name=\"call-tool\", help=\"Call a tool on the server\")'\n    )\n    lines.append(\"app.command(call_tool_app)\")\n    lines.append(\"\")\n    lines.append(\"console = Console()\")\n    lines.append(\"\")\n    lines.append(\"\")\n\n    # --- Shared helpers ---\n    lines.append(\n        textwrap.dedent(\"\"\"\\\n        # ---------------------------------------------------------------------------\n        # Helpers\n        # ---------------------------------------------------------------------------\n\n\n        def _print_tool_result(result):\n            if result.is_error:\n                for block in result.content:\n                    if isinstance(block, mcp.types.TextContent):\n                        console.print(f\"[bold red]Error:[/bold red] {block.text}\")\n                    else:\n                        console.print(f\"[bold red]Error:[/bold red] {block}\")\n                sys.exit(1)\n\n            if result.structured_content is not None:\n                console.print_json(json.dumps(result.structured_content))\n                return\n\n            for block in result.content:\n                if isinstance(block, mcp.types.TextContent):\n                    console.print(block.text)\n                elif isinstance(block, mcp.types.ImageContent):\n                    size = len(block.data) * 3 // 4\n                    console.print(f\"[dim][Image: {block.mimeType}, ~{size} bytes][/dim]\")\n                elif isinstance(block, mcp.types.AudioContent):\n                    size = len(block.data) * 3 // 4\n                    console.print(f\"[dim][Audio: {block.mimeType}, ~{size} bytes][/dim]\")\n\n\n        async def _call_tool(tool_name: str, arguments: dict) -> None:\n            # Filter out None values and empty lists (defaults for optional array params)\n            filtered = {\n                k: v\n                for k, v in arguments.items()\n                if v is not None and (not isinstance(v, list) or len(v) > 0)\n            }\n            async with Client(CLIENT_SPEC) as client:\n                result = await client.call_tool(tool_name, filtered, raise_on_error=False)\n                _print_tool_result(result)\n                if result.is_error:\n                    sys.exit(1)\"\"\")\n    )\n    lines.append(\"\")\n    lines.append(\"\")\n\n    # --- Generic commands ---\n    lines.append(\n        textwrap.dedent(\"\"\"\\\n        # ---------------------------------------------------------------------------\n        # List / read commands\n        # ---------------------------------------------------------------------------\n\n\n        @app.command\n        async def list_tools() -> None:\n            \\\"\\\"\\\"List available tools.\\\"\\\"\\\"\n            async with Client(CLIENT_SPEC) as client:\n                tools = await client.list_tools()\n                if not tools:\n                    console.print(\"[dim]No tools found.[/dim]\")\n                    return\n                for tool in tools:\n                    sig_parts = []\n                    props = tool.inputSchema.get(\"properties\", {})\n                    required = set(tool.inputSchema.get(\"required\", []))\n                    for pname, pschema in props.items():\n                        ptype = pschema.get(\"type\", \"string\")\n                        if pname in required:\n                            sig_parts.append(f\"{pname}: {ptype}\")\n                        else:\n                            sig_parts.append(f\"{pname}: {ptype} = ...\")\n                    sig = f\"{tool.name}({', '.join(sig_parts)})\"\n                    console.print(f\"  [cyan]{sig}[/cyan]\")\n                    if tool.description:\n                        console.print(f\"    {tool.description}\")\n                    console.print()\n\n\n        @app.command\n        async def list_resources() -> None:\n            \\\"\\\"\\\"List available resources.\\\"\\\"\\\"\n            async with Client(CLIENT_SPEC) as client:\n                resources = await client.list_resources()\n                if not resources:\n                    console.print(\"[dim]No resources found.[/dim]\")\n                    return\n                for r in resources:\n                    console.print(f\"  [cyan]{r.uri}[/cyan]\")\n                    desc_parts = [r.name or \"\", r.description or \"\"]\n                    desc = \" — \".join(p for p in desc_parts if p)\n                    if desc:\n                        console.print(f\"    {desc}\")\n                console.print()\n\n\n        @app.command\n        async def read_resource(uri: Annotated[str, cyclopts.Parameter(help=\"Resource URI\")]) -> None:\n            \\\"\\\"\\\"Read a resource by URI.\\\"\\\"\\\"\n            async with Client(CLIENT_SPEC) as client:\n                contents = await client.read_resource(uri)\n                for block in contents:\n                    if isinstance(block, mcp.types.TextResourceContents):\n                        console.print(block.text)\n                    elif isinstance(block, mcp.types.BlobResourceContents):\n                        size = len(block.blob) * 3 // 4\n                        console.print(f\"[dim][Blob: {block.mimeType}, ~{size} bytes][/dim]\")\n\n\n        @app.command\n        async def list_prompts() -> None:\n            \\\"\\\"\\\"List available prompts.\\\"\\\"\\\"\n            async with Client(CLIENT_SPEC) as client:\n                prompts = await client.list_prompts()\n                if not prompts:\n                    console.print(\"[dim]No prompts found.[/dim]\")\n                    return\n                for p in prompts:\n                    args_str = \"\"\n                    if p.arguments:\n                        parts = [a.name for a in p.arguments]\n                        args_str = f\"({', '.join(parts)})\"\n                    console.print(f\"  [cyan]{p.name}{args_str}[/cyan]\")\n                    if p.description:\n                        console.print(f\"    {p.description}\")\n                console.print()\n\n\n        @app.command\n        async def get_prompt(\n            name: Annotated[str, cyclopts.Parameter(help=\"Prompt name\")],\n            *arguments: str,\n        ) -> None:\n            \\\"\\\"\\\"Get a prompt by name. Pass arguments as key=value pairs.\\\"\\\"\\\"\n            parsed: dict[str, str] = {}\n            for arg in arguments:\n                if \"=\" not in arg:\n                    console.print(f\"[bold red]Error:[/bold red] Invalid argument {arg!r} — expected key=value\")\n                    sys.exit(1)\n                key, value = arg.split(\"=\", 1)\n                parsed[key] = value\n\n            async with Client(CLIENT_SPEC) as client:\n                result = await client.get_prompt(name, parsed or None)\n                for msg in result.messages:\n                    console.print(f\"[bold]{msg.role}:[/bold]\")\n                    if isinstance(msg.content, mcp.types.TextContent):\n                        console.print(f\"  {msg.content.text}\")\n                    elif isinstance(msg.content, mcp.types.ImageContent):\n                        size = len(msg.content.data) * 3 // 4\n                        console.print(f\"  [dim][Image: {msg.content.mimeType}, ~{size} bytes][/dim]\")\n                    else:\n                        console.print(f\"  {msg.content}\")\n                    console.print()\"\"\")\n    )\n    lines.append(\"\")\n    lines.append(\"\")\n\n    # --- Generated tool commands ---\n    if tools:\n        lines.append(\n            \"# ---------------------------------------------------------------------------\"\n        )\n        lines.append(\"# Tool commands (generated from server schema)\")\n        lines.append(\n            \"# ---------------------------------------------------------------------------\"\n        )\n\n        for tool in tools:\n            lines.append(_tool_function_source(tool))\n\n    # --- Entry point ---\n    lines.append(\"\")\n    lines.append('if __name__ == \"__main__\":')\n    lines.append(\"    app()\")\n    lines.append(\"\")\n\n    return \"\\n\".join(lines)\n\n\n# ---------------------------------------------------------------------------\n# Skill (SKILL.md) generation\n# ---------------------------------------------------------------------------\n\n_JSON_SCHEMA_TYPE_LABELS: dict[str, str] = {\n    \"string\": \"string\",\n    \"integer\": \"integer\",\n    \"number\": \"number\",\n    \"boolean\": \"boolean\",\n    \"null\": \"null\",\n    \"array\": \"array\",\n    \"object\": \"object\",\n}\n\n\ndef _param_to_cli_flag(prop_name: str) -> str:\n    \"\"\"Convert a JSON Schema property name to its CLI flag form.\n\n    Replicates cyclopts' default_name_transform: camelCase → snake_case,\n    lowercase, underscores → hyphens, strip leading/trailing hyphens.\n    \"\"\"\n    safe = _to_python_identifier(prop_name)\n    # camelCase / PascalCase → snake_case\n    safe = re.sub(r\"(.)([A-Z][a-z]+)\", r\"\\1_\\2\", safe)\n    safe = re.sub(r\"([a-z0-9])([A-Z])\", r\"\\1_\\2\", safe)\n    safe = safe.lower().replace(\"_\", \"-\").strip(\"-\")\n    return f\"--{safe}\" if safe else \"--arg\"\n\n\ndef _schema_type_label(prop_schema: dict[str, Any]) -> str:\n    \"\"\"Return a human-readable type label for a property schema.\"\"\"\n    schema_type = prop_schema.get(\"type\", \"string\")\n    if isinstance(schema_type, list):\n        labels = [_JSON_SCHEMA_TYPE_LABELS.get(t, t) for t in schema_type]\n        return \" | \".join(labels)\n\n    label = _JSON_SCHEMA_TYPE_LABELS.get(schema_type, schema_type)\n\n    # For arrays, include item type if simple\n    if schema_type == \"array\":\n        items = prop_schema.get(\"items\", {})\n        item_type = items.get(\"type\", \"\")\n        if isinstance(item_type, str) and item_type in _JSON_SCHEMA_TYPE_LABELS:\n            return f\"array[{item_type}]\"\n\n    return label\n\n\ndef _tool_skill_section(tool: mcp.types.Tool, cli_filename: str) -> str:\n    \"\"\"Generate a SKILL.md section for a single tool.\"\"\"\n    schema = tool.inputSchema\n    properties: dict[str, Any] = schema.get(\"properties\", {})\n    required = set(schema.get(\"required\", []))\n\n    # Build example invocation flags\n    flag_parts_list: list[str] = []\n    for p, p_schema in properties.items():\n        flag = _param_to_cli_flag(p)\n        schema_type = p_schema.get(\"type\")\n        is_bool = schema_type == \"boolean\" or (\n            isinstance(schema_type, list) and \"boolean\" in schema_type\n        )\n        if is_bool:\n            flag_parts_list.append(flag)\n        else:\n            flag_parts_list.append(f\"{flag} <value>\")\n    flag_parts = \" \".join(flag_parts_list)\n    invocation = f\"uv run --with fastmcp python {cli_filename} call-tool {tool.name}\"\n    if flag_parts:\n        invocation += f\" {flag_parts}\"\n\n    # Build parameter table rows\n    rows: list[str] = []\n    for prop_name, prop_schema in properties.items():\n        flag = f\"`{_param_to_cli_flag(prop_name)}`\"\n        type_label = _schema_type_label(prop_schema).replace(\"|\", \"\\\\|\")\n        is_required = \"yes\" if prop_name in required else \"no\"\n        description = prop_schema.get(\"description\", \"\")\n        _, needs_json = _schema_to_python_type(prop_schema)\n        if needs_json:\n            description = (\n                f\"{description} (JSON string)\" if description else \"JSON string\"\n            )\n        description = description.replace(\"\\n\", \" \").replace(\"|\", \"\\\\|\")\n        rows.append(f\"| {flag} | {type_label} | {is_required} | {description} |\")\n\n    param_table = \"\"\n    if rows:\n        header = \"| Flag | Type | Required | Description |\\n|------|------|----------|-------------|\"\n        param_table = f\"\\n{header}\\n\" + \"\\n\".join(rows) + \"\\n\"\n\n    lines: list[str] = [f\"### {tool.name}\"]\n    if tool.description:\n        lines.extend([\"\", tool.description])\n    lines.extend([\"\", \"```bash\", invocation, \"```\"])\n    if param_table:\n        lines.extend([\"\", param_table.strip(\"\\n\")])\n    return \"\\n\".join(lines)\n\n\ndef generate_skill_content(\n    server_name: str,\n    cli_filename: str,\n    tools: list[mcp.types.Tool],\n) -> str:\n    \"\"\"Generate a SKILL.md file for a generated CLI script.\"\"\"\n    skill_name = (\n        server_name.replace(\" \", \"-\").lower().replace(\"\\\\\", \"\").replace('\"', \"\")\n    )\n    safe_name = server_name.replace(\"\\\\\", \"\").replace('\"', \"\")\n    description = f\"CLI for the {safe_name} MCP server. Call tools, list resources, and get prompts.\"\n\n    lines = [\n        \"---\",\n        f'name: \"{skill_name}-cli\"',\n        f'description: \"{description}\"',\n        \"---\",\n        \"\",\n        f\"# {server_name} CLI\",\n        \"\",\n    ]\n\n    if tools:\n        tool_bodies = \"\\n\\n\".join(\n            _tool_skill_section(tool, cli_filename) for tool in tools\n        )\n        lines.extend([\"## Tool Commands\", \"\", tool_bodies, \"\"])\n\n    lines.extend(\n        [\n            \"## Utility Commands\",\n            \"\",\n            \"```bash\",\n            f\"uv run --with fastmcp python {cli_filename} list-tools\",\n            f\"uv run --with fastmcp python {cli_filename} list-resources\",\n            f\"uv run --with fastmcp python {cli_filename} read-resource <uri>\",\n            f\"uv run --with fastmcp python {cli_filename} list-prompts\",\n            f\"uv run --with fastmcp python {cli_filename} get-prompt <name> [key=value ...]\",\n            \"```\",\n            \"\",\n        ]\n    )\n\n    return \"\\n\".join(lines)\n\n\n# ---------------------------------------------------------------------------\n# CLI command\n# ---------------------------------------------------------------------------\n\n\nasync def generate_cli_command(\n    server_spec: Annotated[\n        str,\n        cyclopts.Parameter(\n            help=\"Server URL, Python file, MCPConfig JSON, discovered name, or .js file\",\n        ),\n    ],\n    output: Annotated[\n        str,\n        cyclopts.Parameter(\n            help=\"Output file path (default: cli.py)\",\n        ),\n    ] = \"cli.py\",\n    *,\n    force: Annotated[\n        bool,\n        cyclopts.Parameter(\n            name=[\"-f\", \"--force\"],\n            help=\"Overwrite output file if it exists\",\n        ),\n    ] = False,\n    timeout: Annotated[\n        float | None,\n        cyclopts.Parameter(\"--timeout\", help=\"Connection timeout in seconds\"),\n    ] = None,\n    auth: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--auth\",\n            help=\"Auth method: 'oauth', a bearer token string, or 'none' to disable\",\n        ),\n    ] = None,\n    no_skill: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--no-skill\",\n            help=\"Skip generating a SKILL.md agent skill alongside the CLI\",\n        ),\n    ] = False,\n) -> None:\n    \"\"\"Generate a standalone CLI script from an MCP server.\n\n    Connects to the server, reads its tools/resources/prompts, and writes\n    a Python script that can invoke them directly. Also generates a SKILL.md\n    agent skill file unless --no-skill is passed.\n\n    Examples:\n        fastmcp generate-cli weather\n        fastmcp generate-cli weather my_cli.py\n        fastmcp generate-cli http://localhost:8000/mcp\n        fastmcp generate-cli server.py output.py -f\n        fastmcp generate-cli weather --no-skill\n    \"\"\"\n    output_path = Path(output)\n    skill_path = output_path.parent / \"SKILL.md\"\n\n    # Check both files up front before doing any work\n    existing: list[Path] = []\n    if output_path.exists() and not force:\n        existing.append(output_path)\n    if not no_skill and skill_path.exists() and not force:\n        existing.append(skill_path)\n    if existing:\n        names = \", \".join(f\"[cyan]{p}[/cyan]\" for p in existing)\n        console.print(\n            f\"[bold red]Error:[/bold red] {names} already exist(s). \"\n            f\"Use [cyan]-f[/cyan] to overwrite.\"\n        )\n        sys.exit(1)\n\n    # Resolve the server spec to a transport\n    resolved = resolve_server_spec(server_spec)\n    transport_code, extra_imports = serialize_transport(resolved)\n\n    # Derive a human-friendly server name from the spec\n    server_name = _derive_server_name(server_spec)\n\n    # Connect and discover capabilities\n    client = _build_client(resolved, timeout=timeout, auth=auth)\n\n    try:\n        async with client:\n            tools = await client.list_tools()\n            console.print(\n                f\"[dim]Discovered {len(tools)} tool(s) from {server_spec}[/dim]\"\n            )\n\n    except (RuntimeError, TimeoutError, McpError, OSError) as exc:\n        console.print(f\"[bold red]Error:[/bold red] Could not connect: {exc}\")\n        sys.exit(1)\n\n    # Generate and write the script\n    script = generate_cli_script(\n        server_name=server_name,\n        server_spec=server_spec,\n        transport_code=transport_code,\n        extra_imports=extra_imports,\n        tools=tools,\n    )\n\n    output_path.write_text(script)\n    output_path.chmod(output_path.stat().st_mode | 0o111)  # make executable\n\n    console.print(\n        f\"[green]✓[/green] Wrote [cyan]{output_path}[/cyan] \"\n        f\"with {len(tools)} tool command(s)\"\n    )\n\n    if not no_skill:\n        skill_content = generate_skill_content(\n            server_name=server_name,\n            cli_filename=output_path.name,\n            tools=tools,\n        )\n        skill_path.write_text(skill_content)\n        console.print(f\"[green]✓[/green] Wrote [cyan]{skill_path}[/cyan]\")\n\n    console.print(f\"[dim]Run: python {output_path} --help[/dim]\")\n\n\ndef _derive_server_name(server_spec: str) -> str:\n    \"\"\"Derive a human-friendly name from a server spec.\"\"\"\n    # URL — use hostname\n    if server_spec.startswith((\"http://\", \"https://\")):\n        parsed = urlparse(server_spec)\n        return parsed.hostname or \"server\"\n\n    # File path — use stem\n    if server_spec.endswith((\".py\", \".js\", \".json\")):\n        return Path(server_spec).stem\n\n    # Bare name or qualified name\n    if \":\" in server_spec:\n        name = server_spec.split(\":\", 1)[1]\n        return name or server_spec.split(\":\", 1)[0]\n\n    return server_spec\n"
  },
  {
    "path": "src/fastmcp/cli/install/__init__.py",
    "content": "\"\"\"Install subcommands for FastMCP CLI using Cyclopts.\"\"\"\n\nimport cyclopts\n\nfrom .claude_code import claude_code_command\nfrom .claude_desktop import claude_desktop_command\nfrom .cursor import cursor_command\nfrom .gemini_cli import gemini_cli_command\nfrom .goose import goose_command\nfrom .mcp_json import mcp_json_command\nfrom .stdio import stdio_command\n\n# Create a cyclopts app for install subcommands\ninstall_app = cyclopts.App(\n    name=\"install\",\n    help=\"Install MCP servers in various clients and formats.\",\n)\n\n# Register each command from its respective module\ninstall_app.command(claude_code_command, name=\"claude-code\")\ninstall_app.command(claude_desktop_command, name=\"claude-desktop\")\ninstall_app.command(cursor_command, name=\"cursor\")\ninstall_app.command(gemini_cli_command, name=\"gemini-cli\")\ninstall_app.command(goose_command, name=\"goose\")\ninstall_app.command(mcp_json_command, name=\"mcp-json\")\ninstall_app.command(stdio_command, name=\"stdio\")\n"
  },
  {
    "path": "src/fastmcp/cli/install/claude_code.py",
    "content": "\"\"\"Claude Code integration for FastMCP install using Cyclopts.\"\"\"\n\nimport shutil\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nfrom rich import print\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nfrom .shared import process_common_args, validate_server_name\n\nlogger = get_logger(__name__)\n\n\ndef find_claude_command() -> str | None:\n    \"\"\"Find the Claude Code CLI command.\n\n    Checks common installation locations since 'claude' is often a shell alias\n    that doesn't work with subprocess calls.\n    \"\"\"\n    # First try shutil.which() in case it's a real executable in PATH\n    claude_in_path = shutil.which(\"claude\")\n    if claude_in_path:\n        try:\n            result = subprocess.run(\n                [claude_in_path, \"--version\"],\n                check=True,\n                capture_output=True,\n                text=True,\n            )\n            if \"Claude Code\" in result.stdout:\n                return claude_in_path\n        except (subprocess.CalledProcessError, FileNotFoundError):\n            pass\n\n    # Check common installation locations (aliases don't work with subprocess)\n    potential_paths = [\n        # Default Claude Code installation location (after migration)\n        Path.home() / \".claude\" / \"local\" / \"claude\",\n        # npm global installation on macOS/Linux (default)\n        Path(\"/usr/local/bin/claude\"),\n        # npm global installation with custom prefix\n        Path.home() / \".npm-global\" / \"bin\" / \"claude\",\n    ]\n\n    for path in potential_paths:\n        if path.exists():\n            try:\n                result = subprocess.run(\n                    [str(path), \"--version\"],\n                    check=True,\n                    capture_output=True,\n                    text=True,\n                )\n                if \"Claude Code\" in result.stdout:\n                    return str(path)\n            except (subprocess.CalledProcessError, FileNotFoundError):\n                continue\n\n    return None\n\n\ndef check_claude_code_available() -> bool:\n    \"\"\"Check if Claude Code CLI is available.\"\"\"\n    return find_claude_command() is not None\n\n\ndef install_claude_code(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n) -> bool:\n    \"\"\"Install FastMCP server in Claude Code.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        name: Name for the server in Claude Code\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n\n    Returns:\n        True if installation was successful, False otherwise\n    \"\"\"\n    # Check if Claude Code CLI is available\n    claude_cmd = find_claude_command()\n    if not claude_cmd:\n        print(\n            \"[red]Claude Code CLI not found.[/red]\\n\"\n            \"[blue]Please ensure Claude Code is installed. Try running 'claude --version' to verify.[/blue]\"\n        )\n        return False\n\n    env_config = UVEnvironment(\n        python=python_version,\n        dependencies=(with_packages or []) + [\"fastmcp\"],\n        requirements=with_requirements,\n        project=project,\n        editable=with_editable,\n    )\n\n    # Build server spec from parsed components\n    if server_object:\n        server_spec = f\"{file.resolve()}:{server_object}\"\n    else:\n        server_spec = str(file.resolve())\n\n    # Build the full command\n    full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n\n    validate_server_name(name)\n\n    # Build claude mcp add command\n    cmd_parts = [claude_cmd, \"mcp\", \"add\", name]\n\n    # Add environment variables if specified\n    if env_vars:\n        for key, value in env_vars.items():\n            cmd_parts.extend([\"-e\", f\"{key}={value}\"])\n\n    # Add server name and command\n    cmd_parts.append(\"--\")\n    cmd_parts.extend(full_command)\n\n    try:\n        # Run the claude mcp add command\n        subprocess.run(cmd_parts, check=True, capture_output=True, text=True)\n        return True\n    except subprocess.CalledProcessError as e:\n        print(\n            f\"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e.stderr.strip() if e.stderr else str(e)}[/red]\"\n        )\n        return False\n    except Exception as e:\n        print(f\"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e}[/red]\")\n        return False\n\n\nasync def claude_code_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the server in Claude Code\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory with pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    env_vars: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--env\",\n            help=\"Environment variables in KEY=VALUE format (can be used multiple times)\",\n        ),\n    ] = None,\n    env_file: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--env-file\",\n            help=\"Load environment variables from .env file\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Install an MCP server in Claude Code.\n\n    Args:\n        server_spec: Python file to install, optionally with :object suffix\n    \"\"\"\n    # Convert None to empty lists for list parameters\n    with_editable = with_editable or []\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n    file, server_object, name, packages, env_dict = await process_common_args(\n        server_spec, server_name, with_packages, env_vars, env_file\n    )\n\n    success = install_claude_code(\n        file=file,\n        server_object=server_object,\n        name=name,\n        with_editable=with_editable,\n        with_packages=packages,\n        env_vars=env_dict,\n        python_version=python,\n        with_requirements=with_requirements,\n        project=project,\n    )\n\n    if success:\n        print(f\"[green]Successfully installed '{name}' in Claude Code[/green]\")\n    else:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/install/claude_desktop.py",
    "content": "\"\"\"Claude Desktop integration for FastMCP install using Cyclopts.\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nfrom rich import print\n\nfrom fastmcp.mcp_config import StdioMCPServer, update_config_file\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nfrom .shared import process_common_args\n\nlogger = get_logger(__name__)\n\n\ndef get_claude_config_path(config_path: Path | None = None) -> Path | None:\n    \"\"\"Get the Claude config directory based on platform.\n\n    Args:\n        config_path: Optional custom path to the Claude Desktop config directory\n    \"\"\"\n\n    if config_path:\n        if not config_path.exists():\n            print(f\"[red]The specified config path does not exist: {config_path}[/red]\")\n            return None\n        return config_path\n\n    if sys.platform == \"win32\":\n        path = Path(Path.home(), \"AppData\", \"Roaming\", \"Claude\")\n    elif sys.platform == \"darwin\":\n        path = Path(Path.home(), \"Library\", \"Application Support\", \"Claude\")\n    elif sys.platform.startswith(\"linux\"):\n        path = Path(\n            os.environ.get(\"XDG_CONFIG_HOME\", Path.home() / \".config\"), \"Claude\"\n        )\n    else:\n        return None\n\n    if path.exists():\n        return path\n    return None\n\n\ndef install_claude_desktop(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n    config_path: Path | None = None,\n) -> bool:\n    \"\"\"Install FastMCP server in Claude Desktop.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        name: Name for the server in Claude's config\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n        config_path: Optional custom path to Claude Desktop config directory\n\n    Returns:\n        True if installation was successful, False otherwise\n    \"\"\"\n    config_dir = get_claude_config_path(config_path=config_path)\n    if not config_dir:\n        if not config_path:\n            print(\n                \"[red]Claude Desktop config directory not found.[/red]\\n\"\n                \"[blue]Please ensure Claude Desktop is installed and has been run at least once to initialize its config.[/blue]\"\n            )\n        return False\n\n    config_file = config_dir / \"claude_desktop_config.json\"\n\n    env_config = UVEnvironment(\n        python=python_version,\n        dependencies=(with_packages or []) + [\"fastmcp\"],\n        requirements=with_requirements,\n        project=project,\n        editable=with_editable,\n    )\n    # Build server spec from parsed components\n    if server_object:\n        server_spec = f\"{file.resolve()}:{server_object}\"\n    else:\n        server_spec = str(file.resolve())\n\n    # Build the full command\n    full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n\n    # Create server configuration\n    server_config = StdioMCPServer(\n        command=full_command[0],\n        args=full_command[1:],\n        env=env_vars or {},\n    )\n\n    try:\n        # Handle environment variable merging manually since we need to preserve existing config\n        if config_file.exists():\n            import json\n\n            content = config_file.read_text().strip()\n            if content:\n                config = json.loads(content)\n                if \"mcpServers\" in config and name in config[\"mcpServers\"]:\n                    existing_env = config[\"mcpServers\"][name].get(\"env\", {})\n                    if env_vars:\n                        # New vars take precedence over existing ones\n                        merged_env = {**existing_env, **env_vars}\n                    else:\n                        merged_env = existing_env\n                    server_config.env = merged_env\n\n        # Update configuration with correct function signature\n        update_config_file(config_file, name, server_config)\n        print(f\"[green]Successfully installed '{name}' in Claude Desktop[/green]\")\n        return True\n    except Exception as e:\n        print(f\"[red]Failed to install server: {e}[/red]\")\n        return False\n\n\nasync def claude_desktop_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the server in Claude Desktop's config\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory with pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    env_vars: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--env\",\n            help=\"Environment variables in KEY=VALUE format (can be used multiple times)\",\n        ),\n    ] = None,\n    env_file: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--env-file\",\n            help=\"Load environment variables from .env file\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n    config_path: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--config-path\",\n            help=\"Custom path to Claude Desktop config directory\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Install an MCP server in Claude Desktop.\n\n    Args:\n        server_spec: Python file to install, optionally with :object suffix\n    \"\"\"\n    # Convert None to empty lists for list parameters\n    with_editable = with_editable or []\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n    file, server_object, name, with_packages, env_dict = await process_common_args(\n        server_spec, server_name, with_packages, env_vars, env_file\n    )\n\n    success = install_claude_desktop(\n        file=file,\n        server_object=server_object,\n        name=name,\n        with_editable=with_editable,\n        with_packages=with_packages,\n        env_vars=env_dict,\n        python_version=python,\n        with_requirements=with_requirements,\n        project=project,\n        config_path=config_path,\n    )\n\n    if not success:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/install/cursor.py",
    "content": "\"\"\"Cursor integration for FastMCP install using Cyclopts.\"\"\"\n\nimport base64\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\nfrom urllib.parse import quote\n\nimport cyclopts\nfrom rich import print\n\nfrom fastmcp.mcp_config import StdioMCPServer, update_config_file\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nfrom .shared import open_deeplink as _shared_open_deeplink\nfrom .shared import process_common_args\n\nlogger = get_logger(__name__)\n\n\ndef generate_cursor_deeplink(\n    server_name: str,\n    server_config: StdioMCPServer,\n) -> str:\n    \"\"\"Generate a Cursor deeplink for installing the MCP server.\n\n    Args:\n        server_name: Name of the server\n        server_config: Server configuration\n\n    Returns:\n        Deeplink URL that can be clicked to install the server\n    \"\"\"\n    # Create the configuration structure expected by Cursor\n    # Base64 encode the configuration (URL-safe for query parameter)\n    config_json = server_config.model_dump_json(exclude_none=True)\n    config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode()\n\n    # Generate the deeplink URL with properly encoded server name\n    encoded_name = quote(server_name, safe=\"\")\n    deeplink = f\"cursor://anysphere.cursor-deeplink/mcp/install?name={encoded_name}&config={config_b64}\"\n\n    return deeplink\n\n\ndef open_deeplink(deeplink: str) -> bool:\n    \"\"\"Attempt to open a Cursor deeplink URL using the system's default handler.\n\n    Args:\n        deeplink: The deeplink URL to open\n\n    Returns:\n        True if the command succeeded, False otherwise\n    \"\"\"\n    return _shared_open_deeplink(deeplink, expected_scheme=\"cursor\")\n\n\ndef install_cursor_workspace(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    workspace_path: Path,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n) -> bool:\n    \"\"\"Install FastMCP server to workspace-specific Cursor configuration.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        name: Name for the server in Cursor\n        workspace_path: Path to the workspace directory\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n\n    Returns:\n        True if installation was successful, False otherwise\n    \"\"\"\n    # Ensure workspace path is absolute and exists\n    workspace_path = workspace_path.resolve()\n    if not workspace_path.exists():\n        print(f\"[red]Workspace directory does not exist: {workspace_path}[/red]\")\n        return False\n    if not workspace_path.is_dir():\n        print(f\"[red]Workspace path is not a directory: {workspace_path}[/red]\")\n        return False\n\n    # Create .cursor directory in workspace\n    cursor_dir = workspace_path / \".cursor\"\n    cursor_dir.mkdir(exist_ok=True)\n\n    config_file = cursor_dir / \"mcp.json\"\n\n    env_config = UVEnvironment(\n        python=python_version,\n        dependencies=(with_packages or []) + [\"fastmcp\"],\n        requirements=with_requirements,\n        project=project,\n        editable=with_editable,\n    )\n    # Build server spec from parsed components\n    if server_object:\n        server_spec = f\"{file.resolve()}:{server_object}\"\n    else:\n        server_spec = str(file.resolve())\n\n    # Build the full command\n    full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n\n    # Create server configuration\n    server_config = StdioMCPServer(\n        command=full_command[0],\n        args=full_command[1:],\n        env=env_vars or {},\n    )\n\n    try:\n        # Create the config file if it doesn't exist\n        if not config_file.exists():\n            config_file.write_text('{\"mcpServers\": {}}')\n\n        # Update configuration with the new server\n        update_config_file(config_file, name, server_config)\n        print(\n            f\"[green]Successfully installed '{name}' to workspace at {workspace_path}[/green]\"\n        )\n        return True\n    except Exception as e:\n        print(f\"[red]Failed to install server to workspace: {e}[/red]\")\n        return False\n\n\ndef install_cursor(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n    workspace: Path | None = None,\n) -> bool:\n    \"\"\"Install FastMCP server in Cursor.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        name: Name for the server in Cursor\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n        workspace: Optional workspace directory for project-specific installation\n\n    Returns:\n        True if installation was successful, False otherwise\n    \"\"\"\n\n    env_config = UVEnvironment(\n        python=python_version,\n        dependencies=(with_packages or []) + [\"fastmcp\"],\n        requirements=with_requirements,\n        project=project,\n        editable=with_editable,\n    )\n    # Build server spec from parsed components\n    if server_object:\n        server_spec = f\"{file.resolve()}:{server_object}\"\n    else:\n        server_spec = str(file.resolve())\n\n    # Build the full command\n    full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n\n    # If workspace is specified, install to workspace-specific config\n    if workspace:\n        return install_cursor_workspace(\n            file=file,\n            server_object=server_object,\n            name=name,\n            workspace_path=workspace,\n            with_editable=with_editable,\n            with_packages=with_packages,\n            env_vars=env_vars,\n            python_version=python_version,\n            with_requirements=with_requirements,\n            project=project,\n        )\n\n    # Create server configuration\n    server_config = StdioMCPServer(\n        command=full_command[0],\n        args=full_command[1:],\n        env=env_vars or {},\n    )\n\n    # Generate deeplink\n    deeplink = generate_cursor_deeplink(name, server_config)\n\n    print(f\"[blue]Opening Cursor to install '{name}'[/blue]\")\n\n    if open_deeplink(deeplink):\n        print(\"[green]Cursor should now open with the installation dialog[/green]\")\n        return True\n    else:\n        print(\n            \"[red]Could not open Cursor automatically.[/red]\\n\"\n            f\"[blue]Please copy this link and open it in Cursor: {deeplink}[/blue]\"\n        )\n        return False\n\n\nasync def cursor_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the server in Cursor\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory with pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    env_vars: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--env\",\n            help=\"Environment variables in KEY=VALUE format (can be used multiple times)\",\n        ),\n    ] = None,\n    env_file: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--env-file\",\n            help=\"Load environment variables from .env file\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n    workspace: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--workspace\",\n            help=\"Install to workspace directory (will create .cursor/ inside it) instead of using deeplink\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Install an MCP server in Cursor.\n\n    Args:\n        server_spec: Python file to install, optionally with :object suffix\n    \"\"\"\n    # Convert None to empty lists for list parameters\n    with_editable = with_editable or []\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n    file, server_object, name, with_packages, env_dict = await process_common_args(\n        server_spec, server_name, with_packages, env_vars, env_file\n    )\n\n    success = install_cursor(\n        file=file,\n        server_object=server_object,\n        name=name,\n        with_editable=with_editable,\n        with_packages=with_packages,\n        env_vars=env_dict,\n        python_version=python,\n        with_requirements=with_requirements,\n        project=project,\n        workspace=workspace,\n    )\n\n    if not success:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/install/gemini_cli.py",
    "content": "\"\"\"Gemini CLI integration for FastMCP install using Cyclopts.\"\"\"\n\nimport shutil\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nfrom rich import print\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nfrom .shared import process_common_args, validate_server_name\n\nlogger = get_logger(__name__)\n\n\ndef find_gemini_command() -> str | None:\n    \"\"\"Find the Gemini CLI command.\"\"\"\n    # First try shutil.which() in case it's a real executable in PATH\n    gemini_in_path = shutil.which(\"gemini\")\n    if gemini_in_path:\n        try:\n            # If 'gemini --version' fails, it's not the correct path\n            subprocess.run(\n                [gemini_in_path, \"--version\"],\n                check=True,\n                capture_output=True,\n            )\n            return gemini_in_path\n        except (subprocess.CalledProcessError, FileNotFoundError):\n            pass\n\n    # Check common installation locations (aliases don't work with subprocess)\n    potential_paths = [\n        # Default Gemini CLI installation location (after migration)\n        Path.home() / \".gemini\" / \"local\" / \"gemini\",\n        # npm global installation on macOS/Linux (default)\n        Path(\"/usr/local/bin/gemini\"),\n        # npm global installation with custom prefix\n        Path.home() / \".npm-global\" / \"bin\" / \"gemini\",\n        # Homebrew installation on macOS\n        Path(\"/opt/homebrew/bin/gemini\"),\n    ]\n\n    for path in potential_paths:\n        if path.exists():\n            # If 'gemini --version' fails, it's not the correct path\n            try:\n                subprocess.run(\n                    [str(path), \"--version\"],\n                    check=True,\n                    capture_output=True,\n                )\n                return str(path)\n            except (subprocess.CalledProcessError, FileNotFoundError):\n                continue\n\n    return None\n\n\ndef check_gemini_cli_available() -> bool:\n    \"\"\"Check if Gemini CLI is available.\"\"\"\n    return find_gemini_command() is not None\n\n\ndef install_gemini_cli(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n) -> bool:\n    \"\"\"Install FastMCP server in Gemini CLI.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        name: Name for the server in Gemini CLI\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n\n    Returns:\n        True if installation was successful, False otherwise\n    \"\"\"\n    # Check if Gemini CLI is available\n    gemini_cmd = find_gemini_command()\n    if not gemini_cmd:\n        print(\n            \"[red]Gemini CLI not found.[/red]\\n\"\n            \"[blue]Please ensure Gemini CLI is installed. Try running 'gemini --version' to verify.[/blue]\\n\"\n            \"[blue]You can install it using 'npm install -g @google/gemini-cli'.[/blue]\\n\"\n        )\n        return False\n\n    env_config = UVEnvironment(\n        python=python_version,\n        dependencies=(with_packages or []) + [\"fastmcp\"],\n        requirements=with_requirements,\n        project=project,\n        editable=with_editable,\n    )\n\n    # Build server spec from parsed components\n    if server_object:\n        server_spec = f\"{file.resolve()}:{server_object}\"\n    else:\n        server_spec = str(file.resolve())\n\n    # Build the full command\n    full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n\n    # Build gemini mcp add command\n    cmd_parts = [gemini_cmd, \"mcp\", \"add\"]\n\n    # Add environment variables if specified (before the name and command)\n    if env_vars:\n        for key, value in env_vars.items():\n            cmd_parts.extend([\"-e\", f\"{key}={value}\"])\n\n    validate_server_name(name)\n\n    # Add server name and command\n    cmd_parts.extend([name, full_command[0], \"--\"])\n    cmd_parts.extend(full_command[1:])\n\n    try:\n        # Run the gemini mcp add command\n        subprocess.run(cmd_parts, check=True, capture_output=True, text=True)\n        return True\n    except subprocess.CalledProcessError as e:\n        print(\n            f\"[red]Failed to install '[bold]{name}[/bold]' in Gemini CLI: {e.stderr.strip() if e.stderr else str(e)}[/red]\"\n        )\n        return False\n    except Exception as e:\n        print(f\"[red]Failed to install '[bold]{name}[/bold]' in Gemini CLI: {e}[/red]\")\n        return False\n\n\nasync def gemini_cli_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the server in Gemini CLI\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory with pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    env_vars: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--env\",\n            help=\"Environment variables in KEY=VALUE format (can be used multiple times)\",\n        ),\n    ] = None,\n    env_file: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--env-file\",\n            help=\"Load environment variables from .env file\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Install an MCP server in Gemini CLI.\n\n    Args:\n        server_spec: Python file to install, optionally with :object suffix\n    \"\"\"\n    # Convert None to empty lists for list parameters\n    with_editable = with_editable or []\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n    file, server_object, name, packages, env_dict = await process_common_args(\n        server_spec, server_name, with_packages, env_vars, env_file\n    )\n\n    success = install_gemini_cli(\n        file=file,\n        server_object=server_object,\n        name=name,\n        with_editable=with_editable,\n        with_packages=packages,\n        env_vars=env_dict,\n        python_version=python,\n        with_requirements=with_requirements,\n        project=project,\n    )\n\n    if success:\n        print(f\"[green]Successfully installed '{name}' in Gemini CLI\")\n    else:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/install/goose.py",
    "content": "\"\"\"Goose integration for FastMCP install using Cyclopts.\"\"\"\n\nimport re\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\nfrom urllib.parse import quote\n\nimport cyclopts\nfrom rich import print\n\nfrom fastmcp.utilities.logging import get_logger\n\nfrom .shared import open_deeplink, process_common_args\n\nlogger = get_logger(__name__)\n\n\ndef _slugify(name: str) -> str:\n    \"\"\"Convert a display name to a URL-safe identifier.\n\n    Lowercases, replaces non-alphanumeric runs with hyphens,\n    and strips leading/trailing hyphens.\n    \"\"\"\n    slug = re.sub(r\"[^a-z0-9]+\", \"-\", name.lower()).strip(\"-\")\n    return slug or \"fastmcp-server\"\n\n\ndef generate_goose_deeplink(\n    name: str,\n    command: str,\n    args: list[str],\n    *,\n    description: str = \"MCP server installed via FastMCP\",\n) -> str:\n    \"\"\"Generate a Goose deeplink for installing an MCP extension.\n\n    Args:\n        name: Human-readable display name for the extension.\n        command: The executable command (e.g. \"uv\").\n        args: Arguments to the command.\n        description: Short description shown in Goose.\n\n    Returns:\n        A goose://extension?... deeplink URL.\n    \"\"\"\n    extension_id = _slugify(name)\n\n    params: list[str] = [f\"cmd={quote(command, safe='')}\"]\n    for arg in args:\n        params.append(f\"arg={quote(arg, safe='')}\")\n    params.append(f\"id={quote(extension_id, safe='')}\")\n    params.append(f\"name={quote(name, safe='')}\")\n    params.append(f\"description={quote(description, safe='')}\")\n\n    return f\"goose://extension?{'&'.join(params)}\"\n\n\ndef _build_uvx_command(\n    server_spec: str,\n    *,\n    python_version: str | None = None,\n    with_packages: list[str] | None = None,\n) -> list[str]:\n    \"\"\"Build a uvx command for running a FastMCP server.\n\n    Goose requires uvx (not uv run) as the command. The uvx format is:\n        uvx [--with pkg] [--python X] fastmcp run <spec>\n\n    uvx automatically infers that the `fastmcp` command comes from the\n    `fastmcp` package, so --from is not needed.\n    \"\"\"\n    args: list[str] = [\"uvx\"]\n\n    if python_version:\n        args.extend([\"--python\", python_version])\n\n    for pkg in sorted(set(with_packages or [])):\n        if pkg != \"fastmcp\":\n            args.extend([\"--with\", pkg])\n\n    args.extend([\"fastmcp\", \"run\", server_spec])\n    return args\n\n\ndef install_goose(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    *,\n    with_packages: list[str] | None = None,\n    python_version: str | None = None,\n) -> bool:\n    \"\"\"Install FastMCP server in Goose via deeplink.\n\n    Args:\n        file: Path to the server file.\n        server_object: Optional server object name (for :object suffix).\n        name: Name for the extension in Goose.\n        with_packages: Optional list of additional packages to install.\n        python_version: Optional Python version to use.\n\n    Returns:\n        True if installation was successful, False otherwise.\n    \"\"\"\n    if server_object:\n        server_spec = f\"{file.resolve()}:{server_object}\"\n    else:\n        server_spec = str(file.resolve())\n\n    full_command = _build_uvx_command(\n        server_spec,\n        python_version=python_version,\n        with_packages=with_packages,\n    )\n\n    deeplink = generate_goose_deeplink(\n        name=name,\n        command=full_command[0],\n        args=full_command[1:],\n    )\n\n    print(f\"[blue]Opening Goose to install '{name}'[/blue]\")\n\n    if open_deeplink(deeplink, expected_scheme=\"goose\"):\n        print(\"[green]Goose should now open with the installation dialog[/green]\")\n        return True\n    else:\n        print(\n            \"[red]Could not open Goose automatically.[/red]\\n\"\n            f\"[blue]Please copy this link and open it in Goose: {deeplink}[/blue]\"\n        )\n        return False\n\n\nasync def goose_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the extension in Goose\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\",\n            help=\"Additional packages to install (can be used multiple times)\",\n        ),\n    ] = None,\n    env_vars: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--env\",\n            help=\"Environment variables in KEY=VALUE format (can be used multiple times)\",\n        ),\n    ] = None,\n    env_file: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--env-file\",\n            help=\"Load environment variables from .env file\",\n        ),\n    ] = None,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Install an MCP server in Goose.\n\n    Uses uvx to run the server. Environment variables are not included\n    in the deeplink; use `fastmcp install mcp-json` to generate a full\n    config for manual installation.\n\n    Args:\n        server_spec: Python file to install, optionally with :object suffix\n    \"\"\"\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n\n    if env_vars or env_file:\n        print(\n            \"[red]Goose deeplinks cannot include environment variables.[/red]\\n\"\n            \"[yellow]Use `fastmcp install mcp-json` to generate a config, then add it \"\n            \"to your Goose config file with env vars: \"\n            \"https://block.github.io/goose/docs/getting-started/using-extensions/#config-entry[/yellow]\"\n        )\n        sys.exit(1)\n\n    file, server_object, name, with_packages, _env_dict = await process_common_args(\n        server_spec, server_name, with_packages, env_vars, env_file\n    )\n\n    success = install_goose(\n        file=file,\n        server_object=server_object,\n        name=name,\n        with_packages=with_packages,\n        python_version=python,\n    )\n\n    if not success:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/install/mcp_json.py",
    "content": "\"\"\"MCP configuration JSON generation for FastMCP install using Cyclopts.\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nimport pyperclip\nfrom rich import print\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nfrom .shared import process_common_args\n\nlogger = get_logger(__name__)\n\n\ndef install_mcp_json(\n    file: Path,\n    server_object: str | None,\n    name: str,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n    copy: bool = False,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n) -> bool:\n    \"\"\"Generate MCP configuration JSON for manual installation.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        name: Name for the server in MCP config\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables\n        copy: If True, copy to clipboard instead of printing to stdout\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n\n    Returns:\n        True if generation was successful, False otherwise\n    \"\"\"\n    try:\n        env_config = UVEnvironment(\n            python=python_version,\n            dependencies=(with_packages or []) + [\"fastmcp\"],\n            requirements=with_requirements,\n            project=project,\n            editable=with_editable,\n        )\n        # Build server spec from parsed components\n        if server_object:\n            server_spec = f\"{file.resolve()}:{server_object}\"\n        else:\n            server_spec = str(file.resolve())\n\n        # Build the full command\n        full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n\n        # Build MCP server configuration\n        server_config: dict[str, str | list[str] | dict[str, str]] = {\n            \"command\": full_command[0],\n            \"args\": full_command[1:],\n        }\n\n        # Add environment variables if provided\n        if env_vars:\n            server_config[\"env\"] = env_vars\n\n        # Wrap with server name as root key\n        config = {name: server_config}\n\n        # Convert to JSON\n        json_output = json.dumps(config, indent=2)\n\n        # Handle output\n        if copy:\n            pyperclip.copy(json_output)\n            print(f\"[green]MCP configuration for '{name}' copied to clipboard[/green]\")\n        else:\n            # Print to stdout (for piping)\n            print(json_output)\n\n        return True\n\n    except Exception as e:\n        print(f\"[red]Failed to generate MCP configuration: {e}[/red]\")\n        return False\n\n\nasync def mcp_json_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the server in MCP config\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory with pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    env_vars: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--env\",\n            help=\"Environment variables in KEY=VALUE format (can be used multiple times)\",\n        ),\n    ] = None,\n    env_file: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--env-file\",\n            help=\"Load environment variables from .env file\",\n        ),\n    ] = None,\n    copy: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--copy\",\n            help=\"Copy configuration to clipboard instead of printing to stdout\",\n        ),\n    ] = False,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Generate MCP configuration JSON for manual installation.\n\n    Args:\n        server_spec: Python file to install, optionally with :object suffix\n    \"\"\"\n    # Convert None to empty lists for list parameters\n    with_editable = with_editable or []\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n    file, server_object, name, packages, env_dict = await process_common_args(\n        server_spec, server_name, with_packages, env_vars, env_file\n    )\n\n    success = install_mcp_json(\n        file=file,\n        server_object=server_object,\n        name=name,\n        with_editable=with_editable,\n        with_packages=packages,\n        env_vars=env_dict,\n        copy=copy,\n        python_version=python,\n        with_requirements=with_requirements,\n        project=project,\n    )\n\n    if not success:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/install/shared.py",
    "content": "\"\"\"Shared utilities for install commands.\"\"\"\n\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom urllib.parse import urlparse\n\nfrom dotenv import dotenv_values\nfrom pydantic import ValidationError\nfrom rich import print\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config import MCPServerConfig\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\nlogger = get_logger(__name__)\n\n# Server names are passed as subprocess arguments to CLI tools like `claude`\n# and `gemini`. On Windows these may resolve to .cmd/.bat wrappers that run\n# through cmd.exe, where shell metacharacters (& | ; etc.) in arguments can\n# cause command injection. Restrict names to safe characters.\n_SAFE_NAME_RE = re.compile(r\"^[\\w\\-. ]+$\")\n\n\ndef validate_server_name(name: str) -> str:\n    \"\"\"Validate that a server name is safe for use as a subprocess argument.\n\n    Raises SystemExit if the name contains shell metacharacters.\n    \"\"\"\n    if not _SAFE_NAME_RE.match(name):\n        print(\n            f\"[red]Invalid server name '[bold]{name}[/bold]': \"\n            \"names may only contain letters, numbers, hyphens, underscores, dots, and spaces.[/red]\"\n        )\n        sys.exit(1)\n    return name\n\n\ndef parse_env_var(env_var: str) -> tuple[str, str]:\n    \"\"\"Parse environment variable string in format KEY=VALUE.\"\"\"\n    if \"=\" not in env_var:\n        print(\n            f\"[red]Invalid environment variable format: '[bold]{env_var}[/bold]'. Must be KEY=VALUE[/red]\"\n        )\n        sys.exit(1)\n    key, value = env_var.split(\"=\", 1)\n    return key.strip(), value.strip()\n\n\nasync def process_common_args(\n    server_spec: str,\n    server_name: str | None,\n    with_packages: list[str] | None,\n    env_vars: list[str] | None,\n    env_file: Path | None,\n) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:\n    \"\"\"Process common arguments shared by all install commands.\n\n    Handles both fastmcp.json config files and traditional file.py:object syntax.\n    \"\"\"\n    # Convert None to empty lists for list parameters\n    with_packages = with_packages or []\n    env_vars = env_vars or []\n    # Create MCPServerConfig from server_spec\n    config = None\n    config_path: Path | None = None\n    if server_spec.endswith(\".json\"):\n        config_path = Path(server_spec).resolve()\n        if not config_path.exists():\n            print(f\"[red]Configuration file not found: {config_path}[/red]\")\n            sys.exit(1)\n\n        try:\n            with open(config_path) as f:\n                data = json.load(f)\n\n            # Check if it's an MCPConfig (has mcpServers key)\n            if \"mcpServers\" in data:\n                # MCPConfig files aren't supported for install\n                print(\"[red]MCPConfig files are not supported for installation[/red]\")\n                sys.exit(1)\n            else:\n                # It's a MCPServerConfig\n                config = MCPServerConfig.from_file(config_path)\n\n                # Merge packages from config if not overridden\n                if config.environment.dependencies:\n                    # Merge with CLI packages (CLI takes precedence)\n                    config_packages = list(config.environment.dependencies)\n                    with_packages = list(set(with_packages + config_packages))\n        except (json.JSONDecodeError, ValidationError) as e:\n            print(f\"[red]Invalid configuration file: {e}[/red]\")\n            sys.exit(1)\n    else:\n        # Create config from file path\n        source = FileSystemSource(path=server_spec)\n        config = MCPServerConfig(source=source)\n\n    # Extract file and server_object from the source\n    # The FileSystemSource handles parsing path:object syntax\n    source_path = Path(config.source.path).expanduser()\n    # If loaded from a JSON config, resolve relative paths against the config's directory\n    if not source_path.is_absolute() and config_path is not None:\n        file = (config_path.parent / source_path).resolve()\n    else:\n        file = source_path.resolve()\n    # Update the source path so load_server() resolves correctly\n    config.source.path = str(file)\n    server_object = (\n        config.source.entrypoint if hasattr(config.source, \"entrypoint\") else None\n    )\n\n    logger.debug(\n        \"Installing server\",\n        extra={\n            \"file\": str(file),\n            \"server_name\": server_name,\n            \"server_object\": server_object,\n            \"with_packages\": with_packages,\n        },\n    )\n\n    # Verify the resolved file actually exists\n    if not file.is_file():\n        print(f\"[red]Server file not found: {file}[/red]\")\n        sys.exit(1)\n\n    # Try to import server to get its name and dependencies.\n    # load_server() resolves paths against cwd, which may differ from our\n    # config-relative resolution, so we catch SystemExit from its file check.\n    name = server_name\n    server = None\n    if not name:\n        try:\n            server = await config.source.load_server()\n            name = server.name\n        except (ImportError, ModuleNotFoundError, SystemExit) as e:\n            logger.debug(\n                \"Could not import server (likely missing dependencies), using file name\",\n                extra={\"error\": str(e)},\n            )\n            name = file.stem\n\n    # Process environment variables if provided\n    env_dict: dict[str, str] | None = None\n    if env_file or env_vars:\n        env_dict = {}\n        # Load from .env file if specified\n        if env_file:\n            try:\n                env_dict |= {\n                    k: v for k, v in dotenv_values(env_file).items() if v is not None\n                }\n            except Exception as e:\n                print(f\"[red]Failed to load .env file: {e}[/red]\")\n                sys.exit(1)\n\n        # Add command line environment variables\n        for env_var in env_vars:\n            key, value = parse_env_var(env_var)\n            env_dict[key] = value\n\n    return file, server_object, name, with_packages, env_dict\n\n\ndef open_deeplink(url: str, *, expected_scheme: str) -> bool:\n    \"\"\"Attempt to open a deeplink URL using the system's default handler.\n\n    Args:\n        url: The deeplink URL to open.\n        expected_scheme: The URL scheme to validate (e.g. \"cursor\", \"goose\").\n\n    Returns:\n        True if the command succeeded, False otherwise.\n    \"\"\"\n    parsed = urlparse(url)\n    if parsed.scheme != expected_scheme:\n        logger.warning(\n            f\"Invalid deeplink scheme: {parsed.scheme}, expected {expected_scheme}\"\n        )\n        return False\n\n    try:\n        if sys.platform == \"darwin\":\n            subprocess.run([\"open\", url], check=True, capture_output=True)\n        elif sys.platform == \"win32\":\n            os.startfile(url)\n        else:\n            subprocess.run([\"xdg-open\", url], check=True, capture_output=True)\n        return True\n    except (subprocess.CalledProcessError, FileNotFoundError, OSError):\n        return False\n"
  },
  {
    "path": "src/fastmcp/cli/install/stdio.py",
    "content": "\"\"\"Stdio command generation for FastMCP install using Cyclopts.\"\"\"\n\nimport builtins\nimport shlex\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport cyclopts\nimport pyperclip\nfrom rich import print as rich_print\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nfrom .shared import process_common_args\n\nlogger = get_logger(__name__)\n\n\ndef install_stdio(\n    file: Path,\n    server_object: str | None,\n    *,\n    with_editable: list[Path] | None = None,\n    with_packages: list[str] | None = None,\n    copy: bool = False,\n    python_version: str | None = None,\n    with_requirements: Path | None = None,\n    project: Path | None = None,\n) -> bool:\n    \"\"\"Generate the stdio command for running a FastMCP server.\n\n    Args:\n        file: Path to the server file\n        server_object: Optional server object name (for :object suffix)\n        with_editable: Optional list of directories to install in editable mode\n        with_packages: Optional list of additional packages to install\n        copy: If True, copy to clipboard instead of printing to stdout\n        python_version: Optional Python version to use\n        with_requirements: Optional requirements file to install from\n        project: Optional project directory to run within\n\n    Returns:\n        True if generation was successful, False otherwise\n    \"\"\"\n    try:\n        env_config = UVEnvironment(\n            python=python_version,\n            dependencies=(with_packages or []) + [\"fastmcp\"],\n            requirements=with_requirements,\n            project=project,\n            editable=with_editable,\n        )\n        # Build server spec from parsed components\n        if server_object:\n            server_spec = f\"{file.resolve()}:{server_object}\"\n        else:\n            server_spec = str(file.resolve())\n\n        # Build the full command\n        full_command = env_config.build_command([\"fastmcp\", \"run\", server_spec])\n        command_str = shlex.join(full_command)\n\n        if copy:\n            pyperclip.copy(command_str)\n            rich_print(\"[green]✓ Command copied to clipboard[/green]\")\n        else:\n            builtins.print(command_str)\n\n        return True\n\n    except (OSError, ValueError, pyperclip.PyperclipException) as e:\n        rich_print(f\"[red]Failed to generate stdio command: {e}[/red]\")\n        return False\n\n\nasync def stdio_command(\n    server_spec: str,\n    *,\n    server_name: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            name=[\"--name\", \"-n\"],\n            help=\"Custom name for the server (used for dependency resolution)\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        list[Path] | None,\n        cyclopts.Parameter(\n            \"--with-editable\",\n            help=\"Directory with pyproject.toml to install in editable mode (can be used multiple times)\",\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str] | None,\n        cyclopts.Parameter(\n            \"--with\", help=\"Additional packages to install (can be used multiple times)\"\n        ),\n    ] = None,\n    copy: Annotated[\n        bool,\n        cyclopts.Parameter(\n            \"--copy\",\n            help=\"Copy command to clipboard instead of printing to stdout\",\n        ),\n    ] = False,\n    python: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            \"--python\",\n            help=\"Python version to use (e.g., 3.10, 3.11)\",\n        ),\n    ] = None,\n    with_requirements: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--with-requirements\",\n            help=\"Requirements file to install dependencies from\",\n        ),\n    ] = None,\n    project: Annotated[\n        Path | None,\n        cyclopts.Parameter(\n            \"--project\",\n            help=\"Run the command within the given project directory\",\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Generate the stdio command for running a FastMCP server.\n\n    Outputs the shell command that an MCP host would use to start this server\n    over stdio transport. Useful for manual configuration or debugging.\n\n    Args:\n        server_spec: Python file to run, optionally with :object suffix\n    \"\"\"\n    with_editable = with_editable or []\n    with_packages = with_packages or []\n    file, server_object, _name, packages, _env_dict = await process_common_args(\n        server_spec, server_name, with_packages, [], None\n    )\n\n    success = install_stdio(\n        file=file,\n        server_object=server_object,\n        with_editable=with_editable,\n        with_packages=packages,\n        copy=copy,\n        python_version=python,\n        with_requirements=with_requirements,\n        project=project,\n    )\n\n    if not success:\n        sys.exit(1)\n"
  },
  {
    "path": "src/fastmcp/cli/run.py",
    "content": "\"\"\"FastMCP run command implementation with enhanced type hints.\"\"\"\n\nimport asyncio\nimport contextlib\nimport json\nimport os\nimport re\nimport signal\nimport subprocess\nimport sys\nfrom collections.abc import Callable\nfrom pathlib import Path\nfrom typing import Any, Literal\n\nfrom mcp.server.fastmcp import FastMCP as FastMCP1x\nfrom watchfiles import Change, awatch\n\nfrom fastmcp.server.server import FastMCP, create_proxy\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config import (\n    MCPServerConfig,\n)\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\nlogger = get_logger(\"cli.run\")\n\n# Type aliases for better type safety\nTransportType = Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"]\nLogLevelType = Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n\n# File extensions to watch for reload\nWATCHED_EXTENSIONS: set[str] = {\n    # Python\n    \".py\",\n    # JavaScript/TypeScript\n    \".js\",\n    \".ts\",\n    \".jsx\",\n    \".tsx\",\n    # Markup/Content\n    \".html\",\n    \".md\",\n    \".mdx\",\n    \".txt\",\n    \".xml\",\n    # Styles\n    \".css\",\n    \".scss\",\n    \".sass\",\n    \".less\",\n    # Data/Config\n    \".json\",\n    \".yaml\",\n    \".yml\",\n    \".toml\",\n    # Framework-specific\n    \".vue\",\n    \".svelte\",\n    # GraphQL\n    \".graphql\",\n    \".gql\",\n    # Images\n    \".svg\",\n    \".png\",\n    \".jpg\",\n    \".jpeg\",\n    \".gif\",\n    \".ico\",\n    \".webp\",\n    # Media\n    \".mp3\",\n    \".mp4\",\n    \".wav\",\n    \".webm\",\n    # Fonts\n    \".woff\",\n    \".woff2\",\n    \".ttf\",\n    \".eot\",\n}\n\n\ndef is_url(path: str) -> bool:\n    \"\"\"Check if a string is a URL.\"\"\"\n    url_pattern = re.compile(r\"^https?://\")\n    return bool(url_pattern.match(path))\n\n\ndef create_client_server(url: str) -> Any:\n    \"\"\"Create a FastMCP server from a client URL.\n\n    Args:\n        url: The URL to connect to\n\n    Returns:\n        A FastMCP server instance\n    \"\"\"\n    try:\n        import fastmcp\n\n        client = fastmcp.Client(url)\n        server = create_proxy(client)\n        return server\n    except Exception as e:\n        logger.error(f\"Failed to create client for URL {url}: {e}\")\n        sys.exit(1)\n\n\ndef create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:\n    \"\"\"Create a FastMCP server from a MCPConfig.\"\"\"\n    with mcp_config_path.open() as src:\n        mcp_config = json.load(src)\n\n    server = create_proxy(mcp_config)\n    return server\n\n\ndef load_mcp_server_config(config_path: Path) -> MCPServerConfig:\n    \"\"\"Load a FastMCP configuration from a fastmcp.json file.\n\n    Args:\n        config_path: Path to fastmcp.json file\n\n    Returns:\n        MCPServerConfig object\n    \"\"\"\n    config = MCPServerConfig.from_file(config_path)\n\n    # Apply runtime settings from deployment config\n    config.deployment.apply_runtime_settings(config_path)\n\n    return config\n\n\nasync def run_command(\n    server_spec: str,\n    transport: TransportType | None = None,\n    host: str | None = None,\n    port: int | None = None,\n    path: str | None = None,\n    log_level: LogLevelType | None = None,\n    server_args: list[str] | None = None,\n    show_banner: bool = True,\n    use_direct_import: bool = False,\n    skip_source: bool = False,\n    stateless: bool = False,\n) -> None:\n    \"\"\"Run a MCP server or connect to a remote one.\n\n    Args:\n        server_spec: Python file, object specification (file:obj), config file, or URL\n        transport: Transport protocol to use\n        host: Host to bind to when using http transport\n        port: Port to bind to when using http transport\n        path: Path to bind to when using http transport\n        log_level: Log level\n        server_args: Additional arguments to pass to the server\n        show_banner: Whether to show the server banner\n        use_direct_import: Whether to use direct import instead of subprocess\n        skip_source: Whether to skip source preparation step\n        stateless: Whether to run in stateless mode (no session)\n    \"\"\"\n    # Special case: URLs\n    if is_url(server_spec):\n        # Handle URL case\n        server = create_client_server(server_spec)\n        logger.debug(f\"Created client proxy server for {server_spec}\")\n    # Special case: MCPConfig files (legacy)\n    elif server_spec.endswith(\".json\"):\n        # Load JSON and check which type of config it is\n        config_path = Path(server_spec)\n        with open(config_path) as f:\n            data = json.load(f)\n\n        # Check if it's an MCPConfig first (has canonical mcpServers key)\n        if \"mcpServers\" in data:\n            # It's an MCP config\n            server = create_mcp_config_server(config_path)\n        else:\n            # It's a FastMCP config - load it properly\n            config = load_mcp_server_config(config_path)\n\n            # Merge deployment config with CLI arguments (CLI takes precedence)\n            transport = transport or config.deployment.transport\n            host = host or config.deployment.host\n            port = port or config.deployment.port\n            path = path or config.deployment.path\n            log_level = log_level or config.deployment.log_level\n            server_args = (\n                server_args if server_args is not None else config.deployment.args\n            )\n\n            # Prepare source only (environment is handled by uv run)\n            await config.prepare_source() if not skip_source else None\n\n            # Load the server using the source\n            from contextlib import nullcontext\n\n            from fastmcp.cli.cli import with_argv\n\n            # Use sys.argv context manager if deployment args specified\n            argv_context = with_argv(server_args) if server_args else nullcontext()\n\n            with argv_context:\n                server = await config.source.load_server()\n\n            logger.debug(f'Found server \"{server.name}\" from config {config_path}')\n    else:\n        # Regular file case - create a MCPServerConfig with FileSystemSource\n        source = FileSystemSource(path=server_spec)\n        config = MCPServerConfig(source=source)\n\n        # Prepare source only (environment is handled by uv run)\n        await config.prepare_source() if not skip_source else None\n\n        # Load the server\n        from contextlib import nullcontext\n\n        from fastmcp.cli.cli import with_argv\n\n        # Use sys.argv context manager if server_args specified\n        argv_context = with_argv(server_args) if server_args else nullcontext()\n\n        with argv_context:\n            server = await config.source.load_server()\n\n        logger.debug(f'Found server \"{server.name}\" in {source.path}')\n\n    # Run the server\n\n    # handle v1 servers\n    if isinstance(server, FastMCP1x):\n        await run_v1_server_async(server, host=host, port=port, transport=transport)\n        return\n\n    kwargs = {}\n    if transport:\n        kwargs[\"transport\"] = transport\n    if host:\n        kwargs[\"host\"] = host\n    if port:\n        kwargs[\"port\"] = port\n    if path:\n        kwargs[\"path\"] = path\n    if log_level:\n        kwargs[\"log_level\"] = log_level\n    if stateless:\n        kwargs[\"stateless\"] = True\n\n    if not show_banner:\n        kwargs[\"show_banner\"] = False\n\n    try:\n        await server.run_async(**kwargs)\n    except Exception as e:\n        logger.error(f\"Failed to run server: {e}\")\n        sys.exit(1)\n\n\ndef run_module_command(\n    module_name: str,\n    *,\n    env_command_builder: Callable[[list[str]], list[str]] | None = None,\n    extra_args: list[str] | None = None,\n) -> None:\n    \"\"\"Run a Python module directly using ``python -m <module>``.\n\n    When ``-m`` is used, the module manages its own server startup.\n    No server-object discovery or transport overrides are applied.\n\n    Args:\n        module_name: Dotted module name (e.g. ``my_package``).\n        env_command_builder: An optional callable that wraps a command list\n            with environment setup (e.g. ``UVEnvironment.build_command``).\n        extra_args: Extra arguments forwarded after the module name.\n    \"\"\"\n    # Use bare \"python\" when an env wrapper (e.g. uv run) is active so that\n    # the wrapper can resolve the interpreter via --python / environment config.\n    # Fall back to sys.executable for direct execution without a wrapper.\n    python = \"python\" if env_command_builder is not None else sys.executable\n    cmd: list[str] = [python, \"-m\", module_name]\n    if extra_args:\n        cmd.extend(extra_args)\n\n    # Wrap with environment (e.g. uv run) if configured\n    if env_command_builder is not None:\n        cmd = env_command_builder(cmd)\n\n    logger.debug(f\"Running module: {' '.join(cmd)}\")\n\n    try:\n        process = subprocess.run(cmd, check=True)\n        sys.exit(process.returncode)\n    except subprocess.CalledProcessError as e:\n        logger.error(f\"Module {module_name} exited with code {e.returncode}\")\n        sys.exit(e.returncode)\n\n\nasync def run_v1_server_async(\n    server: FastMCP1x,\n    host: str | None = None,\n    port: int | None = None,\n    transport: TransportType | None = None,\n) -> None:\n    \"\"\"Run a FastMCP 1.x server using async methods.\n\n    Args:\n        server: FastMCP 1.x server instance\n        host: Host to bind to\n        port: Port to bind to\n        transport: Transport protocol to use\n    \"\"\"\n    if host:\n        server.settings.host = host\n    if port:\n        server.settings.port = port\n\n    match transport:\n        case \"stdio\":\n            await server.run_stdio_async()\n        case \"http\" | \"streamable-http\" | None:\n            await server.run_streamable_http_async()\n        case \"sse\":\n            await server.run_sse_async()\n\n\ndef _watch_filter(_change: Change, path: str) -> bool:\n    \"\"\"Filter for files that should trigger reload.\"\"\"\n    return any(path.endswith(ext) for ext in WATCHED_EXTENSIONS)\n\n\nasync def _terminate_process(process: asyncio.subprocess.Process) -> None:\n    \"\"\"Terminate a subprocess and all its children.\n\n    Sends SIGTERM to the process group first for graceful shutdown,\n    then falls back to SIGKILL if the process doesn't exit in time.\n    \"\"\"\n    if process.returncode is not None:\n        return\n\n    pid = process.pid\n\n    if sys.platform != \"win32\":\n        # Send SIGTERM to the entire process group for graceful shutdown\n        with contextlib.suppress(ProcessLookupError, OSError):\n            os.killpg(os.getpgid(pid), signal.SIGTERM)\n\n        # Wait briefly for graceful exit\n        try:\n            await asyncio.wait_for(process.wait(), timeout=3.0)\n            return\n        except asyncio.TimeoutError:\n            pass\n\n        # Force kill the entire process group\n        with contextlib.suppress(ProcessLookupError, OSError):\n            os.killpg(os.getpgid(pid), signal.SIGKILL)\n    else:\n        process.kill()\n\n    await process.wait()\n\n\nasync def run_with_reload(\n    cmd: list[str],\n    reload_dirs: list[Path] | None = None,\n    is_stdio: bool = False,\n) -> None:\n    \"\"\"Run a command with file watching and auto-reload.\n\n    Args:\n        cmd: Command to run as subprocess (should include --no-reload)\n        reload_dirs: Directories to watch for changes (default: cwd)\n        is_stdio: Whether this is stdio transport\n    \"\"\"\n    watch_paths = reload_dirs or [Path.cwd()]\n    process: asyncio.subprocess.Process | None = None\n    first_run = True\n\n    if is_stdio:\n        logger.info(\"Reload mode enabled (using stateless sessions)\")\n    else:\n        logger.info(\n            \"Reload mode enabled (using stateless HTTP). \"\n            \"Some features requiring bidirectional communication \"\n            \"(like elicitation) are not available.\"\n        )\n\n    # Handle SIGTERM/SIGINT gracefully with proper asyncio integration\n    shutdown_event = asyncio.Event()\n    loop = asyncio.get_running_loop()\n\n    def signal_handler() -> None:\n        logger.info(\"Received shutdown signal, stopping...\")\n        shutdown_event.set()\n\n    # Windows doesn't support add_signal_handler\n    if sys.platform != \"win32\":\n        loop.add_signal_handler(signal.SIGTERM, signal_handler)\n        loop.add_signal_handler(signal.SIGINT, signal_handler)\n\n    try:\n        while not shutdown_event.is_set():\n            # Build command - add --no-banner on restarts to reduce noise\n            if first_run or \"--no-banner\" in cmd:\n                run_cmd = cmd\n            else:\n                run_cmd = [*cmd, \"--no-banner\"]\n            first_run = False\n\n            process = await asyncio.create_subprocess_exec(\n                *run_cmd,\n                stdin=None,\n                stdout=None,\n                stderr=None,\n                # Own process group so _terminate_process can kill the whole tree\n                start_new_session=sys.platform != \"win32\",\n            )\n\n            # Watch for either: file changes OR process death\n            watch_task = asyncio.create_task(\n                anext(aiter(awatch(*watch_paths, watch_filter=_watch_filter)))  # ty: ignore[invalid-argument-type]\n            )\n            wait_task = asyncio.create_task(process.wait())\n            shutdown_task = asyncio.create_task(shutdown_event.wait())\n\n            done, pending = await asyncio.wait(\n                [watch_task, wait_task, shutdown_task],\n                return_when=asyncio.FIRST_COMPLETED,\n            )\n\n            for task in pending:\n                task.cancel()\n                with contextlib.suppress(asyncio.CancelledError):\n                    await task\n\n            if shutdown_task in done:\n                # User requested shutdown\n                break\n\n            if wait_task in done:\n                # Server died on its own - wait for file change before restart\n                code = wait_task.result()\n                if code != 0:\n                    logger.error(\n                        f\"Server exited with code {code}, waiting for file change...\"\n                    )\n                else:\n                    logger.info(\"Server exited, waiting for file change...\")\n\n                # Wait for file change or shutdown (avoid hot loop on crash)\n                watch_task = asyncio.create_task(\n                    anext(aiter(awatch(*watch_paths, watch_filter=_watch_filter)))  # ty: ignore[invalid-argument-type]\n                )\n                shutdown_task = asyncio.create_task(shutdown_event.wait())\n                done, pending = await asyncio.wait(\n                    [watch_task, shutdown_task],\n                    return_when=asyncio.FIRST_COMPLETED,\n                )\n                for task in pending:\n                    task.cancel()\n                    with contextlib.suppress(asyncio.CancelledError):\n                        await task\n                if shutdown_task in done:\n                    break\n                logger.info(\"Detected changes, restarting...\")\n            else:\n                # File changed - restart server\n                changes = watch_task.result()\n                logger.info(\n                    f\"Detected changes in {len(changes)} file(s), restarting...\"\n                )\n                await _terminate_process(process)\n\n    except KeyboardInterrupt:\n        # Handle Ctrl+C on Windows (where add_signal_handler isn't available)\n        logger.info(\"Received shutdown signal, stopping...\")\n\n    finally:\n        # Clean up signal handlers\n        if sys.platform != \"win32\":\n            loop.remove_signal_handler(signal.SIGTERM)\n            loop.remove_signal_handler(signal.SIGINT)\n        if process and process.returncode is None:\n            await _terminate_process(process)\n"
  },
  {
    "path": "src/fastmcp/cli/tasks.py",
    "content": "\"\"\"FastMCP tasks CLI for Docket task management.\"\"\"\n\nimport asyncio\nimport sys\nfrom typing import Annotated\n\nimport cyclopts\nfrom rich.console import Console\n\nfrom fastmcp.utilities.cli import load_and_merge_config\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(\"cli.tasks\")\nconsole = Console()\n\ntasks_app = cyclopts.App(\n    name=\"tasks\",\n    help=\"Manage FastMCP background tasks using Docket\",\n)\n\n\ndef check_distributed_backend() -> None:\n    \"\"\"Check if Docket is configured with a distributed backend.\n\n    The CLI worker runs as a separate process, so it needs Redis/Valkey\n    to coordinate with the main server process.\n\n    Raises:\n        SystemExit: If using memory:// URL\n    \"\"\"\n    import fastmcp\n\n    docket_url = fastmcp.settings.docket.url\n\n    # Check for memory:// URL and provide helpful error\n    if docket_url.startswith(\"memory://\"):\n        console.print(\n            \"[bold red]✗ In-memory backend not supported by CLI[/bold red]\\n\\n\"\n            \"Your Docket configuration uses an in-memory backend (memory://) which\\n\"\n            \"only works within a single process.\\n\\n\"\n            \"To use [cyan]fastmcp tasks[/cyan] CLI commands (which run in separate\\n\"\n            \"processes), you need a distributed backend:\\n\\n\"\n            \"[bold]1. Install Redis or Valkey:[/bold]\\n\"\n            \"   [dim]macOS:[/dim]     brew install redis\\n\"\n            \"   [dim]Ubuntu:[/dim]    apt install redis-server\\n\"\n            \"   [dim]Valkey:[/dim]    See https://valkey.io/\\n\\n\"\n            \"[bold]2. Start the service:[/bold]\\n\"\n            \"   redis-server\\n\\n\"\n            \"[bold]3. Configure Docket URL:[/bold]\\n\"\n            \"   [dim]Environment variable:[/dim]\\n\"\n            \"   export FASTMCP_DOCKET_URL=redis://localhost:6379/0\\n\\n\"\n            \"[bold]4. Try again[/bold]\\n\\n\"\n            \"The memory backend works great for single-process servers, but the CLI\\n\"\n            \"commands need a distributed backend to coordinate across processes.\\n\\n\"\n            \"Need help? See: [cyan]https://gofastmcp.com/docs/tasks[/cyan]\"\n        )\n        sys.exit(1)\n\n\n@tasks_app.command\ndef worker(\n    server_spec: Annotated[\n        str | None,\n        cyclopts.Parameter(\n            help=\"Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json\"\n        ),\n    ] = None,\n) -> None:\n    \"\"\"Start an additional worker to process background tasks.\n\n    Connects to your Docket backend and processes tasks in parallel with\n    any other running workers. Configure via environment variables\n    (FASTMCP_DOCKET_*).\n\n    Example:\n        fastmcp tasks worker server.py\n        fastmcp tasks worker examples/tasks/server.py\n    \"\"\"\n    import fastmcp\n\n    check_distributed_backend()\n\n    # Load server to get task functions\n    try:\n        config, _resolved_spec = load_and_merge_config(server_spec)\n    except FileNotFoundError:\n        sys.exit(1)\n\n    # Load the server\n    server = asyncio.run(config.source.load_server())\n\n    async def run_worker():\n        \"\"\"Enter server lifespan and camp forever.\"\"\"\n        async with server._lifespan_manager():\n            console.print(\n                f\"[bold green]✓[/bold green] Starting worker for [cyan]{server.name}[/cyan]\"\n            )\n            console.print(f\"  Docket: {fastmcp.settings.docket.name}\")\n            console.print(f\"  Backend: {fastmcp.settings.docket.url}\")\n            console.print(f\"  Concurrency: {fastmcp.settings.docket.concurrency}\")\n\n            # Server's lifespan has started its worker - just camp here forever\n            while True:\n                await asyncio.sleep(3600)\n\n    try:\n        asyncio.run(run_worker())\n    except KeyboardInterrupt:\n        console.print(\"\\n[yellow]Worker stopped[/yellow]\")\n        sys.exit(0)\n"
  },
  {
    "path": "src/fastmcp/client/__init__.py",
    "content": "from .auth import OAuth, BearerAuth\nfrom .client import Client\nfrom .transports import (\n    ClientTransport,\n    FastMCPTransport,\n    NodeStdioTransport,\n    NpxStdioTransport,\n    PythonStdioTransport,\n    SSETransport,\n    StdioTransport,\n    StreamableHttpTransport,\n    UvStdioTransport,\n    UvxStdioTransport,\n)\n\n__all__ = [\n    \"BearerAuth\",\n    \"Client\",\n    \"ClientTransport\",\n    \"FastMCPTransport\",\n    \"NodeStdioTransport\",\n    \"NpxStdioTransport\",\n    \"OAuth\",\n    \"PythonStdioTransport\",\n    \"SSETransport\",\n    \"StdioTransport\",\n    \"StreamableHttpTransport\",\n    \"UvStdioTransport\",\n    \"UvxStdioTransport\",\n]\n"
  },
  {
    "path": "src/fastmcp/client/auth/__init__.py",
    "content": "from .bearer import BearerAuth\nfrom .oauth import OAuth\n\n__all__ = [\"BearerAuth\", \"OAuth\"]\n"
  },
  {
    "path": "src/fastmcp/client/auth/bearer.py",
    "content": "import httpx\nfrom pydantic import SecretStr\n\nfrom fastmcp.utilities.logging import get_logger\n\n__all__ = [\"BearerAuth\"]\n\nlogger = get_logger(__name__)\n\n\nclass BearerAuth(httpx.Auth):\n    def __init__(self, token: str):\n        self.token = SecretStr(token)\n\n    def auth_flow(self, request):\n        request.headers[\"Authorization\"] = f\"Bearer {self.token.get_secret_value()}\"\n        yield request\n"
  },
  {
    "path": "src/fastmcp/client/auth/oauth.py",
    "content": "from __future__ import annotations\n\nimport time\nimport webbrowser\nfrom collections.abc import AsyncGenerator\nfrom contextlib import aclosing\nfrom typing import Any\n\nimport anyio\nimport httpx\nfrom key_value.aio.adapters.pydantic import PydanticAdapter\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.client.auth import OAuthClientProvider, TokenStorage\nfrom mcp.shared._httpx_utils import McpHttpClientFactory\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthClientMetadata,\n    OAuthToken,\n)\nfrom pydantic import AnyHttpUrl\nfrom typing_extensions import override\nfrom uvicorn.server import Server\n\nfrom fastmcp.client.oauth_callback import (\n    OAuthCallbackResult,\n    create_oauth_callback_server,\n)\nfrom fastmcp.utilities.http import find_available_port\nfrom fastmcp.utilities.logging import get_logger\n\n__all__ = [\"OAuth\"]\n\nlogger = get_logger(__name__)\n\n\nclass ClientNotFoundError(Exception):\n    \"\"\"Raised when OAuth client credentials are not found on the server.\"\"\"\n\n\nasync def check_if_auth_required(\n    mcp_url: str, httpx_kwargs: dict[str, Any] | None = None\n) -> bool:\n    \"\"\"\n    Check if the MCP endpoint requires authentication by making a test request.\n\n    Returns:\n        True if auth appears to be required, False otherwise\n    \"\"\"\n    async with httpx.AsyncClient(**(httpx_kwargs or {})) as client:\n        try:\n            # Try a simple request to the endpoint\n            response = await client.get(mcp_url, timeout=5.0)\n\n            # If we get 401/403, auth is likely required\n            if response.status_code in (401, 403):\n                return True\n\n            # Check for WWW-Authenticate header\n            if \"WWW-Authenticate\" in response.headers:  # noqa: SIM103\n                return True\n\n            # If we get a successful response, auth may not be required\n            return False\n\n        except httpx.RequestError:\n            # If we can't connect, assume auth might be required\n            return True\n\n\nclass TokenStorageAdapter(TokenStorage):\n    _server_url: str\n    _key_value_store: AsyncKeyValue\n    _storage_oauth_token: PydanticAdapter[OAuthToken]\n    _storage_client_info: PydanticAdapter[OAuthClientInformationFull]\n\n    def __init__(self, async_key_value: AsyncKeyValue, server_url: str):\n        self._server_url = server_url\n        self._key_value_store = async_key_value\n        self._storage_oauth_token = PydanticAdapter[OAuthToken](\n            default_collection=\"mcp-oauth-token\",\n            key_value=async_key_value,\n            pydantic_model=OAuthToken,\n            raise_on_validation_error=True,\n        )\n        self._storage_client_info = PydanticAdapter[OAuthClientInformationFull](\n            default_collection=\"mcp-oauth-client-info\",\n            key_value=async_key_value,\n            pydantic_model=OAuthClientInformationFull,\n            raise_on_validation_error=True,\n        )\n\n    def _get_token_cache_key(self) -> str:\n        return f\"{self._server_url}/tokens\"\n\n    def _get_client_info_cache_key(self) -> str:\n        return f\"{self._server_url}/client_info\"\n\n    async def clear(self) -> None:\n        await self._storage_oauth_token.delete(key=self._get_token_cache_key())\n        await self._storage_client_info.delete(key=self._get_client_info_cache_key())\n\n    @override\n    async def get_tokens(self) -> OAuthToken | None:\n        return await self._storage_oauth_token.get(key=self._get_token_cache_key())\n\n    @override\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        # Don't set TTL based on access token expiry - the refresh token may be\n        # valid much longer. Use 1 year as a reasonable upper bound; the OAuth\n        # provider handles actual token expiry/refresh logic.\n        await self._storage_oauth_token.put(\n            key=self._get_token_cache_key(),\n            value=tokens,\n            ttl=60 * 60 * 24 * 365,  # 1 year\n        )\n\n    @override\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        return await self._storage_client_info.get(\n            key=self._get_client_info_cache_key()\n        )\n\n    @override\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        ttl: int | None = None\n\n        if client_info.client_secret_expires_at:\n            ttl = client_info.client_secret_expires_at - int(time.time())\n\n        await self._storage_client_info.put(\n            key=self._get_client_info_cache_key(),\n            value=client_info,\n            ttl=ttl,\n        )\n\n\nclass OAuth(OAuthClientProvider):\n    \"\"\"\n    OAuth client provider for MCP servers with browser-based authentication.\n\n    This class provides OAuth authentication for FastMCP clients by opening\n    a browser for user authorization and running a local callback server.\n    \"\"\"\n\n    _bound: bool\n\n    def __init__(\n        self,\n        mcp_url: str | None = None,\n        scopes: str | list[str] | None = None,\n        client_name: str = \"FastMCP Client\",\n        token_storage: AsyncKeyValue | None = None,\n        additional_client_metadata: dict[str, Any] | None = None,\n        callback_port: int | None = None,\n        httpx_client_factory: McpHttpClientFactory | None = None,\n        # Alternative to dynamic client registration:\n        # --- Clients host a static JSON document at an HTTPS URL ---\n        client_metadata_url: str | None = None,\n        # --- OR clients provide full client information ---\n        client_id: str | None = None,\n        client_secret: str | None = None,\n    ):\n        \"\"\"\n        Initialize OAuth client provider for an MCP server.\n\n        Args:\n            mcp_url: Full URL to the MCP endpoint (e.g. \"http://host/mcp/sse/\").\n                Optional when OAuth is passed to Client(auth=...), which provides\n                the URL automatically from the transport.\n            scopes: OAuth scopes to request. Can be a\n            space-separated string or a list of strings.\n            client_name: Name for this client during registration\n            token_storage: An AsyncKeyValue-compatible token store, tokens are stored in memory if not provided\n            additional_client_metadata: Extra fields for OAuthClientMetadata\n            callback_port: Fixed port for OAuth callback (default: random available port)\n            client_metadata_url: A CIMD (Client ID Metadata Document) URL. When\n                provided, this URL is used as the client_id instead of performing\n                Dynamic Client Registration. Must be an HTTPS URL with a non-root\n                path (e.g. \"https://myapp.example.com/oauth/client.json\").\n            client_id: Pre-registered OAuth client ID. When provided, skips dynamic\n                client registration and uses these static credentials instead.\n            client_secret: OAuth client secret (optional, used with client_id)\n        \"\"\"\n        # Store config for deferred binding if mcp_url not yet known\n        self._scopes = scopes\n        self._client_name = client_name\n        self._token_storage = token_storage\n        self._additional_client_metadata = additional_client_metadata\n        self._callback_port = callback_port\n        self._client_metadata_url = client_metadata_url\n        self._client_id = client_id\n        self._client_secret = client_secret\n        self._static_client_info = None\n        self.httpx_client_factory = httpx_client_factory or httpx.AsyncClient\n        self._bound = False\n\n        if mcp_url is not None:\n            self._bind(mcp_url)\n\n    def _bind(self, mcp_url: str) -> None:\n        \"\"\"Bind this OAuth provider to a specific MCP server URL.\n\n        Called automatically when mcp_url is provided to __init__, or by the\n        transport when OAuth is used without an explicit URL.\n        \"\"\"\n        if self._bound:\n            return\n\n        mcp_url = mcp_url.rstrip(\"/\")\n\n        self.redirect_port = self._callback_port or find_available_port()\n        redirect_uri = f\"http://localhost:{self.redirect_port}/callback\"\n\n        scopes_str: str\n        if isinstance(self._scopes, list):\n            scopes_str = \" \".join(self._scopes)\n        elif self._scopes is not None:\n            scopes_str = str(self._scopes)\n        else:\n            scopes_str = \"\"\n\n        client_metadata = OAuthClientMetadata(\n            client_name=self._client_name,\n            redirect_uris=[AnyHttpUrl(redirect_uri)],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            scope=scopes_str,\n            **(self._additional_client_metadata or {}),\n        )\n\n        if self._client_id:\n            # Create the full static client info directly which will avoid DCR.\n            # Spread client_metadata so redirect_uris, grant_types, response_types,\n            # scope, etc. are included — servers may validate these fields.\n            metadata = client_metadata.model_dump(exclude_none=True)\n            # Default token_endpoint_auth_method based on whether a secret is\n            # provided, unless the caller already set it via additional_client_metadata.\n            if \"token_endpoint_auth_method\" not in metadata:\n                metadata[\"token_endpoint_auth_method\"] = (\n                    \"client_secret_post\" if self._client_secret else \"none\"\n                )\n            self._static_client_info = OAuthClientInformationFull(\n                client_id=self._client_id,\n                client_secret=self._client_secret,\n                **metadata,\n            )\n\n        token_storage = self._token_storage or MemoryStore()\n\n        if isinstance(token_storage, MemoryStore):\n            from warnings import warn\n\n            warn(\n                message=\"Using in-memory token storage -- tokens will be lost when the client restarts. \"\n                + \"For persistent storage across multiple MCP servers, provide an encrypted AsyncKeyValue backend. \"\n                + \"See https://gofastmcp.com/clients/auth/oauth#token-storage for details.\",\n                stacklevel=2,\n            )\n\n        # Use full URL for token storage to properly separate tokens per MCP endpoint\n        self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(\n            async_key_value=token_storage, server_url=mcp_url\n        )\n\n        self.mcp_url = mcp_url\n\n        super().__init__(\n            server_url=mcp_url,\n            client_metadata=client_metadata,\n            storage=self.token_storage_adapter,\n            redirect_handler=self.redirect_handler,\n            callback_handler=self.callback_handler,\n            client_metadata_url=self._client_metadata_url,\n        )\n\n        self._bound = True\n\n    async def _initialize(self) -> None:\n        \"\"\"Load stored tokens and client info, properly setting token expiry.\"\"\"\n        await super()._initialize()\n\n        if self._static_client_info is not None:\n            self.context.client_info = self._static_client_info\n            await self.token_storage_adapter.set_client_info(self._static_client_info)\n\n        if self.context.current_tokens and self.context.current_tokens.expires_in:\n            self.context.update_token_expiry(self.context.current_tokens)\n\n    async def redirect_handler(self, authorization_url: str) -> None:\n        \"\"\"Open browser for authorization, with pre-flight check for invalid client.\"\"\"\n        # Pre-flight check to detect invalid client_id before opening browser\n        async with self.httpx_client_factory() as client:\n            response = await client.get(authorization_url, follow_redirects=False)\n\n            # Check for client not found error (400 typically means bad client_id)\n            if response.status_code == 400:\n                raise ClientNotFoundError(\n                    \"OAuth client not found - cached credentials may be stale\"\n                )\n\n            # OAuth typically returns redirects, but some providers return 200 with HTML login pages\n            if response.status_code not in (200, 302, 303, 307, 308):\n                raise RuntimeError(\n                    f\"Unexpected authorization response: {response.status_code}\"\n                )\n\n        logger.info(f\"OAuth authorization URL: {authorization_url}\")\n        webbrowser.open(authorization_url)\n\n    async def callback_handler(self) -> tuple[str, str | None]:\n        \"\"\"Handle OAuth callback and return (auth_code, state).\"\"\"\n        # Create result container and event to capture the OAuth response\n        result = OAuthCallbackResult()\n        result_ready = anyio.Event()\n\n        # Create server with result tracking\n        server: Server = create_oauth_callback_server(\n            port=self.redirect_port,\n            server_url=self.mcp_url,\n            result_container=result,\n            result_ready=result_ready,\n        )\n\n        # Run server until response is received with timeout logic\n        async with anyio.create_task_group() as tg:\n            tg.start_soon(server.serve)\n            logger.info(\n                f\"🎧 OAuth callback server started on http://localhost:{self.redirect_port}\"\n            )\n\n            TIMEOUT = 300.0  # 5 minute timeout\n            try:\n                with anyio.fail_after(TIMEOUT):\n                    await result_ready.wait()\n                    if result.error:\n                        raise result.error\n                    return result.code, result.state  # type: ignore\n            except TimeoutError as e:\n                raise TimeoutError(\n                    f\"OAuth callback timed out after {TIMEOUT} seconds\"\n                ) from e\n            finally:\n                server.should_exit = True\n                await anyio.sleep(0.1)  # Allow server to shut down gracefully\n                tg.cancel_scope.cancel()\n\n        raise RuntimeError(\"OAuth callback handler could not be started\")\n\n    async def async_auth_flow(\n        self, request: httpx.Request\n    ) -> AsyncGenerator[httpx.Request, httpx.Response]:\n        \"\"\"HTTPX auth flow with automatic retry on stale cached credentials.\n\n        If the OAuth flow fails due to invalid/stale client credentials,\n        clears the cache and retries once with fresh registration.\n        \"\"\"\n        if not self._bound:\n            raise RuntimeError(\n                \"OAuth provider has no server URL. Either pass mcp_url to OAuth() \"\n                \"or use it with Client(auth=...) which provides the URL automatically.\"\n            )\n        try:\n            # First attempt with potentially cached credentials\n            async with aclosing(super().async_auth_flow(request)) as gen:\n                response = None\n                while True:\n                    try:\n                        # First iteration sends None, subsequent iterations send response\n                        yielded_request = await gen.asend(response)  # ty: ignore[invalid-argument-type]\n                        response = yield yielded_request\n                    except StopAsyncIteration:\n                        break\n\n        except ClientNotFoundError:\n            # Static credentials are fixed — retrying won't help. Surface the\n            # error so the user can correct their client_id / client_secret.\n            if self._static_client_info is not None:\n                raise ClientNotFoundError(\n                    \"OAuth server rejected the static client credentials. \"\n                    \"Verify that the client_id (and client_secret, if provided) \"\n                    \"are correct and that the client is registered with the server.\"\n                ) from None\n\n            logger.debug(\n                \"OAuth client not found on server, clearing cache and retrying...\"\n            )\n            # Clear cached state and retry once\n            self._initialized = False\n            await self.token_storage_adapter.clear()\n\n            # Retry with fresh registration\n            async with aclosing(super().async_auth_flow(request)) as gen:\n                response = None\n                while True:\n                    try:\n                        yielded_request = await gen.asend(response)  # ty: ignore[invalid-argument-type]\n                        response = yield yielded_request\n                    except StopAsyncIteration:\n                        break\n"
  },
  {
    "path": "src/fastmcp/client/client.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport copy\nimport datetime\nimport secrets\nimport ssl\nimport weakref\nfrom collections.abc import Coroutine\nfrom contextlib import AsyncExitStack, asynccontextmanager, suppress\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any, Generic, Literal, TypeVar, cast, overload\n\nimport anyio\nimport httpx\nimport mcp.types\nfrom exceptiongroup import catch\nfrom mcp import ClientSession, McpError\nfrom mcp.types import GetTaskResult, TaskStatusNotification\nfrom pydantic import AnyUrl\n\nimport fastmcp\nfrom fastmcp.client.auth.oauth import OAuth\nfrom fastmcp.client.elicitation import ElicitationHandler, create_elicitation_callback\nfrom fastmcp.client.logging import (\n    LogHandler,\n    create_log_callback,\n    default_log_handler,\n)\nfrom fastmcp.client.messages import MessageHandler, MessageHandlerT\nfrom fastmcp.client.mixins import (\n    ClientPromptsMixin,\n    ClientResourcesMixin,\n    ClientTaskManagementMixin,\n    ClientToolsMixin,\n)\nfrom fastmcp.client.progress import ProgressHandler, default_progress_handler\nfrom fastmcp.client.roots import (\n    RootsHandler,\n    RootsList,\n    create_roots_callback,\n)\nfrom fastmcp.client.sampling import (\n    SamplingHandler,\n    create_sampling_callback,\n)\nfrom fastmcp.client.tasks import (\n    PromptTask,\n    ResourceTask,\n    TaskNotificationHandler,\n    ToolTask,\n)\nfrom fastmcp.mcp_config import MCPConfig\nfrom fastmcp.server import FastMCP\nfrom fastmcp.utilities.exceptions import get_catch_handlers\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.timeout import (\n    normalize_timeout_to_seconds,\n    normalize_timeout_to_timedelta,\n)\n\nfrom .transports import (\n    ClientTransport,\n    ClientTransportT,\n    FastMCP1Server,\n    FastMCPTransport,\n    MCPConfigTransport,\n    NodeStdioTransport,\n    PythonStdioTransport,\n    SessionKwargs,\n    SSETransport,\n    StdioTransport,\n    StreamableHttpTransport,\n    infer_transport,\n)\n\n__all__ = [\n    \"Client\",\n    \"ElicitationHandler\",\n    \"LogHandler\",\n    \"MessageHandler\",\n    \"ProgressHandler\",\n    \"RootsHandler\",\n    \"RootsList\",\n    \"SamplingHandler\",\n    \"SessionKwargs\",\n]\n\nlogger = get_logger(__name__)\n\nT = TypeVar(\"T\", bound=\"ClientTransport\")\nResultT = TypeVar(\"ResultT\")\n\n\n@dataclass\nclass ClientSessionState:\n    \"\"\"Holds all session-related state for a Client instance.\n\n    This allows clean separation of configuration (which is copied) from\n    session state (which should be fresh for each new client instance).\n    \"\"\"\n\n    session: ClientSession | None = None\n    nesting_counter: int = 0\n    lock: anyio.Lock = field(default_factory=anyio.Lock)\n    session_task: asyncio.Task | None = None\n    ready_event: anyio.Event = field(default_factory=anyio.Event)\n    stop_event: anyio.Event = field(default_factory=anyio.Event)\n    initialize_result: mcp.types.InitializeResult | None = None\n\n\n@dataclass\nclass CallToolResult:\n    \"\"\"Parsed result from a tool call.\"\"\"\n\n    content: list[mcp.types.ContentBlock]\n    structured_content: dict[str, Any] | None\n    meta: dict[str, Any] | None\n    data: Any = None\n    is_error: bool = False\n\n\nclass Client(\n    Generic[ClientTransportT],\n    ClientResourcesMixin,\n    ClientPromptsMixin,\n    ClientToolsMixin,\n    ClientTaskManagementMixin,\n):\n    \"\"\"\n    MCP client that delegates connection management to a Transport instance.\n\n    The Client class is responsible for MCP protocol logic, while the Transport\n    handles connection establishment and management. Client provides methods for\n    working with resources, prompts, tools and other MCP capabilities.\n\n    This client supports reentrant context managers (multiple concurrent\n    `async with client:` blocks) using reference counting and background session\n    management. This allows efficient session reuse in any scenario with\n    nested or concurrent client usage.\n\n    MCP SDK 1.10 introduced automatic list_tools() calls during call_tool()\n    execution. This created a race condition where events could be reset while\n    other tasks were waiting on them, causing deadlocks. The issue was exposed\n    in proxy scenarios but affects any reentrant usage.\n\n    The solution uses reference counting to track active context managers,\n    a background task to manage the session lifecycle, events to coordinate\n    between tasks, and ensures all session state changes happen within a lock.\n    Events are only created when needed, never reset outside locks.\n\n    This design prevents race conditions where tasks wait on events that get\n    replaced by other tasks, ensuring reliable coordination in concurrent scenarios.\n\n    Args:\n        transport:\n            Connection source specification, which can be:\n\n                - ClientTransport: Direct transport instance\n                - FastMCP: In-process FastMCP server\n                - AnyUrl or str: URL to connect to\n                - Path: File path for local socket\n                - MCPConfig: MCP server configuration\n                - dict: Transport configuration\n\n        roots: Optional RootsList or RootsHandler for filesystem access\n        sampling_handler: Optional handler for sampling requests\n        log_handler: Optional handler for log messages\n        message_handler: Optional handler for protocol messages\n        progress_handler: Optional handler for progress notifications\n        timeout: Optional timeout for requests (seconds or timedelta)\n        init_timeout: Optional timeout for initial connection (seconds or timedelta).\n            Set to 0 to disable. If None, uses the value in the FastMCP global settings.\n\n    Examples:\n        ```python\n        # Connect to FastMCP server\n        client = Client(\"http://localhost:8080\")\n\n        async with client:\n            # List available resources\n            resources = await client.list_resources()\n\n            # Call a tool\n            result = await client.call_tool(\"my_tool\", {\"param\": \"value\"})\n        ```\n    \"\"\"\n\n    @overload\n    def __init__(self: Client[T], transport: T, *args: Any, **kwargs: Any) -> None: ...\n\n    @overload\n    def __init__(\n        self: Client[SSETransport | StreamableHttpTransport],\n        transport: AnyUrl,\n        *args: Any,\n        **kwargs: Any,\n    ) -> None: ...\n\n    @overload\n    def __init__(\n        self: Client[FastMCPTransport],\n        transport: FastMCP | FastMCP1Server,\n        *args: Any,\n        **kwargs: Any,\n    ) -> None: ...\n\n    @overload\n    def __init__(\n        self: Client[PythonStdioTransport | NodeStdioTransport],\n        transport: Path,\n        *args: Any,\n        **kwargs: Any,\n    ) -> None: ...\n\n    @overload\n    def __init__(\n        self: Client[MCPConfigTransport],\n        transport: MCPConfig | dict[str, Any],\n        *args: Any,\n        **kwargs: Any,\n    ) -> None: ...\n\n    @overload\n    def __init__(\n        self: Client[\n            PythonStdioTransport\n            | NodeStdioTransport\n            | SSETransport\n            | StreamableHttpTransport\n        ],\n        transport: str,\n        *args: Any,\n        **kwargs: Any,\n    ) -> None: ...\n\n    def __init__(\n        self,\n        transport: (\n            ClientTransportT\n            | FastMCP\n            | FastMCP1Server\n            | AnyUrl\n            | Path\n            | MCPConfig\n            | dict[str, Any]\n            | str\n        ),\n        name: str | None = None,\n        roots: RootsList | RootsHandler | None = None,\n        sampling_handler: SamplingHandler | None = None,\n        sampling_capabilities: mcp.types.SamplingCapability | None = None,\n        elicitation_handler: ElicitationHandler | None = None,\n        log_handler: LogHandler | None = None,\n        message_handler: MessageHandlerT | MessageHandler | None = None,\n        progress_handler: ProgressHandler | None = None,\n        timeout: datetime.timedelta | float | int | None = None,\n        auto_initialize: bool = True,\n        init_timeout: datetime.timedelta | float | int | None = None,\n        client_info: mcp.types.Implementation | None = None,\n        auth: httpx.Auth | Literal[\"oauth\"] | str | None = None,\n        verify: ssl.SSLContext | bool | str | None = None,\n    ) -> None:\n        self.name = name or self.generate_name()\n\n        self.transport = cast(ClientTransportT, infer_transport(transport))\n\n        if verify is not None:\n            from fastmcp.client.transports.http import StreamableHttpTransport\n            from fastmcp.client.transports.sse import SSETransport\n\n            if isinstance(self.transport, StreamableHttpTransport | SSETransport):\n                self.transport.verify = verify\n                # Re-sync existing OAuth auth with the new verify setting,\n                # but only if the transport doesn't have a custom factory\n                # (which takes precedence and was already applied to OAuth).\n                if (\n                    isinstance(self.transport.auth, OAuth)\n                    and auth is None\n                    and self.transport.httpx_client_factory is None\n                ):\n                    verify_factory = self.transport._make_verify_factory()\n                    if verify_factory is not None:\n                        self.transport.auth.httpx_client_factory = verify_factory\n            else:\n                raise ValueError(\n                    \"The 'verify' parameter is only supported for HTTP transports.\"\n                )\n\n        if auth is not None:\n            self.transport._set_auth(auth)\n\n        if log_handler is None:\n            log_handler = default_log_handler\n\n        if progress_handler is None:\n            progress_handler = default_progress_handler\n\n        self._progress_handler = progress_handler\n\n        # Convert timeout to timedelta if needed\n        timeout = normalize_timeout_to_timedelta(timeout)\n\n        # handle init handshake timeout (0 means disabled)\n        if init_timeout is None:\n            init_timeout = fastmcp.settings.client_init_timeout\n        self._init_timeout = normalize_timeout_to_seconds(init_timeout)\n\n        self.auto_initialize = auto_initialize\n\n        self._session_kwargs: SessionKwargs = {\n            \"sampling_callback\": None,\n            \"list_roots_callback\": None,\n            \"logging_callback\": create_log_callback(log_handler),\n            \"message_handler\": message_handler or TaskNotificationHandler(self),\n            \"read_timeout_seconds\": timeout,\n            \"client_info\": client_info,\n        }\n\n        if roots is not None:\n            self.set_roots(roots)\n\n        if sampling_handler is not None:\n            self._session_kwargs[\"sampling_callback\"] = create_sampling_callback(\n                sampling_handler\n            )\n            self._session_kwargs[\"sampling_capabilities\"] = (\n                sampling_capabilities\n                if sampling_capabilities is not None\n                else mcp.types.SamplingCapability()\n            )\n\n        if elicitation_handler is not None:\n            self._session_kwargs[\"elicitation_callback\"] = create_elicitation_callback(\n                elicitation_handler\n            )\n\n        # Maximum time to wait for a clean disconnect before giving up.\n        # Normally disconnects complete in <100ms; this is a safety net for\n        # unresponsive servers.\n        self._disconnect_timeout: float = fastmcp.settings.client_disconnect_timeout\n\n        # Session context management - see class docstring for detailed explanation\n        self._session_state = ClientSessionState()\n\n        # Track task IDs submitted by this client (for list_tasks support)\n        self._submitted_task_ids: set[str] = set()\n\n        # Registry for routing notifications/tasks/status to Task objects\n\n        self._task_registry: dict[\n            str, weakref.ref[ToolTask | PromptTask | ResourceTask]\n        ] = {}\n\n    def _reset_session_state(self, full: bool = False) -> None:\n        \"\"\"Reset session state after disconnect or cancellation.\n\n        Args:\n            full: If True, also resets session_task and nesting_counter.\n                  Use full=True for cancellation cleanup where the session\n                  task was started but never completed normally.\n        \"\"\"\n        self._session_state.session = None\n        self._session_state.initialize_result = None\n        if full:\n            self._session_state.session_task = None\n            self._session_state.nesting_counter = 0\n\n    @property\n    def session(self) -> ClientSession:\n        \"\"\"Get the current active session. Raises RuntimeError if not connected.\"\"\"\n        if self._session_state.session is None:\n            raise RuntimeError(\n                \"Client is not connected. Use the 'async with client:' context manager first.\"\n            )\n\n        return self._session_state.session\n\n    @property\n    def initialize_result(self) -> mcp.types.InitializeResult | None:\n        \"\"\"Get the result of the initialization request.\"\"\"\n        return self._session_state.initialize_result\n\n    def set_roots(self, roots: RootsList | RootsHandler) -> None:\n        \"\"\"Set the roots for the client. This does not automatically call `send_roots_list_changed`.\"\"\"\n        self._session_kwargs[\"list_roots_callback\"] = create_roots_callback(roots)\n\n    def set_sampling_callback(\n        self,\n        sampling_callback: SamplingHandler,\n        sampling_capabilities: mcp.types.SamplingCapability | None = None,\n    ) -> None:\n        \"\"\"Set the sampling callback for the client.\"\"\"\n        self._session_kwargs[\"sampling_callback\"] = create_sampling_callback(\n            sampling_callback\n        )\n        self._session_kwargs[\"sampling_capabilities\"] = (\n            sampling_capabilities\n            if sampling_capabilities is not None\n            else mcp.types.SamplingCapability()\n        )\n\n    def set_elicitation_callback(\n        self, elicitation_callback: ElicitationHandler\n    ) -> None:\n        \"\"\"Set the elicitation callback for the client.\"\"\"\n        self._session_kwargs[\"elicitation_callback\"] = create_elicitation_callback(\n            elicitation_callback\n        )\n\n    def is_connected(self) -> bool:\n        \"\"\"Check if the client is currently connected.\"\"\"\n        return self._session_state.session is not None\n\n    def new(self) -> Client[ClientTransportT]:\n        \"\"\"Create a new client instance with the same configuration but fresh session state.\n\n        This creates a new client with the same transport, handlers, and configuration,\n        but with no active session. Useful for creating independent sessions that don't\n        share state with the original client.\n\n        Returns:\n            A new Client instance with the same configuration but disconnected state.\n\n        Example:\n            ```python\n            # Create a fresh client for each concurrent operation\n            fresh_client = client.new()\n            async with fresh_client:\n                await fresh_client.call_tool(\"some_tool\", {})\n            ```\n        \"\"\"\n        new_client = copy.copy(self)\n\n        if not isinstance(self.transport, StdioTransport):\n            # Reset session state to fresh state\n            new_client._session_state = ClientSessionState()\n\n        new_client.name += f\":{secrets.token_hex(2)}\"\n\n        return new_client\n\n    @asynccontextmanager\n    async def _context_manager(self):\n        with catch(get_catch_handlers()):\n            async with self.transport.connect_session(\n                **self._session_kwargs\n            ) as session:\n                self._session_state.session = session\n                # Initialize the session if auto_initialize is enabled\n                try:\n                    if self.auto_initialize:\n                        await self.initialize()\n                    yield\n                except anyio.ClosedResourceError as e:\n                    raise RuntimeError(\"Server session was closed unexpectedly\") from e\n                finally:\n                    self._reset_session_state()\n\n    async def initialize(\n        self,\n        timeout: datetime.timedelta | float | int | None = None,\n    ) -> mcp.types.InitializeResult:\n        \"\"\"Send an initialize request to the server.\n\n        This method performs the MCP initialization handshake with the server,\n        exchanging capabilities and server information. It is idempotent - calling\n        it multiple times returns the cached result from the first call.\n\n        The initialization happens automatically when entering the client context\n        manager unless `auto_initialize=False` was set during client construction.\n        Manual calls to this method are only needed when auto-initialization is disabled.\n\n        Args:\n            timeout: Optional timeout for the initialization request (seconds or timedelta).\n                If None, uses the client's init_timeout setting.\n\n        Returns:\n            InitializeResult: The server's initialization response containing server info,\n                capabilities, protocol version, and optional instructions.\n\n        Raises:\n            RuntimeError: If the client is not connected or initialization times out.\n\n        Example:\n            ```python\n            # With auto-initialization disabled\n            client = Client(server, auto_initialize=False)\n            async with client:\n                result = await client.initialize()\n                print(f\"Server: {result.serverInfo.name}\")\n                print(f\"Instructions: {result.instructions}\")\n            ```\n        \"\"\"\n\n        if self.initialize_result is not None:\n            return self.initialize_result\n\n        if timeout is None:\n            timeout = self._init_timeout\n        else:\n            timeout = normalize_timeout_to_seconds(timeout)\n\n        try:\n            with anyio.fail_after(timeout):\n                self._session_state.initialize_result = await self.session.initialize()\n                return self._session_state.initialize_result\n        except TimeoutError as e:\n            raise RuntimeError(\"Failed to initialize server session\") from e\n\n    async def __aenter__(self):\n        return await self._connect()\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        # Use a timeout to prevent hanging during cleanup if the connection is in a bad\n        # state (e.g., rate-limited). The MCP SDK's transport may try to terminate the\n        # session which can hang if the server is unresponsive.\n        with anyio.move_on_after(self._disconnect_timeout):\n            await self._disconnect()\n\n    async def _connect(self):\n        \"\"\"\n        Establish or reuse a session connection.\n\n        This method implements the reentrant context manager pattern:\n        - First call: Creates background session task and waits for it to be ready\n        - Subsequent calls: Increments reference counter and reuses existing session\n        - All operations protected by _context_lock to prevent race conditions\n\n        The critical fix: Events are only created when starting a new session,\n        never reset outside the lock, preventing the deadlock scenario where\n        tasks wait on events that get replaced by other tasks.\n        \"\"\"\n        # ensure only one session is running at a time to avoid race conditions\n        async with self._session_state.lock:\n            need_to_start = (\n                self._session_state.session_task is None\n                or self._session_state.session_task.done()\n            )\n\n            if need_to_start:\n                if self._session_state.nesting_counter != 0:\n                    raise RuntimeError(\n                        f\"Internal error: nesting counter should be 0 when starting new session, got {self._session_state.nesting_counter}\"\n                    )\n                self._session_state.stop_event = anyio.Event()\n                self._session_state.ready_event = anyio.Event()\n                self._session_state.session_task = asyncio.create_task(\n                    self._session_runner()\n                )\n                try:\n                    await self._session_state.ready_event.wait()\n                except asyncio.CancelledError:\n                    # Cancellation during initial connection startup can leave the\n                    # background session task running because __aexit__ is never invoked\n                    # when __aenter__ is cancelled. Since we hold the session lock here\n                    # and we know we started the session task, it's safe to tear it down\n                    # without impacting other active contexts.\n                    #\n                    # Note: session_task is an asyncio.Task (not anyio) because it needs\n                    # to outlive individual context manager scopes - anyio's structured\n                    # concurrency doesn't allow tasks to escape their task group.\n                    session_task = self._session_state.session_task\n                    if session_task is not None:\n                        # Request a graceful stop if the runner has already reached\n                        # its stop_event wait.\n                        self._session_state.stop_event.set()\n                        session_task.cancel()\n                        with anyio.CancelScope(shield=True):\n                            with anyio.move_on_after(3):\n                                try:\n                                    await session_task\n                                except asyncio.CancelledError:\n                                    pass\n                                except Exception as e:\n                                    logger.debug(\n                                        f\"Error during cancelled session cleanup: {e}\"\n                                    )\n\n                    # Reset session state so future callers can reconnect cleanly.\n                    self._reset_session_state(full=True)\n\n                    with anyio.CancelScope(shield=True):\n                        with anyio.move_on_after(3):\n                            try:\n                                await self.transport.close()\n                            except Exception as e:\n                                logger.debug(\n                                    f\"Error closing transport after cancellation: {e}\"\n                                )\n\n                    raise\n\n                if self._session_state.session_task.done():\n                    exception = self._session_state.session_task.exception()\n                    if exception is None:\n                        raise RuntimeError(\n                            \"Session task completed without exception but connection failed\"\n                        )\n                    # Preserve specific exception types that clients may want to handle\n                    if isinstance(exception, httpx.HTTPStatusError | McpError):\n                        raise exception\n                    raise RuntimeError(\n                        f\"Client failed to connect: {exception}\"\n                    ) from exception\n\n            self._session_state.nesting_counter += 1\n\n        return self\n\n    async def _disconnect(self, force: bool = False):\n        \"\"\"\n        Disconnect from session using reference counting.\n\n        This method implements proper cleanup for reentrant context managers:\n        - Decrements reference counter for normal exits\n        - Only stops session when counter reaches 0 (no more active contexts)\n        - Force flag bypasses reference counting for immediate shutdown\n        - Session cleanup happens inside the lock to ensure atomicity\n\n        Key fix: Removed the problematic \"Reset for future reconnects\" logic\n        that was resetting events outside the lock, causing race conditions.\n        Event recreation now happens only in _connect() when actually needed.\n        \"\"\"\n        # ensure only one session is running at a time to avoid race conditions\n        async with self._session_state.lock:\n            # if we are forcing a disconnect, reset the nesting counter\n            if force:\n                self._session_state.nesting_counter = 0\n\n            # otherwise decrement to check if we are done nesting\n            else:\n                self._session_state.nesting_counter = max(\n                    0, self._session_state.nesting_counter - 1\n                )\n\n            # if we are still nested, return\n            if self._session_state.nesting_counter > 0:\n                return\n\n            # stop the active session\n            if self._session_state.session_task is None:\n                return\n            self._session_state.stop_event.set()\n            # wait for session to finish to ensure state has been reset\n            await self._session_state.session_task\n            self._session_state.session_task = None\n\n    async def _session_runner(self):\n        \"\"\"\n        Background task that manages the actual session lifecycle.\n\n        This task runs in the background and:\n        1. Establishes the transport connection via _context_manager()\n        2. Signals that the session is ready via _ready_event.set()\n        3. Waits for disconnect signal via _stop_event.wait()\n        4. Ensures _ready_event is always set, even on failures\n\n        The simplified error handling (compared to the original) removes\n        redundant exception re-raising while ensuring waiting tasks are\n        always unblocked via the finally block.\n        \"\"\"\n        try:\n            async with AsyncExitStack() as stack:\n                await stack.enter_async_context(self._context_manager())\n                # Session/context is now ready\n                self._session_state.ready_event.set()\n                # Wait until disconnect/stop is requested\n                await self._session_state.stop_event.wait()\n        finally:\n            # Ensure ready event is set even if context manager entry fails\n            self._session_state.ready_event.set()\n\n    async def _await_with_session_monitoring(\n        self, coro: Coroutine[Any, Any, ResultT]\n    ) -> ResultT:\n        \"\"\"Await a coroutine while monitoring the session task for errors.\n\n        When using HTTP transports, server errors (4xx/5xx) are raised in the\n        background session task, not in the coroutine waiting for a response.\n        This causes the client to hang indefinitely since the response never\n        arrives. This method monitors the session task and propagates any\n        exceptions that occur, preventing the client from hanging.\n\n        Args:\n            coro: The coroutine to await (typically a session method call)\n\n        Returns:\n            The result of the coroutine\n\n        Raises:\n            The exception from the session task if it fails, or RuntimeError\n            if the session task completes unexpectedly without an exception.\n        \"\"\"\n        session_task = self._session_state.session_task\n\n        # If no session task, just await directly\n        if session_task is None:\n            return await coro\n\n        # If session task already failed, raise immediately\n        if session_task.done():\n            # Close the coroutine to avoid \"was never awaited\" warning\n            coro.close()\n            exc = session_task.exception()\n            if exc:\n                raise exc\n            raise RuntimeError(\"Session task completed unexpectedly\")\n\n        # Create task for our call\n        call_task = asyncio.create_task(coro)\n\n        try:\n            done, _ = await asyncio.wait(\n                {call_task, session_task},\n                return_when=asyncio.FIRST_COMPLETED,\n            )\n\n            if session_task in done:\n                # Session task completed (likely errored) before our call finished\n                call_task.cancel()\n                with anyio.CancelScope(shield=True), suppress(asyncio.CancelledError):\n                    await call_task\n\n                # Raise the session task exception\n                exc = session_task.exception()\n                if exc:\n                    raise exc\n                raise RuntimeError(\"Session task completed unexpectedly\")\n\n            # Our call completed first - get the result\n            return call_task.result()\n        except asyncio.CancelledError:\n            call_task.cancel()\n            with anyio.CancelScope(shield=True), suppress(asyncio.CancelledError):\n                await call_task\n            raise\n\n    def _handle_task_status_notification(\n        self, notification: TaskStatusNotification\n    ) -> None:\n        \"\"\"Route task status notification to appropriate Task object.\n\n        Called when notifications/tasks/status is received from server.\n        Updates Task object's cache and triggers events/callbacks.\n        \"\"\"\n        # Extract task ID from notification params\n        task_id = notification.params.taskId\n        if not task_id:\n            return\n\n        # Look up task in registry (weakref)\n        task_ref = self._task_registry.get(task_id)\n        if task_ref:\n            task = task_ref()  # Dereference weakref\n            if task:\n                # Convert notification params to GetTaskResult (they share the same fields via Task)\n                status = GetTaskResult.model_validate(notification.params.model_dump())\n                task._handle_status_notification(status)\n\n    async def close(self):\n        await self._disconnect(force=True)\n        await self.transport.close()\n\n    # --- MCP Client Methods ---\n\n    async def ping(self) -> bool:\n        \"\"\"Send a ping request.\"\"\"\n        result = await self._await_with_session_monitoring(self.session.send_ping())\n        return isinstance(result, mcp.types.EmptyResult)\n\n    async def cancel(\n        self,\n        request_id: str | int,\n        reason: str | None = None,\n    ) -> None:\n        \"\"\"Send a cancellation notification for an in-progress request.\"\"\"\n        notification = mcp.types.ClientNotification(\n            root=mcp.types.CancelledNotification(\n                method=\"notifications/cancelled\",\n                params=mcp.types.CancelledNotificationParams(\n                    requestId=request_id,\n                    reason=reason,\n                ),\n            )\n        )\n        await self.session.send_notification(notification)\n\n    async def progress(\n        self,\n        progress_token: str | int,\n        progress: float,\n        total: float | None = None,\n        message: str | None = None,\n    ) -> None:\n        \"\"\"Send a progress notification.\"\"\"\n        await self.session.send_progress_notification(\n            progress_token, progress, total, message\n        )\n\n    async def set_logging_level(self, level: mcp.types.LoggingLevel) -> None:\n        \"\"\"Send a logging/setLevel request.\"\"\"\n        await self._await_with_session_monitoring(self.session.set_logging_level(level))\n\n    async def send_roots_list_changed(self) -> None:\n        \"\"\"Send a roots/list_changed notification.\"\"\"\n        await self.session.send_roots_list_changed()\n\n    # --- Completion ---\n\n    async def complete_mcp(\n        self,\n        ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference,\n        argument: dict[str, str],\n        context_arguments: dict[str, Any] | None = None,\n    ) -> mcp.types.CompleteResult:\n        \"\"\"Send a completion request and return the complete MCP protocol result.\n\n        Args:\n            ref (mcp.types.ResourceTemplateReference | mcp.types.PromptReference): The reference to complete.\n            argument (dict[str, str]): Arguments to pass to the completion request.\n            context_arguments (dict[str, Any] | None, optional): Optional context arguments to\n                include with the completion request. Defaults to None.\n\n        Returns:\n            mcp.types.CompleteResult: The complete response object from the protocol,\n                containing the completion and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        logger.debug(f\"[{self.name}] called complete: {ref}\")\n\n        result = await self._await_with_session_monitoring(\n            self.session.complete(\n                ref=ref, argument=argument, context_arguments=context_arguments\n            )\n        )\n        return result\n\n    async def complete(\n        self,\n        ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference,\n        argument: dict[str, str],\n        context_arguments: dict[str, Any] | None = None,\n    ) -> mcp.types.Completion:\n        \"\"\"Send a completion request to the server.\n\n        Args:\n            ref (mcp.types.ResourceTemplateReference | mcp.types.PromptReference): The reference to complete.\n            argument (dict[str, str]): Arguments to pass to the completion request.\n            context_arguments (dict[str, Any] | None, optional): Optional context arguments to\n                include with the completion request. Defaults to None.\n\n        Returns:\n            mcp.types.Completion: The completion object.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        result = await self.complete_mcp(\n            ref=ref, argument=argument, context_arguments=context_arguments\n        )\n        return result.completion\n\n    @classmethod\n    def generate_name(cls, name: str | None = None) -> str:\n        class_name = cls.__name__\n        if name is None:\n            return f\"{class_name}-{secrets.token_hex(2)}\"\n        else:\n            return f\"{class_name}-{name}-{secrets.token_hex(2)}\"\n"
  },
  {
    "path": "src/fastmcp/client/elicitation.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable\nfrom typing import Any, Generic, TypeAlias\n\nimport mcp.types\nfrom mcp import ClientSession\nfrom mcp.client.session import ElicitationFnT\nfrom mcp.shared.context import LifespanContextT, RequestContext\nfrom mcp.types import ElicitRequestFormParams, ElicitRequestParams\nfrom mcp.types import ElicitResult as MCPElicitResult\nfrom pydantic_core import to_jsonable_python\nfrom typing_extensions import TypeVar\n\nfrom fastmcp.utilities.json_schema_type import json_schema_to_type\n\n__all__ = [\"ElicitRequestParams\", \"ElicitResult\", \"ElicitationHandler\"]\n\nT = TypeVar(\"T\", default=Any)\n\n\nclass ElicitResult(MCPElicitResult, Generic[T]):\n    content: T | None = None\n\n\nElicitationHandler: TypeAlias = Callable[\n    [\n        str,  # message\n        type[T]\n        | None,  # a class for creating a structured response (None for URL elicitation)\n        ElicitRequestParams,\n        RequestContext[ClientSession, LifespanContextT],\n    ],\n    Awaitable[T | dict[str, Any] | ElicitResult[T | dict[str, Any]]],\n]\n\n\ndef create_elicitation_callback(\n    elicitation_handler: ElicitationHandler,\n) -> ElicitationFnT:\n    async def _elicitation_handler(\n        context: RequestContext[ClientSession, LifespanContextT],\n        params: ElicitRequestParams,\n    ) -> MCPElicitResult | mcp.types.ErrorData:\n        try:\n            # requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams\n            if isinstance(params, ElicitRequestFormParams):\n                if params.requestedSchema == {\"type\": \"object\", \"properties\": {}}:\n                    response_type = None\n                else:\n                    response_type = json_schema_to_type(params.requestedSchema)\n            else:\n                # URL-based elicitation doesn't have a schema\n                response_type = None\n\n            result = await elicitation_handler(\n                params.message, response_type, params, context\n            )\n            # if the user returns data, we assume they've accepted the elicitation\n            if not isinstance(result, ElicitResult):\n                result = ElicitResult(action=\"accept\", content=result)\n            content = to_jsonable_python(result.content)\n            if not isinstance(content, dict | None):\n                raise ValueError(\n                    \"Elicitation responses must be serializable as a JSON object (dict). Received: \"\n                    f\"{result.content!r}\"\n                )\n            return MCPElicitResult(\n                _meta=result.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                action=result.action,\n                content=content,\n            )\n\n        except Exception as e:\n            return mcp.types.ErrorData(\n                code=mcp.types.INTERNAL_ERROR,\n                message=str(e),\n            )\n\n    return _elicitation_handler\n"
  },
  {
    "path": "src/fastmcp/client/logging.py",
    "content": "from collections.abc import Awaitable, Callable\nfrom logging import Logger\nfrom typing import TypeAlias\n\nfrom mcp.client.session import LoggingFnT\nfrom mcp.types import LoggingMessageNotificationParams\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger: Logger = get_logger(name=__name__)\nfrom_server_logger: Logger = get_logger(name=\"fastmcp.client.from_server\")\n\nLogMessage: TypeAlias = LoggingMessageNotificationParams\nLogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]\n\n\nasync def default_log_handler(message: LogMessage) -> None:\n    \"\"\"Default handler that properly routes server log messages to appropriate log levels.\"\"\"\n    # data can be any JSON-serializable type, not just a dict\n    data = message.data\n\n    # Map MCP log levels to Python logging levels\n    level_map = {\n        \"debug\": from_server_logger.debug,\n        \"info\": from_server_logger.info,\n        \"notice\": from_server_logger.info,  # Python doesn't have 'notice', map to info\n        \"warning\": from_server_logger.warning,\n        \"error\": from_server_logger.error,\n        \"critical\": from_server_logger.critical,\n        \"alert\": from_server_logger.critical,  # Map alert to critical\n        \"emergency\": from_server_logger.critical,  # Map emergency to critical\n    }\n\n    # Get the appropriate logging function based on the message level\n    log_fn = level_map.get(message.level.lower(), logger.info)\n\n    # Include logger name if available\n    msg_prefix: str = f\"Received {message.level.upper()} from server\"\n\n    if message.logger:\n        msg_prefix += f\" ({message.logger})\"\n\n    # Log with appropriate level and data\n    log_fn(msg=f\"{msg_prefix}: {data}\")\n\n\ndef create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:\n    if handler is None:\n        handler = default_log_handler\n\n    async def log_callback(params: LoggingMessageNotificationParams) -> None:\n        await handler(params)\n\n    return log_callback\n"
  },
  {
    "path": "src/fastmcp/client/messages.py",
    "content": "from typing import TypeAlias\n\nimport mcp.types\nfrom mcp.client.session import MessageHandlerFnT\nfrom mcp.shared.session import RequestResponder\n\nMessage: TypeAlias = (\n    RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]\n    | mcp.types.ServerNotification\n    | Exception\n)\n\nMessageHandlerT: TypeAlias = MessageHandlerFnT\n\n\nclass MessageHandler:\n    \"\"\"\n    This class is used to handle MCP messages sent to the client. It is used to handle all messages,\n    requests, notifications, and exceptions. Users can override any of the hooks\n    \"\"\"\n\n    async def __call__(\n        self,\n        message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]\n        | mcp.types.ServerNotification\n        | Exception,\n    ) -> None:\n        return await self.dispatch(message)\n\n    async def dispatch(self, message: Message) -> None:\n        # handle all messages\n        await self.on_message(message)\n\n        match message:\n            # requests\n            case RequestResponder():\n                # handle all requests\n                # TODO(ty): remove when ty supports match statement narrowing\n                await self.on_request(message)  # type: ignore[arg-type]\n\n                # handle specific requests\n                # TODO(ty): remove type ignores when ty supports match statement narrowing\n                match message.request.root:  # type: ignore[union-attr]\n                    case mcp.types.PingRequest():\n                        await self.on_ping(message.request.root)  # type: ignore[union-attr]\n                    case mcp.types.ListRootsRequest():\n                        await self.on_list_roots(message.request.root)  # type: ignore[union-attr]\n                    case mcp.types.CreateMessageRequest():\n                        await self.on_create_message(message.request.root)  # type: ignore[union-attr]\n\n            # notifications\n            case mcp.types.ServerNotification():\n                # handle all notifications\n                await self.on_notification(message)\n\n                # handle specific notifications\n                match message.root:\n                    case mcp.types.CancelledNotification():\n                        await self.on_cancelled(message.root)\n                    case mcp.types.ProgressNotification():\n                        await self.on_progress(message.root)\n                    case mcp.types.LoggingMessageNotification():\n                        await self.on_logging_message(message.root)\n                    case mcp.types.ToolListChangedNotification():\n                        await self.on_tool_list_changed(message.root)\n                    case mcp.types.ResourceListChangedNotification():\n                        await self.on_resource_list_changed(message.root)\n                    case mcp.types.PromptListChangedNotification():\n                        await self.on_prompt_list_changed(message.root)\n                    case mcp.types.ResourceUpdatedNotification():\n                        await self.on_resource_updated(message.root)\n\n            case Exception():\n                await self.on_exception(message)\n\n    async def on_message(self, message: Message) -> None:\n        pass\n\n    async def on_request(\n        self, message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]\n    ) -> None:\n        pass\n\n    async def on_ping(self, message: mcp.types.PingRequest) -> None:\n        pass\n\n    async def on_list_roots(self, message: mcp.types.ListRootsRequest) -> None:\n        pass\n\n    async def on_create_message(self, message: mcp.types.CreateMessageRequest) -> None:\n        pass\n\n    async def on_notification(self, message: mcp.types.ServerNotification) -> None:\n        pass\n\n    async def on_exception(self, message: Exception) -> None:\n        pass\n\n    async def on_progress(self, message: mcp.types.ProgressNotification) -> None:\n        pass\n\n    async def on_logging_message(\n        self, message: mcp.types.LoggingMessageNotification\n    ) -> None:\n        pass\n\n    async def on_tool_list_changed(\n        self, message: mcp.types.ToolListChangedNotification\n    ) -> None:\n        pass\n\n    async def on_resource_list_changed(\n        self, message: mcp.types.ResourceListChangedNotification\n    ) -> None:\n        pass\n\n    async def on_prompt_list_changed(\n        self, message: mcp.types.PromptListChangedNotification\n    ) -> None:\n        pass\n\n    async def on_resource_updated(\n        self, message: mcp.types.ResourceUpdatedNotification\n    ) -> None:\n        pass\n\n    async def on_cancelled(self, message: mcp.types.CancelledNotification) -> None:\n        pass\n"
  },
  {
    "path": "src/fastmcp/client/mixins/__init__.py",
    "content": "\"\"\"Client mixins for FastMCP.\"\"\"\n\nfrom fastmcp.client.mixins.prompts import ClientPromptsMixin\nfrom fastmcp.client.mixins.resources import ClientResourcesMixin\nfrom fastmcp.client.mixins.task_management import ClientTaskManagementMixin\nfrom fastmcp.client.mixins.tools import ClientToolsMixin\n\n__all__ = [\n    \"ClientPromptsMixin\",\n    \"ClientResourcesMixin\",\n    \"ClientTaskManagementMixin\",\n    \"ClientToolsMixin\",\n]\n"
  },
  {
    "path": "src/fastmcp/client/mixins/prompts.py",
    "content": "\"\"\"Prompt-related methods for FastMCP Client.\"\"\"\n\nfrom __future__ import annotations\n\nimport uuid\nimport weakref\nfrom typing import TYPE_CHECKING, Any, Literal, overload\n\nimport mcp.types\nimport pydantic_core\nfrom pydantic import RootModel\n\nif TYPE_CHECKING:\n    from fastmcp.client.client import Client\n\nfrom fastmcp.client.tasks import PromptTask\nfrom fastmcp.client.telemetry import client_span\nfrom fastmcp.telemetry import inject_trace_context\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nAUTO_PAGINATION_MAX_PAGES = 250\n\n# Type alias for task response union (SEP-1686 graceful degradation)\nPromptTaskResponseUnion = RootModel[\n    mcp.types.CreateTaskResult | mcp.types.GetPromptResult\n]\n\n\nclass ClientPromptsMixin:\n    \"\"\"Mixin providing prompt-related methods for Client.\"\"\"\n\n    # --- Prompts ---\n\n    async def list_prompts_mcp(\n        self: Client, *, cursor: str | None = None\n    ) -> mcp.types.ListPromptsResult:\n        \"\"\"Send a prompts/list request and return the complete MCP protocol result.\n\n        Args:\n            cursor: Optional pagination cursor from a previous request's nextCursor.\n\n        Returns:\n            mcp.types.ListPromptsResult: The complete response object from the protocol,\n                containing the list of prompts and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        logger.debug(f\"[{self.name}] called list_prompts\")\n\n        result = await self._await_with_session_monitoring(\n            self.session.list_prompts(cursor=cursor)\n        )\n        return result\n\n    async def list_prompts(\n        self: Client,\n        max_pages: int = AUTO_PAGINATION_MAX_PAGES,\n    ) -> list[mcp.types.Prompt]:\n        \"\"\"Retrieve all prompts available on the server.\n\n        This method automatically fetches all pages if the server paginates results,\n        returning the complete list. For manual pagination control (e.g., to handle\n        large result sets incrementally), use list_prompts_mcp() with the cursor parameter.\n\n        Args:\n            max_pages: Maximum number of pages to fetch before raising. Defaults to 250.\n\n        Returns:\n            list[mcp.types.Prompt]: A list of all Prompt objects.\n\n        Raises:\n            RuntimeError: If the page limit is reached before pagination completes.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        all_prompts: list[mcp.types.Prompt] = []\n        cursor: str | None = None\n        seen_cursors: set[str] = set()\n\n        for _ in range(max_pages):\n            result = await self.list_prompts_mcp(cursor=cursor)\n            all_prompts.extend(result.prompts)\n            if not result.nextCursor:\n                break\n            if result.nextCursor in seen_cursors:\n                logger.warning(\n                    f\"[{self.name}] Server returned duplicate pagination cursor\"\n                    f\" {result.nextCursor!r} for list_prompts; stopping pagination\"\n                )\n                break\n            seen_cursors.add(result.nextCursor)\n            cursor = result.nextCursor\n        else:\n            raise RuntimeError(\n                f\"[{self.name}] Reached auto-pagination limit\"\n                f\" ({max_pages} pages) for list_prompts.\"\n                \" Use list_prompts_mcp() with cursor for manual pagination,\"\n                \" or increase max_pages.\"\n            )\n\n        return all_prompts\n\n    # --- Prompt ---\n    async def get_prompt_mcp(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> mcp.types.GetPromptResult:\n        \"\"\"Send a prompts/get request and return the complete MCP protocol result.\n\n        Args:\n            name (str): The name of the prompt to retrieve.\n            arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.\n            meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.\n\n        Returns:\n            mcp.types.GetPromptResult: The complete response object from the protocol,\n                containing the prompt messages and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        with client_span(\n            f\"prompts/get {name}\",\n            \"prompts/get\",\n            name,\n            session_id=self.transport.get_session_id(),\n        ):\n            logger.debug(f\"[{self.name}] called get_prompt: {name}\")\n\n            # Serialize arguments for MCP protocol - convert non-string values to JSON\n            serialized_arguments: dict[str, str] | None = None\n            if arguments:\n                serialized_arguments = {}\n                for key, value in arguments.items():\n                    if isinstance(value, str):\n                        serialized_arguments[key] = value\n                    else:\n                        # Use pydantic_core.to_json for consistent serialization\n                        serialized_arguments[key] = pydantic_core.to_json(value).decode(\n                            \"utf-8\"\n                        )\n\n            # Inject trace context into meta for propagation to server\n            propagated_meta = inject_trace_context(meta)\n\n            # If meta provided, use send_request for SEP-1686 task support\n            if propagated_meta:\n                task_dict = propagated_meta.get(\"modelcontextprotocol.io/task\")\n                request = mcp.types.GetPromptRequest(\n                    params=mcp.types.GetPromptRequestParams(\n                        name=name,\n                        arguments=serialized_arguments,\n                        task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,\n                        _meta=propagated_meta,  # type: ignore[unknown-argument]  # pydantic alias\n                    )\n                )\n                result = await self._await_with_session_monitoring(\n                    self.session.send_request(\n                        request=request,  # type: ignore[arg-type]\n                        result_type=mcp.types.GetPromptResult,\n                    )\n                )\n            else:\n                result = await self._await_with_session_monitoring(\n                    self.session.get_prompt(name=name, arguments=serialized_arguments)\n                )\n            return result\n\n    @overload\n    async def get_prompt(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: str | None = None,\n        meta: dict[str, Any] | None = None,\n        task: Literal[False] = False,\n    ) -> mcp.types.GetPromptResult: ...\n\n    @overload\n    async def get_prompt(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: str | None = None,\n        meta: dict[str, Any] | None = None,\n        task: Literal[True],\n        task_id: str | None = None,\n        ttl: int = 60000,\n    ) -> PromptTask: ...\n\n    async def get_prompt(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: str | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool = False,\n        task_id: str | None = None,\n        ttl: int = 60000,\n    ) -> mcp.types.GetPromptResult | PromptTask:\n        \"\"\"Retrieve a rendered prompt message list from the server.\n\n        Args:\n            name (str): The name of the prompt to retrieve.\n            arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.\n            version (str | None, optional): Specific prompt version to get. If None, gets highest version.\n            meta (dict[str, Any] | None): Optional request-level metadata.\n            task (bool): If True, execute as background task (SEP-1686). Defaults to False.\n            task_id (str | None): Optional client-provided task ID (auto-generated if not provided).\n            ttl (int): Time to keep results available in milliseconds (default 60s).\n\n        Returns:\n            mcp.types.GetPromptResult | PromptTask: The complete response object if task=False,\n                or a PromptTask object if task=True.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        # Merge version into request-level meta (not arguments)\n        request_meta = dict(meta) if meta else {}\n        if version is not None:\n            request_meta[\"fastmcp\"] = {\n                **request_meta.get(\"fastmcp\", {}),\n                \"version\": version,\n            }\n\n        if task:\n            return await self._get_prompt_as_task(\n                name, arguments, task_id, ttl, meta=request_meta or None\n            )\n\n        result = await self.get_prompt_mcp(\n            name=name, arguments=arguments, meta=request_meta or None\n        )\n        return result\n\n    async def _get_prompt_as_task(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        task_id: str | None = None,\n        ttl: int = 60000,\n        meta: dict[str, Any] | None = None,\n    ) -> PromptTask:\n        \"\"\"Get a prompt for background execution (SEP-1686).\n\n        Returns a PromptTask object that handles both background and immediate execution.\n\n        Args:\n            name: Prompt name to get\n            arguments: Prompt arguments\n            task_id: Optional client-provided task ID (ignored, for backward compatibility)\n            ttl: Time to keep results available in milliseconds (default 60s)\n            meta: Optional request metadata (e.g., version info)\n\n        Returns:\n            PromptTask: Future-like object for accessing task status and results\n        \"\"\"\n        # Per SEP-1686 final spec: client sends only ttl, server generates taskId\n        # Inject trace context into meta for propagation to server\n        propagated_meta = inject_trace_context(meta)\n\n        # Serialize arguments for MCP protocol\n        serialized_arguments: dict[str, str] | None = None\n        if arguments:\n            serialized_arguments = {}\n            for key, value in arguments.items():\n                if isinstance(value, str):\n                    serialized_arguments[key] = value\n                else:\n                    serialized_arguments[key] = pydantic_core.to_json(value).decode(\n                        \"utf-8\"\n                    )\n\n        request = mcp.types.GetPromptRequest(\n            params=mcp.types.GetPromptRequestParams(\n                name=name,\n                arguments=serialized_arguments,\n                task=mcp.types.TaskMetadata(ttl=ttl),\n                _meta=propagated_meta,  # type: ignore[unknown-argument]  # pydantic alias\n            )\n        )\n\n        # Server returns CreateTaskResult (task accepted) or GetPromptResult (graceful degradation)\n        wrapped_result = await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[arg-type]\n                result_type=PromptTaskResponseUnion,\n            )\n        )\n        raw_result = wrapped_result.root\n\n        if isinstance(raw_result, mcp.types.CreateTaskResult):\n            # Task was accepted - extract task info from CreateTaskResult\n            server_task_id = raw_result.task.taskId\n            self._submitted_task_ids.add(server_task_id)\n\n            task_obj = PromptTask(\n                self, server_task_id, prompt_name=name, immediate_result=None\n            )\n            self._task_registry[server_task_id] = weakref.ref(task_obj)\n            return task_obj\n        else:\n            # Graceful degradation - server returned GetPromptResult\n            synthetic_task_id = task_id or str(uuid.uuid4())\n            return PromptTask(\n                self, synthetic_task_id, prompt_name=name, immediate_result=raw_result\n            )\n"
  },
  {
    "path": "src/fastmcp/client/mixins/resources.py",
    "content": "\"\"\"Resource-related methods for FastMCP Client.\"\"\"\n\nfrom __future__ import annotations\n\nimport uuid\nimport weakref\nfrom typing import TYPE_CHECKING, Any, Literal, overload\n\nimport mcp.types\nfrom pydantic import AnyUrl, RootModel\n\nif TYPE_CHECKING:\n    from fastmcp.client.client import Client\n\nfrom fastmcp.client.tasks import ResourceTask\nfrom fastmcp.client.telemetry import client_span\nfrom fastmcp.telemetry import inject_trace_context\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nAUTO_PAGINATION_MAX_PAGES = 250\n\n# Type alias for task response union (SEP-1686 graceful degradation)\nResourceTaskResponseUnion = RootModel[\n    mcp.types.CreateTaskResult | mcp.types.ReadResourceResult\n]\n\n\nclass ClientResourcesMixin:\n    \"\"\"Mixin providing resource-related methods for Client.\"\"\"\n\n    # --- Resources ---\n\n    async def list_resources_mcp(\n        self: Client, *, cursor: str | None = None\n    ) -> mcp.types.ListResourcesResult:\n        \"\"\"Send a resources/list request and return the complete MCP protocol result.\n\n        Args:\n            cursor: Optional pagination cursor from a previous request's nextCursor.\n\n        Returns:\n            mcp.types.ListResourcesResult: The complete response object from the protocol,\n                containing the list of resources and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        logger.debug(f\"[{self.name}] called list_resources\")\n\n        result = await self._await_with_session_monitoring(\n            self.session.list_resources(cursor=cursor)\n        )\n        return result\n\n    async def list_resources(\n        self: Client,\n        max_pages: int = AUTO_PAGINATION_MAX_PAGES,\n    ) -> list[mcp.types.Resource]:\n        \"\"\"Retrieve all resources available on the server.\n\n        This method automatically fetches all pages if the server paginates results,\n        returning the complete list. For manual pagination control (e.g., to handle\n        large result sets incrementally), use list_resources_mcp() with the cursor parameter.\n\n        Args:\n            max_pages: Maximum number of pages to fetch before raising. Defaults to 250.\n\n        Returns:\n            list[mcp.types.Resource]: A list of all Resource objects.\n\n        Raises:\n            RuntimeError: If the page limit is reached before pagination completes.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        all_resources: list[mcp.types.Resource] = []\n        cursor: str | None = None\n        seen_cursors: set[str] = set()\n\n        for _ in range(max_pages):\n            result = await self.list_resources_mcp(cursor=cursor)\n            all_resources.extend(result.resources)\n            if not result.nextCursor:\n                break\n            if result.nextCursor in seen_cursors:\n                logger.warning(\n                    f\"[{self.name}] Server returned duplicate pagination cursor\"\n                    f\" {result.nextCursor!r} for list_resources; stopping pagination\"\n                )\n                break\n            seen_cursors.add(result.nextCursor)\n            cursor = result.nextCursor\n        else:\n            raise RuntimeError(\n                f\"[{self.name}] Reached auto-pagination limit\"\n                f\" ({max_pages} pages) for list_resources.\"\n                \" Use list_resources_mcp() with cursor for manual pagination,\"\n                \" or increase max_pages.\"\n            )\n\n        return all_resources\n\n    async def list_resource_templates_mcp(\n        self: Client, *, cursor: str | None = None\n    ) -> mcp.types.ListResourceTemplatesResult:\n        \"\"\"Send a resources/listResourceTemplates request and return the complete MCP protocol result.\n\n        Args:\n            cursor: Optional pagination cursor from a previous request's nextCursor.\n\n        Returns:\n            mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,\n                containing the list of resource templates and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        logger.debug(f\"[{self.name}] called list_resource_templates\")\n\n        result = await self._await_with_session_monitoring(\n            self.session.list_resource_templates(cursor=cursor)\n        )\n        return result\n\n    async def list_resource_templates(\n        self: Client,\n        max_pages: int = AUTO_PAGINATION_MAX_PAGES,\n    ) -> list[mcp.types.ResourceTemplate]:\n        \"\"\"Retrieve all resource templates available on the server.\n\n        This method automatically fetches all pages if the server paginates results,\n        returning the complete list. For manual pagination control (e.g., to handle\n        large result sets incrementally), use list_resource_templates_mcp() with the\n        cursor parameter.\n\n        Args:\n            max_pages: Maximum number of pages to fetch before raising. Defaults to 250.\n\n        Returns:\n            list[mcp.types.ResourceTemplate]: A list of all ResourceTemplate objects.\n\n        Raises:\n            RuntimeError: If the page limit is reached before pagination completes.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        all_templates: list[mcp.types.ResourceTemplate] = []\n        cursor: str | None = None\n        seen_cursors: set[str] = set()\n\n        for _ in range(max_pages):\n            result = await self.list_resource_templates_mcp(cursor=cursor)\n            all_templates.extend(result.resourceTemplates)\n            if not result.nextCursor:\n                break\n            if result.nextCursor in seen_cursors:\n                logger.warning(\n                    f\"[{self.name}] Server returned duplicate pagination cursor\"\n                    f\" {result.nextCursor!r} for list_resource_templates;\"\n                    \" stopping pagination\"\n                )\n                break\n            seen_cursors.add(result.nextCursor)\n            cursor = result.nextCursor\n        else:\n            raise RuntimeError(\n                f\"[{self.name}] Reached auto-pagination limit\"\n                f\" ({max_pages} pages) for list_resource_templates.\"\n                \" Use list_resource_templates_mcp() with cursor for manual pagination,\"\n                \" or increase max_pages.\"\n            )\n\n        return all_templates\n\n    async def read_resource_mcp(\n        self: Client, uri: AnyUrl | str, meta: dict[str, Any] | None = None\n    ) -> mcp.types.ReadResourceResult:\n        \"\"\"Send a resources/read request and return the complete MCP protocol result.\n\n        Args:\n            uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.\n            meta (dict[str, Any] | None, optional): Request metadata (e.g., for SEP-1686 tasks). Defaults to None.\n\n        Returns:\n            mcp.types.ReadResourceResult: The complete response object from the protocol,\n                containing the resource contents and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        uri_str = str(uri)\n        with client_span(\n            f\"resources/read {uri_str}\",\n            \"resources/read\",\n            uri_str,\n            session_id=self.transport.get_session_id(),\n            resource_uri=uri_str,\n        ):\n            logger.debug(f\"[{self.name}] called read_resource: {uri}\")\n\n            if isinstance(uri, str):\n                uri = AnyUrl(uri)  # Ensure AnyUrl\n\n            # Inject trace context into meta for propagation to server\n            propagated_meta = inject_trace_context(meta)\n\n            # If meta provided, use send_request for SEP-1686 task support\n            if propagated_meta:\n                task_dict = propagated_meta.get(\"modelcontextprotocol.io/task\")\n                request = mcp.types.ReadResourceRequest(\n                    params=mcp.types.ReadResourceRequestParams(\n                        uri=uri,\n                        task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,\n                        _meta=propagated_meta,  # type: ignore[unknown-argument]  # pydantic alias\n                    )\n                )\n                result = await self._await_with_session_monitoring(\n                    self.session.send_request(\n                        request=request,  # type: ignore[arg-type]\n                        result_type=mcp.types.ReadResourceResult,\n                    )\n                )\n            else:\n                result = await self._await_with_session_monitoring(\n                    self.session.read_resource(uri)\n                )\n            return result\n\n    @overload\n    async def read_resource(\n        self: Client,\n        uri: AnyUrl | str,\n        *,\n        version: str | None = None,\n        meta: dict[str, Any] | None = None,\n        task: Literal[False] = False,\n    ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: ...\n\n    @overload\n    async def read_resource(\n        self: Client,\n        uri: AnyUrl | str,\n        *,\n        version: str | None = None,\n        meta: dict[str, Any] | None = None,\n        task: Literal[True],\n        task_id: str | None = None,\n        ttl: int = 60000,\n    ) -> ResourceTask: ...\n\n    async def read_resource(\n        self: Client,\n        uri: AnyUrl | str,\n        *,\n        version: str | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool = False,\n        task_id: str | None = None,\n        ttl: int = 60000,\n    ) -> (\n        list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]\n        | ResourceTask\n    ):\n        \"\"\"Read the contents of a resource or resolved template.\n\n        Args:\n            uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.\n            version (str | None): Specific version to read. If None, reads highest version.\n            meta (dict[str, Any] | None): Optional request-level metadata.\n            task (bool): If True, execute as background task (SEP-1686). Defaults to False.\n            task_id (str | None): Optional client-provided task ID (auto-generated if not provided).\n            ttl (int): Time to keep results available in milliseconds (default 60s).\n\n        Returns:\n            list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask:\n                A list of content objects if task=False, or a ResourceTask object if task=True.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        # Merge version into request-level meta (not arguments)\n        request_meta = dict(meta) if meta else {}\n        if version is not None:\n            request_meta[\"fastmcp\"] = {\n                **request_meta.get(\"fastmcp\", {}),\n                \"version\": version,\n            }\n\n        if task:\n            return await self._read_resource_as_task(\n                uri, task_id, ttl, meta=request_meta or None\n            )\n\n        if isinstance(uri, str):\n            try:\n                uri = AnyUrl(uri)  # Ensure AnyUrl\n            except Exception as e:\n                raise ValueError(\n                    f\"Provided resource URI is invalid: {str(uri)!r}\"\n                ) from e\n        result = await self.read_resource_mcp(uri, meta=request_meta or None)\n        return result.contents\n\n    async def _read_resource_as_task(\n        self: Client,\n        uri: AnyUrl | str,\n        task_id: str | None = None,\n        ttl: int = 60000,\n        meta: dict[str, Any] | None = None,\n    ) -> ResourceTask:\n        \"\"\"Read a resource for background execution (SEP-1686).\n\n        Returns a ResourceTask object that handles both background and immediate execution.\n\n        Args:\n            uri: Resource URI to read\n            task_id: Optional client-provided task ID (ignored, for backward compatibility)\n            ttl: Time to keep results available in milliseconds (default 60s)\n            meta: Optional metadata to pass with the request (e.g., version info)\n\n        Returns:\n            ResourceTask: Future-like object for accessing task status and results\n        \"\"\"\n        # Per SEP-1686 final spec: client sends only ttl, server generates taskId\n        # Inject trace context into meta for propagation to server\n        propagated_meta = inject_trace_context(meta)\n\n        if isinstance(uri, str):\n            uri = AnyUrl(uri)\n\n        request = mcp.types.ReadResourceRequest(\n            params=mcp.types.ReadResourceRequestParams(\n                uri=uri,\n                task=mcp.types.TaskMetadata(ttl=ttl),\n                _meta=propagated_meta,  # type: ignore[unknown-argument]  # pydantic alias\n            )\n        )\n\n        # Server returns CreateTaskResult (task accepted) or ReadResourceResult (graceful degradation)\n        wrapped_result = await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[arg-type]\n                result_type=ResourceTaskResponseUnion,\n            )\n        )\n        raw_result = wrapped_result.root\n\n        if isinstance(raw_result, mcp.types.CreateTaskResult):\n            # Task was accepted - extract task info from CreateTaskResult\n            server_task_id = raw_result.task.taskId\n            self._submitted_task_ids.add(server_task_id)\n\n            task_obj = ResourceTask(\n                self, server_task_id, uri=str(uri), immediate_result=None\n            )\n            self._task_registry[server_task_id] = weakref.ref(task_obj)\n            return task_obj\n        else:\n            # Graceful degradation - server returned ReadResourceResult\n            synthetic_task_id = task_id or str(uuid.uuid4())\n            return ResourceTask(\n                self,\n                synthetic_task_id,\n                uri=str(uri),\n                immediate_result=raw_result.contents,\n            )\n"
  },
  {
    "path": "src/fastmcp/client/mixins/task_management.py",
    "content": "\"\"\"Task management methods for FastMCP Client.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nimport mcp.types\nfrom mcp import McpError\n\nif TYPE_CHECKING:\n    from fastmcp.client.client import Client\nfrom mcp.types import (\n    CancelTaskRequest,\n    CancelTaskRequestParams,\n    GetTaskPayloadRequest,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequest,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListTasksRequest,\n    PaginatedRequestParams,\n)\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass ClientTaskManagementMixin:\n    \"\"\"Mixin providing task management methods for Client.\"\"\"\n\n    async def get_task_status(self: Client, task_id: str) -> GetTaskResult:\n        \"\"\"Query the status of a background task.\n\n        Sends a 'tasks/get' MCP protocol request over the existing transport.\n\n        Args:\n            task_id: The task ID returned from call_tool_as_task\n\n        Returns:\n            GetTaskResult: Status information including taskId, status, pollInterval, etc.\n\n        Raises:\n            RuntimeError: If client not connected\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        request = GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))\n        return await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[arg-type]\n                result_type=GetTaskResult,\n            )\n        )\n\n    async def get_task_result(self: Client, task_id: str) -> Any:\n        \"\"\"Retrieve the raw result of a completed background task.\n\n        Sends a 'tasks/result' MCP protocol request over the existing transport.\n        Returns the raw result - callers should parse it appropriately.\n\n        Args:\n            task_id: The task ID returned from call_tool_as_task\n\n        Returns:\n            Any: The raw result (could be tool, prompt, or resource result)\n\n        Raises:\n            RuntimeError: If client not connected, task not found, or task failed\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        request = GetTaskPayloadRequest(\n            params=GetTaskPayloadRequestParams(taskId=task_id)\n        )\n        # Return raw result - Task classes handle type-specific parsing\n        result = await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[arg-type]\n                result_type=GetTaskPayloadResult,\n            )\n        )\n        # Return as dict for compatibility with Task class parsing\n        return result.model_dump(exclude_none=True, by_alias=True)\n\n    async def list_tasks(\n        self: Client,\n        cursor: str | None = None,\n        limit: int = 50,\n    ) -> dict[str, Any]:\n        \"\"\"List background tasks.\n\n        Sends a 'tasks/list' MCP protocol request to the server. If the server\n        returns an empty list (indicating client-side tracking), falls back to\n        querying status for locally tracked task IDs.\n\n        Args:\n            cursor: Optional pagination cursor\n            limit: Maximum number of tasks to return (default 50)\n\n        Returns:\n            dict: Response with structure:\n                - tasks: List of task status dicts with taskId, status, etc.\n                - nextCursor: Optional cursor for next page\n\n        Raises:\n            RuntimeError: If client not connected\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        # Send protocol request\n        params = PaginatedRequestParams(cursor=cursor, limit=limit)  # type: ignore[call-arg]  # Optional field in MCP SDK\n        request = ListTasksRequest(params=params)\n        server_response = await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[invalid-argument-type]\n                result_type=mcp.types.ListTasksResult,\n            )\n        )\n\n        # If server returned tasks, use those\n        if server_response.tasks:\n            return server_response.model_dump(by_alias=True)\n\n        # Server returned empty - fall back to client-side tracking\n        tasks = []\n        for task_id in list(self._submitted_task_ids)[:limit]:\n            try:\n                status = await self.get_task_status(task_id)\n                tasks.append(status.model_dump(by_alias=True))\n            except McpError:\n                # Task may have expired or been deleted, skip it\n                continue\n\n        return {\"tasks\": tasks, \"nextCursor\": None}\n\n    async def cancel_task(self: Client, task_id: str) -> mcp.types.CancelTaskResult:\n        \"\"\"Cancel a task, transitioning it to cancelled state.\n\n        Sends a 'tasks/cancel' MCP protocol request. Task will halt execution\n        and transition to cancelled state.\n\n        Args:\n            task_id: The task ID to cancel\n\n        Returns:\n            CancelTaskResult: The task status showing cancelled state\n\n        Raises:\n            RuntimeError: If task doesn't exist\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        request = CancelTaskRequest(params=CancelTaskRequestParams(taskId=task_id))\n        return await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[invalid-argument-type]\n                result_type=mcp.types.CancelTaskResult,\n            )\n        )\n"
  },
  {
    "path": "src/fastmcp/client/mixins/tools.py",
    "content": "\"\"\"Tool-related methods for FastMCP Client.\"\"\"\n\nfrom __future__ import annotations\n\nimport uuid\nimport weakref\nfrom typing import TYPE_CHECKING, Any, Literal, cast, overload\n\nimport mcp.types\nfrom pydantic import RootModel\n\nif TYPE_CHECKING:\n    import datetime\n\n    from fastmcp.client.client import CallToolResult, Client\nfrom fastmcp.client.progress import ProgressHandler\nfrom fastmcp.client.tasks import ToolTask\nfrom fastmcp.client.telemetry import client_span\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.telemetry import inject_trace_context\nfrom fastmcp.utilities.json_schema_type import json_schema_to_type\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.timeout import normalize_timeout_to_timedelta\nfrom fastmcp.utilities.types import get_cached_typeadapter\n\nlogger = get_logger(__name__)\n\nAUTO_PAGINATION_MAX_PAGES = 250\n\n# Type alias for task response union (SEP-1686 graceful degradation)\nToolTaskResponseUnion = RootModel[mcp.types.CreateTaskResult | mcp.types.CallToolResult]\n\n\nclass ClientToolsMixin:\n    \"\"\"Mixin providing tool-related methods for Client.\"\"\"\n\n    # --- Tools ---\n\n    async def list_tools_mcp(\n        self: Client, *, cursor: str | None = None\n    ) -> mcp.types.ListToolsResult:\n        \"\"\"Send a tools/list request and return the complete MCP protocol result.\n\n        Args:\n            cursor: Optional pagination cursor from a previous request's nextCursor.\n\n        Returns:\n            mcp.types.ListToolsResult: The complete response object from the protocol,\n                containing the list of tools and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        logger.debug(f\"[{self.name}] called list_tools\")\n\n        result = await self._await_with_session_monitoring(\n            self.session.list_tools(cursor=cursor)\n        )\n        return result\n\n    async def list_tools(\n        self: Client,\n        max_pages: int = AUTO_PAGINATION_MAX_PAGES,\n    ) -> list[mcp.types.Tool]:\n        \"\"\"Retrieve all tools available on the server.\n\n        This method automatically fetches all pages if the server paginates results,\n        returning the complete list. For manual pagination control (e.g., to handle\n        large result sets incrementally), use list_tools_mcp() with the cursor parameter.\n\n        Args:\n            max_pages: Maximum number of pages to fetch before raising. Defaults to 250.\n\n        Returns:\n            list[mcp.types.Tool]: A list of all Tool objects.\n\n        Raises:\n            RuntimeError: If the page limit is reached before pagination completes.\n            McpError: If the request results in a TimeoutError | JSONRPCError\n        \"\"\"\n        all_tools: list[mcp.types.Tool] = []\n        cursor: str | None = None\n        seen_cursors: set[str] = set()\n\n        for _ in range(max_pages):\n            result = await self.list_tools_mcp(cursor=cursor)\n            all_tools.extend(result.tools)\n            if not result.nextCursor:\n                break\n            if result.nextCursor in seen_cursors:\n                logger.warning(\n                    f\"[{self.name}] Server returned duplicate pagination cursor\"\n                    f\" {result.nextCursor!r} for list_tools; stopping pagination\"\n                )\n                break\n            seen_cursors.add(result.nextCursor)\n            cursor = result.nextCursor\n        else:\n            raise RuntimeError(\n                f\"[{self.name}] Reached auto-pagination limit\"\n                f\" ({max_pages} pages) for list_tools.\"\n                \" Use list_tools_mcp() with cursor for manual pagination,\"\n                \" or increase max_pages.\"\n            )\n\n        return all_tools\n\n    # --- Call Tool ---\n\n    async def call_tool_mcp(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any],\n        progress_handler: ProgressHandler | None = None,\n        timeout: datetime.timedelta | float | int | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> mcp.types.CallToolResult:\n        \"\"\"Send a tools/call request and return the complete MCP protocol result.\n\n        This method returns the raw CallToolResult object, which includes an isError flag\n        and other metadata. It does not raise an exception if the tool call results in an error.\n\n        Args:\n            name (str): The name of the tool to call.\n            arguments (dict[str, Any]): Arguments to pass to the tool.\n            timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.\n            progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.\n            meta (dict[str, Any] | None, optional): Additional metadata to include with the request.\n                This is useful for passing contextual information (like user IDs, trace IDs, or preferences)\n                that shouldn't be tool arguments but may influence server-side processing. The server\n                can access this via `context.request_context.meta`. Defaults to None.\n\n        Returns:\n            mcp.types.CallToolResult: The complete response object from the protocol,\n                containing the tool result and any additional metadata.\n\n        Raises:\n            RuntimeError: If called while the client is not connected.\n            McpError: If the tool call requests results in a TimeoutError | JSONRPCError\n        \"\"\"\n        with client_span(\n            f\"tools/call {name}\",\n            \"tools/call\",\n            name,\n            session_id=self.transport.get_session_id(),\n        ):\n            logger.debug(f\"[{self.name}] called call_tool: {name}\")\n\n            # Inject trace context into meta for propagation to server\n            propagated_meta = inject_trace_context(meta)\n\n            result = await self._await_with_session_monitoring(\n                self.session.call_tool(\n                    name=name,\n                    arguments=arguments,\n                    read_timeout_seconds=normalize_timeout_to_timedelta(timeout),\n                    progress_callback=progress_handler or self._progress_handler,\n                    meta=propagated_meta if propagated_meta else None,\n                )\n            )\n            return result\n\n    async def _parse_call_tool_result(\n        self: Client,\n        name: str,\n        result: mcp.types.CallToolResult,\n        raise_on_error: bool = False,\n    ) -> CallToolResult:\n        \"\"\"Parse an mcp.types.CallToolResult into our CallToolResult dataclass.\n\n        Args:\n            name: Tool name (for schema lookup)\n            result: Raw MCP protocol result\n            raise_on_error: Whether to raise ToolError on errors\n\n        Returns:\n            CallToolResult: Parsed result with structured data\n        \"\"\"\n\n        return await _parse_call_tool_result(\n            name=name,\n            result=result,\n            tool_output_schemas=self.session._tool_output_schemas,\n            list_tools_fn=self.session.list_tools,\n            client_name=self.name,\n            raise_on_error=raise_on_error,\n        )\n\n    @overload\n    async def call_tool(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: str | None = None,\n        timeout: datetime.timedelta | float | int | None = None,\n        progress_handler: ProgressHandler | None = None,\n        raise_on_error: bool = True,\n        meta: dict[str, Any] | None = None,\n        task: Literal[False] = False,\n    ) -> CallToolResult: ...\n\n    @overload\n    async def call_tool(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: str | None = None,\n        timeout: datetime.timedelta | float | int | None = None,\n        progress_handler: ProgressHandler | None = None,\n        raise_on_error: bool = True,\n        meta: dict[str, Any] | None = None,\n        task: Literal[True],\n        task_id: str | None = None,\n        ttl: int = 60000,\n    ) -> ToolTask: ...\n\n    async def call_tool(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: str | None = None,\n        timeout: datetime.timedelta | float | int | None = None,\n        progress_handler: ProgressHandler | None = None,\n        raise_on_error: bool = True,\n        meta: dict[str, Any] | None = None,\n        task: bool = False,\n        task_id: str | None = None,\n        ttl: int = 60000,\n    ) -> CallToolResult | ToolTask:\n        \"\"\"Call a tool on the server.\n\n        Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.\n\n        Args:\n            name (str): The name of the tool to call.\n            arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.\n            version (str | None, optional): Specific tool version to call. If None, calls highest version.\n            timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.\n            progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.\n            raise_on_error (bool, optional): Whether to raise an exception if the tool call results in an error. Defaults to True.\n            meta (dict[str, Any] | None, optional): Additional metadata to include with the request.\n                This is useful for passing contextual information (like user IDs, trace IDs, or preferences)\n                that shouldn't be tool arguments but may influence server-side processing. The server\n                can access this via `context.request_context.meta`. Defaults to None.\n            task (bool): If True, execute as background task (SEP-1686). Defaults to False.\n            task_id (str | None): Optional client-provided task ID (auto-generated if not provided).\n            ttl (int): Time to keep results available in milliseconds (default 60s).\n\n        Returns:\n            CallToolResult | ToolTask: The content returned by the tool if task=False,\n                or a ToolTask object if task=True. If the tool returns structured\n                outputs, they are returned as a dataclass (if an output schema\n                is available) or a dictionary; otherwise, a list of content\n                blocks is returned. Note: to receive both structured and\n                unstructured outputs, use call_tool_mcp instead and access the\n                raw result object.\n\n        Raises:\n            ToolError: If the tool call results in an error.\n            McpError: If the tool call request results in a TimeoutError | JSONRPCError\n            RuntimeError: If called while the client is not connected.\n        \"\"\"\n        # Merge version into request-level meta (not arguments)\n        request_meta = dict(meta) if meta else {}\n        if version is not None:\n            request_meta[\"fastmcp\"] = {\n                **request_meta.get(\"fastmcp\", {}),\n                \"version\": version,\n            }\n\n        if task:\n            return await self._call_tool_as_task(\n                name, arguments, task_id, ttl, meta=request_meta or None\n            )\n\n        result = await self.call_tool_mcp(\n            name=name,\n            arguments=arguments or {},\n            timeout=timeout,\n            progress_handler=progress_handler,\n            meta=request_meta or None,\n        )\n        return await self._parse_call_tool_result(\n            name, result, raise_on_error=raise_on_error\n        )\n\n    async def _call_tool_as_task(\n        self: Client,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        task_id: str | None = None,\n        ttl: int = 60000,\n        meta: dict[str, Any] | None = None,\n    ) -> ToolTask:\n        \"\"\"Call a tool for background execution (SEP-1686).\n\n        Returns a ToolTask object that handles both background and immediate execution.\n        If the server accepts background execution, ToolTask will poll for results.\n        If the server declines (graceful degradation), ToolTask wraps the immediate result.\n\n        Args:\n            name: Tool name to call\n            arguments: Tool arguments\n            task_id: Optional client-provided task ID (ignored, for backward compatibility)\n            ttl: Time to keep results available in milliseconds (default 60s)\n            meta: Optional request metadata (e.g., version info)\n\n        Returns:\n            ToolTask: Future-like object for accessing task status and results\n        \"\"\"\n        # Per SEP-1686 final spec: client sends only ttl, server generates taskId\n        # Inject trace context into meta for propagation to server\n        propagated_meta = inject_trace_context(meta)\n\n        # Build request with task metadata\n        request = mcp.types.CallToolRequest(\n            params=mcp.types.CallToolRequestParams(\n                name=name,\n                arguments=arguments or {},\n                task=mcp.types.TaskMetadata(ttl=ttl),\n                _meta=propagated_meta,  # type: ignore[unknown-argument]  # pydantic alias\n            )\n        )\n\n        # Server returns CreateTaskResult (task accepted) or CallToolResult (graceful degradation)\n        # Use RootModel with Union to handle both response types (SDK calls model_validate)\n        wrapped_result = await self._await_with_session_monitoring(\n            self.session.send_request(\n                request=request,  # type: ignore[arg-type]\n                result_type=ToolTaskResponseUnion,\n            )\n        )\n        raw_result = wrapped_result.root\n\n        if isinstance(raw_result, mcp.types.CreateTaskResult):\n            # Task was accepted - extract task info from CreateTaskResult\n            server_task_id = raw_result.task.taskId\n            self._submitted_task_ids.add(server_task_id)\n\n            task_obj = ToolTask(\n                self, server_task_id, tool_name=name, immediate_result=None\n            )\n            self._task_registry[server_task_id] = weakref.ref(task_obj)\n            return task_obj\n        else:\n            # Graceful degradation - server returned CallToolResult\n            parsed_result = await self._parse_call_tool_result(name, raw_result)\n            synthetic_task_id = task_id or str(uuid.uuid4())\n            return ToolTask(\n                self,\n                synthetic_task_id,\n                tool_name=name,\n                immediate_result=parsed_result,\n            )\n\n\nasync def _parse_call_tool_result(\n    name: str,\n    result: mcp.types.CallToolResult,\n    tool_output_schemas: dict[str, dict[str, Any] | None],\n    list_tools_fn: Any,  # Callable[[], Awaitable[None]]\n    client_name: str | None = None,\n    raise_on_error: bool = False,\n) -> CallToolResult:\n    \"\"\"Parse an mcp.types.CallToolResult into our CallToolResult dataclass.\n\n    Args:\n        name: Tool name (for schema lookup)\n        result: Raw MCP protocol result\n        tool_output_schemas: Dictionary mapping tool names to their output schemas\n        list_tools_fn: Async function to refresh tool schemas if needed\n        client_name: Optional client name for logging\n        raise_on_error: Whether to raise ToolError on errors\n\n    Returns:\n        CallToolResult: Parsed result with structured data\n    \"\"\"\n    # Local import: CallToolResult is under TYPE_CHECKING at module level to\n    # avoid a circular import (client.client -> mixins.tools -> client.client),\n    # but we need the concrete class here to construct the return value.\n    from fastmcp.client.client import CallToolResult\n\n    data = None\n    if result.isError and raise_on_error:\n        msg = cast(mcp.types.TextContent, result.content[0]).text\n        raise ToolError(msg)\n    elif result.structuredContent:\n        try:\n            raw_fastmcp_meta = (result.meta or {}).get(\"fastmcp\")\n            fastmcp_meta = (\n                raw_fastmcp_meta if isinstance(raw_fastmcp_meta, dict) else {}\n            )\n            wrap_from_meta = fastmcp_meta.get(\"wrap_result\", False)\n\n            # Ensure the schema cache is populated for type validation.\n            # When meta tells us the result is wrapped we can skip the\n            # schema check for *wrap detection*, but we still need the\n            # schema for proper type coercion (e.g. list → set, str → datetime).\n            if name not in tool_output_schemas:\n                await list_tools_fn()\n\n            if wrap_from_meta:\n                # Meta tells us the result is wrapped — unwrap and validate.\n                structured_content = result.structuredContent.get(\"result\")\n            elif name in tool_output_schemas:\n                output_schema = tool_output_schemas.get(name)\n                if output_schema and output_schema.get(\"x-fastmcp-wrap-result\"):\n                    structured_content = result.structuredContent.get(\"result\")\n                else:\n                    structured_content = result.structuredContent\n            else:\n                structured_content = result.structuredContent\n\n            # Type-validate through the schema if available.\n            output_schema = tool_output_schemas.get(name)\n            if output_schema:\n                if wrap_from_meta or output_schema.get(\"x-fastmcp-wrap-result\"):\n                    output_schema = output_schema.get(\"properties\", {}).get(\n                        \"result\", output_schema\n                    )\n                output_type = json_schema_to_type(output_schema)\n                type_adapter = get_cached_typeadapter(output_type)\n                data = type_adapter.validate_python(structured_content)\n            else:\n                data = structured_content\n        except Exception as e:\n            logger.error(\n                f\"[{client_name or 'client'}] Error parsing structured content: {e}\"\n            )\n\n    return CallToolResult(\n        content=result.content,\n        structured_content=result.structuredContent,\n        meta=result.meta,\n        data=data,\n        is_error=result.isError,\n    )\n"
  },
  {
    "path": "src/fastmcp/client/oauth_callback.py",
    "content": "\"\"\"\nOAuth callback server for handling authorization code flows.\n\nThis module provides a reusable callback server that can handle OAuth redirects\nand display styled responses to users.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\nimport anyio\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.routing import Route\nfrom uvicorn import Config, Server\n\nfrom fastmcp.utilities.http import find_available_port\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.ui import (\n    HELPER_TEXT_STYLES,\n    INFO_BOX_STYLES,\n    STATUS_MESSAGE_STYLES,\n    create_info_box,\n    create_logo,\n    create_page,\n    create_secure_html_response,\n    create_status_message,\n)\n\nlogger = get_logger(__name__)\n\n\ndef create_callback_html(\n    message: str,\n    is_success: bool = True,\n    title: str = \"FastMCP OAuth\",\n    server_url: str | None = None,\n) -> str:\n    \"\"\"Create a styled HTML response for OAuth callbacks.\"\"\"\n    # Build the main status message\n    status_title = (\n        \"Authentication successful\" if is_success else \"Authentication failed\"\n    )\n\n    # Add detail info box for both success and error cases\n    detail_info = \"\"\n    if is_success and server_url:\n        detail_info = create_info_box(\n            f\"Connected to: {server_url}\", centered=True, monospace=True\n        )\n    elif not is_success:\n        detail_info = create_info_box(\n            message, is_error=True, centered=True, monospace=True\n        )\n\n    # Build the page content\n    content = f\"\"\"\n        <div class=\"container\">\n            {create_logo()}\n            {create_status_message(status_title, is_success=is_success)}\n            {detail_info}\n            <div class=\"close-instruction\">\n                You can safely close this tab now.\n            </div>\n        </div>\n    \"\"\"\n\n    # Additional styles needed for this page\n    additional_styles = STATUS_MESSAGE_STYLES + INFO_BOX_STYLES + HELPER_TEXT_STYLES\n\n    return create_page(\n        content=content,\n        title=title,\n        additional_styles=additional_styles,\n    )\n\n\n@dataclass\nclass CallbackResponse:\n    code: str | None = None\n    state: str | None = None\n    error: str | None = None\n    error_description: str | None = None\n\n    @classmethod\n    def from_dict(cls, data: dict[str, str]) -> CallbackResponse:\n        return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})\n\n    def to_dict(self) -> dict[str, str]:\n        return {k: v for k, v in self.__dict__.items() if v is not None}\n\n\n@dataclass\nclass OAuthCallbackResult:\n    \"\"\"Container for OAuth callback results, used with anyio.Event for async coordination.\"\"\"\n\n    code: str | None = None\n    state: str | None = None\n    error: Exception | None = None\n\n\ndef create_oauth_callback_server(\n    port: int,\n    callback_path: str = \"/callback\",\n    server_url: str | None = None,\n    result_container: OAuthCallbackResult | None = None,\n    result_ready: anyio.Event | None = None,\n) -> Server:\n    \"\"\"\n    Create an OAuth callback server.\n\n    Args:\n        port: The port to run the server on\n        callback_path: The path to listen for OAuth redirects on\n        server_url: Optional server URL to display in success messages\n        result_container: Optional container to store callback results\n        result_ready: Optional event to signal when callback is received\n\n    Returns:\n        Configured uvicorn Server instance (not yet running)\n    \"\"\"\n\n    def store_result_once(\n        *,\n        code: str | None = None,\n        state: str | None = None,\n        error: Exception | None = None,\n    ) -> None:\n        \"\"\"Store the first callback result and ignore subsequent requests.\"\"\"\n        if result_container is None or result_ready is None or result_ready.is_set():\n            return\n\n        result_container.code = code\n        result_container.state = state\n        result_container.error = error\n        result_ready.set()\n\n    async def callback_handler(request: Request):\n        \"\"\"Handle OAuth callback requests with proper HTML responses.\"\"\"\n        query_params = dict(request.query_params)\n        callback_response = CallbackResponse.from_dict(query_params)\n\n        if callback_response.error:\n            error_desc = callback_response.error_description or \"Unknown error\"\n\n            # Create user-friendly error messages\n            if callback_response.error == \"access_denied\":\n                user_message = \"Access was denied by the authorization server.\"\n            else:\n                user_message = f\"Authorization failed: {error_desc}\"\n\n            # Store error and signal completion if result tracking provided\n            store_result_once(error=RuntimeError(user_message))\n\n            return create_secure_html_response(\n                create_callback_html(\n                    user_message,\n                    is_success=False,\n                ),\n                status_code=400,\n            )\n\n        if not callback_response.code:\n            user_message = \"No authorization code was received from the server.\"\n\n            # Store error and signal completion if result tracking provided\n            store_result_once(error=RuntimeError(user_message))\n\n            return create_secure_html_response(\n                create_callback_html(\n                    user_message,\n                    is_success=False,\n                ),\n                status_code=400,\n            )\n\n        # Check for missing state parameter (indicates OAuth flow issue)\n        if callback_response.state is None:\n            user_message = (\n                \"The OAuth server did not return the expected state parameter.\"\n            )\n\n            # Store error and signal completion if result tracking provided\n            store_result_once(error=RuntimeError(user_message))\n\n            return create_secure_html_response(\n                create_callback_html(\n                    user_message,\n                    is_success=False,\n                ),\n                status_code=400,\n            )\n\n        # Success case - store result and signal completion if result tracking provided\n        store_result_once(\n            code=callback_response.code,\n            state=callback_response.state,\n        )\n\n        return create_secure_html_response(\n            create_callback_html(\"\", is_success=True, server_url=server_url)\n        )\n\n    app = Starlette(routes=[Route(callback_path, callback_handler)])\n\n    return Server(\n        Config(\n            app=app,\n            host=\"127.0.0.1\",\n            port=port,\n            lifespan=\"off\",\n            log_level=\"warning\",\n            ws=\"websockets-sansio\",\n        )\n    )\n\n\nif __name__ == \"__main__\":\n    \"\"\"Run a test server when executed directly.\"\"\"\n    import webbrowser\n\n    import uvicorn\n\n    port = find_available_port()\n    print(\"🎭 OAuth Callback Test Server\")\n    print(\"📍 Test URLs:\")\n    print(f\"  Success: http://localhost:{port}/callback?code=test123&state=xyz\")\n    print(\n        f\"  Error:   http://localhost:{port}/callback?error=access_denied&error_description=User%20denied\"\n    )\n    print(f\"  Missing: http://localhost:{port}/callback\")\n    print(\"🛑 Press Ctrl+C to stop\")\n    print()\n\n    # Create test server without future (just for testing HTML responses)\n    server = create_oauth_callback_server(\n        port=port, server_url=\"https://fastmcp-test-server.example.com\"\n    )\n\n    # Open browser to success example\n    webbrowser.open(f\"http://localhost:{port}/callback?code=test123&state=xyz\")\n\n    # Run with uvicorn directly\n    uvicorn.run(\n        server.config.app,\n        host=\"127.0.0.1\",\n        port=port,\n        log_level=\"warning\",\n        access_log=False,\n    )\n"
  },
  {
    "path": "src/fastmcp/client/progress.py",
    "content": "from typing import TypeAlias\n\nfrom mcp.shared.session import ProgressFnT\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nProgressHandler: TypeAlias = ProgressFnT\n\n\nasync def default_progress_handler(\n    progress: float, total: float | None, message: str | None\n) -> None:\n    \"\"\"Default handler for progress notifications.\n\n    Logs progress updates at debug level, properly handling missing total or message values.\n\n    Args:\n        progress: Current progress value\n        total: Optional total expected value\n        message: Optional status message\n    \"\"\"\n    if total not in (None, 0):\n        # We have both progress and total\n        percent = (progress / total) * 100\n        progress_str = f\"{progress}/{total} ({percent:.1f}%)\"\n    elif total == 0:\n        # Avoid division by zero when a server reports an invalid total.\n        progress_str = f\"{progress}/{total}\"\n    else:\n        # We only have progress\n        progress_str = f\"{progress}\"\n\n    # Include message if available\n    if message:\n        log_msg = f\"Progress: {progress_str} - {message}\"\n    else:\n        log_msg = f\"Progress: {progress_str}\"\n\n    logger.debug(log_msg)\n"
  },
  {
    "path": "src/fastmcp/client/roots.py",
    "content": "import inspect\nfrom collections.abc import Awaitable, Callable\nfrom typing import TypeAlias, cast\n\nimport mcp.types\nimport pydantic\nfrom mcp import ClientSession\nfrom mcp.client.session import ListRootsFnT\nfrom mcp.shared.context import LifespanContextT, RequestContext\n\nRootsList: TypeAlias = list[str] | list[mcp.types.Root] | list[str | mcp.types.Root]\n\nRootsHandler: TypeAlias = (\n    Callable[[RequestContext[ClientSession, LifespanContextT]], RootsList]\n    | Callable[[RequestContext[ClientSession, LifespanContextT]], Awaitable[RootsList]]\n)\n\n\ndef convert_roots_list(roots: RootsList) -> list[mcp.types.Root]:\n    roots_list = []\n    for r in roots:\n        if isinstance(r, mcp.types.Root):\n            roots_list.append(r)\n        elif isinstance(r, pydantic.FileUrl):\n            roots_list.append(mcp.types.Root(uri=r))\n        elif isinstance(r, str):\n            roots_list.append(mcp.types.Root(uri=pydantic.FileUrl(r)))\n        else:\n            raise ValueError(f\"Invalid root: {r}\")\n    return roots_list\n\n\ndef create_roots_callback(\n    handler: RootsList | RootsHandler,\n) -> ListRootsFnT:\n    if isinstance(handler, list):\n        # TODO(ty): remove when ty supports isinstance union narrowing\n        return _create_roots_callback_from_roots(handler)  # type: ignore[arg-type]\n    elif inspect.isfunction(handler):\n        return _create_roots_callback_from_fn(handler)\n    else:\n        raise ValueError(f\"Invalid roots handler: {handler}\")\n\n\ndef _create_roots_callback_from_roots(\n    roots: RootsList,\n) -> ListRootsFnT:\n    roots = convert_roots_list(roots)\n\n    async def _roots_callback(\n        context: RequestContext[ClientSession, LifespanContextT],\n    ) -> mcp.types.ListRootsResult:\n        return mcp.types.ListRootsResult(roots=roots)\n\n    return _roots_callback\n\n\ndef _create_roots_callback_from_fn(\n    fn: Callable[[RequestContext[ClientSession, LifespanContextT]], RootsList]\n    | Callable[[RequestContext[ClientSession, LifespanContextT]], Awaitable[RootsList]],\n) -> ListRootsFnT:\n    async def _roots_callback(\n        context: RequestContext[ClientSession, LifespanContextT],\n    ) -> mcp.types.ListRootsResult | mcp.types.ErrorData:\n        try:\n            roots = fn(context)\n            if inspect.isawaitable(roots):\n                roots = await roots\n            return mcp.types.ListRootsResult(\n                roots=convert_roots_list(cast(RootsList, roots))\n            )\n        except Exception as e:\n            return mcp.types.ErrorData(\n                code=mcp.types.INTERNAL_ERROR,\n                message=str(e),\n            )\n\n    return _roots_callback\n"
  },
  {
    "path": "src/fastmcp/client/sampling/__init__.py",
    "content": "import inspect\nfrom collections.abc import Awaitable, Callable\nfrom typing import TypeAlias, TypeVar, cast\n\nimport mcp.types\nfrom mcp import ClientSession, CreateMessageResult\nfrom mcp.client.session import SamplingFnT\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.context import LifespanContextT, RequestContext\nfrom mcp.types import CreateMessageRequestParams as SamplingParams\nfrom mcp.types import CreateMessageResultWithTools, SamplingMessage\n\n# Result type that handlers can return\nSamplingHandlerResult: TypeAlias = (\n    str | CreateMessageResult | CreateMessageResultWithTools\n)\n\n# Session type for sampling handlers - works with both client and server sessions\nSessionT = TypeVar(\"SessionT\", ClientSession, ServerSession)\n\n# Unified sampling handler type that works for both clients and servers.\n# Handlers receive messages and parameters from the MCP sampling flow\n# and return LLM responses.\nSamplingHandler: TypeAlias = Callable[\n    [\n        list[SamplingMessage],\n        SamplingParams,\n        RequestContext[SessionT, LifespanContextT],\n    ],\n    SamplingHandlerResult | Awaitable[SamplingHandlerResult],\n]\n\n\n__all__ = [\n    \"RequestContext\",\n    \"SamplingHandler\",\n    \"SamplingHandlerResult\",\n    \"SamplingMessage\",\n    \"SamplingParams\",\n    \"create_sampling_callback\",\n]\n\n\ndef create_sampling_callback(\n    sampling_handler: SamplingHandler,\n) -> SamplingFnT:\n    async def _sampling_handler(\n        context,\n        params: SamplingParams,\n    ) -> CreateMessageResult | CreateMessageResultWithTools | mcp.types.ErrorData:\n        try:\n            result = sampling_handler(params.messages, params, context)\n            if inspect.isawaitable(result):\n                result = await result\n\n            result = cast(SamplingHandlerResult, result)\n\n            if isinstance(result, str):\n                result = CreateMessageResult(\n                    role=\"assistant\",\n                    model=\"fastmcp-client\",\n                    content=mcp.types.TextContent(type=\"text\", text=result),\n                )\n            return result\n        except Exception as e:\n            return mcp.types.ErrorData(\n                code=mcp.types.INTERNAL_ERROR,\n                message=str(e),\n            )\n\n    return _sampling_handler\n"
  },
  {
    "path": "src/fastmcp/client/sampling/handlers/__init__.py",
    "content": ""
  },
  {
    "path": "src/fastmcp/client/sampling/handlers/anthropic.py",
    "content": "\"\"\"Anthropic sampling handler for FastMCP.\"\"\"\n\nfrom collections.abc import Iterator, Sequence\nfrom typing import Any\n\nfrom mcp.types import (\n    AudioContent,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    ImageContent,\n    ModelPreferences,\n    SamplingMessage,\n    SamplingMessageContentBlock,\n    StopReason,\n    TextContent,\n    Tool,\n    ToolChoice,\n    ToolResultContent,\n    ToolUseContent,\n)\nfrom mcp.types import CreateMessageRequestParams as SamplingParams\n\ntry:\n    from anthropic import AsyncAnthropic\n    from anthropic.types import (\n        Base64ImageSourceParam,\n        ImageBlockParam,\n        Message,\n        MessageParam,\n        TextBlock,\n        TextBlockParam,\n        ToolParam,\n        ToolResultBlockParam,\n        ToolUseBlock,\n        ToolUseBlockParam,\n    )\n    from anthropic.types.model_param import ModelParam\n    from anthropic.types.tool_choice_any_param import ToolChoiceAnyParam\n    from anthropic.types.tool_choice_auto_param import ToolChoiceAutoParam\n    from anthropic.types.tool_choice_param import ToolChoiceParam\nexcept ImportError as e:\n    raise ImportError(\n        \"The `anthropic` package is not installed. \"\n        \"Install it with `pip install fastmcp[anthropic]` or add `anthropic` to your dependencies.\"\n    ) from e\n\n__all__ = [\"AnthropicSamplingHandler\"]\n\n# Anthropic supports these image MIME types\n_ANTHROPIC_IMAGE_MEDIA_TYPES = frozenset(\n    {\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"}\n)\n\n\ndef _image_content_to_anthropic_block(content: ImageContent) -> ImageBlockParam:\n    \"\"\"Convert MCP ImageContent to Anthropic ImageBlockParam.\"\"\"\n    if content.mimeType not in _ANTHROPIC_IMAGE_MEDIA_TYPES:\n        raise ValueError(\n            f\"Unsupported image MIME type for Anthropic: {content.mimeType!r}. \"\n            f\"Supported types: {', '.join(sorted(_ANTHROPIC_IMAGE_MEDIA_TYPES))}\"\n        )\n    return ImageBlockParam(\n        type=\"image\",\n        source=Base64ImageSourceParam(\n            type=\"base64\",\n            media_type=content.mimeType,  # type: ignore[arg-type]\n            data=content.data,\n        ),\n    )\n\n\nclass AnthropicSamplingHandler:\n    \"\"\"Sampling handler that uses the Anthropic API.\n\n    Example:\n        ```python\n        from anthropic import AsyncAnthropic\n        from fastmcp import FastMCP\n        from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler\n\n        handler = AnthropicSamplingHandler(\n            default_model=\"claude-sonnet-4-5\",\n            client=AsyncAnthropic(),\n        )\n\n        server = FastMCP(sampling_handler=handler)\n        ```\n    \"\"\"\n\n    def __init__(\n        self, default_model: ModelParam, client: AsyncAnthropic | None = None\n    ) -> None:\n        self.client: AsyncAnthropic = client or AsyncAnthropic()\n        self.default_model: ModelParam = default_model\n\n    async def __call__(\n        self,\n        messages: list[SamplingMessage],\n        params: SamplingParams,\n        context: Any,\n    ) -> CreateMessageResult | CreateMessageResultWithTools:\n        anthropic_messages: list[MessageParam] = self._convert_to_anthropic_messages(\n            messages=messages,\n        )\n\n        model: ModelParam = self._select_model_from_preferences(params.modelPreferences)\n\n        # Convert MCP tools to Anthropic format\n        anthropic_tools: list[ToolParam] | None = None\n        if params.tools:\n            anthropic_tools = self._convert_tools_to_anthropic(params.tools)\n\n        # Convert tool_choice to Anthropic format\n        # Returns None if mode is \"none\", signaling tools should be omitted\n        anthropic_tool_choice: ToolChoiceParam | None = None\n        if params.toolChoice:\n            converted = self._convert_tool_choice_to_anthropic(params.toolChoice)\n            if converted is None:\n                # tool_choice=\"none\" means don't use tools\n                anthropic_tools = None\n            else:\n                anthropic_tool_choice = converted\n\n        # Build kwargs to avoid sentinel type compatibility issues across\n        # anthropic SDK versions (NotGiven vs Omit)\n        kwargs: dict[str, Any] = {\n            \"model\": model,\n            \"messages\": anthropic_messages,\n            \"max_tokens\": params.maxTokens,\n        }\n        if params.systemPrompt is not None:\n            kwargs[\"system\"] = params.systemPrompt\n        if params.temperature is not None:\n            kwargs[\"temperature\"] = params.temperature\n        if params.stopSequences is not None:\n            kwargs[\"stop_sequences\"] = params.stopSequences\n        if anthropic_tools is not None:\n            kwargs[\"tools\"] = anthropic_tools\n        if anthropic_tool_choice is not None:\n            kwargs[\"tool_choice\"] = anthropic_tool_choice\n\n        response = await self.client.messages.create(**kwargs)\n\n        # Return appropriate result type based on whether tools were provided\n        if params.tools:\n            return self._message_to_result_with_tools(response)\n        return self._message_to_create_message_result(response)\n\n    @staticmethod\n    def _iter_models_from_preferences(\n        model_preferences: ModelPreferences | str | list[str] | None,\n    ) -> Iterator[str]:\n        if model_preferences is None:\n            return\n\n        if isinstance(model_preferences, str):\n            yield model_preferences\n\n        elif isinstance(model_preferences, list):\n            yield from model_preferences\n\n        elif isinstance(model_preferences, ModelPreferences):\n            if not (hints := model_preferences.hints):\n                return\n\n            for hint in hints:\n                if not (name := hint.name):\n                    continue\n\n                yield name\n\n    @staticmethod\n    def _convert_to_anthropic_messages(\n        messages: Sequence[SamplingMessage],\n    ) -> list[MessageParam]:\n        anthropic_messages: list[MessageParam] = []\n\n        for message in messages:\n            content = message.content\n\n            # Handle list content (from CreateMessageResultWithTools)\n            if isinstance(content, list):\n                content_blocks: list[\n                    TextBlockParam\n                    | ImageBlockParam\n                    | ToolUseBlockParam\n                    | ToolResultBlockParam\n                ] = []\n\n                for item in content:\n                    if isinstance(item, ToolUseContent):\n                        content_blocks.append(\n                            ToolUseBlockParam(\n                                type=\"tool_use\",\n                                id=item.id,\n                                name=item.name,\n                                input=item.input,\n                            )\n                        )\n                    elif isinstance(item, TextContent):\n                        content_blocks.append(\n                            TextBlockParam(type=\"text\", text=item.text)\n                        )\n                    elif isinstance(item, ImageContent):\n                        if message.role != \"user\":\n                            raise ValueError(\n                                \"ImageContent is only supported in user messages \"\n                                \"for Anthropic\"\n                            )\n                        content_blocks.append(_image_content_to_anthropic_block(item))\n                    elif isinstance(item, AudioContent):\n                        raise ValueError(\n                            \"AudioContent is not supported by the Anthropic API\"\n                        )\n                    elif isinstance(item, ToolResultContent):\n                        # Extract text content from the result\n                        result_content: str | list[TextBlockParam] = \"\"\n                        if item.content:\n                            text_blocks: list[TextBlockParam] = []\n                            for sub_item in item.content:\n                                if isinstance(sub_item, TextContent):\n                                    text_blocks.append(\n                                        TextBlockParam(type=\"text\", text=sub_item.text)\n                                    )\n                            if len(text_blocks) == 1:\n                                result_content = text_blocks[0][\"text\"]\n                            elif text_blocks:\n                                result_content = text_blocks\n\n                        content_blocks.append(\n                            ToolResultBlockParam(\n                                type=\"tool_result\",\n                                tool_use_id=item.toolUseId,\n                                content=result_content,\n                                is_error=item.isError if item.isError else False,\n                            )\n                        )\n\n                if content_blocks:\n                    anthropic_messages.append(\n                        MessageParam(\n                            role=message.role,\n                            content=content_blocks,\n                        )\n                    )\n                continue\n\n            # Handle ToolUseContent (assistant's tool calls)\n            if isinstance(content, ToolUseContent):\n                anthropic_messages.append(\n                    MessageParam(\n                        role=\"assistant\",\n                        content=[\n                            ToolUseBlockParam(\n                                type=\"tool_use\",\n                                id=content.id,\n                                name=content.name,\n                                input=content.input,\n                            )\n                        ],\n                    )\n                )\n                continue\n\n            # Handle ToolResultContent (user's tool results)\n            if isinstance(content, ToolResultContent):\n                result_content_str: str | list[TextBlockParam] = \"\"\n                if content.content:\n                    text_parts: list[TextBlockParam] = []\n                    for item in content.content:\n                        if isinstance(item, TextContent):\n                            text_parts.append(\n                                TextBlockParam(type=\"text\", text=item.text)\n                            )\n                    if len(text_parts) == 1:\n                        result_content_str = text_parts[0][\"text\"]\n                    elif text_parts:\n                        result_content_str = text_parts\n\n                anthropic_messages.append(\n                    MessageParam(\n                        role=\"user\",\n                        content=[\n                            ToolResultBlockParam(\n                                type=\"tool_result\",\n                                tool_use_id=content.toolUseId,\n                                content=result_content_str,\n                                is_error=content.isError if content.isError else False,\n                            )\n                        ],\n                    )\n                )\n                continue\n\n            # Handle TextContent\n            if isinstance(content, TextContent):\n                anthropic_messages.append(\n                    MessageParam(\n                        role=message.role,\n                        content=content.text,\n                    )\n                )\n                continue\n\n            # Handle ImageContent\n            if isinstance(content, ImageContent):\n                if message.role != \"user\":\n                    raise ValueError(\n                        \"ImageContent is only supported in user messages for Anthropic\"\n                    )\n                anthropic_messages.append(\n                    MessageParam(\n                        role=\"user\",\n                        content=[_image_content_to_anthropic_block(content)],\n                    )\n                )\n                continue\n\n            # Handle AudioContent - not supported by Anthropic\n            if isinstance(content, AudioContent):\n                raise ValueError(\"AudioContent is not supported by the Anthropic API\")\n\n            raise ValueError(f\"Unsupported content type: {type(content)}\")\n\n        return anthropic_messages\n\n    @staticmethod\n    def _message_to_create_message_result(\n        message: Message,\n    ) -> CreateMessageResult:\n        if len(message.content) == 0:\n            raise ValueError(\"No content in response from Anthropic\")\n\n        # Join all text blocks to avoid dropping content\n        text = \"\".join(\n            block.text for block in message.content if isinstance(block, TextBlock)\n        )\n        if text:\n            return CreateMessageResult(\n                content=TextContent(type=\"text\", text=text),\n                role=\"assistant\",\n                model=message.model,\n            )\n\n        raise ValueError(\n            f\"No text content in response from Anthropic: {[type(b).__name__ for b in message.content]}\"\n        )\n\n    def _select_model_from_preferences(\n        self, model_preferences: ModelPreferences | str | list[str] | None\n    ) -> ModelParam:\n        for model_option in self._iter_models_from_preferences(model_preferences):\n            # Accept any model that starts with \"claude\"\n            if model_option.startswith(\"claude\"):\n                return model_option\n\n        return self.default_model\n\n    @staticmethod\n    def _convert_tools_to_anthropic(tools: list[Tool]) -> list[ToolParam]:\n        \"\"\"Convert MCP tools to Anthropic tool format.\"\"\"\n        anthropic_tools: list[ToolParam] = []\n        for tool in tools:\n            # Build input_schema dict, ensuring required fields\n            input_schema: dict[str, Any] = dict(tool.inputSchema)\n            if \"type\" not in input_schema:\n                input_schema[\"type\"] = \"object\"\n\n            anthropic_tools.append(\n                ToolParam(\n                    name=tool.name,\n                    description=tool.description or \"\",\n                    input_schema=input_schema,\n                )\n            )\n        return anthropic_tools\n\n    @staticmethod\n    def _convert_tool_choice_to_anthropic(\n        tool_choice: ToolChoice,\n    ) -> ToolChoiceParam | None:\n        \"\"\"Convert MCP tool_choice to Anthropic format.\n\n        Returns None for \"none\" mode, signaling that tools should be omitted\n        from the request entirely (Anthropic doesn't have an explicit \"none\" option).\n        \"\"\"\n        if tool_choice.mode == \"auto\":\n            return ToolChoiceAutoParam(type=\"auto\")\n        elif tool_choice.mode == \"required\":\n            return ToolChoiceAnyParam(type=\"any\")\n        elif tool_choice.mode == \"none\":\n            # Anthropic doesn't have a \"none\" option - return None to signal\n            # that tools should be omitted from the request entirely\n            return None\n        else:\n            raise ValueError(f\"Unsupported tool_choice mode: {tool_choice.mode!r}\")\n\n    @staticmethod\n    def _message_to_result_with_tools(\n        message: Message,\n    ) -> CreateMessageResultWithTools:\n        \"\"\"Convert Anthropic response to CreateMessageResultWithTools.\"\"\"\n        if len(message.content) == 0:\n            raise ValueError(\"No content in response from Anthropic\")\n\n        # Determine stop reason\n        stop_reason: StopReason\n        if message.stop_reason == \"tool_use\":\n            stop_reason = \"toolUse\"\n        elif message.stop_reason == \"end_turn\":\n            stop_reason = \"endTurn\"\n        elif message.stop_reason == \"max_tokens\":\n            stop_reason = \"maxTokens\"\n        elif message.stop_reason == \"stop_sequence\":\n            stop_reason = \"endTurn\"\n        else:\n            stop_reason = \"endTurn\"\n\n        # Build content list\n        content: list[SamplingMessageContentBlock] = []\n\n        for block in message.content:\n            if isinstance(block, TextBlock):\n                content.append(TextContent(type=\"text\", text=block.text))\n            elif isinstance(block, ToolUseBlock):\n                # Anthropic returns input as dict directly\n                arguments = block.input if isinstance(block.input, dict) else {}\n\n                content.append(\n                    ToolUseContent(\n                        type=\"tool_use\",\n                        id=block.id,\n                        name=block.name,\n                        input=arguments,\n                    )\n                )\n\n        # Must have at least some content\n        if not content:\n            raise ValueError(\"No content in response from Anthropic\")\n\n        return CreateMessageResultWithTools(\n            content=content,\n            role=\"assistant\",\n            model=message.model,\n            stopReason=stop_reason,\n        )\n"
  },
  {
    "path": "src/fastmcp/client/sampling/handlers/google_genai.py",
    "content": "\"\"\"Google GenAI sampling handler with tool support for FastMCP 3.0.\"\"\"\n\nimport base64\nfrom collections.abc import Sequence\nfrom uuid import uuid4\n\ntry:\n    from google.genai import Client as GoogleGenaiClient\n    from google.genai.types import (\n        Blob,\n        Candidate,\n        Content,\n        FunctionCall,\n        FunctionCallingConfig,\n        FunctionCallingConfigMode,\n        FunctionDeclaration,\n        FunctionResponse,\n        GenerateContentConfig,\n        GenerateContentResponse,\n        ModelContent,\n        Part,\n        ThinkingConfig,\n        ToolConfig,\n        UserContent,\n    )\n    from google.genai.types import Tool as GoogleTool\nexcept ImportError as e:\n    raise ImportError(\n        \"The `google-genai` package is not installed. \"\n        \"Install it with `pip install fastmcp[gemini]` or add `google-genai` \"\n        \"to your dependencies.\"\n    ) from e\n\nfrom mcp import ClientSession, ServerSession\nfrom mcp.shared.context import LifespanContextT, RequestContext\nfrom mcp.types import (\n    AudioContent,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    ImageContent,\n    ModelPreferences,\n    SamplingMessage,\n    SamplingMessageContentBlock,\n    StopReason,\n    TextContent,\n    ToolChoice,\n    ToolResultContent,\n    ToolUseContent,\n)\nfrom mcp.types import CreateMessageRequestParams as SamplingParams\nfrom mcp.types import Tool as MCPTool\n\n__all__ = [\"GoogleGenaiSamplingHandler\"]\n\n\nclass GoogleGenaiSamplingHandler:\n    \"\"\"Sampling handler that uses the Google GenAI API with tool support.\n\n    Example:\n        ```python\n        from google.genai import Client\n        from fastmcp import FastMCP\n        from fastmcp.client.sampling.handlers.google_genai import (\n            GoogleGenaiSamplingHandler,\n        )\n\n        handler = GoogleGenaiSamplingHandler(\n            default_model=\"gemini-2.0-flash\",\n            client=Client(),\n        )\n\n        server = FastMCP(sampling_handler=handler)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        default_model: str,\n        client: GoogleGenaiClient | None = None,\n        thinking_budget: int | None = None,\n    ) -> None:\n        self.client: GoogleGenaiClient = client or GoogleGenaiClient()\n        self.default_model: str = default_model\n        self.thinking_budget: int | None = thinking_budget\n\n    async def __call__(\n        self,\n        messages: list[SamplingMessage],\n        params: SamplingParams,\n        context: RequestContext[ServerSession, LifespanContextT]\n        | RequestContext[ClientSession, LifespanContextT],\n    ) -> CreateMessageResult | CreateMessageResultWithTools:\n        contents: list[Content] = _convert_messages_to_google_genai_content(messages)\n\n        # Convert MCP tools to Google GenAI format\n        google_tools: list[GoogleTool] | None = None\n        tool_config: ToolConfig | None = None\n\n        if params.tools:\n            google_tools = [\n                _convert_tool_to_google_genai(tool) for tool in params.tools\n            ]\n            tool_config = _convert_tool_choice_to_google_genai(params.toolChoice)\n\n        # Select the model based on preferences\n        selected_model = self._get_model(model_preferences=params.modelPreferences)\n\n        # Configure thinking if a budget is specified\n        thinking_config = (\n            ThinkingConfig(thinking_budget=self.thinking_budget)\n            if self.thinking_budget is not None\n            else None\n        )\n\n        response: GenerateContentResponse = (\n            await self.client.aio.models.generate_content(\n                model=selected_model,\n                contents=contents,\n                config=GenerateContentConfig(\n                    system_instruction=params.systemPrompt,\n                    temperature=params.temperature,\n                    max_output_tokens=params.maxTokens,\n                    stop_sequences=params.stopSequences,\n                    thinking_config=thinking_config,\n                    tools=google_tools,  # ty: ignore[invalid-argument-type]\n                    tool_config=tool_config,\n                ),\n            )\n        )\n\n        # Return appropriate result type based on whether tools were provided\n        if params.tools:\n            return _response_to_result_with_tools(response, selected_model)\n        return _response_to_create_message_result(response, selected_model)\n\n    def _get_model(self, model_preferences: ModelPreferences | None) -> str:\n        if model_preferences and model_preferences.hints:\n            for hint in model_preferences.hints:\n                if hint.name and hint.name.startswith(\"gemini\"):\n                    return hint.name\n        return self.default_model\n\n\ndef _convert_tool_to_google_genai(tool: MCPTool) -> GoogleTool:\n    \"\"\"Convert an MCP Tool to Google GenAI format.\n\n    Google's parameters_json_schema accepts standard JSON Schema format,\n    so we pass tool.inputSchema directly without conversion.\n    \"\"\"\n    return GoogleTool(\n        function_declarations=[\n            FunctionDeclaration(\n                name=tool.name,\n                description=tool.description or \"\",\n                parameters_json_schema=tool.inputSchema,\n            )\n        ]\n    )\n\n\ndef _convert_tool_choice_to_google_genai(tool_choice: ToolChoice | None) -> ToolConfig:\n    \"\"\"Convert MCP ToolChoice to Google GenAI ToolConfig.\"\"\"\n    if tool_choice is None:\n        return ToolConfig(\n            function_calling_config=FunctionCallingConfig(\n                mode=FunctionCallingConfigMode.AUTO\n            )\n        )\n\n    if tool_choice.mode == \"required\":\n        return ToolConfig(\n            function_calling_config=FunctionCallingConfig(\n                mode=FunctionCallingConfigMode.ANY\n            )\n        )\n    if tool_choice.mode == \"none\":\n        return ToolConfig(\n            function_calling_config=FunctionCallingConfig(\n                mode=FunctionCallingConfigMode.NONE\n            )\n        )\n\n    # Default to AUTO for \"auto\" or any other value\n    return ToolConfig(\n        function_calling_config=FunctionCallingConfig(\n            mode=FunctionCallingConfigMode.AUTO\n        )\n    )\n\n\ndef _sampling_content_to_google_genai_part(\n    content: TextContent\n    | ImageContent\n    | AudioContent\n    | ToolUseContent\n    | ToolResultContent,\n) -> Part:\n    \"\"\"Convert MCP content to Google GenAI Part.\"\"\"\n    if isinstance(content, TextContent):\n        return Part(text=content.text)\n\n    if isinstance(content, ImageContent):\n        return Part(\n            inline_data=Blob(\n                data=base64.b64decode(content.data),\n                mime_type=content.mimeType,\n            )\n        )\n\n    if isinstance(content, AudioContent):\n        return Part(\n            inline_data=Blob(\n                data=base64.b64decode(content.data),\n                mime_type=content.mimeType,\n            )\n        )\n\n    if isinstance(content, ToolUseContent):\n        # Note: thought_signature bypass is required for manually constructed tool calls.\n        # Google's Gemini 3+ models enforce thought signature validation for function calls.\n        # Since we're constructing these Parts from MCP protocol data (not from model responses),\n        # they lack legitimate signatures. The bypass value allows validation to pass.\n        # See: https://ai.google.dev/gemini-api/docs/thought-signatures\n        return Part(\n            function_call=FunctionCall(\n                name=content.name,\n                args=content.input,\n            ),\n            thought_signature=b\"skip_thought_signature_validator\",\n        )\n\n    if isinstance(content, ToolResultContent):\n        # Extract text from tool result content\n        result_parts: list[str] = []\n        if content.content:\n            for item in content.content:\n                if isinstance(item, TextContent):\n                    result_parts.append(item.text)\n                else:\n                    msg = f\"Unsupported tool result content type: {type(item).__name__}\"\n                    raise ValueError(msg)\n        result_text = \"\".join(result_parts)\n\n        # Extract function name from toolUseId\n        # Our IDs are formatted as \"{function_name}_{uuid8}\", so extract the name.\n        # Note: This is a limitation of MCP's ToolResultContent which only carries\n        # toolUseId, while Google's FunctionResponse requires the function name.\n        tool_use_id = content.toolUseId\n        if \"_\" in tool_use_id:\n            # Split and rejoin all but the last part (the UUID suffix)\n            parts = tool_use_id.rsplit(\"_\", 1)\n            function_name = parts[0]\n        else:\n            # Fallback: use the full ID as the name\n            function_name = tool_use_id\n\n        return Part(\n            function_response=FunctionResponse(\n                name=function_name,\n                response={\"result\": result_text},\n            )\n        )\n\n    msg = f\"Unsupported content type: {type(content)}\"\n    raise ValueError(msg)\n\n\ndef _convert_messages_to_google_genai_content(\n    messages: Sequence[SamplingMessage],\n) -> list[Content]:\n    \"\"\"Convert MCP messages to Google GenAI content.\"\"\"\n    google_messages: list[Content] = []\n\n    for message in messages:\n        content = message.content\n\n        # Handle list content (tool calls + results)\n        if isinstance(content, list):\n            parts: list[Part] = []\n            for item in content:\n                parts.append(_sampling_content_to_google_genai_part(item))\n\n            if message.role == \"user\":\n                google_messages.append(UserContent(parts=parts))\n            elif message.role == \"assistant\":\n                google_messages.append(ModelContent(parts=parts))\n            else:\n                msg = f\"Invalid message role: {message.role}\"\n                raise ValueError(msg)\n            continue\n\n        # Handle single content item\n        part = _sampling_content_to_google_genai_part(content)\n\n        if message.role == \"user\":\n            google_messages.append(UserContent(parts=[part]))\n        elif message.role == \"assistant\":\n            google_messages.append(ModelContent(parts=[part]))\n        else:\n            msg = f\"Invalid message role: {message.role}\"\n            raise ValueError(msg)\n\n    return google_messages\n\n\ndef _get_candidate_from_response(response: GenerateContentResponse) -> Candidate:\n    \"\"\"Extract the first candidate from a response.\"\"\"\n    if response.candidates and response.candidates[0]:\n        return response.candidates[0]\n    msg = \"No candidate in response from completion.\"\n    raise ValueError(msg)\n\n\ndef _response_to_create_message_result(\n    response: GenerateContentResponse,\n    model: str,\n) -> CreateMessageResult:\n    \"\"\"Convert Google GenAI response to CreateMessageResult (no tools).\"\"\"\n    if not (text := response.text):\n        candidate = _get_candidate_from_response(response)\n        msg = f\"No content in response: {candidate.finish_reason}\"\n        raise ValueError(msg)\n\n    return CreateMessageResult(\n        content=TextContent(type=\"text\", text=text),\n        role=\"assistant\",\n        model=model,\n    )\n\n\ndef _response_to_result_with_tools(\n    response: GenerateContentResponse,\n    model: str,\n) -> CreateMessageResultWithTools:\n    \"\"\"Convert Google GenAI response to CreateMessageResultWithTools.\"\"\"\n    candidate = _get_candidate_from_response(response)\n\n    # Determine stop reason and check for function calls\n    stop_reason: StopReason\n    finish_reason = candidate.finish_reason\n    has_function_calls = False\n\n    if candidate.content and candidate.content.parts:\n        for part in candidate.content.parts:\n            if part.function_call is not None:\n                has_function_calls = True\n                break\n\n    if has_function_calls:\n        stop_reason = \"toolUse\"\n    elif finish_reason == \"STOP\":\n        stop_reason = \"endTurn\"\n    elif finish_reason == \"MAX_TOKENS\":\n        stop_reason = \"maxTokens\"\n    else:\n        stop_reason = \"endTurn\"\n\n    # Build content list\n    content: list[SamplingMessageContentBlock] = []\n\n    if candidate.content and candidate.content.parts:\n        for part in candidate.content.parts:\n            # Note: Skip thought parts from thinking_config - not relevant for MCP responses\n            if part.text:\n                content.append(TextContent(type=\"text\", text=part.text))\n            elif part.function_call is not None:\n                fc = part.function_call\n                fc_name: str = fc.name or \"unknown\"\n                content.append(\n                    ToolUseContent(\n                        type=\"tool_use\",\n                        id=f\"{fc_name}_{uuid4().hex[:8]}\",  # Generate unique ID\n                        name=fc_name,\n                        input=dict(fc.args) if fc.args else {},\n                    )\n                )\n\n    if not content:\n        raise ValueError(\"No content in response from completion\")\n\n    return CreateMessageResultWithTools(\n        content=content,\n        role=\"assistant\",\n        model=model,\n        stopReason=stop_reason,\n    )\n"
  },
  {
    "path": "src/fastmcp/client/sampling/handlers/openai.py",
    "content": "\"\"\"OpenAI sampling handler for FastMCP.\"\"\"\n\nimport json\nfrom collections.abc import Iterator, Sequence\nfrom typing import Any, get_args\n\nfrom mcp import ClientSession, ServerSession\nfrom mcp.shared.context import LifespanContextT, RequestContext\nfrom mcp.types import (\n    AudioContent,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    ImageContent,\n    ModelPreferences,\n    SamplingMessage,\n    StopReason,\n    TextContent,\n    Tool,\n    ToolChoice,\n    ToolResultContent,\n    ToolUseContent,\n)\nfrom mcp.types import CreateMessageRequestParams as SamplingParams\n\ntry:\n    from openai import AsyncOpenAI\n    from openai.types.chat import (\n        ChatCompletion,\n        ChatCompletionAssistantMessageParam,\n        ChatCompletionContentPartImageParam,\n        ChatCompletionContentPartInputAudioParam,\n        ChatCompletionContentPartParam,\n        ChatCompletionContentPartTextParam,\n        ChatCompletionMessageParam,\n        ChatCompletionMessageToolCallParam,\n        ChatCompletionSystemMessageParam,\n        ChatCompletionToolChoiceOptionParam,\n        ChatCompletionToolMessageParam,\n        ChatCompletionToolParam,\n        ChatCompletionUserMessageParam,\n    )\n    from openai.types.shared.chat_model import ChatModel\n    from openai.types.shared_params import FunctionDefinition\nexcept ImportError as e:\n    raise ImportError(\n        \"The `openai` package is not installed. \"\n        \"Please install `fastmcp[openai]` or add `openai` to your dependencies manually.\"\n    ) from e\n\n# OpenAI only supports wav and mp3 for input audio\n_OPENAI_AUDIO_FORMATS: dict[str, str] = {\n    \"audio/wav\": \"wav\",\n    \"audio/x-wav\": \"wav\",\n    \"audio/mp3\": \"mp3\",\n    \"audio/mpeg\": \"mp3\",\n}\n\n_OPENAI_IMAGE_MEDIA_TYPES: frozenset[str] = frozenset(\n    {\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"}\n)\n\n\ndef _image_content_to_openai_part(\n    content: ImageContent,\n) -> ChatCompletionContentPartImageParam:\n    \"\"\"Convert MCP ImageContent to OpenAI image_url content part.\"\"\"\n    if content.mimeType not in _OPENAI_IMAGE_MEDIA_TYPES:\n        raise ValueError(\n            f\"Unsupported image MIME type for OpenAI: {content.mimeType!r}. \"\n            f\"Supported types: {', '.join(sorted(_OPENAI_IMAGE_MEDIA_TYPES))}\"\n        )\n    data_url = f\"data:{content.mimeType};base64,{content.data}\"\n    return ChatCompletionContentPartImageParam(\n        type=\"image_url\",\n        image_url={\"url\": data_url},\n    )\n\n\ndef _audio_content_to_openai_part(\n    content: AudioContent,\n) -> ChatCompletionContentPartInputAudioParam:\n    \"\"\"Convert MCP AudioContent to OpenAI input_audio content part.\"\"\"\n    audio_format = _OPENAI_AUDIO_FORMATS.get(content.mimeType)\n    if audio_format is None:\n        raise ValueError(\n            f\"Unsupported audio MIME type for OpenAI: {content.mimeType!r}. \"\n            f\"Supported types: {', '.join(sorted(_OPENAI_AUDIO_FORMATS))}\"\n        )\n    return ChatCompletionContentPartInputAudioParam(\n        type=\"input_audio\",\n        input_audio={\"data\": content.data, \"format\": audio_format},\n    )\n\n\nclass OpenAISamplingHandler:\n    \"\"\"Sampling handler that uses the OpenAI API.\"\"\"\n\n    def __init__(\n        self,\n        default_model: ChatModel,\n        client: AsyncOpenAI | None = None,\n    ) -> None:\n        self.client: AsyncOpenAI = client or AsyncOpenAI()\n        self.default_model: ChatModel = default_model\n\n    async def __call__(\n        self,\n        messages: list[SamplingMessage],\n        params: SamplingParams,\n        context: RequestContext[ServerSession, LifespanContextT]\n        | RequestContext[ClientSession, LifespanContextT],\n    ) -> CreateMessageResult | CreateMessageResultWithTools:\n        openai_messages: list[ChatCompletionMessageParam] = (\n            self._convert_to_openai_messages(\n                system_prompt=params.systemPrompt,\n                messages=messages,\n            )\n        )\n\n        model: ChatModel = self._select_model_from_preferences(params.modelPreferences)\n\n        # Convert MCP tools to OpenAI format\n        openai_tools: list[ChatCompletionToolParam] | None = None\n        if params.tools:\n            openai_tools = self._convert_tools_to_openai(params.tools)\n\n        # Convert tool_choice to OpenAI format\n        openai_tool_choice: ChatCompletionToolChoiceOptionParam | None = None\n        if params.toolChoice:\n            openai_tool_choice = self._convert_tool_choice_to_openai(params.toolChoice)\n\n        # Build kwargs to avoid sentinel type compatibility issues across\n        # openai SDK versions (NotGiven vs Omit)\n        kwargs: dict[str, Any] = {\n            \"model\": model,\n            \"messages\": openai_messages,\n        }\n        if params.maxTokens is not None:\n            kwargs[\"max_completion_tokens\"] = params.maxTokens\n        if params.temperature is not None:\n            kwargs[\"temperature\"] = params.temperature\n        if params.stopSequences:\n            kwargs[\"stop\"] = params.stopSequences\n        if openai_tools is not None:\n            kwargs[\"tools\"] = openai_tools\n        if openai_tool_choice is not None:\n            kwargs[\"tool_choice\"] = openai_tool_choice\n\n        response = await self.client.chat.completions.create(**kwargs)\n\n        # Return appropriate result type based on whether tools were provided\n        if params.tools:\n            return self._chat_completion_to_result_with_tools(response)\n        return self._chat_completion_to_create_message_result(response)\n\n    @staticmethod\n    def _iter_models_from_preferences(\n        model_preferences: ModelPreferences | str | list[str] | None,\n    ) -> Iterator[str]:\n        if model_preferences is None:\n            return\n\n        if isinstance(model_preferences, str) and model_preferences in get_args(\n            ChatModel\n        ):\n            yield model_preferences\n\n        elif isinstance(model_preferences, list):\n            yield from model_preferences\n\n        elif isinstance(model_preferences, ModelPreferences):\n            if not (hints := model_preferences.hints):\n                return\n\n            for hint in hints:\n                if not (name := hint.name):\n                    continue\n\n                yield name\n\n    @staticmethod\n    def _convert_to_openai_messages(\n        system_prompt: str | None, messages: Sequence[SamplingMessage]\n    ) -> list[ChatCompletionMessageParam]:\n        openai_messages: list[ChatCompletionMessageParam] = []\n\n        if system_prompt:\n            openai_messages.append(\n                ChatCompletionSystemMessageParam(\n                    role=\"system\",\n                    content=system_prompt,\n                )\n            )\n\n        for message in messages:\n            content = message.content\n\n            # Handle list content (from CreateMessageResultWithTools)\n            if isinstance(content, list):\n                # Collect tool calls, content parts, and text from the list\n                tool_calls: list[ChatCompletionMessageToolCallParam] = []\n                content_parts: list[ChatCompletionContentPartParam] = []\n                text_parts: list[str] = []\n                # Collect tool results separately to maintain correct ordering\n                tool_messages: list[ChatCompletionToolMessageParam] = []\n\n                for item in content:\n                    if isinstance(item, ToolUseContent):\n                        tool_calls.append(\n                            ChatCompletionMessageToolCallParam(\n                                id=item.id,\n                                type=\"function\",\n                                function={\n                                    \"name\": item.name,\n                                    \"arguments\": json.dumps(item.input),\n                                },\n                            )\n                        )\n                    elif isinstance(item, TextContent):\n                        text_parts.append(item.text)\n                        content_parts.append(\n                            ChatCompletionContentPartTextParam(\n                                type=\"text\", text=item.text\n                            )\n                        )\n                    elif isinstance(item, ImageContent):\n                        content_parts.append(_image_content_to_openai_part(item))\n                    elif isinstance(item, AudioContent):\n                        content_parts.append(_audio_content_to_openai_part(item))\n                    elif isinstance(item, ToolResultContent):\n                        # Collect tool results (added after assistant message)\n                        content_text = \"\"\n                        if item.content:\n                            result_texts = []\n                            for sub_item in item.content:\n                                if isinstance(sub_item, TextContent):\n                                    result_texts.append(sub_item.text)\n                            content_text = \"\\n\".join(result_texts)\n                        tool_messages.append(\n                            ChatCompletionToolMessageParam(\n                                role=\"tool\",\n                                tool_call_id=item.toolUseId,\n                                content=content_text,\n                            )\n                        )\n\n                # Add assistant message with tool calls if present\n                # OpenAI requires: assistant (with tool_calls) -> tool messages\n                if tool_calls or content_parts:\n                    if tool_calls:\n                        has_multimodal = len(content_parts) > len(text_parts)\n                        if has_multimodal:\n                            raise ValueError(\n                                \"ImageContent/AudioContent is only supported \"\n                                \"in user messages for OpenAI\"\n                            )\n                        text_str = \"\\n\".join(text_parts) or None\n                        openai_messages.append(\n                            ChatCompletionAssistantMessageParam(\n                                role=\"assistant\",\n                                content=text_str,\n                                tool_calls=tool_calls,\n                            )\n                        )\n                        # Add tool messages AFTER assistant message\n                        openai_messages.extend(tool_messages)\n                    elif content_parts:\n                        if message.role == \"user\":\n                            openai_messages.append(\n                                ChatCompletionUserMessageParam(\n                                    role=\"user\",\n                                    content=content_parts,\n                                )\n                            )\n                        else:\n                            has_multimodal = len(content_parts) > len(text_parts)\n                            if has_multimodal:\n                                raise ValueError(\n                                    \"ImageContent/AudioContent is only supported \"\n                                    \"in user messages for OpenAI\"\n                                )\n                            assistant_text = \"\\n\".join(text_parts)\n                            if assistant_text:\n                                openai_messages.append(\n                                    ChatCompletionAssistantMessageParam(\n                                        role=\"assistant\",\n                                        content=assistant_text,\n                                    )\n                                )\n                elif tool_messages:\n                    # Tool results only (assistant message was in previous message)\n                    openai_messages.extend(tool_messages)\n                continue\n\n            # Handle ToolUseContent (assistant's tool calls)\n            if isinstance(content, ToolUseContent):\n                openai_messages.append(\n                    ChatCompletionAssistantMessageParam(\n                        role=\"assistant\",\n                        tool_calls=[\n                            ChatCompletionMessageToolCallParam(\n                                id=content.id,\n                                type=\"function\",\n                                function={\n                                    \"name\": content.name,\n                                    \"arguments\": json.dumps(content.input),\n                                },\n                            )\n                        ],\n                    )\n                )\n                continue\n\n            # Handle ToolResultContent (user's tool results)\n            if isinstance(content, ToolResultContent):\n                # Extract text parts from the content list\n                result_texts: list[str] = []\n                if content.content:\n                    for item in content.content:\n                        if isinstance(item, TextContent):\n                            result_texts.append(item.text)\n                openai_messages.append(\n                    ChatCompletionToolMessageParam(\n                        role=\"tool\",\n                        tool_call_id=content.toolUseId,\n                        content=\"\\n\".join(result_texts),\n                    )\n                )\n                continue\n\n            # Handle TextContent\n            if isinstance(content, TextContent):\n                if message.role == \"user\":\n                    openai_messages.append(\n                        ChatCompletionUserMessageParam(\n                            role=\"user\",\n                            content=content.text,\n                        )\n                    )\n                else:\n                    openai_messages.append(\n                        ChatCompletionAssistantMessageParam(\n                            role=\"assistant\",\n                            content=content.text,\n                        )\n                    )\n                continue\n\n            # Handle ImageContent\n            if isinstance(content, ImageContent):\n                if message.role != \"user\":\n                    raise ValueError(\n                        \"ImageContent is only supported in user messages for OpenAI\"\n                    )\n                openai_messages.append(\n                    ChatCompletionUserMessageParam(\n                        role=\"user\",\n                        content=[_image_content_to_openai_part(content)],\n                    )\n                )\n                continue\n\n            # Handle AudioContent\n            if isinstance(content, AudioContent):\n                if message.role != \"user\":\n                    raise ValueError(\n                        \"AudioContent is only supported in user messages for OpenAI\"\n                    )\n                openai_messages.append(\n                    ChatCompletionUserMessageParam(\n                        role=\"user\",\n                        content=[_audio_content_to_openai_part(content)],\n                    )\n                )\n                continue\n\n            raise ValueError(f\"Unsupported content type: {type(content)}\")\n\n        return openai_messages\n\n    @staticmethod\n    def _chat_completion_to_create_message_result(\n        chat_completion: ChatCompletion,\n    ) -> CreateMessageResult:\n        if len(chat_completion.choices) == 0:\n            raise ValueError(\"No response for completion\")\n\n        first_choice = chat_completion.choices[0]\n\n        if content := first_choice.message.content:\n            return CreateMessageResult(\n                content=TextContent(type=\"text\", text=content),\n                role=\"assistant\",\n                model=chat_completion.model,\n            )\n\n        raise ValueError(\"No content in response from completion\")\n\n    def _select_model_from_preferences(\n        self, model_preferences: ModelPreferences | str | list[str] | None\n    ) -> ChatModel:\n        for model_option in self._iter_models_from_preferences(model_preferences):\n            if model_option in get_args(ChatModel):\n                chosen_model: ChatModel = model_option  # type: ignore[assignment]\n                return chosen_model\n\n        return self.default_model\n\n    @staticmethod\n    def _convert_tools_to_openai(tools: list[Tool]) -> list[ChatCompletionToolParam]:\n        \"\"\"Convert MCP tools to OpenAI tool format.\"\"\"\n        openai_tools: list[ChatCompletionToolParam] = []\n        for tool in tools:\n            # Build parameters dict, ensuring required fields\n            parameters: dict[str, Any] = dict(tool.inputSchema)\n            if \"type\" not in parameters:\n                parameters[\"type\"] = \"object\"\n\n            openai_tools.append(\n                ChatCompletionToolParam(\n                    type=\"function\",\n                    function=FunctionDefinition(\n                        name=tool.name,\n                        description=tool.description or \"\",\n                        parameters=parameters,\n                    ),\n                )\n            )\n        return openai_tools\n\n    @staticmethod\n    def _convert_tool_choice_to_openai(\n        tool_choice: ToolChoice,\n    ) -> ChatCompletionToolChoiceOptionParam:\n        \"\"\"Convert MCP tool_choice to OpenAI format.\"\"\"\n        if tool_choice.mode == \"auto\":\n            return \"auto\"\n        elif tool_choice.mode == \"required\":\n            return \"required\"\n        elif tool_choice.mode == \"none\":\n            return \"none\"\n        else:\n            raise ValueError(f\"Unsupported tool_choice mode: {tool_choice.mode!r}\")\n\n    @staticmethod\n    def _chat_completion_to_result_with_tools(\n        chat_completion: ChatCompletion,\n    ) -> CreateMessageResultWithTools:\n        \"\"\"Convert OpenAI response to CreateMessageResultWithTools.\"\"\"\n        if len(chat_completion.choices) == 0:\n            raise ValueError(\"No response for completion\")\n\n        first_choice = chat_completion.choices[0]\n        message = first_choice.message\n\n        # Determine stop reason\n        stop_reason: StopReason\n        if first_choice.finish_reason == \"tool_calls\":\n            stop_reason = \"toolUse\"\n        elif first_choice.finish_reason == \"stop\":\n            stop_reason = \"endTurn\"\n        elif first_choice.finish_reason == \"length\":\n            stop_reason = \"maxTokens\"\n        else:\n            stop_reason = \"endTurn\"\n\n        # Build content list\n        content: list[TextContent | ToolUseContent] = []\n\n        # Add text content if present\n        if message.content:\n            content.append(TextContent(type=\"text\", text=message.content))\n\n        # Add tool calls if present\n        if message.tool_calls:\n            for tool_call in message.tool_calls:\n                # Skip non-function tool calls\n                if not hasattr(tool_call, \"function\"):\n                    continue\n                func = tool_call.function\n                # Parse the arguments JSON string\n                try:\n                    arguments = json.loads(func.arguments)  # type: ignore[union-attr]\n                except json.JSONDecodeError as e:\n                    raise ValueError(\n                        f\"Invalid JSON in tool arguments for \"\n                        f\"'{func.name}': {func.arguments}\"  # type: ignore[union-attr]\n                    ) from e\n\n                content.append(\n                    ToolUseContent(\n                        type=\"tool_use\",\n                        id=tool_call.id,\n                        name=func.name,  # type: ignore[union-attr]\n                        input=arguments,\n                    )\n                )\n\n        # Must have at least some content\n        if not content:\n            raise ValueError(\"No content in response from completion\")\n\n        return CreateMessageResultWithTools(\n            content=content,  # type: ignore[arg-type]\n            role=\"assistant\",\n            model=chat_completion.model,\n            stopReason=stop_reason,\n        )\n"
  },
  {
    "path": "src/fastmcp/client/tasks.py",
    "content": "\"\"\"SEP-1686 client Task classes.\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport asyncio\nimport inspect\nimport time\nimport weakref\nfrom collections.abc import Awaitable, Callable\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Generic, TypeVar\n\nimport mcp.types\nfrom mcp.types import GetTaskResult, TaskStatusNotification\n\nfrom fastmcp.client.messages import Message, MessageHandler\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nif TYPE_CHECKING:\n    from fastmcp.client.client import CallToolResult, Client\n\n\nclass TaskNotificationHandler(MessageHandler):\n    \"\"\"MessageHandler that routes task status notifications to Task objects.\"\"\"\n\n    def __init__(self, client: Client):\n        super().__init__()\n        self._client_ref: weakref.ref[Client] = weakref.ref(client)\n\n    async def dispatch(self, message: Message) -> None:\n        \"\"\"Dispatch messages, including task status notifications.\"\"\"\n        if isinstance(message, mcp.types.ServerNotification):\n            if isinstance(message.root, TaskStatusNotification):\n                client = self._client_ref()\n                if client:\n                    client._handle_task_status_notification(message.root)\n\n        await super().dispatch(message)\n\n\nTaskResultT = TypeVar(\"TaskResultT\")\n\n\nclass Task(abc.ABC, Generic[TaskResultT]):\n    \"\"\"\n    Abstract base class for MCP background tasks (SEP-1686).\n\n    Provides a uniform API whether the server accepts background execution\n    or executes synchronously (graceful degradation per SEP-1686).\n\n    Subclasses:\n        - ToolTask: For tool calls (result type: CallToolResult)\n        - PromptTask: For prompts (future, result type: GetPromptResult)\n        - ResourceTask: For resources (future, result type: ReadResourceResult)\n    \"\"\"\n\n    def __init__(\n        self,\n        client: Client,\n        task_id: str,\n        immediate_result: TaskResultT | None = None,\n    ):\n        \"\"\"\n        Create a Task wrapper.\n\n        Args:\n            client: The FastMCP client\n            task_id: The task identifier\n            immediate_result: If server executed synchronously, the immediate result\n        \"\"\"\n        self._client = client\n        self._task_id = task_id\n        self._immediate_result = immediate_result\n        self._is_immediate = immediate_result is not None\n\n        # Notification-based optimization (SEP-1686 notifications/tasks/status)\n        self._status_cache: GetTaskResult | None = None\n        self._status_event: asyncio.Event | None = None  # Lazy init\n        self._status_callbacks: list[\n            Callable[[GetTaskResult], None | Awaitable[None]]\n        ] = []\n        self._cached_result: TaskResultT | None = None\n\n    def _check_client_connected(self) -> None:\n        \"\"\"Validate that client context is still active.\n\n        Raises:\n            RuntimeError: If accessed outside client context (unless immediate)\n        \"\"\"\n        if self._is_immediate:\n            return  # Already resolved, no client needed\n\n        try:\n            _ = self._client.session\n        except RuntimeError as e:\n            raise RuntimeError(\n                \"Cannot access task results outside client context. \"\n                \"Task futures must be used within 'async with client:' block.\"\n            ) from e\n\n    @property\n    def task_id(self) -> str:\n        \"\"\"Get the task ID.\"\"\"\n        return self._task_id\n\n    @property\n    def returned_immediately(self) -> bool:\n        \"\"\"Check if server executed the task immediately.\n\n        Returns:\n            True if server executed synchronously (graceful degradation or no task support)\n            False if server accepted background execution\n        \"\"\"\n        return self._is_immediate\n\n    def _handle_status_notification(self, status: GetTaskResult) -> None:\n        \"\"\"Process incoming notifications/tasks/status (internal).\n\n        Called by Client when a notification is received for this task.\n        Updates cache, triggers events, and invokes user callbacks.\n\n        Args:\n            status: Task status from notification\n        \"\"\"\n        # Update cache for next status() call\n        self._status_cache = status\n\n        # Wake up any wait() calls\n        if self._status_event is not None:\n            self._status_event.set()\n\n        # Invoke user callbacks\n        for callback in self._status_callbacks:\n            try:\n                result = callback(status)\n                if inspect.isawaitable(result):\n                    # Fire and forget async callbacks\n                    asyncio.create_task(result)  # type: ignore[arg-type] # noqa: RUF006\n            except Exception as e:\n                logger.warning(f\"Task callback error: {e}\", exc_info=True)\n\n    def on_status_change(\n        self,\n        callback: Callable[[GetTaskResult], None | Awaitable[None]],\n    ) -> None:\n        \"\"\"Register callback for status change notifications.\n\n        The callback will be invoked when a notifications/tasks/status is received\n        for this task (optional server feature per SEP-1686 lines 436-444).\n\n        Supports both sync and async callbacks (auto-detected).\n\n        Args:\n            callback: Function to call with GetTaskResult when status changes.\n                     Can return None (sync) or Awaitable[None] (async).\n\n        Example:\n            >>> task = await client.call_tool(\"slow_operation\", {}, task=True)\n            >>>\n            >>> def on_update(status: GetTaskResult):\n            ...     print(f\"Task {status.taskId} is now {status.status}\")\n            >>>\n            >>> task.on_status_change(on_update)\n            >>> result = await task  # Callback fires when status changes\n        \"\"\"\n        self._status_callbacks.append(callback)\n\n    async def status(self) -> GetTaskResult:\n        \"\"\"Get current task status.\n\n        If server executed immediately, returns synthetic completed status.\n        Otherwise queries the server for current status.\n        \"\"\"\n        self._check_client_connected()\n\n        if self._is_immediate:\n            # Return synthetic completed status\n            now = datetime.now(timezone.utc)\n            return GetTaskResult(\n                taskId=self._task_id,\n                status=\"completed\",\n                createdAt=now,\n                lastUpdatedAt=now,\n                ttl=None,\n                pollInterval=1000,\n            )\n\n        # Return cached status if available (from notification)\n        if self._status_cache is not None:\n            cached = self._status_cache\n            # Don't clear cache - keep it for next call\n            return cached\n\n        # Query server and cache the result\n        self._status_cache = await self._client.get_task_status(self._task_id)\n        return self._status_cache\n\n    @abc.abstractmethod\n    async def result(self) -> TaskResultT:\n        \"\"\"Wait for and return the task result.\n\n        Must be implemented by subclasses to return the appropriate result type.\n        \"\"\"\n        ...\n\n    async def wait(\n        self, *, state: str | None = None, timeout: float = 300.0\n    ) -> GetTaskResult:\n        \"\"\"Wait for task to reach a specific state or complete.\n\n        Uses event-based waiting when notifications are available (fast),\n        with fallback to polling (reliable). Optimally wakes up immediately\n        on status changes when server sends notifications/tasks/status.\n\n        Args:\n            state: Desired state ('submitted', 'working', 'completed', 'failed').\n                   If None, waits for any terminal state (completed/failed)\n            timeout: Maximum time to wait in seconds\n\n        Returns:\n            GetTaskResult: Final task status\n\n        Raises:\n            TimeoutError: If desired state not reached within timeout\n        \"\"\"\n        self._check_client_connected()\n\n        if self._is_immediate:\n            # Already done\n            return await self.status()\n\n        # Initialize event for notification wake-ups\n        if self._status_event is None:\n            self._status_event = asyncio.Event()\n\n        start = time.time()\n        terminal_states = {\"completed\", \"failed\", \"cancelled\"}\n        poll_interval = 0.5  # Fallback polling interval (500ms)\n\n        while True:\n            # Check cached status first (updated by notifications)\n            if self._status_cache:\n                current = self._status_cache.status\n                if state is None:\n                    if current in terminal_states:\n                        return self._status_cache\n                elif current == state:\n                    return self._status_cache\n\n            # Check timeout\n            elapsed = time.time() - start\n            if elapsed >= timeout:\n                raise TimeoutError(\n                    f\"Task {self._task_id} did not reach {state or 'terminal state'} within {timeout}s\"\n                )\n\n            remaining = timeout - elapsed\n\n            # Wait for notification event OR poll timeout\n            try:\n                await asyncio.wait_for(\n                    self._status_event.wait(), timeout=min(poll_interval, remaining)\n                )\n                self._status_event.clear()\n            except asyncio.TimeoutError:\n                # Fallback: poll server (notification didn't arrive in time)\n                self._status_cache = await self._client.get_task_status(self._task_id)\n\n    async def cancel(self) -> None:\n        \"\"\"Cancel this task, transitioning it to cancelled state.\n\n        Sends a tasks/cancel protocol request. The server will attempt to halt\n        execution and move the task to cancelled state.\n\n        Note: If server executed immediately (graceful degradation), this is a no-op\n        as there's no server-side task to cancel.\n        \"\"\"\n        if self._is_immediate:\n            # No server-side task to cancel\n            return\n        self._check_client_connected()\n        await self._client.cancel_task(self._task_id)\n        # Invalidate cache to force fresh status fetch\n        self._status_cache = None\n\n    def __await__(self):\n        \"\"\"Allow 'await task' to get result.\"\"\"\n        return self.result().__await__()\n\n\nclass ToolTask(Task[\"CallToolResult\"]):\n    \"\"\"\n    Represents a tool call that may execute in background or immediately.\n\n    Provides a uniform API whether the server accepts background execution\n    or executes synchronously (graceful degradation per SEP-1686).\n\n    Usage:\n        task = await client.call_tool_as_task(\"analyze\", args)\n\n        # Check status\n        status = await task.status()\n\n        # Wait for completion\n        await task.wait()\n\n        # Get result (waits if needed)\n        result = await task.result()  # Returns CallToolResult\n\n        # Or just await the task directly\n        result = await task\n    \"\"\"\n\n    def __init__(\n        self,\n        client: Client,\n        task_id: str,\n        tool_name: str,\n        immediate_result: CallToolResult | None = None,\n    ):\n        \"\"\"\n        Create a ToolTask wrapper.\n\n        Args:\n            client: The FastMCP client\n            task_id: The task identifier\n            tool_name: Name of the tool being executed\n            immediate_result: If server executed synchronously, the immediate result\n        \"\"\"\n        super().__init__(client, task_id, immediate_result)\n        self._tool_name = tool_name\n\n    async def result(self) -> CallToolResult:\n        \"\"\"Wait for and return the tool result.\n\n        If server executed immediately, returns the immediate result.\n        Otherwise waits for background task to complete and retrieves result.\n\n        Returns:\n            CallToolResult: The parsed tool result (same as call_tool returns)\n        \"\"\"\n        # Check cache first\n        if self._cached_result is not None:\n            return self._cached_result\n\n        if self._is_immediate:\n            assert self._immediate_result is not None  # Type narrowing\n            result = self._immediate_result\n        else:\n            # Check client connected\n            self._check_client_connected()\n\n            # Wait for completion using event-based wait (respects notifications)\n            await self.wait()\n\n            # Get the raw result (dict or CallToolResult)\n            raw_result = await self._client.get_task_result(self._task_id)\n\n            # Convert to CallToolResult if needed and parse\n            if isinstance(raw_result, dict):\n                # Raw dict from get_task_result - parse as CallToolResult\n                mcp_result = mcp.types.CallToolResult.model_validate(raw_result)\n                result = await self._client._parse_call_tool_result(\n                    self._tool_name, mcp_result, raise_on_error=True\n                )\n            elif isinstance(raw_result, mcp.types.CallToolResult):\n                # Already a CallToolResult from MCP protocol - parse it\n                result = await self._client._parse_call_tool_result(\n                    self._tool_name, raw_result, raise_on_error=True\n                )\n            else:\n                # Legacy ToolResult format - convert to MCP type\n                if hasattr(raw_result, \"content\") and hasattr(\n                    raw_result, \"structured_content\"\n                ):\n                    mcp_result = mcp.types.CallToolResult(\n                        content=raw_result.content,\n                        structuredContent=raw_result.structured_content,\n                        _meta=raw_result.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                    )\n                    result = await self._client._parse_call_tool_result(\n                        self._tool_name, mcp_result, raise_on_error=True\n                    )\n                else:\n                    # Unknown type - just return it\n                    result = raw_result\n\n        # Cache before returning\n        self._cached_result = result\n        return result\n\n\nclass PromptTask(Task[mcp.types.GetPromptResult]):\n    \"\"\"\n    Represents a prompt call that may execute in background or immediately.\n\n    Provides a uniform API whether the server accepts background execution\n    or executes synchronously (graceful degradation per SEP-1686).\n\n    Usage:\n        task = await client.get_prompt_as_task(\"analyze\", args)\n        result = await task  # Returns GetPromptResult\n    \"\"\"\n\n    def __init__(\n        self,\n        client: Client,\n        task_id: str,\n        prompt_name: str,\n        immediate_result: mcp.types.GetPromptResult | None = None,\n    ):\n        \"\"\"\n        Create a PromptTask wrapper.\n\n        Args:\n            client: The FastMCP client\n            task_id: The task identifier\n            prompt_name: Name of the prompt being executed\n            immediate_result: If server executed synchronously, the immediate result\n        \"\"\"\n        super().__init__(client, task_id, immediate_result)\n        self._prompt_name = prompt_name\n\n    async def result(self) -> mcp.types.GetPromptResult:\n        \"\"\"Wait for and return the prompt result.\n\n        If server executed immediately, returns the immediate result.\n        Otherwise waits for background task to complete and retrieves result.\n\n        Returns:\n            GetPromptResult: The prompt result with messages and description\n        \"\"\"\n        # Check cache first\n        if self._cached_result is not None:\n            return self._cached_result\n\n        if self._is_immediate:\n            assert self._immediate_result is not None\n            result = self._immediate_result\n        else:\n            # Check client connected\n            self._check_client_connected()\n\n            # Wait for completion using event-based wait (respects notifications)\n            await self.wait()\n\n            # Get the raw MCP result\n            mcp_result = await self._client.get_task_result(self._task_id)\n\n            # Parse as GetPromptResult\n            result = mcp.types.GetPromptResult.model_validate(mcp_result)\n\n        # Cache before returning\n        self._cached_result = result\n        return result\n\n\nclass ResourceTask(\n    Task[list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]]\n):\n    \"\"\"\n    Represents a resource read that may execute in background or immediately.\n\n    Provides a uniform API whether the server accepts background execution\n    or executes synchronously (graceful degradation per SEP-1686).\n\n    Usage:\n        task = await client.read_resource_as_task(\"file://data.txt\")\n        contents = await task  # Returns list[ReadResourceContents]\n    \"\"\"\n\n    def __init__(\n        self,\n        client: Client,\n        task_id: str,\n        uri: str,\n        immediate_result: list[\n            mcp.types.TextResourceContents | mcp.types.BlobResourceContents\n        ]\n        | None = None,\n    ):\n        \"\"\"\n        Create a ResourceTask wrapper.\n\n        Args:\n            client: The FastMCP client\n            task_id: The task identifier\n            uri: URI of the resource being read\n            immediate_result: If server executed synchronously, the immediate result\n        \"\"\"\n        super().__init__(client, task_id, immediate_result)\n        self._uri = uri\n\n    async def result(\n        self,\n    ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:\n        \"\"\"Wait for and return the resource contents.\n\n        If server executed immediately, returns the immediate result.\n        Otherwise waits for background task to complete and retrieves result.\n\n        Returns:\n            list[ReadResourceContents]: The resource contents\n        \"\"\"\n        # Check cache first\n        if self._cached_result is not None:\n            return self._cached_result\n\n        if self._is_immediate:\n            assert self._immediate_result is not None\n            result = self._immediate_result\n        else:\n            # Check client connected\n            self._check_client_connected()\n\n            # Wait for completion using event-based wait (respects notifications)\n            await self.wait()\n\n            # Get the raw MCP result\n            mcp_result = await self._client.get_task_result(self._task_id)\n\n            # Parse as ReadResourceResult or extract contents\n            if isinstance(mcp_result, mcp.types.ReadResourceResult):\n                # Already parsed by TasksResponse - extract contents\n                result = list(mcp_result.contents)\n            elif isinstance(mcp_result, dict) and \"contents\" in mcp_result:\n                # Dict format - parse each content item\n                parsed_contents = []\n                for item in mcp_result[\"contents\"]:\n                    if isinstance(item, dict):\n                        if \"blob\" in item:\n                            parsed_contents.append(\n                                mcp.types.BlobResourceContents.model_validate(item)\n                            )\n                        else:\n                            parsed_contents.append(\n                                mcp.types.TextResourceContents.model_validate(item)\n                            )\n                    else:\n                        parsed_contents.append(item)\n                result = parsed_contents\n            else:\n                # Fallback - might be the list directly\n                result = mcp_result if isinstance(mcp_result, list) else [mcp_result]\n\n        # Cache before returning\n        self._cached_result = result\n        return result\n"
  },
  {
    "path": "src/fastmcp/client/telemetry.py",
    "content": "\"\"\"Client-side telemetry helpers.\"\"\"\n\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\n\nfrom opentelemetry.trace import Span, SpanKind, Status, StatusCode\n\nfrom fastmcp.telemetry import get_tracer\n\n\n@contextmanager\ndef client_span(\n    name: str,\n    method: str,\n    component_key: str,\n    session_id: str | None = None,\n    resource_uri: str | None = None,\n) -> Generator[Span, None, None]:\n    \"\"\"Create a CLIENT span with standard MCP attributes.\n\n    Automatically records any exception on the span and sets error status.\n    \"\"\"\n    tracer = get_tracer()\n    with tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span:\n        attrs: dict[str, str] = {\n            # RPC semantic conventions\n            \"rpc.system\": \"mcp\",\n            \"rpc.method\": method,\n            # MCP semantic conventions\n            \"mcp.method.name\": method,\n            # FastMCP-specific attributes\n            \"fastmcp.component.key\": component_key,\n        }\n        if session_id:\n            attrs[\"mcp.session.id\"] = session_id\n        if resource_uri:\n            attrs[\"mcp.resource.uri\"] = resource_uri\n        span.set_attributes(attrs)\n        try:\n            yield span\n        except Exception as e:\n            span.record_exception(e)\n            span.set_status(Status(StatusCode.ERROR))\n            raise\n\n\n__all__ = [\"client_span\"]\n"
  },
  {
    "path": "src/fastmcp/client/transports/__init__.py",
    "content": "# Re-export all public APIs for backward compatibility\nfrom mcp.server.fastmcp import FastMCP as FastMCP1Server\n\nfrom fastmcp.client.transports.base import (\n    ClientTransport,\n    ClientTransportT,\n    SessionKwargs,\n)\nfrom fastmcp.client.transports.config import MCPConfigTransport\nfrom fastmcp.client.transports.http import StreamableHttpTransport\nfrom fastmcp.client.transports.inference import infer_transport\nfrom fastmcp.client.transports.sse import SSETransport\nfrom fastmcp.client.transports.memory import FastMCPTransport\nfrom fastmcp.client.transports.stdio import (\n    FastMCPStdioTransport,\n    NodeStdioTransport,\n    NpxStdioTransport,\n    PythonStdioTransport,\n    StdioTransport,\n    UvStdioTransport,\n    UvxStdioTransport,\n)\nfrom fastmcp.server.server import FastMCP\n\n__all__ = [\n    \"ClientTransport\",\n    \"FastMCPStdioTransport\",\n    \"FastMCPTransport\",\n    \"NodeStdioTransport\",\n    \"NpxStdioTransport\",\n    \"PythonStdioTransport\",\n    \"SSETransport\",\n    \"StdioTransport\",\n    \"StreamableHttpTransport\",\n    \"UvStdioTransport\",\n    \"UvxStdioTransport\",\n    \"infer_transport\",\n]\n"
  },
  {
    "path": "src/fastmcp/client/transports/base.py",
    "content": "import abc\nimport contextlib\nimport datetime\nfrom collections.abc import AsyncIterator\nfrom typing import Literal, TypeVar\n\nimport httpx\nimport mcp.types\nfrom mcp import ClientSession\nfrom mcp.client.session import (\n    ElicitationFnT,\n    ListRootsFnT,\n    LoggingFnT,\n    MessageHandlerFnT,\n    SamplingFnT,\n)\nfrom typing_extensions import TypedDict, Unpack\n\n# TypeVar for preserving specific ClientTransport subclass types\nClientTransportT = TypeVar(\"ClientTransportT\", bound=\"ClientTransport\")\n\n\nclass SessionKwargs(TypedDict, total=False):\n    \"\"\"Keyword arguments for the MCP ClientSession constructor.\"\"\"\n\n    read_timeout_seconds: datetime.timedelta | None\n    sampling_callback: SamplingFnT | None\n    sampling_capabilities: mcp.types.SamplingCapability | None\n    list_roots_callback: ListRootsFnT | None\n    logging_callback: LoggingFnT | None\n    elicitation_callback: ElicitationFnT | None\n    message_handler: MessageHandlerFnT | None\n    client_info: mcp.types.Implementation | None\n\n\nclass ClientTransport(abc.ABC):\n    \"\"\"\n    Abstract base class for different MCP client transport mechanisms.\n\n    A Transport is responsible for establishing and managing connections\n    to an MCP server, and providing a ClientSession within an async context.\n\n    \"\"\"\n\n    @abc.abstractmethod\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> AsyncIterator[ClientSession]:\n        \"\"\"\n        Establishes a connection and yields an active ClientSession.\n\n        The ClientSession is *not* expected to be initialized in this context manager.\n\n        The session is guaranteed to be valid only within the scope of the\n        async context manager. Connection setup and teardown are handled\n        within this context.\n\n        Args:\n            **session_kwargs: Keyword arguments to pass to the ClientSession\n                              constructor (e.g., callbacks, timeouts).\n\n        Yields:\n            A mcp.ClientSession instance.\n        \"\"\"\n        raise NotImplementedError\n        yield\n\n    def __repr__(self) -> str:\n        # Basic representation for subclasses\n        return f\"<{self.__class__.__name__}>\"\n\n    async def close(self):  # noqa: B027\n        \"\"\"Close the transport.\"\"\"\n\n    def get_session_id(self) -> str | None:\n        \"\"\"Get the session ID for this transport, if available.\"\"\"\n        return None\n\n    def _set_auth(self, auth: httpx.Auth | Literal[\"oauth\"] | str | None):\n        if auth is not None:\n            raise ValueError(\"This transport does not support auth\")\n"
  },
  {
    "path": "src/fastmcp/client/transports/config.py",
    "content": "import contextlib\nimport datetime\nfrom collections.abc import AsyncIterator\nfrom typing import Any\n\nfrom mcp import ClientSession\nfrom typing_extensions import Unpack\n\nfrom fastmcp.client.transports.base import ClientTransport, SessionKwargs\nfrom fastmcp.client.transports.memory import FastMCPTransport\nfrom fastmcp.mcp_config import (\n    MCPConfig,\n    MCPServerTypes,\n    RemoteMCPServer,\n    StdioMCPServer,\n    TransformingRemoteMCPServer,\n    TransformingStdioMCPServer,\n)\nfrom fastmcp.server.server import FastMCP, create_proxy\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass MCPConfigTransport(ClientTransport):\n    \"\"\"Transport for connecting to one or more MCP servers defined in an MCPConfig.\n\n    This transport provides a unified interface to multiple MCP servers defined in an MCPConfig\n    object or dictionary matching the MCPConfig schema. It supports two key scenarios:\n\n    1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.\n    2. If the MCPConfig contains multiple servers, it creates a composite client by mounting\n       all servers on a single FastMCP instance, with each server's name, by default, used as its mounting prefix.\n\n    In the multiserver case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`\n    and resources with the pattern `protocol://{server_name}/path/to/resource`.\n\n    This is particularly useful for creating clients that need to interact with multiple specialized\n    MCP servers through a single interface, simplifying client code.\n\n    Examples:\n        ```python\n        from fastmcp import Client\n\n        # Create a config with multiple servers\n        config = {\n            \"mcpServers\": {\n                \"weather\": {\n                    \"url\": \"https://weather-api.example.com/mcp\",\n                    \"transport\": \"http\"\n                },\n                \"calendar\": {\n                    \"url\": \"https://calendar-api.example.com/mcp\",\n                    \"transport\": \"http\"\n                }\n            }\n        }\n\n        # Create a client with the config\n        client = Client(config)\n\n        async with client:\n            # Access tools with prefixes\n            weather = await client.call_tool(\"weather_get_forecast\", {\"city\": \"London\"})\n            events = await client.call_tool(\"calendar_list_events\", {\"date\": \"2023-06-01\"})\n\n            # Access resources with prefixed URIs\n            icons = await client.read_resource(\"weather://weather/icons/sunny\")\n        ```\n    \"\"\"\n\n    def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True):\n        if isinstance(config, dict):\n            config = MCPConfig.from_dict(config)\n        self.config = config\n        self.name_as_prefix = name_as_prefix\n        self._transports: list[ClientTransport] = []\n\n        if not self.config.mcpServers:\n            raise ValueError(\"No MCP servers defined in the config\")\n\n        # For single server, create transport eagerly so it can be inspected\n        if len(self.config.mcpServers) == 1:\n            self.transport = next(iter(self.config.mcpServers.values())).to_transport()\n            self._transports.append(self.transport)\n\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> AsyncIterator[ClientSession]:\n        # Single server - delegate directly to pre-created transport\n        if len(self.config.mcpServers) == 1:\n            async with self.transport.connect_session(**session_kwargs) as session:\n                yield session\n            return\n\n        # Multiple servers - create composite with mounted proxies, connecting\n        # each ProxyClient so its underlying transport session stays alive for\n        # the duration of this context (fixes session persistence for\n        # streamable-http backends — see #2790).\n        timeout = session_kwargs.get(\"read_timeout_seconds\")\n        composite = FastMCP[Any](name=\"MCPRouter\")\n\n        async with contextlib.AsyncExitStack() as stack:\n            # Close any previous transports from prior connections to avoid leaking\n            for t in self._transports:\n                await t.close()\n            self._transports = []\n\n            for name, server_config in self.config.mcpServers.items():\n                try:\n                    transport, _client, proxy = await self._create_proxy(\n                        name, server_config, timeout, stack\n                    )\n                except Exception:  # Broad catch is intentional: failure modes\n                    # are diverse (OSError, TimeoutError, RuntimeError, etc.)\n                    # and the whole point is to skip any server that can't connect.\n                    logger.warning(\n                        \"Failed to connect to MCP server %r, skipping\",\n                        name,\n                        exc_info=True,\n                    )\n                    continue\n                self._transports.append(transport)\n                composite.mount(proxy, namespace=name if self.name_as_prefix else None)\n\n            if not self._transports:\n                raise ConnectionError(\"All MCP servers failed to connect\")\n\n            async with FastMCPTransport(mcp=composite).connect_session(\n                **session_kwargs\n            ) as session:\n                yield session\n\n    async def _create_proxy(\n        self,\n        name: str,\n        config: MCPServerTypes,\n        timeout: datetime.timedelta | None,\n        stack: contextlib.AsyncExitStack,\n    ) -> tuple[ClientTransport, Any, FastMCP[Any]]:\n        \"\"\"Create underlying transport, proxy client, and proxy server for a single backend.\n\n        The ProxyClient is connected via the AsyncExitStack *before* being\n        passed to create_proxy so the factory sees it as connected and reuses\n        the same session for all tool calls (instead of creating fresh copies).\n\n        Returns a tuple of (transport, proxy_client, proxy_server).\n        \"\"\"\n        # Import here to avoid circular dependency\n        from fastmcp.server.providers.proxy import StatefulProxyClient\n\n        tool_transforms = None\n        include_tags = None\n        exclude_tags = None\n\n        # Handle transforming servers - call base class to_transport() for underlying transport\n        if isinstance(config, TransformingStdioMCPServer):\n            transport = StdioMCPServer.to_transport(config)\n            tool_transforms = config.tools\n            include_tags = config.include_tags\n            exclude_tags = config.exclude_tags\n        elif isinstance(config, TransformingRemoteMCPServer):\n            transport = RemoteMCPServer.to_transport(config)\n            tool_transforms = config.tools\n            include_tags = config.include_tags\n            exclude_tags = config.exclude_tags\n        else:\n            transport = config.to_transport()\n\n        client = StatefulProxyClient(transport=transport, timeout=timeout)\n        # Connect the client *before* create_proxy so _create_client_factory\n        # detects it as connected and reuses it for all tool calls, preserving\n        # the session ID across requests. StatefulProxyClient is used instead\n        # of ProxyClient because its context-restoring handler wrappers prevent\n        # stale ContextVars in the reused session's receive loop.\n        #\n        # StatefulProxyClient.__aexit__ is a no-op (by design, for the\n        # new_stateful() use case), so we cannot rely on enter_async_context\n        # alone to clean up.  Instead we connect manually and push an\n        # explicit force-disconnect callback so the subprocess is terminated\n        # when the AsyncExitStack unwinds.\n        await client.__aenter__()\n        # Callbacks run LIFO: transport.close() must run *after*\n        # client._disconnect so push it first.\n        stack.push_async_callback(transport.close)\n        stack.push_async_callback(client._disconnect, force=True)\n        # Create proxy without include_tags/exclude_tags - we'll add them after tool transforms\n        proxy = create_proxy(\n            client,\n            name=f\"Proxy-{name}\",\n        )\n        # Add tool transforms FIRST - they may add/modify tags\n        if tool_transforms:\n            from fastmcp.server.transforms import ToolTransform\n\n            proxy.add_transform(ToolTransform(tool_transforms))\n        # Then add enabled filters - they filter based on tags\n        if include_tags:\n            proxy.enable(tags=set(include_tags), only=True)\n        if exclude_tags:\n            proxy.disable(tags=set(exclude_tags))\n        return transport, client, proxy\n\n    async def close(self):\n        for transport in self._transports:\n            await transport.close()\n\n    def __repr__(self) -> str:\n        return f\"<MCPConfigTransport(config='{self.config}')>\"\n"
  },
  {
    "path": "src/fastmcp/client/transports/http.py",
    "content": "\"\"\"Streamable HTTP transport for FastMCP Client.\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport datetime\nimport ssl\nfrom collections.abc import AsyncIterator, Callable\nfrom typing import Any, Literal, cast\n\nimport httpx\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client\nfrom pydantic import AnyUrl\nfrom typing_extensions import Unpack\n\nimport fastmcp\nfrom fastmcp.client.auth.bearer import BearerAuth\nfrom fastmcp.client.auth.oauth import OAuth\nfrom fastmcp.client.transports.base import ClientTransport, SessionKwargs\nfrom fastmcp.server.dependencies import get_http_headers\nfrom fastmcp.utilities.timeout import normalize_timeout_to_timedelta\n\n\nclass StreamableHttpTransport(ClientTransport):\n    \"\"\"Transport implementation that connects to an MCP server via Streamable HTTP Requests.\"\"\"\n\n    def __init__(\n        self,\n        url: str | AnyUrl,\n        headers: dict[str, str] | None = None,\n        auth: httpx.Auth | Literal[\"oauth\"] | str | None = None,\n        sse_read_timeout: datetime.timedelta | float | int | None = None,\n        httpx_client_factory: McpHttpClientFactory | None = None,\n        verify: ssl.SSLContext | bool | str | None = None,\n    ):\n        \"\"\"Initialize a Streamable HTTP transport.\n\n        Args:\n            url: The MCP server endpoint URL.\n            headers: Optional headers to include in requests.\n            auth: Authentication method - httpx.Auth, \"oauth\" for OAuth flow,\n                or a bearer token string.\n            sse_read_timeout: Deprecated. Use read_timeout_seconds in session_kwargs.\n            httpx_client_factory: Optional factory for creating httpx.AsyncClient.\n                If provided, must accept keyword arguments: headers, auth,\n                follow_redirects, and optionally timeout. Using **kwargs is\n                recommended to ensure forward compatibility.\n            verify: SSL certificate verification. Accepts False to disable\n                verification, a path to a CA bundle, or an ssl.SSLContext\n                for full control. None (default) uses httpx defaults (verification\n                enabled). Ignored when httpx_client_factory is provided.\n        \"\"\"\n        if isinstance(url, AnyUrl):\n            url = str(url)\n        if not isinstance(url, str) or not url.startswith(\"http\"):\n            raise ValueError(\"Invalid HTTP/S URL provided for Streamable HTTP.\")\n\n        # Don't modify the URL path - respect the exact URL provided by the user\n        # Some servers are strict about trailing slashes (e.g., PayPal MCP)\n\n        self.url: str = url\n        self.headers = headers or {}\n        self.httpx_client_factory = httpx_client_factory\n        self.verify: ssl.SSLContext | bool | str | None = verify\n\n        if httpx_client_factory is not None and verify is not None:\n            import warnings\n\n            warnings.warn(\n                \"Both 'httpx_client_factory' and 'verify' were provided. \"\n                \"The 'verify' parameter will be ignored because \"\n                \"'httpx_client_factory' takes precedence. Configure SSL \"\n                \"verification directly in your httpx_client_factory instead.\",\n                UserWarning,\n                stacklevel=2,\n            )\n\n        self._set_auth(auth)\n\n        if sse_read_timeout is not None:\n            if fastmcp.settings.deprecation_warnings:\n                import warnings\n\n                warnings.warn(\n                    \"The `sse_read_timeout` parameter is deprecated and no longer used. \"\n                    \"The new streamable_http_client API does not support this parameter. \"\n                    \"Use `read_timeout_seconds` in session_kwargs or configure timeout on \"\n                    \"the httpx client via `httpx_client_factory` instead.\",\n                    DeprecationWarning,\n                    stacklevel=2,\n                )\n        self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout)\n\n        self._get_session_id_cb: Callable[[], str | None] | None = None\n\n    def _set_auth(self, auth: httpx.Auth | Literal[\"oauth\"] | str | None):\n        resolved: httpx.Auth | None\n        if auth == \"oauth\":\n            resolved = OAuth(\n                self.url,\n                httpx_client_factory=self.httpx_client_factory\n                or self._make_verify_factory(),\n            )\n        elif isinstance(auth, OAuth):\n            auth._bind(self.url)\n            # Only inject the transport's factory into OAuth if OAuth still\n            # has the bare default — preserve any factory the caller attached\n            if auth.httpx_client_factory is httpx.AsyncClient:\n                factory = self.httpx_client_factory or self._make_verify_factory()\n                if factory is not None:\n                    auth.httpx_client_factory = factory\n            resolved = auth\n        elif isinstance(auth, str):\n            resolved = BearerAuth(auth)\n        else:\n            resolved = auth\n        self.auth: httpx.Auth | None = resolved\n\n    def _make_verify_factory(self) -> McpHttpClientFactory | None:\n        if self.verify is None:\n            return None\n        verify = self.verify\n\n        def factory(\n            headers: dict[str, str] | None = None,\n            timeout: httpx.Timeout | None = None,\n            auth: httpx.Auth | None = None,\n        ) -> httpx.AsyncClient:\n            if timeout is None:\n                timeout = httpx.Timeout(30.0, read=300.0)\n            kwargs: dict[str, Any] = {\n                \"follow_redirects\": True,\n                \"timeout\": timeout,\n                \"verify\": verify,\n            }\n            if headers is not None:\n                kwargs[\"headers\"] = headers\n            if auth is not None:\n                kwargs[\"auth\"] = auth\n            return httpx.AsyncClient(**kwargs)\n\n        return cast(McpHttpClientFactory, factory)\n\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> AsyncIterator[ClientSession]:\n        # Load headers from an active HTTP request, if available. This will only be true\n        # if the client is used in a FastMCP Proxy, in which case the MCP client headers\n        # need to be forwarded to the remote server.\n        headers = get_http_headers(include={\"authorization\"}) | self.headers\n\n        # Configure timeout if provided, preserving MCP's 30s connect default\n        timeout: httpx.Timeout | None = None\n        if session_kwargs.get(\"read_timeout_seconds\") is not None:\n            read_timeout_seconds = cast(\n                datetime.timedelta, session_kwargs.get(\"read_timeout_seconds\")\n            )\n            timeout = httpx.Timeout(30.0, read=read_timeout_seconds.total_seconds())\n\n        # Create httpx client from factory or use default with MCP-appropriate\n        # timeouts. Note: create_mcp_http_client enables follow_redirects, but\n        # httpx automatically strips Authorization headers on cross-origin\n        # redirects to prevent credential leakage.\n        verify_factory = self._make_verify_factory()\n        if self.httpx_client_factory is not None:\n            http_client = self.httpx_client_factory(\n                headers=headers,\n                auth=self.auth,\n                follow_redirects=True,  # type: ignore[call-arg]\n                **({\"timeout\": timeout} if timeout else {}),\n            )\n        elif verify_factory is not None:\n            http_client = verify_factory(\n                headers=headers,\n                timeout=timeout,\n                auth=self.auth,\n            )\n        else:\n            http_client = create_mcp_http_client(\n                headers=headers,\n                timeout=timeout,\n                auth=self.auth,\n            )\n\n        # Ensure httpx client is closed after use\n        async with (\n            http_client,\n            streamable_http_client(self.url, http_client=http_client) as transport,\n        ):\n            read_stream, write_stream, get_session_id = transport\n            self._get_session_id_cb = get_session_id\n            async with ClientSession(\n                read_stream, write_stream, **session_kwargs\n            ) as session:\n                yield session\n\n    def get_session_id(self) -> str | None:\n        if self._get_session_id_cb:\n            try:\n                return self._get_session_id_cb()\n            except Exception:\n                return None\n        return None\n\n    async def close(self):\n        # Reset the session id callback\n        self._get_session_id_cb = None\n\n    def __repr__(self) -> str:\n        return f\"<StreamableHttpTransport(url='{self.url}')>\"\n"
  },
  {
    "path": "src/fastmcp/client/transports/inference.py",
    "content": "from pathlib import Path\nfrom typing import TYPE_CHECKING, Any, cast, overload\n\nfrom mcp.server.fastmcp import FastMCP as FastMCP1Server\nfrom pydantic import AnyUrl\n\nfrom fastmcp.client.transports.base import ClientTransport, ClientTransportT\nfrom fastmcp.client.transports.config import MCPConfigTransport\nfrom fastmcp.client.transports.http import StreamableHttpTransport\nfrom fastmcp.client.transports.memory import FastMCPTransport\nfrom fastmcp.client.transports.sse import SSETransport\nfrom fastmcp.client.transports.stdio import NodeStdioTransport, PythonStdioTransport\nfrom fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    pass\n\nlogger = get_logger(__name__)\n\n\n@overload\ndef infer_transport(transport: ClientTransportT) -> ClientTransportT: ...\n\n\n@overload\ndef infer_transport(transport: FastMCP) -> FastMCPTransport: ...\n\n\n@overload\ndef infer_transport(transport: FastMCP1Server) -> FastMCPTransport: ...\n\n\n@overload\ndef infer_transport(transport: MCPConfig) -> MCPConfigTransport: ...\n\n\n@overload\ndef infer_transport(transport: dict[str, Any]) -> MCPConfigTransport: ...\n\n\n@overload\ndef infer_transport(\n    transport: AnyUrl,\n) -> SSETransport | StreamableHttpTransport: ...\n\n\n@overload\ndef infer_transport(\n    transport: str,\n) -> (\n    PythonStdioTransport | NodeStdioTransport | SSETransport | StreamableHttpTransport\n): ...\n\n\n@overload\ndef infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTransport: ...\n\n\ndef infer_transport(\n    transport: ClientTransport\n    | FastMCP\n    | FastMCP1Server\n    | AnyUrl\n    | Path\n    | MCPConfig\n    | dict[str, Any]\n    | str,\n) -> ClientTransport:\n    \"\"\"\n    Infer the appropriate transport type from the given transport argument.\n\n    This function attempts to infer the correct transport type from the provided\n    argument, handling various input types and converting them to the appropriate\n    ClientTransport subclass.\n\n    The function supports these input types:\n    - ClientTransport: Used directly without modification\n    - FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport\n    - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)\n    - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)\n    - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers\n\n    For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.\n\n    For MCPConfig with multiple servers, a composite client is created where each server\n    is mounted with its name as prefix. This allows accessing tools and resources from multiple\n    servers through a single unified client interface, using naming patterns like\n    `servername_toolname` for tools and `protocol://servername/path` for resources.\n    If the MCPConfig contains only one server, a direct connection is established without prefixing.\n\n    Examples:\n        ```python\n        # Connect to a local Python script\n        transport = infer_transport(\"my_script.py\")\n\n        # Connect to a remote server via HTTP\n        transport = infer_transport(\"http://example.com/mcp\")\n\n        # Connect to multiple servers using MCPConfig\n        config = {\n            \"mcpServers\": {\n                \"weather\": {\"url\": \"http://weather.example.com/mcp\"},\n                \"calendar\": {\"url\": \"http://calendar.example.com/mcp\"}\n            }\n        }\n        transport = infer_transport(config)\n        ```\n    \"\"\"\n\n    # the transport is already a ClientTransport\n    if isinstance(transport, ClientTransport):\n        return transport\n\n    # the transport is a FastMCP server (2.x or 1.0)\n    elif isinstance(transport, FastMCP | FastMCP1Server):\n        inferred_transport = FastMCPTransport(\n            mcp=cast(FastMCP[Any] | FastMCP1Server, transport)\n        )\n\n    # the transport is a path to a script\n    elif isinstance(transport, Path | str) and Path(transport).exists():\n        if str(transport).endswith(\".py\"):\n            inferred_transport = PythonStdioTransport(script_path=cast(Path, transport))\n        elif str(transport).endswith(\".js\"):\n            inferred_transport = NodeStdioTransport(script_path=cast(Path, transport))\n        else:\n            raise ValueError(f\"Unsupported script type: {transport}\")\n\n    # the transport is an http(s) URL\n    elif isinstance(transport, AnyUrl | str) and str(transport).startswith(\"http\"):\n        inferred_transport_type = infer_transport_type_from_url(\n            cast(AnyUrl | str, transport)\n        )\n        if inferred_transport_type == \"sse\":\n            inferred_transport = SSETransport(url=cast(AnyUrl | str, transport))\n        else:\n            inferred_transport = StreamableHttpTransport(\n                url=cast(AnyUrl | str, transport)\n            )\n\n    # if the transport is a config dict or MCPConfig\n    elif isinstance(transport, dict | MCPConfig):\n        inferred_transport = MCPConfigTransport(\n            config=cast(dict | MCPConfig, transport)\n        )\n\n    # the transport is an unknown type\n    else:\n        raise ValueError(f\"Could not infer a valid transport from: {transport}\")\n\n    logger.debug(f\"Inferred transport: {inferred_transport}\")\n    return inferred_transport\n"
  },
  {
    "path": "src/fastmcp/client/transports/memory.py",
    "content": "import contextlib\nfrom collections.abc import AsyncIterator\n\nimport anyio\nfrom mcp import ClientSession\nfrom mcp.server.fastmcp import FastMCP as FastMCP1Server\nfrom mcp.shared.memory import create_client_server_memory_streams\nfrom typing_extensions import Unpack\n\nfrom fastmcp.client.transports.base import ClientTransport, SessionKwargs\nfrom fastmcp.server.server import FastMCP\n\n\nclass FastMCPTransport(ClientTransport):\n    \"\"\"In-memory transport for FastMCP servers.\n\n    This transport connects directly to a FastMCP server instance in the same\n    Python process. It works with both FastMCP 2.x servers and FastMCP 1.0\n    servers from the low-level MCP SDK. This is particularly useful for unit\n    tests or scenarios where client and server run in the same runtime.\n    \"\"\"\n\n    def __init__(self, mcp: FastMCP | FastMCP1Server, raise_exceptions: bool = False):\n        \"\"\"Initialize a FastMCPTransport from a FastMCP server instance.\"\"\"\n\n        # Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a\n        # ``_mcp_server`` attribute pointing to the underlying MCP server\n        # implementation, so we can treat them identically.\n        self.server = mcp\n        self.raise_exceptions = raise_exceptions\n\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> AsyncIterator[ClientSession]:\n        async with create_client_server_memory_streams() as (\n            client_streams,\n            server_streams,\n        ):\n            client_read, client_write = client_streams\n            server_read, server_write = server_streams\n\n            # Capture exceptions to re-raise after task group cleanup.\n            # anyio task groups can suppress exceptions when cancel_scope.cancel()\n            # is called during cleanup, so we capture and re-raise manually.\n            exception_to_raise: BaseException | None = None\n\n            # IMPORTANT: The lifespan MUST be the outer context and the task\n            # group MUST be the inner context. This ensures the task group\n            # (containing the server's run() and all its pub/sub subscriptions)\n            # is cancelled and fully drained BEFORE the lifespan tears down\n            # the Docket Worker and closes Redis connections. Reversing this\n            # order (e.g. via `async with (tg, lifespan):`) causes the Worker\n            # shutdown to hang for 5 seconds per test because fakeredis\n            # blocking operations hold references that prevent clean\n            # cancellation.\n            async with _enter_server_lifespan(server=self.server):  # noqa: SIM117\n                async with anyio.create_task_group() as tg:\n                    tg.start_soon(\n                        lambda: self.server._mcp_server.run(\n                            server_read,\n                            server_write,\n                            self.server._mcp_server.create_initialization_options(),\n                            raise_exceptions=self.raise_exceptions,\n                        )\n                    )\n\n                    try:\n                        async with ClientSession(\n                            read_stream=client_read,\n                            write_stream=client_write,\n                            **session_kwargs,\n                        ) as client_session:\n                            yield client_session\n                    except BaseException as e:\n                        exception_to_raise = e\n                    finally:\n                        tg.cancel_scope.cancel()\n\n            # Re-raise after task group has exited cleanly\n            if exception_to_raise is not None:\n                raise exception_to_raise\n\n    def __repr__(self) -> str:\n        return f\"<FastMCPTransport(server='{self.server.name}')>\"\n\n\n@contextlib.asynccontextmanager\nasync def _enter_server_lifespan(\n    server: FastMCP | FastMCP1Server,\n) -> AsyncIterator[None]:\n    \"\"\"Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers.\"\"\"\n    if isinstance(server, FastMCP):\n        async with server._lifespan_manager():\n            yield\n    else:\n        yield\n"
  },
  {
    "path": "src/fastmcp/client/transports/sse.py",
    "content": "\"\"\"Server-Sent Events (SSE) transport for FastMCP Client.\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport datetime\nimport ssl\nfrom collections.abc import AsyncIterator\nfrom typing import Any, Literal, cast\n\nimport httpx\nfrom mcp import ClientSession\nfrom mcp.client.sse import sse_client\nfrom mcp.shared._httpx_utils import McpHttpClientFactory\nfrom pydantic import AnyUrl\nfrom typing_extensions import Unpack\n\nfrom fastmcp.client.auth.bearer import BearerAuth\nfrom fastmcp.client.auth.oauth import OAuth\nfrom fastmcp.client.transports.base import ClientTransport, SessionKwargs\nfrom fastmcp.server.dependencies import get_http_headers\nfrom fastmcp.utilities.timeout import normalize_timeout_to_timedelta\n\n\nclass SSETransport(ClientTransport):\n    \"\"\"Transport implementation that connects to an MCP server via Server-Sent Events.\"\"\"\n\n    def __init__(\n        self,\n        url: str | AnyUrl,\n        headers: dict[str, str] | None = None,\n        auth: httpx.Auth | Literal[\"oauth\"] | str | None = None,\n        sse_read_timeout: datetime.timedelta | float | int | None = None,\n        httpx_client_factory: McpHttpClientFactory | None = None,\n        verify: ssl.SSLContext | bool | str | None = None,\n    ):\n        if isinstance(url, AnyUrl):\n            url = str(url)\n        if not isinstance(url, str) or not url.startswith(\"http\"):\n            raise ValueError(\"Invalid HTTP/S URL provided for SSE.\")\n\n        # Don't modify the URL path - respect the exact URL provided by the user\n        # Some servers are strict about trailing slashes (e.g., PayPal MCP)\n\n        self.url: str = url\n        self.headers = headers or {}\n        self.httpx_client_factory = httpx_client_factory\n        self.verify: ssl.SSLContext | bool | str | None = verify\n\n        if httpx_client_factory is not None and verify is not None:\n            import warnings\n\n            warnings.warn(\n                \"Both 'httpx_client_factory' and 'verify' were provided. \"\n                \"The 'verify' parameter will be ignored because \"\n                \"'httpx_client_factory' takes precedence. Configure SSL \"\n                \"verification directly in your httpx_client_factory instead.\",\n                UserWarning,\n                stacklevel=2,\n            )\n\n        self._set_auth(auth)\n\n        self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout)\n\n    def _set_auth(self, auth: httpx.Auth | Literal[\"oauth\"] | str | None):\n        resolved: httpx.Auth | None\n        if auth == \"oauth\":\n            resolved = OAuth(\n                self.url,\n                httpx_client_factory=self.httpx_client_factory\n                or self._make_verify_factory(),\n            )\n        elif isinstance(auth, OAuth):\n            auth._bind(self.url)\n            # Only inject the transport's factory into OAuth if OAuth still\n            # has the bare default — preserve any factory the caller attached\n            if auth.httpx_client_factory is httpx.AsyncClient:\n                factory = self.httpx_client_factory or self._make_verify_factory()\n                if factory is not None:\n                    auth.httpx_client_factory = factory\n            resolved = auth\n        elif isinstance(auth, str):\n            resolved = BearerAuth(auth)\n        else:\n            resolved = auth\n        self.auth: httpx.Auth | None = resolved\n\n    def _make_verify_factory(self) -> McpHttpClientFactory | None:\n        if self.verify is None:\n            return None\n        verify = self.verify\n\n        def factory(\n            headers: dict[str, str] | None = None,\n            timeout: httpx.Timeout | None = None,\n            auth: httpx.Auth | None = None,\n        ) -> httpx.AsyncClient:\n            if timeout is None:\n                timeout = httpx.Timeout(30.0, read=300.0)\n            kwargs: dict[str, Any] = {\n                \"follow_redirects\": True,\n                \"timeout\": timeout,\n                \"verify\": verify,\n            }\n            if headers is not None:\n                kwargs[\"headers\"] = headers\n            if auth is not None:\n                kwargs[\"auth\"] = auth\n            return httpx.AsyncClient(**kwargs)\n\n        return cast(McpHttpClientFactory, factory)\n\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> AsyncIterator[ClientSession]:\n        client_kwargs: dict[str, Any] = {}\n\n        # load headers from an active HTTP request, if available. This will only be true\n        # if the client is used in a FastMCP Proxy, in which case the MCP client headers\n        # need to be forwarded to the remote server.\n        client_kwargs[\"headers\"] = (\n            get_http_headers(include={\"authorization\"}) | self.headers\n        )\n\n        # sse_read_timeout has a default value set, so we can't pass None without overriding it\n        # instead we simply leave the kwarg out if it's not provided\n        if self.sse_read_timeout is not None:\n            client_kwargs[\"sse_read_timeout\"] = self.sse_read_timeout.total_seconds()\n        if session_kwargs.get(\"read_timeout_seconds\") is not None:\n            read_timeout_seconds = cast(\n                datetime.timedelta, session_kwargs.get(\"read_timeout_seconds\")\n            )\n            client_kwargs[\"timeout\"] = read_timeout_seconds.total_seconds()\n\n        if self.httpx_client_factory is not None:\n            client_kwargs[\"httpx_client_factory\"] = self.httpx_client_factory\n        else:\n            verify_factory = self._make_verify_factory()\n            if verify_factory is not None:\n                client_kwargs[\"httpx_client_factory\"] = verify_factory\n\n        async with sse_client(self.url, auth=self.auth, **client_kwargs) as transport:\n            read_stream, write_stream = transport\n            async with ClientSession(\n                read_stream, write_stream, **session_kwargs\n            ) as session:\n                yield session\n\n    def __repr__(self) -> str:\n        return f\"<SSETransport(url='{self.url}')>\"\n"
  },
  {
    "path": "src/fastmcp/client/transports/stdio.py",
    "content": "import asyncio\nimport contextlib\nimport os\nimport shutil\nimport sys\nfrom collections.abc import AsyncIterator\nfrom pathlib import Path\nfrom typing import TextIO, cast\n\nimport anyio\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom typing_extensions import Unpack\n\nfrom fastmcp.client.transports.base import ClientTransport, SessionKwargs\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\nlogger = get_logger(__name__)\n\n\nclass StdioTransport(ClientTransport):\n    \"\"\"\n    Base transport for connecting to an MCP server via subprocess with stdio.\n\n    This is a base class that can be subclassed for specific command-based\n    transports like Python, Node, Uvx, etc.\n    \"\"\"\n\n    def __init__(\n        self,\n        command: str,\n        args: list[str],\n        env: dict[str, str] | None = None,\n        cwd: str | None = None,\n        keep_alive: bool | None = None,\n        log_file: Path | TextIO | None = None,\n    ):\n        \"\"\"\n        Initialize a Stdio transport.\n\n        Args:\n            command: The command to run (e.g., \"python\", \"node\", \"uvx\")\n            args: The arguments to pass to the command\n            env: Environment variables to set for the subprocess\n            cwd: Current working directory for the subprocess\n            keep_alive: Whether to keep the subprocess alive between connections.\n                       Defaults to True. When True, the subprocess remains active\n                       after the connection context exits, allowing reuse in\n                       subsequent connections.\n            log_file: Optional path or file-like object where subprocess stderr will\n                   be written. Can be a Path or TextIO object. Defaults to sys.stderr\n                   if not provided. When a Path is provided, the file will be created\n                   if it doesn't exist, or appended to if it does. When set, server\n                   errors will be written to this file instead of appearing in the console.\n        \"\"\"\n        self.command = command\n        self.args = args\n        self.env = env\n        self.cwd = cwd\n        if keep_alive is None:\n            keep_alive = True\n        self.keep_alive = keep_alive\n        self.log_file = log_file\n\n        self._session: ClientSession | None = None\n        self._connect_task: asyncio.Task | None = None\n        self._ready_event = anyio.Event()\n        self._stop_event = anyio.Event()\n\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> AsyncIterator[ClientSession]:\n        try:\n            await self.connect(**session_kwargs)\n            yield cast(ClientSession, self._session)\n        finally:\n            if not self.keep_alive:\n                await self.disconnect()\n            else:\n                logger.debug(\"Stdio transport has keep_alive=True, not disconnecting\")\n\n    async def connect(\n        self, **session_kwargs: Unpack[SessionKwargs]\n    ) -> ClientSession | None:\n        if self._connect_task is not None:\n            return\n\n        session_future: asyncio.Future[ClientSession] = asyncio.Future()\n\n        # start the connection task\n        self._connect_task = asyncio.create_task(\n            _stdio_transport_connect_task(\n                command=self.command,\n                args=self.args,\n                env=self.env,\n                cwd=self.cwd,\n                log_file=self.log_file,\n                # TODO(ty): remove when ty supports Unpack[TypedDict] inference\n                session_kwargs=session_kwargs,  # type: ignore[arg-type]\n                ready_event=self._ready_event,\n                stop_event=self._stop_event,\n                session_future=session_future,\n            )\n        )\n\n        # wait for the client to be ready before returning\n        await self._ready_event.wait()\n\n        # Check if connect task completed with an exception (early failure)\n        if self._connect_task.done():\n            exception = self._connect_task.exception()\n            if exception is not None:\n                raise exception\n\n        self._session = await session_future\n        return self._session\n\n    async def disconnect(self):\n        if self._connect_task is None:\n            return\n\n        # signal the connection task to stop\n        self._stop_event.set()\n\n        # wait for the connection task to finish cleanly\n        await self._connect_task\n\n        # reset variables and events for potential future reconnects\n        self._connect_task = None\n        self._stop_event = anyio.Event()\n        self._ready_event = anyio.Event()\n\n    async def close(self):\n        await self.disconnect()\n\n    def __del__(self):\n        \"\"\"Ensure that we send a disconnection signal to the transport task if we are being garbage collected.\"\"\"\n        if not self._stop_event.is_set():\n            self._stop_event.set()\n\n    def __repr__(self) -> str:\n        return (\n            f\"<{self.__class__.__name__}(command='{self.command}', args={self.args})>\"\n        )\n\n\nasync def _stdio_transport_connect_task(\n    command: str,\n    args: list[str],\n    env: dict[str, str] | None,\n    cwd: str | None,\n    log_file: Path | TextIO | None,\n    session_kwargs: SessionKwargs,\n    ready_event: anyio.Event,\n    stop_event: anyio.Event,\n    session_future: asyncio.Future[ClientSession],\n):\n    \"\"\"A standalone connection task for a stdio transport. It is not a part of the StdioTransport class\n    to ensure that the connection task does not hold a reference to the Transport object.\"\"\"\n\n    try:\n        async with contextlib.AsyncExitStack() as stack:\n            try:\n                server_params = StdioServerParameters(\n                    command=command,\n                    args=args,\n                    env=env,\n                    cwd=cwd,\n                )\n                # Handle log_file: Path needs to be opened, TextIO used as-is\n                if log_file is None:\n                    log_file_handle = sys.stderr\n                elif isinstance(log_file, Path):\n                    log_file_handle = stack.enter_context(log_file.open(\"a\"))\n                else:\n                    # Must be TextIO - use it directly\n                    log_file_handle = log_file\n\n                transport = await stack.enter_async_context(\n                    stdio_client(server_params, errlog=log_file_handle)\n                )\n                read_stream, write_stream = transport\n                session_future.set_result(\n                    await stack.enter_async_context(\n                        ClientSession(read_stream, write_stream, **session_kwargs)\n                    )\n                )\n\n                logger.debug(\"Stdio transport connected\")\n                ready_event.set()\n\n                # Wait until disconnect is requested (stop_event is set)\n                await stop_event.wait()\n            finally:\n                # Clean up client on exit\n                logger.debug(\"Stdio transport disconnected\")\n    except Exception:\n        # Ensure ready event is set even if connection fails\n        ready_event.set()\n        raise\n\n\nclass PythonStdioTransport(StdioTransport):\n    \"\"\"Transport for running Python scripts.\"\"\"\n\n    def __init__(\n        self,\n        script_path: str | Path,\n        args: list[str] | None = None,\n        env: dict[str, str] | None = None,\n        cwd: str | None = None,\n        python_cmd: str = sys.executable,\n        keep_alive: bool | None = None,\n        log_file: Path | TextIO | None = None,\n    ):\n        \"\"\"\n        Initialize a Python transport.\n\n        Args:\n            script_path: Path to the Python script to run\n            args: Additional arguments to pass to the script\n            env: Environment variables to set for the subprocess\n            cwd: Current working directory for the subprocess\n            python_cmd: Python command to use (default: \"python\")\n            keep_alive: Whether to keep the subprocess alive between connections.\n                       Defaults to True. When True, the subprocess remains active\n                       after the connection context exits, allowing reuse in\n                       subsequent connections.\n            log_file: Optional path or file-like object where subprocess stderr will\n                   be written. Can be a Path or TextIO object. Defaults to sys.stderr\n                   if not provided. When a Path is provided, the file will be created\n                   if it doesn't exist, or appended to if it does. When set, server\n                   errors will be written to this file instead of appearing in the console.\n        \"\"\"\n        script_path = Path(script_path).resolve()\n        if not script_path.is_file():\n            raise FileNotFoundError(f\"Script not found: {script_path}\")\n        if not str(script_path).endswith(\".py\"):\n            raise ValueError(f\"Not a Python script: {script_path}\")\n\n        full_args = [str(script_path)]\n        if args:\n            full_args.extend(args)\n\n        super().__init__(\n            command=python_cmd,\n            args=full_args,\n            env=env,\n            cwd=cwd,\n            keep_alive=keep_alive,\n            log_file=log_file,\n        )\n        self.script_path = script_path\n\n\nclass FastMCPStdioTransport(StdioTransport):\n    \"\"\"Transport for running FastMCP servers using the FastMCP CLI.\"\"\"\n\n    def __init__(\n        self,\n        script_path: str | Path,\n        args: list[str] | None = None,\n        env: dict[str, str] | None = None,\n        cwd: str | None = None,\n        keep_alive: bool | None = None,\n        log_file: Path | TextIO | None = None,\n    ):\n        script_path = Path(script_path).resolve()\n        if not script_path.is_file():\n            raise FileNotFoundError(f\"Script not found: {script_path}\")\n        if not str(script_path).endswith(\".py\"):\n            raise ValueError(f\"Not a Python script: {script_path}\")\n\n        super().__init__(\n            command=\"fastmcp\",\n            args=[\"run\", str(script_path)],\n            env=env,\n            cwd=cwd,\n            keep_alive=keep_alive,\n            log_file=log_file,\n        )\n        self.script_path = script_path\n\n\nclass NodeStdioTransport(StdioTransport):\n    \"\"\"Transport for running Node.js scripts.\"\"\"\n\n    def __init__(\n        self,\n        script_path: str | Path,\n        args: list[str] | None = None,\n        env: dict[str, str] | None = None,\n        cwd: str | None = None,\n        node_cmd: str = \"node\",\n        keep_alive: bool | None = None,\n        log_file: Path | TextIO | None = None,\n    ):\n        \"\"\"\n        Initialize a Node transport.\n\n        Args:\n            script_path: Path to the Node.js script to run\n            args: Additional arguments to pass to the script\n            env: Environment variables to set for the subprocess\n            cwd: Current working directory for the subprocess\n            node_cmd: Node.js command to use (default: \"node\")\n            keep_alive: Whether to keep the subprocess alive between connections.\n                       Defaults to True. When True, the subprocess remains active\n                       after the connection context exits, allowing reuse in\n                       subsequent connections.\n            log_file: Optional path or file-like object where subprocess stderr will\n                   be written. Can be a Path or TextIO object. Defaults to sys.stderr\n                   if not provided. When a Path is provided, the file will be created\n                   if it doesn't exist, or appended to if it does. When set, server\n                   errors will be written to this file instead of appearing in the console.\n        \"\"\"\n        script_path = Path(script_path).resolve()\n        if not script_path.is_file():\n            raise FileNotFoundError(f\"Script not found: {script_path}\")\n        if not str(script_path).endswith(\".js\"):\n            raise ValueError(f\"Not a JavaScript script: {script_path}\")\n\n        full_args = [str(script_path)]\n        if args:\n            full_args.extend(args)\n\n        super().__init__(\n            command=node_cmd,\n            args=full_args,\n            env=env,\n            cwd=cwd,\n            keep_alive=keep_alive,\n            log_file=log_file,\n        )\n        self.script_path = script_path\n\n\nclass UvStdioTransport(StdioTransport):\n    \"\"\"Transport for running commands via the uv tool.\"\"\"\n\n    def __init__(\n        self,\n        command: str,\n        args: list[str] | None = None,\n        module: bool = False,\n        project_directory: Path | None = None,\n        python_version: str | None = None,\n        with_packages: list[str] | None = None,\n        with_requirements: Path | None = None,\n        env_vars: dict[str, str] | None = None,\n        keep_alive: bool | None = None,\n    ):\n        # Basic validation\n        if project_directory and not project_directory.exists():\n            raise NotADirectoryError(\n                f\"Project directory not found: {project_directory}\"\n            )\n\n        # Create Environment from provided parameters (internal use)\n        env_config = UVEnvironment(\n            python=python_version,\n            dependencies=with_packages,\n            requirements=with_requirements,\n            project=project_directory,\n            editable=None,  # Not exposed in this transport\n        )\n\n        # Build uv arguments using the config\n        uv_args: list[str] = []\n\n        # Check if we need any environment setup\n        if env_config._must_run_with_uv():\n            # Use the config to build args, but we need to handle the command differently\n            # since transport has specific needs\n            uv_args = [\"run\"]\n\n            if python_version:\n                uv_args.extend([\"--python\", python_version])\n            if project_directory:\n                uv_args.extend([\"--directory\", str(project_directory)])\n\n            # Note: Don't add fastmcp as dependency here, transport is for general use\n            for pkg in with_packages or []:\n                uv_args.extend([\"--with\", pkg])\n            if with_requirements:\n                uv_args.extend([\"--with-requirements\", str(with_requirements)])\n        else:\n            # No environment setup needed\n            uv_args = [\"run\"]\n\n        if module:\n            uv_args.append(\"--module\")\n\n        if not args:\n            args = []\n\n        uv_args.extend([command, *args])\n\n        # Get environment with any additional variables\n        env: dict[str, str] | None = None\n        if env_vars or project_directory:\n            env = os.environ.copy()\n            if project_directory:\n                env[\"UV_PROJECT_DIR\"] = str(project_directory)\n            if env_vars:\n                env.update(env_vars)\n\n        super().__init__(\n            command=\"uv\",\n            args=uv_args,\n            env=env,\n            cwd=None,  # Use --directory flag instead of cwd\n            keep_alive=keep_alive,\n        )\n\n\nclass UvxStdioTransport(StdioTransport):\n    \"\"\"Transport for running commands via the uvx tool.\"\"\"\n\n    def __init__(\n        self,\n        tool_name: str,\n        tool_args: list[str] | None = None,\n        project_directory: str | None = None,\n        python_version: str | None = None,\n        with_packages: list[str] | None = None,\n        from_package: str | None = None,\n        env_vars: dict[str, str] | None = None,\n        keep_alive: bool | None = None,\n    ):\n        \"\"\"\n        Initialize a Uvx transport.\n\n        Args:\n            tool_name: Name of the tool to run via uvx\n            tool_args: Arguments to pass to the tool\n            project_directory: Project directory (for package resolution)\n            python_version: Python version to use\n            with_packages: Additional packages to include\n            from_package: Package to install the tool from\n            env_vars: Additional environment variables\n            keep_alive: Whether to keep the subprocess alive between connections.\n                       Defaults to True. When True, the subprocess remains active\n                       after the connection context exits, allowing reuse in\n                       subsequent connections.\n        \"\"\"\n        # Basic validation\n        if project_directory and not Path(project_directory).exists():\n            raise NotADirectoryError(\n                f\"Project directory not found: {project_directory}\"\n            )\n\n        # Build uvx arguments\n        uvx_args: list[str] = []\n        if python_version:\n            uvx_args.extend([\"--python\", python_version])\n        if from_package:\n            uvx_args.extend([\"--from\", from_package])\n        for pkg in with_packages or []:\n            uvx_args.extend([\"--with\", pkg])\n\n        # Add the tool name and tool args\n        uvx_args.append(tool_name)\n        if tool_args:\n            uvx_args.extend(tool_args)\n\n        env: dict[str, str] | None = None\n        if env_vars:\n            env = os.environ.copy()\n            env.update(env_vars)\n\n        super().__init__(\n            command=\"uvx\",\n            args=uvx_args,\n            env=env,\n            cwd=project_directory,\n            keep_alive=keep_alive,\n        )\n        self.tool_name: str = tool_name\n\n\nclass NpxStdioTransport(StdioTransport):\n    \"\"\"Transport for running commands via the npx tool.\"\"\"\n\n    def __init__(\n        self,\n        package: str,\n        args: list[str] | None = None,\n        project_directory: str | None = None,\n        env_vars: dict[str, str] | None = None,\n        use_package_lock: bool = True,\n        keep_alive: bool | None = None,\n    ):\n        \"\"\"\n        Initialize an Npx transport.\n\n        Args:\n            package: Name of the npm package to run\n            args: Arguments to pass to the package command\n            project_directory: Project directory with package.json\n            env_vars: Additional environment variables\n            use_package_lock: Whether to use package-lock.json (--prefer-offline)\n            keep_alive: Whether to keep the subprocess alive between connections.\n                       Defaults to True. When True, the subprocess remains active\n                       after the connection context exits, allowing reuse in\n                       subsequent connections.\n        \"\"\"\n        # verify npx is installed\n        if shutil.which(\"npx\") is None:\n            raise ValueError(\"Command 'npx' not found\")\n\n        # Basic validation\n        if project_directory and not Path(project_directory).exists():\n            raise NotADirectoryError(\n                f\"Project directory not found: {project_directory}\"\n            )\n\n        # Build npx arguments\n        npx_args = []\n        if use_package_lock:\n            npx_args.append(\"--prefer-offline\")\n\n        # Add the package name and args\n        npx_args.append(package)\n        if args:\n            npx_args.extend(args)\n\n        # Get environment with any additional variables\n        env = None\n        if env_vars:\n            env = os.environ.copy()\n            env.update(env_vars)\n\n        super().__init__(\n            command=\"npx\",\n            args=npx_args,\n            env=env,\n            cwd=project_directory,\n            keep_alive=keep_alive,\n        )\n        self.package = package\n"
  },
  {
    "path": "src/fastmcp/contrib/README.md",
    "content": "# FastMCP Contrib Modules\n\nThis directory holds community-contributed modules for FastMCP. These modules extend FastMCP's functionality but are not officially maintained by the core team.\n\n**Guarantees:**\n*   Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.\n*   Changes to the core FastMCP library might break modules in `contrib` without explicit warnings in the main changelog.\n\nUse these modules at your own discretion. Contributions are welcome, but please include tests and documentation.\n\n## Usage\n\nTo use a contrib module, import it from the `fastmcp.contrib` package.\n\n```python\nfrom fastmcp.contrib import my_module\n```\n\nNote that the contrib modules may have different dependencies than the core library, which can be noted in their respective README's or even separate requirements / dependency files."
  },
  {
    "path": "src/fastmcp/contrib/bulk_tool_caller/README.md",
    "content": "# Bulk Tool Caller\n\nThis module provides the `BulkToolCaller` class, which extends the `MCPMixin` to offer tools for performing multiple tool calls in a single request to a FastMCP server. This can be useful for optimizing interactions with the server by reducing the overhead of individual tool calls.\n\n## Usage\n\nTo use the `BulkToolCaller`, see the example [example.py](./example.py) file. The `BulkToolCaller` can be instantiated and then registered with a FastMCP server URL. It provides methods to call multiple tools in bulk, either different tools or the same tool with different arguments.\n\n\n## Provided Tools\n\nThe `BulkToolCaller` provides the following tools:\n\n### `call_tools_bulk`\n\nCalls multiple different tools registered on the MCP server in a single request.\n\n- **Arguments:**\n    - `tool_calls` (list of `CallToolRequest`): A list of objects, where each object specifies the `tool` name and `arguments` for an individual tool call.\n    - `continue_on_error` (bool, optional): If `True`, continue executing subsequent tool calls even if a previous one resulted in an error. Defaults to `True`.\n\n- **Returns:**\n    A list of `CallToolRequestResult` objects, each containing the result (`isError`, `content`) and the original `tool` name and `arguments` for each call.\n\n### `call_tool_bulk`\n\nCalls a single tool registered on the MCP server multiple times with different arguments in a single request.\n\n- **Arguments:**\n    - `tool` (str): The name of the tool to call.\n    - `tool_arguments` (list of dict): A list of dictionaries, where each dictionary contains the arguments for an individual run of the tool.\n    - `continue_on_error` (bool, optional): If `True`, continue executing subsequent tool calls even if a previous one resulted in an error. Defaults to `True`.\n\n- **Returns:**\n    A list of `CallToolRequestResult` objects, each containing the result (`isError`, `content`) and the original `tool` name and `arguments` for each call."
  },
  {
    "path": "src/fastmcp/contrib/bulk_tool_caller/__init__.py",
    "content": "from .bulk_tool_caller import BulkToolCaller\n\n__all__ = [\"BulkToolCaller\"]\n"
  },
  {
    "path": "src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py",
    "content": "from typing import Any\n\nfrom mcp.types import CallToolResult, TextContent\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.contrib.mcp_mixin.mcp_mixin import (\n    _DEFAULT_SEPARATOR_TOOL,\n    MCPMixin,\n    mcp_tool,\n)\n\n\nclass CallToolRequest(BaseModel):\n    \"\"\"A class to represent a request to call a tool with specific arguments.\"\"\"\n\n    tool: str = Field(description=\"The name of the tool to call.\")\n    arguments: dict[str, Any] = Field(\n        description=\"A dictionary containing the arguments for the tool call.\"\n    )\n\n\nclass CallToolRequestResult(CallToolResult):\n    \"\"\"\n    A class to represent the result of a bulk tool call.\n    It extends CallToolResult to include information about the requested tool call.\n    \"\"\"\n\n    tool: str = Field(description=\"The name of the tool that was called.\")\n    arguments: dict[str, Any] = Field(\n        description=\"The arguments used for the tool call.\"\n    )\n\n    @classmethod\n    def from_call_tool_result(\n        cls, result: CallToolResult, tool: str, arguments: dict[str, Any]\n    ) -> \"CallToolRequestResult\":\n        \"\"\"\n        Create a CallToolRequestResult from a CallToolResult.\n        \"\"\"\n        return cls(\n            tool=tool,\n            arguments=arguments,\n            isError=result.isError,\n            content=result.content,\n        )\n\n\nclass BulkToolCaller(MCPMixin):\n    \"\"\"\n    A class to provide a \"bulk tool call\" tool for a FastMCP server\n    \"\"\"\n\n    _BULK_TOOL_NAMES: frozenset[str] = frozenset({\"call_tools_bulk\", \"call_tool_bulk\"})\n\n    def register_tools(\n        self,\n        mcp_server: \"FastMCP\",\n        prefix: str | None = None,\n        separator: str = _DEFAULT_SEPARATOR_TOOL,\n    ) -> None:\n        \"\"\"\n        Register the tools provided by this class with the given MCP server.\n        \"\"\"\n        self.connection = FastMCPTransport(mcp_server)\n\n        super().register_tools(mcp_server=mcp_server)\n\n    @mcp_tool()\n    async def call_tools_bulk(\n        self, tool_calls: list[CallToolRequest], continue_on_error: bool = True\n    ) -> list[CallToolRequestResult]:\n        \"\"\"\n        Call multiple tools registered on this MCP server in a single request. Each call can\n         be for a different tool and can include different arguments. Useful for speeding up\n         what would otherwise take several individual tool calls.\n        \"\"\"\n        results = []\n\n        for tool_call in tool_calls:\n            result = await self._call_tool(tool_call.tool, tool_call.arguments)\n\n            results.append(result)\n\n            if result.isError and not continue_on_error:\n                return results\n\n        return results\n\n    @mcp_tool()\n    async def call_tool_bulk(\n        self,\n        tool: str,\n        tool_arguments: list[dict[str, str | int | float | bool | None]],\n        continue_on_error: bool = True,\n    ) -> list[CallToolRequestResult]:\n        \"\"\"\n        Call a single tool registered on this MCP server multiple times with a single request.\n         Each call can include different arguments. Useful for speeding up what would otherwise\n         take several individual tool calls.\n\n        Args:\n            tool: The name of the tool to call.\n            tool_arguments: A list of dictionaries, where each dictionary contains the arguments for an individual run of the tool.\n        \"\"\"\n        results = []\n\n        for tool_call_arguments in tool_arguments:\n            result = await self._call_tool(tool, tool_call_arguments)\n\n            results.append(result)\n\n            if result.isError and not continue_on_error:\n                return results\n\n        return results\n\n    async def _call_tool(\n        self, tool: str, arguments: dict[str, Any]\n    ) -> CallToolRequestResult:\n        \"\"\"\n        Helper method to call a tool with the provided arguments.\n        \"\"\"\n\n        if tool in self._BULK_TOOL_NAMES:\n            return CallToolRequestResult(\n                tool=tool,\n                arguments=arguments,\n                isError=True,\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=(\n                            \"BulkToolCaller cannot call itself. \"\n                            \"The tools 'call_tools_bulk' and 'call_tool_bulk' are disallowed.\"\n                        ),\n                    )\n                ],\n            )\n\n        async with Client(self.connection) as client:\n            result = await client.call_tool_mcp(name=tool, arguments=arguments)\n\n            return CallToolRequestResult(\n                tool=tool,\n                arguments=arguments,\n                isError=result.isError,\n                content=result.content,\n            )\n"
  },
  {
    "path": "src/fastmcp/contrib/bulk_tool_caller/example.py",
    "content": "\"\"\"Sample code for FastMCP using MCPMixin.\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.bulk_tool_caller import BulkToolCaller\n\nmcp = FastMCP()\n\n\n@mcp.tool\ndef echo_tool(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return text\n\n\nbulk_tool_caller = BulkToolCaller()\n\nbulk_tool_caller.register_tools(mcp)\n"
  },
  {
    "path": "src/fastmcp/contrib/component_manager/README.md",
    "content": "# Component Manager – Contrib Module for FastMCP\n\nThe **Component Manager** provides a unified API for enabling and disabling tools, resources, and prompts at runtime in a FastMCP server. This module is useful for dynamic control over which components are active, enabling advanced features like feature toggling, admin interfaces, or automation workflows.\n\n---\n\n## 🔧 Features\n\n- Enable/disable **tools**, **resources**, and **prompts** via HTTP endpoints.\n- Supports **local** and **mounted (server)** components.\n- Customizable **API root path**.\n- Optional **Auth scopes** for secured access.\n- Fully integrates with FastMCP with minimal configuration.\n\n---\n\n## 📦 Installation\n\nThis module is part of the `fastmcp.contrib` package. No separate installation is required if you're already using **FastMCP**.\n\n---\n\n## 🚀 Usage\n\n### Basic Setup\n\n```python\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.component_manager import set_up_component_manager\n\nmcp = FastMCP(name=\"Component Manager\", instructions=\"This is a test server with component manager.\")\nset_up_component_manager(server=mcp)\n```\n\n---\n\n## 🔗 API Endpoints\n\nAll endpoints are registered at `/` by default, or under the custom path if one is provided.\n\n### Tools\n\n```http\nPOST /tools/{tool_name}/enable\nPOST /tools/{tool_name}/disable\n```\n\n### Resources\n\n```http\nPOST /resources/{uri:path}/enable\nPOST /resources/{uri:path}/disable\n```\n\n * Supports template URIs as well\n```http\nPOST /resources/example://test/{id}/enable\nPOST /resources/example://test/{id}/disable\n```\n\n### Prompts\n\n```http\nPOST /prompts/{prompt_name}/enable\nPOST /prompts/{prompt_name}/disable\n```\n---\n\n#### 🧪 Example Response\n\n```http\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{\n  \"message\": \"Disabled tool: example_tool\"\n}\n\n```\n\n---\n\n## ⚙️ Configuration Options\n\n### Custom Root Path\n\nTo mount the API under a different path:\n\n```python\nset_up_component_manager(server=mcp, path=\"/admin\")\n```\n\n### Securing Endpoints with Auth Scopes\n\nIf your server uses authentication:\n\n```python\nmcp = FastMCP(name=\"Component Manager\", instructions=\"This is a test server with component manager.\", auth=auth)\nset_up_component_manager(server=mcp, required_scopes=[\"write\", \"read\"])\n```\n\n---\n\n## 🧪 Example: Enabling a Tool with Curl\n\n```bash\ncurl -X POST \\\n  -H \"Authorization: Bearer YOUR_TOKEN_HERE\" \\\n  -H \"Content-Type: application/json\" \\\n  http://localhost:8001/tools/example_tool/enable\n```\n\n---\n\n## 🧱 Working with Mounted Servers\n\nYou can also combine different configurations when working with mounted servers — for example, using different scopes:\n\n```python\nmcp = FastMCP(name=\"Component Manager\", instructions=\"This is a test server with component manager.\", auth=auth)\nset_up_component_manager(server=mcp, required_scopes=[\"mcp:write\"])\n\nmounted = FastMCP(name=\"Component Manager\", instructions=\"This is a test server with component manager.\", auth=auth)\nset_up_component_manager(server=mounted, required_scopes=[\"mounted:write\"])\n\nmcp.mount(server=mounted, namespace=\"mo\")\n```\n\nThis allows you to grant different levels of access:\n\n```bash\n# Accessing the main server gives you control over both local and mounted components\ncurl -X POST \\\n  -H \"Authorization: Bearer YOUR_TOKEN_HERE\" \\\n  -H \"Content-Type: application/json\" \\\n  http://localhost:8001/tools/mo_example_tool/enable\n\n# Accessing the mounted server gives you control only over its own components\ncurl -X POST \\\n  -H \"Authorization: Bearer YOUR_TOKEN_HERE\" \\\n  -H \"Content-Type: application/json\" \\\n  http://localhost:8002/tools/example_tool/enable\n```\n\n---\n\n## ⚙️ How It Works\n\n- `set_up_component_manager()` registers HTTP routes for tools, resources, and prompts.\n- Each endpoint calls `server.enable()` or `server.disable()` with the component name.\n- Returns a success message in JSON.\n\n---\n\n## Maintenance Notice\n\nThis module is not officially maintained by the core FastMCP team. It is an independent extension developed by [gorocode](https://github.com/gorocode).\n\nIf you encounter any issues or wish to contribute, please feel free to open an issue or submit a pull request, and kindly notify me. I'd love to stay up to date.\n\n\n## 📄 License\n\nThis module follows the license of the main [FastMCP](https://github.com/PrefectHQ/fastmcp) project."
  },
  {
    "path": "src/fastmcp/contrib/component_manager/__init__.py",
    "content": "from .component_manager import set_up_component_manager\n\n__all__ = [\"set_up_component_manager\"]\n"
  },
  {
    "path": "src/fastmcp/contrib/component_manager/component_manager.py",
    "content": "\"\"\"\nHTTP routes for enabling/disabling components in FastMCP.\n\nProvides REST endpoints for controlling component enabled state with optional\nauthentication scopes.\n\"\"\"\n\nfrom mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Mount, Route\n\nfrom fastmcp.server.server import FastMCP\n\n\ndef set_up_component_manager(\n    server: FastMCP, path: str = \"/\", required_scopes: list[str] | None = None\n) -> None:\n    \"\"\"Set up HTTP routes for enabling/disabling tools, resources, and prompts.\n\n    Args:\n        server: The FastMCP server instance.\n        path: Base path for component management routes.\n        required_scopes: Optional list of scopes required for these routes.\n            Applies only if authentication is enabled.\n\n    Routes created:\n        POST /tools/{name}/enable[?version=v1]\n        POST /tools/{name}/disable[?version=v1]\n        POST /resources/{uri}/enable[?version=v1]\n        POST /resources/{uri}/disable[?version=v1]\n        POST /prompts/{name}/enable[?version=v1]\n        POST /prompts/{name}/disable[?version=v1]\n    \"\"\"\n    if required_scopes is None:\n        # No auth - include path prefix in routes\n        routes = _build_routes(server, path)\n        server._additional_http_routes.extend(routes)\n    else:\n        # With auth - Mount handles path prefix, routes shouldn't have it\n        routes = _build_routes(server, \"/\")\n        mount = Mount(\n            path if path != \"/\" else \"\",\n            app=RequireAuthMiddleware(Starlette(routes=routes), required_scopes),\n        )\n        server._additional_http_routes.append(mount)\n\n\ndef _build_routes(server: FastMCP, base_path: str) -> list[Route]:\n    \"\"\"Build all component management routes.\"\"\"\n    prefix = base_path.rstrip(\"/\") if base_path != \"/\" else \"\"\n\n    return [\n        # Tools\n        Route(\n            f\"{prefix}/tools/{{name}}/enable\",\n            endpoint=_make_endpoint(server, \"tool\", \"enable\"),\n            methods=[\"POST\"],\n        ),\n        Route(\n            f\"{prefix}/tools/{{name}}/disable\",\n            endpoint=_make_endpoint(server, \"tool\", \"disable\"),\n            methods=[\"POST\"],\n        ),\n        # Resources\n        Route(\n            f\"{prefix}/resources/{{uri:path}}/enable\",\n            endpoint=_make_endpoint(server, \"resource\", \"enable\"),\n            methods=[\"POST\"],\n        ),\n        Route(\n            f\"{prefix}/resources/{{uri:path}}/disable\",\n            endpoint=_make_endpoint(server, \"resource\", \"disable\"),\n            methods=[\"POST\"],\n        ),\n        # Prompts\n        Route(\n            f\"{prefix}/prompts/{{name}}/enable\",\n            endpoint=_make_endpoint(server, \"prompt\", \"enable\"),\n            methods=[\"POST\"],\n        ),\n        Route(\n            f\"{prefix}/prompts/{{name}}/disable\",\n            endpoint=_make_endpoint(server, \"prompt\", \"disable\"),\n            methods=[\"POST\"],\n        ),\n    ]\n\n\ndef _make_endpoint(server: FastMCP, component_type: str, action: str):\n    \"\"\"Create an endpoint function for enabling/disabling a component type.\"\"\"\n\n    async def endpoint(request: Request) -> JSONResponse:\n        # Get name from path params (tools/prompts use 'name', resources use 'uri')\n        name = request.path_params.get(\"name\") or request.path_params.get(\"uri\")\n        version = request.query_params.get(\"version\")\n\n        # Map component type to components list\n        # Note: \"resource\" in the route can refer to either a resource or template\n        # We need to check if it's a template (contains {}) and use \"template\" if so\n        if component_type == \"resource\" and name is not None and \"{\" in name:\n            components = [\"template\"]\n        elif component_type == \"resource\":\n            components = [\"resource\"]\n        else:\n            component_map = {\n                \"tool\": [\"tool\"],\n                \"prompt\": [\"prompt\"],\n            }\n            components = component_map[component_type]\n\n        # Call server.enable() or server.disable()\n        method = getattr(server, action)\n        method(names={name} if name else None, version=version, components=components)\n\n        return JSONResponse(\n            {\"message\": f\"{action.capitalize()}d {component_type}: {name}\"}\n        )\n\n    return endpoint\n"
  },
  {
    "path": "src/fastmcp/contrib/component_manager/example.py",
    "content": "from fastmcp import FastMCP\nfrom fastmcp.contrib.component_manager import set_up_component_manager\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\nkey_pair = RSAKeyPair.generate()\n\nauth = JWTVerifier(\n    public_key=key_pair.public_key,\n    issuer=\"https://dev.example.com\",\n    audience=\"my-dev-server\",\n    required_scopes=[\"mcp:read\"],\n)\n\n# Build main server\nmcp_token = key_pair.create_token(\n    subject=\"dev-user\",\n    issuer=\"https://dev.example.com\",\n    audience=\"my-dev-server\",\n    scopes=[\"mcp:write\", \"mcp:read\"],\n)\nmcp = FastMCP(\n    name=\"Component Manager\",\n    instructions=\"This is a test server with component manager.\",\n    auth=auth,\n)\n\n# Set up main server component manager\nset_up_component_manager(server=mcp, required_scopes=[\"mcp:write\"])\n\n# Build mounted server\nmounted_token = key_pair.create_token(\n    subject=\"dev-user\",\n    issuer=\"https://dev.example.com\",\n    audience=\"my-dev-server\",\n    scopes=[\"mounted:write\", \"mcp:read\"],\n)\nmounted = FastMCP(\n    name=\"Component Manager\",\n    instructions=\"This is a test server with component manager.\",\n    auth=auth,\n)\n\n# Set up mounted server component manager\nset_up_component_manager(server=mounted, required_scopes=[\"mounted:write\"])\n\n# Mount\nmcp.mount(server=mounted, namespace=\"mo\")\n\n\n@mcp.resource(\"resource://greeting\")\ndef get_greeting() -> str:\n    \"\"\"Provides a simple greeting message.\"\"\"\n    return \"Hello from FastMCP Resources!\"\n\n\n@mounted.tool(\"greeting\")\ndef get_info() -> str:\n    \"\"\"Provides a simple info.\"\"\"\n    return \"You are using component manager contrib module!\"\n"
  },
  {
    "path": "src/fastmcp/contrib/mcp_mixin/README.md",
    "content": "from mcp.types import ToolAnnotations\n\n# MCP Mixin\n\nThis module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`).\n\nIt allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin.\n\nIncludes support for\nTools:\n* [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)\n* [annotations](https://gofastmcp.com/servers/tools#annotations-2)\n* [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)\n* [meta](https://gofastmcp.com/servers/tools#param-meta)\n\nPrompts:\n* [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)\n* [meta](https://gofastmcp.com/servers/prompts#param-meta)\n\nResources:\n* [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources)\n* [meta](https://gofastmcp.com/servers/resources#param-meta)\n  \n## Usage\n\nInherit from `MCPMixin` and use the decorators on the methods you want to register.\n\n```python\nfrom mcp.types import ToolAnnotations\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt\n\nclass MyComponent(MCPMixin):\n    @mcp_tool(name=\"my_tool\", description=\"Does something cool.\")\n    def tool_method(self):\n        return \"Tool executed!\"\n\n    # example of disabled tool\n    @mcp_tool(name=\"my_tool\", description=\"Does something cool.\", enabled=False)\n    def disabled_tool_method(self):\n        # This function can't be called by client because it's disabled\n        return \"You'll never get here!\"\n\n    # example of excluded parameter tool\n    @mcp_tool(\n        name=\"my_tool\", description=\"Does something cool.\",\n        enabled=False, exclude_args=['delete_everything'],\n    )\n    def excluded_param_tool_method(self, delete_everything=False):\n        # MCP tool calls can't pass the \"delete_everything\" argument\n        if delete_everything:\n            return \"Nothing to delete, I bet you're not a tool :)\"\n        return \"You might be a tool if...\"\n\n    # example tool w/annotations\n    @mcp_tool(\n        name=\"my_tool\", description=\"Does something cool.\",\n        annotations=ToolAnnotations(\n            title=\"Attn LLM, use this tool first!\",\n            readOnlyHint=False,\n            destructiveHint=False,\n            idempotentHint=False,\n        )\n    )\n    def tool_method(self):\n        return \"Tool executed!\"\n\n    # example tool w/everything\n    @mcp_tool(\n        name=\"my_tool\", description=\"Does something cool.\",\n        enabled=True,\n        exclude_args=['delete_all'],\n        annotations=ToolAnnotations(\n            title=\"Attn LLM, use this tool first!\",\n            readOnlyHint=False,\n            destructiveHint=False,\n            idempotentHint=False,\n        )\n    )\n    def tool_method(self, delete_all=False):\n        if delete_all:\n            return \"99 records deleted. I bet you're not a tool :)\"\n        return \"Tool executed, but you might be a tool!\"\n\n    # example tool w/ meta\n    @mcp_tool(\n        name=\"data_tool\",\n        description=\"Fetches user data from database\",\n        meta={\"version\": \"2.0\", \"category\": \"database\", \"author\": \"dev-team\"}\n    )\n    def data_tool_method(self, user_id: int):\n        return f\"Fetching data for user {user_id}\"\n\n    @mcp_resource(uri=\"component://data\")\n    def resource_method(self):\n        return {\"data\": \"some data\"}\n\n    # Disabled resource\n    @mcp_resource(uri=\"component://data\", enabled=False)\n    def resource_method(self):\n        return {\"data\": \"some data\"}\n\n    # example resource w/meta and title\n    @mcp_resource(\n        uri=\"component://config\",\n        title=\"Data resource Title,\n        meta={\"internal\": True, \"cache_ttl\": 3600, \"priority\": \"high\"}\n    )\n    def config_resource_method(self):\n        return {\"config\": \"data\"}\n\n    # prompt\n    @mcp_prompt(name=\"A prompt\")\n    def prompt_method(self, name):\n        return f\"What's up {name}?\"\n\n    # disabled prompt\n    @mcp_prompt(name=\"A prompt\", enabled=False)\n    def prompt_method(self, name):\n        return f\"What's up {name}?\"\n\n    # example prompt w/title and meta\n    @mcp_prompt(\n        name=\"analysis_prompt\",\n        title=\"Data Analysis Prompt\",\n        description=\"Analyzes data patterns\",\n        meta={\"complexity\": \"high\", \"domain\": \"analytics\", \"requires_context\": True}\n    )\n    def analysis_prompt_method(self, dataset: str):\n        return f\"Analyze the patterns in {dataset}\"\n\nmcp_server = FastMCP()\ncomponent = MyComponent()\n\n# Register all decorated methods with a prefix\n# Useful if you will have multiple instantiated objects of the same class\n# and want to avoid name collisions.\ncomponent.register_all(mcp_server, prefix=\"my_comp\") \n\n# Register without a prefix\n# component.register_all(mcp_server) \n\n# Now 'my_comp_my_tool' tool and 'my_comp+component://data' resource are registered (if prefix used)\n# Or 'my_tool' and 'component://data' are registered (if no prefix used)\n```\n\nThe `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types.\n"
  },
  {
    "path": "src/fastmcp/contrib/mcp_mixin/__init__.py",
    "content": "from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt\n\n__all__ = [\n    \"MCPMixin\",\n    \"mcp_prompt\",\n    \"mcp_resource\",\n    \"mcp_tool\",\n]\n"
  },
  {
    "path": "src/fastmcp/contrib/mcp_mixin/example.py",
    "content": "\"\"\"Sample code for FastMCP using MCPMixin.\"\"\"\n\nimport asyncio\n\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.mcp_mixin import (\n    MCPMixin,\n    mcp_prompt,\n    mcp_resource,\n    mcp_tool,\n)\n\nmcp = FastMCP()\n\n\nclass Sample(MCPMixin):\n    def __init__(self, name):\n        self.name = name\n\n    @mcp_tool()\n    def first_tool(self):\n        \"\"\"First tool description.\"\"\"\n        return f\"Executed tool {self.name}.\"\n\n    @mcp_resource(uri=\"test://test\")\n    def first_resource(self):\n        \"\"\"First resource description.\"\"\"\n        return f\"Executed resource {self.name}.\"\n\n    @mcp_prompt()\n    def first_prompt(self):\n        \"\"\"First prompt description.\"\"\"\n        return f\"here's a prompt! {self.name}.\"\n\n\nfirst_sample = Sample(\"First\")\nsecond_sample = Sample(\"Second\")\n\nfirst_sample.register_all(mcp_server=mcp, prefix=\"first\")\nsecond_sample.register_all(mcp_server=mcp, prefix=\"second\")\n\n\nasync def list_components() -> None:\n    print(\"MCP Server running with registered components...\")\n    print(\"Tools:\", list(await mcp.list_tools()))\n    print(\"Resources:\", list(await mcp.list_resources()))\n    print(\"Prompts:\", list(await mcp.list_prompts()))\n\n\nif __name__ == \"__main__\":\n    asyncio.run(list_components())\n    mcp.run()\n"
  },
  {
    "path": "src/fastmcp/contrib/mcp_mixin/mcp_mixin.py",
    "content": "\"\"\"Provides a base mixin class and decorators for easy registration of class methods with FastMCP.\"\"\"\n\nimport inspect\nimport warnings\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nimport fastmcp\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.types import get_fn_name\n\nif TYPE_CHECKING:\n    from fastmcp.server import FastMCP\n\n_MCP_REGISTRATION_TOOL_ATTR = \"_mcp_tool_registration\"\n_MCP_REGISTRATION_RESOURCE_ATTR = \"_mcp_resource_registration\"\n_MCP_REGISTRATION_PROMPT_ATTR = \"_mcp_prompt_registration\"\n\n_DEFAULT_SEPARATOR_TOOL = \"_\"\n_DEFAULT_SEPARATOR_RESOURCE = \"+\"\n_DEFAULT_SEPARATOR_PROMPT = \"_\"\n\n# Sentinel key stored in registration dicts for the mixin-only `enabled` flag.\n# Prefixed with an underscore to avoid collisions with any from_function parameter.\n_MIXIN_ENABLED_KEY = \"_mixin_enabled\"\n\n# Valid keyword arguments for each from_function, derived once at import time\n# directly from the live signatures.  They stay in sync automatically whenever\n# the underlying signatures gain or lose parameters — no manual updates needed.\n_TOOL_VALID_KWARGS: frozenset[str] = frozenset(\n    p for p in inspect.signature(Tool.from_function).parameters if p != \"fn\"\n)\n_RESOURCE_VALID_KWARGS: frozenset[str] = frozenset(\n    p\n    for p in inspect.signature(Resource.from_function).parameters\n    if p not in (\"fn\", \"uri\")\n)\n_PROMPT_VALID_KWARGS: frozenset[str] = frozenset(\n    p for p in inspect.signature(Prompt.from_function).parameters if p != \"fn\"\n)\n\n\ndef mcp_tool(\n    name: str | None = None,\n    *,\n    enabled: bool | None = None,\n    **kwargs: Any,\n) -> Callable[[Callable[..., Any]], Callable[..., Any]]:\n    \"\"\"Decorator to mark a method as an MCP tool for later registration.\n\n    Accepts all parameters supported by ``Tool.from_function``.  Any new\n    parameters added to ``Tool.from_function`` are automatically forwarded\n    without requiring changes here.\n\n    Args:\n        name: Tool name.  Defaults to the decorated method name.\n        enabled: If ``False``, the tool is skipped during registration.\n        **kwargs: Additional keyword arguments forwarded verbatim to\n            ``Tool.from_function`` (e.g. ``description``, ``tags``,\n            ``annotations``, ``auth``, ``timeout``, ``version``, …).\n\n    Raises:\n        TypeError: If an unrecognised keyword argument is supplied.  The error\n            is raised immediately at decoration time rather than later.\n    \"\"\"\n    unknown = set(kwargs) - _TOOL_VALID_KWARGS\n    if unknown:\n        raise TypeError(\n            f\"mcp_tool() got unexpected keyword argument(s): {sorted(unknown)!r}. \"\n            f\"Valid keyword arguments are: {sorted(_TOOL_VALID_KWARGS)}\"\n        )\n\n    if \"serializer\" in kwargs and fastmcp.settings.deprecation_warnings:\n        warnings.warn(\n            \"The `serializer` parameter is deprecated. \"\n            \"Return ToolResult from your tools for full control over serialization. \"\n            \"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n\n    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:\n        call_args: dict[str, Any] = {\"name\": name or get_fn_name(func), **kwargs}\n        if enabled is not None:\n            call_args[_MIXIN_ENABLED_KEY] = enabled\n        setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args)\n        return func\n\n    return decorator\n\n\ndef mcp_resource(\n    uri: str,\n    *,\n    name: str | None = None,\n    enabled: bool | None = None,\n    **kwargs: Any,\n) -> Callable[[Callable[..., Any]], Callable[..., Any]]:\n    \"\"\"Decorator to mark a method as an MCP resource for later registration.\n\n    Accepts all parameters supported by ``Resource.from_function``.  Any new\n    parameters added to ``Resource.from_function`` are automatically forwarded\n    without requiring changes here.\n\n    Args:\n        uri: Resource URI (required).\n        name: Resource name.  Defaults to the decorated method name.\n        enabled: If ``False``, the resource is skipped during registration.\n        **kwargs: Additional keyword arguments forwarded verbatim to\n            ``Resource.from_function`` (e.g. ``description``, ``tags``,\n            ``mime_type``, ``auth``, ``version``, …).\n\n    Raises:\n        TypeError: If an unrecognised keyword argument is supplied.  The error\n            is raised immediately at decoration time rather than later.\n    \"\"\"\n    unknown = set(kwargs) - _RESOURCE_VALID_KWARGS\n    if unknown:\n        raise TypeError(\n            f\"mcp_resource() got unexpected keyword argument(s): {sorted(unknown)!r}. \"\n            f\"Valid keyword arguments are: {sorted(_RESOURCE_VALID_KWARGS)}\"\n        )\n\n    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:\n        call_args: dict[str, Any] = {\n            \"uri\": uri,\n            \"name\": name or get_fn_name(func),\n            **kwargs,\n        }\n        if enabled is not None:\n            call_args[_MIXIN_ENABLED_KEY] = enabled\n        setattr(func, _MCP_REGISTRATION_RESOURCE_ATTR, call_args)\n        return func\n\n    return decorator\n\n\ndef mcp_prompt(\n    name: str | None = None,\n    *,\n    enabled: bool | None = None,\n    **kwargs: Any,\n) -> Callable[[Callable[..., Any]], Callable[..., Any]]:\n    \"\"\"Decorator to mark a method as an MCP prompt for later registration.\n\n    Accepts all parameters supported by ``Prompt.from_function``.  Any new\n    parameters added to ``Prompt.from_function`` are automatically forwarded\n    without requiring changes here.\n\n    Args:\n        name: Prompt name.  Defaults to the decorated method name.\n        enabled: If ``False``, the prompt is skipped during registration.\n        **kwargs: Additional keyword arguments forwarded verbatim to\n            ``Prompt.from_function`` (e.g. ``description``, ``tags``,\n            ``auth``, ``version``, …).\n\n    Raises:\n        TypeError: If an unrecognised keyword argument is supplied.  The error\n            is raised immediately at decoration time rather than later.\n    \"\"\"\n    unknown = set(kwargs) - _PROMPT_VALID_KWARGS\n    if unknown:\n        raise TypeError(\n            f\"mcp_prompt() got unexpected keyword argument(s): {sorted(unknown)!r}. \"\n            f\"Valid keyword arguments are: {sorted(_PROMPT_VALID_KWARGS)}\"\n        )\n\n    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:\n        call_args: dict[str, Any] = {\"name\": name or get_fn_name(func), **kwargs}\n        if enabled is not None:\n            call_args[_MIXIN_ENABLED_KEY] = enabled\n        setattr(func, _MCP_REGISTRATION_PROMPT_ATTR, call_args)\n        return func\n\n    return decorator\n\n\nclass MCPMixin:\n    \"\"\"Base mixin class for objects that can register tools, resources, and prompts\n    with a FastMCP server instance using decorators.\n\n    This mixin provides methods like ``register_all``, ``register_tools``, etc.,\n    which iterate over the methods of the inheriting class, find methods\n    decorated with ``@mcp_tool``, ``@mcp_resource``, or ``@mcp_prompt``, and\n    register them with the provided FastMCP server instance.\n    \"\"\"\n\n    def _get_methods_to_register(self, registration_type: str):\n        \"\"\"Retrieves all methods marked for a specific registration type.\"\"\"\n        return [\n            (\n                getattr(self, method_name),\n                getattr(getattr(self, method_name), registration_type).copy(),\n            )\n            for method_name in dir(self)\n            if callable(getattr(self, method_name))\n            and hasattr(getattr(self, method_name), registration_type)\n        ]\n\n    def register_tools(\n        self,\n        mcp_server: \"FastMCP\",\n        prefix: str | None = None,\n        separator: str = _DEFAULT_SEPARATOR_TOOL,\n    ) -> None:\n        \"\"\"Registers all methods marked with @mcp_tool with the FastMCP server.\n\n        Args:\n            mcp_server: The FastMCP server instance to register tools with.\n            prefix: Optional prefix to prepend to tool names.  If provided, the\n                final name will be ``f\"{prefix}{separator}{original_name}\"``.\n            separator: The separator string used between prefix and original name.\n                Defaults to ``'_'``.\n        \"\"\"\n        for method, registration_info in self._get_methods_to_register(\n            _MCP_REGISTRATION_TOOL_ATTR\n        ):\n            if prefix:\n                registration_info[\"name\"] = (\n                    f\"{prefix}{separator}{registration_info['name']}\"\n                )\n\n            enabled = registration_info.pop(_MIXIN_ENABLED_KEY, True)\n            if enabled is False:\n                continue\n\n            tool = Tool.from_function(fn=method, **registration_info)\n            mcp_server.add_tool(tool)\n\n    def register_resources(\n        self,\n        mcp_server: \"FastMCP\",\n        prefix: str | None = None,\n        separator: str = _DEFAULT_SEPARATOR_RESOURCE,\n    ) -> None:\n        \"\"\"Registers all methods marked with @mcp_resource with the FastMCP server.\n\n        Args:\n            mcp_server: The FastMCP server instance to register resources with.\n            prefix: Optional prefix to prepend to resource names and URIs.  If\n                provided, the final name will be\n                ``f\"{prefix}{separator}{original_name}\"`` and the final URI will\n                be ``f\"{prefix}{separator}{original_uri}\"``.\n            separator: The separator string used between prefix and original\n                name/URI.  Defaults to ``'+'``.\n        \"\"\"\n        for method, registration_info in self._get_methods_to_register(\n            _MCP_REGISTRATION_RESOURCE_ATTR\n        ):\n            if prefix:\n                registration_info[\"name\"] = (\n                    f\"{prefix}{separator}{registration_info['name']}\"\n                )\n                registration_info[\"uri\"] = (\n                    f\"{prefix}{separator}{registration_info['uri']}\"\n                )\n\n            enabled = registration_info.pop(_MIXIN_ENABLED_KEY, True)\n            if enabled is False:\n                continue\n\n            resource = Resource.from_function(fn=method, **registration_info)\n            mcp_server.add_resource(resource)\n\n    def register_prompts(\n        self,\n        mcp_server: \"FastMCP\",\n        prefix: str | None = None,\n        separator: str = _DEFAULT_SEPARATOR_PROMPT,\n    ) -> None:\n        \"\"\"Registers all methods marked with @mcp_prompt with the FastMCP server.\n\n        Args:\n            mcp_server: The FastMCP server instance to register prompts with.\n            prefix: Optional prefix to prepend to prompt names.  If provided,\n                the final name will be ``f\"{prefix}{separator}{original_name}\"``.\n            separator: The separator string used between prefix and original name.\n                Defaults to ``'_'``.\n        \"\"\"\n        for method, registration_info in self._get_methods_to_register(\n            _MCP_REGISTRATION_PROMPT_ATTR\n        ):\n            if prefix:\n                registration_info[\"name\"] = (\n                    f\"{prefix}{separator}{registration_info['name']}\"\n                )\n\n            enabled = registration_info.pop(_MIXIN_ENABLED_KEY, True)\n            if enabled is False:\n                continue\n\n            prompt = Prompt.from_function(fn=method, **registration_info)\n            mcp_server.add_prompt(prompt)\n\n    def register_all(\n        self,\n        mcp_server: \"FastMCP\",\n        prefix: str | None = None,\n        tool_separator: str = _DEFAULT_SEPARATOR_TOOL,\n        resource_separator: str = _DEFAULT_SEPARATOR_RESOURCE,\n        prompt_separator: str = _DEFAULT_SEPARATOR_PROMPT,\n    ) -> None:\n        \"\"\"Registers all marked tools, resources, and prompts with the server.\n\n        This method calls ``register_tools``, ``register_resources``, and\n        ``register_prompts`` internally, passing the provided prefix and\n        separators.\n\n        Args:\n            mcp_server: The FastMCP server instance to register with.\n            prefix: Optional prefix applied to all registered items.\n            tool_separator: Separator for tool names (defaults to ``'_'``).\n            resource_separator: Separator for resource names/URIs (defaults to ``'+'``).\n            prompt_separator: Separator for prompt names (defaults to ``'_'``).\n        \"\"\"\n        self.register_tools(mcp_server, prefix=prefix, separator=tool_separator)\n        self.register_resources(mcp_server, prefix=prefix, separator=resource_separator)\n        self.register_prompts(mcp_server, prefix=prefix, separator=prompt_separator)\n"
  },
  {
    "path": "src/fastmcp/decorators.py",
    "content": "\"\"\"Shared decorator utilities for FastMCP.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom typing import TYPE_CHECKING, Any, Protocol, runtime_checkable\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.function_prompt import PromptMeta\n    from fastmcp.resources.function_resource import ResourceMeta\n    from fastmcp.server.tasks.config import TaskConfig\n    from fastmcp.tools.function_tool import ToolMeta\n\n    FastMCPMeta = ToolMeta | ResourceMeta | PromptMeta\n\n\ndef resolve_task_config(task: bool | TaskConfig | None) -> bool | TaskConfig:\n    \"\"\"Resolve task config, defaulting None to False.\"\"\"\n    return task if task is not None else False\n\n\n@runtime_checkable\nclass HasFastMCPMeta(Protocol):\n    \"\"\"Protocol for callables decorated with FastMCP metadata.\"\"\"\n\n    __fastmcp__: Any\n\n\ndef get_fastmcp_meta(fn: Any) -> Any | None:\n    \"\"\"Extract FastMCP metadata from a function, handling bound methods and wrappers.\"\"\"\n    if hasattr(fn, \"__fastmcp__\"):\n        return fn.__fastmcp__\n    if hasattr(fn, \"__func__\") and hasattr(fn.__func__, \"__fastmcp__\"):\n        return fn.__func__.__fastmcp__\n    try:\n        unwrapped = inspect.unwrap(fn)\n        if unwrapped is not fn and hasattr(unwrapped, \"__fastmcp__\"):\n            return unwrapped.__fastmcp__\n    except ValueError:\n        pass\n    return None\n"
  },
  {
    "path": "src/fastmcp/dependencies.py",
    "content": "\"\"\"Dependency injection exports for FastMCP.\n\nThis module re-exports dependency injection symbols to provide a clean,\ncentralized import location for all dependency-related functionality.\n\nDI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket\nusing the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,\nCurrentWorker) and background task execution require fastmcp[tasks].\n\"\"\"\n\nfrom uncalled_for import Dependency, Depends, Shared\n\nfrom fastmcp.server.dependencies import (\n    CurrentAccessToken,\n    CurrentContext,\n    CurrentDocket,\n    CurrentFastMCP,\n    CurrentHeaders,\n    CurrentRequest,\n    CurrentWorker,\n    Progress,\n    ProgressLike,\n    TokenClaim,\n)\n\n__all__ = [\n    \"CurrentAccessToken\",\n    \"CurrentContext\",\n    \"CurrentDocket\",\n    \"CurrentFastMCP\",\n    \"CurrentHeaders\",\n    \"CurrentRequest\",\n    \"CurrentWorker\",\n    \"Dependency\",\n    \"Depends\",\n    \"Progress\",\n    \"ProgressLike\",\n    \"Shared\",\n    \"TokenClaim\",\n]\n"
  },
  {
    "path": "src/fastmcp/exceptions.py",
    "content": "\"\"\"Custom exceptions for FastMCP.\"\"\"\n\nfrom mcp import McpError  # noqa: F401\n\n\nclass FastMCPError(Exception):\n    \"\"\"Base error for FastMCP.\"\"\"\n\n\nclass ValidationError(FastMCPError):\n    \"\"\"Error in validating parameters or return values.\"\"\"\n\n\nclass ResourceError(FastMCPError):\n    \"\"\"Error in resource operations.\"\"\"\n\n\nclass ToolError(FastMCPError):\n    \"\"\"Error in tool operations.\"\"\"\n\n\nclass PromptError(FastMCPError):\n    \"\"\"Error in prompt operations.\"\"\"\n\n\nclass InvalidSignature(Exception):\n    \"\"\"Invalid signature for use with FastMCP.\"\"\"\n\n\nclass ClientError(Exception):\n    \"\"\"Error in client operations.\"\"\"\n\n\nclass NotFoundError(Exception):\n    \"\"\"Object not found.\"\"\"\n\n\nclass DisabledError(Exception):\n    \"\"\"Object is disabled.\"\"\"\n\n\nclass AuthorizationError(FastMCPError):\n    \"\"\"Error when authorization check fails.\"\"\"\n"
  },
  {
    "path": "src/fastmcp/experimental/__init__.py",
    "content": ""
  },
  {
    "path": "src/fastmcp/experimental/sampling/__init__.py",
    "content": ""
  },
  {
    "path": "src/fastmcp/experimental/sampling/handlers/__init__.py",
    "content": "# Re-export for backwards compatibility\n# The canonical location is now fastmcp.client.sampling.handlers\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\n__all__ = [\"OpenAISamplingHandler\"]\n"
  },
  {
    "path": "src/fastmcp/experimental/sampling/handlers/openai.py",
    "content": "# Re-export for backwards compatibility\n# The canonical location is now fastmcp.client.sampling.handlers.openai\nfrom fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler\n\n__all__ = [\"OpenAISamplingHandler\"]\n"
  },
  {
    "path": "src/fastmcp/experimental/server/openapi/__init__.py",
    "content": "\"\"\"Deprecated: Import from fastmcp.server.providers.openapi instead.\"\"\"\n\nimport warnings\n\n# Deprecated in 2.14 when OpenAPI support was promoted out of experimental\nwarnings.warn(\n    \"Importing from fastmcp.experimental.server.openapi is deprecated. \"\n    \"Import from fastmcp.server.providers.openapi instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\n# Import from canonical location\nfrom fastmcp.server.openapi.server import FastMCPOpenAPI as FastMCPOpenAPI  # noqa: E402\nfrom fastmcp.server.providers.openapi import (  # noqa: E402\n    ComponentFn as ComponentFn,\n    MCPType as MCPType,\n    OpenAPIResource as OpenAPIResource,\n    OpenAPIResourceTemplate as OpenAPIResourceTemplate,\n    OpenAPITool as OpenAPITool,\n    RouteMap as RouteMap,\n    RouteMapFn as RouteMapFn,\n)\nfrom fastmcp.server.providers.openapi.routing import (  # noqa: E402\n    DEFAULT_ROUTE_MAPPINGS as DEFAULT_ROUTE_MAPPINGS,\n    _determine_route_type as _determine_route_type,\n)\n\n__all__ = [\n    \"DEFAULT_ROUTE_MAPPINGS\",\n    \"ComponentFn\",\n    \"FastMCPOpenAPI\",\n    \"MCPType\",\n    \"OpenAPIResource\",\n    \"OpenAPIResourceTemplate\",\n    \"OpenAPITool\",\n    \"RouteMap\",\n    \"RouteMapFn\",\n    \"_determine_route_type\",\n]\n"
  },
  {
    "path": "src/fastmcp/experimental/transforms/__init__.py",
    "content": "\n"
  },
  {
    "path": "src/fastmcp/experimental/transforms/code_mode.py",
    "content": "import importlib\nimport json\nfrom collections.abc import Awaitable, Callable, Sequence\nfrom typing import Annotated, Any, Literal, Protocol\n\nfrom mcp.types import TextContent\nfrom pydantic import Field\n\nfrom fastmcp.exceptions import NotFoundError\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.transforms import GetToolNext\nfrom fastmcp.server.transforms.catalog import CatalogTransform\nfrom fastmcp.server.transforms.search.base import (\n    serialize_tools_for_output_json,\n    serialize_tools_for_output_markdown,\n)\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.async_utils import is_coroutine_function\nfrom fastmcp.utilities.versions import VersionSpec\n\n# ---------------------------------------------------------------------------\n# Type aliases\n# ---------------------------------------------------------------------------\n\nGetToolCatalog = Callable[[Context], Awaitable[Sequence[Tool]]]\n\"\"\"Async callable that returns the auth-filtered tool catalog.\"\"\"\n\nSearchFn = Callable[[Sequence[Tool], str], Awaitable[Sequence[Tool]]]\n\"\"\"Async callable that searches a tool sequence by query string.\"\"\"\n\nDiscoveryToolFactory = Callable[[GetToolCatalog], Tool]\n\"\"\"Factory that receives catalog access and returns a synthetic Tool.\"\"\"\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _ensure_async(fn: Callable[..., Any]) -> Callable[..., Any]:\n    if is_coroutine_function(fn):\n        return fn\n\n    async def wrapper(*args: Any, **kwargs: Any) -> Any:\n        return fn(*args, **kwargs)\n\n    return wrapper\n\n\ndef _unwrap_tool_result(result: ToolResult) -> dict[str, Any] | str:\n    \"\"\"Convert a ToolResult for use in the sandbox.\n\n    - Output schema present → structured_content dict (matches the schema)\n    - Otherwise → concatenated text content as a string\n    \"\"\"\n    if result.structured_content is not None:\n        return result.structured_content\n\n    parts: list[str] = []\n    for content in result.content:\n        if isinstance(content, TextContent):\n            parts.append(content.text)\n        else:\n            parts.append(str(content))\n    return \"\\n\".join(parts)\n\n\n# ---------------------------------------------------------------------------\n# Sandbox providers\n# ---------------------------------------------------------------------------\n\n\nclass SandboxProvider(Protocol):\n    \"\"\"Interface for executing LLM-generated Python code in a sandbox.\n\n    WARNING: The ``code`` parameter passed to ``run`` contains untrusted,\n    LLM-generated Python.  Implementations MUST execute it in an isolated\n    sandbox — never with plain ``exec()``.  Use ``MontySandboxProvider``\n    (backed by ``pydantic-monty``) for production workloads.\n    \"\"\"\n\n    async def run(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        external_functions: dict[str, Callable[..., Any]] | None = None,\n    ) -> Any: ...\n\n\nclass MontySandboxProvider:\n    \"\"\"Sandbox provider backed by `pydantic-monty`.\n\n    Args:\n        limits: Resource limits for sandbox execution. Supported keys:\n            ``max_duration_secs`` (float), ``max_allocations`` (int),\n            ``max_memory`` (int), ``max_recursion_depth`` (int),\n            ``gc_interval`` (int).  All are optional; omit a key to\n            leave that limit uncapped.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        limits: dict[str, Any] | None = None,\n    ) -> None:\n        self.limits = limits\n\n    async def run(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        external_functions: dict[str, Callable[..., Any]] | None = None,\n    ) -> Any:\n        try:\n            pydantic_monty = importlib.import_module(\"pydantic_monty\")\n        except ModuleNotFoundError as exc:\n            raise ImportError(\n                \"CodeMode requires pydantic-monty for the Monty sandbox provider. \"\n                \"Install it with `fastmcp[code-mode]` or pass a custom SandboxProvider.\"\n            ) from exc\n\n        inputs = inputs or {}\n        async_functions = {\n            key: _ensure_async(value)\n            for key, value in (external_functions or {}).items()\n        }\n\n        monty = pydantic_monty.Monty(\n            code,\n            inputs=list(inputs.keys()),\n        )\n        run_kwargs: dict[str, Any] = {\"external_functions\": async_functions}\n        if inputs:\n            run_kwargs[\"inputs\"] = inputs\n        if self.limits is not None:\n            run_kwargs[\"limits\"] = self.limits\n        return await pydantic_monty.run_monty_async(monty, **run_kwargs)\n\n\n# ---------------------------------------------------------------------------\n# Built-in discovery tools\n# ---------------------------------------------------------------------------\n\n\nToolDetailLevel = Literal[\"brief\", \"detailed\", \"full\"]\n\"\"\"Detail level for discovery tool output.\n\n- ``\"brief\"``: tool names and one-line descriptions\n- ``\"detailed\"``: compact markdown with parameter names, types, and required markers\n- ``\"full\"``: complete JSON schema\n\"\"\"\n\n\ndef _render_tools(tools: Sequence[Tool], detail: ToolDetailLevel) -> str:\n    \"\"\"Render tools at the requested detail level.\n\n    The same detail value produces the same output format regardless of\n    which discovery tool calls this, so ``detail=\"detailed\"`` on Search\n    gives identical formatting to ``detail=\"detailed\"`` on GetSchemas.\n    \"\"\"\n    if not tools:\n        if detail == \"full\":\n            return json.dumps([], indent=2)\n        return \"No tools matched the query.\"\n    if detail == \"full\":\n        return json.dumps(serialize_tools_for_output_json(tools), indent=2)\n    if detail == \"detailed\":\n        return serialize_tools_for_output_markdown(tools)\n    # brief\n    lines: list[str] = []\n    for tool in tools:\n        desc = f\": {tool.description}\" if tool.description else \"\"\n        lines.append(f\"- {tool.name}{desc}\")\n    return \"\\n\".join(lines)\n\n\nclass Search:\n    \"\"\"Discovery tool factory that searches the catalog by query.\n\n    Args:\n        search_fn: Async callable ``(tools, query) -> matching_tools``.\n            Defaults to BM25 ranking.\n        name: Name of the synthetic tool exposed to the LLM.\n        default_detail: Default detail level for search results.\n            ``\"brief\"`` returns tool names and descriptions only.\n            ``\"detailed\"`` returns compact markdown with parameter schemas.\n            ``\"full\"`` returns complete JSON tool definitions.\n        default_limit: Maximum number of results to return.\n            The LLM can override this per call.  ``None`` means no limit.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        search_fn: SearchFn | None = None,\n        name: str = \"search\",\n        default_detail: ToolDetailLevel | None = None,\n        default_limit: int | None = None,\n    ) -> None:\n        if search_fn is None:\n            from fastmcp.server.transforms.search.bm25 import BM25SearchTransform\n\n            _bm25 = BM25SearchTransform(max_results=default_limit or 50)\n            search_fn = _bm25._search\n        self._search_fn = search_fn\n        self._name = name\n        self._default_detail: ToolDetailLevel = default_detail or \"brief\"\n        self._default_limit = default_limit\n\n    def __call__(self, get_catalog: GetToolCatalog) -> Tool:\n        search_fn = self._search_fn\n        default_detail = self._default_detail\n        default_limit = self._default_limit\n\n        async def search(\n            query: Annotated[str, \"Search query to find available tools\"],\n            tags: Annotated[\n                list[str] | None,\n                \"Filter to tools with any of these tags before searching\",\n            ] = None,\n            detail: Annotated[\n                ToolDetailLevel,\n                \"'brief' for names and descriptions, 'detailed' for parameter schemas as markdown, 'full' for complete JSON schemas\",\n            ] = default_detail,\n            limit: Annotated[\n                int | None,\n                \"Maximum number of results to return\",\n            ] = default_limit,\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str:\n            \"\"\"Search for available tools by query.\n\n            Returns matching tools ranked by relevance.\n            \"\"\"\n            catalog = await get_catalog(ctx)\n            catalog_size = len(catalog)\n            tools: Sequence[Tool] = catalog\n            if tags:\n                tag_set = set(tags)\n                has_untagged = \"untagged\" in tag_set\n                real_tags = tag_set - {\"untagged\"}\n                tools = [\n                    t\n                    for t in tools\n                    if (t.tags & real_tags) or (has_untagged and not t.tags)\n                ]\n            results = await search_fn(tools, query)\n            if limit is not None:\n                results = results[:limit]\n            rendered = _render_tools(results, detail)\n            if len(results) < catalog_size and detail != \"full\":\n                n = len(results)\n                rendered = f\"{n} of {catalog_size} tools:\\n\\n{rendered}\"\n            return rendered\n\n        return Tool.from_function(fn=search, name=self._name)\n\n\nclass GetSchemas:\n    \"\"\"Discovery tool factory that returns schemas for tools by name.\n\n    Args:\n        name: Name of the synthetic tool exposed to the LLM.\n        default_detail: Default detail level for schema results.\n            ``\"brief\"`` returns tool names and descriptions only.\n            ``\"detailed\"`` renders compact markdown with parameter names,\n            types, and required markers.\n            ``\"full\"`` returns the complete JSON schema.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        name: str = \"get_schema\",\n        default_detail: ToolDetailLevel | None = None,\n    ) -> None:\n        self._name = name\n        self._default_detail: ToolDetailLevel = default_detail or \"detailed\"\n\n    def __call__(self, get_catalog: GetToolCatalog) -> Tool:\n        default_detail = self._default_detail\n\n        async def get_schema(\n            tools: Annotated[\n                list[str],\n                \"List of tool names to get schemas for\",\n            ],\n            detail: Annotated[\n                ToolDetailLevel,\n                \"'brief' for names and descriptions, 'detailed' for parameter schemas as markdown, 'full' for complete JSON schemas\",\n            ] = default_detail,\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str:\n            \"\"\"Get parameter schemas for specific tools.\n\n            Use after searching to get the detail needed to call a tool.\n            \"\"\"\n            catalog = await get_catalog(ctx)\n            catalog_by_name = {t.name: t for t in catalog}\n            matched = [catalog_by_name[n] for n in tools if n in catalog_by_name]\n            not_found = [n for n in tools if n not in catalog_by_name]\n\n            if not matched and not_found:\n                return f\"Tools not found: {', '.join(not_found)}\"\n\n            if detail == \"full\":\n                data = serialize_tools_for_output_json(matched)\n                if not_found:\n                    data.append({\"not_found\": not_found})\n                return json.dumps(data, indent=2)\n\n            result = _render_tools(matched, detail)\n            if not_found:\n                result += f\"\\n\\nTools not found: {', '.join(not_found)}\"\n            return result\n\n        return Tool.from_function(fn=get_schema, name=self._name)\n\n\nclass GetTags:\n    \"\"\"Discovery tool factory that lists tool tags from the catalog.\n\n    Reads ``tool.tags`` from the catalog and groups tools by tag. Tools\n    without tags appear under ``\"untagged\"``.\n\n    Args:\n        name: Name of the synthetic tool exposed to the LLM.\n        default_detail: Default detail level.\n            ``\"brief\"`` returns tag names with tool counts.\n            ``\"full\"`` lists all tools under each tag.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        name: str = \"tags\",\n        default_detail: Literal[\"brief\", \"full\"] | None = None,\n    ) -> None:\n        self._name = name\n        self._default_detail: Literal[\"brief\", \"full\"] = default_detail or \"brief\"\n\n    def __call__(self, get_catalog: GetToolCatalog) -> Tool:\n        default_detail = self._default_detail\n\n        async def tags(\n            detail: Annotated[\n                Literal[\"brief\", \"full\"],\n                \"Level of detail: 'brief' for tag names and counts, 'full' for tools listed under each tag\",\n            ] = default_detail,\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str:\n            \"\"\"List available tool tags.\n\n            Use to browse available tools by tag before searching.\n            \"\"\"\n            catalog = await get_catalog(ctx)\n            by_tag: dict[str, list[Tool]] = {}\n            for tool in catalog:\n                if tool.tags:\n                    for tag in tool.tags:\n                        by_tag.setdefault(tag, []).append(tool)\n                else:\n                    by_tag.setdefault(\"untagged\", []).append(tool)\n\n            if not by_tag:\n                return \"No tools available.\"\n\n            if detail == \"brief\":\n                lines = [\n                    f\"- {tag} ({len(tools)} tool{'s' if len(tools) != 1 else ''})\"\n                    for tag, tools in sorted(by_tag.items())\n                ]\n                return \"\\n\".join(lines)\n\n            blocks: list[str] = []\n            for tag, tools in sorted(by_tag.items()):\n                lines = [f\"### {tag}\"]\n                for tool in tools:\n                    desc = f\": {tool.description}\" if tool.description else \"\"\n                    lines.append(f\"- {tool.name}{desc}\")\n                blocks.append(\"\\n\".join(lines))\n            return \"\\n\\n\".join(blocks)\n\n        return Tool.from_function(fn=tags, name=self._name)\n\n\nclass ListTools:\n    \"\"\"Discovery tool factory that lists all tools in the catalog.\n\n    Args:\n        name: Name of the synthetic tool exposed to the LLM.\n        default_detail: Default detail level.\n            ``\"brief\"`` returns tool names and one-line descriptions.\n            ``\"detailed\"`` returns compact markdown with parameter schemas.\n            ``\"full\"`` returns the complete JSON schema.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        name: str = \"list_tools\",\n        default_detail: ToolDetailLevel | None = None,\n    ) -> None:\n        self._name = name\n        self._default_detail: ToolDetailLevel = default_detail or \"brief\"\n\n    def __call__(self, get_catalog: GetToolCatalog) -> Tool:\n        default_detail = self._default_detail\n\n        async def list_tools(\n            detail: Annotated[\n                ToolDetailLevel,\n                \"'brief' for names and descriptions, 'detailed' for parameter schemas as markdown, 'full' for complete JSON schemas\",\n            ] = default_detail,\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str:\n            \"\"\"List all available tools.\n\n            Use to see the full catalog before searching or calling tools.\n            \"\"\"\n            catalog = await get_catalog(ctx)\n            return _render_tools(catalog, detail)\n\n        return Tool.from_function(fn=list_tools, name=self._name)\n\n\n# ---------------------------------------------------------------------------\n# CodeMode\n# ---------------------------------------------------------------------------\n\n\ndef _default_discovery_tools() -> list[DiscoveryToolFactory]:\n    return [Search(), GetSchemas()]\n\n\nclass CodeMode(CatalogTransform):\n    \"\"\"Transform that collapses all tools into discovery + execute meta-tools.\n\n    Discovery tools are composable via the ``discovery_tools`` parameter.\n    Each is a callable that receives catalog access and returns a ``Tool``.\n    By default, ``Search`` and ``GetSchemas`` are included for\n    progressive disclosure: search finds candidates, get_schema retrieves\n    parameter details, and execute runs code.\n\n    The ``execute`` tool is always present and provides a sandboxed Python\n    environment with ``call_tool(name, params)`` in scope.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        sandbox_provider: SandboxProvider | None = None,\n        discovery_tools: list[DiscoveryToolFactory] | None = None,\n        execute_tool_name: str = \"execute\",\n        execute_description: str | None = None,\n    ) -> None:\n        super().__init__()\n        self.execute_tool_name = execute_tool_name\n        self.execute_description = execute_description\n        self.sandbox_provider = sandbox_provider or MontySandboxProvider()\n\n        self._discovery_factories = (\n            discovery_tools\n            if discovery_tools is not None\n            else _default_discovery_tools()\n        )\n        self._built_discovery_tools: list[Tool] | None = None\n        self._cached_execute_tool: Tool | None = None\n\n    def _build_discovery_tools(self) -> list[Tool]:\n        if self._built_discovery_tools is None:\n            tools = [\n                factory(self.get_tool_catalog) for factory in self._discovery_factories\n            ]\n            names = {t.name for t in tools}\n            if self.execute_tool_name in names:\n                raise ValueError(\n                    f\"Discovery tool name '{self.execute_tool_name}' \"\n                    f\"collides with execute_tool_name.\"\n                )\n            if len(names) != len(tools):\n                raise ValueError(\"Discovery tools must have unique names.\")\n            self._built_discovery_tools = tools\n        return self._built_discovery_tools\n\n    async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        return [*self._build_discovery_tools(), self._get_execute_tool()]\n\n    async def get_tool(\n        self,\n        name: str,\n        call_next: GetToolNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> Tool | None:\n        for tool in self._build_discovery_tools():\n            if tool.name == name:\n                return tool\n        if name == self.execute_tool_name:\n            return self._get_execute_tool()\n        return await call_next(name, version=version)\n\n    def _build_execute_description(self) -> str:\n        if self.execute_description is not None:\n            return self.execute_description\n\n        return (\n            \"Chain `await call_tool(...)` calls in one Python block; prefer returning the final answer from a single block.\\n\"\n            \"Use `return` to produce output.\\n\"\n            \"Only `call_tool(tool_name: str, params: dict) -> Any` is available in scope.\"\n        )\n\n    @staticmethod\n    def _find_tool(name: str, tools: Sequence[Tool]) -> Tool | None:\n        \"\"\"Find a tool by name from a pre-fetched list.\"\"\"\n        for tool in tools:\n            if tool.name == name:\n                return tool\n        return None\n\n    def _get_execute_tool(self) -> Tool:\n        if self._cached_execute_tool is None:\n            self._cached_execute_tool = self._make_execute_tool()\n        return self._cached_execute_tool\n\n    def _make_execute_tool(self) -> Tool:\n        transform = self\n\n        async def execute(\n            code: Annotated[\n                str,\n                Field(\n                    description=(\n                        \"Python async code to execute tool calls via call_tool(name, arguments)\"\n                    )\n                ),\n            ],\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> Any:\n            \"\"\"Execute tool calls using Python code.\"\"\"\n\n            async def call_tool(tool_name: str, params: dict[str, Any]) -> Any:\n                backend_tools = await transform.get_tool_catalog(ctx)\n                tool = transform._find_tool(tool_name, backend_tools)\n                if tool is None:\n                    raise NotFoundError(f\"Unknown tool: {tool_name}\")\n\n                result = await ctx.fastmcp.call_tool(tool.name, params)\n                return _unwrap_tool_result(result)\n\n            return await transform.sandbox_provider.run(\n                code,\n                external_functions={\"call_tool\": call_tool},\n            )\n\n        return Tool.from_function(\n            fn=execute,\n            name=self.execute_tool_name,\n            description=self._build_execute_description(),\n        )\n\n\n__all__ = [\n    \"CodeMode\",\n    \"GetSchemas\",\n    \"GetTags\",\n    \"GetToolCatalog\",\n    \"ListTools\",\n    \"MontySandboxProvider\",\n    \"SandboxProvider\",\n    \"Search\",\n]\n"
  },
  {
    "path": "src/fastmcp/experimental/utilities/openapi/__init__.py",
    "content": "\"\"\"Deprecated: Import from fastmcp.utilities.openapi instead.\"\"\"\n\nimport warnings\n\nfrom fastmcp.utilities.openapi import (\n    HTTPRoute,\n    HttpMethod,\n    ParameterInfo,\n    ParameterLocation,\n    RequestBodyInfo,\n    ResponseInfo,\n    extract_output_schema_from_responses,\n    parse_openapi_to_http_routes,\n    _combine_schemas,\n)\n\n# Deprecated in 2.14 when OpenAPI support was promoted out of experimental\nwarnings.warn(\n    \"Importing from fastmcp.experimental.utilities.openapi is deprecated. \"\n    \"Import from fastmcp.utilities.openapi instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\n__all__ = [\n    \"HTTPRoute\",\n    \"HttpMethod\",\n    \"ParameterInfo\",\n    \"ParameterLocation\",\n    \"RequestBodyInfo\",\n    \"ResponseInfo\",\n    \"_combine_schemas\",\n    \"extract_output_schema_from_responses\",\n    \"parse_openapi_to_http_routes\",\n]\n"
  },
  {
    "path": "src/fastmcp/mcp_config.py",
    "content": "\"\"\"Canonical MCP Configuration Format.\n\nThis module defines the standard configuration format for Model Context Protocol (MCP) servers.\nIt provides a client-agnostic, extensible format that can be used across all MCP implementations.\n\nThe configuration format supports both stdio and remote (HTTP/SSE) transports, with comprehensive\nfield definitions for server metadata, authentication, and execution parameters.\n\nExample configuration:\n```json\n{\n    \"mcpServers\": {\n        \"my-server\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@my/mcp-server\"],\n            \"env\": {\"API_KEY\": \"secret\"},\n            \"timeout\": 30000,\n            \"description\": \"My MCP server\"\n        }\n    }\n}\n```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime\nimport re\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Annotated, Any, Literal, cast\nfrom urllib.parse import urlparse\n\nimport httpx\nfrom pydantic import (\n    AnyUrl,\n    BaseModel,\n    ConfigDict,\n    Field,\n    model_validator,\n)\nfrom typing_extensions import Self, override\n\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\nfrom fastmcp.utilities.types import FastMCPBaseModel\n\nif TYPE_CHECKING:\n    from fastmcp.client.transports import (\n        ClientTransport,\n        SSETransport,\n        StdioTransport,\n        StreamableHttpTransport,\n    )\n    from fastmcp.server.server import FastMCP\n\n\ndef infer_transport_type_from_url(\n    url: str | AnyUrl,\n) -> Literal[\"http\", \"sse\"]:\n    \"\"\"\n    Infer the appropriate transport type from the given URL.\n    \"\"\"\n    url = str(url)\n    if not url.startswith(\"http\"):\n        raise ValueError(f\"Invalid URL: {url}\")\n\n    parsed_url = urlparse(url)\n    path = parsed_url.path\n\n    # Match /sse followed by /, ?, &, or end of string\n    if re.search(r\"/sse(/|\\?|&|$)\", path):\n        return \"sse\"\n    else:\n        return \"http\"\n\n\nclass _TransformingMCPServerMixin(FastMCPBaseModel):\n    \"\"\"A mixin that enables wrapping an MCP Server with tool transforms.\"\"\"\n\n    tools: dict[str, ToolTransformConfig] = Field(default_factory=dict)\n    \"\"\"The multi-tool transform to apply to the tools.\"\"\"\n\n    include_tags: set[str] | None = Field(\n        default=None,\n        description=\"The tags to include in the proxy.\",\n    )\n\n    exclude_tags: set[str] | None = Field(\n        default=None,\n        description=\"The tags to exclude in the proxy.\",\n    )\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _require_at_least_one_transform_field(\n        cls, values: dict[str, Any]\n    ) -> dict[str, Any]:\n        \"\"\"Reject if none of the transforming fields are set.\n\n        This ensures that plain server configs (without tools, include_tags,\n        or exclude_tags) fall through to the base server types during union\n        validation, avoiding unnecessary proxy wrapping.\n        \"\"\"\n        if isinstance(values, dict):\n            has_tools = bool(values.get(\"tools\"))\n            has_include = values.get(\"include_tags\") is not None\n            has_exclude = values.get(\"exclude_tags\") is not None\n            if not (has_tools or has_include or has_exclude):\n                raise ValueError(\n                    \"At least one of 'tools', 'include_tags', or 'exclude_tags' is required\"\n                )\n        return values\n\n    def _to_server_and_underlying_transport(\n        self,\n        server_name: str | None = None,\n        client_name: str | None = None,\n    ) -> tuple[FastMCP[Any], ClientTransport]:\n        \"\"\"Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport.\"\"\"\n        from fastmcp.client import Client\n        from fastmcp.client.transports import (\n            ClientTransport,  # pyright: ignore[reportUnusedImport]\n        )\n        from fastmcp.server import create_proxy\n\n        transport: ClientTransport = super().to_transport()  # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]  # ty: ignore[unresolved-attribute]\n        transport = cast(ClientTransport, transport)\n\n        client: Client[ClientTransport] = Client(transport=transport, name=client_name)\n\n        wrapped_mcp_server = create_proxy(\n            client,\n            name=server_name,\n        )\n\n        if self.include_tags is not None:\n            wrapped_mcp_server.enable(tags=self.include_tags, only=True)\n        if self.exclude_tags is not None:\n            wrapped_mcp_server.disable(tags=self.exclude_tags)\n\n        # Apply tool transforms if configured\n        if self.tools:\n            from fastmcp.server.transforms import ToolTransform\n\n            wrapped_mcp_server.add_transform(ToolTransform(self.tools))\n\n        return wrapped_mcp_server, transport\n\n    def to_transport(self) -> ClientTransport:\n        \"\"\"Get the transport for the transforming MCP server.\"\"\"\n        from fastmcp.client.transports import FastMCPTransport\n\n        return FastMCPTransport(mcp=self._to_server_and_underlying_transport()[0])\n\n\nclass StdioMCPServer(BaseModel):\n    \"\"\"MCP server configuration for stdio transport.\n\n    This is the canonical configuration format for MCP servers using stdio transport.\n    \"\"\"\n\n    # Required fields\n    command: str\n\n    # Common optional fields\n    args: list[str] = Field(default_factory=list)\n    env: dict[str, Any] = Field(default_factory=dict)\n\n    # Transport specification\n    transport: Literal[\"stdio\"] = \"stdio\"\n    type: Literal[\"stdio\"] | None = None  # Alternative transport field name\n\n    # Execution context\n    cwd: str | None = None  # Working directory for command execution\n    timeout: int | None = None  # Maximum response time in milliseconds\n    keep_alive: bool | None = (\n        None  # Whether to keep the subprocess alive between connections\n    )\n\n    # Metadata\n    description: str | None = None  # Human-readable server description\n    icon: str | None = None  # Icon path or URL for UI display\n\n    # Authentication configuration\n    authentication: dict[str, Any] | None = None  # Auth configuration object\n\n    model_config = ConfigDict(extra=\"allow\")  # Preserve unknown fields\n\n    def to_transport(self) -> StdioTransport:\n        from fastmcp.client.transports import StdioTransport\n\n        return StdioTransport(\n            command=self.command,\n            args=self.args,\n            env=self.env,\n            cwd=self.cwd,\n            keep_alive=self.keep_alive,\n        )\n\n\nclass TransformingStdioMCPServer(_TransformingMCPServerMixin, StdioMCPServer):\n    \"\"\"A Stdio server with tool transforms.\"\"\"\n\n\nclass RemoteMCPServer(BaseModel):\n    \"\"\"MCP server configuration for HTTP/SSE transport.\n\n    This is the canonical configuration format for MCP servers using remote transports.\n    \"\"\"\n\n    # Required fields\n    url: str\n\n    # Transport configuration\n    transport: Literal[\"http\", \"streamable-http\", \"sse\"] | None = None\n    headers: dict[str, str] = Field(default_factory=dict)\n\n    # Authentication\n    auth: Annotated[\n        str | Literal[\"oauth\"] | httpx.Auth | None,\n        Field(\n            description='Either a string representing a Bearer token, the literal \"oauth\" to use OAuth authentication, or an httpx.Auth instance for custom authentication.',\n        ),\n    ] = None\n\n    # Timeout configuration\n    sse_read_timeout: datetime.timedelta | int | float | None = None\n    timeout: int | None = None  # Maximum response time in milliseconds\n\n    # Metadata\n    description: str | None = None  # Human-readable server description\n    icon: str | None = None  # Icon path or URL for UI display\n\n    # Authentication configuration\n    authentication: dict[str, Any] | None = None  # Auth configuration object\n\n    model_config = ConfigDict(\n        extra=\"allow\", arbitrary_types_allowed=True\n    )  # Preserve unknown fields\n\n    def to_transport(self) -> StreamableHttpTransport | SSETransport:\n        from fastmcp.client.transports import SSETransport, StreamableHttpTransport\n\n        if self.transport is None:\n            transport = infer_transport_type_from_url(self.url)\n        else:\n            transport = self.transport\n\n        if transport == \"sse\":\n            return SSETransport(\n                self.url,\n                headers=self.headers,\n                auth=self.auth,\n                sse_read_timeout=self.sse_read_timeout,\n            )\n        else:\n            # Both \"http\" and \"streamable-http\" map to StreamableHttpTransport\n            return StreamableHttpTransport(\n                self.url,\n                headers=self.headers,\n                auth=self.auth,\n                sse_read_timeout=self.sse_read_timeout,\n            )\n\n\nclass TransformingRemoteMCPServer(_TransformingMCPServerMixin, RemoteMCPServer):\n    \"\"\"A Remote server with tool transforms.\"\"\"\n\n\nTransformingMCPServerTypes = TransformingStdioMCPServer | TransformingRemoteMCPServer\n\nCanonicalMCPServerTypes = StdioMCPServer | RemoteMCPServer\n\nMCPServerTypes = TransformingMCPServerTypes | CanonicalMCPServerTypes\n\n\nclass MCPConfig(BaseModel):\n    \"\"\"A configuration object for MCP Servers that conforms to the canonical MCP configuration format\n    while adding additional fields for enabling FastMCP-specific features like tool transformations\n    and filtering by tags.\n\n    For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.\n    \"\"\"\n\n    mcpServers: dict[str, MCPServerTypes] = Field(default_factory=dict)\n\n    model_config = ConfigDict(extra=\"allow\")  # Preserve unknown top-level fields\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"If there's no mcpServers key but there are server configs at root, wrap them.\"\"\"\n        if \"mcpServers\" not in values:\n            # Check if any values look like server configs\n            has_servers = any(\n                isinstance(v, dict) and (\"command\" in v or \"url\" in v)\n                for v in values.values()\n            )\n            if has_servers:\n                # Move all server-like configs under mcpServers\n                return {\"mcpServers\": values}\n        return values\n\n    def add_server(self, name: str, server: MCPServerTypes) -> None:\n        \"\"\"Add or update a server in the configuration.\"\"\"\n        self.mcpServers[name] = server\n\n    @classmethod\n    def from_dict(cls, config: dict[str, Any]) -> Self:\n        \"\"\"Parse MCP configuration from dictionary format.\"\"\"\n        return cls.model_validate(config)\n\n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"Convert MCPConfig to dictionary format, preserving all fields.\"\"\"\n        return self.model_dump(exclude_none=True)\n\n    def write_to_file(self, file_path: Path) -> None:\n        \"\"\"Write configuration to JSON file.\"\"\"\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(self.model_dump_json(indent=2))\n\n    @classmethod\n    def from_file(cls, file_path: Path) -> Self:\n        \"\"\"Load configuration from JSON file.\"\"\"\n        if file_path.exists() and (content := file_path.read_text().strip()):\n            return cls.model_validate_json(content)\n\n        raise ValueError(f\"No MCP servers defined in the config: {file_path}\")\n\n\nclass CanonicalMCPConfig(MCPConfig):\n    \"\"\"Canonical MCP configuration format.\n\n    This defines the standard configuration format for Model Context Protocol servers.\n    The format is designed to be client-agnostic and extensible for future use cases.\n    \"\"\"\n\n    mcpServers: dict[str, CanonicalMCPServerTypes] = Field(default_factory=dict)\n\n    @override\n    def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:\n        \"\"\"Add or update a server in the configuration.\"\"\"\n        self.mcpServers[name] = server\n\n\ndef update_config_file(\n    file_path: Path,\n    server_name: str,\n    server_config: CanonicalMCPServerTypes,\n) -> None:\n    \"\"\"Update an MCP configuration file from a server object, preserving existing fields.\n\n    This is used for updating the mcpServer configurations of third-party tools so we do not\n    worry about transforming server objects here.\"\"\"\n    config = MCPConfig.from_file(file_path)\n\n    # If updating an existing server, merge with existing configuration\n    # to preserve any unknown fields\n    if existing_server := config.mcpServers.get(server_name):\n        # Get the raw dict representation of both servers\n        existing_dict = existing_server.model_dump()\n\n        new_dict = server_config.model_dump(exclude_none=True)\n\n        # Merge, with new values taking precedence\n        merged_config = server_config.model_validate({**existing_dict, **new_dict})\n\n        config.add_server(server_name, merged_config)\n    else:\n        config.add_server(server_name, server_config)\n\n    config.write_to_file(file_path)\n"
  },
  {
    "path": "src/fastmcp/prompts/__init__.py",
    "content": "import sys\n\nfrom .function_prompt import FunctionPrompt, prompt\nfrom .base import Message, Prompt, PromptArgument, PromptMessage, PromptResult\n\n# Backward compat: prompt.py was renamed to base.py to stop Pyright from resolving\n# `from fastmcp.prompts import prompt` as the submodule instead of the decorator function.\n# This shim keeps `from fastmcp.prompts.prompt import Prompt` working at runtime.\n# Safe to remove once we're confident no external code imports from the old path.\nsys.modules[f\"{__name__}.prompt\"] = sys.modules[f\"{__name__}.base\"]\n\n__all__ = [\n    \"FunctionPrompt\",\n    \"Message\",\n    \"Prompt\",\n    \"PromptArgument\",\n    \"PromptMessage\",\n    \"PromptResult\",\n    \"prompt\",\n]\n"
  },
  {
    "path": "src/fastmcp/prompts/base.py",
    "content": "\"\"\"Base classes for FastMCP prompts.\"\"\"\n\nfrom __future__ import annotations as _annotations\n\nimport warnings\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any, ClassVar, Literal, overload\n\nimport pydantic\nimport pydantic_core\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\n    from fastmcp.prompts.function_prompt import FunctionPrompt\nimport mcp.types\nfrom mcp import GetPromptResult\nfrom mcp.types import (\n    AudioContent,\n    EmbeddedResource,\n    Icon,\n    ImageContent,\n    PromptMessage,\n    TextContent,\n)\nfrom mcp.types import Prompt as SDKPrompt\nfrom mcp.types import PromptArgument as SDKPromptArgument\nfrom pydantic import Field\nfrom pydantic.json_schema import SkipJsonSchema\n\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.tasks.config import TaskConfig, TaskMeta\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import (\n    FastMCPBaseModel,\n)\n\nlogger = get_logger(__name__)\n\n\nclass Message(pydantic.BaseModel):\n    \"\"\"Wrapper for prompt message with auto-serialization.\n\n    Accepts any content - strings pass through, other types\n    (dict, list, BaseModel) are JSON-serialized to text.\n\n    Example:\n        ```python\n        from fastmcp.prompts import Message\n\n        # String content (user role by default)\n        Message(\"Hello, world!\")\n\n        # Explicit role\n        Message(\"I can help with that.\", role=\"assistant\")\n\n        # Auto-serialized to JSON\n        Message({\"key\": \"value\"})\n        Message([\"item1\", \"item2\"])\n        ```\n    \"\"\"\n\n    role: Literal[\"user\", \"assistant\"]\n    content: TextContent | ImageContent | AudioContent | EmbeddedResource\n\n    def __init__(\n        self,\n        content: Any,\n        role: Literal[\"user\", \"assistant\"] = \"user\",\n    ):\n        \"\"\"Create Message with automatic serialization.\n\n        Args:\n            content: The message content. str passes through directly.\n                     TextContent, ImageContent, AudioContent, and\n                     EmbeddedResource pass through.\n                     Other types (dict, list, BaseModel) are JSON-serialized.\n            role: The message role, either \"user\" or \"assistant\".\n        \"\"\"\n        # Handle already-wrapped content types\n        if isinstance(\n            content, (TextContent, ImageContent, AudioContent, EmbeddedResource)\n        ):\n            normalized_content: (\n                TextContent | ImageContent | AudioContent | EmbeddedResource\n            ) = content\n        elif isinstance(content, str):\n            normalized_content = TextContent(type=\"text\", text=content)\n        else:\n            # dict, list, BaseModel → JSON string\n            serialized = pydantic_core.to_json(content, fallback=str).decode()\n            normalized_content = TextContent(type=\"text\", text=serialized)\n\n        super().__init__(role=role, content=normalized_content)\n\n    def to_mcp_prompt_message(self) -> PromptMessage:\n        \"\"\"Convert to MCP PromptMessage.\"\"\"\n        return PromptMessage(role=self.role, content=self.content)\n\n\nclass PromptArgument(FastMCPBaseModel):\n    \"\"\"An argument that can be passed to a prompt.\"\"\"\n\n    name: str = Field(description=\"Name of the argument\")\n    description: str | None = Field(\n        default=None, description=\"Description of what the argument does\"\n    )\n    required: bool = Field(\n        default=False, description=\"Whether the argument is required\"\n    )\n\n\nclass PromptResult(pydantic.BaseModel):\n    \"\"\"Canonical result type for prompt rendering.\n\n    Provides explicit control over prompt responses: multiple messages,\n    roles, and metadata at both the message and result level.\n\n    Accepts:\n        - str: Wrapped as single Message (user role)\n        - list[Message]: Used directly for multiple messages or custom roles\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.prompts import PromptResult, Message\n\n        mcp = FastMCP()\n\n        # Simple string content\n        @mcp.prompt\n        def greet() -> PromptResult:\n            return PromptResult(\"Hello!\")\n\n        # Multiple messages with roles\n        @mcp.prompt\n        def conversation() -> PromptResult:\n            return PromptResult([\n                Message(\"What's the weather?\"),\n                Message(\"It's sunny today.\", role=\"assistant\"),\n            ])\n        ```\n    \"\"\"\n\n    messages: list[Message]\n    description: str | None = None\n    meta: dict[str, Any] | None = None\n\n    def __init__(\n        self,\n        messages: str | list[Message],\n        description: str | None = None,\n        meta: dict[str, Any] | None = None,\n    ):\n        \"\"\"Create PromptResult.\n\n        Args:\n            messages: String or list of Message objects.\n            description: Optional description of the prompt result.\n            meta: Optional metadata about the prompt result.\n        \"\"\"\n        normalized = self._normalize_messages(messages)\n        super().__init__(messages=normalized, description=description, meta=meta)\n\n    @staticmethod\n    def _normalize_messages(\n        messages: str | list[Message],\n    ) -> list[Message]:\n        \"\"\"Normalize input to list[Message].\"\"\"\n        if isinstance(messages, str):\n            return [Message(messages)]\n        if isinstance(messages, list):\n            # Validate all items are Message\n            for i, item in enumerate(messages):\n                if not isinstance(item, Message):\n                    raise TypeError(\n                        f\"messages[{i}] must be Message, got {type(item).__name__}. \"\n                        f\"Use Message({item!r}) to wrap the value.\"\n                    )\n            return messages\n        raise TypeError(\n            f\"messages must be str or list[Message], got {type(messages).__name__}\"\n        )\n\n    def to_mcp_prompt_result(self) -> GetPromptResult:\n        \"\"\"Convert to MCP GetPromptResult.\"\"\"\n        mcp_messages = [m.to_mcp_prompt_message() for m in self.messages]\n        return GetPromptResult(\n            description=self.description,\n            messages=mcp_messages,\n            _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n        )\n\n\nclass Prompt(FastMCPComponent):\n    \"\"\"A prompt template that can be rendered with parameters.\"\"\"\n\n    KEY_PREFIX: ClassVar[str] = \"prompt\"\n\n    arguments: list[PromptArgument] | None = Field(\n        default=None, description=\"Arguments that can be passed to the prompt\"\n    )\n    auth: SkipJsonSchema[AuthCheck | list[AuthCheck] | None] = Field(\n        default=None, description=\"Authorization checks for this prompt\", exclude=True\n    )\n\n    def to_mcp_prompt(\n        self,\n        **overrides: Any,\n    ) -> SDKPrompt:\n        \"\"\"Convert the prompt to an MCP prompt.\"\"\"\n        arguments = [\n            SDKPromptArgument(\n                name=arg.name,\n                description=arg.description,\n                required=arg.required,\n            )\n            for arg in self.arguments or []\n        ]\n\n        return SDKPrompt(\n            name=overrides.get(\"name\", self.name),\n            description=overrides.get(\"description\", self.description),\n            arguments=arguments,\n            title=overrides.get(\"title\", self.title),\n            icons=overrides.get(\"icons\", self.icons),\n            _meta=overrides.get(  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                \"_meta\", self.get_meta()\n            ),\n        )\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        tags: set[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionPrompt:\n        \"\"\"Create a Prompt from a function.\n\n        The function can return:\n        - str: wrapped as single user Message\n        - list[Message | str]: converted to list[Message]\n        - PromptResult: used directly\n        \"\"\"\n        from fastmcp.prompts.function_prompt import FunctionPrompt\n\n        return FunctionPrompt.from_function(\n            fn=fn,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            meta=meta,\n            task=task,\n            auth=auth,\n        )\n\n    async def render(\n        self,\n        arguments: dict[str, Any] | None = None,\n    ) -> str | list[Message | str] | PromptResult:\n        \"\"\"Render the prompt with arguments.\n\n        Subclasses must implement this method. Return one of:\n        - str: Wrapped as single user Message\n        - list[Message | str]: Converted to list[Message]\n        - PromptResult: Used directly\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement render()\")\n\n    def convert_result(self, raw_value: Any) -> PromptResult:\n        \"\"\"Convert a raw return value to PromptResult.\n\n        Accepts:\n            - PromptResult: passed through\n            - str: wrapped as single Message\n            - list[Message | str]: converted to list[Message]\n\n        Raises:\n            TypeError: for unsupported types\n        \"\"\"\n        if isinstance(raw_value, PromptResult):\n            return raw_value\n\n        if isinstance(raw_value, str):\n            return PromptResult(raw_value, description=self.description, meta=self.meta)\n\n        if isinstance(raw_value, list | tuple):\n            messages: list[Message] = []\n            for i, item in enumerate(raw_value):\n                if isinstance(item, Message):\n                    messages.append(item)\n                elif isinstance(item, str):\n                    messages.append(Message(item))\n                else:\n                    raise TypeError(\n                        f\"messages[{i}] must be Message or str, got {type(item).__name__}. \"\n                        f\"Use Message({item!r}) to wrap the value.\"\n                    )\n            return PromptResult(messages, description=self.description, meta=self.meta)\n\n        raise TypeError(\n            f\"Prompt must return str, list[Message], or PromptResult, \"\n            f\"got {type(raw_value).__name__}\"\n        )\n\n    @overload\n    async def _render(\n        self,\n        arguments: dict[str, Any] | None = None,\n        task_meta: None = None,\n    ) -> PromptResult: ...\n\n    @overload\n    async def _render(\n        self,\n        arguments: dict[str, Any] | None,\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _render(\n        self,\n        arguments: dict[str, Any] | None = None,\n        task_meta: TaskMeta | None = None,\n    ) -> PromptResult | mcp.types.CreateTaskResult:\n        \"\"\"Server entry point that handles task routing.\n\n        This allows ANY Prompt subclass to support background execution by setting\n        task_config.mode to \"supported\" or \"required\". The server calls this\n        method instead of render() directly.\n\n        Args:\n            arguments: Prompt arguments\n            task_meta: If provided, execute as background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return PromptResult.\n\n        Returns:\n            PromptResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Subclasses can override this to customize task routing behavior.\n        For example, FastMCPProviderPrompt overrides to delegate to child\n        middleware without submitting to Docket.\n        \"\"\"\n        from fastmcp.server.tasks.routing import check_background_task\n\n        task_result = await check_background_task(\n            component=self,\n            task_type=\"prompt\",\n            arguments=arguments,\n            task_meta=task_meta,\n        )\n        if task_result:\n            return task_result\n\n        # Synchronous execution\n        result = await self.render(arguments)\n        return self.convert_result(result)\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this prompt with docket for background execution.\"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.render, names=[self.key])\n\n    async def add_to_docket(  # type: ignore[override]\n        self,\n        docket: Docket,\n        arguments: dict[str, Any] | None,\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this prompt for background execution via docket.\n\n        Args:\n            docket: The Docket instance\n            arguments: Prompt arguments\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(arguments)\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.component.type\": \"prompt\",\n            \"fastmcp.provider.type\": \"LocalProvider\",\n        }\n\n\n__all__ = [\n    \"Message\",\n    \"Prompt\",\n    \"PromptArgument\",\n    \"PromptResult\",\n]\n\n\ndef __getattr__(name: str) -> Any:\n    \"\"\"Deprecated re-exports for backwards compatibility.\"\"\"\n    deprecated_exports = {\n        \"FunctionPrompt\": \"FunctionPrompt\",\n        \"prompt\": \"prompt\",\n    }\n\n    if name in deprecated_exports:\n        import fastmcp\n\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                f\"Importing {name} from fastmcp.prompts.prompt is deprecated. \"\n                f\"Import from fastmcp.prompts.function_prompt instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        from fastmcp.prompts import function_prompt\n\n        return getattr(function_prompt, name)\n\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n"
  },
  {
    "path": "src/fastmcp/prompts/function_prompt.py",
    "content": "\"\"\"Standalone @prompt decorator for FastMCP.\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport inspect\nimport json\nimport warnings\nfrom collections.abc import Callable\nfrom dataclasses import dataclass, field\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Literal,\n    Protocol,\n    TypeVar,\n    overload,\n    runtime_checkable,\n)\n\nimport pydantic_core\nfrom mcp.types import Icon\nfrom pydantic.json_schema import SkipJsonSchema\n\nimport fastmcp\nfrom fastmcp.decorators import resolve_task_config\nfrom fastmcp.exceptions import PromptError\nfrom fastmcp.prompts.base import Prompt, PromptArgument, PromptResult\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.dependencies import (\n    transform_context_annotations,\n    without_injected_parameters,\n)\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.utilities.async_utils import (\n    call_sync_fn_in_threadpool,\n    is_coroutine_function,\n)\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import get_cached_typeadapter\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\nlogger = get_logger(__name__)\n\n\n@runtime_checkable\nclass DecoratedPrompt(Protocol):\n    \"\"\"Protocol for functions decorated with @prompt.\"\"\"\n\n    __fastmcp__: PromptMeta\n\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n\n\n@dataclass(frozen=True, kw_only=True)\nclass PromptMeta:\n    \"\"\"Metadata attached to functions by the @prompt decorator.\"\"\"\n\n    type: Literal[\"prompt\"] = field(default=\"prompt\", init=False)\n    name: str | None = None\n    version: str | int | None = None\n    title: str | None = None\n    description: str | None = None\n    icons: list[Icon] | None = None\n    tags: set[str] | None = None\n    meta: dict[str, Any] | None = None\n    task: bool | TaskConfig | None = None\n    auth: AuthCheck | list[AuthCheck] | None = None\n    enabled: bool = True\n\n\nclass FunctionPrompt(Prompt):\n    \"\"\"A prompt that is a function.\"\"\"\n\n    fn: SkipJsonSchema[Callable[..., Any]]\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        *,\n        metadata: PromptMeta | None = None,\n        # Keep individual params for backwards compat\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        tags: set[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionPrompt:\n        \"\"\"Create a Prompt from a function.\n\n        Args:\n            fn: The function to wrap\n            metadata: PromptMeta object with all configuration. If provided,\n                individual parameters must not be passed.\n            name, title, etc.: Individual parameters for backwards compatibility.\n                Cannot be used together with metadata parameter.\n\n        The function can return:\n        - str: wrapped as single user Message\n        - list[Message | str]: converted to list[Message]\n        - PromptResult: used directly\n        \"\"\"\n        # Check mutual exclusion\n        individual_params_provided = any(\n            x is not None\n            for x in [name, version, title, description, icons, tags, meta, task, auth]\n        )\n\n        if metadata is not None and individual_params_provided:\n            raise TypeError(\n                \"Cannot pass both 'metadata' and individual parameters to from_function(). \"\n                \"Use metadata alone or individual parameters alone.\"\n            )\n\n        # Build metadata from kwargs if not provided\n        if metadata is None:\n            metadata = PromptMeta(\n                name=name,\n                version=version,\n                title=title,\n                description=description,\n                icons=icons,\n                tags=tags,\n                meta=meta,\n                task=task,\n                auth=auth,\n            )\n\n        func_name = (\n            metadata.name or getattr(fn, \"__name__\", None) or fn.__class__.__name__\n        )\n\n        if func_name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        # Reject functions with *args or **kwargs\n        sig = inspect.signature(fn)\n        for param in sig.parameters.values():\n            if param.kind == inspect.Parameter.VAR_POSITIONAL:\n                raise ValueError(\"Functions with *args are not supported as prompts\")\n            if param.kind == inspect.Parameter.VAR_KEYWORD:\n                raise ValueError(\"Functions with **kwargs are not supported as prompts\")\n\n        description = metadata.description or inspect.getdoc(fn)\n\n        # Normalize task to TaskConfig and validate\n        task_value = metadata.task\n        if task_value is None:\n            task_config = TaskConfig(mode=\"forbidden\")\n        elif isinstance(task_value, bool):\n            task_config = TaskConfig.from_bool(task_value)\n        else:\n            task_config = task_value\n        task_config.validate_function(fn, func_name)\n\n        # if the fn is a callable class, we need to get the __call__ method from here out\n        if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):\n            fn = fn.__call__\n        # if the fn is a staticmethod, we need to work with the underlying function\n        if isinstance(fn, staticmethod):\n            fn = fn.__func__\n\n        # Transform Context type annotations to Depends() for unified DI\n        fn = transform_context_annotations(fn)\n\n        # Wrap fn to handle dependency resolution internally\n        wrapped_fn = without_injected_parameters(fn)\n        type_adapter = get_cached_typeadapter(wrapped_fn)\n        parameters = type_adapter.json_schema()\n        parameters = compress_schema(parameters, prune_titles=True)\n\n        # Convert parameters to PromptArguments\n        arguments: list[PromptArgument] = []\n        if \"properties\" in parameters:\n            for param_name, param in parameters[\"properties\"].items():\n                arg_description = param.get(\"description\")\n\n                # For non-string parameters, append JSON schema info to help users\n                # understand the expected format when passing as strings (MCP requirement)\n                if param_name in sig.parameters:\n                    sig_param = sig.parameters[param_name]\n                    if (\n                        sig_param.annotation != inspect.Parameter.empty\n                        and sig_param.annotation is not str\n                    ):\n                        # Get the JSON schema for this specific parameter type\n                        try:\n                            param_adapter = get_cached_typeadapter(sig_param.annotation)\n                            param_schema = param_adapter.json_schema()\n\n                            # Create compact schema representation\n                            schema_str = json.dumps(param_schema, separators=(\",\", \":\"))\n\n                            # Append schema info to description\n                            schema_note = f\"Provide as a JSON string matching the following schema: {schema_str}\"\n                            if arg_description:\n                                arg_description = f\"{arg_description}\\n\\n{schema_note}\"\n                            else:\n                                arg_description = schema_note\n                        except Exception as e:\n                            # If schema generation fails, skip enhancement\n                            logger.debug(\n                                \"Failed to generate schema for prompt argument %s: %s\",\n                                param_name,\n                                e,\n                            )\n\n                arguments.append(\n                    PromptArgument(\n                        name=param_name,\n                        description=arg_description,\n                        required=param_name in parameters.get(\"required\", []),\n                    )\n                )\n\n        return cls(\n            name=func_name,\n            version=str(metadata.version) if metadata.version is not None else None,\n            title=metadata.title,\n            description=description,\n            icons=metadata.icons,\n            arguments=arguments,\n            tags=metadata.tags or set(),\n            fn=wrapped_fn,\n            meta=metadata.meta,\n            task_config=task_config,\n            auth=metadata.auth,\n        )\n\n    def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Convert string arguments to expected types based on function signature.\"\"\"\n        from fastmcp.server.dependencies import without_injected_parameters\n\n        wrapper_fn = without_injected_parameters(self.fn)\n        sig = inspect.signature(wrapper_fn)\n        converted_kwargs = {}\n\n        for param_name, param_value in kwargs.items():\n            if param_name in sig.parameters:\n                param = sig.parameters[param_name]\n\n                # If parameter has no annotation or annotation is str, pass as-is\n                if (\n                    param.annotation == inspect.Parameter.empty\n                    or param.annotation is str\n                ) or not isinstance(param_value, str):\n                    converted_kwargs[param_name] = param_value\n                else:\n                    # Try to convert string argument using type adapter\n                    try:\n                        adapter = get_cached_typeadapter(param.annotation)\n                        # Try JSON parsing first for complex types\n                        try:\n                            converted_kwargs[param_name] = adapter.validate_json(\n                                param_value\n                            )\n                        except (ValueError, TypeError, pydantic_core.ValidationError):\n                            # Fallback to direct validation\n                            converted_kwargs[param_name] = adapter.validate_python(\n                                param_value\n                            )\n                    except (ValueError, TypeError, pydantic_core.ValidationError) as e:\n                        # If conversion fails, provide informative error\n                        raise PromptError(\n                            f\"Could not convert argument '{param_name}' with value '{param_value}' \"\n                            f\"to expected type {param.annotation}. Error: {e}\"\n                        ) from e\n            else:\n                # Parameter not in function signature, pass as-is\n                converted_kwargs[param_name] = param_value\n\n        return converted_kwargs\n\n    async def render(\n        self,\n        arguments: dict[str, Any] | None = None,\n    ) -> PromptResult:\n        \"\"\"Render the prompt with arguments.\"\"\"\n        # Validate required arguments\n        if self.arguments:\n            required = {arg.name for arg in self.arguments if arg.required}\n            provided = set(arguments or {})\n            missing = required - provided\n            if missing:\n                raise ValueError(f\"Missing required arguments: {missing}\")\n\n        try:\n            # Prepare arguments\n            kwargs = arguments.copy() if arguments else {}\n\n            # Convert string arguments to expected types BEFORE validation\n            kwargs = self._convert_string_arguments(kwargs)\n\n            # Filter out arguments that aren't in the function signature\n            # This is important for security: dependencies should not be overridable\n            # from external callers. self.fn is wrapped by without_injected_parameters,\n            # so we only accept arguments that are in the wrapped function's signature.\n            sig = inspect.signature(self.fn)\n            valid_params = set(sig.parameters.keys())\n            kwargs = {k: v for k, v in kwargs.items() if k in valid_params}\n\n            # Use type adapter to validate arguments and handle Field() defaults\n            # This matches the behavior of tools in function_tool\n            type_adapter = get_cached_typeadapter(self.fn)\n\n            # self.fn is wrapped by without_injected_parameters which handles\n            # dependency resolution internally\n            if is_coroutine_function(self.fn):\n                result = await type_adapter.validate_python(kwargs)\n            else:\n                # Run sync functions in threadpool to avoid blocking the event loop\n                result = await call_sync_fn_in_threadpool(\n                    type_adapter.validate_python, kwargs\n                )\n                # Handle sync wrappers that return awaitables (e.g., partial(async_fn))\n                if inspect.isawaitable(result):\n                    result = await result\n\n            return self.convert_result(result)\n        except Exception as e:\n            logger.exception(f\"Error rendering prompt {self.name}\")\n            raise PromptError(f\"Error rendering prompt {self.name}.\") from e\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this prompt with docket for background execution.\n\n        FunctionPrompt registers the underlying function, which has the user's\n        Depends parameters for docket to resolve.\n        \"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.fn, names=[self.key])\n\n    async def add_to_docket(\n        self,\n        docket: Docket,\n        arguments: dict[str, Any] | None,\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this prompt for background execution via docket.\n\n        FunctionPrompt splats the arguments dict since .fn expects **kwargs.\n\n        Args:\n            docket: The Docket instance\n            arguments: Prompt arguments\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(**(arguments or {}))\n\n\n@overload\ndef prompt(fn: F) -> F: ...\n@overload\ndef prompt(\n    name_or_fn: str,\n    *,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    tags: set[str] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Callable[[F], F]: ...\n@overload\ndef prompt(\n    name_or_fn: None = None,\n    *,\n    name: str | None = None,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    tags: set[str] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Callable[[F], F]: ...\n\n\ndef prompt(\n    name_or_fn: str | Callable[..., Any] | None = None,\n    *,\n    name: str | None = None,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    tags: set[str] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Any:\n    \"\"\"Standalone decorator to mark a function as an MCP prompt.\n\n    Returns the original function with metadata attached. Register with a server\n    using mcp.add_prompt().\n    \"\"\"\n    if isinstance(name_or_fn, classmethod):\n        raise TypeError(\n            \"To decorate a classmethod, use @classmethod above @prompt. \"\n            \"See https://gofastmcp.com/servers/prompts#using-with-methods\"\n        )\n\n    def create_prompt(\n        fn: Callable[..., Any], prompt_name: str | None\n    ) -> FunctionPrompt:\n        # Create metadata first, then pass it\n        prompt_meta = PromptMeta(\n            name=prompt_name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            meta=meta,\n            task=resolve_task_config(task),\n            auth=auth,\n        )\n        return FunctionPrompt.from_function(fn, metadata=prompt_meta)\n\n    def attach_metadata(fn: F, prompt_name: str | None) -> F:\n        metadata = PromptMeta(\n            name=prompt_name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            meta=meta,\n            task=task,\n            auth=auth,\n        )\n        target = fn.__func__ if hasattr(fn, \"__func__\") else fn\n        target.__fastmcp__ = metadata\n        return fn\n\n    def decorator(fn: F, prompt_name: str | None) -> F:\n        if fastmcp.settings.decorator_mode == \"object\":\n            warnings.warn(\n                \"decorator_mode='object' is deprecated and will be removed in a future version. \"\n                \"Decorators now return the original function with metadata attached.\",\n                DeprecationWarning,\n                stacklevel=4,\n            )\n            return create_prompt(fn, prompt_name)  # type: ignore[return-value]\n        return attach_metadata(fn, prompt_name)\n\n    if inspect.isroutine(name_or_fn):\n        return decorator(name_or_fn, name)\n    elif isinstance(name_or_fn, str):\n        if name is not None:\n            raise TypeError(\"Cannot specify name both as first argument and keyword\")\n        prompt_name = name_or_fn\n    elif name_or_fn is None:\n        prompt_name = name\n    else:\n        raise TypeError(f\"Invalid first argument: {type(name_or_fn)}\")\n\n    def wrapper(fn: F) -> F:\n        return decorator(fn, prompt_name)\n\n    return wrapper\n"
  },
  {
    "path": "src/fastmcp/py.typed",
    "content": ""
  },
  {
    "path": "src/fastmcp/resources/__init__.py",
    "content": "import sys\n\nfrom .function_resource import FunctionResource, resource\nfrom .base import Resource, ResourceContent, ResourceResult\nfrom .template import ResourceTemplate\nfrom .types import (\n    BinaryResource,\n    DirectoryResource,\n    FileResource,\n    HttpResource,\n    TextResource,\n)\n\n__all__ = [\n    \"BinaryResource\",\n    \"DirectoryResource\",\n    \"FileResource\",\n    \"FunctionResource\",\n    \"HttpResource\",\n    \"Resource\",\n    \"ResourceContent\",\n    \"ResourceResult\",\n    \"ResourceTemplate\",\n    \"TextResource\",\n    \"resource\",\n]\n\n# Backward compat: resource.py was renamed to base.py to stop Pyright from resolving\n# `from fastmcp.resources import resource` as the submodule instead of the decorator function.\n# This shim keeps `from fastmcp.resources.resource import Resource` working at runtime.\n# Safe to remove once we're confident no external code imports from the old path.\nsys.modules[f\"{__name__}.resource\"] = sys.modules[f\"{__name__}.base\"]\n"
  },
  {
    "path": "src/fastmcp/resources/base.py",
    "content": "\"\"\"Base classes and interfaces for FastMCP resources.\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Annotated, Any, ClassVar, overload\n\nimport mcp.types\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\n    from fastmcp.resources.function_resource import FunctionResource\n\nimport pydantic\nimport pydantic_core\nfrom mcp.types import Annotations, Icon\nfrom mcp.types import Resource as SDKResource\nfrom pydantic import (\n    AnyUrl,\n    ConfigDict,\n    Field,\n    UrlConstraints,\n    field_validator,\n    model_validator,\n)\nfrom pydantic.json_schema import SkipJsonSchema\nfrom typing_extensions import Self\n\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.tasks.config import TaskConfig, TaskMeta\nfrom fastmcp.utilities.components import FastMCPComponent\n\n\nclass ResourceContent(pydantic.BaseModel):\n    \"\"\"Wrapper for resource content with optional MIME type and metadata.\n\n    Accepts any value for content - strings and bytes pass through directly,\n    other types (dict, list, BaseModel, etc.) are automatically JSON-serialized.\n\n    Example:\n        ```python\n        from fastmcp.resources import ResourceContent\n\n        # String content\n        ResourceContent(\"plain text\")\n\n        # Binary content\n        ResourceContent(b\"binary data\", mime_type=\"application/octet-stream\")\n\n        # Auto-serialized to JSON\n        ResourceContent({\"key\": \"value\"})\n        ResourceContent([\"a\", \"b\", \"c\"])\n        ```\n    \"\"\"\n\n    content: str | bytes\n    mime_type: str | None = None\n    meta: dict[str, Any] | None = None\n\n    def __init__(\n        self,\n        content: Any,\n        mime_type: str | None = None,\n        meta: dict[str, Any] | None = None,\n    ):\n        \"\"\"Create ResourceContent with automatic serialization.\n\n        Args:\n            content: The content value. str and bytes pass through directly.\n                     Other types (dict, list, BaseModel) are JSON-serialized.\n            mime_type: Optional MIME type. Defaults based on content type:\n                       str → \"text/plain\", bytes → \"application/octet-stream\",\n                       other → \"application/json\"\n            meta: Optional metadata dictionary.\n        \"\"\"\n        if isinstance(content, str):\n            normalized_content: str | bytes = content\n            mime_type = mime_type or \"text/plain\"\n        elif isinstance(content, bytes):\n            normalized_content = content\n            mime_type = mime_type or \"application/octet-stream\"\n        else:\n            # dict, list, BaseModel, etc → JSON\n            normalized_content = pydantic_core.to_json(content, fallback=str).decode()\n            mime_type = mime_type or \"application/json\"\n\n        super().__init__(content=normalized_content, mime_type=mime_type, meta=meta)\n\n    def to_mcp_resource_contents(\n        self, uri: AnyUrl | str\n    ) -> mcp.types.TextResourceContents | mcp.types.BlobResourceContents:\n        \"\"\"Convert to MCP resource contents type.\n\n        Args:\n            uri: The URI of the resource (required by MCP types)\n\n        Returns:\n            TextResourceContents for str content, BlobResourceContents for bytes\n        \"\"\"\n        if isinstance(self.content, str):\n            return mcp.types.TextResourceContents(\n                uri=AnyUrl(uri) if isinstance(uri, str) else uri,\n                text=self.content,\n                mimeType=self.mime_type or \"text/plain\",\n                _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n            )\n        else:\n            return mcp.types.BlobResourceContents(\n                uri=AnyUrl(uri) if isinstance(uri, str) else uri,\n                blob=base64.b64encode(self.content).decode(),\n                mimeType=self.mime_type or \"application/octet-stream\",\n                _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n            )\n\n\nclass ResourceResult(pydantic.BaseModel):\n    \"\"\"Canonical result type for resource reads.\n\n    Provides explicit control over resource responses: multiple content items,\n    per-item MIME types, and metadata at both the item and result level.\n\n    Accepts:\n        - str: Wrapped as single ResourceContent (text/plain)\n        - bytes: Wrapped as single ResourceContent (application/octet-stream)\n        - list[ResourceContent]: Used directly for multiple items or custom MIME types\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.resources import ResourceResult, ResourceContent\n\n        mcp = FastMCP()\n\n        # Simple string content\n        @mcp.resource(\"data://simple\")\n        def get_simple() -> ResourceResult:\n            return ResourceResult(\"hello world\")\n\n        # Multiple items with custom MIME types\n        @mcp.resource(\"data://items\")\n        def get_items() -> ResourceResult:\n            return ResourceResult(\n                contents=[\n                    ResourceContent({\"key\": \"value\"}),  # auto-serialized to JSON\n                    ResourceContent(b\"binary data\"),\n                ],\n                meta={\"count\": 2}\n            )\n        ```\n    \"\"\"\n\n    contents: list[ResourceContent]\n    meta: dict[str, Any] | None = None\n\n    def __init__(\n        self,\n        contents: str | bytes | list[ResourceContent],\n        meta: dict[str, Any] | None = None,\n    ):\n        \"\"\"Create ResourceResult.\n\n        Args:\n            contents: String, bytes, or list of ResourceContent objects.\n            meta: Optional metadata about the resource result.\n        \"\"\"\n        normalized = self._normalize_contents(contents)\n        super().__init__(contents=normalized, meta=meta)\n\n    @staticmethod\n    def _normalize_contents(\n        contents: str | bytes | list[ResourceContent],\n    ) -> list[ResourceContent]:\n        \"\"\"Normalize input to list[ResourceContent].\"\"\"\n        if isinstance(contents, str):\n            return [ResourceContent(contents)]\n        if isinstance(contents, bytes):\n            return [ResourceContent(contents)]\n        if isinstance(contents, list):\n            # Validate all items are ResourceContent\n            for i, item in enumerate(contents):\n                if not isinstance(item, ResourceContent):\n                    raise TypeError(\n                        f\"contents[{i}] must be ResourceContent, got {type(item).__name__}. \"\n                        f\"Use ResourceContent({item!r}) to wrap the value.\"\n                    )\n            return contents\n        raise TypeError(\n            f\"contents must be str, bytes, or list[ResourceContent], got {type(contents).__name__}\"\n        )\n\n    def to_mcp_result(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult:\n        \"\"\"Convert to MCP ReadResourceResult.\n\n        Args:\n            uri: The URI of the resource (required by MCP types)\n\n        Returns:\n            MCP ReadResourceResult with converted contents\n        \"\"\"\n        mcp_contents = [item.to_mcp_resource_contents(uri) for item in self.contents]\n        return mcp.types.ReadResourceResult(\n            contents=mcp_contents,\n            _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n        )\n\n\nclass Resource(FastMCPComponent):\n    \"\"\"Base class for all resources.\"\"\"\n\n    KEY_PREFIX: ClassVar[str] = \"resource\"\n\n    model_config = ConfigDict(validate_default=True)\n\n    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(\n        default=..., description=\"URI of the resource\"\n    )\n    name: str = Field(default=\"\", description=\"Name of the resource\")\n    mime_type: str = Field(\n        default=\"text/plain\",\n        description=\"MIME type of the resource content\",\n    )\n    annotations: Annotated[\n        Annotations | None,\n        Field(description=\"Optional annotations about the resource's behavior\"),\n    ] = None\n    auth: Annotated[\n        SkipJsonSchema[AuthCheck | list[AuthCheck] | None],\n        Field(description=\"Authorization checks for this resource\", exclude=True),\n    ] = None\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        uri: str | AnyUrl,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        mime_type: str | None = None,\n        tags: set[str] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionResource:\n        from fastmcp.resources.function_resource import (\n            FunctionResource,\n        )\n\n        return FunctionResource.from_function(\n            fn=fn,\n            uri=uri,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            mime_type=mime_type,\n            tags=tags,\n            annotations=annotations,\n            meta=meta,\n            task=task,\n            auth=auth,\n        )\n\n    @field_validator(\"mime_type\", mode=\"before\")\n    @classmethod\n    def set_default_mime_type(cls, mime_type: str | None) -> str:\n        \"\"\"Set default MIME type if not provided.\"\"\"\n        if mime_type:\n            return mime_type\n        return \"text/plain\"\n\n    @model_validator(mode=\"after\")\n    def set_default_name(self) -> Self:\n        \"\"\"Set default name from URI if not provided.\"\"\"\n        if self.name:\n            pass\n        elif self.uri:\n            self.name = str(self.uri)\n        else:\n            raise ValueError(\"Either name or uri must be provided\")\n        return self\n\n    async def read(\n        self,\n    ) -> str | bytes | ResourceResult:\n        \"\"\"Read the resource content.\n\n        Subclasses implement this to return resource data. Supported return types:\n            - str: Text content\n            - bytes: Binary content\n            - ResourceResult: Full control over contents and result-level meta\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement read()\")\n\n    def convert_result(self, raw_value: Any) -> ResourceResult:\n        \"\"\"Convert a raw result to ResourceResult.\n\n        This is used in two contexts:\n        1. In _read() to convert user function return values to ResourceResult\n        2. In tasks_result_handler() to convert Docket task results to ResourceResult\n\n        Handles ResourceResult passthrough and converts raw values using\n        ResourceResult's normalization.  When the raw value is a plain\n        string or bytes, the resource's own ``mime_type`` is forwarded so\n        that ``ui://`` resources (and others with non-default MIME types)\n        don't fall back to ``text/plain``.\n\n        The resource's component-level ``meta`` (e.g. ``ui`` metadata for\n        MCP Apps CSP/permissions) is propagated to each content item so\n        that hosts can read it from the ``resources/read`` response.\n        \"\"\"\n        if isinstance(raw_value, ResourceResult):\n            return raw_value\n\n        # For plain str/bytes returns, wrap in ResourceContent with the\n        # resource's MIME type and component meta so the wire response\n        # carries the correct type and metadata (e.g. CSP for MCP Apps).\n        if isinstance(raw_value, (str, bytes)):\n            return ResourceResult(\n                [ResourceContent(raw_value, mime_type=self.mime_type, meta=self.meta)]\n            )\n\n        # ResourceResult.__init__ handles all other normalization\n        return ResourceResult(raw_value)\n\n    @overload\n    async def _read(self, task_meta: None = None) -> ResourceResult: ...\n\n    @overload\n    async def _read(self, task_meta: TaskMeta) -> mcp.types.CreateTaskResult: ...\n\n    async def _read(\n        self, task_meta: TaskMeta | None = None\n    ) -> ResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Server entry point that handles task routing.\n\n        This allows ANY Resource subclass to support background execution by setting\n        task_config.mode to \"supported\" or \"required\". The server calls this\n        method instead of read() directly.\n\n        Args:\n            task_meta: If provided, execute as a background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return ResourceResult.\n\n        Returns:\n            ResourceResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Subclasses can override this to customize task routing behavior.\n        For example, FastMCPProviderResource overrides to delegate to child\n        middleware without submitting to Docket.\n        \"\"\"\n        from fastmcp.server.tasks.routing import check_background_task\n\n        task_result = await check_background_task(\n            component=self, task_type=\"resource\", arguments=None, task_meta=task_meta\n        )\n        if task_result:\n            return task_result\n\n        # Synchronous execution - convert result to ResourceResult\n        result = await self.read()\n        return self.convert_result(result)\n\n    def to_mcp_resource(\n        self,\n        **overrides: Any,\n    ) -> SDKResource:\n        \"\"\"Convert the resource to an SDKResource.\"\"\"\n\n        return SDKResource(\n            name=overrides.get(\"name\", self.name),\n            uri=overrides.get(\"uri\", self.uri),\n            description=overrides.get(\"description\", self.description),\n            mimeType=overrides.get(\"mimeType\", self.mime_type),\n            title=overrides.get(\"title\", self.title),\n            icons=overrides.get(\"icons\", self.icons),\n            annotations=overrides.get(\"annotations\", self.annotations),\n            _meta=overrides.get(  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                \"_meta\", self.get_meta()\n            ),\n        )\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})\"\n\n    @property\n    def key(self) -> str:\n        \"\"\"The globally unique lookup key for this resource.\"\"\"\n        base_key = self.make_key(str(self.uri))\n        return f\"{base_key}@{self.version or ''}\"\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this resource with docket for background execution.\"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.read, names=[self.key])\n\n    async def add_to_docket(  # type: ignore[override]\n        self,\n        docket: Docket,\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this resource for background execution via docket.\n\n        Args:\n            docket: The Docket instance\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)()\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.component.type\": \"resource\",\n            \"fastmcp.provider.type\": \"LocalProvider\",\n        }\n\n\n__all__ = [\n    \"Resource\",\n    \"ResourceContent\",\n    \"ResourceResult\",\n]\n\n\ndef __getattr__(name: str) -> Any:\n    \"\"\"Deprecated re-exports for backwards compatibility.\"\"\"\n    deprecated_exports = {\n        \"FunctionResource\": \"FunctionResource\",\n        \"resource\": \"resource\",\n    }\n\n    if name in deprecated_exports:\n        import warnings\n\n        import fastmcp\n\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                f\"Importing {name} from fastmcp.resources.resource is deprecated. \"\n                f\"Import from fastmcp.resources.function_resource instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        from fastmcp.resources import function_resource\n\n        return getattr(function_resource, name)\n\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n"
  },
  {
    "path": "src/fastmcp/resources/function_resource.py",
    "content": "\"\"\"Standalone @resource decorator for FastMCP.\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport inspect\nimport warnings\nfrom collections.abc import Callable\nfrom dataclasses import dataclass, field\nfrom typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, runtime_checkable\n\nfrom mcp.types import Annotations, Icon\nfrom pydantic import AnyUrl\nfrom pydantic.json_schema import SkipJsonSchema\n\nimport fastmcp\nfrom fastmcp.decorators import resolve_task_config\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.server.apps import resolve_ui_mime_type\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.dependencies import (\n    transform_context_annotations,\n    without_injected_parameters,\n)\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.utilities.async_utils import (\n    call_sync_fn_in_threadpool,\n    is_coroutine_function,\n)\n\nif TYPE_CHECKING:\n    from docket import Docket\n\n    from fastmcp.resources.template import ResourceTemplate\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\n\n@runtime_checkable\nclass DecoratedResource(Protocol):\n    \"\"\"Protocol for functions decorated with @resource.\"\"\"\n\n    __fastmcp__: ResourceMeta\n\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n\n\n@dataclass(frozen=True, kw_only=True)\nclass ResourceMeta:\n    \"\"\"Metadata attached to functions by the @resource decorator.\"\"\"\n\n    type: Literal[\"resource\"] = field(default=\"resource\", init=False)\n    uri: str\n    name: str | None = None\n    version: str | int | None = None\n    title: str | None = None\n    description: str | None = None\n    icons: list[Icon] | None = None\n    tags: set[str] | None = None\n    mime_type: str | None = None\n    annotations: Annotations | None = None\n    meta: dict[str, Any] | None = None\n    task: bool | TaskConfig | None = None\n    auth: AuthCheck | list[AuthCheck] | None = None\n    enabled: bool = True\n\n\nclass FunctionResource(Resource):\n    \"\"\"A resource that defers data loading by wrapping a function.\n\n    The function is only called when the resource is read, allowing for lazy loading\n    of potentially expensive data. This is particularly useful when listing resources,\n    as the function won't be called until the resource is actually accessed.\n\n    The function can return:\n    - str for text content (default)\n    - bytes for binary content\n    - other types will be converted to JSON\n    \"\"\"\n\n    fn: SkipJsonSchema[Callable[..., Any]]\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        uri: str | AnyUrl | None = None,\n        *,\n        metadata: ResourceMeta | None = None,\n        # Keep individual params for backwards compat\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        mime_type: str | None = None,\n        tags: set[str] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionResource:\n        \"\"\"Create a FunctionResource from a function.\n\n        Args:\n            fn: The function to wrap\n            uri: The URI for the resource (required if metadata not provided)\n            metadata: ResourceMeta object with all configuration. If provided,\n                individual parameters must not be passed.\n            name, title, etc.: Individual parameters for backwards compatibility.\n                Cannot be used together with metadata parameter.\n        \"\"\"\n        # Check mutual exclusion\n        individual_params_provided = (\n            any(\n                x is not None\n                for x in [\n                    name,\n                    version,\n                    title,\n                    description,\n                    icons,\n                    mime_type,\n                    tags,\n                    annotations,\n                    meta,\n                    task,\n                    auth,\n                ]\n            )\n            or uri is not None\n        )\n\n        if metadata is not None and individual_params_provided:\n            raise TypeError(\n                \"Cannot pass both 'metadata' and individual parameters to from_function(). \"\n                \"Use metadata alone or individual parameters alone.\"\n            )\n\n        # Build metadata from kwargs if not provided\n        if metadata is None:\n            if uri is None:\n                raise TypeError(\"uri is required when metadata is not provided\")\n            metadata = ResourceMeta(\n                uri=str(uri),\n                name=name,\n                version=version,\n                title=title,\n                description=description,\n                icons=icons,\n                tags=tags,\n                mime_type=mime_type,\n                annotations=annotations,\n                meta=meta,\n                task=task,\n                auth=auth,\n            )\n\n        uri_obj = AnyUrl(metadata.uri)\n\n        # Get function name - use class name for callable objects\n        func_name = (\n            metadata.name or getattr(fn, \"__name__\", None) or fn.__class__.__name__\n        )\n\n        # Normalize task to TaskConfig and validate\n        task_value = metadata.task\n        if task_value is None:\n            task_config = TaskConfig(mode=\"forbidden\")\n        elif isinstance(task_value, bool):\n            task_config = TaskConfig.from_bool(task_value)\n        else:\n            task_config = task_value\n        task_config.validate_function(fn, func_name)\n\n        # if the fn is a callable class, we need to get the __call__ method from here out\n        if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):\n            fn = fn.__call__\n        # if the fn is a staticmethod, we need to work with the underlying function\n        if isinstance(fn, staticmethod):\n            fn = fn.__func__\n\n        # Transform Context type annotations to Depends() for unified DI\n        fn = transform_context_annotations(fn)\n\n        # Wrap fn to handle dependency resolution internally\n        wrapped_fn = without_injected_parameters(fn)\n\n        # Apply ui:// MIME default, then fall back to text/plain\n        resolved_mime = resolve_ui_mime_type(metadata.uri, metadata.mime_type)\n\n        return cls(\n            fn=wrapped_fn,\n            uri=uri_obj,\n            name=func_name,\n            version=str(metadata.version) if metadata.version is not None else None,\n            title=metadata.title,\n            description=metadata.description or inspect.getdoc(fn),\n            icons=metadata.icons,\n            mime_type=resolved_mime or \"text/plain\",\n            tags=metadata.tags or set(),\n            annotations=metadata.annotations,\n            meta=metadata.meta,\n            task_config=task_config,\n            auth=metadata.auth,\n        )\n\n    async def read(\n        self,\n    ) -> str | bytes | ResourceResult:\n        \"\"\"Read the resource by calling the wrapped function.\"\"\"\n        # self.fn is wrapped by without_injected_parameters which handles\n        # dependency resolution internally\n        if is_coroutine_function(self.fn):\n            result = await self.fn()\n        else:\n            # Run sync functions in threadpool to avoid blocking the event loop\n            result = await call_sync_fn_in_threadpool(self.fn)\n            # Handle sync wrappers that return awaitables (e.g., partial(async_fn))\n            if inspect.isawaitable(result):\n                result = await result\n\n        # If user returned another Resource, read it recursively\n        if isinstance(result, Resource):\n            return await result.read()\n\n        return result\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this resource with docket for background execution.\n\n        FunctionResource registers the underlying function, which has the user's\n        Depends parameters for docket to resolve.\n        \"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.fn, names=[self.key])\n\n\ndef resource(\n    uri: str,\n    *,\n    name: str | None = None,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    mime_type: str | None = None,\n    tags: set[str] | None = None,\n    annotations: Annotations | dict[str, Any] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Callable[[F], F]:\n    \"\"\"Standalone decorator to mark a function as an MCP resource.\n\n    Returns the original function with metadata attached. Register with a server\n    using mcp.add_resource().\n    \"\"\"\n    if isinstance(annotations, dict):\n        annotations = Annotations(**annotations)\n\n    if inspect.isroutine(uri):\n        raise TypeError(\n            \"The @resource decorator requires a URI. \"\n            \"Use @resource('uri') instead of @resource\"\n        )\n\n    def create_resource(fn: Callable[..., Any]) -> FunctionResource | ResourceTemplate:\n        from fastmcp.resources.template import ResourceTemplate\n        from fastmcp.server.dependencies import without_injected_parameters\n\n        resolved = resolve_task_config(task)\n        has_uri_params = \"{\" in uri and \"}\" in uri\n        wrapper_fn = without_injected_parameters(fn)\n        has_func_params = bool(inspect.signature(wrapper_fn).parameters)\n\n        # Create metadata first\n        resource_meta = ResourceMeta(\n            uri=uri,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            mime_type=mime_type,\n            annotations=annotations,\n            meta=meta,\n            task=resolved,\n            auth=auth,\n        )\n\n        if has_uri_params or has_func_params:\n            # ResourceTemplate doesn't have metadata support yet, so pass individual params\n            return ResourceTemplate.from_function(\n                fn=fn,\n                uri_template=uri,\n                name=name,\n                version=version,\n                title=title,\n                description=description,\n                icons=icons,\n                mime_type=mime_type,\n                tags=tags,\n                annotations=annotations,\n                meta=meta,\n                task=resolved,\n                auth=auth,\n            )\n        else:\n            return FunctionResource.from_function(fn, metadata=resource_meta)\n\n    def attach_metadata(fn: F) -> F:\n        metadata = ResourceMeta(\n            uri=uri,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            mime_type=mime_type,\n            annotations=annotations,\n            meta=meta,\n            task=task,\n            auth=auth,\n        )\n        target = fn.__func__ if hasattr(fn, \"__func__\") else fn\n        target.__fastmcp__ = metadata\n        return fn\n\n    def decorator(fn: F) -> F:\n        if fastmcp.settings.decorator_mode == \"object\":\n            warnings.warn(\n                \"decorator_mode='object' is deprecated and will be removed in a future version. \"\n                \"Decorators now return the original function with metadata attached.\",\n                DeprecationWarning,\n                stacklevel=3,\n            )\n            return create_resource(fn)  # type: ignore[return-value]\n        return attach_metadata(fn)\n\n    return decorator\n"
  },
  {
    "path": "src/fastmcp/resources/template.py",
    "content": "\"\"\"Resource template functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any, ClassVar, overload\nfrom urllib.parse import parse_qs, unquote\n\nimport mcp.types\nfrom mcp.types import Annotations, Icon\nfrom pydantic.json_schema import SkipJsonSchema\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\nfrom mcp.types import ResourceTemplate as SDKResourceTemplate\nfrom pydantic import (\n    Field,\n    field_validator,\n    validate_call,\n)\n\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.server.apps import resolve_ui_mime_type\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.dependencies import (\n    transform_context_annotations,\n    without_injected_parameters,\n)\nfrom fastmcp.server.tasks.config import TaskConfig, TaskMeta\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.types import get_cached_typeadapter\n\n\ndef extract_query_params(uri_template: str) -> set[str]:\n    \"\"\"Extract query parameter names from RFC 6570 `{?param1,param2}` syntax.\"\"\"\n    match = re.search(r\"\\{\\?([^}]+)\\}\", uri_template)\n    if match:\n        return {p.strip() for p in match.group(1).split(\",\")}\n    return set()\n\n\ndef build_regex(template: str) -> re.Pattern[str] | None:\n    \"\"\"Build regex pattern for URI template, handling RFC 6570 syntax.\n\n    Supports:\n    - `{var}` - simple path parameter\n    - `{var*}` - wildcard path parameter (captures multiple segments)\n    - `{?var1,var2}` - query parameters (ignored in path matching)\n\n    Returns None if the template produces an invalid regex (e.g. parameter\n    names with hyphens, leading digits, or duplicates from a remote server).\n    \"\"\"\n    # Remove query parameter syntax for path matching\n    template_without_query = re.sub(r\"\\{\\?[^}]+\\}\", \"\", template)\n\n    parts = re.split(r\"(\\{[^}]+\\})\", template_without_query)\n    pattern = \"\"\n    for part in parts:\n        if part.startswith(\"{\") and part.endswith(\"}\"):\n            name = part[1:-1]\n            if name.endswith(\"*\"):\n                name = name[:-1]\n                pattern += f\"(?P<{name}>.+)\"\n            else:\n                pattern += f\"(?P<{name}>[^/]+)\"\n        else:\n            pattern += re.escape(part)\n    try:\n        return re.compile(f\"^{pattern}$\")\n    except re.error:\n        return None\n\n\ndef match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:\n    \"\"\"Match URI against template and extract both path and query parameters.\n\n    Supports RFC 6570 URI templates:\n    - Path params: `{var}`, `{var*}`\n    - Query params: `{?var1,var2}`\n    \"\"\"\n    # Split URI into path and query parts\n    uri_path, _, query_string = uri.partition(\"?\")\n\n    # Match path parameters\n    regex = build_regex(uri_template)\n    if regex is None:\n        return None\n    match = regex.match(uri_path)\n    if not match:\n        return None\n\n    params = {k: unquote(v) for k, v in match.groupdict().items()}\n\n    # Extract query parameters if present in URI and template\n    if query_string:\n        query_param_names = extract_query_params(uri_template)\n        parsed_query = parse_qs(query_string)\n\n        for name in query_param_names:\n            if name in parsed_query:\n                # Take first value if multiple provided\n                params[name] = parsed_query[name][0]\n\n    return params\n\n\nclass ResourceTemplate(FastMCPComponent):\n    \"\"\"A template for dynamically creating resources.\"\"\"\n\n    KEY_PREFIX: ClassVar[str] = \"template\"\n\n    uri_template: str = Field(\n        description=\"URI template with parameters (e.g. weather://{city}/current)\"\n    )\n    mime_type: str = Field(\n        default=\"text/plain\", description=\"MIME type of the resource content\"\n    )\n    parameters: dict[str, Any] = Field(\n        description=\"JSON schema for function parameters\"\n    )\n    annotations: Annotations | None = Field(\n        default=None, description=\"Optional annotations about the resource's behavior\"\n    )\n    auth: SkipJsonSchema[AuthCheck | list[AuthCheck] | None] = Field(\n        default=None,\n        description=\"Authorization checks for this resource template\",\n        exclude=True,\n    )\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})\"\n\n    @staticmethod\n    def from_function(\n        fn: Callable[..., Any],\n        uri_template: str,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        mime_type: str | None = None,\n        tags: set[str] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionResourceTemplate:\n        return FunctionResourceTemplate.from_function(\n            fn=fn,\n            uri_template=uri_template,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            mime_type=mime_type,\n            tags=tags,\n            annotations=annotations,\n            meta=meta,\n            task=task,\n            auth=auth,\n        )\n\n    @field_validator(\"mime_type\", mode=\"before\")\n    @classmethod\n    def set_default_mime_type(cls, mime_type: str | None) -> str:\n        \"\"\"Set default MIME type if not provided.\"\"\"\n        if mime_type:\n            return mime_type\n        return \"text/plain\"\n\n    def matches(self, uri: str) -> dict[str, Any] | None:\n        \"\"\"Check if URI matches template and extract parameters.\"\"\"\n        return match_uri_template(uri, self.uri_template)\n\n    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:\n        \"\"\"Read the resource content.\"\"\"\n        raise NotImplementedError(\n            \"Subclasses must implement read() or override create_resource()\"\n        )\n\n    def convert_result(self, raw_value: Any) -> ResourceResult:\n        \"\"\"Convert a raw result to ResourceResult.\n\n        This is used in two contexts:\n        1. In _read() to convert user function return values to ResourceResult\n        2. In tasks_result_handler() to convert Docket task results to ResourceResult\n\n        Handles ResourceResult passthrough and converts raw values using\n        ResourceResult's normalization.\n        \"\"\"\n        if isinstance(raw_value, ResourceResult):\n            return raw_value\n\n        # ResourceResult.__init__ handles all normalization\n        return ResourceResult(raw_value)\n\n    @overload\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: None = None\n    ) -> ResourceResult: ...\n\n    @overload\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: TaskMeta\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None\n    ) -> ResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Server entry point that handles task routing.\n\n        This allows ANY ResourceTemplate subclass to support background execution\n        by setting task_config.mode to \"supported\" or \"required\". The server calls\n        this method instead of create_resource()/read() directly.\n\n        Args:\n            uri: The concrete URI being read\n            params: Template parameters extracted from the URI\n            task_meta: If provided, execute as a background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return ResourceResult.\n\n        Returns:\n            ResourceResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Subclasses can override this to customize task routing behavior.\n        For example, FastMCPProviderResourceTemplate overrides to delegate to child\n        middleware without submitting to Docket.\n        \"\"\"\n        from fastmcp.server.tasks.routing import check_background_task\n\n        task_result = await check_background_task(\n            component=self, task_type=\"template\", arguments=params, task_meta=task_meta\n        )\n        if task_result:\n            return task_result\n\n        # Synchronous execution - create resource and read directly\n        # Call resource.read() not resource._read() to avoid task routing on ephemeral resource\n        resource = await self.create_resource(uri, params)\n        result = await resource.read()\n        return self.convert_result(result)\n\n    async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:\n        \"\"\"Create a resource from the template with the given parameters.\n\n        The base implementation does not support background tasks.\n        Use FunctionResourceTemplate for task support.\n        \"\"\"\n        raise NotImplementedError(\n            \"Subclasses must implement create_resource(). \"\n            \"Use FunctionResourceTemplate for task support.\"\n        )\n\n    def to_mcp_template(\n        self,\n        **overrides: Any,\n    ) -> SDKResourceTemplate:\n        \"\"\"Convert the resource template to an SDKResourceTemplate.\"\"\"\n\n        return SDKResourceTemplate(\n            name=overrides.get(\"name\", self.name),\n            uriTemplate=overrides.get(\"uriTemplate\", self.uri_template),\n            description=overrides.get(\"description\", self.description),\n            mimeType=overrides.get(\"mimeType\", self.mime_type),\n            title=overrides.get(\"title\", self.title),\n            icons=overrides.get(\"icons\", self.icons),\n            annotations=overrides.get(\"annotations\", self.annotations),\n            _meta=overrides.get(  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                \"_meta\", self.get_meta()\n            ),\n        )\n\n    @classmethod\n    def from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate:\n        \"\"\"Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object.\"\"\"\n        # Note: This creates a simple ResourceTemplate instance. For function-based templates,\n        # the original function is lost, which is expected for remote templates.\n        return cls(\n            uri_template=mcp_template.uriTemplate,\n            name=mcp_template.name,\n            description=mcp_template.description,\n            mime_type=mcp_template.mimeType or \"text/plain\",\n            parameters={},  # Remote templates don't have local parameters\n        )\n\n    @property\n    def key(self) -> str:\n        \"\"\"The globally unique lookup key for this template.\"\"\"\n        base_key = self.make_key(self.uri_template)\n        return f\"{base_key}@{self.version or ''}\"\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this template with docket for background execution.\"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.read, names=[self.key])\n\n    async def add_to_docket(  # type: ignore[override]\n        self,\n        docket: Docket,\n        params: dict[str, Any],\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this template for background execution via docket.\n\n        Args:\n            docket: The Docket instance\n            params: Template parameters\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(params)\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.component.type\": \"resource_template\",\n            \"fastmcp.provider.type\": \"LocalProvider\",\n        }\n\n\nclass FunctionResourceTemplate(ResourceTemplate):\n    \"\"\"A template for dynamically creating resources.\"\"\"\n\n    fn: SkipJsonSchema[Callable[..., Any]]\n\n    @overload\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: None = None\n    ) -> ResourceResult: ...\n\n    @overload\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: TaskMeta\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None\n    ) -> ResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Optimized server entry point that skips ephemeral resource creation.\n\n        For FunctionResourceTemplate, we can call read() directly instead of\n        creating a temporary resource, which is more efficient.\n\n        Args:\n            uri: The concrete URI being read\n            params: Template parameters extracted from the URI\n            task_meta: If provided, execute as a background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return ResourceResult.\n\n        Returns:\n            ResourceResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n        \"\"\"\n        from fastmcp.server.tasks.routing import check_background_task\n\n        task_result = await check_background_task(\n            component=self, task_type=\"template\", arguments=params, task_meta=task_meta\n        )\n        if task_result:\n            return task_result\n\n        # Synchronous execution - call read() directly, skip resource creation\n        result = await self.read(arguments=params)\n        return self.convert_result(result)\n\n    async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:\n        \"\"\"Create a resource from the template with the given parameters.\"\"\"\n\n        async def resource_read_fn() -> str | bytes | ResourceResult:\n            # Call function and check if result is a coroutine\n            result = await self.read(arguments=params)\n            return result\n\n        return Resource.from_function(\n            fn=resource_read_fn,\n            uri=uri,\n            name=self.name,\n            description=self.description,\n            mime_type=self.mime_type,\n            tags=self.tags,\n            task=self.task_config,\n            auth=self.auth,\n        )\n\n    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:\n        \"\"\"Read the resource content.\"\"\"\n        # Type coercion for query parameters (which arrive as strings)\n        kwargs = arguments.copy()\n        sig = inspect.signature(self.fn)\n        for param_name, param_value in list(kwargs.items()):\n            if param_name in sig.parameters and isinstance(param_value, str):\n                param = sig.parameters[param_name]\n                annotation = param.annotation\n\n                if annotation is inspect.Parameter.empty or annotation is str:\n                    continue\n\n                try:\n                    if annotation is int:\n                        kwargs[param_name] = int(param_value)\n                    elif annotation is float:\n                        kwargs[param_name] = float(param_value)\n                    elif annotation is bool:\n                        lower = param_value.lower()\n                        if lower in (\"true\", \"1\", \"yes\"):\n                            kwargs[param_name] = True\n                        elif lower in (\"false\", \"0\", \"no\"):\n                            kwargs[param_name] = False\n                        else:\n                            raise ValueError(\n                                f\"Invalid boolean value for {param_name}: {param_value!r}\"\n                            )\n                except (ValueError, AttributeError):\n                    raise\n\n        # self.fn is wrapped by without_injected_parameters which handles\n        # dependency resolution internally, so we call it directly\n        result = self.fn(**kwargs)\n        if inspect.isawaitable(result):\n            result = await result\n\n        return result\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this template with docket for background execution.\n\n        FunctionResourceTemplate registers the underlying function, which has the\n        user's Depends parameters for docket to resolve.\n        \"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.fn, names=[self.key])\n\n    async def add_to_docket(\n        self,\n        docket: Docket,\n        params: dict[str, Any],\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this template for background execution via docket.\n\n        FunctionResourceTemplate splats the params dict since .fn expects **kwargs.\n\n        Args:\n            docket: The Docket instance\n            params: Template parameters\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(**params)\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        uri_template: str,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        mime_type: str | None = None,\n        tags: set[str] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionResourceTemplate:\n        \"\"\"Create a template from a function.\"\"\"\n\n        func_name = name or getattr(fn, \"__name__\", None) or fn.__class__.__name__\n        if func_name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        # Reject functions with *args\n        # (**kwargs is allowed because the URI will define the parameter names)\n        sig = inspect.signature(fn)\n        for param in sig.parameters.values():\n            if param.kind == inspect.Parameter.VAR_POSITIONAL:\n                raise ValueError(\n                    \"Functions with *args are not supported as resource templates\"\n                )\n\n        # Extract path and query parameters from URI template\n        path_params = set(re.findall(r\"{(\\w+)(?:\\*)?}\", uri_template))\n        query_params = extract_query_params(uri_template)\n        all_uri_params = path_params | query_params\n\n        if not all_uri_params:\n            raise ValueError(\"URI template must contain at least one parameter\")\n\n        # Use wrapper to get user-facing parameters (excludes injected params)\n        wrapper_fn = without_injected_parameters(fn)\n        user_sig = inspect.signature(wrapper_fn)\n        func_params = set(user_sig.parameters.keys())\n\n        # Get required and optional function parameters\n        required_params = {\n            p\n            for p in func_params\n            if user_sig.parameters[p].default is inspect.Parameter.empty\n            and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD\n        }\n        optional_params = {\n            p\n            for p in func_params\n            if user_sig.parameters[p].default is not inspect.Parameter.empty\n            and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD\n        }\n\n        # Validate RFC 6570 query parameters\n        # Query params must be optional (have defaults)\n        if query_params:\n            invalid_query_params = query_params - optional_params\n            if invalid_query_params:\n                raise ValueError(\n                    f\"Query parameters {invalid_query_params} must be optional function parameters with default values\"\n                )\n\n        # Check if required parameters are a subset of the path parameters\n        if not required_params.issubset(path_params):\n            raise ValueError(\n                f\"Required function arguments {required_params} must be a subset of the URI path parameters {path_params}\"\n            )\n\n        # Check if all URI parameters are valid function parameters (skip if **kwargs present)\n        if not any(\n            param.kind == inspect.Parameter.VAR_KEYWORD\n            for param in sig.parameters.values()\n        ):\n            if not all_uri_params.issubset(func_params):\n                raise ValueError(\n                    f\"URI parameters {all_uri_params} must be a subset of the function arguments: {func_params}\"\n                )\n\n        description = description or inspect.getdoc(fn)\n\n        # Normalize task to TaskConfig and validate\n        if task is None:\n            task_config = TaskConfig(mode=\"forbidden\")\n        elif isinstance(task, bool):\n            task_config = TaskConfig.from_bool(task)\n        else:\n            task_config = task\n        task_config.validate_function(fn, func_name)\n\n        # if the fn is a callable class, we need to get the __call__ method from here out\n        if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):\n            fn = fn.__call__\n        # if the fn is a staticmethod, we need to work with the underlying function\n        if isinstance(fn, staticmethod):\n            fn = fn.__func__\n\n        # Transform Context type annotations to Depends() for unified DI\n        fn = transform_context_annotations(fn)\n\n        wrapper_fn = without_injected_parameters(fn)\n        type_adapter = get_cached_typeadapter(wrapper_fn)\n        parameters = type_adapter.json_schema()\n        parameters = compress_schema(parameters, prune_titles=True)\n\n        # Use validate_call on wrapper for runtime type coercion\n        fn = validate_call(wrapper_fn)\n\n        # Apply ui:// MIME default, then fall back to text/plain\n        resolved_mime = resolve_ui_mime_type(uri_template, mime_type)\n\n        return cls(\n            uri_template=uri_template,\n            name=func_name,\n            version=str(version) if version is not None else None,\n            title=title,\n            description=description,\n            icons=icons,\n            mime_type=resolved_mime or \"text/plain\",\n            fn=fn,\n            parameters=parameters,\n            tags=tags or set(),\n            annotations=annotations,\n            meta=meta,\n            task_config=task_config,\n            auth=auth,\n        )\n"
  },
  {
    "path": "src/fastmcp/resources/types.py",
    "content": "\"\"\"Concrete resource implementations.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nimport httpx\nimport pydantic.json\nfrom anyio import Path as AsyncPath\nfrom pydantic import Field, ValidationInfo\nfrom typing_extensions import override\n\nfrom fastmcp.exceptions import ResourceError\nfrom fastmcp.resources.base import Resource, ResourceContent, ResourceResult\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass TextResource(Resource):\n    \"\"\"A resource that reads from a string.\"\"\"\n\n    text: str = Field(description=\"Text content of the resource\")\n\n    async def read(self) -> ResourceResult:\n        \"\"\"Read the text content.\"\"\"\n        return ResourceResult(\n            contents=[\n                ResourceContent(\n                    content=self.text, mime_type=self.mime_type, meta=self.meta\n                )\n            ]\n        )\n\n\nclass BinaryResource(Resource):\n    \"\"\"A resource that reads from bytes.\"\"\"\n\n    data: bytes = Field(description=\"Binary content of the resource\")\n\n    async def read(self) -> ResourceResult:\n        \"\"\"Read the binary content.\"\"\"\n        return ResourceResult(\n            contents=[\n                ResourceContent(\n                    content=self.data, mime_type=self.mime_type, meta=self.meta\n                )\n            ]\n        )\n\n\nclass FileResource(Resource):\n    \"\"\"A resource that reads from a file.\n\n    Set is_binary=True to read file as binary data instead of text.\n    \"\"\"\n\n    path: Path = Field(description=\"Path to the file\")\n    is_binary: bool = Field(\n        default=False,\n        description=\"Whether to read the file as binary data\",\n    )\n    mime_type: str = Field(\n        default=\"text/plain\",\n        description=\"MIME type of the resource content\",\n    )\n\n    @property\n    def _async_path(self) -> AsyncPath:\n        return AsyncPath(self.path)\n\n    @pydantic.field_validator(\"path\")\n    @classmethod\n    def validate_absolute_path(cls, path: Path) -> Path:\n        \"\"\"Ensure path is absolute.\"\"\"\n        if not path.is_absolute():\n            raise ValueError(\"Path must be absolute\")\n        return path\n\n    @pydantic.field_validator(\"is_binary\")\n    @classmethod\n    def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool:\n        \"\"\"Set is_binary based on mime_type if not explicitly set.\"\"\"\n        if is_binary:\n            return True\n        mime_type = info.data.get(\"mime_type\", \"text/plain\")\n        return not mime_type.startswith(\"text/\")\n\n    @override\n    async def read(self) -> ResourceResult:\n        \"\"\"Read the file content.\"\"\"\n        try:\n            if self.is_binary:\n                content: str | bytes = await self._async_path.read_bytes()\n            else:\n                content = await self._async_path.read_text()\n            return ResourceResult(\n                contents=[ResourceContent(content=content, mime_type=self.mime_type)]\n            )\n        except Exception as e:\n            raise ResourceError(f\"Error reading file {self.path}\") from e\n\n\nclass HttpResource(Resource):\n    \"\"\"A resource that reads from an HTTP endpoint.\"\"\"\n\n    url: str = Field(description=\"URL to fetch content from\")\n    mime_type: str = Field(\n        default=\"application/json\", description=\"MIME type of the resource content\"\n    )\n\n    @override\n    async def read(self) -> ResourceResult:\n        \"\"\"Read the HTTP content.\"\"\"\n        async with httpx.AsyncClient() as client:\n            response = await client.get(self.url)\n            _ = response.raise_for_status()\n            return ResourceResult(\n                contents=[\n                    ResourceContent(content=response.text, mime_type=self.mime_type)\n                ]\n            )\n\n\nclass DirectoryResource(Resource):\n    \"\"\"A resource that lists files in a directory.\"\"\"\n\n    path: Path = Field(description=\"Path to the directory\")\n    recursive: bool = Field(\n        default=False, description=\"Whether to list files recursively\"\n    )\n    pattern: str | None = Field(\n        default=None, description=\"Optional glob pattern to filter files\"\n    )\n    mime_type: str = Field(\n        default=\"application/json\", description=\"MIME type of the resource content\"\n    )\n\n    @property\n    def _async_path(self) -> AsyncPath:\n        return AsyncPath(self.path)\n\n    @pydantic.field_validator(\"path\")\n    @classmethod\n    def validate_absolute_path(cls, path: Path) -> Path:\n        \"\"\"Ensure path is absolute.\"\"\"\n        if not path.is_absolute():\n            raise ValueError(\"Path must be absolute\")\n        return path\n\n    async def list_files(self) -> list[Path]:\n        \"\"\"List files in the directory.\"\"\"\n        if not await self._async_path.exists():\n            raise FileNotFoundError(f\"Directory not found: {self.path}\")\n        if not await self._async_path.is_dir():\n            raise NotADirectoryError(f\"Not a directory: {self.path}\")\n\n        pattern = self.pattern or \"*\"\n\n        glob_fn = self._async_path.rglob if self.recursive else self._async_path.glob\n        try:\n            return [Path(p) async for p in glob_fn(pattern) if await p.is_file()]\n        except Exception as e:\n            raise ResourceError(f\"Error listing directory {self.path}\") from e\n\n    @override\n    async def read(self) -> ResourceResult:\n        \"\"\"Read the directory listing.\"\"\"\n        try:\n            files: list[Path] = await self.list_files()\n\n            file_list = [str(f.relative_to(self.path)) for f in files]\n\n            content = json.dumps({\"files\": file_list}, indent=2)\n            return ResourceResult(\n                contents=[ResourceContent(content=content, mime_type=self.mime_type)]\n            )\n        except Exception as e:\n            raise ResourceError(f\"Error reading directory {self.path}\") from e\n"
  },
  {
    "path": "src/fastmcp/server/__init__.py",
    "content": "import importlib\n\nfrom .context import Context\nfrom .server import FastMCP, create_proxy\n\n\ndef __getattr__(name: str) -> object:\n    if name == \"dependencies\":\n        return importlib.import_module(\"fastmcp.server.dependencies\")\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n\n\n__all__ = [\"Context\", \"FastMCP\", \"create_proxy\"]\n"
  },
  {
    "path": "src/fastmcp/server/app.py",
    "content": "\"\"\"FastMCPApp — a Provider that represents a composable MCP application.\n\nFastMCPApp binds entry-point tools (model calls these) together with backend\ntools (the UI calls these via CallTool). Backend tools get global keys —\nUUID-suffixed stable identifiers that survive namespace transforms when\nservers are composed — so ``CallTool(save_contact)`` keeps working even when\nthe app is mounted under a namespace.\n\nUsage::\n\n    from fastmcp import FastMCP, FastMCPApp\n\n    app = FastMCPApp(\"Dashboard\")\n\n    @app.ui()\n    def show_dashboard() -> Component:\n        return Column(...)\n\n    @app.tool()\n    def save_contact(name: str, email: str) -> dict:\n        return {\"name\": name, \"email\": email}\n\n    server = FastMCP(\"Platform\")\n    server.add_provider(app)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport uuid\nfrom collections.abc import AsyncIterator, Callable, Sequence\nfrom contextlib import asynccontextmanager, suppress\nfrom typing import Any, Literal, TypeVar, overload\n\nfrom mcp.types import AnyFunction, Icon, ToolAnnotations\n\nfrom fastmcp.decorators import get_fastmcp_meta\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.providers.local_provider import LocalProvider\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\n# ---------------------------------------------------------------------------\n# Process-level registries\n# ---------------------------------------------------------------------------\n# Global key → Tool object.  FastMCP.call_tool checks this before normal\n# provider resolution so that CallTool(\"save_contact-a1b2c3d4\") reaches the\n# right tool regardless of namespace transforms.\n_APP_TOOL_REGISTRY: dict[str, Tool] = {}\n\n# id(original_fn) → global key.  Used by the CallTool callable resolver to\n# translate ``CallTool(save_contact)`` → ``\"save_contact-a1b2c3d4\"``.\n_FN_TO_GLOBAL_KEY: dict[int, str] = {}\n\n\ndef get_global_tool(name: str) -> Tool | None:\n    \"\"\"Look up a tool by its global key, or return None.\"\"\"\n    return _APP_TOOL_REGISTRY.get(name)\n\n\n# ---------------------------------------------------------------------------\n# Global key helpers\n# ---------------------------------------------------------------------------\n\n\ndef _make_global_key(name: str) -> str:\n    \"\"\"Generate a global key: ``{name}-{8_hex_chars}``.\"\"\"\n    return f\"{name}-{uuid.uuid4().hex[:8]}\"\n\n\ndef _register_global_key(tool: Tool, fn: Any, global_key: str) -> None:\n    \"\"\"Register a tool in both process-level registries.\"\"\"\n    _APP_TOOL_REGISTRY[global_key] = tool\n    _FN_TO_GLOBAL_KEY[id(fn)] = global_key\n\n\ndef _stamp_global_key(tool: Tool, global_key: str) -> None:\n    \"\"\"Write the global key into the tool's ``meta[\"ui\"][\"globalKey\"]``.\"\"\"\n    meta = dict(tool.meta) if tool.meta else {}\n    ui = dict(meta.get(\"ui\", {})) if isinstance(meta.get(\"ui\"), dict) else {}\n    ui[\"globalKey\"] = global_key\n    meta[\"ui\"] = ui\n    tool.meta = meta\n\n\n# ---------------------------------------------------------------------------\n# CallTool callable resolver\n# ---------------------------------------------------------------------------\n\n\ndef _resolve_tool_ref(fn: Any) -> Any:\n    \"\"\"Resolve a callable to a ``ResolvedTool`` for CallTool serialization.\n\n    Always returns a ``ResolvedTool`` with the resolved name and any\n    metadata the renderer needs (e.g. ``unwrap_result``).\n\n    Resolution order:\n    1. Global key registry (FastMCPApp tools) — includes metadata\n    2. ``__fastmcp__`` metadata (decorated but not on a FastMCPApp)\n    3. ``fn.__name__`` (bare function — works for standalone servers)\n    \"\"\"\n    from prefab_ui.app import ResolvedTool\n\n    global_key = _FN_TO_GLOBAL_KEY.get(id(fn))\n    if global_key is not None:\n        tool = _APP_TOOL_REGISTRY.get(global_key)\n        unwrap = bool(\n            tool is not None\n            and tool.output_schema\n            and tool.output_schema.get(\"x-fastmcp-wrap-result\")\n        )\n        return ResolvedTool(name=global_key, unwrap_result=unwrap)\n\n    fmeta = get_fastmcp_meta(fn)\n    if fmeta is not None:\n        name: str | None = getattr(fmeta, \"name\", None)\n        if name is not None:\n            return ResolvedTool(name=name)\n\n    fn_name = getattr(fn, \"__name__\", None)\n    if fn_name is not None:\n        return ResolvedTool(name=fn_name)\n\n    raise ValueError(f\"Cannot resolve tool reference: {fn!r}\")\n\n\ndef _dispatch_decorator(\n    name_or_fn: str | AnyFunction | None,\n    name: str | None,\n    register: Callable[[Any, str | None], Any],\n    decorator_name: str,\n) -> Any:\n    \"\"\"Shared dispatch logic for @app.tool() and @app.ui() calling patterns.\"\"\"\n    if inspect.isroutine(name_or_fn):\n        return register(name_or_fn, name)\n\n    if isinstance(name_or_fn, str):\n        if name is not None:\n            raise TypeError(\n                \"Cannot specify both a name as first argument and as keyword argument.\"\n            )\n        tool_name: str | None = name_or_fn\n    elif name_or_fn is None:\n        tool_name = name\n    else:\n        raise TypeError(\n            f\"First argument to @{decorator_name} must be a function, string, or None, \"\n            f\"got {type(name_or_fn)}\"\n        )\n\n    def decorator(fn: F) -> F:\n        return register(fn, tool_name)\n\n    return decorator\n\n\n# ---------------------------------------------------------------------------\n# FastMCPApp\n# ---------------------------------------------------------------------------\n\n\nclass FastMCPApp(Provider):\n    \"\"\"A Provider that represents an MCP application.\n\n    Binds together entry-point tools (``@app.ui``), backend tools\n    (``@app.tool``), the Prefab renderer resource, and global-key\n    infrastructure so that composed/namespaced servers can still reach\n    backend tools by stable identifiers.\n    \"\"\"\n\n    def __init__(self, name: str) -> None:\n        super().__init__()\n        self.name = name\n        self._local = LocalProvider(on_duplicate=\"error\")\n\n    def __repr__(self) -> str:\n        return f\"FastMCPApp({self.name!r})\"\n\n    # ------------------------------------------------------------------\n    # @app.tool() — backend tools called by the UI\n    # ------------------------------------------------------------------\n\n    @overload\n    def tool(\n        self,\n        name_or_fn: F,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        model: bool = False,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n        timeout: float | None = None,\n    ) -> F: ...\n\n    @overload\n    def tool(\n        self,\n        name_or_fn: str | None = None,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        model: bool = False,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n        timeout: float | None = None,\n    ) -> Callable[[F], F]: ...\n\n    def tool(\n        self,\n        name_or_fn: str | AnyFunction | None = None,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        model: bool = False,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n        timeout: float | None = None,\n    ) -> Any:\n        \"\"\"Register a backend tool that the UI calls via CallTool.\n\n        Backend tools get a global key for composition safety and default\n        to ``visibility=[\"app\"]``.  Pass ``model=True`` to also expose the\n        tool to the model (``visibility=[\"app\", \"model\"]``).\n\n        Supports multiple calling patterns::\n\n            @app.tool\n            def save(name: str): ...\n\n            @app.tool()\n            def save(name: str): ...\n\n            @app.tool(\"custom_name\")\n            def save(name: str): ...\n        \"\"\"\n        visibility: list[Literal[\"app\", \"model\"]] = (\n            [\"app\", \"model\"] if model else [\"app\"]\n        )\n\n        def _register(fn: F, tool_name: str | None) -> F:\n            resolved_name = tool_name or getattr(fn, \"__name__\", None)\n            if resolved_name is None:\n                raise ValueError(f\"Cannot determine tool name for {fn!r}\")\n\n            from fastmcp.server.apps import AppConfig, app_config_to_meta_dict\n\n            global_key = _make_global_key(resolved_name)\n            app_config = AppConfig(visibility=visibility)\n            meta: dict[str, Any] = {\"ui\": app_config_to_meta_dict(app_config)}\n            meta[\"ui\"][\"globalKey\"] = global_key\n\n            tool_obj = Tool.from_function(\n                fn,\n                name=resolved_name,\n                description=description,\n                meta=meta,\n                timeout=timeout,\n                auth=auth,\n            )\n            self._local._add_component(tool_obj)\n            _register_global_key(tool_obj, fn, global_key)\n            return fn\n\n        return _dispatch_decorator(name_or_fn, name, _register, \"tool\")\n\n    # ------------------------------------------------------------------\n    # @app.ui() — entry-point tools the model calls to open the app\n    # ------------------------------------------------------------------\n\n    @overload\n    def ui(\n        self,\n        name_or_fn: F,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        title: str | None = None,\n        tags: set[str] | None = None,\n        icons: list[Icon] | None = None,\n        annotations: ToolAnnotations | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n        timeout: float | None = None,\n    ) -> F: ...\n\n    @overload\n    def ui(\n        self,\n        name_or_fn: str | None = None,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        title: str | None = None,\n        tags: set[str] | None = None,\n        icons: list[Icon] | None = None,\n        annotations: ToolAnnotations | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n        timeout: float | None = None,\n    ) -> Callable[[F], F]: ...\n\n    def ui(\n        self,\n        name_or_fn: str | AnyFunction | None = None,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        title: str | None = None,\n        tags: set[str] | None = None,\n        icons: list[Icon] | None = None,\n        annotations: ToolAnnotations | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n        timeout: float | None = None,\n    ) -> Any:\n        \"\"\"Register a UI entry-point tool that the model calls.\n\n        Entry-point tools default to ``visibility=[\"model\"]`` and auto-wire\n        the Prefab renderer resource and CSP. They do NOT get a global key —\n        the model resolves them through the normal transform chain.\n\n        Supports multiple calling patterns::\n\n            @app.ui\n            def dashboard() -> Component: ...\n\n            @app.ui()\n            def dashboard() -> Component: ...\n\n            @app.ui(\"my_dashboard\")\n            def dashboard() -> Component: ...\n        \"\"\"\n\n        def _register(fn: F, tool_name: str | None) -> F:\n            from fastmcp.server.apps import AppConfig, app_config_to_meta_dict\n            from fastmcp.server.providers.local_provider.decorators.tools import (\n                PREFAB_RENDERER_URI,\n                _ensure_prefab_renderer,\n            )\n\n            try:\n                from prefab_ui.renderer import get_renderer_csp\n\n                from fastmcp.server.apps import ResourceCSP\n\n                csp = get_renderer_csp()\n                app_config = AppConfig(\n                    resource_uri=PREFAB_RENDERER_URI,\n                    visibility=[\"model\"],\n                    csp=ResourceCSP(\n                        resource_domains=csp.get(\"resource_domains\"),\n                        connect_domains=csp.get(\"connect_domains\"),\n                    ),\n                )\n            except ImportError:\n                app_config = AppConfig(\n                    resource_uri=PREFAB_RENDERER_URI,\n                    visibility=[\"model\"],\n                )\n\n            meta: dict[str, Any] = {\"ui\": app_config_to_meta_dict(app_config)}\n\n            tool_obj = Tool.from_function(\n                fn,\n                name=tool_name,\n                description=description,\n                title=title,\n                tags=tags,\n                icons=icons,\n                annotations=annotations,\n                meta=meta,\n                timeout=timeout,\n                auth=auth,\n            )\n            self._local._add_component(tool_obj)\n\n            # Register the Prefab renderer resource on the internal provider\n            with suppress(ImportError):\n                _ensure_prefab_renderer(self._local)\n\n            return fn\n\n        return _dispatch_decorator(name_or_fn, name, _register, \"ui\")\n\n    # ------------------------------------------------------------------\n    # Programmatic tool addition\n    # ------------------------------------------------------------------\n\n    def add_tool(\n        self,\n        tool: Tool | Callable[..., Any],\n        *,\n        fn: Any | None = None,\n    ) -> Tool:\n        \"\"\"Add a tool to this app programmatically.\n\n        If the tool has ``meta[\"ui\"][\"globalKey\"]``, it is assumed to already\n        be configured (but still registered for lookup). Otherwise it is\n        treated as a backend tool and gets a global key assigned automatically.\n\n        Pass ``fn`` to register the original callable in the resolver so that\n        ``CallTool(fn)`` can resolve to the global key.\n        \"\"\"\n        if not isinstance(tool, Tool):\n            fn = fn or tool\n            tool = Tool._ensure_tool(tool)\n\n        meta = tool.meta or {}\n        ui = meta.get(\"ui\", {})\n        if isinstance(ui, dict) and \"globalKey\" in ui:\n            global_key = ui[\"globalKey\"]\n        else:\n            global_key = _make_global_key(tool.name)\n            _stamp_global_key(tool, global_key)\n\n        self._local._add_component(tool)\n\n        _APP_TOOL_REGISTRY[global_key] = tool\n        if fn is not None:\n            _FN_TO_GLOBAL_KEY[id(fn)] = global_key\n\n        return tool\n\n    # ------------------------------------------------------------------\n    # Provider interface — delegate to internal LocalProvider\n    # ------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        return await self._local._list_tools()\n\n    async def _get_tool(self, name: str, version: Any = None) -> Tool | None:\n        return await self._local._get_tool(name, version)\n\n    async def _list_resources(self) -> Sequence[Any]:\n        return await self._local._list_resources()\n\n    async def _get_resource(self, uri: str, version: Any = None) -> Any | None:\n        return await self._local._get_resource(uri, version)\n\n    async def _list_resource_templates(self) -> Sequence[Any]:\n        return await self._local._list_resource_templates()\n\n    async def _get_resource_template(self, uri: str, version: Any = None) -> Any | None:\n        return await self._local._get_resource_template(uri, version)\n\n    async def _list_prompts(self) -> Sequence[Any]:\n        return await self._local._list_prompts()\n\n    async def _get_prompt(self, name: str, version: Any = None) -> Any | None:\n        return await self._local._get_prompt(name, version)\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        async with self._local.lifespan():\n            yield\n\n    # ------------------------------------------------------------------\n    # Convenience runner\n    # ------------------------------------------------------------------\n\n    def run(\n        self,\n        transport: Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Create a temporary FastMCP server and run this app standalone.\"\"\"\n        from fastmcp.server.server import FastMCP\n\n        server = FastMCP(self.name)\n        server.add_provider(self)\n        server.run(transport=transport, **kwargs)\n"
  },
  {
    "path": "src/fastmcp/server/apps.py",
    "content": "\"\"\"MCP Apps support — extension negotiation and typed UI metadata models.\n\nProvides constants and Pydantic models for the MCP Apps extension\n(io.modelcontextprotocol/ui), enabling tools and resources to carry\nUI metadata for clients that support interactive app rendering.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any, Literal\n\nfrom pydantic import BaseModel, Field\n\nUI_EXTENSION_ID = \"io.modelcontextprotocol/ui\"\nUI_MIME_TYPE = \"text/html;profile=mcp-app\"\n\n\nclass ResourceCSP(BaseModel):\n    \"\"\"Content Security Policy for MCP App resources.\n\n    Declares which external origins the app is allowed to connect to or\n    load resources from.  Hosts use these declarations to build the\n    ``Content-Security-Policy`` header for the sandboxed iframe.\n    \"\"\"\n\n    connect_domains: list[str] | None = Field(\n        default=None,\n        alias=\"connectDomains\",\n        description=\"Origins allowed for fetch/XHR/WebSocket (connect-src)\",\n    )\n    resource_domains: list[str] | None = Field(\n        default=None,\n        alias=\"resourceDomains\",\n        description=\"Origins allowed for scripts, images, styles, fonts (script-src etc.)\",\n    )\n    frame_domains: list[str] | None = Field(\n        default=None,\n        alias=\"frameDomains\",\n        description=\"Origins allowed for nested iframes (frame-src)\",\n    )\n    base_uri_domains: list[str] | None = Field(\n        default=None,\n        alias=\"baseUriDomains\",\n        description=\"Allowed base URIs for the document (base-uri)\",\n    )\n\n    model_config = {\"populate_by_name\": True, \"extra\": \"allow\"}\n\n\nclass ResourcePermissions(BaseModel):\n    \"\"\"Iframe sandbox permissions for MCP App resources.\n\n    Each field, when set (typically to ``{}``), requests that the host\n    grant the corresponding Permission Policy feature to the sandboxed\n    iframe.  Hosts MAY honour these; apps should use JS feature detection\n    as a fallback.\n    \"\"\"\n\n    camera: dict[str, Any] | None = Field(\n        default=None, description=\"Request camera access\"\n    )\n    microphone: dict[str, Any] | None = Field(\n        default=None, description=\"Request microphone access\"\n    )\n    geolocation: dict[str, Any] | None = Field(\n        default=None, description=\"Request geolocation access\"\n    )\n    clipboard_write: dict[str, Any] | None = Field(\n        default=None,\n        alias=\"clipboardWrite\",\n        description=\"Request clipboard-write access\",\n    )\n\n    model_config = {\"populate_by_name\": True, \"extra\": \"allow\"}\n\n\nclass AppConfig(BaseModel):\n    \"\"\"Configuration for MCP App tools and resources.\n\n    Controls how a tool or resource participates in the MCP Apps extension.\n    On tools, ``resource_uri`` and ``visibility`` specify which UI resource\n    to render and where the tool appears.  On resources, those fields must\n    be left unset (the resource itself is the UI).\n\n    All fields use ``exclude_none`` serialization so only explicitly-set\n    values appear on the wire.  Aliases match the MCP Apps wire format\n    (camelCase).\n    \"\"\"\n\n    resource_uri: str | None = Field(\n        default=None,\n        alias=\"resourceUri\",\n        description=\"URI of the UI resource (typically ui:// scheme). Tools only.\",\n    )\n    visibility: list[Literal[\"app\", \"model\"]] | None = Field(\n        default=None,\n        description=\"Where this tool is visible: 'app', 'model', or both. Tools only.\",\n    )\n    csp: ResourceCSP | None = Field(\n        default=None, description=\"Content Security Policy for the app iframe\"\n    )\n    permissions: ResourcePermissions | None = Field(\n        default=None, description=\"Iframe sandbox permissions\"\n    )\n    domain: str | None = Field(default=None, description=\"Domain for the iframe\")\n    prefers_border: bool | None = Field(\n        default=None,\n        alias=\"prefersBorder\",\n        description=\"Whether the UI prefers a visible border\",\n    )\n\n    model_config = {\"populate_by_name\": True, \"extra\": \"allow\"}\n\n\ndef app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Convert an AppConfig or dict to the wire-format dict for ``meta[\"ui\"]``.\"\"\"\n    if isinstance(app, AppConfig):\n        return app.model_dump(by_alias=True, exclude_none=True)\n    return app\n\n\ndef resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None:\n    \"\"\"Return the appropriate MIME type for a resource URI.\n\n    For ``ui://`` scheme resources, defaults to ``UI_MIME_TYPE`` when no\n    explicit MIME type is provided. This ensures UI resources are correctly\n    identified regardless of how they're registered (via FastMCP.resource,\n    the standalone @resource decorator, or resource templates).\n\n    Args:\n        uri: The resource URI string\n        explicit_mime_type: The MIME type explicitly provided by the user\n\n    Returns:\n        The resolved MIME type (explicit value, UI default, or None)\n    \"\"\"\n    if explicit_mime_type is not None:\n        return explicit_mime_type\n    # Case-insensitive scheme check per RFC 3986\n    if uri.lower().startswith(\"ui://\"):\n        return UI_MIME_TYPE\n    return None\n"
  },
  {
    "path": "src/fastmcp/server/auth/__init__.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom .auth import (\n    OAuthProvider,\n    TokenVerifier,\n    RemoteAuthProvider,\n    MultiAuth,\n    AccessToken,\n    AuthProvider,\n)\nfrom .authorization import (\n    AuthCheck,\n    AuthContext,\n    require_scopes,\n    restrict_tag,\n    run_auth_checks,\n)\n\nif TYPE_CHECKING:\n    from .oauth_proxy import OAuthProxy as OAuthProxy\n    from .oidc_proxy import OIDCProxy as OIDCProxy\n    from .providers.debug import DebugTokenVerifier as DebugTokenVerifier\n    from .providers.jwt import JWTVerifier as JWTVerifier\n    from .providers.jwt import StaticTokenVerifier as StaticTokenVerifier\n\n\n# --- Lazy imports for performance (see #3292) ---\n# These providers pull in heavy deps (authlib, cryptography, key_value.aio,\n# beartype) that most users never need. Keeping them behind __getattr__\n# avoids ~150ms+ of import overhead for the common server-only case.\n# Do not convert these back to top-level imports.\n\n\ndef __getattr__(name: str) -> object:\n    if name == \"DebugTokenVerifier\":\n        from .providers.debug import DebugTokenVerifier\n\n        return DebugTokenVerifier\n    if name == \"JWTVerifier\":\n        from .providers.jwt import JWTVerifier\n\n        return JWTVerifier\n    if name == \"StaticTokenVerifier\":\n        from .providers.jwt import StaticTokenVerifier\n\n        return StaticTokenVerifier\n    if name == \"OAuthProxy\":\n        from .oauth_proxy import OAuthProxy\n\n        return OAuthProxy\n    if name == \"OIDCProxy\":\n        from .oidc_proxy import OIDCProxy\n\n        return OIDCProxy\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n\n\n__all__ = [\n    \"AccessToken\",\n    \"AuthCheck\",\n    \"AuthContext\",\n    \"AuthProvider\",\n    \"DebugTokenVerifier\",\n    \"JWTVerifier\",\n    \"MultiAuth\",\n    \"OAuthProvider\",\n    \"OAuthProxy\",\n    \"OIDCProxy\",\n    \"RemoteAuthProvider\",\n    \"StaticTokenVerifier\",\n    \"TokenVerifier\",\n    \"require_scopes\",\n    \"restrict_tag\",\n    \"run_auth_checks\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/auth/auth.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom typing import TYPE_CHECKING, Any, cast\nfrom urllib.parse import urlparse\n\nfrom mcp.server.auth.handlers.token import TokenErrorResponse\nfrom mcp.server.auth.handlers.token import TokenHandler as _SDKTokenHandler\nfrom mcp.server.auth.json_response import PydanticJSONResponse\nfrom mcp.server.auth.middleware.auth_context import AuthContextMiddleware\nfrom mcp.server.auth.middleware.bearer_auth import BearerAuthBackend\nfrom mcp.server.auth.middleware.client_auth import (\n    AuthenticationError,\n    ClientAuthenticator,\n)\nfrom mcp.server.auth.middleware.client_auth import (\n    ClientAuthenticator as _SDKClientAuthenticator,\n)\nfrom mcp.server.auth.provider import (\n    AccessToken as _SDKAccessToken,\n)\nfrom mcp.server.auth.provider import (\n    AuthorizationCode,\n    OAuthAuthorizationServerProvider,\n    RefreshToken,\n)\nfrom mcp.server.auth.provider import (\n    TokenVerifier as TokenVerifierProtocol,\n)\nfrom mcp.server.auth.routes import (\n    cors_middleware,\n    create_auth_routes,\n    create_protected_resource_routes,\n)\nfrom mcp.server.auth.settings import (\n    ClientRegistrationOptions,\n    RevocationOptions,\n)\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyHttpUrl, Field\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.authentication import AuthenticationMiddleware\nfrom starlette.requests import Request\nfrom starlette.routing import Route\n\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from fastmcp.server.auth.cimd import CIMDClientManager\n\nlogger = get_logger(__name__)\n\n\nclass AccessToken(_SDKAccessToken):\n    \"\"\"AccessToken that includes all JWT claims.\"\"\"\n\n    claims: dict[str, Any] = Field(default_factory=dict)\n\n\nclass TokenHandler(_SDKTokenHandler):\n    \"\"\"TokenHandler that returns MCP-compliant error responses.\n\n    This handler addresses two SDK issues:\n\n    1. Error code: The SDK returns `unauthorized_client` for client authentication\n       failures, but RFC 6749 Section 5.2 requires `invalid_client` with HTTP 401.\n       This distinction matters for client re-registration behavior.\n\n    2. Status code: The SDK returns HTTP 400 for all token errors including\n       `invalid_grant` (expired/invalid tokens). However, the MCP spec requires:\n       \"Invalid or expired tokens MUST receive a HTTP 401 response.\"\n\n    This handler transforms responses to be compliant with both OAuth 2.1 and MCP specs.\n    \"\"\"\n\n    async def handle(self, request: Any):\n        \"\"\"Wrap SDK handle() and transform auth error responses.\"\"\"\n        response = await super().handle(request)\n\n        # Transform 401 unauthorized_client -> invalid_client\n        if response.status_code == 401:\n            try:\n                body = json.loads(response.body)\n                if body.get(\"error\") == \"unauthorized_client\":\n                    return PydanticJSONResponse(\n                        content=TokenErrorResponse(\n                            error=\"invalid_client\",\n                            error_description=body.get(\"error_description\"),\n                        ),\n                        status_code=401,\n                        headers={\n                            \"Cache-Control\": \"no-store\",\n                            \"Pragma\": \"no-cache\",\n                        },\n                    )\n            except (json.JSONDecodeError, AttributeError):\n                pass  # Not JSON or unexpected format, return as-is\n\n        # Transform 400 invalid_grant -> 401 for expired/invalid tokens\n        # Per MCP spec: \"Invalid or expired tokens MUST receive a HTTP 401 response.\"\n        if response.status_code == 400:\n            try:\n                body = json.loads(response.body)\n                if body.get(\"error\") == \"invalid_grant\":\n                    return PydanticJSONResponse(\n                        content=TokenErrorResponse(\n                            error=\"invalid_grant\",\n                            error_description=body.get(\"error_description\"),\n                        ),\n                        status_code=401,\n                        headers={\n                            \"Cache-Control\": \"no-store\",\n                            \"Pragma\": \"no-cache\",\n                        },\n                    )\n            except (json.JSONDecodeError, AttributeError):\n                pass  # Not JSON or unexpected format, return as-is\n\n        return response\n\n\n# Expected assertion type for private_key_jwt\nJWT_BEARER_ASSERTION_TYPE = \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\"\n\n\nclass PrivateKeyJWTClientAuthenticator(_SDKClientAuthenticator):\n    \"\"\"Client authenticator with private_key_jwt support for CIMD clients.\n\n    Extends the SDK's ClientAuthenticator to add support for the `private_key_jwt`\n    authentication method per RFC 7523. This is required for CIMD (Client ID Metadata\n    Document) clients that use asymmetric keys for authentication.\n\n    The authenticator:\n    1. Delegates to SDK for standard methods (client_secret_basic, client_secret_post, none)\n    2. Adds private_key_jwt handling for CIMD clients\n    3. Validates JWT assertions against client's JWKS\n    \"\"\"\n\n    def __init__(\n        self,\n        provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        cimd_manager: CIMDClientManager,\n        token_endpoint_url: str,\n    ):\n        \"\"\"Initialize the authenticator.\n\n        Args:\n            provider: OAuth provider for client lookups\n            cimd_manager: CIMD manager for private_key_jwt validation\n            token_endpoint_url: Token endpoint URL for audience validation\n        \"\"\"\n        super().__init__(provider)\n        self._cimd_manager = cimd_manager\n        self._token_endpoint_url = token_endpoint_url\n\n    async def authenticate_request(\n        self, request: Request\n    ) -> OAuthClientInformationFull:\n        \"\"\"Authenticate a client from an HTTP request.\n\n        Extends SDK authentication to support private_key_jwt for CIMD clients.\n        Delegates to SDK for client_secret_basic (Authorization header) and\n        client_secret_post (form body) authentication.\n        \"\"\"\n        form_data = await request.form()\n        client_id = form_data.get(\"client_id\")\n\n        # If client_id is not in form data, delegate to SDK\n        # This handles client_secret_basic which sends credentials in Authorization header\n        if not client_id:\n            return await super().authenticate_request(request)\n\n        client = await self.provider.get_client(str(client_id))\n        if not client:\n            raise AuthenticationError(\"Invalid client_id\")\n\n        # Handle private_key_jwt authentication for CIMD clients\n        if client.token_endpoint_auth_method == \"private_key_jwt\":\n            # Validate assertion parameters\n            assertion_type = form_data.get(\"client_assertion_type\")\n            assertion = form_data.get(\"client_assertion\")\n\n            if assertion_type != JWT_BEARER_ASSERTION_TYPE:\n                raise AuthenticationError(\n                    f\"Invalid client_assertion_type: expected {JWT_BEARER_ASSERTION_TYPE}\"\n                )\n\n            if not assertion or not isinstance(assertion, str):\n                raise AuthenticationError(\"Missing client_assertion\")\n\n            # Validate the JWT assertion using CIMD manager\n            try:\n                await self._cimd_manager.validate_private_key_jwt(\n                    assertion=assertion,\n                    client=client,\n                    token_endpoint=self._token_endpoint_url,\n                )\n            except ValueError as e:\n                raise AuthenticationError(f\"Invalid client assertion: {e}\") from e\n\n            return client\n\n        # Delegate to SDK for other authentication methods\n        return await super().authenticate_request(request)\n\n\nclass AuthProvider(TokenVerifierProtocol):\n    \"\"\"Base class for all FastMCP authentication providers.\n\n    This class provides a unified interface for all authentication providers,\n    whether they are simple token verifiers or full OAuth authorization servers.\n    All providers must be able to verify tokens and can optionally provide\n    custom authentication routes.\n    \"\"\"\n\n    def __init__(\n        self,\n        base_url: AnyHttpUrl | str | None = None,\n        required_scopes: list[str] | None = None,\n    ):\n        \"\"\"\n        Initialize the auth provider.\n\n        Args:\n            base_url: The base URL of this server (e.g., http://localhost:8000).\n                This is used for constructing .well-known endpoints and OAuth metadata.\n            required_scopes: List of OAuth scopes required for all requests.\n        \"\"\"\n        if isinstance(base_url, str):\n            base_url = AnyHttpUrl(base_url)\n        self.base_url = base_url\n        self.required_scopes = required_scopes or []\n        self._mcp_path: str | None = None\n        self._resource_url: AnyHttpUrl | None = None\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify a bearer token and return access info if valid.\n\n        All auth providers must implement token verification.\n\n        Args:\n            token: The token string to validate\n\n        Returns:\n            AccessToken object if valid, None if invalid or expired\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement verify_token\")\n\n    def set_mcp_path(self, mcp_path: str | None) -> None:\n        \"\"\"Set the MCP endpoint path and compute resource URL.\n\n        This method is called by get_routes() to configure the expected\n        resource URL before route creation. Subclasses can override to\n        perform additional initialization that depends on knowing the\n        MCP endpoint path.\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n        \"\"\"\n        self._mcp_path = mcp_path\n        self._resource_url = self._get_resource_url(mcp_path)\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get all routes for this authentication provider.\n\n        This includes both well-known discovery routes and operational routes.\n        Each provider is responsible for creating whatever routes it needs:\n        - TokenVerifier: typically no routes (default implementation)\n        - RemoteAuthProvider: protected resource metadata routes\n        - OAuthProvider: full OAuth authorization server routes\n        - Custom providers: whatever routes they need\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata, but the\n                provider does not create the actual MCP endpoint route.\n\n        Returns:\n            List of all routes for this provider (excluding the MCP endpoint itself)\n        \"\"\"\n        return []\n\n    def get_well_known_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get well-known discovery routes for this authentication provider.\n\n        This is a utility method that filters get_routes() to return only\n        well-known discovery routes (those starting with /.well-known/).\n\n        Well-known routes provide OAuth metadata and discovery endpoints that\n        clients use to discover authentication capabilities. These routes should\n        be mounted at the root level of the application to comply with RFC 8414\n        and RFC 9728.\n\n        Common well-known routes:\n        - /.well-known/oauth-authorization-server (authorization server metadata)\n        - /.well-known/oauth-protected-resource/* (protected resource metadata)\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to construct path-scoped well-known URLs.\n\n        Returns:\n            List of well-known discovery routes (typically mounted at root level)\n        \"\"\"\n        all_routes = self.get_routes(mcp_path)\n        return [\n            route\n            for route in all_routes\n            if isinstance(route, Route) and route.path.startswith(\"/.well-known/\")\n        ]\n\n    def get_middleware(self) -> list:\n        \"\"\"Get HTTP application-level middleware for this auth provider.\n\n        Returns:\n            List of Starlette Middleware instances to apply to the HTTP app\n        \"\"\"\n        # TODO(ty): remove type ignores when ty supports Starlette Middleware typing\n        return [\n            Middleware(\n                AuthenticationMiddleware,  # type: ignore[arg-type]\n                backend=BearerAuthBackend(self),\n            ),\n            Middleware(AuthContextMiddleware),  # type: ignore[arg-type]\n        ]\n\n    def _get_resource_url(self, path: str | None = None) -> AnyHttpUrl | None:\n        \"\"\"Get the actual resource URL being protected.\n\n        Args:\n            path: The path where the resource endpoint is mounted (e.g., \"/mcp\")\n\n        Returns:\n            The full URL of the protected resource\n        \"\"\"\n        if self.base_url is None:\n            return None\n\n        if path:\n            prefix = str(self.base_url).rstrip(\"/\")\n            suffix = path.lstrip(\"/\")\n            return AnyHttpUrl(f\"{prefix}/{suffix}\")\n        return self.base_url\n\n\nclass TokenVerifier(AuthProvider):\n    \"\"\"Base class for token verifiers (Resource Servers).\n\n    This class provides token verification capability without OAuth server functionality.\n    Token verifiers typically don't provide authentication routes by default.\n    \"\"\"\n\n    def __init__(\n        self,\n        base_url: AnyHttpUrl | str | None = None,\n        required_scopes: list[str] | None = None,\n    ):\n        \"\"\"\n        Initialize the token verifier.\n\n        Args:\n            base_url: The base URL of this server\n            required_scopes: Scopes that are required for all requests\n        \"\"\"\n        super().__init__(base_url=base_url, required_scopes=required_scopes)\n\n    @property\n    def scopes_supported(self) -> list[str]:\n        \"\"\"Scopes to advertise in OAuth metadata.\n\n        Defaults to required_scopes. Override in subclasses when the\n        advertised scopes differ from the validation scopes (e.g., Azure AD\n        where tokens contain short-form scopes but clients request full URI\n        scopes).\n        \"\"\"\n        return self.required_scopes or []\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify a bearer token and return access info if valid.\"\"\"\n        raise NotImplementedError(\"Subclasses must implement verify_token\")\n\n\nclass RemoteAuthProvider(AuthProvider):\n    \"\"\"Authentication provider for resource servers that verify tokens from known authorization servers.\n\n    This provider composes a TokenVerifier with authorization server metadata to create\n    standardized OAuth 2.0 Protected Resource endpoints (RFC 9728). Perfect for:\n    - JWT verification with known issuers\n    - Remote token introspection services\n    - Any resource server that knows where its tokens come from\n\n    Use this when you have token verification logic and want to advertise\n    the authorization servers that issue valid tokens.\n    \"\"\"\n\n    base_url: AnyHttpUrl\n\n    def __init__(\n        self,\n        token_verifier: TokenVerifier,\n        authorization_servers: list[AnyHttpUrl],\n        base_url: AnyHttpUrl | str,\n        scopes_supported: list[str] | None = None,\n        resource_name: str | None = None,\n        resource_documentation: AnyHttpUrl | None = None,\n    ):\n        \"\"\"Initialize the remote auth provider.\n\n        Args:\n            token_verifier: TokenVerifier instance for token validation\n            authorization_servers: List of authorization servers that issue valid tokens\n            base_url: The base URL of this server\n            scopes_supported: Scopes to advertise in OAuth metadata. If None,\n                uses the token verifier's scopes_supported property. Use this\n                when the scopes clients request differ from the scopes that\n                appear in tokens (e.g., Azure AD full URI scopes vs short-form).\n            resource_name: Optional name for the protected resource\n            resource_documentation: Optional documentation URL for the protected resource\n        \"\"\"\n        super().__init__(\n            base_url=base_url,\n            required_scopes=token_verifier.required_scopes,\n        )\n        self.token_verifier = token_verifier\n        self.authorization_servers = authorization_servers\n        self._scopes_supported = scopes_supported\n        self.resource_name = resource_name\n        self.resource_documentation = resource_documentation\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token using the configured token verifier.\"\"\"\n        return await self.token_verifier.verify_token(token)\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get routes for this provider.\n\n        Creates protected resource metadata routes (RFC 9728).\n        \"\"\"\n        routes = []\n\n        # Get the resource URL based on the MCP path\n        resource_url = self._get_resource_url(mcp_path)\n\n        if resource_url:\n            # Add protected resource metadata routes\n            routes.extend(\n                create_protected_resource_routes(\n                    resource_url=resource_url,\n                    authorization_servers=self.authorization_servers,\n                    scopes_supported=(\n                        self._scopes_supported\n                        if self._scopes_supported is not None\n                        else self.token_verifier.scopes_supported\n                    ),\n                    resource_name=self.resource_name,\n                    resource_documentation=self.resource_documentation,\n                )\n            )\n\n        return routes\n\n\nclass MultiAuth(AuthProvider):\n    \"\"\"Composes an optional auth server with additional token verifiers.\n\n    Use this when a single server needs to accept tokens from multiple sources.\n    For example, an OAuth proxy for interactive clients combined with a JWT\n    verifier for machine-to-machine tokens.\n\n    Token verification tries the server first (if present), then each verifier\n    in order, returning the first successful result. Routes and OAuth metadata\n    come from the server; verifiers contribute only token verification.\n\n    Example:\n        ```python\n        from fastmcp.server.auth import MultiAuth, JWTVerifier, OAuthProxy\n\n        auth = MultiAuth(\n            server=OAuthProxy(issuer_url=\"https://login.example.com/...\"),\n            verifiers=[JWTVerifier(jwks_uri=\"https://example.com/.well-known/jwks.json\")],\n        )\n        mcp = FastMCP(\"my-server\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        server: AuthProvider | None = None,\n        verifiers: list[TokenVerifier] | TokenVerifier | None = None,\n        base_url: AnyHttpUrl | str | None = None,\n        required_scopes: list[str] | None = None,\n    ):\n        \"\"\"Initialize the multi-auth provider.\n\n        Args:\n            server: Optional auth provider (e.g., OAuthProxy) that owns routes\n                and OAuth metadata. Also participates in token verification as\n                the first verifier tried.\n            verifiers: One or more token verifiers to try after the server.\n            base_url: Override the base URL. Defaults to the server's base_url.\n            required_scopes: Override required scopes. Defaults to the server's.\n        \"\"\"\n        if verifiers is None:\n            verifiers = []\n        elif isinstance(verifiers, TokenVerifier):\n            verifiers = [verifiers]\n\n        if server is None and not verifiers:\n            raise ValueError(\"MultiAuth requires at least a server or one verifier\")\n\n        effective_base_url = base_url or (server.base_url if server else None)\n        effective_scopes = (\n            required_scopes\n            if required_scopes is not None\n            else (server.required_scopes if server else None)\n        )\n\n        super().__init__(base_url=effective_base_url, required_scopes=effective_scopes)\n        self.server = server\n        self.verifiers = list(verifiers)\n\n        self._sources: list[AuthProvider] = []\n        if self.server is not None:\n            self._sources.append(self.server)\n        self._sources.extend(self.verifiers)\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify a token by trying the server, then each verifier in order.\n\n        Each source is tried independently. If a source raises an exception,\n        it is logged and treated as a non-match so that remaining sources\n        still get a chance to verify the token.\n        \"\"\"\n        for source in self._sources:\n            try:\n                result = await source.verify_token(token)\n                if result is not None:\n                    return result\n            except Exception:\n                logger.debug(\n                    \"Token verification failed for %s, trying next source\",\n                    type(source).__name__,\n                    exc_info=True,\n                )\n\n        return None\n\n    def set_mcp_path(self, mcp_path: str | None) -> None:\n        \"\"\"Propagate MCP path to the server and all verifiers.\"\"\"\n        super().set_mcp_path(mcp_path)\n        if self.server is not None:\n            self.server.set_mcp_path(mcp_path)\n        for verifier in self.verifiers:\n            verifier.set_mcp_path(mcp_path)\n\n    def get_routes(self, mcp_path: str | None = None) -> list[Route]:\n        \"\"\"Delegate route creation to the server.\"\"\"\n        if self.server is not None:\n            return self.server.get_routes(mcp_path)\n        return []\n\n    def get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]:\n        \"\"\"Delegate well-known route creation to the server.\n\n        This ensures that server-specific well-known route logic (e.g.,\n        OAuthProvider's RFC 8414 path-aware discovery) is preserved.\n        \"\"\"\n        if self.server is not None:\n            return self.server.get_well_known_routes(mcp_path)\n        return []\n\n\nclass OAuthProvider(\n    AuthProvider,\n    OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken],\n):\n    \"\"\"OAuth Authorization Server provider.\n\n    This class provides full OAuth server functionality including client registration,\n    authorization flows, token issuance, and token verification.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        service_documentation_url: AnyHttpUrl | str | None = None,\n        client_registration_options: ClientRegistrationOptions | None = None,\n        revocation_options: RevocationOptions | None = None,\n        required_scopes: list[str] | None = None,\n    ):\n        \"\"\"\n        Initialize the OAuth provider.\n\n        Args:\n            base_url: The public URL of this FastMCP server\n            issuer_url: The issuer URL for OAuth metadata (defaults to base_url)\n            service_documentation_url: The URL of the service documentation.\n            client_registration_options: The client registration options.\n            revocation_options: The revocation options.\n            required_scopes: Scopes that are required for all requests.\n        \"\"\"\n\n        super().__init__(base_url=base_url, required_scopes=required_scopes)\n\n        if issuer_url is None:\n            self.issuer_url = self.base_url\n        elif isinstance(issuer_url, str):\n            self.issuer_url = AnyHttpUrl(issuer_url)\n        else:\n            self.issuer_url = issuer_url\n\n        # Log if issuer_url and base_url differ (requires additional setup)\n        if (\n            self.base_url is not None\n            and self.issuer_url is not None\n            and str(self.base_url) != str(self.issuer_url)\n        ):\n            logger.info(\n                f\"OAuth endpoints at {self.base_url}, issuer at {self.issuer_url}. \"\n                f\"Ensure well-known routes are accessible at root ({self.issuer_url}/.well-known/). \"\n                f\"See: https://gofastmcp.com/deployment/http#mounting-authenticated-servers\"\n            )\n\n        # Initialize OAuth Authorization Server Provider\n        OAuthAuthorizationServerProvider.__init__(self)\n\n        if isinstance(service_documentation_url, str):\n            service_documentation_url = AnyHttpUrl(service_documentation_url)\n\n        self.service_documentation_url = service_documentation_url\n        self.client_registration_options = client_registration_options\n        self.revocation_options = revocation_options\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"\n        Verify a bearer token and return access info if valid.\n\n        This method implements the TokenVerifier protocol by delegating\n        to our existing load_access_token method.\n\n        Args:\n            token: The token string to validate\n\n        Returns:\n            AccessToken object if valid, None if invalid or expired\n        \"\"\"\n        return await self.load_access_token(token)\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get OAuth authorization server routes and optional protected resource routes.\n\n        This method creates the full set of OAuth routes including:\n        - Standard OAuth authorization server routes (/.well-known/oauth-authorization-server, /authorize, /token, etc.)\n        - Optional protected resource routes\n\n        Returns:\n            List of OAuth routes\n        \"\"\"\n        # Configure resource URL before creating routes\n        self.set_mcp_path(mcp_path)\n\n        # Create standard OAuth authorization server routes\n        # Pass base_url as issuer_url to ensure metadata declares endpoints where\n        # they're actually accessible (operational routes are mounted at\n        # base_url)\n        assert self.base_url is not None  # typing check\n        assert (\n            self.issuer_url is not None\n        )  # typing check (issuer_url defaults to base_url)\n\n        sdk_routes = create_auth_routes(\n            provider=self,\n            issuer_url=self.base_url,\n            service_documentation_url=self.service_documentation_url,\n            client_registration_options=self.client_registration_options,\n            revocation_options=self.revocation_options,\n        )\n\n        # Replace the token endpoint with our custom handler that returns\n        # proper OAuth 2.1 error codes (invalid_client instead of unauthorized_client)\n        oauth_routes: list[Route] = []\n        for route in sdk_routes:\n            if (\n                isinstance(route, Route)\n                and route.path == \"/token\"\n                and route.methods is not None\n                and \"POST\" in route.methods\n            ):\n                # Replace with our OAuth 2.1 compliant token handler\n                token_handler = TokenHandler(\n                    provider=self, client_authenticator=ClientAuthenticator(self)\n                )\n                oauth_routes.append(\n                    Route(\n                        path=\"/token\",\n                        endpoint=cors_middleware(\n                            token_handler.handle, [\"POST\", \"OPTIONS\"]\n                        ),\n                        methods=[\"POST\", \"OPTIONS\"],\n                    )\n                )\n            else:\n                oauth_routes.append(route)\n\n        # Add protected resource routes if this server is also acting as a resource server\n        if self._resource_url:\n            supported_scopes = (\n                self.client_registration_options.valid_scopes\n                if self.client_registration_options\n                and self.client_registration_options.valid_scopes\n                else self.required_scopes\n            )\n            protected_routes = create_protected_resource_routes(\n                resource_url=self._resource_url,\n                authorization_servers=[cast(AnyHttpUrl, self.issuer_url)],\n                scopes_supported=supported_scopes,\n            )\n            oauth_routes.extend(protected_routes)\n\n        # Add base routes\n        oauth_routes.extend(super().get_routes(mcp_path))\n\n        return oauth_routes\n\n    def get_well_known_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get well-known discovery routes with RFC 8414 path-aware support.\n\n        Overrides the base implementation to support path-aware authorization\n        server metadata discovery per RFC 8414. If issuer_url has a path component,\n        the authorization server metadata route is adjusted to include that path.\n\n        For example, if issuer_url is \"http://example.com/api\", the discovery\n        endpoint will be at \"/.well-known/oauth-authorization-server/api\" instead\n        of just \"/.well-known/oauth-authorization-server\".\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n\n        Returns:\n            List of well-known discovery routes\n        \"\"\"\n        routes = super().get_well_known_routes(mcp_path)\n\n        # RFC 8414: If issuer_url has a path, use path-aware discovery\n        if self.issuer_url:\n            parsed = urlparse(str(self.issuer_url))\n            issuer_path = parsed.path.rstrip(\"/\")\n\n            if issuer_path and issuer_path != \"/\":\n                # Replace /.well-known/oauth-authorization-server with path-aware version\n                new_routes = []\n                for route in routes:\n                    if route.path == \"/.well-known/oauth-authorization-server\":\n                        new_path = (\n                            f\"/.well-known/oauth-authorization-server{issuer_path}\"\n                        )\n                        new_routes.append(\n                            Route(\n                                new_path,\n                                endpoint=route.endpoint,\n                                methods=route.methods,\n                            )\n                        )\n                    else:\n                        new_routes.append(route)\n                return new_routes\n\n        return routes\n"
  },
  {
    "path": "src/fastmcp/server/auth/authorization.py",
    "content": "\"\"\"Authorization checks for FastMCP components.\n\nThis module provides callable-based authorization for tools, resources, and prompts.\nAuth checks are functions that receive an AuthContext and return True to allow access\nor False to deny.\n\nAuth checks can also raise exceptions:\n- AuthorizationError: Propagates with the custom message for explicit denial\n- Other exceptions: Masked for security (logged, treated as auth failure)\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth import require_scopes\n\n    mcp = FastMCP()\n\n    @mcp.tool(auth=require_scopes(\"write\"))\n    def protected_tool(): ...\n\n    @mcp.resource(\"data://secret\", auth=require_scopes(\"read\"))\n    def secret_data(): ...\n\n    @mcp.prompt(auth=require_scopes(\"admin\"))\n    def admin_prompt(): ...\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport logging\nfrom collections.abc import Awaitable, Callable\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, cast\n\nfrom fastmcp.exceptions import AuthorizationError\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from fastmcp.server.auth import AccessToken\n    from fastmcp.tools.base import Tool\n    from fastmcp.utilities.components import FastMCPComponent\n\n\n@dataclass\nclass AuthContext:\n    \"\"\"Context passed to auth check callables.\n\n    This object is passed to each auth check function and provides\n    access to the current authentication token and the component being accessed.\n\n    Attributes:\n        token: The current access token, or None if unauthenticated.\n        component: The component (tool, resource, or prompt) being accessed.\n        tool: Backwards-compatible alias for component when it's a Tool.\n    \"\"\"\n\n    token: AccessToken | None\n    component: FastMCPComponent\n\n    @property\n    def tool(self) -> Tool | None:\n        \"\"\"Backwards-compatible access to the component as a Tool.\n\n        Returns the component if it's a Tool, None otherwise.\n        \"\"\"\n        from fastmcp.tools.base import Tool\n\n        return self.component if isinstance(self.component, Tool) else None\n\n\n# Type alias for auth check functions (sync or async)\nAuthCheck = Callable[[AuthContext], bool] | Callable[[AuthContext], Awaitable[bool]]\n\n\ndef require_scopes(*scopes: str) -> AuthCheck:\n    \"\"\"Require specific OAuth scopes.\n\n    Returns an auth check that requires ALL specified scopes to be present\n    in the token (AND logic).\n\n    Args:\n        *scopes: One or more scope strings that must all be present.\n\n    Example:\n        ```python\n        @mcp.tool(auth=require_scopes(\"admin\"))\n        def admin_tool(): ...\n\n        @mcp.tool(auth=require_scopes(\"read\", \"write\"))\n        def read_write_tool(): ...\n        ```\n    \"\"\"\n    required = set(scopes)\n\n    def check(ctx: AuthContext) -> bool:\n        if ctx.token is None:\n            return False\n        return required.issubset(set(ctx.token.scopes))\n\n    return check\n\n\ndef restrict_tag(tag: str, *, scopes: list[str]) -> AuthCheck:\n    \"\"\"Restrict components with a specific tag to require certain scopes.\n\n    If the component has the specified tag, the token must have ALL the\n    required scopes. If the component doesn't have the tag, access is allowed.\n\n    Args:\n        tag: The tag that triggers the scope requirement.\n        scopes: List of scopes required when the tag is present.\n\n    Example:\n        ```python\n        # Components tagged \"admin\" require the \"admin\" scope\n        AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))\n        ```\n    \"\"\"\n    required = set(scopes)\n\n    def check(ctx: AuthContext) -> bool:\n        if tag not in ctx.component.tags:\n            return True  # Tag not present, no restriction\n        if ctx.token is None:\n            return False\n        return required.issubset(set(ctx.token.scopes))\n\n    return check\n\n\nasync def run_auth_checks(\n    checks: AuthCheck | list[AuthCheck],\n    ctx: AuthContext,\n) -> bool:\n    \"\"\"Run auth checks with AND logic.\n\n    All checks must pass for authorization to succeed. Checks can be\n    synchronous or asynchronous functions.\n\n    Auth checks can:\n    - Return True to allow access\n    - Return False to deny access\n    - Raise AuthorizationError to deny with a custom message (propagates)\n    - Raise other exceptions (masked for security, treated as denial)\n\n    Args:\n        checks: A single check function or list of check functions.\n            Each check can be sync (returns bool) or async (returns Awaitable[bool]).\n        ctx: The auth context to pass to each check.\n\n    Returns:\n        True if all checks pass, False if any check fails.\n\n    Raises:\n        AuthorizationError: If an auth check explicitly raises it.\n    \"\"\"\n    check_list = [checks] if not isinstance(checks, list) else checks\n    check_list = cast(list[AuthCheck], check_list)\n\n    for check in check_list:\n        try:\n            result = check(ctx)\n            if inspect.isawaitable(result):\n                result = await result\n            if not result:\n                return False\n        except AuthorizationError:\n            # Let AuthorizationError propagate with its custom message\n            raise\n        except Exception:\n            # Mask other exceptions for security - log and treat as auth failure\n            logger.warning(\n                f\"Auth check {getattr(check, '__name__', repr(check))} \"\n                \"raised an unexpected exception\",\n                exc_info=True,\n            )\n            return False\n\n    return True\n"
  },
  {
    "path": "src/fastmcp/server/auth/cimd.py",
    "content": "\"\"\"CIMD (Client ID Metadata Document) support for FastMCP.\n\n.. warning::\n    **Beta Feature**: CIMD support is currently in beta. The API may change\n    in future releases. Please report any issues you encounter.\n\nCIMD is a simpler alternative to Dynamic Client Registration where clients\nhost a static JSON document at an HTTPS URL, and that URL becomes their\nclient_id. See the IETF draft: draft-parecki-oauth-client-id-metadata-document\n\nThis module provides:\n- CIMDDocument: Pydantic model for CIMD document validation\n- CIMDFetcher: Fetch and validate CIMD documents with SSRF protection\n- CIMDClientManager: Manages CIMD client operations\n\"\"\"\n\nfrom __future__ import annotations\n\nimport fnmatch\nimport json\nimport time\nfrom collections.abc import Mapping\nfrom dataclasses import dataclass\nfrom datetime import timezone\nfrom email.utils import parsedate_to_datetime\nfrom typing import TYPE_CHECKING, Any, Literal\nfrom urllib.parse import urlparse\n\nfrom pydantic import AnyHttpUrl, BaseModel, Field, field_validator\n\nfrom fastmcp.server.auth.ssrf import (\n    SSRFError,\n    SSRFFetchError,\n    ssrf_safe_fetch_response,\n    validate_url,\n)\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from fastmcp.server.auth.providers.jwt import JWTVerifier\n\nlogger = get_logger(__name__)\n\n\nclass CIMDDocument(BaseModel):\n    \"\"\"CIMD document per draft-parecki-oauth-client-id-metadata-document.\n\n    The client metadata document is a JSON document containing OAuth client\n    metadata. The client_id property MUST match the URL where this document\n    is hosted.\n\n    Key constraint: token_endpoint_auth_method MUST NOT use shared secrets\n    (client_secret_post, client_secret_basic, client_secret_jwt).\n\n    redirect_uris is required and must contain at least one entry.\n    \"\"\"\n\n    client_id: AnyHttpUrl = Field(\n        ...,\n        description=\"Must match the URL where this document is hosted\",\n    )\n    client_name: str | None = Field(\n        default=None,\n        description=\"Human-readable name of the client\",\n    )\n    client_uri: AnyHttpUrl | None = Field(\n        default=None,\n        description=\"URL of the client's home page\",\n    )\n    logo_uri: AnyHttpUrl | None = Field(\n        default=None,\n        description=\"URL of the client's logo image\",\n    )\n    redirect_uris: list[str] = Field(\n        ...,\n        description=\"Array of allowed redirect URIs (may include wildcards like http://localhost:*/callback)\",\n    )\n    token_endpoint_auth_method: Literal[\"none\", \"private_key_jwt\"] = Field(\n        default=\"none\",\n        description=\"Authentication method for token endpoint (no shared secrets allowed)\",\n    )\n    grant_types: list[str] = Field(\n        default_factory=lambda: [\"authorization_code\"],\n        description=\"OAuth grant types the client will use\",\n    )\n    response_types: list[str] = Field(\n        default_factory=lambda: [\"code\"],\n        description=\"OAuth response types the client will use\",\n    )\n    scope: str | None = Field(\n        default=None,\n        description=\"Space-separated list of scopes the client may request\",\n    )\n    contacts: list[str] | None = Field(\n        default=None,\n        description=\"Contact information for the client developer\",\n    )\n    tos_uri: AnyHttpUrl | None = Field(\n        default=None,\n        description=\"URL of the client's terms of service\",\n    )\n    policy_uri: AnyHttpUrl | None = Field(\n        default=None,\n        description=\"URL of the client's privacy policy\",\n    )\n    jwks_uri: AnyHttpUrl | None = Field(\n        default=None,\n        description=\"URL of the client's JSON Web Key Set (for private_key_jwt)\",\n    )\n    jwks: dict[str, Any] | None = Field(\n        default=None,\n        description=\"Client's JSON Web Key Set (for private_key_jwt)\",\n    )\n    software_id: str | None = Field(\n        default=None,\n        description=\"Unique identifier for the client software\",\n    )\n    software_version: str | None = Field(\n        default=None,\n        description=\"Version of the client software\",\n    )\n\n    @field_validator(\"token_endpoint_auth_method\")\n    @classmethod\n    def validate_auth_method(cls, v: str) -> str:\n        \"\"\"Ensure no shared-secret auth methods are used.\"\"\"\n        forbidden = {\"client_secret_post\", \"client_secret_basic\", \"client_secret_jwt\"}\n        if v in forbidden:\n            raise ValueError(\n                f\"CIMD documents cannot use shared-secret auth methods: {v}. \"\n                \"Use 'none' or 'private_key_jwt' instead.\"\n            )\n        return v\n\n    @field_validator(\"redirect_uris\")\n    @classmethod\n    def validate_redirect_uris(cls, v: list[str]) -> list[str]:\n        \"\"\"Ensure redirect_uris is non-empty and each entry is a valid URI.\"\"\"\n        if not v:\n            raise ValueError(\"CIMD documents must include at least one redirect_uri\")\n        for uri in v:\n            if not uri or not uri.strip():\n                raise ValueError(\"CIMD redirect_uris must be non-empty strings\")\n            parsed = urlparse(uri)\n            if not parsed.scheme:\n                raise ValueError(\n                    f\"CIMD redirect_uri must have a scheme (e.g. http:// or https://): {uri!r}\"\n                )\n            if not parsed.netloc and not uri.startswith(\"urn:\"):\n                raise ValueError(f\"CIMD redirect_uri must have a host: {uri!r}\")\n        return v\n\n\nclass CIMDValidationError(Exception):\n    \"\"\"Raised when CIMD document validation fails.\"\"\"\n\n\nclass CIMDFetchError(Exception):\n    \"\"\"Raised when CIMD document fetching fails.\"\"\"\n\n\n@dataclass\nclass _CIMDCacheEntry:\n    \"\"\"Cached CIMD document and associated HTTP cache metadata.\"\"\"\n\n    doc: CIMDDocument\n    etag: str | None\n    last_modified: str | None\n    expires_at: float\n    freshness_lifetime: float\n    must_revalidate: bool\n\n\n@dataclass\nclass _CIMDCachePolicy:\n    \"\"\"Normalized cache directives parsed from HTTP response headers.\"\"\"\n\n    etag: str | None\n    last_modified: str | None\n    expires_at: float\n    freshness_lifetime: float\n    no_store: bool\n    must_revalidate: bool\n\n\nclass CIMDFetcher:\n    \"\"\"Fetch and validate CIMD documents with SSRF protection.\n\n    Delegates HTTP fetching to ssrf_safe_fetch_response, which provides DNS\n    pinning, IP validation, size limits, and timeout enforcement. Documents are\n    cached using HTTP caching semantics (Cache-Control/ETag/Last-Modified), with\n    a TTL fallback when response headers do not define caching behavior.\n    \"\"\"\n\n    # Maximum response size (bytes)\n    MAX_RESPONSE_SIZE = 5120  # 5KB\n    # Default cache TTL (seconds)\n    DEFAULT_CACHE_TTL_SECONDS = 3600\n\n    def __init__(\n        self,\n        timeout: float = 10.0,\n    ):\n        \"\"\"Initialize the CIMD fetcher.\n\n        Args:\n            timeout: HTTP request timeout in seconds (default 10.0)\n        \"\"\"\n        self.timeout = timeout\n        self._cache: dict[str, _CIMDCacheEntry] = {}\n\n    def _parse_cache_policy(\n        self, headers: Mapping[str, str], now: float\n    ) -> _CIMDCachePolicy:\n        \"\"\"Parse HTTP cache headers and derive cache behavior.\"\"\"\n        normalized = {k.lower(): v for k, v in headers.items()}\n        cache_control = normalized.get(\"cache-control\", \"\")\n        directives = {\n            part.strip().lower() for part in cache_control.split(\",\") if part.strip()\n        }\n\n        no_store = \"no-store\" in directives\n        must_revalidate = \"no-cache\" in directives\n        max_age: int | None = None\n\n        for directive in directives:\n            if directive.startswith(\"max-age=\"):\n                value = directive.removeprefix(\"max-age=\").strip()\n                try:\n                    max_age = max(0, int(value))\n                except ValueError:\n                    logger.debug(\n                        \"Ignoring invalid Cache-Control max-age value: %s\", value\n                    )\n                break\n\n        expires_at: float | None = None\n        if max_age is not None:\n            expires_at = now + max_age\n        elif \"expires\" in normalized:\n            try:\n                dt = parsedate_to_datetime(normalized[\"expires\"])\n                if dt.tzinfo is None:\n                    dt = dt.replace(tzinfo=timezone.utc)\n                expires_at = dt.timestamp()\n            except (TypeError, ValueError):\n                logger.debug(\n                    \"Ignoring invalid Expires header on CIMD response: %s\",\n                    normalized[\"expires\"],\n                )\n\n        if expires_at is None:\n            expires_at = now + self.DEFAULT_CACHE_TTL_SECONDS\n        freshness_lifetime = max(0.0, expires_at - now)\n\n        return _CIMDCachePolicy(\n            etag=normalized.get(\"etag\"),\n            last_modified=normalized.get(\"last-modified\"),\n            expires_at=expires_at,\n            freshness_lifetime=freshness_lifetime,\n            no_store=no_store,\n            must_revalidate=must_revalidate,\n        )\n\n    def _has_freshness_headers(self, headers: Mapping[str, str]) -> bool:\n        \"\"\"Return True when response includes cache freshness directives.\"\"\"\n        normalized = {k.lower() for k in headers}\n        return \"cache-control\" in normalized or \"expires\" in normalized\n\n    def is_cimd_client_id(self, client_id: str) -> bool:\n        \"\"\"Check if a client_id looks like a CIMD URL.\n\n        CIMD URLs must be HTTPS with a host and non-root path.\n        \"\"\"\n        if not client_id:\n            return False\n        try:\n            parsed = urlparse(client_id)\n            return (\n                parsed.scheme == \"https\"\n                and bool(parsed.netloc)\n                and parsed.path not in (\"\", \"/\")\n            )\n        except (ValueError, AttributeError):\n            return False\n\n    async def fetch(self, client_id_url: str) -> CIMDDocument:\n        \"\"\"Fetch and validate a CIMD document with SSRF protection.\n\n        Uses ssrf_safe_fetch_response for the HTTP layer, which provides:\n        - HTTPS only, DNS resolution with IP validation\n        - DNS pinning (connects to validated IP directly)\n        - Blocks private/loopback/link-local/multicast IPs\n        - Response size limit and timeout enforcement\n        - Redirects disabled\n\n        Args:\n            client_id_url: The URL to fetch (also the expected client_id)\n\n        Returns:\n            Validated CIMDDocument\n\n        Raises:\n            CIMDValidationError: If document is invalid or URL blocked\n            CIMDFetchError: If document cannot be fetched\n        \"\"\"\n        cached = self._cache.get(client_id_url)\n        now = time.time()\n        request_headers: dict[str, str] | None = None\n        allowed_status_codes = {200}\n\n        if cached is not None:\n            if not cached.must_revalidate and now < cached.expires_at:\n                return cached.doc\n\n            request_headers = {}\n            if cached.etag:\n                request_headers[\"If-None-Match\"] = cached.etag\n            if cached.last_modified:\n                request_headers[\"If-Modified-Since\"] = cached.last_modified\n            if request_headers:\n                allowed_status_codes = {200, 304}\n\n        try:\n            response = await ssrf_safe_fetch_response(\n                client_id_url,\n                require_path=True,\n                max_size=self.MAX_RESPONSE_SIZE,\n                timeout=self.timeout,\n                overall_timeout=30.0,\n                request_headers=request_headers,\n                allowed_status_codes=allowed_status_codes,\n            )\n        except SSRFError as e:\n            raise CIMDValidationError(str(e)) from e\n        except SSRFFetchError as e:\n            raise CIMDFetchError(str(e)) from e\n\n        if response.status_code == 304:\n            if cached is None:\n                raise CIMDFetchError(\n                    \"CIMD server returned 304 Not Modified without cached document\"\n                )\n\n            now = time.time()\n            if self._has_freshness_headers(response.headers):\n                policy = self._parse_cache_policy(response.headers, now)\n            else:\n                # RFC allows 304 to omit unchanged headers. Preserve existing\n                # cache policy rather than resetting to fallback defaults.\n                policy = _CIMDCachePolicy(\n                    etag=None,\n                    last_modified=None,\n                    expires_at=now + cached.freshness_lifetime,\n                    freshness_lifetime=cached.freshness_lifetime,\n                    no_store=False,\n                    must_revalidate=cached.must_revalidate,\n                )\n\n            if not policy.no_store:\n                self._cache[client_id_url] = _CIMDCacheEntry(\n                    doc=cached.doc,\n                    etag=policy.etag or cached.etag,\n                    last_modified=policy.last_modified or cached.last_modified,\n                    expires_at=policy.expires_at,\n                    freshness_lifetime=policy.freshness_lifetime,\n                    must_revalidate=policy.must_revalidate,\n                )\n            else:\n                self._cache.pop(client_id_url, None)\n            return cached.doc\n\n        now = time.time()\n        policy = self._parse_cache_policy(response.headers, now)\n\n        try:\n            data = json.loads(response.content)\n        except json.JSONDecodeError as e:\n            raise CIMDValidationError(f\"CIMD document is not valid JSON: {e}\") from e\n\n        try:\n            doc = CIMDDocument.model_validate(data)\n        except Exception as e:\n            raise CIMDValidationError(f\"Invalid CIMD document: {e}\") from e\n\n        if str(doc.client_id).rstrip(\"/\") != client_id_url.rstrip(\"/\"):\n            raise CIMDValidationError(\n                f\"CIMD client_id mismatch: document says '{doc.client_id}' \"\n                f\"but was fetched from '{client_id_url}'\"\n            )\n\n        # Validate jwks_uri if present (SSRF check for JWKS endpoint)\n        if doc.jwks_uri:\n            jwks_uri_str = str(doc.jwks_uri)\n            try:\n                await validate_url(jwks_uri_str)\n            except SSRFError as e:\n                raise CIMDValidationError(\n                    f\"CIMD jwks_uri failed SSRF validation: {e}\"\n                ) from e\n\n        logger.info(\n            \"CIMD document fetched and validated: %s (client_name=%s)\",\n            client_id_url,\n            doc.client_name,\n        )\n\n        if not policy.no_store:\n            self._cache[client_id_url] = _CIMDCacheEntry(\n                doc=doc,\n                etag=policy.etag,\n                last_modified=policy.last_modified,\n                expires_at=policy.expires_at,\n                freshness_lifetime=policy.freshness_lifetime,\n                must_revalidate=policy.must_revalidate,\n            )\n        else:\n            self._cache.pop(client_id_url, None)\n\n        return doc\n\n    def validate_redirect_uri(self, doc: CIMDDocument, redirect_uri: str) -> bool:\n        \"\"\"Validate that a redirect_uri is allowed by the CIMD document.\n\n        Args:\n            doc: The CIMD document\n            redirect_uri: The redirect URI to validate\n\n        Returns:\n            True if valid, False otherwise\n        \"\"\"\n        if not doc.redirect_uris:\n            # No redirect_uris specified - reject all\n            return False\n\n        # Normalize for comparison\n        redirect_uri = redirect_uri.rstrip(\"/\")\n\n        for allowed in doc.redirect_uris:\n            allowed_str = allowed.rstrip(\"/\")\n            if redirect_uri == allowed_str:\n                return True\n\n            # Check for wildcard port matching (http://localhost:*/callback)\n            if \"*\" in allowed_str:\n                if fnmatch.fnmatch(redirect_uri, allowed_str):\n                    return True\n\n        return False\n\n\nclass CIMDAssertionValidator:\n    \"\"\"Validates JWT assertions for private_key_jwt CIMD clients.\n\n    Implements RFC 7523 (JSON Web Token (JWT) Profile for OAuth 2.0 Client\n    Authentication and Authorization Grants) for CIMD client authentication.\n\n    JTI replay protection uses TTL-based caching to ensure proper security:\n    - JTIs are cached with expiration matching the JWT's exp claim\n    - Expired JTIs are automatically cleaned up\n    - Maximum assertion lifetime is enforced (5 minutes)\n    \"\"\"\n\n    # Maximum allowed assertion lifetime in seconds (RFC 7523 recommends short-lived)\n    MAX_ASSERTION_LIFETIME = 300  # 5 minutes\n\n    def __init__(self):\n        # JTI cache: maps jti -> expiration timestamp\n        self._jti_cache: dict[str, float] = {}\n        self._jti_cache_max_size = 10000\n        self._last_cleanup = time.monotonic()\n        self._cleanup_interval = 60  # Cleanup every 60 seconds\n        # Cache JWTVerifier per jwks_uri so JWKS keys are not re-fetched\n        # on every token exchange\n        self._verifier_cache: dict[str, JWTVerifier] = {}\n        self._verifier_cache_max_size = 100\n        self.logger = get_logger(__name__)\n\n    def _cleanup_expired_jtis(self) -> None:\n        \"\"\"Remove expired JTIs from cache.\"\"\"\n        now = time.time()\n        expired = [jti for jti, exp in self._jti_cache.items() if exp < now]\n        for jti in expired:\n            del self._jti_cache[jti]\n        if expired:\n            self.logger.debug(\"Cleaned up %d expired JTIs from cache\", len(expired))\n\n    def _maybe_cleanup(self) -> None:\n        \"\"\"Periodically cleanup expired JTIs to prevent unbounded growth.\"\"\"\n        now = time.monotonic()\n        if now - self._last_cleanup > self._cleanup_interval:\n            self._cleanup_expired_jtis()\n            self._last_cleanup = now\n\n    async def validate_assertion(\n        self,\n        assertion: str,\n        client_id: str,\n        token_endpoint: str,\n        cimd_doc: CIMDDocument,\n    ) -> bool:\n        \"\"\"Validate JWT assertion from client.\n\n        Args:\n            assertion: The JWT assertion string\n            client_id: Expected client_id (must match iss and sub claims)\n            token_endpoint: Token endpoint URL (must match aud claim)\n            cimd_doc: CIMD document containing JWKS for key verification\n\n        Returns:\n            True if valid\n\n        Raises:\n            ValueError: If validation fails\n        \"\"\"\n        from fastmcp.server.auth.providers.jwt import JWTVerifier as _JWTVerifier\n\n        # Periodic cleanup of expired JTIs\n        self._maybe_cleanup()\n\n        # 1. Validate CIMD document has key material and get/create verifier\n        if cimd_doc.jwks_uri:\n            jwks_uri_str = str(cimd_doc.jwks_uri)\n            cache_key = f\"{jwks_uri_str}|{client_id}|{token_endpoint}\"\n            verifier = self._verifier_cache.get(cache_key)\n            if verifier is None:\n                verifier = _JWTVerifier(\n                    jwks_uri=jwks_uri_str,\n                    issuer=client_id,\n                    audience=token_endpoint,\n                    ssrf_safe=True,\n                )\n                if len(self._verifier_cache) >= self._verifier_cache_max_size:\n                    oldest_key = next(iter(self._verifier_cache))\n                    del self._verifier_cache[oldest_key]\n                self._verifier_cache[cache_key] = verifier\n        elif cimd_doc.jwks:\n            # Inline JWKS — no caching since the key is embedded\n            public_key = self._extract_public_key_from_jwks(assertion, cimd_doc.jwks)\n            verifier = _JWTVerifier(\n                public_key=public_key,\n                issuer=client_id,\n                audience=token_endpoint,\n            )\n        else:\n            raise ValueError(\n                \"CIMD document must have jwks_uri or jwks for private_key_jwt\"\n            )\n\n        # 2. Verify JWT using JWTVerifier (handles signature, exp, iss, aud)\n        access_token = await verifier.load_access_token(assertion)\n        if not access_token:\n            raise ValueError(\"Invalid JWT assertion\")\n\n        claims = access_token.claims\n\n        # 3. Validate assertion lifetime (exp and iat)\n        now = time.time()\n        exp = claims.get(\"exp\")\n        iat = claims.get(\"iat\")\n\n        if not exp:\n            raise ValueError(\"Assertion must include exp claim\")\n\n        # Validate exp is in the future (with small clock skew tolerance)\n        if exp < now - 30:  # 30 second clock skew tolerance\n            raise ValueError(\"Assertion has expired\")\n\n        # If iat is present, validate it and check assertion lifetime\n        if iat:\n            if iat > now + 30:  # 30 second clock skew tolerance\n                raise ValueError(\"Assertion iat is in the future\")\n            if exp - iat > self.MAX_ASSERTION_LIFETIME:\n                raise ValueError(\n                    f\"Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASSERTION_LIFETIME}s)\"\n                )\n        else:\n            # No iat, enforce max lifetime from now\n            if exp > now + self.MAX_ASSERTION_LIFETIME:\n                raise ValueError(\n                    f\"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)\"\n                )\n\n        # 4. Additional RFC 7523 validation: sub claim must equal client_id\n        if claims.get(\"sub\") != client_id:\n            raise ValueError(f\"Assertion sub claim must be {client_id}\")\n\n        # 5. Check jti for replay attacks (RFC 7523 requirement)\n        jti = claims.get(\"jti\")\n        if not jti:\n            raise ValueError(\"Assertion must include jti claim\")\n\n        # Check if JTI was already used (and hasn't expired from cache)\n        if jti in self._jti_cache:\n            cached_exp = self._jti_cache[jti]\n            if cached_exp > now:  # Still valid in cache\n                raise ValueError(f\"Assertion replay detected: jti {jti} already used\")\n            # Expired in cache, can be reused (clean it up)\n            del self._jti_cache[jti]\n\n        # Add to cache with expiration time\n        # Use the assertion's exp claim so it stays cached until it would expire anyway\n        self._jti_cache[jti] = exp\n\n        # Emergency size limit (shouldn't hit with proper TTL cleanup)\n        if len(self._jti_cache) > self._jti_cache_max_size:\n            self._cleanup_expired_jtis()\n            # If still over limit after cleanup, reject to prevent DoS\n            if len(self._jti_cache) > self._jti_cache_max_size:\n                self.logger.warning(\n                    \"JTI cache at max capacity (%d), possible attack\",\n                    self._jti_cache_max_size,\n                )\n                raise ValueError(\"Server overloaded, please retry\")\n\n        self.logger.debug(\n            \"JWT assertion validated successfully for client %s\", client_id\n        )\n        return True\n\n    def _extract_public_key_from_jwks(self, token: str, jwks: dict) -> str:\n        \"\"\"Extract public key from inline JWKS.\n\n        Args:\n            token: JWT token to extract kid from\n            jwks: JWKS document containing keys\n\n        Returns:\n            PEM-encoded public key\n\n        Raises:\n            ValueError: If key cannot be found or extracted\n        \"\"\"\n        import base64\n        import json\n\n        from authlib.jose import JsonWebKey\n\n        # Extract kid from token header\n        try:\n            header_b64 = token.split(\".\")[0]\n            header_b64 += \"=\" * (4 - len(header_b64) % 4)  # Add padding\n            header = json.loads(base64.urlsafe_b64decode(header_b64))\n            kid = header.get(\"kid\")\n        except Exception as e:\n            raise ValueError(f\"Failed to extract key ID from token: {e}\") from e\n\n        # Find matching key in JWKS\n        keys = jwks.get(\"keys\", [])\n        if not keys:\n            raise ValueError(\"JWKS document contains no keys\")\n\n        matching_key = None\n        for key in keys:\n            if kid and key.get(\"kid\") == kid:\n                matching_key = key\n                break\n\n        if not matching_key:\n            # If no kid match, try first key as fallback\n            if len(keys) == 1:\n                matching_key = keys[0]\n                self.logger.warning(\n                    \"No matching kid in JWKS, using single available key\"\n                )\n            else:\n                raise ValueError(f\"No matching key found for kid={kid} in JWKS\")\n\n        # Convert JWK to PEM\n        try:\n            jwk = JsonWebKey.import_key(matching_key)\n            return jwk.as_pem().decode(\"utf-8\")\n        except Exception as e:\n            raise ValueError(f\"Failed to convert JWK to PEM: {e}\") from e\n\n\nclass CIMDClientManager:\n    \"\"\"Manages all CIMD client operations for OAuth proxy.\n\n    This class encapsulates:\n    - CIMD client detection\n    - Document fetching and validation\n    - Synthetic OAuth client creation\n    - Private key JWT assertion validation\n\n    This allows the OAuth proxy to delegate all CIMD-specific logic to a\n    single, focused manager class.\n    \"\"\"\n\n    def __init__(\n        self,\n        enable_cimd: bool = True,\n        default_scope: str = \"\",\n        allowed_redirect_uri_patterns: list[str] | None = None,\n    ):\n        \"\"\"Initialize CIMD client manager.\n\n        Args:\n            enable_cimd: Whether CIMD support is enabled\n            default_scope: Default scope for CIMD clients if not specified in document\n            allowed_redirect_uri_patterns: Allowed redirect URI patterns (proxy's config)\n        \"\"\"\n        self.enabled = enable_cimd\n        self.default_scope = default_scope\n        self.allowed_redirect_uri_patterns = allowed_redirect_uri_patterns\n\n        self._fetcher = CIMDFetcher()\n        self._assertion_validator = CIMDAssertionValidator()\n        self.logger = get_logger(__name__)\n\n    def is_cimd_client_id(self, client_id: str) -> bool:\n        \"\"\"Check if client_id is a CIMD URL.\n\n        Args:\n            client_id: Client ID to check\n\n        Returns:\n            True if client_id is an HTTPS URL (CIMD format)\n        \"\"\"\n        return self.enabled and self._fetcher.is_cimd_client_id(client_id)\n\n    async def get_client(self, client_id_url: str):\n        \"\"\"Fetch CIMD document and create synthetic OAuth client.\n\n        Args:\n            client_id_url: HTTPS URL pointing to CIMD document\n\n        Returns:\n            OAuthProxyClient with CIMD document attached, or None if fetch fails\n\n        Note:\n            Return type is left untyped to avoid circular import with oauth_proxy.\n            Returns OAuthProxyClient instance or None.\n        \"\"\"\n        if not self.enabled:\n            return None\n\n        try:\n            cimd_doc = await self._fetcher.fetch(client_id_url)\n        except (CIMDFetchError, CIMDValidationError) as e:\n            self.logger.warning(\"CIMD fetch failed for %s: %s\", client_id_url, e)\n            return None\n\n        # Import here to avoid circular dependency\n        from fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient\n\n        # Create synthetic client from CIMD document.\n        # Keep CIMD redirect_uris as strings on the document itself so wildcard\n        # patterns like http://localhost:*/callback remain valid.\n        redirect_uris = None\n        client = ProxyDCRClient(\n            client_id=client_id_url,\n            client_secret=None,\n            redirect_uris=redirect_uris,\n            grant_types=cimd_doc.grant_types,\n            scope=cimd_doc.scope or self.default_scope,\n            token_endpoint_auth_method=cimd_doc.token_endpoint_auth_method,\n            allowed_redirect_uri_patterns=self.allowed_redirect_uri_patterns,\n            client_name=cimd_doc.client_name,\n            cimd_document=cimd_doc,\n            cimd_fetched_at=time.time(),\n        )\n\n        self.logger.debug(\n            \"CIMD client resolved: %s (name=%s)\",\n            client_id_url,\n            cimd_doc.client_name,\n        )\n        return client\n\n    async def validate_private_key_jwt(\n        self,\n        assertion: str,\n        client,  # OAuthProxyClient, untyped to avoid circular import\n        token_endpoint: str,\n    ) -> bool:\n        \"\"\"Validate JWT assertion for private_key_jwt auth.\n\n        Args:\n            assertion: JWT assertion string from client\n            client: OAuth proxy client (must have cimd_document)\n            token_endpoint: Token endpoint URL for aud validation\n\n        Returns:\n            True if assertion is valid\n\n        Raises:\n            ValueError: If client doesn't have CIMD document or validation fails\n        \"\"\"\n        if not hasattr(client, \"cimd_document\") or not client.cimd_document:\n            raise ValueError(\"Client must have CIMD document for private_key_jwt\")\n\n        cimd_doc = client.cimd_document\n        if cimd_doc.token_endpoint_auth_method != \"private_key_jwt\":\n            raise ValueError(\"CIMD document must specify private_key_jwt auth method\")\n\n        return await self._assertion_validator.validate_assertion(\n            assertion, client.client_id, token_endpoint, cimd_doc\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/handlers/authorize.py",
    "content": "\"\"\"Enhanced authorization handler with improved error responses.\n\nThis module provides an enhanced authorization handler that wraps the MCP SDK's\nAuthorizationHandler to provide better error messages when clients attempt to\nauthorize with unregistered client IDs.\n\nThe enhancement adds:\n- Content negotiation: HTML for browsers, JSON for API clients\n- Enhanced JSON responses with registration endpoint hints\n- Styled HTML error pages with registration links/forms\n- Link headers pointing to registration endpoints\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import TYPE_CHECKING\n\nfrom mcp.server.auth.handlers.authorize import (\n    AuthorizationHandler as SDKAuthorizationHandler,\n)\nfrom pydantic import AnyHttpUrl\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.ui import (\n    INFO_BOX_STYLES,\n    TOOLTIP_STYLES,\n    create_logo,\n    create_page,\n    create_secure_html_response,\n)\n\nif TYPE_CHECKING:\n    from mcp.server.auth.provider import OAuthAuthorizationServerProvider\n\nlogger = get_logger(__name__)\n\n\ndef create_unregistered_client_html(\n    client_id: str,\n    registration_endpoint: str,\n    discovery_endpoint: str,\n    server_name: str | None = None,\n    server_icon_url: str | None = None,\n    title: str = \"Client Not Registered\",\n) -> str:\n    \"\"\"Create styled HTML error page for unregistered client attempts.\n\n    Args:\n        client_id: The unregistered client ID that was provided\n        registration_endpoint: URL of the registration endpoint\n        discovery_endpoint: URL of the OAuth metadata discovery endpoint\n        server_name: Optional server name for branding\n        server_icon_url: Optional server icon URL\n        title: Page title\n\n    Returns:\n        HTML string for the error page\n    \"\"\"\n    import html as html_module\n\n    client_id_escaped = html_module.escape(client_id)\n\n    # Main error message\n    error_box = f\"\"\"\n        <div class=\"info-box error\">\n            <p>The client ID <code>{client_id_escaped}</code> was not found in the server's client registry.</p>\n        </div>\n    \"\"\"\n\n    # What to do - yellow warning box\n    warning_box = \"\"\"\n        <div class=\"info-box warning\">\n            <p>Your MCP client opened this page to complete OAuth authorization,\n            but the server did not recognize its client ID. To fix this:</p>\n            <ul>\n                <li>Close this browser window</li>\n                <li>Clear authentication tokens in your MCP client (or restart it)</li>\n                <li>Try connecting again - your client should automatically re-register</li>\n            </ul>\n        </div>\n    \"\"\"\n\n    # Help link with tooltip (similar to consent screen)\n    help_link = \"\"\"\n        <div class=\"help-link-container\">\n            <span class=\"help-link\">\n                Why am I seeing this?\n                <span class=\"tooltip\">\n                    OAuth 2.0 requires clients to register before authorization.\n                    This server returned a 400 error because the provided client\n                    ID was not found.\n                    <br><br>\n                    In browser-delegated OAuth flows, your application cannot\n                    detect this error automatically; it's waiting for a\n                    callback that will never arrive. You must manually clear\n                    auth tokens and reconnect.\n                </span>\n            </span>\n        </div>\n    \"\"\"\n\n    # Build page content\n    content = f\"\"\"\n        <div class=\"container\">\n            {create_logo(icon_url=server_icon_url, alt_text=server_name or \"FastMCP\")}\n            <h1>{title}</h1>\n            {error_box}\n            {warning_box}\n        </div>\n        {help_link}\n    \"\"\"\n\n    # Use same styles as consent page\n    additional_styles = (\n        INFO_BOX_STYLES\n        + TOOLTIP_STYLES\n        + \"\"\"\n        /* Error variant for info-box */\n        .info-box.error {\n            background: #fef2f2;\n            border-color: #f87171;\n        }\n        .info-box.error strong {\n            color: #991b1b;\n        }\n        /* Warning variant for info-box (yellow) */\n        .info-box.warning {\n            background: #fffbeb;\n            border-color: #fbbf24;\n        }\n        .info-box.warning strong {\n            color: #92400e;\n        }\n        .info-box code {\n            background: rgba(0, 0, 0, 0.05);\n            padding: 2px 6px;\n            border-radius: 3px;\n            font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;\n            font-size: 0.9em;\n        }\n        .info-box ul {\n            margin: 10px 0;\n            padding-left: 20px;\n        }\n        .info-box li {\n            margin: 6px 0;\n        }\n        \"\"\"\n    )\n\n    return create_page(\n        content=content,\n        title=title,\n        additional_styles=additional_styles,\n    )\n\n\nclass AuthorizationHandler(SDKAuthorizationHandler):\n    \"\"\"Authorization handler with enhanced error responses for unregistered clients.\n\n    This handler extends the MCP SDK's AuthorizationHandler to provide better UX\n    when clients attempt to authorize without being registered. It implements\n    content negotiation to return:\n\n    - HTML error pages for browser requests\n    - Enhanced JSON with registration hints for API clients\n    - Link headers pointing to registration endpoints\n\n    This maintains OAuth 2.1 compliance (returns 400 for invalid client_id)\n    while providing actionable guidance to fix the error.\n    \"\"\"\n\n    def __init__(\n        self,\n        provider: OAuthAuthorizationServerProvider,\n        base_url: AnyHttpUrl | str,\n        server_name: str | None = None,\n        server_icon_url: str | None = None,\n    ):\n        \"\"\"Initialize the enhanced authorization handler.\n\n        Args:\n            provider: OAuth authorization server provider\n            base_url: Base URL of the server for constructing endpoint URLs\n            server_name: Optional server name for branding\n            server_icon_url: Optional server icon URL for branding\n        \"\"\"\n        super().__init__(provider)\n        self._base_url = str(base_url).rstrip(\"/\")\n        self._server_name = server_name\n        self._server_icon_url = server_icon_url\n\n    async def handle(self, request: Request) -> Response:\n        \"\"\"Handle authorization request with enhanced error responses.\n\n        This method extends the SDK's authorization handler and intercepts\n        errors for unregistered clients to provide better error responses\n        based on the client's Accept header.\n\n        Args:\n            request: The authorization request\n\n        Returns:\n            Response (redirect on success, error response on failure)\n        \"\"\"\n        # Call the SDK handler\n        response = await super().handle(request)\n\n        # Check if this is a client not found error\n        if response.status_code == 400:\n            # Try to extract client_id from request for enhanced error\n            client_id: str | None = None\n            if request.method == \"GET\":\n                client_id = request.query_params.get(\"client_id\")\n            else:\n                form = await request.form()\n                client_id_value = form.get(\"client_id\")\n                # Ensure client_id is a string, not UploadFile\n                if isinstance(client_id_value, str):\n                    client_id = client_id_value\n\n            # If we have a client_id and the error is about it not being found,\n            # enhance the response\n            if client_id:\n                try:\n                    # Check if response body contains \"not found\" error\n                    if hasattr(response, \"body\"):\n                        body = json.loads(bytes(response.body))\n                        if (\n                            body.get(\"error\") == \"invalid_request\"\n                            and \"not found\" in body.get(\"error_description\", \"\").lower()\n                        ):\n                            return await self._create_enhanced_error_response(\n                                request, client_id, body.get(\"state\")\n                            )\n                except Exception:\n                    # If we can't parse the response, just return the original\n                    pass\n\n        return response\n\n    async def _create_enhanced_error_response(\n        self, request: Request, client_id: str, state: str | None\n    ) -> Response:\n        \"\"\"Create enhanced error response with content negotiation.\n\n        Args:\n            request: The original request\n            client_id: The unregistered client ID\n            state: The state parameter from the request\n\n        Returns:\n            HTML or JSON error response based on Accept header\n        \"\"\"\n        registration_endpoint = f\"{self._base_url}/register\"\n        discovery_endpoint = f\"{self._base_url}/.well-known/oauth-authorization-server\"\n\n        # Extract server metadata from app state (same pattern as consent screen)\n        from fastmcp.server.server import FastMCP\n\n        fastmcp = getattr(request.app.state, \"fastmcp_server\", None)\n\n        if isinstance(fastmcp, FastMCP):\n            server_name = fastmcp.name\n            icons = fastmcp.icons\n            server_icon_url = icons[0].src if icons else None\n        else:\n            server_name = self._server_name\n            server_icon_url = self._server_icon_url\n\n        # Check Accept header for content negotiation\n        accept = request.headers.get(\"accept\", \"\")\n\n        # Prefer HTML for browsers\n        if \"text/html\" in accept:\n            html = create_unregistered_client_html(\n                client_id=client_id,\n                registration_endpoint=registration_endpoint,\n                discovery_endpoint=discovery_endpoint,\n                server_name=server_name,\n                server_icon_url=server_icon_url,\n            )\n            response = create_secure_html_response(html, status_code=400)\n        else:\n            # Return enhanced JSON for API clients\n            from mcp.server.auth.handlers.authorize import AuthorizationErrorResponse\n\n            error_data = AuthorizationErrorResponse(\n                error=\"invalid_request\",\n                error_description=(\n                    f\"Client ID '{client_id}' is not registered with this server. \"\n                    f\"MCP clients should automatically re-register by sending a POST request to \"\n                    f\"the registration_endpoint and retry authorization. \"\n                    f\"If this persists, clear cached authentication tokens and reconnect.\"\n                ),\n                state=state,\n            )\n\n            # Add extra fields to help clients discover registration\n            error_dict = error_data.model_dump(exclude_none=True)\n            error_dict[\"registration_endpoint\"] = registration_endpoint\n            error_dict[\"authorization_server_metadata\"] = discovery_endpoint\n\n            from starlette.responses import JSONResponse\n\n            response = JSONResponse(\n                status_code=400,\n                content=error_dict,\n                headers={\"Cache-Control\": \"no-store\"},\n            )\n\n        # Add Link header for registration endpoint discovery\n        response.headers[\"Link\"] = (\n            f'<{registration_endpoint}>; rel=\"http://oauth.net/core/2.1/#registration\"'\n        )\n\n        logger.info(\n            \"Unregistered client_id=%s, returned %s error response\",\n            client_id,\n            \"HTML\" if \"text/html\" in accept else \"JSON\",\n        )\n\n        return response\n"
  },
  {
    "path": "src/fastmcp/server/auth/jwt_issuer.py",
    "content": "\"\"\"JWT token issuance and verification for FastMCP OAuth Proxy.\n\nThis module implements the token factory pattern for OAuth proxies, where the proxy\nissues its own JWT tokens to clients instead of forwarding upstream provider tokens.\nThis maintains proper OAuth 2.0 token audience boundaries.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport time\nfrom typing import Any, overload\n\nfrom authlib.jose import JsonWebToken\nfrom authlib.jose.errors import JoseError\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.kdf.hkdf import HKDF\nfrom cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\n\nimport fastmcp\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nKDF_ITERATIONS = 1_000_000\nKDF_ITERATIONS_TEST = 10\n\n\n@overload\ndef derive_jwt_key(*, high_entropy_material: str, salt: str) -> bytes:\n    \"\"\"Derive JWT signing key from a high-entropy key material and server salt.\"\"\"\n\n\n@overload\ndef derive_jwt_key(*, low_entropy_material: str, salt: str) -> bytes:\n    \"\"\"Derive JWT signing key from a low-entropy key material and server salt.\"\"\"\n\n\ndef derive_jwt_key(\n    *,\n    high_entropy_material: str | None = None,\n    low_entropy_material: str | None = None,\n    salt: str,\n) -> bytes:\n    \"\"\"Derive JWT signing key from a high-entropy or low-entropy key material and server salt.\"\"\"\n    if high_entropy_material is not None and low_entropy_material is not None:\n        raise ValueError(\n            \"Either high_entropy_material or low_entropy_material must be provided, but not both\"\n        )\n\n    if high_entropy_material is not None:\n        derived_key = HKDF(\n            algorithm=hashes.SHA256(),\n            length=32,\n            salt=salt.encode(),\n            info=b\"Fernet\",\n        ).derive(key_material=high_entropy_material.encode())\n\n        return base64.urlsafe_b64encode(derived_key)\n\n    if low_entropy_material is not None:\n        iterations = (\n            KDF_ITERATIONS_TEST if fastmcp.settings.test_mode else KDF_ITERATIONS\n        )\n        pbkdf2 = PBKDF2HMAC(\n            algorithm=hashes.SHA256(),\n            length=32,\n            salt=salt.encode(),\n            iterations=iterations,\n        ).derive(key_material=low_entropy_material.encode())\n\n        return base64.urlsafe_b64encode(pbkdf2)\n\n    raise ValueError(\n        \"Either high_entropy_material or low_entropy_material must be provided\"\n    )\n\n\nclass JWTIssuer:\n    \"\"\"Issues and validates FastMCP-signed JWT tokens using HS256.\n\n    This issuer creates JWT tokens for MCP clients with proper audience claims,\n    maintaining OAuth 2.0 token boundaries. Tokens are signed with HS256 using\n    a key derived from the upstream client secret.\n    \"\"\"\n\n    def __init__(\n        self,\n        issuer: str,\n        audience: str,\n        signing_key: bytes,\n    ):\n        \"\"\"Initialize JWT issuer.\n\n        Args:\n            issuer: Token issuer (FastMCP server base URL)\n            audience: Token audience (typically {base_url}/mcp)\n            signing_key: HS256 signing key (32 bytes)\n        \"\"\"\n        self.issuer = issuer\n        self.audience = audience\n        self._signing_key = signing_key\n        self._jwt = JsonWebToken([\"HS256\"])\n\n    def issue_access_token(\n        self,\n        client_id: str,\n        scopes: list[str],\n        jti: str,\n        expires_in: int = 3600,\n        upstream_claims: dict[str, Any] | None = None,\n    ) -> str:\n        \"\"\"Issue a minimal FastMCP access token.\n\n        FastMCP tokens are reference tokens containing only the minimal claims\n        needed for validation and lookup. The JTI maps to the upstream token\n        which contains actual user identity and authorization data.\n\n        Args:\n            client_id: MCP client ID\n            scopes: Token scopes\n            jti: Unique token identifier (maps to upstream token)\n            expires_in: Token lifetime in seconds\n            upstream_claims: Optional claims from upstream IdP token to include\n\n        Returns:\n            Signed JWT token\n        \"\"\"\n        now = int(time.time())\n\n        header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}\n        payload: dict[str, Any] = {\n            \"iss\": self.issuer,\n            \"aud\": self.audience,\n            \"client_id\": client_id,\n            \"scope\": \" \".join(scopes),\n            \"exp\": now + expires_in,\n            \"iat\": now,\n            \"jti\": jti,\n        }\n\n        if upstream_claims:\n            payload[\"upstream_claims\"] = upstream_claims\n\n        token_bytes = self._jwt.encode(header, payload, self._signing_key)\n        token = token_bytes.decode(\"utf-8\")\n\n        logger.debug(\n            \"Issued access token for client=%s jti=%s exp=%d\",\n            client_id,\n            jti[:8],\n            payload[\"exp\"],\n        )\n\n        return token\n\n    def issue_refresh_token(\n        self,\n        client_id: str,\n        scopes: list[str],\n        jti: str,\n        expires_in: int,\n        upstream_claims: dict[str, Any] | None = None,\n    ) -> str:\n        \"\"\"Issue a minimal FastMCP refresh token.\n\n        FastMCP refresh tokens are reference tokens containing only the minimal\n        claims needed for validation and lookup. The JTI maps to the upstream\n        token which contains actual user identity and authorization data.\n\n        Args:\n            client_id: MCP client ID\n            scopes: Token scopes\n            jti: Unique token identifier (maps to upstream token)\n            expires_in: Token lifetime in seconds (should match upstream refresh expiry)\n            upstream_claims: Optional claims from upstream IdP token to include\n\n        Returns:\n            Signed JWT token\n        \"\"\"\n        now = int(time.time())\n\n        header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}\n        payload: dict[str, Any] = {\n            \"iss\": self.issuer,\n            \"aud\": self.audience,\n            \"client_id\": client_id,\n            \"scope\": \" \".join(scopes),\n            \"exp\": now + expires_in,\n            \"iat\": now,\n            \"jti\": jti,\n            \"token_use\": \"refresh\",\n        }\n\n        if upstream_claims:\n            payload[\"upstream_claims\"] = upstream_claims\n\n        token_bytes = self._jwt.encode(header, payload, self._signing_key)\n        token = token_bytes.decode(\"utf-8\")\n\n        logger.debug(\n            \"Issued refresh token for client=%s jti=%s exp=%d\",\n            client_id,\n            jti[:8],\n            payload[\"exp\"],\n        )\n\n        return token\n\n    def verify_token(\n        self,\n        token: str,\n        expected_token_use: str = \"access\",\n    ) -> dict[str, Any]:\n        \"\"\"Verify and decode a FastMCP token.\n\n        Validates JWT signature, expiration, issuer, audience, and token type.\n\n        Args:\n            token: JWT token to verify\n            expected_token_use: Expected token type (\"access\" or \"refresh\").\n                Defaults to \"access\", which rejects refresh tokens.\n\n        Returns:\n            Decoded token payload\n\n        Raises:\n            JoseError: If token is invalid, expired, or has wrong claims\n        \"\"\"\n        try:\n            # Decode and verify signature\n            payload = self._jwt.decode(token, self._signing_key)\n\n            # Validate token type\n            token_use = payload.get(\"token_use\", \"access\")\n            if token_use != expected_token_use:\n                logger.debug(\n                    \"Token type mismatch: expected %s, got %s\",\n                    expected_token_use,\n                    token_use,\n                )\n                raise JoseError(\n                    f\"Token type mismatch: expected {expected_token_use}, \"\n                    f\"got {token_use}\"\n                )\n\n            # Validate expiration\n            exp = payload.get(\"exp\")\n            if exp and exp < time.time():\n                logger.debug(\"Token expired\")\n                raise JoseError(\"Token has expired\")\n\n            # Validate issuer\n            if payload.get(\"iss\") != self.issuer:\n                logger.debug(\"Token has invalid issuer\")\n                raise JoseError(\"Invalid token issuer\")\n\n            # Validate audience\n            if payload.get(\"aud\") != self.audience:\n                logger.debug(\"Token has invalid audience\")\n                raise JoseError(\"Invalid token audience\")\n\n            logger.debug(\n                \"Token verified successfully for subject=%s\", payload.get(\"sub\")\n            )\n            return payload\n\n        except JoseError as e:\n            logger.debug(\"Token validation failed: %s\", e)\n            raise\n"
  },
  {
    "path": "src/fastmcp/server/auth/middleware.py",
    "content": "\"\"\"Enhanced authentication middleware with better error messages.\n\nThis module provides enhanced versions of MCP SDK authentication middleware\nthat return more helpful error messages for developers troubleshooting\nauthentication issues.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\n\nfrom mcp.server.auth.middleware.bearer_auth import (\n    RequireAuthMiddleware as SDKRequireAuthMiddleware,\n)\nfrom starlette.types import Send\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass RequireAuthMiddleware(SDKRequireAuthMiddleware):\n    \"\"\"Enhanced authentication middleware with detailed error messages.\n\n    Extends the SDK's RequireAuthMiddleware to provide more actionable\n    error messages when authentication fails. This helps developers\n    understand what went wrong and how to fix it.\n    \"\"\"\n\n    async def _send_auth_error(\n        self, send: Send, status_code: int, error: str, description: str\n    ) -> None:\n        \"\"\"Send an authentication error response with enhanced error messages.\n\n        Overrides the SDK's _send_auth_error to provide more detailed\n        error descriptions that help developers troubleshoot authentication\n        issues.\n\n        Args:\n            send: ASGI send callable\n            status_code: HTTP status code (401 or 403)\n            error: OAuth error code\n            description: Base error description\n        \"\"\"\n        # Enhance error descriptions based on error type\n        enhanced_description = description\n\n        if error == \"invalid_token\" and status_code == 401:\n            # This is the \"Authentication required\" error\n            enhanced_description = (\n                \"Authentication failed. The provided bearer token is invalid, expired, or no longer recognized by the server. \"\n                \"To resolve: clear authentication tokens in your MCP client and reconnect. \"\n                \"Your client should automatically re-register and obtain new tokens.\"\n            )\n        elif error == \"insufficient_scope\":\n            # Scope error - already has good detail from SDK\n            pass\n\n        # Build WWW-Authenticate header value\n        www_auth_parts = [\n            f'error=\"{error}\"',\n            f'error_description=\"{enhanced_description}\"',\n        ]\n        if self.resource_metadata_url:\n            www_auth_parts.append(f'resource_metadata=\"{self.resource_metadata_url}\"')\n\n        www_authenticate = f\"Bearer {', '.join(www_auth_parts)}\"\n\n        # Send response\n        body = {\"error\": error, \"error_description\": enhanced_description}\n        body_bytes = json.dumps(body).encode()\n\n        await send(\n            {\n                \"type\": \"http.response.start\",\n                \"status\": status_code,\n                \"headers\": [\n                    (b\"content-type\", b\"application/json\"),\n                    (b\"content-length\", str(len(body_bytes)).encode()),\n                    (b\"www-authenticate\", www_authenticate.encode()),\n                ],\n            }\n        )\n\n        await send(\n            {\n                \"type\": \"http.response.body\",\n                \"body\": body_bytes,\n            }\n        )\n\n        logger.info(\n            \"Auth error returned: %s (status=%d)\",\n            error,\n            status_code,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/oauth_proxy/__init__.py",
    "content": "\"\"\"OAuth Proxy Provider for FastMCP.\n\nThis package provides OAuth proxy functionality split across multiple modules:\n- models: Pydantic models and constants\n- ui: HTML generation functions\n- consent: Consent management mixin\n- proxy: Main OAuthProxy class\n\"\"\"\n\nfrom fastmcp.server.auth.oauth_proxy.proxy import OAuthProxy\n\n__all__ = [\n    \"OAuthProxy\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/auth/oauth_proxy/consent.py",
    "content": "\"\"\"OAuth Proxy Consent Management.\n\nThis module contains consent management functionality for the OAuth proxy.\nThe ConsentMixin class provides methods for handling user consent flows,\ncookie management, and consent page rendering.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport secrets\nimport time\nfrom base64 import urlsafe_b64encode\nfrom typing import TYPE_CHECKING, Any\nfrom urllib.parse import urlencode, urlparse\n\nfrom pydantic import AnyUrl\nfrom starlette.requests import Request\nfrom starlette.responses import HTMLResponse, RedirectResponse\n\nfrom fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient\nfrom fastmcp.server.auth.oauth_proxy.ui import create_consent_html\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.ui import create_secure_html_response\n\nif TYPE_CHECKING:\n    from fastmcp.server.auth.oauth_proxy.proxy import OAuthProxy\n\nlogger = get_logger(__name__)\n\n\nclass ConsentMixin:\n    \"\"\"Mixin class providing consent management functionality for OAuthProxy.\n\n    This mixin contains all methods related to:\n    - Cookie signing and verification\n    - Consent page rendering\n    - Consent approval/denial handling\n    - URI normalization for consent tracking\n    \"\"\"\n\n    def _normalize_uri(self, uri: str) -> str:\n        \"\"\"Normalize a URI to a canonical form for consent tracking.\"\"\"\n        parsed = urlparse(uri)\n        path = parsed.path or \"\"\n        normalized = f\"{parsed.scheme.lower()}://{parsed.netloc.lower()}{path}\"\n        if normalized.endswith(\"/\") and len(path) > 1:\n            normalized = normalized[:-1]\n        return normalized\n\n    def _make_client_key(self, client_id: str, redirect_uri: str | AnyUrl) -> str:\n        \"\"\"Create a stable key for consent tracking from client_id and redirect_uri.\"\"\"\n        normalized = self._normalize_uri(str(redirect_uri))\n        return f\"{client_id}:{normalized}\"\n\n    def _cookie_name(self: OAuthProxy, base_name: str) -> str:\n        \"\"\"Return secure cookie name for HTTPS, fallback for HTTP development.\"\"\"\n        if self._is_https:\n            return f\"__Host-{base_name}\"\n        return f\"__{base_name}\"\n\n    def _cookie_signing_key(self: OAuthProxy) -> bytes:\n        \"\"\"Return the key used for HMAC-signing consent cookies.\n\n        Uses the upstream client secret when available, falling back to the\n        JWT signing key (which is always present — OAuthProxy requires it\n        when no client secret is provided).\n        \"\"\"\n        if self._upstream_client_secret is not None:\n            return self._upstream_client_secret.get_secret_value().encode()\n        return self._jwt_signing_key\n\n    def _sign_cookie(self: OAuthProxy, payload: str) -> str:\n        \"\"\"Sign a cookie payload with HMAC-SHA256.\n\n        Returns: base64(payload).base64(signature)\n        \"\"\"\n        key = self._cookie_signing_key()\n        signature = hmac.new(key, payload.encode(), hashlib.sha256).digest()\n        signature_b64 = base64.b64encode(signature).decode()\n        return f\"{payload}.{signature_b64}\"\n\n    def _verify_cookie(self: OAuthProxy, signed_value: str) -> str | None:\n        \"\"\"Verify and extract payload from signed cookie.\n\n        Returns: payload if signature valid, None otherwise\n        \"\"\"\n        try:\n            if \".\" not in signed_value:\n                return None\n            payload, signature_b64 = signed_value.rsplit(\".\", 1)\n\n            # Verify signature\n            key = self._cookie_signing_key()\n            expected_sig = hmac.new(key, payload.encode(), hashlib.sha256).digest()\n            provided_sig = base64.b64decode(signature_b64.encode())\n\n            # Constant-time comparison\n            if not hmac.compare_digest(expected_sig, provided_sig):\n                return None\n\n            return payload\n        except Exception:\n            return None\n\n    def _decode_list_cookie(\n        self: OAuthProxy, request: Request, base_name: str\n    ) -> list[str]:\n        \"\"\"Decode and verify a signed base64-encoded JSON list from cookie. Returns [] if missing/invalid.\"\"\"\n        secure_name = self._cookie_name(base_name)\n        raw = request.cookies.get(secure_name)\n        # Only fall back to the non-__Host- name over plain HTTP. On HTTPS,\n        # __Host- enforces host-only scope; accepting the weaker name would\n        # let a sibling-subdomain attacker inject a domain-scoped cookie.\n        if not raw and not self._is_https:\n            raw = request.cookies.get(f\"__{base_name}\")\n        if not raw:\n            return []\n        try:\n            # Verify signature\n            payload = self._verify_cookie(raw)\n            if not payload:\n                logger.debug(\"Cookie signature verification failed for %s\", secure_name)\n                return []\n\n            # Decode payload\n            data = base64.b64decode(payload.encode())\n            value = json.loads(data.decode())\n            if isinstance(value, list):\n                return [str(x) for x in value]\n        except Exception:\n            logger.debug(\"Failed to decode cookie %s; treating as empty\", secure_name)\n        return []\n\n    def _encode_list_cookie(self: OAuthProxy, values: list[str]) -> str:\n        \"\"\"Encode values to base64 and sign with HMAC.\n\n        Returns: signed cookie value (payload.signature)\n        \"\"\"\n        payload = json.dumps(values, separators=(\",\", \":\")).encode()\n        payload_b64 = base64.b64encode(payload).decode()\n        return self._sign_cookie(payload_b64)\n\n    def _set_list_cookie(\n        self: OAuthProxy,\n        response: HTMLResponse | RedirectResponse,\n        base_name: str,\n        value_b64: str,\n        max_age: int,\n    ) -> None:\n        name = self._cookie_name(base_name)\n        response.set_cookie(\n            name,\n            value_b64,\n            max_age=max_age,\n            secure=self._is_https,\n            httponly=True,\n            samesite=\"lax\",\n            path=\"/\",\n        )\n\n    def _read_consent_bindings(self: OAuthProxy, request: Request) -> dict[str, str]:\n        \"\"\"Read the consent binding map from the signed cookie.\n\n        Returns a dict of {txn_id: consent_token} for all pending flows.\n        \"\"\"\n        cookie_name = self._cookie_name(\"MCP_CONSENT_BINDING\")\n        raw = request.cookies.get(cookie_name)\n        # Only fall back to the non-__Host- name over plain HTTP. On HTTPS,\n        # __Host- enforces host-only scope; accepting the weaker name would\n        # bypass that guarantee.\n        if not raw and not self._is_https:\n            raw = request.cookies.get(\"__MCP_CONSENT_BINDING\")\n        if not raw:\n            return {}\n        payload = self._verify_cookie(raw)\n        if not payload:\n            return {}\n        try:\n            data = json.loads(base64.b64decode(payload.encode()).decode())\n            if isinstance(data, dict):\n                return {str(k): str(v) for k, v in data.items()}\n        except Exception:\n            logger.debug(\"Failed to decode consent binding cookie\")\n        return {}\n\n    def _write_consent_bindings(\n        self: OAuthProxy,\n        response: HTMLResponse | RedirectResponse,\n        bindings: dict[str, str],\n    ) -> None:\n        \"\"\"Write the consent binding map to a signed cookie.\"\"\"\n        name = self._cookie_name(\"MCP_CONSENT_BINDING\")\n        if not bindings:\n            response.set_cookie(\n                name,\n                \"\",\n                max_age=0,\n                secure=self._is_https,\n                httponly=True,\n                samesite=\"lax\",\n                path=\"/\",\n            )\n            return\n        payload_bytes = json.dumps(bindings, separators=(\",\", \":\")).encode()\n        payload_b64 = base64.b64encode(payload_bytes).decode()\n        signed_value = self._sign_cookie(payload_b64)\n        response.set_cookie(\n            name,\n            signed_value,\n            max_age=15 * 60,\n            secure=self._is_https,\n            httponly=True,\n            samesite=\"lax\",\n            path=\"/\",\n        )\n\n    def _set_consent_binding_cookie(\n        self: OAuthProxy,\n        request: Request,\n        response: HTMLResponse | RedirectResponse,\n        txn_id: str,\n        consent_token: str,\n    ) -> None:\n        \"\"\"Add a consent binding entry for a transaction.\n\n        This cookie binds the browser that approved consent to the IdP callback,\n        ensuring a different browser cannot complete the OAuth flow. Multiple\n        concurrent flows are supported by storing a map of txn_id → consent_token.\n        \"\"\"\n        bindings = self._read_consent_bindings(request)\n        bindings[txn_id] = consent_token\n        self._write_consent_bindings(response, bindings)\n\n    def _clear_consent_binding_cookie(\n        self: OAuthProxy,\n        request: Request,\n        response: HTMLResponse | RedirectResponse,\n        txn_id: str,\n    ) -> None:\n        \"\"\"Remove a specific consent binding entry after successful callback.\"\"\"\n        bindings = self._read_consent_bindings(request)\n        bindings.pop(txn_id, None)\n        self._write_consent_bindings(response, bindings)\n\n    def _verify_consent_binding_cookie(\n        self: OAuthProxy,\n        request: Request,\n        txn_id: str,\n        expected_token: str,\n    ) -> bool:\n        \"\"\"Verify the consent binding for a specific transaction.\"\"\"\n        bindings = self._read_consent_bindings(request)\n        actual = bindings.get(txn_id)\n        if not actual:\n            return False\n        return hmac.compare_digest(actual, expected_token)\n\n    def _build_upstream_authorize_url(\n        self: OAuthProxy, txn_id: str, transaction: dict[str, Any]\n    ) -> str:\n        \"\"\"Construct the upstream IdP authorization URL using stored transaction data.\"\"\"\n        query_params: dict[str, Any] = {\n            \"response_type\": \"code\",\n            \"client_id\": self._upstream_client_id,\n            \"redirect_uri\": f\"{str(self.base_url).rstrip('/')}{self._redirect_path}\",\n            \"state\": txn_id,\n        }\n\n        scopes_to_use = transaction.get(\"scopes\") or self.required_scopes or []\n        if scopes_to_use:\n            query_params[\"scope\"] = \" \".join(scopes_to_use)\n\n        # If PKCE forwarding was enabled, include the proxy challenge\n        proxy_code_verifier = transaction.get(\"proxy_code_verifier\")\n        if proxy_code_verifier:\n            challenge_bytes = hashlib.sha256(proxy_code_verifier.encode()).digest()\n            proxy_code_challenge = (\n                urlsafe_b64encode(challenge_bytes).decode().rstrip(\"=\")\n            )\n            query_params[\"code_challenge\"] = proxy_code_challenge\n            query_params[\"code_challenge_method\"] = \"S256\"\n\n        # Forward resource indicator if present in transaction\n        if resource := transaction.get(\"resource\"):\n            query_params[\"resource\"] = resource\n\n        # Extra configured parameters\n        if self._extra_authorize_params:\n            query_params.update(self._extra_authorize_params)\n\n        separator = \"&\" if \"?\" in self._upstream_authorization_endpoint else \"?\"\n        return f\"{self._upstream_authorization_endpoint}{separator}{urlencode(query_params)}\"\n\n    async def _handle_consent(\n        self: OAuthProxy, request: Request\n    ) -> HTMLResponse | RedirectResponse:\n        \"\"\"Handle consent page - dispatch to GET or POST handler based on method.\"\"\"\n        if request.method == \"POST\":\n            return await self._submit_consent(request)\n        return await self._show_consent_page(request)\n\n    async def _show_consent_page(\n        self: OAuthProxy, request: Request\n    ) -> HTMLResponse | RedirectResponse:\n        \"\"\"Display consent page or auto-approve/deny based on cookies.\"\"\"\n        from fastmcp.server.server import FastMCP\n\n        txn_id = request.query_params.get(\"txn_id\")\n        if not txn_id:\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Invalid or expired transaction</p>\", status_code=400\n            )\n\n        txn_model = await self._transaction_store.get(key=txn_id)\n        if not txn_model:\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Invalid or expired transaction</p>\", status_code=400\n            )\n\n        txn = txn_model.model_dump()\n        client_key = self._make_client_key(txn[\"client_id\"], txn[\"client_redirect_uri\"])\n\n        approved = set(self._decode_list_cookie(request, \"MCP_APPROVED_CLIENTS\"))\n        denied = set(self._decode_list_cookie(request, \"MCP_DENIED_CLIENTS\"))\n\n        if client_key in approved:\n            consent_token = secrets.token_urlsafe(32)\n            txn_model.consent_token = consent_token\n            await self._transaction_store.put(key=txn_id, value=txn_model, ttl=15 * 60)\n            upstream_url = self._build_upstream_authorize_url(txn_id, txn)\n            response = RedirectResponse(url=upstream_url, status_code=302)\n            self._set_consent_binding_cookie(request, response, txn_id, consent_token)\n            return response\n\n        if client_key in denied:\n            callback_params = {\n                \"error\": \"access_denied\",\n                \"state\": txn.get(\"client_state\") or \"\",\n            }\n            sep = \"&\" if \"?\" in txn[\"client_redirect_uri\"] else \"?\"\n            return RedirectResponse(\n                url=f\"{txn['client_redirect_uri']}{sep}{urlencode(callback_params)}\",\n                status_code=302,\n            )\n\n        # Need consent: issue CSRF token and show HTML\n        csrf_token = secrets.token_urlsafe(32)\n        csrf_expires_at = time.time() + 15 * 60\n\n        # Update transaction with CSRF token\n        txn_model.csrf_token = csrf_token\n        txn_model.csrf_expires_at = csrf_expires_at\n        await self._transaction_store.put(\n            key=txn_id, value=txn_model, ttl=15 * 60\n        )  # Auto-expire after 15 minutes\n\n        # Update dict for use in HTML generation\n        txn[\"csrf_token\"] = csrf_token\n        txn[\"csrf_expires_at\"] = csrf_expires_at\n\n        # Load client to get client_name and CIMD info if available\n        client = await self.get_client(txn[\"client_id\"])\n        client_name = getattr(client, \"client_name\", None) if client else None\n\n        # Detect CIMD clients for verified domain badge\n        is_cimd_client = False\n        cimd_domain: str | None = None\n        if isinstance(client, ProxyDCRClient) and client.cimd_document is not None:\n            is_cimd_client = True\n            cimd_domain = urlparse(txn[\"client_id\"]).hostname\n\n        # Extract server metadata from app state\n        fastmcp = getattr(request.app.state, \"fastmcp_server\", None)\n\n        if isinstance(fastmcp, FastMCP):\n            server_name = fastmcp.name\n            icons = fastmcp.icons\n            server_icon_url = icons[0].src if icons else None\n            server_website_url = fastmcp.website_url\n        else:\n            server_name = None\n            server_icon_url = None\n            server_website_url = None\n\n        html = create_consent_html(\n            client_id=txn[\"client_id\"],\n            redirect_uri=txn[\"client_redirect_uri\"],\n            scopes=txn.get(\"scopes\") or [],\n            txn_id=txn_id,\n            csrf_token=csrf_token,\n            client_name=client_name,\n            server_name=server_name,\n            server_icon_url=server_icon_url,\n            server_website_url=server_website_url,\n            csp_policy=self._consent_csp_policy,\n            is_cimd_client=is_cimd_client,\n            cimd_domain=cimd_domain,\n        )\n        response = create_secure_html_response(html)\n        # Merge new CSRF token with any existing ones (supports concurrent flows)\n        existing_tokens = self._decode_list_cookie(request, \"MCP_CONSENT_STATE\")\n        existing_tokens.append(csrf_token)\n        self._set_list_cookie(\n            response,\n            \"MCP_CONSENT_STATE\",\n            self._encode_list_cookie(existing_tokens),\n            max_age=15 * 60,\n        )\n        return response\n\n    async def _submit_consent(\n        self: OAuthProxy, request: Request\n    ) -> RedirectResponse | HTMLResponse:\n        \"\"\"Handle consent approval/denial, set cookies, and redirect appropriately.\"\"\"\n        form = await request.form()\n        txn_id = str(form.get(\"txn_id\", \"\"))\n        action = str(form.get(\"action\", \"\"))\n        csrf_token = str(form.get(\"csrf_token\", \"\"))\n\n        if not txn_id:\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Invalid or expired transaction</p>\", status_code=400\n            )\n\n        txn_model = await self._transaction_store.get(key=txn_id)\n        if not txn_model:\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Invalid or expired transaction</p>\", status_code=400\n            )\n\n        txn = txn_model.model_dump()\n        expected_csrf = txn.get(\"csrf_token\")\n        expires_at = float(txn.get(\"csrf_expires_at\") or 0)\n\n        if not expected_csrf or csrf_token != expected_csrf or time.time() > expires_at:\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Invalid or expired consent token</p>\", status_code=400\n            )\n\n        # Double-submit CSRF check: verify the form token matches the cookie.\n        # Without this, an attacker who knows their own tx_id/csrf_token can\n        # CSRF the victim's browser into approving consent, bypassing the\n        # consent binding cookie protection.\n        cookie_csrf_tokens = self._decode_list_cookie(request, \"MCP_CONSENT_STATE\")\n        if csrf_token not in cookie_csrf_tokens:\n            logger.warning(\n                \"CSRF double-submit check failed for transaction %s \"\n                \"(possible cross-site consent forgery)\",\n                txn_id,\n            )\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Authorization session mismatch. \"\n                \"Please try authenticating again.</p>\",\n                status_code=403,\n            )\n\n        client_key = self._make_client_key(txn[\"client_id\"], txn[\"client_redirect_uri\"])\n\n        if action == \"approve\":\n            approved = set(self._decode_list_cookie(request, \"MCP_APPROVED_CLIENTS\"))\n            if client_key not in approved:\n                approved.add(client_key)\n            approved_b64 = self._encode_list_cookie(sorted(approved))\n\n            consent_token = secrets.token_urlsafe(32)\n            txn_model.consent_token = consent_token\n            await self._transaction_store.put(key=txn_id, value=txn_model, ttl=15 * 60)\n\n            upstream_url = self._build_upstream_authorize_url(txn_id, txn)\n            response = RedirectResponse(url=upstream_url, status_code=302)\n            self._set_list_cookie(\n                response, \"MCP_APPROVED_CLIENTS\", approved_b64, max_age=365 * 24 * 3600\n            )\n            # Clear CSRF cookie by setting empty short-lived value\n            self._set_list_cookie(\n                response, \"MCP_CONSENT_STATE\", self._encode_list_cookie([]), max_age=60\n            )\n            self._set_consent_binding_cookie(request, response, txn_id, consent_token)\n            return response\n\n        elif action == \"deny\":\n            denied = set(self._decode_list_cookie(request, \"MCP_DENIED_CLIENTS\"))\n            if client_key not in denied:\n                denied.add(client_key)\n            denied_b64 = self._encode_list_cookie(sorted(denied))\n\n            callback_params = {\n                \"error\": \"access_denied\",\n                \"state\": txn.get(\"client_state\") or \"\",\n            }\n            sep = \"&\" if \"?\" in txn[\"client_redirect_uri\"] else \"?\"\n            client_callback_url = (\n                f\"{txn['client_redirect_uri']}{sep}{urlencode(callback_params)}\"\n            )\n            response = RedirectResponse(url=client_callback_url, status_code=302)\n            self._set_list_cookie(\n                response, \"MCP_DENIED_CLIENTS\", denied_b64, max_age=365 * 24 * 3600\n            )\n            self._set_list_cookie(\n                response, \"MCP_CONSENT_STATE\", self._encode_list_cookie([]), max_age=60\n            )\n            return response\n\n        else:\n            return create_secure_html_response(\n                \"<h1>Error</h1><p>Invalid action</p>\", status_code=400\n            )\n"
  },
  {
    "path": "src/fastmcp/server/auth/oauth_proxy/models.py",
    "content": "\"\"\"OAuth Proxy Models and Constants.\n\nThis module contains all Pydantic models and constants used by the OAuth proxy.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nfrom typing import Any, Final\n\nfrom mcp.shared.auth import InvalidRedirectUriError, OAuthClientInformationFull\nfrom pydantic import AnyUrl, BaseModel, Field\n\nfrom fastmcp.server.auth.cimd import CIMDDocument\nfrom fastmcp.server.auth.redirect_validation import (\n    matches_allowed_pattern,\n    validate_redirect_uri,\n)\n\n# -------------------------------------------------------------------------\n# Constants\n# -------------------------------------------------------------------------\n\n# Default token expiration times\nDEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS: Final[int] = 60 * 60  # 1 hour\nDEFAULT_ACCESS_TOKEN_EXPIRY_NO_REFRESH_SECONDS: Final[int] = (\n    60 * 60 * 24 * 365\n)  # 1 year\nDEFAULT_AUTH_CODE_EXPIRY_SECONDS: Final[int] = 5 * 60  # 5 minutes\n\n# HTTP client timeout\nHTTP_TIMEOUT_SECONDS: Final[int] = 30\n\n\n# -------------------------------------------------------------------------\n# Pydantic Models\n# -------------------------------------------------------------------------\n\n\nclass OAuthTransaction(BaseModel):\n    \"\"\"OAuth transaction state for consent flow.\n\n    Stored server-side to track active authorization flows with client context.\n    Includes CSRF tokens for consent protection per MCP security best practices.\n    \"\"\"\n\n    txn_id: str\n    client_id: str\n    client_redirect_uri: str\n    client_state: str\n    code_challenge: str | None\n    code_challenge_method: str\n    scopes: list[str]\n    created_at: float\n    resource: str | None = None\n    proxy_code_verifier: str | None = None\n    csrf_token: str | None = None\n    csrf_expires_at: float | None = None\n    consent_token: str | None = None\n\n\nclass ClientCode(BaseModel):\n    \"\"\"Client authorization code with PKCE and upstream tokens.\n\n    Stored server-side after upstream IdP callback. Contains the upstream\n    tokens bound to the client's PKCE challenge for secure token exchange.\n    \"\"\"\n\n    code: str\n    client_id: str\n    redirect_uri: str\n    code_challenge: str | None\n    code_challenge_method: str\n    scopes: list[str]\n    idp_tokens: dict[str, Any]\n    expires_at: float\n    created_at: float\n\n\nclass UpstreamTokenSet(BaseModel):\n    \"\"\"Stored upstream OAuth tokens from identity provider.\n\n    These tokens are obtained from the upstream provider (Google, GitHub, etc.)\n    and stored in plaintext within this model. Encryption is handled transparently\n    at the storage layer via FernetEncryptionWrapper. Tokens are never exposed to MCP clients.\n    \"\"\"\n\n    upstream_token_id: str  # Unique ID for this token set\n    access_token: str  # Upstream access token\n    refresh_token: str | None  # Upstream refresh token\n    refresh_token_expires_at: (\n        float | None\n    )  # Unix timestamp when refresh token expires (if known)\n    expires_at: float  # Unix timestamp when access token expires\n    token_type: str  # Usually \"Bearer\"\n    scope: str  # Space-separated scopes\n    client_id: str  # MCP client this is bound to\n    created_at: float  # Unix timestamp\n    raw_token_data: dict[str, Any] = Field(default_factory=dict)  # Full token response\n\n\nclass JTIMapping(BaseModel):\n    \"\"\"Maps FastMCP token JTI to upstream token ID.\n\n    This allows stateless JWT validation while still being able to look up\n    the corresponding upstream token when tools need to access upstream APIs.\n    \"\"\"\n\n    jti: str  # JWT ID from FastMCP-issued token\n    upstream_token_id: str  # References UpstreamTokenSet\n    created_at: float  # Unix timestamp\n\n\nclass RefreshTokenMetadata(BaseModel):\n    \"\"\"Metadata for a refresh token, stored keyed by token hash.\n\n    We store only metadata (not the token itself) for security - if storage\n    is compromised, attackers get hashes they can't reverse into usable tokens.\n    \"\"\"\n\n    client_id: str\n    scopes: list[str]\n    expires_at: int | None = None\n    created_at: float\n\n\ndef _hash_token(token: str) -> str:\n    \"\"\"Hash a token for secure storage lookup.\n\n    Uses SHA-256 to create a one-way hash. The original token cannot be\n    recovered from the hash, providing defense in depth if storage is compromised.\n    \"\"\"\n    return hashlib.sha256(token.encode()).hexdigest()\n\n\nclass ProxyDCRClient(OAuthClientInformationFull):\n    \"\"\"Client for DCR proxy with configurable redirect URI validation.\n\n    This special client class is critical for the OAuth proxy to work correctly\n    with Dynamic Client Registration (DCR). Here's why it exists:\n\n    Problem:\n    --------\n    When MCP clients use OAuth, they dynamically register with random localhost\n    ports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:\n    1. Accept these dynamic redirect URIs from clients based on configured patterns\n    2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)\n    3. Forward the authorization code back to the client's dynamic URI\n\n    Solution:\n    ---------\n    This class validates redirect URIs against configurable patterns,\n    while the proxy internally uses its own fixed redirect URI with the upstream\n    provider. This allows the flow to work even when clients reconnect with\n    different ports or when tokens are cached.\n\n    Without proper validation, clients could get \"Redirect URI not registered\" errors\n    when trying to authenticate with cached tokens, or security vulnerabilities could\n    arise from accepting arbitrary redirect URIs.\n    \"\"\"\n\n    allowed_redirect_uri_patterns: list[str] | None = Field(default=None)\n    client_name: str | None = Field(default=None)\n    cimd_document: CIMDDocument | None = Field(default=None)\n    cimd_fetched_at: float | None = Field(default=None)\n\n    def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:\n        \"\"\"Validate redirect URI against proxy patterns and optionally CIMD redirect_uris.\n\n        For CIMD clients: validates against BOTH the CIMD document's redirect_uris\n        AND the proxy's allowed patterns (if configured). Both must pass.\n\n        For DCR clients: validates against proxy patterns first, falling back to\n        base validation (registered redirect_uris) if patterns don't match.\n        \"\"\"\n        if redirect_uri is None and self.cimd_document is not None:\n            cimd_redirect_uris = self.cimd_document.redirect_uris\n            if len(cimd_redirect_uris) == 1:\n                candidate = cimd_redirect_uris[0]\n                if \"*\" in candidate:\n                    raise InvalidRedirectUriError(\n                        \"redirect_uri must be specified when CIMD redirect_uris uses wildcards.\"\n                    )\n                try:\n                    resolved = AnyUrl(candidate)\n                except Exception as e:\n                    raise InvalidRedirectUriError(\n                        f\"Invalid CIMD redirect_uri: {e}\"\n                    ) from e\n\n                # Respect proxy-level redirect URI restrictions even when the\n                # client omits redirect_uri and we fall back to CIMD defaults.\n                if (\n                    self.allowed_redirect_uri_patterns is not None\n                    and not validate_redirect_uri(\n                        redirect_uri=resolved,\n                        allowed_patterns=self.allowed_redirect_uri_patterns,\n                    )\n                ):\n                    raise InvalidRedirectUriError(\n                        f\"Redirect URI '{resolved}' does not match allowed patterns.\"\n                    )\n\n                return resolved\n\n            raise InvalidRedirectUriError(\n                \"redirect_uri must be specified when CIMD lists multiple redirect_uris.\"\n            )\n\n        if redirect_uri is not None:\n            cimd_redirect_uris = (\n                self.cimd_document.redirect_uris if self.cimd_document else None\n            )\n\n            if cimd_redirect_uris:\n                uri_str = str(redirect_uri)\n                cimd_match = any(\n                    matches_allowed_pattern(uri_str, pattern)\n                    for pattern in cimd_redirect_uris\n                )\n                if not cimd_match:\n                    raise InvalidRedirectUriError(\n                        f\"Redirect URI '{redirect_uri}' does not match CIMD redirect_uris.\"\n                    )\n\n                if self.allowed_redirect_uri_patterns is not None:\n                    if not validate_redirect_uri(\n                        redirect_uri=redirect_uri,\n                        allowed_patterns=self.allowed_redirect_uri_patterns,\n                    ):\n                        raise InvalidRedirectUriError(\n                            f\"Redirect URI '{redirect_uri}' does not match allowed patterns.\"\n                        )\n\n                return redirect_uri\n\n            pattern_matches = validate_redirect_uri(\n                redirect_uri=redirect_uri,\n                allowed_patterns=self.allowed_redirect_uri_patterns,\n            )\n\n            if pattern_matches:\n                return redirect_uri\n\n            # Patterns configured but didn't match\n            if self.allowed_redirect_uri_patterns:\n                raise InvalidRedirectUriError(\n                    f\"Redirect URI '{redirect_uri}' does not match allowed patterns.\"\n                )\n\n        # No redirect_uri provided or no patterns configured — use base validation\n        return super().validate_redirect_uri(redirect_uri)\n"
  },
  {
    "path": "src/fastmcp/server/auth/oauth_proxy/proxy.py",
    "content": "\"\"\"OAuth Proxy Provider for FastMCP.\n\nThis provider acts as a transparent proxy to an upstream OAuth Authorization Server,\nhandling Dynamic Client Registration locally while forwarding all other OAuth flows.\nThis enables authentication with upstream providers that don't support DCR or have\nrestricted client registration policies.\n\nKey features:\n- Proxies authorization and token endpoints to upstream server\n- Implements local Dynamic Client Registration with fixed upstream credentials\n- Validates tokens using upstream JWKS\n- Maintains minimal local state for bookkeeping\n- Enhanced logging with request correlation\n\nThis implementation is based on the OAuth 2.1 specification and is designed for\nproduction use with enterprise identity providers.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport secrets\nimport time\nfrom base64 import urlsafe_b64encode\nfrom typing import Any, Literal\nfrom urllib.parse import urlencode, urlparse, urlunparse\n\nimport httpx\nfrom authlib.common.security import generate_token\nfrom authlib.integrations.httpx_client import AsyncOAuth2Client\nfrom cryptography.fernet import Fernet\nfrom key_value.aio.adapters.pydantic import PydanticAdapter\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom key_value.aio.stores.filetree import (\n    FileTreeStore,\n    FileTreeV1CollectionSanitizationStrategy,\n    FileTreeV1KeySanitizationStrategy,\n)\nfrom key_value.aio.wrappers.encryption import FernetEncryptionWrapper\nfrom mcp.server.auth.handlers.metadata import MetadataHandler\nfrom mcp.server.auth.provider import (\n    AccessToken,\n    AuthorizationCode,\n    AuthorizationParams,\n    AuthorizeError,\n    RefreshToken,\n    TokenError,\n)\nfrom mcp.server.auth.routes import build_metadata, cors_middleware\nfrom mcp.server.auth.settings import (\n    ClientRegistrationOptions,\n    RevocationOptions,\n)\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthToken\nfrom pydantic import AnyHttpUrl, AnyUrl, SecretStr\nfrom starlette.requests import Request\nfrom starlette.responses import HTMLResponse, RedirectResponse\nfrom starlette.routing import Route\nfrom typing_extensions import override\n\nfrom fastmcp import settings\nfrom fastmcp.server.auth.auth import (\n    OAuthProvider,\n    PrivateKeyJWTClientAuthenticator,\n    TokenHandler,\n    TokenVerifier,\n)\nfrom fastmcp.server.auth.cimd import CIMDClientManager\nfrom fastmcp.server.auth.handlers.authorize import AuthorizationHandler\nfrom fastmcp.server.auth.jwt_issuer import (\n    JWTIssuer,\n    derive_jwt_key,\n)\nfrom fastmcp.server.auth.oauth_proxy.consent import ConsentMixin\nfrom fastmcp.server.auth.oauth_proxy.models import (\n    DEFAULT_ACCESS_TOKEN_EXPIRY_NO_REFRESH_SECONDS,\n    DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,\n    DEFAULT_AUTH_CODE_EXPIRY_SECONDS,\n    HTTP_TIMEOUT_SECONDS,\n    ClientCode,\n    JTIMapping,\n    OAuthTransaction,\n    ProxyDCRClient,\n    RefreshTokenMetadata,\n    UpstreamTokenSet,\n    _hash_token,\n)\nfrom fastmcp.server.auth.oauth_proxy.ui import create_error_html\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\ndef _normalize_resource_url(url: str) -> str:\n    \"\"\"Normalize a resource URL by removing query parameters and trailing slashes.\n\n    RFC 8707 allows clients to include query parameters in resource URLs, but the\n    server's configured resource URL typically doesn't include them. This function\n    normalizes URLs for comparison by stripping query params and fragments.\n\n    Args:\n        url: The URL to normalize\n\n    Returns:\n        Normalized URL with scheme, host, and path only (no query/fragment)\n    \"\"\"\n    parsed = urlparse(str(url))\n    return urlunparse(\n        (parsed.scheme, parsed.netloc, parsed.path.rstrip(\"/\"), \"\", \"\", \"\")\n    )\n\n\ndef _server_url_has_query(url: str) -> bool:\n    \"\"\"Check if a URL has query parameters.\"\"\"\n    return bool(urlparse(str(url)).query)\n\n\nclass OAuthProxy(OAuthProvider, ConsentMixin):\n    \"\"\"OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.\n\n    Purpose\n    -------\n    MCP clients expect OAuth providers to support Dynamic Client Registration (DCR),\n    where clients can register themselves dynamically and receive unique credentials.\n    Most enterprise IDPs (Google, GitHub, Azure AD, etc.) don't support DCR and require\n    pre-registered OAuth applications with fixed credentials.\n\n    This proxy bridges that gap by:\n    - Presenting a full DCR-compliant OAuth interface to MCP clients\n    - Translating DCR registration requests to use pre-configured upstream credentials\n    - Proxying all OAuth flows to the upstream IDP with appropriate translations\n    - Managing the state and security requirements of both protocols\n\n    Architecture Overview\n    --------------------\n    The proxy maintains a single OAuth app registration with the upstream provider\n    while allowing unlimited MCP clients to register and authenticate dynamically.\n    It implements the complete OAuth 2.1 + DCR specification for clients while\n    translating to whatever OAuth variant the upstream provider requires.\n\n    Key Translation Challenges Solved\n    ---------------------------------\n    1. Dynamic Client Registration:\n       - MCP clients expect to register dynamically and get unique credentials\n       - Upstream IDPs require pre-registered apps with fixed credentials\n       - Solution: Accept DCR requests, return shared upstream credentials\n\n    2. Dynamic Redirect URIs:\n       - MCP clients use random localhost ports that change between sessions\n       - Upstream IDPs require fixed, pre-registered redirect URIs\n       - Solution: Use proxy's fixed callback URL with upstream, forward to client's dynamic URI\n\n    3. Authorization Code Mapping:\n       - Upstream returns codes for the proxy's redirect URI\n       - Clients expect codes for their own redirect URIs\n       - Solution: Exchange upstream code server-side, issue new code to client\n\n    4. State Parameter Collision:\n       - Both client and proxy need to maintain state through the flow\n       - Only one state parameter available in OAuth\n       - Solution: Use transaction ID as state with upstream, preserve client's state\n\n    5. Token Management:\n       - Clients may expect different token formats/claims than upstream provides\n       - Need to track tokens for revocation and refresh\n       - Solution: Store token relationships, forward upstream tokens transparently\n\n    OAuth Flow Implementation\n    ------------------------\n    1. Client Registration (DCR):\n       - Accept any client registration request\n       - Store ProxyDCRClient that accepts dynamic redirect URIs\n\n    2. Authorization:\n       - Store transaction mapping client details to proxy flow\n       - Redirect to upstream with proxy's fixed redirect URI\n       - Use transaction ID as state parameter with upstream\n\n    3. Upstream Callback:\n       - Exchange upstream authorization code for tokens (server-side)\n       - Generate new authorization code bound to client's PKCE challenge\n       - Redirect to client's original dynamic redirect URI\n\n    4. Token Exchange:\n       - Validate client's code and PKCE verifier\n       - Return previously obtained upstream tokens\n       - Clean up one-time use authorization code\n\n    5. Token Refresh:\n       - Forward refresh requests to upstream using authlib\n       - Handle token rotation if upstream issues new refresh token\n       - Update local token mappings\n\n    State Management\n    ---------------\n    The proxy maintains minimal but crucial state via pluggable storage (client_storage):\n    - _oauth_transactions: Active authorization flows with client context\n    - _client_codes: Authorization codes with PKCE challenges and upstream tokens\n    - _jti_mapping_store: Maps FastMCP token JTIs to upstream token IDs\n    - _refresh_token_store: Refresh token metadata (keyed by token hash)\n\n    All state is stored in the configured client_storage backend (Redis, disk, etc.)\n    enabling horizontal scaling across multiple instances.\n\n    Security Considerations\n    ----------------------\n    - Refresh tokens stored by hash only (defense in depth if storage compromised)\n    - PKCE enforced end-to-end (client to proxy, proxy to upstream)\n    - Authorization codes are single-use with short expiry\n    - Transaction IDs are cryptographically random\n    - All state is cleaned up after use to prevent replay\n    - Token validation delegates to upstream provider\n\n    Provider Compatibility\n    ---------------------\n    Works with any OAuth 2.0 provider that supports:\n    - Authorization code flow\n    - Fixed redirect URI (configured in provider's app settings)\n    - Standard token endpoint\n\n    Handles provider-specific requirements:\n    - Google: Ensures minimum scope requirements\n    - GitHub: Compatible with OAuth Apps and GitHub Apps\n    - Azure AD: Handles tenant-specific endpoints\n    - Generic: Works with any spec-compliant provider\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        # Upstream server configuration\n        upstream_authorization_endpoint: str,\n        upstream_token_endpoint: str,\n        upstream_client_id: str,\n        upstream_client_secret: str | None = None,\n        upstream_revocation_endpoint: str | None = None,\n        # Token validation\n        token_verifier: TokenVerifier,\n        # FastMCP server configuration\n        base_url: AnyHttpUrl | str,\n        redirect_path: str | None = None,\n        issuer_url: AnyHttpUrl | str | None = None,\n        service_documentation_url: AnyHttpUrl | str | None = None,\n        # Client redirect URI validation\n        allowed_client_redirect_uris: list[str] | None = None,\n        valid_scopes: list[str] | None = None,\n        # PKCE configuration\n        forward_pkce: bool = True,\n        # Token endpoint authentication\n        token_endpoint_auth_method: str | None = None,\n        # Extra parameters to forward to authorization endpoint\n        extra_authorize_params: dict[str, str] | None = None,\n        # Extra parameters to forward to token endpoint\n        extra_token_params: dict[str, str] | None = None,\n        # Client storage\n        client_storage: AsyncKeyValue | None = None,\n        # JWT signing key\n        jwt_signing_key: str | bytes | None = None,\n        # Consent screen configuration\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        # Token expiry fallback\n        fallback_access_token_expiry_seconds: int | None = None,\n        # CIMD (Client ID Metadata Document) support\n        enable_cimd: bool = True,\n    ):\n        \"\"\"Initialize the OAuth proxy provider.\n\n        Args:\n            upstream_authorization_endpoint: URL of upstream authorization endpoint\n            upstream_token_endpoint: URL of upstream token endpoint\n            upstream_client_id: Client ID registered with upstream server\n            upstream_client_secret: Client secret for upstream server. Optional for\n                PKCE public clients or when using alternative credentials (e.g.,\n                managed identity). When omitted, jwt_signing_key must be provided.\n            upstream_revocation_endpoint: Optional upstream revocation endpoint\n            token_verifier: Token verifier for validating access tokens\n            base_url: Public URL of the server that exposes this FastMCP server; redirect path is\n                relative to this URL\n            redirect_path: Redirect path configured in upstream OAuth app (defaults to \"/auth/callback\")\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url)\n            service_documentation_url: Optional service documentation URL\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                Patterns support wildcards (e.g., \"http://localhost:*\", \"https://*.example.com/*\").\n                If None (default), all redirect URIs are allowed (for DCR compatibility).\n                If empty list, no redirect URIs are allowed.\n                These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.\n            valid_scopes: List of all the possible valid scopes for a client.\n                These are advertised to clients through the `/.well-known` endpoints. Defaults to `required_scopes` if not provided.\n            forward_pkce: Whether to forward PKCE to upstream server (default True).\n                Enable for providers that support/require PKCE (Google, Azure, AWS, etc.).\n                Disable only if upstream provider doesn't support PKCE.\n            token_endpoint_auth_method: Token endpoint authentication method for upstream server.\n                Common values: \"client_secret_basic\", \"client_secret_post\", \"none\".\n                If None, authlib will use its default (typically \"client_secret_basic\").\n            extra_authorize_params: Additional parameters to forward to the upstream authorization endpoint.\n                Useful for provider-specific parameters like Auth0's \"audience\".\n                Example: {\"audience\": \"https://api.example.com\"}\n            extra_token_params: Additional parameters to forward to the upstream token endpoint.\n                Useful for provider-specific parameters during token exchange.\n            client_storage: Storage backend for OAuth state (client registrations, tokens).\n                If None, an encrypted file store will be created in the data directory.\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes).\n                If bytes are provided, they will be used as-is.\n                If a string is provided, it will be derived into a 32-byte key using PBKDF2 (1.2M iterations).\n                If not provided, it will be derived from the upstream client secret using HKDF.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to the upstream IdP.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            consent_csp_policy: Content Security Policy for the consent page.\n                If None (default), uses the built-in CSP policy with appropriate directives.\n                If empty string \"\", disables CSP entirely (no meta tag is rendered).\n                If a non-empty string, uses that as the CSP policy value.\n                This allows organizations with their own CSP policies to override or disable\n                the built-in CSP directives.\n            fallback_access_token_expiry_seconds: Expiry time to use when upstream provider\n                doesn't return `expires_in` in the token response. If not set, uses smart\n                defaults: 1 hour if a refresh token is available (since we can refresh),\n                or 1 year if no refresh token (for API-key-style tokens like GitHub OAuth Apps).\n                Set explicitly to override these defaults.\n            enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based\n                client IDs. When True, clients can authenticate using HTTPS URLs as client\n                IDs, with metadata fetched from the URL. Supports private_key_jwt auth.\n        \"\"\"\n\n        # Always enable DCR since we implement it locally for MCP clients\n        client_registration_options = ClientRegistrationOptions(\n            enabled=True,\n            valid_scopes=valid_scopes or token_verifier.required_scopes,\n        )\n\n        # Enable revocation only if upstream endpoint provided\n        revocation_options = (\n            RevocationOptions(enabled=True) if upstream_revocation_endpoint else None\n        )\n\n        super().__init__(\n            base_url=base_url,\n            issuer_url=issuer_url,\n            service_documentation_url=service_documentation_url,\n            client_registration_options=client_registration_options,\n            revocation_options=revocation_options,\n            required_scopes=token_verifier.required_scopes,\n        )\n\n        # Store upstream configuration\n        self._upstream_authorization_endpoint: str = upstream_authorization_endpoint\n        self._upstream_token_endpoint: str = upstream_token_endpoint\n        self._upstream_client_id: str = upstream_client_id\n        self._upstream_client_secret: SecretStr | None = (\n            SecretStr(secret_value=upstream_client_secret)\n            if upstream_client_secret is not None\n            else None\n        )\n        self._upstream_revocation_endpoint: str | None = upstream_revocation_endpoint\n        self._default_scope_str: str = \" \".join(self.required_scopes or [])\n\n        # Store redirect configuration\n        if not redirect_path:\n            self._redirect_path = \"/auth/callback\"\n        else:\n            self._redirect_path = (\n                redirect_path if redirect_path.startswith(\"/\") else f\"/{redirect_path}\"\n            )\n\n        if (\n            isinstance(allowed_client_redirect_uris, list)\n            and not allowed_client_redirect_uris\n        ):\n            logger.warning(\n                \"allowed_client_redirect_uris is empty list; no redirect URIs will be accepted. \"\n                + \"This will block all OAuth clients.\"\n            )\n        self._allowed_client_redirect_uris: list[str] | None = (\n            allowed_client_redirect_uris\n        )\n\n        # PKCE configuration\n        self._forward_pkce: bool = forward_pkce\n\n        # Token endpoint authentication\n        self._token_endpoint_auth_method: str | None = token_endpoint_auth_method\n\n        # Consent screen configuration\n        self._require_authorization_consent: bool | Literal[\"external\"] = (\n            require_authorization_consent\n        )\n        self._consent_csp_policy: str | None = consent_csp_policy\n        if require_authorization_consent == \"external\":\n            logger.info(\n                \"Built-in consent screen disabled; consent is handled externally.\"\n            )\n        elif not require_authorization_consent:\n            logger.warning(\n                \"Authorization consent screen disabled - only use for local development or testing. \"\n                + \"In production, this screen protects against confused deputy attacks.\"\n            )\n\n        # Extra parameters for authorization and token endpoints\n        self._extra_authorize_params: dict[str, str] = extra_authorize_params or {}\n        self._extra_token_params: dict[str, str] = extra_token_params or {}\n\n        # Token expiry fallback (None means use smart default based on refresh token)\n        self._fallback_access_token_expiry_seconds: int | None = (\n            fallback_access_token_expiry_seconds\n        )\n\n        if jwt_signing_key is None:\n            if upstream_client_secret is None:\n                raise ValueError(\n                    \"jwt_signing_key is required when upstream_client_secret is not provided. \"\n                    \"The JWT signing key cannot be derived without a client secret.\"\n                )\n            jwt_signing_key = derive_jwt_key(\n                high_entropy_material=upstream_client_secret,\n                salt=\"fastmcp-jwt-signing-key\",\n            )\n\n        if isinstance(jwt_signing_key, str):\n            if len(jwt_signing_key) < 12:\n                logger.warning(\n                    \"jwt_signing_key is less than 12 characters; it is recommended to use a longer. \"\n                    + \"string for the key derivation.\"\n                )\n            jwt_signing_key = derive_jwt_key(\n                low_entropy_material=jwt_signing_key,\n                salt=\"fastmcp-jwt-signing-key\",\n            )\n\n        # Store JWT signing key for deferred JWTIssuer creation in set_mcp_path()\n        self._jwt_signing_key: bytes = jwt_signing_key\n        # JWTIssuer will be created in set_mcp_path() with correct audience\n        self._jwt_issuer: JWTIssuer | None = None\n\n        # If the user does not provide a store, we will provide an encrypted file store.\n        # The storage directory is derived from the encryption key so that different\n        # keys get isolated directories (e.g. two servers on the same machine with\n        # different keys won't collide). Decryption errors are treated as cache misses\n        # rather than hard failures, so key rotation just causes re-registration.\n        if client_storage is None:\n            storage_encryption_key = derive_jwt_key(\n                high_entropy_material=jwt_signing_key.decode(),\n                salt=\"fastmcp-storage-encryption-key\",\n            )\n\n            key_fingerprint = hashlib.sha256(storage_encryption_key).hexdigest()[:12]\n            storage_dir = settings.home / \"oauth-proxy\" / key_fingerprint\n            storage_dir.mkdir(parents=True, exist_ok=True)\n\n            file_store = FileTreeStore(\n                data_directory=storage_dir,\n                key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(\n                    storage_dir\n                ),\n                collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(\n                    storage_dir\n                ),\n            )\n\n            client_storage = FernetEncryptionWrapper(\n                key_value=file_store,\n                fernet=Fernet(key=storage_encryption_key),\n                raise_on_decryption_error=False,\n            )\n\n        self._client_storage: AsyncKeyValue = client_storage\n\n        # Cache HTTPS check to avoid repeated logging\n        self._is_https: bool = str(self.base_url).startswith(\"https://\")\n        if not self._is_https:\n            logger.warning(\n                \"Using non-secure cookies for development; deploy with HTTPS for production.\"\n            )\n\n        self._upstream_token_store: PydanticAdapter[UpstreamTokenSet] = PydanticAdapter[\n            UpstreamTokenSet\n        ](\n            key_value=self._client_storage,\n            pydantic_model=UpstreamTokenSet,\n            default_collection=\"mcp-upstream-tokens\",\n            raise_on_validation_error=True,\n        )\n\n        self._client_store: PydanticAdapter[ProxyDCRClient] = PydanticAdapter[\n            ProxyDCRClient\n        ](\n            key_value=self._client_storage,\n            pydantic_model=ProxyDCRClient,\n            default_collection=\"mcp-oauth-proxy-clients\",\n            raise_on_validation_error=True,\n        )\n\n        # OAuth transaction storage for IdP callback forwarding\n        # Reuse client_storage with different collections for state management\n        self._transaction_store: PydanticAdapter[OAuthTransaction] = PydanticAdapter[\n            OAuthTransaction\n        ](\n            key_value=self._client_storage,\n            pydantic_model=OAuthTransaction,\n            default_collection=\"mcp-oauth-transactions\",\n            raise_on_validation_error=True,\n        )\n\n        self._code_store: PydanticAdapter[ClientCode] = PydanticAdapter[ClientCode](\n            key_value=self._client_storage,\n            pydantic_model=ClientCode,\n            default_collection=\"mcp-authorization-codes\",\n            raise_on_validation_error=True,\n        )\n\n        # Storage for JTI mappings (FastMCP token -> upstream token)\n        self._jti_mapping_store: PydanticAdapter[JTIMapping] = PydanticAdapter[\n            JTIMapping\n        ](\n            key_value=self._client_storage,\n            pydantic_model=JTIMapping,\n            default_collection=\"mcp-jti-mappings\",\n            raise_on_validation_error=True,\n        )\n\n        # Refresh token metadata storage, keyed by token hash for security.\n        # We only store metadata (not the token itself) - if storage is compromised,\n        # attackers get hashes they can't reverse into usable tokens.\n        self._refresh_token_store: PydanticAdapter[RefreshTokenMetadata] = (\n            PydanticAdapter[RefreshTokenMetadata](\n                key_value=self._client_storage,\n                pydantic_model=RefreshTokenMetadata,\n                default_collection=\"mcp-refresh-tokens\",\n                raise_on_validation_error=True,\n            )\n        )\n\n        # Use the provided token validator\n        self._token_validator: TokenVerifier = token_verifier\n\n        # CIMD (Client ID Metadata Document) support\n        self._cimd_manager: CIMDClientManager | None = None\n        if enable_cimd:\n            self._cimd_manager = CIMDClientManager(\n                enable_cimd=True,\n                default_scope=self._default_scope_str,\n                allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,\n            )\n\n        logger.debug(\n            \"Initialized OAuth proxy provider with upstream server %s\",\n            self._upstream_authorization_endpoint,\n        )\n\n    # -------------------------------------------------------------------------\n    # MCP Path Configuration\n    # -------------------------------------------------------------------------\n\n    def set_mcp_path(self, mcp_path: str | None) -> None:\n        \"\"\"Set the MCP endpoint path and create JWTIssuer with correct audience.\n\n        This method is called by get_routes() to configure the resource URL\n        and create the JWTIssuer. The JWT audience is set to the full resource\n        URL (e.g., http://localhost:8000/mcp) to ensure tokens are bound to\n        this specific MCP endpoint.\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n        \"\"\"\n        super().set_mcp_path(mcp_path)\n\n        # Create JWT issuer with correct audience based on actual MCP path\n        # This ensures tokens are bound to the specific resource URL\n        self._jwt_issuer = JWTIssuer(\n            issuer=str(self.base_url),\n            audience=str(self._resource_url),\n            signing_key=self._jwt_signing_key,\n        )\n\n        logger.debug(\"Configured OAuth proxy for resource URL: %s\", self._resource_url)\n\n    @property\n    def jwt_issuer(self) -> JWTIssuer:\n        \"\"\"Get the JWT issuer, ensuring it has been initialized.\n\n        The JWT issuer is created when set_mcp_path() is called (via get_routes()).\n        This property ensures a clear error if used before initialization.\n        \"\"\"\n        if self._jwt_issuer is None:\n            raise RuntimeError(\n                \"JWT issuer not initialized. Ensure get_routes() is called \"\n                \"before token operations.\"\n            )\n        return self._jwt_issuer\n\n    # -------------------------------------------------------------------------\n    # Upstream OAuth Client\n    # -------------------------------------------------------------------------\n\n    def _create_upstream_oauth_client(self) -> AsyncOAuth2Client:\n        \"\"\"Create an OAuth2 client for communicating with the upstream IdP.\n\n        This is the single point for constructing the client used in token\n        exchange, refresh, and other upstream interactions. Subclasses can\n        override this to provide alternative authentication methods (e.g.,\n        managed-identity client assertions instead of a static client secret).\n        \"\"\"\n        return AsyncOAuth2Client(\n            client_id=self._upstream_client_id,\n            client_secret=(\n                self._upstream_client_secret.get_secret_value()\n                if self._upstream_client_secret is not None\n                else None\n            ),\n            token_endpoint_auth_method=self._token_endpoint_auth_method,\n            timeout=HTTP_TIMEOUT_SECONDS,\n        )\n\n    # -------------------------------------------------------------------------\n    # PKCE Helper Methods\n    # -------------------------------------------------------------------------\n\n    def _generate_pkce_pair(self) -> tuple[str, str]:\n        \"\"\"Generate PKCE code verifier and challenge pair.\n\n        Returns:\n            Tuple of (code_verifier, code_challenge) using S256 method\n        \"\"\"\n        # Generate code verifier: 43-128 characters from unreserved set\n        code_verifier = generate_token(48)\n\n        # Generate code challenge using S256 (SHA256 + base64url)\n        challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()\n        code_challenge = urlsafe_b64encode(challenge_bytes).decode().rstrip(\"=\")\n\n        return code_verifier, code_challenge\n\n    # -------------------------------------------------------------------------\n    # Client Registration (Local Implementation)\n    # -------------------------------------------------------------------------\n\n    @override\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        \"\"\"Get client information by ID. This is generally the random ID\n        provided to the DCR client during registration, not the upstream client ID.\n\n        For unregistered clients, returns None (which will raise an error in the SDK).\n        CIMD clients (URL-based client IDs) are looked up and cached automatically.\n        \"\"\"\n        # Load from storage\n        client = await self._client_store.get(key=client_id)\n\n        if client is not None:\n            if client.allowed_redirect_uri_patterns is None:\n                client.allowed_redirect_uri_patterns = (\n                    self._allowed_client_redirect_uris\n                )\n\n            # Refresh CIMD clients using HTTP cache-aware fetcher.\n            if self._cimd_manager is not None and client.cimd_document is not None:\n                try:\n                    refreshed = await self._cimd_manager.get_client(client_id)\n                    if refreshed is not None:\n                        await self._client_store.put(key=client_id, value=refreshed)\n                        return refreshed\n                except Exception as e:\n                    logger.debug(\n                        \"CIMD refresh failed for %s, using cached client: %s\",\n                        client_id,\n                        e,\n                    )\n\n            return client\n\n        # Client not in storage — try CIMD lookup for URL-based client IDs\n        if self._cimd_manager is not None and self._cimd_manager.is_cimd_client_id(\n            client_id\n        ):\n            cimd_client = await self._cimd_manager.get_client(client_id)\n            if cimd_client is not None:\n                await self._client_store.put(key=client_id, value=cimd_client)\n                return cimd_client\n\n        return None\n\n    @override\n    async def register_client(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Register a client locally\n\n        When a client registers, we create a ProxyDCRClient that is more\n        forgiving about validating redirect URIs, since the DCR client's\n        redirect URI will likely be localhost or unknown to the proxied IDP. The\n        proxied IDP only knows about this server's fixed redirect URI.\n        \"\"\"\n\n        # Create a ProxyDCRClient with configured redirect URI validation\n        if client_info.client_id is None:\n            raise ValueError(\"client_id is required for client registration\")\n        # We use token_endpoint_auth_method=\"none\" because the proxy handles\n        # all upstream authentication. The client_secret must also be None\n        # because the SDK requires secrets to be provided if they're set,\n        # regardless of auth method.\n        proxy_client: ProxyDCRClient = ProxyDCRClient(\n            client_id=client_info.client_id,\n            client_secret=None,\n            redirect_uris=client_info.redirect_uris or [AnyUrl(\"http://localhost\")],\n            grant_types=client_info.grant_types\n            or [\"authorization_code\", \"refresh_token\"],\n            scope=client_info.scope or self._default_scope_str,\n            token_endpoint_auth_method=\"none\",\n            allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,\n            client_name=getattr(client_info, \"client_name\", None),\n        )\n\n        await self._client_store.put(\n            key=client_info.client_id,\n            value=proxy_client,\n        )\n\n        # Log redirect URIs to help users discover what patterns they might need\n        if client_info.redirect_uris:\n            for uri in client_info.redirect_uris:\n                logger.debug(\n                    \"Client registered with redirect_uri: %s - if restricting redirect URIs, \"\n                    \"ensure this pattern is allowed in allowed_client_redirect_uris\",\n                    uri,\n                )\n\n        logger.debug(\n            \"Registered client %s with %d redirect URIs\",\n            client_info.client_id,\n            len(proxy_client.redirect_uris) if proxy_client.redirect_uris else 0,\n        )\n\n    # -------------------------------------------------------------------------\n    # Authorization Flow (Proxy to Upstream)\n    # -------------------------------------------------------------------------\n\n    @override\n    async def authorize(\n        self,\n        client: OAuthClientInformationFull,\n        params: AuthorizationParams,\n    ) -> str:\n        \"\"\"Start OAuth transaction and route through consent interstitial.\n\n        Flow:\n        1. Validate client's resource matches server's resource URL (security check)\n        2. Store transaction with client details and PKCE (if forwarding)\n        3. Return local /consent URL; browser visits consent first\n        4. Consent handler redirects to upstream IdP if approved/already approved\n\n        If consent is disabled (require_authorization_consent=False), skip the consent screen\n        and redirect directly to the upstream IdP.\n        \"\"\"\n        # Security check: validate client's requested resource matches this server\n        # This prevents tokens intended for one server from being used on another\n        #\n        # Per RFC 8707, clients may include query parameters in resource URLs (e.g.,\n        # ChatGPT sends ?kb_name=X). We handle two cases:\n        #\n        # 1. Server URL has NO query params: normalize both URLs (strip query/fragment)\n        #    to allow clients like ChatGPT that add query params to still match.\n        #\n        # 2. Server URL HAS query params (e.g., multi-tenant ?tenant=X): require exact\n        #    match to prevent clients from bypassing tenant isolation by changing params.\n        #\n        # Claude doesn't send a resource parameter at all, so this check is skipped.\n        client_resource = getattr(params, \"resource\", None)\n        if client_resource and self._resource_url:\n            server_url = str(self._resource_url)\n            client_url = str(client_resource)\n\n            if _server_url_has_query(server_url):\n                # Server has query params - require exact match for security\n                urls_match = client_url.rstrip(\"/\") == server_url.rstrip(\"/\")\n            else:\n                # Server has no query params - normalize both for comparison\n                urls_match = _normalize_resource_url(\n                    client_url\n                ) == _normalize_resource_url(server_url)\n\n            if not urls_match:\n                logger.warning(\n                    \"Resource mismatch: client requested %s but server is %s\",\n                    client_resource,\n                    self._resource_url,\n                )\n                raise AuthorizeError(\n                    error=\"invalid_target\",  # type: ignore[arg-type]\n                    error_description=\"Resource does not match this server\",\n                )\n\n        # Generate transaction ID for this authorization request\n        txn_id = secrets.token_urlsafe(32)\n\n        # Generate proxy's own PKCE parameters if forwarding is enabled\n        proxy_code_verifier = None\n        proxy_code_challenge = None\n        if self._forward_pkce and params.code_challenge:\n            proxy_code_verifier, proxy_code_challenge = self._generate_pkce_pair()\n            logger.debug(\n                \"Generated proxy PKCE for transaction %s (forwarding client PKCE to upstream)\",\n                txn_id,\n            )\n\n        # Store transaction data for IdP callback processing\n        if client.client_id is None:\n            raise AuthorizeError(\n                error=\"invalid_client\",  # type: ignore[arg-type]  # \"invalid_client\" is valid OAuth error but not in Literal type\n                error_description=\"Client ID is required\",\n            )\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=client.client_id,\n            client_redirect_uri=str(params.redirect_uri),\n            client_state=params.state or \"\",\n            code_challenge=params.code_challenge,\n            code_challenge_method=getattr(params, \"code_challenge_method\", \"S256\"),\n            scopes=params.scopes or [],\n            created_at=time.time(),\n            resource=getattr(params, \"resource\", None),\n            proxy_code_verifier=proxy_code_verifier,\n        )\n        await self._transaction_store.put(\n            key=txn_id,\n            value=transaction,\n            ttl=15 * 60,  # Auto-expire after 15 minutes\n        )\n\n        # If consent is disabled or handled externally, skip consent screen\n        if self._require_authorization_consent is not True:\n            upstream_url = self._build_upstream_authorize_url(\n                txn_id, transaction.model_dump()\n            )\n            logger.debug(\n                \"Starting OAuth transaction %s for client %s, redirecting directly to upstream IdP (consent disabled, PKCE forwarding: %s)\",\n                txn_id,\n                client.client_id,\n                \"enabled\" if proxy_code_challenge else \"disabled\",\n            )\n            return upstream_url\n\n        consent_url = f\"{str(self.base_url).rstrip('/')}/consent?txn_id={txn_id}\"\n\n        logger.debug(\n            \"Starting OAuth transaction %s for client %s, redirecting to consent page (PKCE forwarding: %s)\",\n            txn_id,\n            client.client_id,\n            \"enabled\" if proxy_code_challenge else \"disabled\",\n        )\n        return consent_url\n\n    # -------------------------------------------------------------------------\n    # Authorization Code Handling\n    # -------------------------------------------------------------------------\n\n    @override\n    async def load_authorization_code(\n        self,\n        client: OAuthClientInformationFull,\n        authorization_code: str,\n    ) -> AuthorizationCode | None:\n        \"\"\"Load authorization code for validation.\n\n        Look up our client code and return authorization code object\n        with PKCE challenge for validation.\n        \"\"\"\n        # Look up client code data\n        code_model = await self._code_store.get(key=authorization_code)\n        if not code_model:\n            logger.debug(\"Authorization code not found: %s\", authorization_code)\n            return None\n\n        # Check if code expired\n        if time.time() > code_model.expires_at:\n            logger.debug(\"Authorization code expired: %s\", authorization_code)\n            _ = await self._code_store.delete(key=authorization_code)\n            return None\n\n        # Verify client ID matches\n        if code_model.client_id != client.client_id:\n            logger.debug(\n                \"Authorization code client ID mismatch: %s vs %s\",\n                code_model.client_id,\n                client.client_id,\n            )\n            return None\n\n        # Create authorization code object with PKCE challenge\n        if client.client_id is None:\n            raise AuthorizeError(\n                error=\"invalid_client\",  # type: ignore[arg-type]  # \"invalid_client\" is valid OAuth error but not in Literal type\n                error_description=\"Client ID is required\",\n            )\n        return AuthorizationCode(\n            code=authorization_code,\n            client_id=client.client_id,\n            redirect_uri=AnyUrl(url=code_model.redirect_uri),\n            redirect_uri_provided_explicitly=True,\n            scopes=code_model.scopes,\n            expires_at=code_model.expires_at,\n            code_challenge=code_model.code_challenge or \"\",\n        )\n\n    @override\n    async def exchange_authorization_code(\n        self,\n        client: OAuthClientInformationFull,\n        authorization_code: AuthorizationCode,\n    ) -> OAuthToken:\n        \"\"\"Exchange authorization code for FastMCP-issued tokens.\n\n        Implements the token factory pattern:\n        1. Retrieves upstream tokens from stored authorization code\n        2. Extracts user identity from upstream token\n        3. Encrypts and stores upstream tokens\n        4. Issues FastMCP-signed JWT tokens\n        5. Returns FastMCP tokens (NOT upstream tokens)\n\n        PKCE validation is handled by the MCP framework before this method is called.\n        \"\"\"\n        # Look up stored code data\n        code_model = await self._code_store.get(key=authorization_code.code)\n        if not code_model:\n            logger.error(\n                \"Authorization code not found in client codes: %s\",\n                authorization_code.code,\n            )\n            raise TokenError(\"invalid_grant\", \"Authorization code not found\")\n\n        # Get stored upstream tokens\n        idp_tokens = code_model.idp_tokens\n\n        # Use IdP-granted scopes when available (RFC 6749 §5.1: the IdP MUST\n        # include a scope parameter when the granted scope differs from the\n        # requested scope).  Fall back to requested scopes only when the IdP\n        # omits scope, meaning it granted exactly what was requested.\n        granted_scopes: list[str] = (\n            parse_scopes(idp_tokens[\"scope\"]) or []\n            if \"scope\" in idp_tokens\n            else list(authorization_code.scopes)\n        )\n\n        # Clean up client code (one-time use)\n        await self._code_store.delete(key=authorization_code.code)\n\n        # Generate IDs for token storage\n        upstream_token_id = secrets.token_urlsafe(32)\n        access_jti = secrets.token_urlsafe(32)\n        refresh_jti = (\n            secrets.token_urlsafe(32) if idp_tokens.get(\"refresh_token\") else None\n        )\n\n        # Calculate token expiry times\n        # If upstream provides expires_in, use it. Otherwise use fallback based on:\n        # - User-provided fallback if set\n        # - 1 hour if refresh token available (can refresh when expired)\n        # - 1 year if no refresh token (likely API-key-style token like GitHub OAuth Apps)\n        if \"expires_in\" in idp_tokens:\n            expires_in = int(idp_tokens[\"expires_in\"])\n            logger.debug(\n                \"Access token TTL: %d seconds (from IdP expires_in)\", expires_in\n            )\n        elif self._fallback_access_token_expiry_seconds is not None:\n            expires_in = self._fallback_access_token_expiry_seconds\n            logger.debug(\n                \"Access token TTL: %d seconds (using configured fallback)\", expires_in\n            )\n        elif idp_tokens.get(\"refresh_token\"):\n            expires_in = DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS\n            logger.debug(\n                \"Access token TTL: %d seconds (default, has refresh token)\", expires_in\n            )\n        else:\n            expires_in = DEFAULT_ACCESS_TOKEN_EXPIRY_NO_REFRESH_SECONDS\n            logger.debug(\n                \"Access token TTL: %d seconds (default, no refresh token)\", expires_in\n            )\n\n        # Calculate refresh token expiry if provided by upstream\n        # Some providers include refresh_expires_in, some don't\n        refresh_expires_in = None\n        refresh_token_expires_at = None\n        if idp_tokens.get(\"refresh_token\"):\n            if \"refresh_expires_in\" in idp_tokens and int(\n                idp_tokens[\"refresh_expires_in\"]\n            ):\n                refresh_expires_in = int(idp_tokens[\"refresh_expires_in\"])\n                refresh_token_expires_at = time.time() + refresh_expires_in\n                logger.debug(\n                    \"Upstream refresh token expires in %d seconds\", refresh_expires_in\n                )\n            else:\n                # Default to 30 days if upstream doesn't specify\n                # This is conservative - most providers use longer expiry\n                refresh_expires_in = 60 * 60 * 24 * 30  # 30 days\n                refresh_token_expires_at = time.time() + refresh_expires_in\n                logger.debug(\n                    \"Upstream refresh token expiry unknown, using 30-day default\"\n                )\n\n        # Encrypt and store upstream tokens\n        upstream_token_set = UpstreamTokenSet(\n            upstream_token_id=upstream_token_id,\n            access_token=idp_tokens[\"access_token\"],\n            refresh_token=idp_tokens[\"refresh_token\"]\n            if idp_tokens.get(\"refresh_token\")\n            else None,\n            refresh_token_expires_at=refresh_token_expires_at,\n            expires_at=time.time() + expires_in,\n            token_type=idp_tokens.get(\"token_type\", \"Bearer\"),\n            scope=\" \".join(granted_scopes),\n            client_id=client.client_id or \"\",\n            created_at=time.time(),\n            raw_token_data=idp_tokens,\n        )\n        await self._upstream_token_store.put(\n            key=upstream_token_id,\n            value=upstream_token_set,\n            ttl=max(\n                refresh_expires_in or 0, expires_in, 1\n            ),  # Keep until longest-lived token expires (min 1s for safety)\n        )\n        logger.debug(\"Stored encrypted upstream tokens (jti=%s)\", access_jti[:8])\n\n        # Extract upstream claims to embed in FastMCP JWT (if subclass implements)\n        upstream_claims = await self._extract_upstream_claims(idp_tokens)\n\n        # Issue minimal FastMCP access token (just a reference via JTI)\n        if client.client_id is None:\n            raise TokenError(\"invalid_client\", \"Client ID is required\")\n        fastmcp_access_token = self.jwt_issuer.issue_access_token(\n            client_id=client.client_id,\n            scopes=granted_scopes,\n            jti=access_jti,\n            expires_in=expires_in,\n            upstream_claims=upstream_claims,\n        )\n\n        # Issue minimal FastMCP refresh token if upstream provided one\n        # Use upstream refresh token expiry to align lifetimes\n        fastmcp_refresh_token = None\n        if refresh_jti and refresh_expires_in:\n            fastmcp_refresh_token = self.jwt_issuer.issue_refresh_token(\n                client_id=client.client_id,\n                scopes=granted_scopes,\n                jti=refresh_jti,\n                expires_in=refresh_expires_in,\n                upstream_claims=upstream_claims,\n            )\n\n        # Store JTI mappings\n        await self._jti_mapping_store.put(\n            key=access_jti,\n            value=JTIMapping(\n                jti=access_jti,\n                upstream_token_id=upstream_token_id,\n                created_at=time.time(),\n            ),\n            ttl=expires_in,  # Auto-expire with access token\n        )\n        if refresh_jti:\n            await self._jti_mapping_store.put(\n                key=refresh_jti,\n                value=JTIMapping(\n                    jti=refresh_jti,\n                    upstream_token_id=upstream_token_id,\n                    created_at=time.time(),\n                ),\n                ttl=60 * 60 * 24 * 30,  # Auto-expire with refresh token (30 days)\n            )\n\n        # Store refresh token metadata (keyed by hash for security)\n        if fastmcp_refresh_token and refresh_expires_in:\n            await self._refresh_token_store.put(\n                key=_hash_token(fastmcp_refresh_token),\n                value=RefreshTokenMetadata(\n                    client_id=client.client_id,\n                    scopes=granted_scopes,\n                    expires_at=int(time.time()) + refresh_expires_in,\n                    created_at=time.time(),\n                ),\n                ttl=refresh_expires_in,\n            )\n\n        logger.debug(\n            \"Issued FastMCP tokens for client=%s (access_jti=%s, refresh_jti=%s)\",\n            client.client_id,\n            access_jti[:8],\n            refresh_jti[:8] if refresh_jti else \"none\",\n        )\n\n        # Return FastMCP-issued tokens (NOT upstream tokens!)\n        return OAuthToken(\n            access_token=fastmcp_access_token,\n            token_type=\"Bearer\",\n            expires_in=expires_in,\n            refresh_token=fastmcp_refresh_token,\n            scope=\" \".join(granted_scopes),\n        )\n\n    # -------------------------------------------------------------------------\n    # Refresh Token Flow\n    # -------------------------------------------------------------------------\n\n    def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]:\n        \"\"\"Prepare scopes for initial token exchange (auth code -> tokens).\n\n        Override this method to provide scopes during the authorization\n        code exchange. Some providers (like Azure) require scopes to be sent.\n\n        Args:\n            scopes: Scopes from the authorization request\n\n        Returns:\n            List of scopes to send, or empty list to omit scope parameter\n        \"\"\"\n        return scopes\n\n    def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:\n        \"\"\"Prepare scopes for upstream token refresh request.\n\n        Override this method to transform scopes before sending to upstream provider.\n        For example, Azure needs to prefix scopes and add additional Graph scopes.\n\n        The scopes parameter represents what should be stored in the RefreshToken.\n        This method returns what should be sent to the upstream provider.\n\n        Args:\n            scopes: Base scopes that will be stored in RefreshToken\n\n        Returns:\n            Scopes to send to upstream provider (may be transformed/augmented)\n        \"\"\"\n        return scopes\n\n    async def _extract_upstream_claims(\n        self, idp_tokens: dict[str, Any]\n    ) -> dict[str, Any] | None:\n        \"\"\"Extract upstream claims to embed in FastMCP JWT.\n\n        Override this method to decode upstream tokens, call userinfo endpoints,\n        or otherwise extract claims that should be embedded in the FastMCP JWT\n        issued to MCP clients. This enables gateways to inspect upstream identity\n        information by decoding the JWT without server-side storage lookups.\n\n        Args:\n            idp_tokens: Full token response from upstream provider. Contains\n                access_token, and for OIDC providers may include id_token,\n                refresh_token, and other response fields.\n\n        Returns:\n            Dict of claims to embed in JWT under the \"upstream_claims\" key,\n            or None to not embed any upstream claims.\n\n        Example:\n            For Azure/Entra ID, you might decode the access_token JWT and\n            extract claims like sub, oid, name, preferred_username, email,\n            roles, and groups.\n        \"\"\"\n        _ = idp_tokens\n        return None\n\n    async def load_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: str,\n    ) -> RefreshToken | None:\n        \"\"\"Load refresh token metadata from distributed storage.\n\n        Looks up by token hash and reconstructs the RefreshToken object.\n        Validates that the token belongs to the requesting client.\n        \"\"\"\n        token_hash = _hash_token(refresh_token)\n        metadata = await self._refresh_token_store.get(key=token_hash)\n        if not metadata:\n            return None\n        # Verify token belongs to this client (prevents cross-client token usage)\n        if metadata.client_id != client.client_id:\n            logger.warning(\n                \"Refresh token client_id mismatch: expected %s, got %s\",\n                client.client_id,\n                metadata.client_id,\n            )\n            return None\n        return RefreshToken(\n            token=refresh_token,\n            client_id=metadata.client_id,\n            scopes=metadata.scopes,\n            expires_at=metadata.expires_at,\n        )\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshToken,\n        scopes: list[str],\n    ) -> OAuthToken:\n        \"\"\"Exchange FastMCP refresh token for new FastMCP access token.\n\n        Implements two-tier refresh:\n        1. Verify FastMCP refresh token\n        2. Look up upstream token via JTI mapping\n        3. Refresh upstream token with upstream provider\n        4. Update stored upstream token\n        5. Issue new FastMCP access token\n        6. Keep same FastMCP refresh token (unless upstream rotates)\n        \"\"\"\n        # Verify FastMCP refresh token\n        try:\n            refresh_payload = self.jwt_issuer.verify_token(\n                refresh_token.token, expected_token_use=\"refresh\"\n            )\n            refresh_jti = refresh_payload[\"jti\"]\n        except Exception as e:\n            logger.debug(\"FastMCP refresh token validation failed: %s\", e)\n            raise TokenError(\"invalid_grant\", \"Invalid refresh token\") from e\n\n        # Look up upstream token via JTI mapping\n        jti_mapping = await self._jti_mapping_store.get(key=refresh_jti)\n        if not jti_mapping:\n            logger.error(\"JTI mapping not found for refresh token: %s\", refresh_jti[:8])\n            raise TokenError(\"invalid_grant\", \"Refresh token mapping not found\")\n\n        upstream_token_set = await self._upstream_token_store.get(\n            key=jti_mapping.upstream_token_id\n        )\n        if not upstream_token_set:\n            logger.error(\n                \"Upstream token set not found: %s\", jti_mapping.upstream_token_id[:8]\n            )\n            raise TokenError(\"invalid_grant\", \"Upstream token not found\")\n\n        # Decrypt upstream refresh token\n        if not upstream_token_set.refresh_token:\n            logger.error(\"No upstream refresh token available\")\n            raise TokenError(\"invalid_grant\", \"Refresh not supported for this token\")\n\n        # Refresh upstream token using authlib\n        oauth_client = self._create_upstream_oauth_client()\n\n        # Allow child classes to transform scopes before sending to upstream\n        # This enables provider-specific scope formatting (e.g., Azure prefixing)\n        # while keeping original scopes in storage\n        upstream_scopes = self._prepare_scopes_for_upstream_refresh(scopes)\n\n        try:\n            logger.debug(\"Refreshing upstream token (jti=%s)\", refresh_jti[:8])\n            token_response: dict[str, Any] = await oauth_client.refresh_token(\n                url=self._upstream_token_endpoint,\n                refresh_token=upstream_token_set.refresh_token,\n                scope=\" \".join(upstream_scopes) if upstream_scopes else None,\n                **self._extra_token_params,\n            )\n            logger.debug(\"Successfully refreshed upstream token\")\n        except Exception as e:\n            logger.error(\"Upstream token refresh failed: %s\", e)\n            raise TokenError(\"invalid_grant\", f\"Upstream refresh failed: {e}\") from e\n\n        # Update stored upstream token\n        # In refresh flow, we know there's a refresh token, so default to 1 hour\n        # (user override still applies if set)\n        if \"expires_in\" in token_response:\n            new_expires_in = int(token_response[\"expires_in\"])\n            logger.debug(\n                \"Refreshed access token TTL: %d seconds (from IdP expires_in)\",\n                new_expires_in,\n            )\n        elif self._fallback_access_token_expiry_seconds is not None:\n            new_expires_in = self._fallback_access_token_expiry_seconds\n            logger.debug(\n                \"Refreshed access token TTL: %d seconds (using configured fallback)\",\n                new_expires_in,\n            )\n        else:\n            new_expires_in = DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS\n            logger.debug(\n                \"Refreshed access token TTL: %d seconds (default)\", new_expires_in\n            )\n        upstream_token_set.access_token = token_response[\"access_token\"]\n        upstream_token_set.expires_at = time.time() + new_expires_in\n\n        # Prefer IdP-granted scopes from refresh response (RFC 6749 §5.1)\n        refreshed_scopes: list[str] = (\n            parse_scopes(token_response[\"scope\"]) or []\n            if \"scope\" in token_response\n            else scopes\n        )\n        upstream_token_set.scope = \" \".join(refreshed_scopes)\n\n        # Handle upstream refresh token rotation and expiry\n        new_refresh_expires_in = None\n        if new_upstream_refresh := token_response.get(\"refresh_token\"):\n            if new_upstream_refresh != upstream_token_set.refresh_token:\n                upstream_token_set.refresh_token = new_upstream_refresh\n                logger.debug(\"Upstream refresh token rotated\")\n\n            # Update refresh token expiry if provided\n            if \"refresh_expires_in\" in token_response and int(\n                token_response[\"refresh_expires_in\"]\n            ):\n                new_refresh_expires_in = int(token_response[\"refresh_expires_in\"])\n                upstream_token_set.refresh_token_expires_at = (\n                    time.time() + new_refresh_expires_in\n                )\n                logger.debug(\n                    \"Upstream refresh token expires in %d seconds\",\n                    new_refresh_expires_in,\n                )\n            elif upstream_token_set.refresh_token_expires_at:\n                # Keep existing expiry if upstream doesn't provide new one\n                new_refresh_expires_in = int(\n                    upstream_token_set.refresh_token_expires_at - time.time()\n                )\n            else:\n                # Default to 30 days if unknown\n                new_refresh_expires_in = 60 * 60 * 24 * 30\n                upstream_token_set.refresh_token_expires_at = (\n                    time.time() + new_refresh_expires_in\n                )\n\n        upstream_token_set.raw_token_data = {\n            **upstream_token_set.raw_token_data,\n            **token_response,\n        }\n        # Calculate refresh TTL for storage\n        refresh_ttl = new_refresh_expires_in or (\n            int(upstream_token_set.refresh_token_expires_at - time.time())\n            if upstream_token_set.refresh_token_expires_at\n            else 60 * 60 * 24 * 30  # Default to 30 days if unknown\n        )\n        await self._upstream_token_store.put(\n            key=upstream_token_set.upstream_token_id,\n            value=upstream_token_set,\n            ttl=max(\n                refresh_ttl, new_expires_in, 1\n            ),  # Keep until longest-lived token expires (min 1s for safety)\n        )\n\n        # Re-extract upstream claims from refreshed token response\n        upstream_claims = await self._extract_upstream_claims(\n            upstream_token_set.raw_token_data\n        )\n\n        # Issue new minimal FastMCP access token (just a reference via JTI)\n        if client.client_id is None:\n            raise TokenError(\"invalid_client\", \"Client ID is required\")\n        new_access_jti = secrets.token_urlsafe(32)\n        new_fastmcp_access = self.jwt_issuer.issue_access_token(\n            client_id=client.client_id,\n            scopes=refreshed_scopes,\n            jti=new_access_jti,\n            expires_in=new_expires_in,\n            upstream_claims=upstream_claims,\n        )\n\n        # Store new access token JTI mapping\n        await self._jti_mapping_store.put(\n            key=new_access_jti,\n            value=JTIMapping(\n                jti=new_access_jti,\n                upstream_token_id=upstream_token_set.upstream_token_id,\n                created_at=time.time(),\n            ),\n            ttl=new_expires_in,  # Auto-expire with refreshed access token\n        )\n\n        # Issue NEW minimal FastMCP refresh token (rotation for security)\n        # Use upstream refresh token expiry to align lifetimes\n        new_refresh_jti = secrets.token_urlsafe(32)\n        new_fastmcp_refresh = self.jwt_issuer.issue_refresh_token(\n            client_id=client.client_id,\n            scopes=refreshed_scopes,\n            jti=new_refresh_jti,\n            expires_in=new_refresh_expires_in\n            or 60 * 60 * 24 * 30,  # Fallback to 30 days\n            upstream_claims=upstream_claims,\n        )\n\n        # Store new refresh token JTI mapping with aligned expiry\n        # (reuse refresh_ttl calculated above for upstream token store)\n        await self._jti_mapping_store.put(\n            key=new_refresh_jti,\n            value=JTIMapping(\n                jti=new_refresh_jti,\n                upstream_token_id=upstream_token_set.upstream_token_id,\n                created_at=time.time(),\n            ),\n            ttl=refresh_ttl,  # Align with upstream refresh token expiry\n        )\n\n        # Invalidate old refresh token (refresh token rotation - enforces one-time use)\n        await self._jti_mapping_store.delete(key=refresh_jti)\n        logger.debug(\n            \"Rotated refresh token (old JTI invalidated - one-time use enforced)\"\n        )\n\n        # Store new refresh token metadata (keyed by hash)\n        await self._refresh_token_store.put(\n            key=_hash_token(new_fastmcp_refresh),\n            value=RefreshTokenMetadata(\n                client_id=client.client_id,\n                scopes=refreshed_scopes,\n                expires_at=int(time.time()) + refresh_ttl,\n                created_at=time.time(),\n            ),\n            ttl=refresh_ttl,\n        )\n\n        # Delete old refresh token (by hash)\n        await self._refresh_token_store.delete(key=_hash_token(refresh_token.token))\n\n        logger.info(\n            \"Issued new FastMCP tokens (rotated refresh) for client=%s (access_jti=%s, refresh_jti=%s)\",\n            client.client_id,\n            new_access_jti[:8],\n            new_refresh_jti[:8],\n        )\n\n        # Return new FastMCP tokens (both access AND refresh are new)\n        return OAuthToken(\n            access_token=new_fastmcp_access,\n            token_type=\"Bearer\",\n            expires_in=new_expires_in,\n            refresh_token=new_fastmcp_refresh,  # NEW refresh token (rotated)\n            scope=\" \".join(refreshed_scopes),\n        )\n\n    # -------------------------------------------------------------------------\n    # Token Validation\n    # -------------------------------------------------------------------------\n\n    def _get_verification_token(\n        self, upstream_token_set: UpstreamTokenSet\n    ) -> str | None:\n        \"\"\"Get the token string to pass to the token verifier.\n\n        Returns the upstream access token by default. Subclasses can override\n        to verify a different token (e.g., the OIDC id_token for providers\n        that issue opaque access tokens).\n        \"\"\"\n        return upstream_token_set.access_token\n\n    def _uses_alternate_verification(self) -> bool:\n        \"\"\"Whether this provider verifies a different token than the access token.\n\n        When True, ``load_access_token`` patches the validated result with\n        the upstream access token, scopes, and expiry so that the returned\n        ``AccessToken`` reflects the access token rather than the\n        verification token.\n\n        The default implementation compares token values, but subclasses\n        should override this to use an intent-based flag so the patch is\n        applied even when the verification token and access token happen to\n        carry the same value (e.g., some OIDC providers issue identical\n        JWTs for both).\n        \"\"\"\n        return False\n\n    async def load_access_token(self, token: str) -> AccessToken | None:  # type: ignore[override]\n        \"\"\"Validate FastMCP JWT by swapping for upstream token.\n\n        This implements the token swap pattern:\n        1. Verify FastMCP JWT signature (proves it's our token)\n        2. Look up upstream token via JTI mapping\n        3. Decrypt upstream token\n        4. Validate upstream token with provider (GitHub API, JWT validation, etc.)\n        5. Return upstream validation result\n\n        The FastMCP JWT is a reference token - all authorization data comes\n        from validating the upstream token via the TokenVerifier.\n        \"\"\"\n        try:\n            # 1. Verify FastMCP JWT signature and claims\n            payload = self.jwt_issuer.verify_token(token)\n            jti = payload[\"jti\"]\n\n            # 2. Look up upstream token via JTI mapping\n            jti_mapping = await self._jti_mapping_store.get(key=jti)\n            if not jti_mapping:\n                logger.info(\n                    \"JTI mapping not found (token may have expired): jti=%s...\",\n                    jti[:16],\n                )\n                return None\n\n            upstream_token_set = await self._upstream_token_store.get(\n                key=jti_mapping.upstream_token_id\n            )\n            if not upstream_token_set:\n                logger.debug(\n                    \"Upstream token not found: %s\", jti_mapping.upstream_token_id\n                )\n                return None\n\n            # 3. Validate with upstream provider (delegated to TokenVerifier)\n            # This calls the real token validator (GitHub API, JWKS, etc.)\n            verification_token = self._get_verification_token(upstream_token_set)\n            if verification_token is None:\n                logger.debug(\"No verification token available\")\n                return None\n            validated = await self._token_validator.verify_token(verification_token)\n\n            if not validated:\n                logger.debug(\"Upstream token validation failed\")\n                return None\n\n            # When alternate verification is in use (e.g., id_token\n            # verification in OIDCProxy), ensure the returned AccessToken\n            # carries the upstream access token and its scopes, not the\n            # verification token's values.  We use an intent-based check\n            # rather than value equality because some IdPs issue identical\n            # JWTs for both access_token and id_token, which would cause\n            # the scope patch to be skipped even though it's needed.\n            if self._uses_alternate_verification():\n                validated = validated.model_copy(\n                    update={\n                        \"token\": upstream_token_set.access_token,\n                        \"scopes\": upstream_token_set.scope.split()\n                        if upstream_token_set.scope\n                        else validated.scopes,\n                        \"expires_at\": int(upstream_token_set.expires_at),\n                    }\n                )\n\n            logger.debug(\n                \"Token swap successful for JTI=%s (upstream validated)\", jti[:8]\n            )\n            return validated\n\n        except Exception as e:\n            logger.debug(\"Token swap validation failed: %s\", e)\n            return None\n\n    # -------------------------------------------------------------------------\n    # Token Revocation\n    # -------------------------------------------------------------------------\n\n    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:\n        \"\"\"Revoke token locally and with upstream server if supported.\n\n        For refresh tokens, removes from local storage by hash.\n        For all tokens, attempts upstream revocation if endpoint is configured.\n        Access token JTI mappings expire via TTL.\n        \"\"\"\n        # For refresh tokens, delete from local storage by hash\n        if isinstance(token, RefreshToken):\n            await self._refresh_token_store.delete(key=_hash_token(token.token))\n\n        # Attempt upstream revocation if endpoint is configured\n        if self._upstream_revocation_endpoint:\n            try:\n                async with httpx.AsyncClient(\n                    timeout=HTTP_TIMEOUT_SECONDS\n                ) as http_client:\n                    revocation_data: dict[str, str] = {\"token\": token.token}\n                    request_kwargs: dict[str, Any] = {\"data\": revocation_data}\n\n                    # Use the factory method when available (supports alternative auth like\n                    # client assertions for managed identity), falling back to basic auth\n                    # or client_id-only for public clients per RFC 7009\n                    oauth_client = self._create_upstream_oauth_client()\n                    if oauth_client.client_secret is not None:\n                        # Client secret is available, use HTTP Basic auth\n                        request_kwargs[\"auth\"] = (\n                            self._upstream_client_id,\n                            oauth_client.client_secret,\n                        )\n                    else:\n                        # No secret; public client must still identify itself per RFC 7009\n                        revocation_data[\"client_id\"] = self._upstream_client_id\n\n                    await http_client.post(\n                        self._upstream_revocation_endpoint,\n                        **request_kwargs,\n                    )\n                    logger.debug(\"Successfully revoked token with upstream server\")\n            except Exception as e:\n                logger.warning(\"Failed to revoke token with upstream server: %s\", e)\n        else:\n            logger.debug(\"No upstream revocation endpoint configured\")\n\n        logger.debug(\"Token revoked successfully\")\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get OAuth routes with custom handlers for better error UX.\n\n        This method creates standard OAuth routes and replaces:\n        - /authorize endpoint: Enhanced error responses for unregistered clients\n        - /token endpoint: OAuth 2.1 compliant error codes\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata.\n        \"\"\"\n        # Get standard OAuth routes from parent class\n        # Note: parent already replaces /token with TokenHandler for proper error codes\n        routes = super().get_routes(mcp_path)\n        custom_routes = []\n\n        logger.debug(\n            f\"get_routes called - configuring OAuth routes in {len(routes)} routes\"\n        )\n\n        for i, route in enumerate(routes):\n            logger.debug(\n                f\"Route {i}: {route} - path: {getattr(route, 'path', 'N/A')}, methods: {getattr(route, 'methods', 'N/A')}\"\n            )\n\n            # Replace the authorize endpoint with our enhanced handler for better error UX\n            if (\n                isinstance(route, Route)\n                and route.path == \"/authorize\"\n                and route.methods is not None\n                and (\"GET\" in route.methods or \"POST\" in route.methods)\n            ):\n                # Replace with our enhanced authorization handler\n                # Note: self.base_url is guaranteed to be set in parent __init__\n                authorize_handler = AuthorizationHandler(\n                    provider=self,\n                    base_url=self.base_url,  # ty: ignore[invalid-argument-type]\n                    server_name=None,  # Could be extended to pass server metadata\n                    server_icon_url=None,\n                )\n                custom_routes.append(\n                    Route(\n                        path=\"/authorize\",\n                        endpoint=authorize_handler.handle,\n                        methods=[\"GET\", \"POST\"],\n                    )\n                )\n            elif (\n                self._cimd_manager is not None\n                and isinstance(route, Route)\n                and route.path == \"/token\"\n                and route.methods is not None\n                and \"POST\" in route.methods\n            ):\n                # Replace the token endpoint authenticator with one that supports\n                # private_key_jwt for CIMD clients\n                token_endpoint_url = f\"{self.base_url}/token\"\n                cimd_authenticator = PrivateKeyJWTClientAuthenticator(\n                    provider=self,\n                    cimd_manager=self._cimd_manager,\n                    token_endpoint_url=token_endpoint_url,\n                )\n                token_handler = TokenHandler(\n                    provider=self, client_authenticator=cimd_authenticator\n                )\n                custom_routes.append(\n                    Route(\n                        path=\"/token\",\n                        endpoint=cors_middleware(\n                            token_handler.handle, [\"POST\", \"OPTIONS\"]\n                        ),\n                        methods=[\"POST\", \"OPTIONS\"],\n                    )\n                )\n            elif (\n                self._cimd_manager is not None\n                and isinstance(route, Route)\n                and route.path.startswith(\"/.well-known/oauth-authorization-server\")\n            ):\n                client_registration_options = (\n                    self.client_registration_options or ClientRegistrationOptions()\n                )\n                revocation_options = self.revocation_options or RevocationOptions()\n                metadata = build_metadata(\n                    self.base_url,  # ty: ignore[invalid-argument-type]\n                    self.service_documentation_url,\n                    client_registration_options,\n                    revocation_options,\n                )\n                metadata.client_id_metadata_document_supported = True\n                handler = MetadataHandler(metadata)\n                methods = route.methods or [\"GET\", \"OPTIONS\"]\n\n                custom_routes.append(\n                    Route(\n                        path=route.path,\n                        endpoint=cors_middleware(handler.handle, [\"GET\", \"OPTIONS\"]),\n                        methods=methods,\n                        name=route.name,\n                        include_in_schema=route.include_in_schema,\n                    )\n                )\n            else:\n                # Keep all other standard OAuth routes unchanged\n                custom_routes.append(route)\n\n        # Add OAuth callback endpoint for forwarding to client callbacks\n        custom_routes.append(\n            Route(\n                path=self._redirect_path,\n                endpoint=self._handle_idp_callback,\n                methods=[\"GET\"],\n            )\n        )\n\n        # Add consent endpoints\n        # Handle both GET (show page) and POST (submit) at /consent\n        custom_routes.append(\n            Route(\n                path=\"/consent\", endpoint=self._handle_consent, methods=[\"GET\", \"POST\"]\n            )\n        )\n\n        return custom_routes\n\n    # -------------------------------------------------------------------------\n    # IdP Callback Forwarding\n    # -------------------------------------------------------------------------\n\n    async def _handle_idp_callback(\n        self, request: Request\n    ) -> HTMLResponse | RedirectResponse:\n        \"\"\"Handle callback from upstream IdP and forward to client.\n\n        This implements the DCR-compliant callback forwarding:\n        1. Receive IdP callback with code and txn_id as state\n        2. Exchange IdP code for tokens (server-side)\n        3. Generate our own client code bound to PKCE challenge\n        4. Redirect to client's callback with client code and original state\n        \"\"\"\n        try:\n            idp_code = request.query_params.get(\"code\")\n            txn_id = request.query_params.get(\"state\")\n            error = request.query_params.get(\"error\")\n\n            if error:\n                error_description = request.query_params.get(\"error_description\")\n                logger.error(\n                    \"IdP callback error: %s - %s\",\n                    error,\n                    error_description,\n                )\n                # Show error page to user\n                html_content = create_error_html(\n                    error_title=\"OAuth Error\",\n                    error_message=f\"Authentication failed: {error_description or 'Unknown error'}\",\n                    error_details={\"Error Code\": error} if error else None,\n                )\n                return HTMLResponse(content=html_content, status_code=400)\n\n            if not idp_code or not txn_id:\n                logger.error(\"IdP callback missing code or transaction ID\")\n                html_content = create_error_html(\n                    error_title=\"OAuth Error\",\n                    error_message=\"Missing authorization code or transaction ID from the identity provider.\",\n                )\n                return HTMLResponse(content=html_content, status_code=400)\n\n            # Look up transaction data\n            transaction_model = await self._transaction_store.get(key=txn_id)\n            if not transaction_model:\n                logger.error(\"IdP callback with invalid transaction ID: %s\", txn_id)\n                html_content = create_error_html(\n                    error_title=\"OAuth Error\",\n                    error_message=\"Invalid or expired authorization transaction. Please try authenticating again.\",\n                )\n                return HTMLResponse(content=html_content, status_code=400)\n            # Verify consent binding cookie to prevent confused deputy attacks.\n            # When consent is enabled, the browser that approved consent receives\n            # a signed cookie. A different browser (e.g., a victim lured to the\n            # IdP URL) won't have this cookie and will be rejected.\n            if self._require_authorization_consent is True:\n                consent_token = transaction_model.consent_token\n                if not consent_token:\n                    logger.error(\"Transaction %s missing consent_token\", txn_id)\n                    html_content = create_error_html(\n                        error_title=\"Authorization Error\",\n                        error_message=\"Invalid authorization flow. Please try authenticating again.\",\n                    )\n                    return HTMLResponse(content=html_content, status_code=403)\n\n                if not self._verify_consent_binding_cookie(\n                    request, txn_id, consent_token\n                ):\n                    logger.warning(\n                        \"Consent binding cookie missing or invalid for transaction %s \"\n                        \"(possible confused deputy attack)\",\n                        txn_id,\n                    )\n                    html_content = create_error_html(\n                        error_title=\"Authorization Error\",\n                        error_message=(\n                            \"Authorization session mismatch. This can happen if you \"\n                            \"followed a link from another person or your session expired. \"\n                            \"Please try authenticating again.\"\n                        ),\n                    )\n                    return HTMLResponse(content=html_content, status_code=403)\n\n            transaction = transaction_model.model_dump()\n\n            # Exchange IdP code for tokens (server-side)\n            oauth_client = self._create_upstream_oauth_client()\n\n            try:\n                idp_redirect_uri = (\n                    f\"{str(self.base_url).rstrip('/')}{self._redirect_path}\"\n                )\n                logger.debug(\n                    f\"Exchanging IdP code for tokens with redirect_uri: {idp_redirect_uri}\"\n                )\n\n                # Build token exchange parameters\n                token_params = {\n                    \"url\": self._upstream_token_endpoint,\n                    \"code\": idp_code,\n                    \"redirect_uri\": idp_redirect_uri,\n                }\n\n                # Include proxy's code_verifier if we forwarded PKCE\n                proxy_code_verifier = transaction.get(\"proxy_code_verifier\")\n                if proxy_code_verifier:\n                    token_params[\"code_verifier\"] = proxy_code_verifier\n                    logger.debug(\n                        \"Including proxy code_verifier in token exchange for transaction %s\",\n                        txn_id,\n                    )\n\n                # Allow providers to specify scope for token exchange\n                exchange_scopes = self._prepare_scopes_for_token_exchange(\n                    transaction.get(\"scopes\") or []\n                )\n                if exchange_scopes:\n                    token_params[\"scope\"] = \" \".join(exchange_scopes)\n\n                # Add any extra token parameters configured for this proxy\n                if self._extra_token_params:\n                    token_params.update(self._extra_token_params)\n                    logger.debug(\n                        \"Adding extra token parameters for transaction %s: %s\",\n                        txn_id,\n                        list(self._extra_token_params.keys()),\n                    )\n\n                idp_tokens: dict[str, Any] = await oauth_client.fetch_token(\n                    **token_params\n                )\n\n                logger.debug(\n                    f\"Successfully exchanged IdP code for tokens (transaction: {txn_id}, PKCE: {bool(proxy_code_verifier)})\"\n                )\n                logger.debug(\n                    \"IdP token response: expires_in=%s, has_refresh_token=%s\",\n                    idp_tokens.get(\"expires_in\"),\n                    \"refresh_token\" in idp_tokens,\n                )\n\n            except Exception as e:\n                logger.error(\"IdP token exchange failed: %s\", e)\n                html_content = create_error_html(\n                    error_title=\"OAuth Error\",\n                    error_message=f\"Token exchange with identity provider failed: {e}\",\n                )\n                return HTMLResponse(content=html_content, status_code=500)\n\n            # Generate our own authorization code for the client\n            client_code = secrets.token_urlsafe(32)\n            code_expires_at = int(time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS)\n\n            # Store client code with PKCE challenge and IdP tokens\n            await self._code_store.put(\n                key=client_code,\n                value=ClientCode(\n                    code=client_code,\n                    client_id=transaction[\"client_id\"],\n                    redirect_uri=transaction[\"client_redirect_uri\"],\n                    code_challenge=transaction[\"code_challenge\"],\n                    code_challenge_method=transaction[\"code_challenge_method\"],\n                    scopes=transaction[\"scopes\"],\n                    idp_tokens=idp_tokens,\n                    expires_at=code_expires_at,\n                    created_at=time.time(),\n                ),\n                ttl=DEFAULT_AUTH_CODE_EXPIRY_SECONDS,  # Auto-expire after 5 minutes\n            )\n\n            # Clean up transaction\n            await self._transaction_store.delete(key=txn_id)\n\n            # Build client callback URL with our code and original state\n            client_redirect_uri = transaction[\"client_redirect_uri\"]\n            client_state = transaction[\"client_state\"]\n\n            callback_params = {\n                \"code\": client_code,\n                \"state\": client_state,\n            }\n\n            # Add query parameters to client redirect URI\n            separator = \"&\" if \"?\" in client_redirect_uri else \"?\"\n            client_callback_url = (\n                f\"{client_redirect_uri}{separator}{urlencode(callback_params)}\"\n            )\n\n            logger.debug(f\"Forwarding to client callback for transaction {txn_id}\")\n\n            response = RedirectResponse(url=client_callback_url, status_code=302)\n            self._clear_consent_binding_cookie(request, response, txn_id)\n            return response\n\n        except Exception as e:\n            logger.error(\"Error in IdP callback handler: %s\", e, exc_info=True)\n            html_content = create_error_html(\n                error_title=\"OAuth Error\",\n                error_message=\"Internal server error during OAuth callback processing. Please try again.\",\n            )\n            return HTMLResponse(content=html_content, status_code=500)\n"
  },
  {
    "path": "src/fastmcp/server/auth/oauth_proxy/ui.py",
    "content": "\"\"\"OAuth Proxy UI Generation Functions.\n\nThis module contains HTML generation functions for consent and error pages.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastmcp.utilities.ui import (\n    BUTTON_STYLES,\n    DETAIL_BOX_STYLES,\n    DETAILS_STYLES,\n    INFO_BOX_STYLES,\n    REDIRECT_SECTION_STYLES,\n    TOOLTIP_STYLES,\n    create_logo,\n    create_page,\n)\n\n\ndef create_consent_html(\n    client_id: str,\n    redirect_uri: str,\n    scopes: list[str],\n    txn_id: str,\n    csrf_token: str,\n    client_name: str | None = None,\n    title: str = \"Application Access Request\",\n    server_name: str | None = None,\n    server_icon_url: str | None = None,\n    server_website_url: str | None = None,\n    client_website_url: str | None = None,\n    csp_policy: str | None = None,\n    is_cimd_client: bool = False,\n    cimd_domain: str | None = None,\n) -> str:\n    \"\"\"Create a styled HTML consent page for OAuth authorization requests.\n\n    Args:\n        csp_policy: Content Security Policy override.\n            If None, uses the built-in CSP policy with appropriate directives.\n            If empty string \"\", disables CSP entirely (no meta tag is rendered).\n            If a non-empty string, uses that as the CSP policy value.\n    \"\"\"\n    import html as html_module\n\n    client_display = html_module.escape(client_name or client_id)\n    server_name_escaped = html_module.escape(server_name or \"FastMCP\")\n\n    # Make server name a hyperlink if website URL is available\n    if server_website_url:\n        website_url_escaped = html_module.escape(server_website_url)\n        server_display = f'<a href=\"{website_url_escaped}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"server-name-link\">{server_name_escaped}</a>'\n    else:\n        server_display = server_name_escaped\n\n    # Build intro box with call-to-action\n    intro_box = f\"\"\"\n        <div class=\"info-box\">\n            <p>The application <strong>{client_display}</strong> wants to access the MCP server <strong>{server_display}</strong>. Please ensure you recognize the callback address below.</p>\n        </div>\n    \"\"\"\n\n    # Build CIMD verified domain badge if applicable\n    cimd_badge = \"\"\n    if is_cimd_client and cimd_domain:\n        cimd_domain_escaped = html_module.escape(cimd_domain)\n        cimd_badge = f\"\"\"\n        <div class=\"cimd-badge\">\n            <span class=\"cimd-check\">&#x2713;</span>\n            Verified domain: <strong>{cimd_domain_escaped}</strong>\n        </div>\n        \"\"\"\n\n    # Build redirect URI section (yellow box, centered)\n    redirect_uri_escaped = html_module.escape(redirect_uri)\n    redirect_section = f\"\"\"\n        <div class=\"redirect-section\">\n            <span class=\"label\">Credentials will be sent to:</span>\n            <div class=\"value\">{redirect_uri_escaped}</div>\n        </div>\n    \"\"\"\n\n    # Build advanced details with collapsible section\n    detail_rows = [\n        (\"Application Name\", html_module.escape(client_name or client_id)),\n        (\"Application Website\", html_module.escape(client_website_url or \"N/A\")),\n        (\"Application ID\", html_module.escape(client_id)),\n        (\"Redirect URI\", redirect_uri_escaped),\n        (\n            \"Requested Scopes\",\n            \", \".join(html_module.escape(s) for s in scopes) if scopes else \"None\",\n        ),\n    ]\n\n    detail_rows_html = \"\\n\".join(\n        [\n            f\"\"\"\n        <div class=\"detail-row\">\n            <div class=\"detail-label\">{label}:</div>\n            <div class=\"detail-value\">{value}</div>\n        </div>\n        \"\"\"\n            for label, value in detail_rows\n        ]\n    )\n\n    advanced_details = f\"\"\"\n        <details>\n            <summary>Advanced Details</summary>\n            <div class=\"detail-box\">\n                {detail_rows_html}\n            </div>\n        </details>\n    \"\"\"\n\n    # Build form with buttons\n    # Use empty action to submit to current URL (/consent or /mcp/consent)\n    # The POST handler is registered at the same path as GET\n    form = f\"\"\"\n        <form id=\"consentForm\" method=\"POST\" action=\"\">\n            <input type=\"hidden\" name=\"txn_id\" value=\"{txn_id}\" />\n            <input type=\"hidden\" name=\"csrf_token\" value=\"{csrf_token}\" />\n            <input type=\"hidden\" name=\"submit\" value=\"true\" />\n            <div class=\"button-group\">\n                <button type=\"submit\" name=\"action\" value=\"approve\" class=\"btn-approve\">Allow Access</button>\n                <button type=\"submit\" name=\"action\" value=\"deny\" class=\"btn-deny\">Deny</button>\n            </div>\n        </form>\n    \"\"\"\n\n    # Build help link with tooltip (identical to current implementation)\n    help_link = \"\"\"\n        <div class=\"help-link-container\">\n            <span class=\"help-link\">\n                Why am I seeing this?\n                <span class=\"tooltip\">\n                    This FastMCP server requires your consent to allow a new client\n                    to connect. This protects you from <a\n                    href=\"https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem\"\n                    target=\"_blank\" class=\"tooltip-link\">confused deputy\n                    attacks</a>, where malicious clients could impersonate you\n                    and steal access.<br><br>\n                    <a\n                    href=\"https://gofastmcp.com/servers/auth/oauth-proxy#confused-deputy-attacks\"\n                    target=\"_blank\" class=\"tooltip-link\">Learn more about\n                    FastMCP security →</a>\n                </span>\n            </span>\n        </div>\n    \"\"\"\n\n    # Build the page content\n    content = f\"\"\"\n        <div class=\"container\">\n            {create_logo(icon_url=server_icon_url, alt_text=server_name or \"FastMCP\")}\n            <h1>Application Access Request</h1>\n            {intro_box}\n            {cimd_badge}\n            {redirect_section}\n            {advanced_details}\n            {form}\n        </div>\n        {help_link}\n    \"\"\"\n\n    # Additional styles needed for this page\n    cimd_badge_styles = \"\"\"\n        .cimd-badge {\n            background: #ecfdf5;\n            border: 1px solid #6ee7b7;\n            border-radius: 8px;\n            padding: 8px 16px;\n            margin-bottom: 16px;\n            font-size: 14px;\n            color: #065f46;\n            text-align: center;\n        }\n        .cimd-check {\n            color: #059669;\n            font-weight: bold;\n            margin-right: 4px;\n        }\n    \"\"\"\n    additional_styles = (\n        INFO_BOX_STYLES\n        + REDIRECT_SECTION_STYLES\n        + DETAILS_STYLES\n        + DETAIL_BOX_STYLES\n        + BUTTON_STYLES\n        + TOOLTIP_STYLES\n        + cimd_badge_styles\n    )\n\n    # Determine CSP policy to use\n    # If csp_policy is None, build the default CSP policy\n    # If csp_policy is empty string, CSP will be disabled entirely in create_page\n    # If csp_policy is a non-empty string, use it as-is\n    if csp_policy is None:\n        # The consent form posts to itself (action=\"\") and all subsequent redirects\n        # are server-controlled. Chrome enforces form-action across the entire redirect\n        # chain (Chromium issue #40923007), which breaks flows where an HTTPS callback\n        # internally redirects to a custom scheme (e.g., claude:// or cursor://).\n        # Since the form target is same-origin and we control the redirect chain,\n        # omitting form-action is safe and avoids these browser-specific CSP issues.\n        csp_policy = \"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'\"\n\n    return create_page(\n        content=content,\n        title=title,\n        additional_styles=additional_styles,\n        csp_policy=csp_policy,\n    )\n\n\ndef create_error_html(\n    error_title: str,\n    error_message: str,\n    error_details: dict[str, str] | None = None,\n    server_name: str | None = None,\n    server_icon_url: str | None = None,\n) -> str:\n    \"\"\"Create a styled HTML error page for OAuth errors.\n\n    Args:\n        error_title: The error title (e.g., \"OAuth Error\", \"Authorization Failed\")\n        error_message: The main error message to display\n        error_details: Optional dictionary of error details to show (e.g., `{\"Error Code\": \"invalid_client\"}`)\n        server_name: Optional server name to display\n        server_icon_url: Optional URL to server icon/logo\n\n    Returns:\n        Complete HTML page as a string\n    \"\"\"\n    import html as html_module\n\n    error_message_escaped = html_module.escape(error_message)\n\n    # Build error message box\n    error_box = f\"\"\"\n        <div class=\"info-box error\">\n            <p>{error_message_escaped}</p>\n        </div>\n    \"\"\"\n\n    # Build error details section if provided\n    details_section = \"\"\n    if error_details:\n        detail_rows_html = \"\\n\".join(\n            [\n                f\"\"\"\n            <div class=\"detail-row\">\n                <div class=\"detail-label\">{html_module.escape(label)}:</div>\n                <div class=\"detail-value\">{html_module.escape(value)}</div>\n            </div>\n            \"\"\"\n                for label, value in error_details.items()\n            ]\n        )\n\n        details_section = f\"\"\"\n            <details>\n                <summary>Error Details</summary>\n                <div class=\"detail-box\">\n                    {detail_rows_html}\n                </div>\n            </details>\n        \"\"\"\n\n    # Build the page content\n    content = f\"\"\"\n        <div class=\"container\">\n            {create_logo(icon_url=server_icon_url, alt_text=server_name or \"FastMCP\")}\n            <h1>{html_module.escape(error_title)}</h1>\n            {error_box}\n            {details_section}\n        </div>\n    \"\"\"\n\n    # Additional styles needed for this page\n    # Override .info-box.error to use normal text color instead of red\n    additional_styles = (\n        INFO_BOX_STYLES\n        + DETAILS_STYLES\n        + DETAIL_BOX_STYLES\n        + \"\"\"\n        .info-box.error {\n            color: #111827;\n        }\n        \"\"\"\n    )\n\n    # Simple CSP policy for error pages (no forms needed)\n    csp_policy = \"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'\"\n\n    return create_page(\n        content=content,\n        title=error_title,\n        additional_styles=additional_styles,\n        csp_policy=csp_policy,\n    )\n"
  },
  {
    "path": "src/fastmcp/server/auth/oidc_proxy.py",
    "content": "\"\"\"OIDC Proxy Provider for FastMCP.\n\nThis provider acts as a transparent proxy to an upstream OIDC compliant Authorization\nServer. It leverages the OAuthProxy class to handle Dynamic Client Registration and\nforwarding of all OAuth flows.\n\nThis implementation is based on:\n    OpenID Connect Discovery 1.0 - https://openid.net/specs/openid-connect-discovery-1_0.html\n    OAuth 2.0 Authorization Server Metadata - https://datatracker.ietf.org/doc/html/rfc8414\n\"\"\"\n\nfrom collections.abc import Sequence\nfrom typing import Literal\n\nimport httpx\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl, BaseModel, model_validator\nfrom typing_extensions import Self\n\nfrom fastmcp.server.auth import TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.models import UpstreamTokenSet\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass OIDCConfiguration(BaseModel):\n    \"\"\"OIDC Configuration.\n\n    See:\n        https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata\n        https://datatracker.ietf.org/doc/html/rfc8414#section-2\n    \"\"\"\n\n    strict: bool = True\n\n    # OpenID Connect Discovery 1.0\n    issuer: AnyHttpUrl | str | None = None  # Strict\n\n    authorization_endpoint: AnyHttpUrl | str | None = None  # Strict\n    token_endpoint: AnyHttpUrl | str | None = None  # Strict\n    userinfo_endpoint: AnyHttpUrl | str | None = None\n\n    jwks_uri: AnyHttpUrl | str | None = None  # Strict\n\n    registration_endpoint: AnyHttpUrl | str | None = None\n\n    scopes_supported: Sequence[str] | None = None\n\n    response_types_supported: Sequence[str] | None = None  # Strict\n    response_modes_supported: Sequence[str] | None = None\n\n    grant_types_supported: Sequence[str] | None = None\n\n    acr_values_supported: Sequence[str] | None = None\n\n    subject_types_supported: Sequence[str] | None = None  # Strict\n\n    id_token_signing_alg_values_supported: Sequence[str] | None = None  # Strict\n    id_token_encryption_alg_values_supported: Sequence[str] | None = None\n    id_token_encryption_enc_values_supported: Sequence[str] | None = None\n\n    userinfo_signing_alg_values_supported: Sequence[str] | None = None\n    userinfo_encryption_alg_values_supported: Sequence[str] | None = None\n    userinfo_encryption_enc_values_supported: Sequence[str] | None = None\n\n    request_object_signing_alg_values_supported: Sequence[str] | None = None\n    request_object_encryption_alg_values_supported: Sequence[str] | None = None\n    request_object_encryption_enc_values_supported: Sequence[str] | None = None\n\n    token_endpoint_auth_methods_supported: Sequence[str] | None = None\n    token_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = None\n\n    display_values_supported: Sequence[str] | None = None\n\n    claim_types_supported: Sequence[str] | None = None\n    claims_supported: Sequence[str] | None = None\n\n    service_documentation: AnyHttpUrl | str | None = None\n\n    claims_locales_supported: Sequence[str] | None = None\n    ui_locales_supported: Sequence[str] | None = None\n\n    claims_parameter_supported: bool | None = None\n    request_parameter_supported: bool | None = None\n    request_uri_parameter_supported: bool | None = None\n\n    require_request_uri_registration: bool | None = None\n\n    op_policy_uri: AnyHttpUrl | str | None = None\n    op_tos_uri: AnyHttpUrl | str | None = None\n\n    # OAuth 2.0 Authorization Server Metadata\n    revocation_endpoint: AnyHttpUrl | str | None = None\n    revocation_endpoint_auth_methods_supported: Sequence[str] | None = None\n    revocation_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = None\n\n    introspection_endpoint: AnyHttpUrl | str | None = None\n    introspection_endpoint_auth_methods_supported: Sequence[str] | None = None\n    introspection_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = (\n        None\n    )\n\n    code_challenge_methods_supported: Sequence[str] | None = None\n\n    signed_metadata: str | None = None\n\n    @model_validator(mode=\"after\")\n    def _enforce_strict(self) -> Self:\n        \"\"\"Enforce strict rules.\"\"\"\n        if not self.strict:\n            return self\n\n        def enforce(attr: str, is_url: bool = False) -> None:\n            value = getattr(self, attr, None)\n            if not value:\n                message = f\"Missing required configuration metadata: {attr}\"\n                logger.error(message)\n                raise ValueError(message)\n\n            if not is_url or isinstance(value, AnyHttpUrl):\n                return\n\n            try:\n                AnyHttpUrl(value)\n            except Exception as e:\n                message = f\"Invalid URL for configuration metadata: {attr}\"\n                logger.error(message)\n                raise ValueError(message) from e\n\n        enforce(\"issuer\", True)\n        enforce(\"authorization_endpoint\", True)\n        enforce(\"token_endpoint\", True)\n        enforce(\"jwks_uri\", True)\n        enforce(\"response_types_supported\")\n        enforce(\"subject_types_supported\")\n        enforce(\"id_token_signing_alg_values_supported\")\n\n        return self\n\n    @classmethod\n    def get_oidc_configuration(\n        cls, config_url: AnyHttpUrl, *, strict: bool | None, timeout_seconds: int | None\n    ) -> Self:\n        \"\"\"Get the OIDC configuration for the specified config URL.\n\n        Args:\n            config_url: The OIDC config URL\n            strict: The strict flag for the configuration\n            timeout_seconds: HTTP request timeout in seconds\n        \"\"\"\n        get_kwargs = {}\n        if timeout_seconds is not None:\n            get_kwargs[\"timeout\"] = timeout_seconds\n\n        try:\n            response = httpx.get(str(config_url), **get_kwargs)\n            response.raise_for_status()\n\n            config_data = response.json()\n            if strict is not None:\n                config_data[\"strict\"] = strict\n\n            return cls.model_validate(config_data)\n        except Exception:\n            logger.exception(\n                f\"Unable to get OIDC configuration for config url: {config_url}\"\n            )\n            raise\n\n\nclass OIDCProxy(OAuthProxy):\n    \"\"\"OAuth provider that wraps OAuthProxy to provide configuration via an OIDC configuration URL.\n\n    This provider makes it easier to add OAuth protection for any upstream provider\n    that is OIDC compliant.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.oidc_proxy import OIDCProxy\n\n        # Simple OIDC based protection\n        auth = OIDCProxy(\n            config_url=\"https://oidc.config.url\",\n            client_id=\"your-oidc-client-id\",\n            client_secret=\"your-oidc-client-secret\",\n            base_url=\"https://your.server.url\",\n        )\n\n        mcp = FastMCP(\"My Protected Server\", auth=auth)\n        ```\n    \"\"\"\n\n    oidc_config: OIDCConfiguration\n\n    def __init__(\n        self,\n        *,\n        # OIDC configuration\n        config_url: AnyHttpUrl | str,\n        strict: bool | None = None,\n        # Upstream server configuration\n        client_id: str,\n        client_secret: str | None = None,\n        audience: str | None = None,\n        timeout_seconds: int | None = None,\n        # Token verifier\n        token_verifier: TokenVerifier | None = None,\n        algorithm: str | None = None,\n        required_scopes: list[str] | None = None,\n        verify_id_token: bool = False,\n        # FastMCP server configuration\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        redirect_path: str | None = None,\n        # Client configuration\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        # JWT and encryption keys\n        jwt_signing_key: str | bytes | None = None,\n        # Token validation configuration\n        token_endpoint_auth_method: str | None = None,\n        # Consent screen configuration\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        # Extra parameters\n        extra_authorize_params: dict[str, str] | None = None,\n        extra_token_params: dict[str, str] | None = None,\n        # Token expiry fallback\n        fallback_access_token_expiry_seconds: int | None = None,\n        # CIMD configuration\n        enable_cimd: bool = True,\n    ) -> None:\n        \"\"\"Initialize the OIDC proxy provider.\n\n        Args:\n            config_url: URL of upstream configuration\n            strict: Optional strict flag for the configuration\n            client_id: Client ID registered with upstream server\n            client_secret: Client secret for upstream server. Optional for PKCE public\n                clients or when using alternative credentials. When omitted,\n                jwt_signing_key must be provided.\n            audience: Audience for upstream server\n            timeout_seconds: HTTP request timeout in seconds\n            token_verifier: Optional custom token verifier (e.g., IntrospectionTokenVerifier for opaque tokens).\n                If not provided, a JWTVerifier will be created using the OIDC configuration.\n                Cannot be used with algorithm or required_scopes parameters (configure these on your verifier instead).\n            algorithm: Token verifier algorithm (only used if token_verifier is not provided)\n            required_scopes: Required scopes for token validation (only used if token_verifier is not provided)\n            verify_id_token: If True, verify the OIDC id_token instead of the access_token.\n                Useful for providers that issue opaque (non-JWT) access tokens, since the\n                id_token is always a standard JWT verifiable via the provider's JWKS.\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in upstream OAuth app (defaults to \"/auth/callback\")\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                Patterns support wildcards (e.g., \"http://localhost:*\", \"https://*.example.com/*\").\n                If None (default), all redirect URIs are allowed (for DCR compatibility).\n                If empty list, no redirect URIs are allowed.\n                These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            token_endpoint_auth_method: Token endpoint authentication method for upstream server.\n                Common values: \"client_secret_basic\", \"client_secret_post\", \"none\".\n                If None, authlib will use its default (typically \"client_secret_basic\").\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to the upstream IdP.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            consent_csp_policy: Content Security Policy for the consent page.\n                If None (default), uses the built-in CSP policy with appropriate directives.\n                If empty string \"\", disables CSP entirely (no meta tag is rendered).\n                If a non-empty string, uses that as the CSP policy value.\n            extra_authorize_params: Additional parameters to forward to the upstream authorization endpoint.\n                Useful for provider-specific parameters like prompt=consent or access_type=offline.\n                Example: {\"prompt\": \"consent\", \"access_type\": \"offline\"}\n            extra_token_params: Additional parameters to forward to the upstream token endpoint.\n                Useful for provider-specific parameters during token exchange.\n            fallback_access_token_expiry_seconds: Expiry time to use when upstream provider\n                doesn't return `expires_in` in the token response. If not set, uses smart\n                defaults: 1 hour if a refresh token is available (since we can refresh),\n                or 1 year if no refresh token (for API-key-style tokens like GitHub OAuth Apps).\n            enable_cimd: Whether to enable CIMD (Client ID Metadata Document) client support.\n                When True, clients can use their metadata document URL as client_id instead of\n                Dynamic Client Registration. Default is True.\n        \"\"\"\n        if not config_url:\n            raise ValueError(\"Missing required config URL\")\n\n        if not client_id:\n            raise ValueError(\"Missing required client id\")\n\n        if not client_secret and not jwt_signing_key:\n            raise ValueError(\n                \"Either client_secret or jwt_signing_key must be provided. \"\n                \"jwt_signing_key is required when client_secret is omitted \"\n                \"(e.g., for PKCE public clients).\"\n            )\n\n        if not base_url:\n            raise ValueError(\"Missing required base URL\")\n\n        # Validate that verifier-specific parameters are not used with custom verifier\n        if token_verifier is not None:\n            if algorithm is not None:\n                raise ValueError(\n                    \"Cannot specify 'algorithm' when providing a custom token_verifier. \"\n                    \"Configure the algorithm on your token verifier instead.\"\n                )\n            if required_scopes is not None:\n                raise ValueError(\n                    \"Cannot specify 'required_scopes' when providing a custom token_verifier. \"\n                    \"Configure required scopes on your token verifier instead.\"\n                )\n\n        if isinstance(config_url, str):\n            config_url = AnyHttpUrl(config_url)\n\n        self.oidc_config = self.get_oidc_configuration(\n            config_url, strict, timeout_seconds\n        )\n        if (\n            not self.oidc_config.authorization_endpoint\n            or not self.oidc_config.token_endpoint\n        ):\n            logger.debug(f\"Invalid OIDC Configuration: {self.oidc_config}\")\n            raise ValueError(\"Missing required OIDC endpoints\")\n\n        revocation_endpoint = (\n            str(self.oidc_config.revocation_endpoint)\n            if self.oidc_config.revocation_endpoint\n            else None\n        )\n\n        # Use custom verifier if provided, otherwise create default JWTVerifier\n        if token_verifier is None:\n            # When verifying id_tokens:\n            # - aud is always the OAuth client_id (per OIDC Core §2), not\n            #   the API audience, so use client_id for audience validation.\n            # - id_tokens don't carry scope/scp claims, so don't pass\n            #   required_scopes to the verifier (scope enforcement happens\n            #   at the FastMCP token level instead).\n            verifier_audience = client_id if verify_id_token else audience\n            verifier_scopes = None if verify_id_token else required_scopes\n            token_verifier = self.get_token_verifier(\n                algorithm=algorithm,\n                audience=verifier_audience,\n                required_scopes=verifier_scopes,\n                timeout_seconds=timeout_seconds,\n            )\n\n        init_kwargs: dict[str, object] = {\n            \"upstream_authorization_endpoint\": str(\n                self.oidc_config.authorization_endpoint\n            ),\n            \"upstream_token_endpoint\": str(self.oidc_config.token_endpoint),\n            \"upstream_client_id\": client_id,\n            \"upstream_client_secret\": client_secret,\n            \"upstream_revocation_endpoint\": revocation_endpoint,\n            \"token_verifier\": token_verifier,\n            \"base_url\": base_url,\n            \"issuer_url\": issuer_url or base_url,\n            \"service_documentation_url\": self.oidc_config.service_documentation,\n            \"allowed_client_redirect_uris\": allowed_client_redirect_uris,\n            \"client_storage\": client_storage,\n            \"jwt_signing_key\": jwt_signing_key,\n            \"token_endpoint_auth_method\": token_endpoint_auth_method,\n            \"require_authorization_consent\": require_authorization_consent,\n            \"consent_csp_policy\": consent_csp_policy,\n            \"fallback_access_token_expiry_seconds\": fallback_access_token_expiry_seconds,\n            \"enable_cimd\": enable_cimd,\n        }\n\n        if redirect_path:\n            init_kwargs[\"redirect_path\"] = redirect_path\n\n        # Build extra params, merging audience with user-provided params\n        # User params override audience if there's a conflict\n        final_authorize_params: dict[str, str] = {}\n        final_token_params: dict[str, str] = {}\n\n        if audience:\n            final_authorize_params[\"audience\"] = audience\n            final_token_params[\"audience\"] = audience\n\n        if extra_authorize_params:\n            final_authorize_params.update(extra_authorize_params)\n        if extra_token_params:\n            final_token_params.update(extra_token_params)\n\n        if final_authorize_params:\n            init_kwargs[\"extra_authorize_params\"] = final_authorize_params\n        if final_token_params:\n            init_kwargs[\"extra_token_params\"] = final_token_params\n\n        super().__init__(**init_kwargs)  # ty: ignore[invalid-argument-type]\n\n        self._verify_id_token = verify_id_token\n\n        # When verify_id_token strips scopes from the verifier, restore\n        # them on the provider so they're still advertised to clients\n        # and enforced at the FastMCP token level.  We also need to\n        # recompute derived state that OAuthProxy.__init__ already built\n        # from the (empty) verifier scopes.\n        if verify_id_token and required_scopes:\n            self.required_scopes = required_scopes\n            self._default_scope_str = \" \".join(required_scopes)\n            if self.client_registration_options:\n                self.client_registration_options.valid_scopes = required_scopes\n            if self._cimd_manager is not None:\n                self._cimd_manager.default_scope = self._default_scope_str\n\n    def _get_verification_token(\n        self, upstream_token_set: UpstreamTokenSet\n    ) -> str | None:\n        \"\"\"Get the token to verify from the upstream token set.\n\n        When verify_id_token is enabled, returns the id_token from the\n        upstream token response instead of the access_token.\n        \"\"\"\n        if self._verify_id_token:\n            id_token = upstream_token_set.raw_token_data.get(\"id_token\")\n            if id_token is None:\n                logger.warning(\n                    \"verify_id_token is enabled but no id_token found in\"\n                    \" upstream token response\"\n                )\n            return id_token\n        return upstream_token_set.access_token\n\n    def _uses_alternate_verification(self) -> bool:\n        \"\"\"Return True when id_token verification is enabled.\n\n        This ensures ``load_access_token`` always patches the validated\n        result with upstream scopes, even when the IdP issues the same\n        JWT for both ``access_token`` and ``id_token``.\n        \"\"\"\n        return self._verify_id_token\n\n    def get_oidc_configuration(\n        self,\n        config_url: AnyHttpUrl,\n        strict: bool | None,\n        timeout_seconds: int | None,\n    ) -> OIDCConfiguration:\n        \"\"\"Gets the OIDC configuration for the specified configuration URL.\n\n        Args:\n            config_url: The OIDC configuration URL\n            strict: The strict flag for the configuration\n            timeout_seconds: HTTP request timeout in seconds\n        \"\"\"\n        return OIDCConfiguration.get_oidc_configuration(\n            config_url, strict=strict, timeout_seconds=timeout_seconds\n        )\n\n    def get_token_verifier(\n        self,\n        *,\n        algorithm: str | None = None,\n        audience: str | None = None,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int | None = None,\n    ) -> TokenVerifier:\n        \"\"\"Creates the token verifier for the specified OIDC configuration and arguments.\n\n        Args:\n            algorithm: Optional token verifier algorithm\n            audience: Optional token verifier audience\n            required_scopes: Optional token verifier required_scopes\n            timeout_seconds: HTTP request timeout in seconds\n        \"\"\"\n        return JWTVerifier(\n            jwks_uri=str(self.oidc_config.jwks_uri),\n            issuer=str(self.oidc_config.issuer),\n            algorithm=algorithm,\n            audience=audience,\n            required_scopes=required_scopes,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/__init__.py",
    "content": ""
  },
  {
    "path": "src/fastmcp/server/auth/providers/auth0.py",
    "content": "\"\"\"Auth0 OAuth provider for FastMCP.\n\nThis module provides a complete Auth0 integration that's ready to use with\njust the configuration URL, client ID, client secret, audience, and base URL.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.auth0 import Auth0Provider\n\n    # Simple Auth0 OAuth protection\n    auth = Auth0Provider(\n        config_url=\"https://auth0.config.url\",\n        client_id=\"your-auth0-client-id\",\n        client_secret=\"your-auth0-client-secret\",\n        audience=\"your-auth0-api-audience\",\n        base_url=\"http://localhost:8000\",\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom typing import Literal\n\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.oidc_proxy import OIDCProxy\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass Auth0Provider(OIDCProxy):\n    \"\"\"An Auth0 provider implementation for FastMCP.\n\n    This provider is a complete Auth0 integration that's ready to use with\n    just the configuration URL, client ID, client secret, audience, and base URL.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.auth0 import Auth0Provider\n\n        # Simple Auth0 OAuth protection\n        auth = Auth0Provider(\n            config_url=\"https://auth0.config.url\",\n            client_id=\"your-auth0-client-id\",\n            client_secret=\"your-auth0-client-secret\",\n            audience=\"your-auth0-api-audience\",\n            base_url=\"http://localhost:8000\",\n        )\n\n        mcp = FastMCP(\"My Protected Server\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        config_url: AnyHttpUrl | str,\n        client_id: str,\n        client_secret: str,\n        audience: str,\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        required_scopes: list[str] | None = None,\n        redirect_path: str | None = None,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n    ) -> None:\n        \"\"\"Initialize Auth0 OAuth provider.\n\n        Args:\n            config_url: Auth0 config URL\n            client_id: Auth0 application client id\n            client_secret: Auth0 application client secret\n            audience: Auth0 API audience\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            required_scopes: Required Auth0 scopes (defaults to [\"openid\"])\n            redirect_path: Redirect path configured in Auth0 application\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to Auth0.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n        \"\"\"\n        # Parse scopes if provided as string\n        auth0_required_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else [\"openid\"]\n        )\n\n        super().__init__(\n            config_url=config_url,\n            client_id=client_id,\n            client_secret=client_secret,\n            audience=audience,\n            base_url=base_url,\n            issuer_url=issuer_url,\n            redirect_path=redirect_path,\n            required_scopes=auth0_required_scopes,\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n        )\n\n        logger.debug(\n            \"Initialized Auth0 OAuth provider for client %s with scopes: %s\",\n            client_id,\n            auth0_required_scopes,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/aws.py",
    "content": "\"\"\"AWS Cognito OAuth provider for FastMCP.\n\nThis module provides a complete AWS Cognito OAuth integration that's ready to use\nwith a user pool ID, domain prefix, client ID and client secret. It handles all\nthe complexity of AWS Cognito's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider\n\n    # Simple AWS Cognito OAuth protection\n    auth = AWSCognitoProvider(\n        user_pool_id=\"your-user-pool-id\",\n        aws_region=\"eu-central-1\",\n        client_id=\"your-cognito-client-id\",\n        client_secret=\"your-cognito-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Literal\n\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.server.auth.oidc_proxy import OIDCProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass AWSCognitoTokenVerifier(JWTVerifier):\n    \"\"\"Token verifier that filters claims to Cognito-specific subset.\"\"\"\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token and filter claims to Cognito-specific subset.\"\"\"\n        # Use base JWT verification\n        access_token = await super().verify_token(token)\n        if not access_token:\n            return None\n\n        # Filter claims to Cognito-specific subset\n        cognito_claims = {\n            \"sub\": access_token.claims.get(\"sub\"),\n            \"username\": access_token.claims.get(\"username\"),\n            \"cognito:groups\": access_token.claims.get(\"cognito:groups\", []),\n        }\n\n        # Return new AccessToken with filtered claims\n        return AccessToken(\n            token=access_token.token,\n            client_id=access_token.client_id,\n            scopes=access_token.scopes,\n            expires_at=access_token.expires_at,\n            claims=cognito_claims,\n        )\n\n\nclass AWSCognitoProvider(OIDCProxy):\n    \"\"\"Complete AWS Cognito OAuth provider for FastMCP.\n\n    This provider makes it trivial to add AWS Cognito OAuth protection to any\n    FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,\n    client credentials, and a base URL, and you're ready to go.\n\n    Features:\n    - Automatic OIDC Discovery from AWS Cognito User Pool\n    - Automatic JWT token validation via Cognito's public keys\n    - Cognito-specific claim filtering (sub, username, cognito:groups)\n    - Support for Cognito User Pools\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider\n\n        auth = AWSCognitoProvider(\n            user_pool_id=\"eu-central-1_XXXXXXXXX\",\n            aws_region=\"eu-central-1\",\n            client_id=\"your-cognito-client-id\",\n            client_secret=\"your-cognito-client-secret\",\n            base_url=\"https://my-server.com\",\n            redirect_path=\"/custom/callback\",\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        user_pool_id: str,\n        client_id: str,\n        client_secret: str,\n        base_url: AnyHttpUrl | str,\n        aws_region: str = \"eu-central-1\",\n        issuer_url: AnyHttpUrl | str | None = None,\n        redirect_path: str = \"/auth/callback\",\n        required_scopes: list[str] | None = None,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n    ):\n        \"\"\"Initialize AWS Cognito OAuth provider.\n\n        Args:\n            user_pool_id: Your Cognito User Pool ID (e.g., \"eu-central-1_XXXXXXXXX\")\n            client_id: Cognito app client ID\n            client_secret: Cognito app client secret\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            aws_region: AWS region where your User Pool is located (defaults to \"eu-central-1\")\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in Cognito app (defaults to \"/auth/callback\")\n            required_scopes: Required Cognito scopes (defaults to [\"openid\"])\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to AWS Cognito.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n        \"\"\"\n        # Parse scopes if provided as string\n        required_scopes_final = (\n            parse_scopes(required_scopes) if required_scopes is not None else [\"openid\"]\n        )\n\n        # Construct OIDC discovery URL\n        config_url = f\"https://cognito-idp.{aws_region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration\"\n\n        # Store Cognito-specific info for claim filtering\n        self.user_pool_id = user_pool_id\n        self.aws_region = aws_region\n        self.client_id = client_id\n\n        # Initialize OIDC proxy with Cognito discovery\n        super().__init__(\n            config_url=config_url,\n            client_id=client_id,\n            client_secret=client_secret,\n            algorithm=\"RS256\",\n            required_scopes=required_scopes_final,\n            base_url=base_url,\n            issuer_url=issuer_url,\n            redirect_path=redirect_path,\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n        )\n\n        logger.debug(\n            \"Initialized AWS Cognito OAuth provider for client %s with scopes: %s\",\n            client_id,\n            required_scopes_final,\n        )\n\n    def get_token_verifier(\n        self,\n        *,\n        algorithm: str | None = None,\n        audience: str | None = None,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int | None = None,\n    ) -> AWSCognitoTokenVerifier:\n        \"\"\"Creates a Cognito-specific token verifier with claim filtering.\n\n        Args:\n            algorithm: Optional token verifier algorithm\n            audience: Optional token verifier audience\n            required_scopes: Optional token verifier required_scopes\n            timeout_seconds: HTTP request timeout in seconds\n        \"\"\"\n        return AWSCognitoTokenVerifier(\n            issuer=str(self.oidc_config.issuer),\n            audience=audience or self.client_id,\n            algorithm=algorithm,\n            jwks_uri=str(self.oidc_config.jwks_uri),\n            required_scopes=required_scopes,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/azure.py",
    "content": "\"\"\"Azure (Microsoft Entra) OAuth provider for FastMCP.\n\nThis provider implements Azure/Microsoft Entra ID OAuth authentication\nusing the OAuth Proxy pattern for non-DCR OAuth flows.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nfrom collections import OrderedDict\nfrom typing import TYPE_CHECKING, Any, Literal, cast\n\nimport httpx\nfrom key_value.aio.protocols import AsyncKeyValue\n\nfrom fastmcp.dependencies import Dependency\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.auth import decode_jwt_payload, parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from azure.identity.aio import OnBehalfOfCredential\n    from mcp.server.auth.provider import AuthorizationParams\n    from mcp.shared.auth import OAuthClientInformationFull\n\nlogger = get_logger(__name__)\n\n# Standard OIDC scopes that should never be prefixed with identifier_uri.\n# Per Microsoft docs: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc\n# \"OIDC scopes are requested as simple string identifiers without resource prefixes\"\nOIDC_SCOPES = frozenset({\"openid\", \"profile\", \"email\", \"offline_access\"})\n\n\nclass AzureProvider(OAuthProxy):\n    \"\"\"Azure (Microsoft Entra) OAuth provider for FastMCP.\n\n    This provider implements Azure/Microsoft Entra ID authentication using the\n    OAuth Proxy pattern. It supports both organizational accounts and personal\n    Microsoft accounts depending on the tenant configuration.\n\n    Scope Handling:\n    - required_scopes: Provide unprefixed scope names (e.g., [\"read\", \"write\"])\n      → Automatically prefixed with identifier_uri during initialization\n      → Validated on all tokens and advertised to MCP clients\n    - additional_authorize_scopes: Provide full format (e.g., [\"User.Read\"])\n      → NOT prefixed, NOT validated, NOT advertised to clients\n      → Used to request Microsoft Graph or other upstream API permissions\n\n    Features:\n    - OAuth proxy to Azure/Microsoft identity platform\n    - JWT validation using tenant issuer and JWKS\n    - Supports tenant configurations: specific tenant ID, \"organizations\", or \"consumers\"\n    - Custom API scopes and Microsoft Graph scopes in a single provider\n\n    Setup:\n    1. Create an App registration in Azure Portal\n    2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)\n    3. Add an Application ID URI under \"Expose an API\" (defaults to api://{client_id})\n    4. Add custom scopes (e.g., \"read\", \"write\") under \"Expose an API\"\n    5. Set access token version to 2 in the App manifest: \"requestedAccessTokenVersion\": 2\n    6. Create a client secret\n    7. Get Application (client) ID, Directory (tenant) ID, and client secret\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.azure import AzureProvider\n\n        # Standard Azure (Public Cloud)\n        auth = AzureProvider(\n            client_id=\"your-client-id\",\n            client_secret=\"your-client-secret\",\n            tenant_id=\"your-tenant-id\",\n            required_scopes=[\"read\", \"write\"],  # Unprefixed scope names\n            additional_authorize_scopes=[\"User.Read\", \"Mail.Read\"],  # Optional Graph scopes\n            base_url=\"http://localhost:8000\",\n            # identifier_uri defaults to api://{client_id}\n        )\n\n        # Azure Government\n        auth_gov = AzureProvider(\n            client_id=\"your-client-id\",\n            client_secret=\"your-client-secret\",\n            tenant_id=\"your-tenant-id\",\n            required_scopes=[\"read\", \"write\"],\n            base_authority=\"login.microsoftonline.us\",  # Override for Azure Gov\n            base_url=\"http://localhost:8000\",\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_id: str,\n        client_secret: str | None = None,\n        tenant_id: str,\n        required_scopes: list[str],\n        base_url: str,\n        identifier_uri: str | None = None,\n        issuer_url: str | None = None,\n        redirect_path: str | None = None,\n        additional_authorize_scopes: list[str] | None = None,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        base_authority: str = \"login.microsoftonline.com\",\n        http_client: httpx.AsyncClient | None = None,\n    ) -> None:\n        \"\"\"Initialize Azure OAuth provider.\n\n        Args:\n            client_id: Azure application (client) ID from your App registration\n            client_secret: Azure client secret from your App registration. Optional when\n                using alternative credentials (e.g., managed identity with a custom\n                _create_upstream_oauth_client override). When omitted, jwt_signing_key\n                must be provided.\n            tenant_id: Azure tenant ID (specific tenant GUID, \"organizations\", or \"consumers\")\n            identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).\n                This URI is automatically prefixed to all required_scopes during initialization.\n                Example: identifier_uri=\"api://my-api\" + required_scopes=[\"read\"]\n                → tokens validated for \"api://my-api/read\"\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in Azure App registration (defaults to \"/auth/callback\")\n            base_authority: Azure authority base URL (defaults to \"login.microsoftonline.com\").\n                For Azure Government, use \"login.microsoftonline.us\".\n            required_scopes: Custom API scope names WITHOUT prefix (e.g., [\"read\", \"write\"]).\n                - Automatically prefixed with identifier_uri during initialization\n                - Validated on all tokens\n                - Advertised in Protected Resource Metadata\n                - Must match scope names defined in Azure Portal under \"Expose an API\"\n                Example: [\"read\", \"write\"] → validates tokens containing [\"api://xxx/read\", \"api://xxx/write\"]\n            additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.\n                - NOT prefixed with identifier_uri\n                - NOT validated on tokens\n                - NOT advertised to MCP clients\n                - Used to request additional permissions from Azure (e.g., Graph API access)\n                Example: [\"User.Read\", \"Mail.Read\"]\n                These scopes allow your FastMCP server to call Microsoft Graph APIs using the\n                upstream Azure token, but MCP clients are unaware of them.\n                Note: \"offline_access\" is automatically included to obtain refresh tokens.\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to Azure.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            http_client: Optional httpx.AsyncClient for connection pooling in JWKS fetches.\n                When provided, the client is reused for JWT key fetches and the caller\n                is responsible for its lifecycle. When None (default), a fresh client is created per fetch.\n        \"\"\"\n        # Parse scopes if provided as string\n        parsed_required_scopes = parse_scopes(required_scopes)\n        parsed_additional_scopes: list[str] = (\n            parse_scopes(additional_authorize_scopes) or []\n            if additional_authorize_scopes\n            else []\n        )\n\n        # Always include offline_access to get refresh tokens from Azure\n        if \"offline_access\" not in parsed_additional_scopes:\n            parsed_additional_scopes = [*parsed_additional_scopes, \"offline_access\"]\n\n        # Store Azure-specific config for OBO credential creation\n        self._tenant_id = tenant_id\n        self._base_authority = base_authority\n\n        # Cache of OBO credentials keyed by hash of user assertion token.\n        # Reusing credentials allows the Azure SDK's internal token cache\n        # to avoid redundant OBO exchanges for the same user + scopes.\n        self._obo_credentials: OrderedDict[str, OnBehalfOfCredential] = OrderedDict()\n        self._obo_max_credentials: int = 128\n\n        # Apply defaults\n        self.identifier_uri = identifier_uri or f\"api://{client_id}\"\n        self.additional_authorize_scopes: list[str] = parsed_additional_scopes\n\n        # Always validate tokens against the app's API client ID using JWT\n        issuer = f\"https://{base_authority}/{tenant_id}/v2.0\"\n        jwks_uri = f\"https://{base_authority}/{tenant_id}/discovery/v2.0/keys\"\n\n        # Azure access tokens only include custom API scopes in the `scp` claim,\n        # NOT standard OIDC scopes (openid, profile, email, offline_access).\n        # Filter out OIDC scopes from validation - they'll still be sent to Azure\n        # during authorization (handled by _prefix_scopes_for_azure).\n        validation_scopes = [\n            s for s in (parsed_required_scopes or []) if s not in OIDC_SCOPES\n        ]\n        if not validation_scopes:\n            raise ValueError(\n                \"AzureProvider requires at least one non-OIDC scope in \"\n                \"required_scopes (e.g., 'read', 'write'). OIDC scopes like \"\n                \"'openid', 'profile', 'email', and 'offline_access' are not \"\n                \"included in Azure access token claims and cannot be used for \"\n                \"scope enforcement.\"\n            )\n\n        token_verifier = JWTVerifier(\n            jwks_uri=jwks_uri,\n            issuer=issuer,\n            audience=client_id,\n            algorithm=\"RS256\",\n            required_scopes=validation_scopes,  # Only validate non-OIDC scopes\n            http_client=http_client,\n        )\n\n        # Build Azure OAuth endpoints with tenant\n        authorization_endpoint = (\n            f\"https://{base_authority}/{tenant_id}/oauth2/v2.0/authorize\"\n        )\n        token_endpoint = f\"https://{base_authority}/{tenant_id}/oauth2/v2.0/token\"\n\n        # Initialize OAuth proxy with Azure endpoints\n        # Remember there's hooks called, such as _prepare_scopes_for_token_exchange\n        # and _prepare_scopes_for_upstream_refresh\n        super().__init__(\n            upstream_authorization_endpoint=authorization_endpoint,\n            upstream_token_endpoint=token_endpoint,\n            upstream_client_id=client_id,\n            upstream_client_secret=client_secret,\n            token_verifier=token_verifier,\n            base_url=base_url,\n            redirect_path=redirect_path,\n            issuer_url=issuer_url or base_url,  # Default to base_url if not specified\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n            valid_scopes=parsed_required_scopes,\n        )\n\n        authority_info = \"\"\n        if base_authority != \"login.microsoftonline.com\":\n            authority_info = f\" using authority {base_authority}\"\n        logger.info(\n            \"Initialized Azure OAuth provider for client %s with tenant %s%s%s\",\n            client_id,\n            tenant_id,\n            f\" and identifier_uri {self.identifier_uri}\" if self.identifier_uri else \"\",\n            authority_info,\n        )\n\n    async def authorize(\n        self,\n        client: OAuthClientInformationFull,\n        params: AuthorizationParams,\n    ) -> str:\n        \"\"\"Start OAuth transaction and redirect to Azure AD.\n\n        Override parent's authorize method to filter out the 'resource' parameter\n        which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use\n        scopes to determine the resource/audience instead of a separate parameter.\n\n        Args:\n            client: OAuth client information\n            params: Authorization parameters from the client\n\n        Returns:\n            Authorization URL to redirect the user to Azure AD\n        \"\"\"\n        # Clear the resource parameter that Azure AD v2.0 doesn't support\n        # This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)\n        # but Azure AD v2.0 uses scopes instead to determine the audience\n        params_to_use = params\n        if hasattr(params, \"resource\"):\n            original_resource = getattr(params, \"resource\", None)\n            if original_resource is not None:\n                params_to_use = params.model_copy(update={\"resource\": None})\n                if original_resource:\n                    logger.debug(\n                        \"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)\",\n                        original_resource,\n                    )\n        # Don't modify the scopes in params - they stay unprefixed for MCP clients\n        # We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)\n        auth_url = await super().authorize(client, params_to_use)\n        separator = \"&\" if \"?\" in auth_url else \"?\"\n        return f\"{auth_url}{separator}prompt=select_account\"\n\n    def _prefix_scopes_for_azure(self, scopes: list[str]) -> list[str]:\n        \"\"\"Prefix unprefixed custom API scopes with identifier_uri for Azure.\n\n        This helper centralizes the scope prefixing logic used in both\n        authorization and token refresh flows.\n\n        Scopes that are NOT prefixed:\n        - Standard OIDC scopes (openid, profile, email, offline_access)\n        - Fully-qualified URIs (contain \"://\")\n        - Scopes with path component (contain \"/\")\n\n        Note: Microsoft Graph scopes (e.g., User.Read) should be passed via\n        `additional_authorize_scopes` or use fully-qualified format\n        (e.g., https://graph.microsoft.com/User.Read).\n\n        Args:\n            scopes: List of scopes, may be prefixed or unprefixed\n\n        Returns:\n            List of scopes with identifier_uri prefix applied where needed\n        \"\"\"\n        prefixed = []\n        for scope in scopes:\n            if scope in OIDC_SCOPES:\n                # Standard OIDC scopes - never prefix\n                prefixed.append(scope)\n            elif \"://\" in scope or \"/\" in scope:\n                # Already fully-qualified (e.g., \"api://xxx/read\" or\n                # \"https://graph.microsoft.com/User.Read\")\n                prefixed.append(scope)\n            else:\n                # Unprefixed custom API scope - prefix with identifier_uri\n                prefixed.append(f\"{self.identifier_uri}/{scope}\")\n        return prefixed\n\n    def _build_upstream_authorize_url(\n        self, txn_id: str, transaction: dict[str, Any]\n    ) -> str:\n        \"\"\"Build Azure authorization URL with prefixed scopes.\n\n        Overrides parent to prefix scopes with identifier_uri before sending to Azure,\n        while keeping unprefixed scopes in the transaction for MCP clients.\n        \"\"\"\n        # Get unprefixed scopes from transaction\n        unprefixed_scopes = transaction.get(\"scopes\") or self.required_scopes or []\n\n        # Prefix scopes for Azure authorization request\n        prefixed_scopes = self._prefix_scopes_for_azure(unprefixed_scopes)\n\n        # Add Microsoft Graph scopes (not validated, not prefixed)\n        if self.additional_authorize_scopes:\n            prefixed_scopes.extend(self.additional_authorize_scopes)\n\n        # Temporarily modify transaction dict for parent's URL building\n        modified_transaction = transaction.copy()\n        modified_transaction[\"scopes\"] = prefixed_scopes\n\n        # Let parent build the URL with prefixed scopes\n        return super()._build_upstream_authorize_url(txn_id, modified_transaction)\n\n    def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]:\n        \"\"\"Prepare scopes for Azure authorization code exchange.\n\n        Azure requires scopes during token exchange (AADSTS28003 error if missing).\n        Azure only allows ONE resource per token request (AADSTS28000), so we only\n        include scopes for this API plus OIDC scopes.\n\n        Args:\n            scopes: Scopes from the authorization request (unprefixed)\n\n        Returns:\n            List of scopes for Azure token endpoint\n        \"\"\"\n        # Prefix scopes for this API\n        prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])\n\n        # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000\n        if self.additional_authorize_scopes:\n            prefixed_scopes.extend(\n                s for s in self.additional_authorize_scopes if s in OIDC_SCOPES\n            )\n\n        deduplicated = list(dict.fromkeys(prefixed_scopes))\n        logger.debug(\"Token exchange scopes: %s\", deduplicated)\n        return deduplicated\n\n    def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:\n        \"\"\"Prepare scopes for Azure token refresh.\n\n        Azure requires fully-qualified scopes and only allows ONE resource per\n        token request (AADSTS28000). We include scopes for this API plus OIDC scopes.\n\n        Args:\n            scopes: Base scopes from RefreshToken (unprefixed, e.g., [\"read\"])\n\n        Returns:\n            Deduplicated list of scopes formatted for Azure token endpoint\n        \"\"\"\n        logger.debug(\"Base scopes from storage: %s\", scopes)\n\n        # Filter out any additional_authorize_scopes that may have been stored\n        additional_scopes_set = set(self.additional_authorize_scopes or [])\n        base_scopes = [s for s in scopes if s not in additional_scopes_set]\n\n        # Prefix base scopes with identifier_uri for Azure\n        prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)\n\n        # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000\n        if self.additional_authorize_scopes:\n            prefixed_scopes.extend(\n                s for s in self.additional_authorize_scopes if s in OIDC_SCOPES\n            )\n\n        deduplicated_scopes = list(dict.fromkeys(prefixed_scopes))\n        logger.debug(\"Scopes for Azure token endpoint: %s\", deduplicated_scopes)\n        return deduplicated_scopes\n\n    async def _extract_upstream_claims(\n        self, idp_tokens: dict[str, Any]\n    ) -> dict[str, Any] | None:\n        \"\"\"Extract claims from Azure token response to embed in FastMCP JWT.\n\n        Decodes the Azure access token (which is a JWT) to extract user identity\n        claims. This allows gateways to inspect upstream identity information by\n        decoding the FastMCP JWT without needing server-side storage lookups.\n\n        Azure access tokens contain claims like:\n        - sub: Subject identifier (unique per user per application)\n        - oid: Object ID (unique user identifier across Azure AD)\n        - tid: Tenant ID\n        - azp: Authorized party (client ID that requested the token)\n        - name: Display name\n        - given_name: First name\n        - family_name: Last name\n        - preferred_username: User principal name (email format)\n        - upn: User Principal Name\n        - email: Email address (if available)\n        - roles: Application roles assigned to the user\n        - groups: Group memberships (if configured)\n\n        Args:\n            idp_tokens: Full token response from Azure, containing access_token\n                and potentially id_token.\n\n        Returns:\n            Dict of extracted claims, or None if extraction fails.\n        \"\"\"\n        access_token = idp_tokens.get(\"access_token\")\n        if not access_token:\n            return None\n\n        try:\n            # Azure access tokens are JWTs - decode without verification\n            # (already validated by token_verifier during token exchange)\n            payload = decode_jwt_payload(access_token)\n\n            # Extract useful identity claims\n            claims: dict[str, Any] = {}\n            claim_keys = [\n                \"sub\",\n                \"oid\",\n                \"tid\",\n                \"azp\",\n                \"name\",\n                \"given_name\",\n                \"family_name\",\n                \"preferred_username\",\n                \"upn\",\n                \"email\",\n                \"roles\",\n                \"groups\",\n            ]\n            for claim in claim_keys:\n                if claim in payload:\n                    claims[claim] = payload[claim]\n\n            if claims:\n                logger.debug(\n                    \"Extracted %d Azure claims for embedding in FastMCP JWT\",\n                    len(claims),\n                )\n                return claims\n\n            return None\n\n        except Exception as e:\n            logger.debug(\"Failed to extract Azure claims: %s\", e)\n            return None\n\n    async def get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:\n        \"\"\"Get a cached or new OnBehalfOfCredential for OBO token exchange.\n\n        Credentials are cached by user assertion so the Azure SDK's internal\n        token cache can avoid redundant OBO exchanges when the same user\n        calls multiple tools with the same scopes.\n\n        Args:\n            user_assertion: The user's access token to exchange via OBO.\n\n        Returns:\n            A configured OnBehalfOfCredential ready for get_token() calls.\n\n        Raises:\n            ImportError: If azure-identity is not installed (requires fastmcp[azure]).\n        \"\"\"\n        _require_azure_identity(\"OBO token exchange\")\n        from azure.identity.aio import OnBehalfOfCredential\n\n        key = hashlib.sha256(user_assertion.encode()).hexdigest()\n\n        if key in self._obo_credentials:\n            self._obo_credentials.move_to_end(key)\n            return self._obo_credentials[key]\n\n        obo_kwargs: dict[str, Any] = {\n            \"tenant_id\": self._tenant_id,\n            \"client_id\": self._upstream_client_id,\n            \"user_assertion\": user_assertion,\n            \"authority\": f\"https://{self._base_authority}\",\n        }\n        if self._upstream_client_secret is not None:\n            obo_kwargs[\"client_secret\"] = (\n                self._upstream_client_secret.get_secret_value()\n            )\n        else:\n            raise ValueError(\n                \"OBO token exchange requires either a client_secret or a subclass \"\n                \"that overrides get_obo_credential() to provide alternative credentials \"\n                \"(e.g., client_assertion_func for managed identity).\"\n            )\n        credential = OnBehalfOfCredential(**obo_kwargs)\n        self._obo_credentials[key] = credential\n\n        # Evict oldest if over capacity\n        while len(self._obo_credentials) > self._obo_max_credentials:\n            _, evicted = self._obo_credentials.popitem(last=False)\n            await evicted.close()\n\n        return credential\n\n    async def close_obo_credentials(self) -> None:\n        \"\"\"Close all cached OBO credentials.\"\"\"\n        credentials = list(self._obo_credentials.values())\n        self._obo_credentials.clear()\n        for credential in credentials:\n            try:\n                await credential.close()\n            except Exception:\n                logger.debug(\"Error closing OBO credential\", exc_info=True)\n\n\nclass AzureJWTVerifier(JWTVerifier):\n    \"\"\"JWT verifier pre-configured for Azure AD / Microsoft Entra ID.\n\n    Auto-configures JWKS URI, issuer, audience, and scope handling from your\n    Azure app registration details. Designed for Managed Identity and other\n    token-verification-only scenarios where AzureProvider's full OAuth proxy\n    isn't needed.\n\n    Handles Azure's scope format automatically:\n    - Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)\n    - Advertises full-URI scopes in OAuth metadata (what clients need to request)\n\n    Example::\n\n        from fastmcp.server.auth import RemoteAuthProvider\n        from fastmcp.server.auth.providers.azure import AzureJWTVerifier\n        from pydantic import AnyHttpUrl\n\n        verifier = AzureJWTVerifier(\n            client_id=\"your-client-id\",\n            tenant_id=\"your-tenant-id\",\n            required_scopes=[\"access_as_user\"],\n        )\n\n        auth = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[\n                AnyHttpUrl(\"https://login.microsoftonline.com/your-tenant-id/v2.0\")\n            ],\n            base_url=\"https://my-server.com\",\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_id: str,\n        tenant_id: str,\n        required_scopes: list[str] | None = None,\n        identifier_uri: str | None = None,\n        base_authority: str = \"login.microsoftonline.com\",\n    ):\n        \"\"\"Initialize Azure JWT verifier.\n\n        Args:\n            client_id: Azure application (client) ID from your App registration\n            tenant_id: Azure tenant ID (specific tenant GUID, \"organizations\", or \"consumers\").\n                For multi-tenant apps (\"organizations\" or \"consumers\"), issuer validation\n                is skipped since Azure tokens carry the actual tenant GUID as issuer.\n            required_scopes: Scope names as they appear in Azure Portal under \"Expose an API\"\n                (e.g., [\"access_as_user\", \"read\"]). These are validated against\n                the short-form scopes in token ``scp`` claims, and automatically\n                prefixed with identifier_uri for OAuth metadata.\n            identifier_uri: Application ID URI (defaults to ``api://{client_id}``).\n                Used to prefix scopes in OAuth metadata so clients know the full\n                scope URIs to request from Azure.\n            base_authority: Azure authority base URL (defaults to \"login.microsoftonline.com\").\n                For Azure Government, use \"login.microsoftonline.us\".\n        \"\"\"\n        self._identifier_uri = identifier_uri or f\"api://{client_id}\"\n\n        # For multi-tenant apps, Azure tokens carry the actual tenant GUID as\n        # issuer, not the literal \"organizations\" or \"consumers\" string. Skip\n        # issuer validation for these — audience still protects against wrong-app tokens.\n        multi_tenant_values = {\"organizations\", \"consumers\", \"common\"}\n        issuer: str | None = (\n            None\n            if tenant_id in multi_tenant_values\n            else f\"https://{base_authority}/{tenant_id}/v2.0\"\n        )\n\n        super().__init__(\n            jwks_uri=f\"https://{base_authority}/{tenant_id}/discovery/v2.0/keys\",\n            issuer=issuer,\n            audience=client_id,\n            algorithm=\"RS256\",\n            required_scopes=required_scopes,\n        )\n\n    @property\n    def scopes_supported(self) -> list[str]:\n        \"\"\"Return scopes with Azure URI prefix for OAuth metadata.\n\n        Azure tokens contain short-form scopes (e.g., ``read``) in the ``scp``\n        claim, but clients must request full URI scopes (e.g.,\n        ``api://client-id/read``) from the Azure authorization endpoint. This\n        property returns the full-URI form for OAuth metadata while\n        ``required_scopes`` retains the short form for token validation.\n        \"\"\"\n        if not self.required_scopes:\n            return []\n        prefixed = []\n        for scope in self.required_scopes:\n            if scope in OIDC_SCOPES or \"://\" in scope or \"/\" in scope:\n                prefixed.append(scope)\n            else:\n                prefixed.append(f\"{self._identifier_uri}/{scope}\")\n        return prefixed\n\n\n# --- Dependency injection support ---\n# These require fastmcp[azure] extra for azure-identity\n\n\ndef _require_azure_identity(feature: str) -> None:\n    \"\"\"Raise ImportError with install instructions if azure-identity is not available.\"\"\"\n    try:\n        import azure.identity  # noqa: F401\n    except ImportError as e:\n        raise ImportError(\n            f\"{feature} requires the `azure` extra. \"\n            \"Install with: pip install 'fastmcp[azure]'\"\n        ) from e\n\n\nclass _EntraOBOToken(Dependency[str]):\n    \"\"\"Dependency that performs OBO token exchange for Microsoft Entra.\n\n    Uses azure.identity's OnBehalfOfCredential for async-native OBO,\n    with automatic token caching and refresh. Credentials are cached on\n    the AzureProvider so repeated tool calls reuse existing credentials\n    and benefit from the Azure SDK's internal token cache.\n    \"\"\"\n\n    def __init__(self, scopes: list[str]):\n        self.scopes = scopes\n\n    async def __aenter__(self) -> str:\n        _require_azure_identity(\"EntraOBOToken\")\n\n        from fastmcp.server.dependencies import get_access_token, get_server\n\n        access_token = get_access_token()\n        if access_token is None:\n            raise RuntimeError(\n                \"No access token available. Cannot perform OBO exchange.\"\n            )\n\n        server = get_server()\n        if not isinstance(server.auth, AzureProvider):\n            raise RuntimeError(\n                \"EntraOBOToken requires an AzureProvider as the auth provider. \"\n                f\"Current provider: {type(server.auth).__name__}\"\n            )\n\n        credential = await server.auth.get_obo_credential(\n            user_assertion=access_token.token,\n        )\n\n        result = await credential.get_token(*self.scopes)\n        return result.token\n\n\ndef EntraOBOToken(scopes: list[str]) -> str:\n    \"\"\"Exchange the user's Entra token for a downstream API token via OBO.\n\n    This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,\n    allowing your MCP server to call downstream APIs (like Microsoft Graph) on\n    behalf of the authenticated user.\n\n    Args:\n        scopes: The scopes to request for the downstream API. For Microsoft Graph,\n            use scopes like [\"https://graph.microsoft.com/Mail.Read\"] or\n            [\"https://graph.microsoft.com/.default\"].\n\n    Returns:\n        A dependency that resolves to the downstream API access token string\n\n    Raises:\n        ImportError: If fastmcp[azure] is not installed\n        RuntimeError: If no access token is available, provider is not Azure,\n            or OBO exchange fails\n\n    Example:\n        ```python\n        from fastmcp.server.auth.providers.azure import EntraOBOToken\n        import httpx\n\n        @mcp.tool()\n        async def get_my_emails(\n            graph_token: str = EntraOBOToken([\"https://graph.microsoft.com/Mail.Read\"])\n        ):\n            async with httpx.AsyncClient() as client:\n                resp = await client.get(\n                    \"https://graph.microsoft.com/v1.0/me/messages\",\n                    headers={\"Authorization\": f\"Bearer {graph_token}\"}\n                )\n                return resp.json()\n        ```\n\n    Note:\n        For OBO to work, ensure the scopes are included in the AzureProvider's\n        `additional_authorize_scopes` parameter, and that admin consent has been\n        granted for those scopes in your Entra app registration.\n    \"\"\"\n    return cast(str, _EntraOBOToken(scopes))\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/debug.py",
    "content": "\"\"\"Debug token verifier for testing and special cases.\n\nThis module provides a flexible token verifier that delegates validation\nto a custom callable. Useful for testing, development, or scenarios where\nstandard verification isn't possible (like opaque tokens without introspection).\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n    # Accept all tokens (default - useful for testing)\n    auth = DebugTokenVerifier()\n\n    # Custom sync validation logic\n    auth = DebugTokenVerifier(validate=lambda token: token.startswith(\"valid-\"))\n\n    # Custom async validation logic\n    async def check_cache(token: str) -> bool:\n        return await redis.exists(f\"token:{token}\")\n\n    auth = DebugTokenVerifier(validate=check_cache)\n\n    mcp = FastMCP(\"My Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom collections.abc import Awaitable, Callable\n\nfrom fastmcp.server.auth import TokenVerifier\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass DebugTokenVerifier(TokenVerifier):\n    \"\"\"Token verifier with custom validation logic.\n\n    This verifier delegates token validation to a user-provided callable.\n    By default, it accepts all non-empty tokens (useful for testing).\n\n    Use cases:\n    - Testing: Accept any token without real verification\n    - Development: Custom validation logic for prototyping\n    - Opaque tokens: When you have tokens with no introspection endpoint\n\n    WARNING: This bypasses standard security checks. Only use in controlled\n    environments or when you understand the security implications.\n    \"\"\"\n\n    def __init__(\n        self,\n        validate: Callable[[str], bool]\n        | Callable[[str], Awaitable[bool]] = lambda token: True,\n        client_id: str = \"debug-client\",\n        scopes: list[str] | None = None,\n        required_scopes: list[str] | None = None,\n    ):\n        \"\"\"Initialize the debug token verifier.\n\n        Args:\n            validate: Callable that takes a token string and returns True if valid.\n                Can be sync or async. Default accepts all tokens.\n            client_id: Client ID to assign to validated tokens\n            scopes: Scopes to assign to validated tokens\n            required_scopes: Required scopes (inherited from TokenVerifier base class)\n        \"\"\"\n        super().__init__(required_scopes=required_scopes)\n        self.validate = validate\n        self.client_id = client_id\n        self.scopes = scopes or []\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token using custom validation logic.\n\n        Args:\n            token: The token string to validate\n\n        Returns:\n            AccessToken if validation succeeds, None otherwise\n        \"\"\"\n        # Reject empty tokens\n        if not token or not token.strip():\n            logger.debug(\"Rejecting empty token\")\n            return None\n\n        try:\n            # Call validation function and await if result is awaitable\n            result = self.validate(token)\n            if inspect.isawaitable(result):\n                is_valid = await result\n            else:\n                is_valid = result\n\n            if not is_valid:\n                logger.debug(\"Token validation failed: callable returned False\")\n                return None\n\n            # Return valid AccessToken\n            return AccessToken(\n                token=token,\n                client_id=self.client_id,\n                scopes=self.scopes,\n                expires_at=None,  # No expiration\n                claims={\"token\": token},  # Store original token in claims\n            )\n\n        except Exception as e:\n            logger.debug(\"Token validation error: %s\", e, exc_info=True)\n            return None\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/descope.py",
    "content": "\"\"\"Descope authentication provider for FastMCP.\n\nThis module provides DescopeProvider - a complete authentication solution that integrates\nwith Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)\nfor seamless MCP client authentication.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom urllib.parse import urlparse\n\nimport httpx\nfrom pydantic import AnyHttpUrl\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.server.auth import RemoteAuthProvider, TokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass DescopeProvider(RemoteAuthProvider):\n    \"\"\"Descope metadata provider for DCR (Dynamic Client Registration).\n\n    This provider implements Descope integration using metadata forwarding.\n    This is the recommended approach for Descope DCR\n    as it allows Descope to handle the OAuth flow directly while FastMCP acts\n    as a resource server.\n\n    IMPORTANT SETUP REQUIREMENTS:\n\n    1. Create an MCP Server in Descope Console:\n       - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console\n       - Create a new MCP Server\n       - Ensure that **Dynamic Client Registration (DCR)** is enabled\n       - Note your Well-Known URL\n\n    2. Note your Well-Known URL:\n       - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)\n       - Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``\n\n    For detailed setup instructions, see:\n    https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr\n\n    Example:\n        ```python\n        from fastmcp.server.auth.providers.descope import DescopeProvider\n\n        # Create Descope metadata provider (JWT verifier created automatically)\n        descope_auth = DescopeProvider(\n            config_url=\"https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration\",\n            base_url=\"https://your-fastmcp-server.com\",\n        )\n\n        # Use with FastMCP\n        mcp = FastMCP(\"My App\", auth=descope_auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: AnyHttpUrl | str,\n        config_url: AnyHttpUrl | str | None = None,\n        project_id: str | None = None,\n        descope_base_url: AnyHttpUrl | str | None = None,\n        required_scopes: list[str] | None = None,\n        scopes_supported: list[str] | None = None,\n        resource_name: str | None = None,\n        resource_documentation: AnyHttpUrl | None = None,\n        token_verifier: TokenVerifier | None = None,\n    ):\n        \"\"\"Initialize Descope metadata provider.\n\n        Args:\n            base_url: Public URL of this FastMCP server\n            config_url: Your Descope Well-Known URL (e.g., \"https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration\")\n                This is the new recommended way. If provided, project_id and descope_base_url are ignored.\n            project_id: Your Descope Project ID (e.g., \"P2abc123\"). Used with descope_base_url for backwards compatibility.\n            descope_base_url: Your Descope base URL (e.g., \"https://api.descope.com\"). Used with project_id for backwards compatibility.\n            required_scopes: Optional list of scopes that must be present in validated tokens.\n                These scopes will be included in the protected resource metadata.\n            scopes_supported: Optional list of scopes to advertise in OAuth metadata.\n                If None, uses required_scopes. Use this when the scopes clients should\n                request differ from the scopes enforced on tokens.\n            resource_name: Optional name for the protected resource metadata.\n            resource_documentation: Optional documentation URL for the protected resource.\n            token_verifier: Optional token verifier. If None, creates JWT verifier for Descope\n        \"\"\"\n        self.base_url = AnyHttpUrl(str(base_url).rstrip(\"/\"))\n\n        # Parse scopes if provided as string\n        parsed_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else None\n        )\n\n        # Determine which API is being used\n        if config_url is not None:\n            # New API: use config_url\n            # Strip /.well-known/openid-configuration from config_url if present\n            issuer_url = str(config_url)\n            if issuer_url.endswith(\"/.well-known/openid-configuration\"):\n                issuer_url = issuer_url[: -len(\"/.well-known/openid-configuration\")]\n\n            # Parse the issuer URL to extract descope_base_url and project_id for other uses\n            parsed_url = urlparse(issuer_url)\n            path_parts = parsed_url.path.strip(\"/\").split(\"/\")\n\n            # Extract project_id from path (format: /v1/apps/agentic/P.../M...)\n            if \"agentic\" in path_parts:\n                agentic_index = path_parts.index(\"agentic\")\n                if agentic_index + 1 < len(path_parts):\n                    self.project_id = path_parts[agentic_index + 1]\n                else:\n                    raise ValueError(\n                        f\"Could not extract project_id from config_url: {issuer_url}\"\n                    )\n            else:\n                raise ValueError(\n                    f\"Could not find 'agentic' in config_url path: {issuer_url}\"\n                )\n\n            # Extract descope_base_url (scheme + netloc)\n            self.descope_base_url = f\"{parsed_url.scheme}://{parsed_url.netloc}\".rstrip(\n                \"/\"\n            )\n        elif project_id is not None and descope_base_url is not None:\n            # Old API: use project_id and descope_base_url\n            self.project_id = project_id\n            descope_base_url_str = str(descope_base_url).rstrip(\"/\")\n            # Ensure descope_base_url has a scheme\n            if not descope_base_url_str.startswith((\"http://\", \"https://\")):\n                descope_base_url_str = f\"https://{descope_base_url_str}\"\n            self.descope_base_url = descope_base_url_str\n            # Old issuer format\n            issuer_url = f\"{self.descope_base_url}/v1/apps/{self.project_id}\"\n        else:\n            raise ValueError(\n                \"Either config_url (new API) or both project_id and descope_base_url (old API) must be provided\"\n            )\n\n        # Create default JWT verifier if none provided\n        if token_verifier is None:\n            token_verifier = JWTVerifier(\n                jwks_uri=f\"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json\",\n                issuer=issuer_url,\n                algorithm=\"RS256\",\n                audience=self.project_id,\n                required_scopes=parsed_scopes,\n            )\n\n        # Initialize RemoteAuthProvider with Descope as the authorization server\n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(issuer_url)],\n            base_url=self.base_url,\n            scopes_supported=scopes_supported,\n            resource_name=resource_name,\n            resource_documentation=resource_documentation,\n        )\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get OAuth routes including Descope authorization server metadata forwarding.\n\n        This returns the standard protected resource routes plus an authorization server\n        metadata endpoint that forwards Descope's OAuth metadata to clients.\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata.\n        \"\"\"\n        # Get the standard protected resource routes from RemoteAuthProvider\n        routes = super().get_routes(mcp_path)\n\n        async def oauth_authorization_server_metadata(request):\n            \"\"\"Forward Descope OAuth authorization server metadata with FastMCP customizations.\"\"\"\n            try:\n                async with httpx.AsyncClient() as client:\n                    response = await client.get(\n                        f\"{self.descope_base_url}/v1/apps/{self.project_id}/.well-known/oauth-authorization-server\"\n                    )\n                    response.raise_for_status()\n                    metadata = response.json()\n                    return JSONResponse(metadata)\n            except Exception as e:\n                return JSONResponse(\n                    {\n                        \"error\": \"server_error\",\n                        \"error_description\": f\"Failed to fetch Descope metadata: {e}\",\n                    },\n                    status_code=500,\n                )\n\n        # Add Descope authorization server metadata forwarding\n        routes.append(\n            Route(\n                \"/.well-known/oauth-authorization-server\",\n                endpoint=oauth_authorization_server_metadata,\n                methods=[\"GET\"],\n            )\n        )\n\n        return routes\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/discord.py",
    "content": "\"\"\"Discord OAuth provider for FastMCP.\n\nThis module provides a complete Discord OAuth integration that's ready to use\nwith just a client ID and client secret. It handles all the complexity of\nDiscord's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.discord import DiscordProvider\n\n    # Simple Discord OAuth protection\n    auth = DiscordProvider(\n        client_id=\"your-discord-client-id\",\n        client_secret=\"your-discord-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport time\nfrom datetime import datetime\nfrom typing import Literal\n\nimport httpx\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth import TokenVerifier\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass DiscordTokenVerifier(TokenVerifier):\n    \"\"\"Token verifier for Discord OAuth tokens.\n\n    Discord OAuth tokens are opaque (not JWTs), so we verify them\n    by calling Discord's tokeninfo API to check if they're valid and get user info.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        expected_client_id: str,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize the Discord token verifier.\n\n        Args:\n            expected_client_id: Expected Discord OAuth client ID for audience binding\n            required_scopes: Required OAuth scopes (e.g., ['email'])\n            timeout_seconds: HTTP request timeout\n            http_client: Optional httpx.AsyncClient for connection pooling. When provided,\n                the client is reused across calls and the caller is responsible for its\n                lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        super().__init__(required_scopes=required_scopes)\n        self.expected_client_id = expected_client_id\n        self.timeout_seconds = timeout_seconds\n        self._http_client = http_client\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify Discord OAuth token by calling Discord's tokeninfo API.\"\"\"\n        try:\n            async with (\n                contextlib.nullcontext(self._http_client)\n                if self._http_client is not None\n                else httpx.AsyncClient(timeout=self.timeout_seconds)\n            ) as client:\n                # Use Discord's tokeninfo endpoint to validate the token\n                headers = {\n                    \"Authorization\": f\"Bearer {token}\",\n                    \"User-Agent\": \"FastMCP-Discord-OAuth\",\n                }\n                response = await client.get(\n                    \"https://discord.com/api/oauth2/@me\",\n                    headers=headers,\n                )\n\n                if response.status_code != 200:\n                    logger.debug(\n                        \"Discord token verification failed: %d\",\n                        response.status_code,\n                    )\n                    return None\n\n                token_info = response.json()\n\n                # Check if token is expired (Discord returns ISO timestamp)\n                expires_str = token_info.get(\"expires\")\n                expires_at = None\n                if expires_str:\n                    expires_dt = datetime.fromisoformat(\n                        expires_str.replace(\"Z\", \"+00:00\")\n                    )\n                    expires_at = int(expires_dt.timestamp())\n                    if expires_at <= int(time.time()):\n                        logger.debug(\"Discord token has expired\")\n                        return None\n\n                token_scopes = token_info.get(\"scopes\", [])\n\n                # Check required scopes\n                if self.required_scopes:\n                    token_scopes_set = set(token_scopes)\n                    required_scopes_set = set(self.required_scopes)\n                    if not required_scopes_set.issubset(token_scopes_set):\n                        logger.debug(\n                            \"Discord token missing required scopes. Has %d, needs %d\",\n                            len(token_scopes_set),\n                            len(required_scopes_set),\n                        )\n                        return None\n\n                user_data = token_info.get(\"user\", {})\n                application = token_info.get(\"application\") or {}\n                client_id = str(application.get(\"id\", \"unknown\"))\n                if client_id != self.expected_client_id:\n                    logger.debug(\n                        \"Discord token app ID mismatch: expected %s, got %s\",\n                        self.expected_client_id,\n                        client_id,\n                    )\n                    return None\n\n                # Create AccessToken with Discord user info\n                access_token = AccessToken(\n                    token=token,\n                    client_id=client_id,\n                    scopes=token_scopes,\n                    expires_at=expires_at,\n                    claims={\n                        \"sub\": user_data.get(\"id\"),\n                        \"username\": user_data.get(\"username\"),\n                        \"discriminator\": user_data.get(\"discriminator\"),\n                        \"avatar\": user_data.get(\"avatar\"),\n                        \"email\": user_data.get(\"email\"),\n                        \"verified\": user_data.get(\"verified\"),\n                        \"locale\": user_data.get(\"locale\"),\n                        \"discord_user\": user_data,\n                        \"discord_token_info\": token_info,\n                    },\n                )\n                logger.debug(\"Discord token verified successfully\")\n                return access_token\n\n        except httpx.RequestError as e:\n            logger.debug(\"Failed to verify Discord token: %s\", e)\n            return None\n        except Exception as e:\n            logger.debug(\"Discord token verification error: %s\", e)\n            return None\n\n\nclass DiscordProvider(OAuthProxy):\n    \"\"\"Complete Discord OAuth provider for FastMCP.\n\n    This provider makes it trivial to add Discord OAuth protection to any\n    FastMCP server. Just provide your Discord OAuth app credentials and\n    a base URL, and you're ready to go.\n\n    Features:\n    - Transparent OAuth proxy to Discord\n    - Automatic token validation via Discord's API\n    - User information extraction from Discord APIs\n    - Minimal configuration required\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.discord import DiscordProvider\n\n        auth = DiscordProvider(\n            client_id=\"123456789\",\n            client_secret=\"discord-client-secret-abc123...\",\n            base_url=\"https://my-server.com\"\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_id: str,\n        client_secret: str,\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        redirect_path: str | None = None,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize Discord OAuth provider.\n\n        Args:\n            client_id: Discord OAuth client ID (e.g., \"123456789\")\n            client_secret: Discord OAuth client secret (e.g., \"S....\")\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in Discord OAuth app (defaults to \"/auth/callback\")\n            required_scopes: Required Discord scopes (defaults to [\"identify\"]). Common scopes include:\n                - \"identify\" for profile info (default)\n                - \"email\" for email access\n                - \"guilds\" for server membership info\n            timeout_seconds: HTTP request timeout for Discord API calls (defaults to 10)\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to Discord.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            http_client: Optional httpx.AsyncClient for connection pooling in token verification.\n                When provided, the client is reused across verify_token calls and the caller\n                is responsible for its lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        # Parse scopes if provided as string\n        required_scopes_final = (\n            parse_scopes(required_scopes)\n            if required_scopes is not None\n            else [\"identify\"]\n        )\n\n        # Create Discord token verifier\n        token_verifier = DiscordTokenVerifier(\n            expected_client_id=client_id,\n            required_scopes=required_scopes_final,\n            timeout_seconds=timeout_seconds,\n            http_client=http_client,\n        )\n\n        # Initialize OAuth proxy with Discord endpoints\n        super().__init__(\n            upstream_authorization_endpoint=\"https://discord.com/oauth2/authorize\",\n            upstream_token_endpoint=\"https://discord.com/api/oauth2/token\",\n            upstream_client_id=client_id,\n            upstream_client_secret=client_secret,\n            token_verifier=token_verifier,\n            base_url=base_url,\n            redirect_path=redirect_path,\n            issuer_url=issuer_url or base_url,  # Default to base_url if not specified\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n        )\n\n        logger.debug(\n            \"Initialized Discord OAuth provider for client %s with scopes: %s\",\n            client_id,\n            required_scopes_final,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/github.py",
    "content": "\"\"\"GitHub OAuth provider for FastMCP.\n\nThis module provides a complete GitHub OAuth integration that's ready to use\nwith just a client ID and client secret. It handles all the complexity of\nGitHub's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.github import GitHubProvider\n\n    # Simple GitHub OAuth protection\n    auth = GitHubProvider(\n        client_id=\"your-github-client-id\",\n        client_secret=\"your-github-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nfrom typing import Literal\n\nimport httpx\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth import TokenVerifier\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.token_cache import TokenCache\n\nlogger = get_logger(__name__)\n\n\nclass GitHubTokenVerifier(TokenVerifier):\n    \"\"\"Token verifier for GitHub OAuth tokens.\n\n    GitHub OAuth tokens are opaque (not JWTs), so we verify them\n    by calling GitHub's API to check if they're valid and get user info.\n\n    Caching is disabled by default.  Set ``cache_ttl_seconds`` to a positive\n    integer to cache successful verification results and avoid repeated\n    GitHub API calls for the same token.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        cache_ttl_seconds: int | None = None,\n        max_cache_size: int | None = None,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize the GitHub token verifier.\n\n        Args:\n            required_scopes: Required OAuth scopes (e.g., ['user:email'])\n            timeout_seconds: HTTP request timeout\n            cache_ttl_seconds: How long to cache verification results in seconds.\n                Caching is disabled by default (None).  Set to a positive integer\n                to enable (e.g., 300 for 5 minutes).\n            max_cache_size: Maximum number of tokens to cache.  Default: 10 000.\n            http_client: Optional httpx.AsyncClient for connection pooling. When provided,\n                the client is reused across calls and the caller is responsible for its\n                lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        super().__init__(required_scopes=required_scopes)\n        self.timeout_seconds = timeout_seconds\n        self._http_client = http_client\n        self._cache = TokenCache(\n            ttl_seconds=cache_ttl_seconds,\n            max_size=max_cache_size,\n        )\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify GitHub OAuth token by calling GitHub API.\"\"\"\n        is_cached, cached_result = self._cache.get(token)\n        if is_cached:\n            logger.debug(\"GitHub token cache hit\")\n            return cached_result\n\n        try:\n            async with (\n                contextlib.nullcontext(self._http_client)\n                if self._http_client is not None\n                else httpx.AsyncClient(timeout=self.timeout_seconds)\n            ) as client:\n                # Get token info from GitHub API\n                response = await client.get(\n                    \"https://api.github.com/user\",\n                    headers={\n                        \"Authorization\": f\"Bearer {token}\",\n                        \"Accept\": \"application/vnd.github.v3+json\",\n                        \"User-Agent\": \"FastMCP-GitHub-OAuth\",\n                    },\n                )\n\n                if response.status_code != 200:\n                    logger.debug(\n                        \"GitHub token verification failed: %d - %s\",\n                        response.status_code,\n                        response.text[:200],\n                    )\n                    return None\n\n                user_data = response.json()\n\n                # Get token scopes from GitHub API\n                # GitHub includes scopes in the X-OAuth-Scopes header\n                scopes_response = await client.get(\n                    \"https://api.github.com/user/repos\",  # Any authenticated endpoint\n                    headers={\n                        \"Authorization\": f\"Bearer {token}\",\n                        \"Accept\": \"application/vnd.github.v3+json\",\n                        \"User-Agent\": \"FastMCP-GitHub-OAuth\",\n                    },\n                )\n\n                # Extract scopes from X-OAuth-Scopes header if available\n                scopes_verified = scopes_response.status_code == 200\n                oauth_scopes_header = scopes_response.headers.get(\"x-oauth-scopes\", \"\")\n                token_scopes = [\n                    scope.strip()\n                    for scope in oauth_scopes_header.split(\",\")\n                    if scope.strip()\n                ]\n\n                # If no scopes in header, assume basic scopes based on successful user API call\n                if not token_scopes:\n                    token_scopes = [\"user\"]  # Basic scope if we can access user info\n\n                # Check required scopes\n                if self.required_scopes:\n                    token_scopes_set = set(token_scopes)\n                    required_scopes_set = set(self.required_scopes)\n                    if not required_scopes_set.issubset(token_scopes_set):\n                        logger.debug(\n                            \"GitHub token missing required scopes. Has %d, needs %d\",\n                            len(token_scopes_set),\n                            len(required_scopes_set),\n                        )\n                        return None\n\n                # Create AccessToken with GitHub user info\n                result = AccessToken(\n                    token=token,\n                    client_id=str(user_data.get(\"id\", \"unknown\")),  # Use GitHub user ID\n                    scopes=token_scopes,\n                    expires_at=None,  # GitHub tokens don't typically expire\n                    claims={\n                        \"sub\": str(user_data[\"id\"]),\n                        \"login\": user_data.get(\"login\"),\n                        \"name\": user_data.get(\"name\"),\n                        \"email\": user_data.get(\"email\"),\n                        \"avatar_url\": user_data.get(\"avatar_url\"),\n                        \"github_user_data\": user_data,\n                    },\n                )\n                if scopes_verified:\n                    self._cache.set(token, result)\n                return result\n\n        except httpx.RequestError as e:\n            logger.debug(\"Failed to verify GitHub token: %s\", e)\n            return None\n        except Exception as e:\n            logger.debug(\"GitHub token verification error: %s\", e)\n            return None\n\n\nclass GitHubProvider(OAuthProxy):\n    \"\"\"Complete GitHub OAuth provider for FastMCP.\n\n    This provider makes it trivial to add GitHub OAuth protection to any\n    FastMCP server. Just provide your GitHub OAuth app credentials and\n    a base URL, and you're ready to go.\n\n    Features:\n    - Transparent OAuth proxy to GitHub\n    - Automatic token validation via GitHub API\n    - User information extraction\n    - Minimal configuration required\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.github import GitHubProvider\n\n        auth = GitHubProvider(\n            client_id=\"Ov23li...\",\n            client_secret=\"abc123...\",\n            base_url=\"https://my-server.com\"\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_id: str,\n        client_secret: str,\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        redirect_path: str | None = None,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        cache_ttl_seconds: int | None = None,\n        max_cache_size: int | None = None,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize GitHub OAuth provider.\n\n        Args:\n            client_id: GitHub OAuth app client ID (e.g., \"Ov23li...\")\n            client_secret: GitHub OAuth app client secret\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in GitHub OAuth app (defaults to \"/auth/callback\")\n            required_scopes: Required GitHub scopes (defaults to [\"user\"])\n            timeout_seconds: HTTP request timeout for GitHub API calls (defaults to 10)\n            cache_ttl_seconds: How long to cache token verification results in seconds.\n                Caching is disabled by default (None).  Set to a positive integer to\n                enable (e.g., 300 for 5 minutes).\n            max_cache_size: Maximum number of tokens to cache.  Default: 10 000.\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to GitHub.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            http_client: Optional httpx.AsyncClient for connection pooling in token verification.\n                When provided, the client is reused across verify_token calls and the caller\n                is responsible for its lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        # Parse scopes if provided as string\n        required_scopes_final = (\n            parse_scopes(required_scopes) if required_scopes is not None else [\"user\"]\n        )\n\n        # Create GitHub token verifier\n        token_verifier = GitHubTokenVerifier(\n            required_scopes=required_scopes_final,\n            timeout_seconds=timeout_seconds,\n            cache_ttl_seconds=cache_ttl_seconds,\n            max_cache_size=max_cache_size,\n            http_client=http_client,\n        )\n\n        # Initialize OAuth proxy with GitHub endpoints\n        super().__init__(\n            upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n            upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n            upstream_client_id=client_id,\n            upstream_client_secret=client_secret,\n            token_verifier=token_verifier,\n            base_url=base_url,\n            redirect_path=redirect_path,\n            issuer_url=issuer_url or base_url,  # Default to base_url if not specified\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n        )\n\n        logger.debug(\n            \"Initialized GitHub OAuth provider for client %s with scopes: %s\",\n            client_id,\n            required_scopes_final,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/google.py",
    "content": "\"\"\"Google OAuth provider for FastMCP.\n\nThis module provides a complete Google OAuth integration that's ready to use\nwith just a client ID and client secret. It handles all the complexity of\nGoogle's OAuth flow, token validation, and user management.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.google import GoogleProvider\n\n    # Simple Google OAuth protection\n    auth = GoogleProvider(\n        client_id=\"your-google-client-id.apps.googleusercontent.com\",\n        client_secret=\"your-google-client-secret\"\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport time\nfrom typing import Literal\n\nimport httpx\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth import TokenVerifier\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nGOOGLE_SCOPE_ALIASES: dict[str, str] = {\n    \"email\": \"https://www.googleapis.com/auth/userinfo.email\",\n    \"profile\": \"https://www.googleapis.com/auth/userinfo.profile\",\n}\n\n\ndef _normalize_google_scope(scope: str) -> str:\n    \"\"\"Normalize a Google scope shorthand to its canonical full URI.\n\n    Google accepts shorthand scopes like \"email\" and \"profile\" in authorization\n    requests, but returns the full URI form in token responses. This normalizes\n    to the full URI so comparisons work regardless of which form was used.\n    \"\"\"\n    return GOOGLE_SCOPE_ALIASES.get(scope, scope)\n\n\nclass GoogleTokenVerifier(TokenVerifier):\n    \"\"\"Token verifier for Google OAuth tokens.\n\n    Google OAuth tokens are opaque (not JWTs), so we verify them\n    by calling Google's tokeninfo API to check if they're valid and get user info.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize the Google token verifier.\n\n        Args:\n            required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])\n            timeout_seconds: HTTP request timeout\n            http_client: Optional httpx.AsyncClient for connection pooling. When provided,\n                the client is reused across calls and the caller is responsible for its\n                lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        normalized = (\n            [_normalize_google_scope(s) for s in required_scopes]\n            if required_scopes\n            else required_scopes\n        )\n        super().__init__(required_scopes=normalized)\n        self.timeout_seconds = timeout_seconds\n        self._http_client = http_client\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify Google OAuth token by calling Google's tokeninfo API.\"\"\"\n        try:\n            async with (\n                contextlib.nullcontext(self._http_client)\n                if self._http_client is not None\n                else httpx.AsyncClient(timeout=self.timeout_seconds)\n            ) as client:\n                # Use Google's tokeninfo endpoint to validate the token\n                response = await client.get(\n                    \"https://www.googleapis.com/oauth2/v1/tokeninfo\",\n                    params={\"access_token\": token},\n                    headers={\"User-Agent\": \"FastMCP-Google-OAuth\"},\n                )\n\n                if response.status_code != 200:\n                    logger.debug(\n                        \"Google token verification failed: %d\",\n                        response.status_code,\n                    )\n                    return None\n\n                token_info = response.json()\n\n                # Check if token is expired\n                expires_in = token_info.get(\"expires_in\")\n                if expires_in and int(expires_in) <= 0:\n                    logger.debug(\"Google token has expired\")\n                    return None\n\n                # Extract scopes from token info\n                scope_string = token_info.get(\"scope\", \"\")\n                token_scopes = [\n                    scope.strip() for scope in scope_string.split(\" \") if scope.strip()\n                ]\n\n                # Check required scopes\n                if self.required_scopes:\n                    token_scopes_set = set(token_scopes)\n                    required_scopes_set = set(self.required_scopes)\n                    if not required_scopes_set.issubset(token_scopes_set):\n                        logger.debug(\n                            \"Google token missing required scopes. Has %d, needs %d\",\n                            len(token_scopes_set),\n                            len(required_scopes_set),\n                        )\n                        return None\n\n                # Get additional user info if we have the right scopes\n                user_data = {}\n                if \"openid\" in token_scopes or \"profile\" in token_scopes:\n                    try:\n                        userinfo_response = await client.get(\n                            \"https://www.googleapis.com/oauth2/v2/userinfo\",\n                            headers={\n                                \"Authorization\": f\"Bearer {token}\",\n                                \"User-Agent\": \"FastMCP-Google-OAuth\",\n                            },\n                        )\n                        if userinfo_response.status_code == 200:\n                            user_data = userinfo_response.json()\n                    except Exception as e:\n                        logger.debug(\"Failed to fetch Google user info: %s\", e)\n\n                # Calculate expiration time\n                expires_at = None\n                if expires_in:\n                    expires_at = int(time.time() + int(expires_in))\n\n                # Create AccessToken with Google user info\n                access_token = AccessToken(\n                    token=token,\n                    client_id=token_info.get(\n                        \"audience\", \"unknown\"\n                    ),  # Use audience as client_id\n                    scopes=token_scopes,\n                    expires_at=expires_at,\n                    claims={\n                        \"sub\": user_data.get(\"id\")\n                        or token_info.get(\"user_id\", \"unknown\"),\n                        \"email\": user_data.get(\"email\"),\n                        \"name\": user_data.get(\"name\"),\n                        \"picture\": user_data.get(\"picture\"),\n                        \"given_name\": user_data.get(\"given_name\"),\n                        \"family_name\": user_data.get(\"family_name\"),\n                        \"locale\": user_data.get(\"locale\"),\n                        \"google_user_data\": user_data,\n                        \"google_token_info\": token_info,\n                    },\n                )\n                logger.debug(\"Google token verified successfully\")\n                return access_token\n\n        except httpx.RequestError as e:\n            logger.debug(\"Failed to verify Google token: %s\", e)\n            return None\n        except Exception as e:\n            logger.debug(\"Google token verification error: %s\", e)\n            return None\n\n\nclass GoogleProvider(OAuthProxy):\n    \"\"\"Complete Google OAuth provider for FastMCP.\n\n    This provider makes it trivial to add Google OAuth protection to any\n    FastMCP server. Just provide your Google OAuth app credentials and\n    a base URL, and you're ready to go.\n\n    Features:\n    - Transparent OAuth proxy to Google\n    - Automatic token validation via Google's tokeninfo API\n    - User information extraction from Google APIs\n    - Minimal configuration required\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.google import GoogleProvider\n\n        auth = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-abc123...\",\n            base_url=\"https://my-server.com\"\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_id: str,\n        client_secret: str | None = None,\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        redirect_path: str | None = None,\n        required_scopes: list[str] | None = None,\n        valid_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        extra_authorize_params: dict[str, str] | None = None,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize Google OAuth provider.\n\n        Args:\n            client_id: Google OAuth client ID (e.g., \"123456789.apps.googleusercontent.com\")\n            client_secret: Google OAuth client secret (e.g., \"GOCSPX-abc123...\").\n                Optional for PKCE public clients (e.g., native apps). When omitted,\n                jwt_signing_key must be provided.\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in Google OAuth app (defaults to \"/auth/callback\")\n            required_scopes: Required Google scopes (defaults to [\"openid\"]). Common scopes include:\n                - \"openid\" for OpenID Connect (default)\n                - \"https://www.googleapis.com/auth/userinfo.email\" for email access\n                - \"https://www.googleapis.com/auth/userinfo.profile\" for profile info\n                Google scope shorthands like \"email\" and \"profile\" are automatically\n                normalized to their full URI forms for token verification.\n            valid_scopes: All scopes that clients are allowed to request, advertised through\n                well-known endpoints. Defaults to required_scopes if not provided. Use this\n                when you want clients to be able to request additional scopes beyond the\n                required minimum. Shorthands are normalized to full URI forms.\n            timeout_seconds: HTTP request timeout for Google API calls (defaults to 10)\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to Google.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by Google's own consent).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            extra_authorize_params: Additional parameters to forward to Google's authorization endpoint.\n                By default, GoogleProvider sets {\"access_type\": \"offline\", \"prompt\": \"consent\"} to ensure\n                refresh tokens are returned. You can override these defaults or add additional parameters.\n                Example: {\"prompt\": \"select_account\"} to let users choose their Google account.\n            http_client: Optional httpx.AsyncClient for connection pooling in token verification.\n                When provided, the client is reused across verify_token calls and the caller\n                is responsible for its lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        # Parse scopes if provided as string\n        # Google requires at least one scope - openid is the minimal OIDC scope\n        required_scopes_final = (\n            parse_scopes(required_scopes) if required_scopes is not None else [\"openid\"]\n        )\n\n        # Normalize valid_scopes if provided\n        parsed_valid_scopes = (\n            parse_scopes(valid_scopes) if valid_scopes is not None else None\n        )\n        valid_scopes_final = (\n            [_normalize_google_scope(s) for s in parsed_valid_scopes]\n            if parsed_valid_scopes is not None\n            else None\n        )\n\n        # Create Google token verifier\n        # Normalization of shorthand scopes (e.g. \"email\" -> full URI) happens\n        # inside GoogleTokenVerifier so required_scopes match what Google returns.\n        token_verifier = GoogleTokenVerifier(\n            required_scopes=required_scopes_final,\n            timeout_seconds=timeout_seconds,\n            http_client=http_client,\n        )\n\n        # Set Google-specific defaults for extra authorize params\n        # access_type=offline ensures refresh tokens are returned\n        # prompt=consent forces consent screen to get refresh token (Google only issues on first auth otherwise)\n        google_defaults = {\n            \"access_type\": \"offline\",\n            \"prompt\": \"consent\",\n        }\n        # User-provided params override defaults\n        if extra_authorize_params:\n            google_defaults.update(extra_authorize_params)\n        extra_authorize_params_final = google_defaults\n\n        # Initialize OAuth proxy with Google endpoints\n        super().__init__(\n            upstream_authorization_endpoint=\"https://accounts.google.com/o/oauth2/v2/auth\",\n            upstream_token_endpoint=\"https://oauth2.googleapis.com/token\",\n            upstream_client_id=client_id,\n            upstream_client_secret=client_secret,\n            token_verifier=token_verifier,\n            base_url=base_url,\n            redirect_path=redirect_path,\n            issuer_url=issuer_url or base_url,  # Default to base_url if not specified\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n            extra_authorize_params=extra_authorize_params_final,\n            valid_scopes=valid_scopes_final,\n        )\n\n        logger.debug(\n            \"Initialized Google OAuth provider for client %s with scopes: %s\",\n            client_id,\n            required_scopes_final,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/in_memory.py",
    "content": "import secrets\nimport time\n\nfrom mcp.server.auth.provider import (\n    AccessToken,\n    AuthorizationCode,\n    AuthorizationParams,\n    AuthorizeError,\n    RefreshToken,\n    TokenError,\n    construct_redirect_uri,\n)\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthToken,\n)\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.auth import (\n    ClientRegistrationOptions,\n    OAuthProvider,\n    RevocationOptions,\n)\n\n# Default expiration times (in seconds)\nDEFAULT_AUTH_CODE_EXPIRY_SECONDS = 5 * 60  # 5 minutes\nDEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS = 60 * 60  # 1 hour\nDEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS = None  # No expiry\n\n\nclass InMemoryOAuthProvider(OAuthProvider):\n    \"\"\"\n    An in-memory OAuth provider for testing purposes.\n    It simulates the OAuth 2.1 flow locally without external calls.\n    \"\"\"\n\n    def __init__(\n        self,\n        base_url: AnyHttpUrl | str | None = None,\n        service_documentation_url: AnyHttpUrl | str | None = None,\n        client_registration_options: ClientRegistrationOptions | None = None,\n        revocation_options: RevocationOptions | None = None,\n        required_scopes: list[str] | None = None,\n    ):\n        super().__init__(\n            base_url=base_url or \"http://fastmcp.example.com\",\n            service_documentation_url=service_documentation_url,\n            client_registration_options=client_registration_options,\n            revocation_options=revocation_options,\n            required_scopes=required_scopes,\n        )\n        self.clients: dict[str, OAuthClientInformationFull] = {}\n        self.auth_codes: dict[str, AuthorizationCode] = {}\n        self.access_tokens: dict[str, AccessToken] = {}\n        self.refresh_tokens: dict[str, RefreshToken] = {}\n\n        # For revoking associated tokens\n        self._access_to_refresh_map: dict[\n            str, str\n        ] = {}  # access_token_str -> refresh_token_str\n        self._refresh_to_access_map: dict[\n            str, str\n        ] = {}  # refresh_token_str -> access_token_str\n\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        return self.clients.get(client_id)\n\n    async def register_client(self, client_info: OAuthClientInformationFull) -> None:\n        # Validate scopes against valid_scopes if configured (matches MCP SDK behavior)\n        if (\n            client_info.scope is not None\n            and self.client_registration_options is not None\n            and self.client_registration_options.valid_scopes is not None\n        ):\n            requested_scopes = set(client_info.scope.split())\n            valid_scopes = set(self.client_registration_options.valid_scopes)\n            invalid_scopes = requested_scopes - valid_scopes\n            if invalid_scopes:\n                raise ValueError(\n                    f\"Requested scopes are not valid: {', '.join(invalid_scopes)}\"\n                )\n\n        if client_info.client_id is None:\n            raise ValueError(\"client_id is required for client registration\")\n        if client_info.client_id in self.clients:\n            # As per RFC 7591, if client_id is already known, it's an update.\n            # For this simple provider, we'll treat it as re-registration.\n            # A real provider might handle updates or raise errors for conflicts.\n            pass\n        self.clients[client_info.client_id] = client_info\n\n    async def authorize(\n        self, client: OAuthClientInformationFull, params: AuthorizationParams\n    ) -> str:\n        \"\"\"\n        Simulates user authorization and generates an authorization code.\n        Returns a redirect URI with the code and state.\n        \"\"\"\n        if client.client_id not in self.clients:\n            raise AuthorizeError(\n                error=\"unauthorized_client\",\n                error_description=f\"Client '{client.client_id}' not registered.\",\n            )\n\n        # Validate redirect_uri (already validated by AuthorizationHandler, but good practice)\n        try:\n            # OAuthClientInformationFull should have a method like validate_redirect_uri\n            # For this test provider, we assume it's valid if it matches one in client_info\n            # The AuthorizationHandler already does robust validation using client.validate_redirect_uri\n            if client.redirect_uris and params.redirect_uri not in client.redirect_uris:\n                # This check might be too simplistic if redirect_uris can be patterns\n                # or if params.redirect_uri is None and client has a default.\n                # However, the AuthorizationHandler handles the primary validation.\n                pass  # Let's assume AuthorizationHandler did its job.\n        except Exception as e:  # Replace with specific validation error if client.validate_redirect_uri existed\n            raise AuthorizeError(\n                error=\"invalid_request\", error_description=\"Invalid redirect_uri.\"\n            ) from e\n\n        auth_code_value = f\"test_auth_code_{secrets.token_hex(16)}\"\n        expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS\n\n        # Ensure scopes are a list\n        scopes_list = params.scopes if params.scopes is not None else []\n        if client.scope:  # Filter params.scopes against client's registered scopes\n            client_allowed_scopes = set(client.scope.split())\n            scopes_list = [s for s in scopes_list if s in client_allowed_scopes]\n\n        if client.client_id is None:\n            raise AuthorizeError(\n                error=\"invalid_client\", error_description=\"Client ID is required\"\n            )\n        auth_code = AuthorizationCode(\n            code=auth_code_value,\n            client_id=client.client_id,\n            redirect_uri=params.redirect_uri,\n            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,\n            scopes=scopes_list,\n            expires_at=expires_at,\n            code_challenge=params.code_challenge,\n            # code_challenge_method is assumed S256 by the framework\n        )\n        self.auth_codes[auth_code_value] = auth_code\n\n        return construct_redirect_uri(\n            str(params.redirect_uri), code=auth_code_value, state=params.state\n        )\n\n    async def load_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: str\n    ) -> AuthorizationCode | None:\n        auth_code_obj = self.auth_codes.get(authorization_code)\n        if auth_code_obj:\n            if auth_code_obj.client_id != client.client_id:\n                return None  # Belongs to a different client\n            if auth_code_obj.expires_at < time.time():\n                del self.auth_codes[authorization_code]  # Expired\n                return None\n            return auth_code_obj\n        return None\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode\n    ) -> OAuthToken:\n        # Authorization code should have been validated (existence, expiry, client_id match)\n        # by the TokenHandler calling load_authorization_code before this.\n        # We might want to re-verify or simply trust it's valid.\n\n        if authorization_code.code not in self.auth_codes:\n            raise TokenError(\n                \"invalid_grant\", \"Authorization code not found or already used.\"\n            )\n\n        # Consume the auth code\n        del self.auth_codes[authorization_code.code]\n\n        access_token_value = f\"test_access_token_{secrets.token_hex(32)}\"\n        refresh_token_value = f\"test_refresh_token_{secrets.token_hex(32)}\"\n\n        access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)\n\n        # Refresh token expiry\n        refresh_token_expires_at = None\n        if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:\n            refresh_token_expires_at = int(\n                time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS\n            )\n\n        if client.client_id is None:\n            raise TokenError(\"invalid_client\", \"Client ID is required\")\n        self.access_tokens[access_token_value] = AccessToken(\n            token=access_token_value,\n            client_id=client.client_id,\n            scopes=authorization_code.scopes,\n            expires_at=access_token_expires_at,\n        )\n        self.refresh_tokens[refresh_token_value] = RefreshToken(\n            token=refresh_token_value,\n            client_id=client.client_id,\n            scopes=authorization_code.scopes,  # Refresh token inherits scopes\n            expires_at=refresh_token_expires_at,\n        )\n\n        self._access_to_refresh_map[access_token_value] = refresh_token_value\n        self._refresh_to_access_map[refresh_token_value] = access_token_value\n\n        return OAuthToken(\n            access_token=access_token_value,\n            token_type=\"Bearer\",\n            expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,\n            refresh_token=refresh_token_value,\n            scope=\" \".join(authorization_code.scopes),\n        )\n\n    async def load_refresh_token(\n        self, client: OAuthClientInformationFull, refresh_token: str\n    ) -> RefreshToken | None:\n        token_obj = self.refresh_tokens.get(refresh_token)\n        if token_obj:\n            if token_obj.client_id != client.client_id:\n                return None  # Belongs to different client\n            if token_obj.expires_at is not None and token_obj.expires_at < time.time():\n                self._revoke_internal(\n                    refresh_token_str=token_obj.token\n                )  # Clean up expired\n                return None\n            return token_obj\n        return None\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshToken,  # This is the RefreshToken object, already loaded\n        scopes: list[str],  # Requested scopes for the new access token\n    ) -> OAuthToken:\n        # Validate scopes: requested scopes must be a subset of original scopes\n        original_scopes = set(refresh_token.scopes)\n        requested_scopes = set(scopes)\n        if not requested_scopes.issubset(original_scopes):\n            raise TokenError(\n                \"invalid_scope\",\n                \"Requested scopes exceed those authorized by the refresh token.\",\n            )\n\n        # Invalidate old refresh token and its associated access token (rotation)\n        self._revoke_internal(refresh_token_str=refresh_token.token)\n\n        # Issue new tokens\n        new_access_token_value = f\"test_access_token_{secrets.token_hex(32)}\"\n        new_refresh_token_value = f\"test_refresh_token_{secrets.token_hex(32)}\"\n\n        access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)\n\n        # Refresh token expiry\n        refresh_token_expires_at = None\n        if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:\n            refresh_token_expires_at = int(\n                time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS\n            )\n\n        if client.client_id is None:\n            raise TokenError(\"invalid_client\", \"Client ID is required\")\n        self.access_tokens[new_access_token_value] = AccessToken(\n            token=new_access_token_value,\n            client_id=client.client_id,\n            scopes=scopes,  # Use newly requested (and validated) scopes\n            expires_at=access_token_expires_at,\n        )\n        self.refresh_tokens[new_refresh_token_value] = RefreshToken(\n            token=new_refresh_token_value,\n            client_id=client.client_id,\n            scopes=scopes,  # New refresh token also gets these scopes\n            expires_at=refresh_token_expires_at,\n        )\n\n        self._access_to_refresh_map[new_access_token_value] = new_refresh_token_value\n        self._refresh_to_access_map[new_refresh_token_value] = new_access_token_value\n\n        return OAuthToken(\n            access_token=new_access_token_value,\n            token_type=\"Bearer\",\n            expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,\n            refresh_token=new_refresh_token_value,\n            scope=\" \".join(scopes),\n        )\n\n    async def load_access_token(self, token: str) -> AccessToken | None:  # type: ignore[override]\n        token_obj = self.access_tokens.get(token)\n        if token_obj:\n            if token_obj.expires_at is not None and token_obj.expires_at < time.time():\n                self._revoke_internal(\n                    access_token_str=token_obj.token\n                )  # Clean up expired\n                return None\n            return token_obj\n        return None\n\n    async def verify_token(self, token: str) -> AccessToken | None:  # type: ignore[override]\n        \"\"\"\n        Verify a bearer token and return access info if valid.\n\n        This method implements the TokenVerifier protocol by delegating\n        to our existing load_access_token method.\n\n        Args:\n            token: The token string to validate\n\n        Returns:\n            AccessToken object if valid, None if invalid or expired\n        \"\"\"\n        return await self.load_access_token(token)\n\n    def _revoke_internal(\n        self, access_token_str: str | None = None, refresh_token_str: str | None = None\n    ):\n        \"\"\"Internal helper to remove tokens and their associations.\"\"\"\n        removed_access_token = None\n        removed_refresh_token = None\n\n        if access_token_str:\n            if access_token_str in self.access_tokens:\n                del self.access_tokens[access_token_str]\n                removed_access_token = access_token_str\n\n            # Get associated refresh token\n            associated_refresh = self._access_to_refresh_map.pop(access_token_str, None)\n            if associated_refresh:\n                if associated_refresh in self.refresh_tokens:\n                    del self.refresh_tokens[associated_refresh]\n                    removed_refresh_token = associated_refresh\n                self._refresh_to_access_map.pop(associated_refresh, None)\n\n        if refresh_token_str:\n            if refresh_token_str in self.refresh_tokens:\n                del self.refresh_tokens[refresh_token_str]\n                removed_refresh_token = refresh_token_str\n\n            # Get associated access token\n            associated_access = self._refresh_to_access_map.pop(refresh_token_str, None)\n            if associated_access:\n                if associated_access in self.access_tokens:\n                    del self.access_tokens[associated_access]\n                    removed_access_token = associated_access\n                self._access_to_refresh_map.pop(associated_access, None)\n\n        # Clean up any dangling references if one part of the pair was already gone\n        if removed_access_token and removed_access_token in self._access_to_refresh_map:\n            del self._access_to_refresh_map[removed_access_token]\n        if (\n            removed_refresh_token\n            and removed_refresh_token in self._refresh_to_access_map\n        ):\n            del self._refresh_to_access_map[removed_refresh_token]\n\n    async def revoke_token(\n        self,\n        token: AccessToken | RefreshToken,\n    ) -> None:\n        \"\"\"Revokes an access or refresh token and its counterpart.\"\"\"\n        if isinstance(token, AccessToken):\n            self._revoke_internal(access_token_str=token.token)\n        elif isinstance(token, RefreshToken):\n            self._revoke_internal(refresh_token_str=token.token)\n        # If token is not found or already revoked, _revoke_internal does nothing, which is correct.\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/introspection.py",
    "content": "\"\"\"OAuth 2.0 Token Introspection (RFC 7662) provider for FastMCP.\n\nThis module provides token verification for opaque tokens using the OAuth 2.0\nToken Introspection protocol defined in RFC 7662. It allows FastMCP servers to\nvalidate tokens issued by authorization servers that don't use JWT format.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\n\n    # Verify opaque tokens via RFC 7662 introspection\n    verifier = IntrospectionTokenVerifier(\n        introspection_url=\"https://auth.example.com/oauth/introspect\",\n        client_id=\"your-client-id\",\n        client_secret=\"your-client-secret\",\n        required_scopes=[\"read\", \"write\"]\n    )\n\n    mcp = FastMCP(\"My Protected Server\", auth=verifier)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport contextlib\nimport time\nfrom typing import Any, Literal, get_args\n\nimport httpx\nfrom pydantic import AnyHttpUrl, SecretStr\n\nfrom fastmcp.server.auth import AccessToken, TokenVerifier\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.token_cache import TokenCache\n\nlogger = get_logger(__name__)\n\n\nClientAuthMethod = Literal[\"client_secret_basic\", \"client_secret_post\"]\n\n\nclass IntrospectionTokenVerifier(TokenVerifier):\n    \"\"\"\n    OAuth 2.0 Token Introspection verifier (RFC 7662).\n\n    This verifier validates opaque tokens by calling an OAuth 2.0 token introspection\n    endpoint. Unlike JWT verification which is stateless, token introspection requires\n    a network call to the authorization server for each token validation.\n\n    The verifier authenticates to the introspection endpoint using either:\n    - HTTP Basic Auth (client_secret_basic, default): credentials in Authorization header\n    - POST body authentication (client_secret_post): credentials in request body\n\n    Both methods are specified in RFC 6749 (OAuth 2.0) and RFC 7662 (Token Introspection).\n\n    Use this when:\n    - Your authorization server issues opaque (non-JWT) tokens\n    - You need to validate tokens from Auth0, Okta, Keycloak, or other OAuth servers\n    - Your tokens require real-time revocation checking\n    - Your authorization server supports RFC 7662 introspection\n\n    Caching is disabled by default to preserve real-time revocation semantics.\n    Set ``cache_ttl_seconds`` to enable caching and reduce load on the\n    introspection endpoint (e.g., ``cache_ttl_seconds=300`` for 5 minutes).\n\n    Example:\n        ```python\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"my-service\",\n            client_secret=\"secret-key\",\n            required_scopes=[\"api:read\"]\n        )\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        introspection_url: str,\n        client_id: str,\n        client_secret: str | SecretStr,\n        client_auth_method: ClientAuthMethod = \"client_secret_basic\",\n        timeout_seconds: int = 10,\n        required_scopes: list[str] | None = None,\n        base_url: AnyHttpUrl | str | None = None,\n        cache_ttl_seconds: int | None = None,\n        max_cache_size: int | None = None,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"\n        Initialize the introspection token verifier.\n\n        Args:\n            introspection_url: URL of the OAuth 2.0 token introspection endpoint\n            client_id: OAuth client ID for authenticating to the introspection endpoint\n            client_secret: OAuth client secret for authenticating to the introspection endpoint\n            client_auth_method: Client authentication method. \"client_secret_basic\" (default)\n                uses HTTP Basic Auth header, \"client_secret_post\" sends credentials in POST body\n            timeout_seconds: HTTP request timeout in seconds (default: 10)\n            required_scopes: Required scopes for all tokens (optional)\n            base_url: Base URL for TokenVerifier protocol\n            cache_ttl_seconds: How long to cache introspection results in seconds.\n                Caching is disabled by default (None) to preserve real-time\n                revocation semantics. Set to a positive integer to enable caching\n                (e.g., 300 for 5 minutes).\n            max_cache_size: Maximum number of tokens to cache when caching is\n                enabled. Default: 10000.\n            http_client: Optional httpx.AsyncClient for connection pooling. When provided,\n                the client is reused across calls and the caller is responsible for its\n                lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        # Parse scopes if provided as string\n        parsed_required_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else None\n        )\n\n        super().__init__(base_url=base_url, required_scopes=parsed_required_scopes)\n\n        self.introspection_url = introspection_url\n        self.client_id = client_id\n        self.client_secret = (\n            client_secret.get_secret_value()\n            if isinstance(client_secret, SecretStr)\n            else client_secret\n        )\n\n        # Validate client_auth_method to catch typos/invalid values early\n        valid_methods = get_args(ClientAuthMethod)\n        if client_auth_method not in valid_methods:\n            options = \" or \".join(f\"'{m}'\" for m in valid_methods)\n            raise ValueError(\n                f\"Invalid client_auth_method: {client_auth_method!r}. \"\n                f\"Must be {options}.\"\n            )\n        self.client_auth_method: ClientAuthMethod = client_auth_method\n\n        self.timeout_seconds = timeout_seconds\n        self._http_client = http_client\n        self.logger = get_logger(__name__)\n\n        self._cache = TokenCache(\n            ttl_seconds=cache_ttl_seconds,\n            max_size=max_cache_size,\n        )\n\n    def _create_basic_auth_header(self) -> str:\n        \"\"\"Create HTTP Basic Auth header value from client credentials.\"\"\"\n        credentials = f\"{self.client_id}:{self.client_secret}\"\n        encoded = base64.b64encode(credentials.encode(\"utf-8\")).decode(\"utf-8\")\n        return f\"Basic {encoded}\"\n\n    def _extract_scopes(self, introspection_response: dict[str, Any]) -> list[str]:\n        \"\"\"\n        Extract scopes from introspection response.\n\n        RFC 7662 allows scopes to be returned as either:\n        - A space-separated string in the 'scope' field\n        - An array of strings in the 'scope' field (less common but valid)\n        \"\"\"\n        scope_value = introspection_response.get(\"scope\")\n\n        if scope_value is None:\n            return []\n\n        # Handle string (space-separated) scopes\n        if isinstance(scope_value, str):\n            return [s.strip() for s in scope_value.split() if s.strip()]\n\n        # Handle array of scopes\n        if isinstance(scope_value, list):\n            return [str(s) for s in scope_value if s]\n\n        return []\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"\n        Verify a bearer token using OAuth 2.0 Token Introspection (RFC 7662).\n\n        This method makes a POST request to the introspection endpoint with the token,\n        authenticated using the configured client authentication method (client_secret_basic\n        or client_secret_post).\n\n        Results are cached in-memory to reduce load on the introspection endpoint.\n        Cache TTL and size are configurable via constructor parameters.\n\n        Args:\n            token: The opaque token string to validate\n\n        Returns:\n            AccessToken object if valid and active, None if invalid, inactive, or expired\n        \"\"\"\n        # Check cache first\n        is_cached, cached_result = self._cache.get(token)\n        if is_cached:\n            self.logger.debug(\"Token introspection cache hit\")\n            return cached_result\n\n        try:\n            async with (\n                contextlib.nullcontext(self._http_client)\n                if self._http_client is not None\n                else httpx.AsyncClient(timeout=self.timeout_seconds)\n            ) as client:\n                # Prepare introspection request per RFC 7662\n                # Build request data with token and token_type_hint\n                data = {\n                    \"token\": token,\n                    \"token_type_hint\": \"access_token\",\n                }\n\n                # Build headers\n                headers = {\n                    \"Content-Type\": \"application/x-www-form-urlencoded\",\n                    \"Accept\": \"application/json\",\n                }\n\n                # Add client authentication based on method\n                if self.client_auth_method == \"client_secret_basic\":\n                    headers[\"Authorization\"] = self._create_basic_auth_header()\n                elif self.client_auth_method == \"client_secret_post\":\n                    data[\"client_id\"] = self.client_id\n                    data[\"client_secret\"] = self.client_secret\n\n                response = await client.post(\n                    self.introspection_url,\n                    data=data,\n                    headers=headers,\n                )\n\n                # Check for HTTP errors - don't cache HTTP errors (may be transient)\n                if response.status_code != 200:\n                    self.logger.debug(\n                        \"Token introspection failed: HTTP %d - %s\",\n                        response.status_code,\n                        response.text[:200] if response.text else \"\",\n                    )\n                    return None\n\n                introspection_data = response.json()\n\n                # Check if token is active (required field per RFC 7662)\n                # Don't cache inactive tokens - they may become valid later\n                # (e.g., tokens with future nbf, or propagation delays)\n                if not introspection_data.get(\"active\", False):\n                    self.logger.debug(\"Token introspection returned active=false\")\n                    return None\n\n                # Extract client_id (should be present for active tokens)\n                client_id = introspection_data.get(\n                    \"client_id\"\n                ) or introspection_data.get(\"sub\", \"unknown\")\n\n                # Extract expiration time\n                exp = introspection_data.get(\"exp\")\n                if exp:\n                    # Validate expiration (belt and suspenders - server should set active=false)\n                    if exp < time.time():\n                        self.logger.debug(\n                            \"Token validation failed: expired token for client %s\",\n                            client_id,\n                        )\n                        return None\n\n                # Extract scopes\n                scopes = self._extract_scopes(introspection_data)\n\n                # Check required scopes\n                # Don't cache scope failures - permissions may be updated dynamically\n                if self.required_scopes:\n                    token_scopes = set(scopes)\n                    required_scopes = set(self.required_scopes)\n                    if not required_scopes.issubset(token_scopes):\n                        self.logger.debug(\n                            \"Token missing required scopes. Has: %s, Required: %s\",\n                            token_scopes,\n                            required_scopes,\n                        )\n                        return None\n\n                # Create AccessToken with introspection response data\n                result = AccessToken(\n                    token=token,\n                    client_id=str(client_id),\n                    scopes=scopes,\n                    expires_at=int(exp) if exp else None,\n                    claims=introspection_data,  # Store full response for extensibility\n                )\n                self._cache.set(token, result)\n                return result\n\n        except httpx.TimeoutException:\n            self.logger.debug(\n                \"Token introspection timed out after %d seconds\", self.timeout_seconds\n            )\n            return None\n        except httpx.RequestError as e:\n            self.logger.debug(\"Token introspection request failed: %s\", e)\n            return None\n        except Exception as e:\n            self.logger.debug(\"Token introspection error: %s\", e)\n            return None\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/jwt.py",
    "content": "\"\"\"TokenVerifier implementations for FastMCP.\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport json\nimport time\nfrom dataclasses import dataclass\nfrom typing import Any, cast\n\nimport httpx\nfrom authlib.jose import JsonWebKey, JsonWebToken\nfrom authlib.jose.errors import JoseError\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom pydantic import AnyHttpUrl, SecretStr\nfrom typing_extensions import TypedDict\n\nfrom fastmcp.server.auth import AccessToken, TokenVerifier\nfrom fastmcp.server.auth.ssrf import SSRFError, SSRFFetchError, ssrf_safe_fetch\nfrom fastmcp.utilities.auth import decode_jwt_header, parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass JWKData(TypedDict, total=False):\n    \"\"\"JSON Web Key data structure.\"\"\"\n\n    kty: str  # Key type (e.g., \"RSA\") - required\n    kid: str  # Key ID (optional but recommended)\n    use: str  # Usage (e.g., \"sig\")\n    alg: str  # Algorithm (e.g., \"RS256\")\n    n: str  # Modulus (for RSA keys)\n    e: str  # Exponent (for RSA keys)\n    x5c: list[str]  # X.509 certificate chain (for JWKs)\n    x5t: str  # X.509 certificate thumbprint (for JWKs)\n\n\nclass JWKSData(TypedDict):\n    \"\"\"JSON Web Key Set data structure.\"\"\"\n\n    keys: list[JWKData]\n\n\n@dataclass(frozen=True, kw_only=True, repr=False)\nclass RSAKeyPair:\n    \"\"\"RSA key pair for JWT testing.\"\"\"\n\n    private_key: SecretStr\n    public_key: str\n\n    @classmethod\n    def generate(cls) -> RSAKeyPair:\n        \"\"\"\n        Generate an RSA key pair for testing.\n\n        Returns:\n            RSAKeyPair: Generated key pair\n        \"\"\"\n        # Generate private key\n        private_key = rsa.generate_private_key(\n            public_exponent=65537,\n            key_size=2048,\n        )\n\n        # Serialize private key to PEM format\n        private_pem = private_key.private_bytes(\n            encoding=serialization.Encoding.PEM,\n            format=serialization.PrivateFormat.PKCS8,\n            encryption_algorithm=serialization.NoEncryption(),\n        ).decode(\"utf-8\")\n\n        # Serialize public key to PEM format\n        public_pem = (\n            private_key.public_key()\n            .public_bytes(\n                encoding=serialization.Encoding.PEM,\n                format=serialization.PublicFormat.SubjectPublicKeyInfo,\n            )\n            .decode(\"utf-8\")\n        )\n\n        return cls(\n            private_key=SecretStr(private_pem),\n            public_key=public_pem,\n        )\n\n    def create_token(\n        self,\n        subject: str = \"fastmcp-user\",\n        issuer: str = \"https://fastmcp.example.com\",\n        audience: str | list[str] | None = None,\n        scopes: list[str] | None = None,\n        expires_in_seconds: int = 3600,\n        additional_claims: dict[str, Any] | None = None,\n        kid: str | None = None,\n    ) -> str:\n        \"\"\"\n        Generate a test JWT token for testing purposes.\n\n        Args:\n            subject: Subject claim (usually user ID)\n            issuer: Issuer claim\n            audience: Audience claim - can be a string or list of strings (optional)\n            scopes: List of scopes to include\n            expires_in_seconds: Token expiration time in seconds\n            additional_claims: Any additional claims to include\n            kid: Key ID to include in header\n        \"\"\"\n        # Create header\n        header = {\"alg\": \"RS256\"}\n        if kid:\n            header[\"kid\"] = kid\n\n        # Create payload\n        payload: dict[str, str | int | list[str]] = {\n            \"sub\": subject,\n            \"iss\": issuer,\n            \"iat\": int(time.time()),\n            \"exp\": int(time.time()) + expires_in_seconds,\n        }\n\n        if audience:\n            payload[\"aud\"] = audience\n\n        if scopes:\n            payload[\"scope\"] = \" \".join(scopes)\n\n        if additional_claims:\n            payload.update(additional_claims)\n\n        # Create JWT\n        jwt_lib = JsonWebToken([\"RS256\"])\n        token_bytes = jwt_lib.encode(\n            header, payload, self.private_key.get_secret_value()\n        )\n\n        return token_bytes.decode(\"utf-8\")\n\n\ndef _looks_like_pem_public_key(key: str | bytes) -> bool:\n    \"\"\"Return True when key text appears to be PEM-encoded asymmetric key material.\"\"\"\n    if isinstance(key, bytes):\n        key = key.decode(\"utf-8\", errors=\"replace\")\n    key_text = key.strip()\n    pem_markers = (\n        \"-----BEGIN PUBLIC KEY-----\",\n        \"-----BEGIN RSA PUBLIC KEY-----\",\n        \"-----BEGIN EC PUBLIC KEY-----\",\n        \"-----BEGIN CERTIFICATE-----\",\n    )\n    return any(marker in key_text for marker in pem_markers)\n\n\nclass JWTVerifier(TokenVerifier):\n    \"\"\"\n    JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.\n\n    This verifier validates JWT tokens using various signing algorithms:\n    - **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):\n      Uses public/private key pairs. Ideal for external clients and services where\n      only the authorization server has the private key.\n    - **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both\n      signing and verification. Perfect for internal microservices and trusted\n      environments where the secret can be securely shared.\n\n    Use this when:\n    - You have JWT tokens issued by an external service (asymmetric)\n    - You need JWKS support for automatic key rotation (asymmetric)\n    - You have internal microservices sharing a secret key (symmetric)\n    - Your tokens contain standard OAuth scopes and claims\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        public_key: str | bytes | None = None,\n        jwks_uri: str | None = None,\n        issuer: str | list[str] | None = None,\n        audience: str | list[str] | None = None,\n        algorithm: str | None = None,\n        required_scopes: list[str] | None = None,\n        base_url: AnyHttpUrl | str | None = None,\n        ssrf_safe: bool = False,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"\n        Initialize a JWTVerifier configured to validate JWTs using either a static key or a JWKS endpoint.\n\n        Parameters:\n            public_key: PEM-encoded public key for asymmetric algorithms or shared secret for symmetric algorithms.\n            jwks_uri: URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS.\n            issuer: Expected issuer claim value or list of allowed issuer values.\n            audience: Expected audience claim value or list of allowed audience values.\n            algorithm: JWT signing algorithm to accept (default: \"RS256\"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512.\n            required_scopes: Scopes that must be present in validated tokens.\n            base_url: Base URL passed to the parent TokenVerifier.\n            ssrf_safe: If True, JWKS fetches use SSRF protection (HTTPS-only,\n                public IPs, DNS pinning). Enable when the JWKS URI comes from\n                untrusted input (e.g. CIMD documents). Defaults to False so\n                operator-configured JWKS URIs (including localhost) work normally.\n            http_client: Optional httpx.AsyncClient for connection pooling. When provided,\n                the client is reused for JWKS fetches and the caller is responsible for\n                its lifecycle. When None (default), a fresh client is created per fetch.\n                Cannot be used with ssrf_safe=True.\n\n        Raises:\n            ValueError: If neither or both of `public_key` and `jwks_uri` are provided,\n                if `algorithm` is unsupported, or if `http_client` is provided with `ssrf_safe=True`.\n        \"\"\"\n        if not public_key and not jwks_uri:\n            raise ValueError(\"Either public_key or jwks_uri must be provided\")\n\n        if public_key and jwks_uri:\n            raise ValueError(\"Provide either public_key or jwks_uri, not both\")\n\n        # Only enforce ssrf_safe/http_client exclusivity when JWKS fetching is used\n        if jwks_uri and ssrf_safe and http_client is not None:\n            raise ValueError(\n                \"http_client cannot be used with ssrf_safe=True; \"\n                \"SSRF-safe mode requires its own hardened transport\"\n            )\n\n        algorithm = algorithm or \"RS256\"\n        if algorithm not in {\n            \"HS256\",\n            \"HS384\",\n            \"HS512\",\n            \"RS256\",\n            \"RS384\",\n            \"RS512\",\n            \"ES256\",\n            \"ES384\",\n            \"ES512\",\n            \"PS256\",\n            \"PS384\",\n            \"PS512\",\n        }:\n            raise ValueError(f\"Unsupported algorithm: {algorithm}.\")\n\n        if algorithm.startswith(\"HS\"):\n            if jwks_uri:\n                raise ValueError(\n                    \"Symmetric HS* algorithms cannot be used with jwks_uri; \"\n                    \"configure a shared secret via public_key instead.\"\n                )\n            if public_key and _looks_like_pem_public_key(public_key):\n                raise ValueError(\n                    \"Symmetric HS* algorithms require a shared secret, not a public key.\"\n                )\n\n        # Parse scopes if provided as string\n        parsed_required_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else None\n        )\n\n        # Initialize parent TokenVerifier\n        super().__init__(\n            base_url=base_url,\n            required_scopes=parsed_required_scopes,\n        )\n\n        self.algorithm = algorithm\n        self.issuer = issuer\n        self.audience = audience\n        self.public_key = public_key\n        self.jwks_uri = jwks_uri\n        self.ssrf_safe = ssrf_safe\n        self._http_client = http_client\n        self.jwt = JsonWebToken([self.algorithm])\n        self.logger = get_logger(__name__)\n\n        # Simple JWKS cache\n        self._jwks_cache: dict[str, str] = {}\n        self._jwks_cache_time: float = 0\n        self._cache_ttl = 3600  # 1 hour\n\n    async def _get_verification_key(self, token: str) -> str | bytes:\n        \"\"\"Get the verification key for the token.\"\"\"\n        if self.public_key:\n            return self.public_key\n\n        # Extract kid from token header for JWKS lookup\n        try:\n            header = decode_jwt_header(token)\n            kid = header.get(\"kid\")\n            return await self._get_jwks_key(kid)\n\n        except (ValueError, KeyError, IndexError, json.JSONDecodeError) as e:\n            raise ValueError(f\"Failed to extract key ID from token: {e}\") from e\n\n    async def _get_jwks_key(self, kid: str | None) -> str:\n        \"\"\"Fetch key from JWKS with simple caching and SSRF protection.\"\"\"\n        if not self.jwks_uri:\n            raise ValueError(\"JWKS URI not configured\")\n\n        current_time = time.time()\n\n        # Check cache first\n        if current_time - self._jwks_cache_time < self._cache_ttl:\n            if kid and kid in self._jwks_cache:\n                return self._jwks_cache[kid]\n            elif not kid and len(self._jwks_cache) == 1:\n                # If no kid but only one key cached, use it\n                return next(iter(self._jwks_cache.values()))\n\n        # Fetch JWKS — with SSRF protection when enabled (untrusted URIs)\n        try:\n            jwks_data = await self._fetch_jwks()\n\n            # Cache all keys\n            self._jwks_cache = {}\n            for key_data in jwks_data.get(\"keys\", []):\n                key_kid = key_data.get(\"kid\")\n                jwk = JsonWebKey.import_key(key_data)\n                public_key = jwk.get_public_key()\n\n                if key_kid:\n                    self._jwks_cache[key_kid] = public_key\n                else:\n                    # Key without kid - use a default identifier\n                    self._jwks_cache[\"_default\"] = public_key\n\n            self._jwks_cache_time = current_time\n\n            # Select the appropriate key\n            if kid:\n                if kid not in self._jwks_cache:\n                    self.logger.debug(\n                        \"JWKS key lookup failed: key ID '%s' not found\", kid\n                    )\n                    raise ValueError(f\"Key ID '{kid}' not found in JWKS\")\n                return self._jwks_cache[kid]\n            else:\n                # No kid in token - only allow if there's exactly one key\n                if len(self._jwks_cache) == 1:\n                    return next(iter(self._jwks_cache.values()))\n                elif len(self._jwks_cache) > 1:\n                    raise ValueError(\n                        \"Multiple keys in JWKS but no key ID (kid) in token\"\n                    )\n                else:\n                    raise ValueError(\"No keys found in JWKS\")\n\n        except (SSRFError, SSRFFetchError) as e:\n            self.logger.debug(\"JWKS fetch blocked by SSRF protection: %s\", e)\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except httpx.HTTPError as e:\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except json.JSONDecodeError as e:\n            raise ValueError(f\"Invalid JWKS JSON: {e}\") from e\n        except (JoseError, TypeError, KeyError) as e:\n            self.logger.debug(\"JWKS key processing failed: %s\", e)\n            raise ValueError(f\"Failed to process JWKS: {e}\") from e\n\n    async def _fetch_jwks(self) -> dict[str, Any]:\n        \"\"\"Fetch JWKS data, using SSRF-safe or standard fetch based on config.\"\"\"\n        if not self.jwks_uri:\n            raise ValueError(\"JWKS URI not configured\")\n\n        if self.ssrf_safe:\n            content = await ssrf_safe_fetch(\n                self.jwks_uri,\n                max_size=65536,\n                timeout=10.0,\n                overall_timeout=30.0,\n            )\n            return json.loads(content)\n        else:\n            async with (\n                contextlib.nullcontext(self._http_client)\n                if self._http_client is not None\n                else httpx.AsyncClient(timeout=httpx.Timeout(10.0))\n            ) as client:\n                response = await client.get(self.jwks_uri)\n                response.raise_for_status()\n                return response.json()\n\n    def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:\n        \"\"\"\n        Extract scopes from JWT claims. Supports both 'scope' and 'scp'\n        claims.\n\n        Checks the `scope` claim first (standard OAuth2 claim), then the `scp`\n        claim (used by some Identity Providers).\n        \"\"\"\n        for claim in [\"scope\", \"scp\"]:\n            if claim in claims:\n                if isinstance(claims[claim], str):\n                    return claims[claim].split()\n                elif isinstance(claims[claim], list):\n                    return claims[claim]\n\n        return []\n\n    async def load_access_token(self, token: str) -> AccessToken | None:\n        \"\"\"\n        Validate a JWT bearer token and return an AccessToken when the token is valid.\n\n        Parameters:\n            token (str): The JWT bearer token string to validate.\n\n        Returns:\n            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.\n        \"\"\"\n        try:\n            # Get verification key (static or from JWKS)\n            verification_key = await self._get_verification_key(token)\n\n            # Decode and verify the JWT token\n            claims = self.jwt.decode(token, verification_key)\n\n            # Extract client ID early for logging\n            client_id = (\n                claims.get(\"client_id\")\n                or claims.get(\"azp\")\n                or claims.get(\"sub\")\n                or \"unknown\"\n            )\n\n            # Validate expiration\n            exp = claims.get(\"exp\")\n            if exp and exp < time.time():\n                self.logger.debug(\n                    \"Token validation failed: expired token for client %s\", client_id\n                )\n                self.logger.info(\"Bearer token rejected for client %s\", client_id)\n                return None\n\n            # Validate issuer - note we use issuer instead of issuer_url here because\n            # issuer is optional, allowing users to make this check optional\n            if self.issuer:\n                iss = claims.get(\"iss\")\n\n                # Handle different combinations of issuer types\n                issuer_valid = False\n                if isinstance(self.issuer, list):\n                    # self.issuer is a list - check if token issuer matches any expected issuer\n                    issuer_valid = iss in self.issuer\n                else:\n                    # self.issuer is a string - check for equality\n                    issuer_valid = iss == self.issuer\n\n                if not issuer_valid:\n                    self.logger.debug(\n                        \"Token validation failed: issuer mismatch for client %s\",\n                        client_id,\n                    )\n                    self.logger.info(\"Bearer token rejected for client %s\", client_id)\n                    return None\n\n            # Validate audience if configured\n            if self.audience:\n                aud = claims.get(\"aud\")\n\n                # Handle different combinations of audience types\n                audience_valid = False\n                if isinstance(self.audience, list):\n                    # self.audience is a list - check if any expected audience is present\n                    if isinstance(aud, list):\n                        # Both are lists - check for intersection\n                        audience_valid = any(\n                            expected in aud for expected in self.audience\n                        )\n                    else:\n                        # aud is a string - check if it's in our expected list\n                        audience_valid = aud in cast(list, self.audience)\n                else:\n                    # self.audience is a string - use original logic\n                    if isinstance(aud, list):\n                        audience_valid = self.audience in aud\n                    else:\n                        audience_valid = aud == self.audience\n\n                if not audience_valid:\n                    self.logger.debug(\n                        \"Token validation failed: audience mismatch for client %s\",\n                        client_id,\n                    )\n                    self.logger.info(\"Bearer token rejected for client %s\", client_id)\n                    return None\n\n            # Extract scopes\n            scopes = self._extract_scopes(claims)\n\n            # Check required scopes\n            if self.required_scopes:\n                token_scopes = set(scopes)\n                required_scopes = set(self.required_scopes)\n                if not required_scopes.issubset(token_scopes):\n                    self.logger.debug(\n                        \"Token missing required scopes. Has: %s, Required: %s\",\n                        token_scopes,\n                        required_scopes,\n                    )\n                    self.logger.info(\"Bearer token rejected for client %s\", client_id)\n                    return None\n\n            return AccessToken(\n                token=token,\n                client_id=str(client_id),\n                scopes=scopes,\n                expires_at=int(exp) if exp else None,\n                claims=claims,\n            )\n\n        except JoseError:\n            self.logger.debug(\"Token validation failed: JWT signature/format invalid\")\n            return None\n        except (ValueError, TypeError, KeyError, AttributeError) as e:\n            self.logger.debug(\"Token validation failed: %s\", str(e))\n            return None\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"\n        Verify a bearer token and return access info if valid.\n\n        This method implements the TokenVerifier protocol by delegating\n        to our existing load_access_token method.\n\n        Args:\n            token: The JWT token string to validate\n\n        Returns:\n            AccessToken object if valid, None if invalid or expired\n        \"\"\"\n        return await self.load_access_token(token)\n\n\nclass StaticTokenVerifier(TokenVerifier):\n    \"\"\"\n    Simple static token verifier for testing and development.\n\n    This verifier validates tokens against a predefined dictionary of valid token\n    strings and their associated claims. When a token string matches a key in the\n    dictionary, the verifier returns the corresponding claims as if the token was\n    validated by a real authorization server.\n\n    Use this when:\n    - You're developing or testing locally without a real OAuth server\n    - You need predictable tokens for automated testing\n    - You want to simulate different users/scopes without complex setup\n    - You're prototyping and need simple API key-style authentication\n\n    WARNING: Never use this in production - tokens are stored in plain text!\n    \"\"\"\n\n    def __init__(\n        self,\n        tokens: dict[str, dict[str, Any]],\n        required_scopes: list[str] | None = None,\n    ):\n        \"\"\"\n        Initialize the static token verifier.\n\n        Args:\n            tokens: Dict mapping token strings to token metadata\n                   Each token should have: client_id, scopes, expires_at (optional)\n            required_scopes: Required scopes for all tokens\n        \"\"\"\n        super().__init__(required_scopes=required_scopes)\n        self.tokens = tokens\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token against static token dictionary.\"\"\"\n        token_data = self.tokens.get(token)\n        if not token_data:\n            return None\n\n        # Check expiration if present\n        expires_at = token_data.get(\"expires_at\")\n        if expires_at is not None and expires_at < time.time():\n            return None\n\n        scopes = token_data.get(\"scopes\", [])\n\n        # Check required scopes\n        if self.required_scopes:\n            token_scopes = set(scopes)\n            required_scopes = set(self.required_scopes)\n            if not required_scopes.issubset(token_scopes):\n                logger.debug(\n                    f\"Token missing required scopes. Has: {token_scopes}, Required: {required_scopes}\"\n                )\n                return None\n\n        return AccessToken(\n            token=token,\n            client_id=token_data[\"client_id\"],\n            scopes=scopes,\n            expires_at=expires_at,\n            claims=token_data,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/oci.py",
    "content": "\"\"\"OCI OIDC provider for FastMCP.\n\nThe pull request for the provider is submitted to fastmcp.\n\nThis module provides OIDC Implementation to integrate MCP servers with OCI.\nYou only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.\n\nPost Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.\nYou need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.\nThe sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.\nYou can use the signer object to create OCI service object.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.oci import OCIProvider\n    from fastmcp.server.dependencies import get_access_token\n    from fastmcp.utilities.logging import get_logger\n\n    import os\n\n    import oci\n    from oci.auth.signers import TokenExchangeSigner\n\n    logger = get_logger(__name__)\n\n    # Load configuration from environment\n    config_url = os.environ.get(\"OCI_CONFIG_URL\")  # OCI IAM Domain OIDC discovery URL\n    client_id = os.environ.get(\"OCI_CLIENT_ID\")  # Client ID configured for the OCI IAM Domain Integrated Application\n    client_secret = os.environ.get(\"OCI_CLIENT_SECRET\")  # Client secret configured for the OCI IAM Domain Integrated Application\n    iam_guid = os.environ.get(\"OCI_IAM_GUID\")  # IAM GUID configured for the OCI IAM Domain\n\n    # Simple OCI OIDC protection\n    auth = OCIProvider(\n        config_url=config_url,  # config URL is the OCI IAM Domain OIDC discovery URL\n        client_id=client_id,  # This is same as the client ID configured for the OCI IAM Domain Integrated Application\n        client_secret=client_secret,  # This is same as the client secret configured for the OCI IAM Domain Integrated Application\n        required_scopes=[\"openid\", \"profile\", \"email\"],\n        redirect_path=\"/auth/callback\",\n        base_url=\"http://localhost:8000\",\n    )\n\n    # NOTE: For production use, replace this with a thread-safe cache implementation\n    # such as threading.Lock-protected dict or a proper caching library\n    _global_token_cache = {}  # In memory cache for OCI session token signer\n\n    def get_oci_signer() -> TokenExchangeSigner:\n\n        authntoken = get_access_token()\n        tokenID = authntoken.claims.get(\"jti\")\n        token = authntoken.token\n\n        # Check if the signer exists for the token ID in memory cache\n        cached_signer = _global_token_cache.get(tokenID)\n        logger.debug(f\"Global cached signer: {cached_signer}\")\n        if cached_signer:\n            logger.debug(f\"Using globally cached signer for token ID: {tokenID}\")\n            return cached_signer\n\n        # If the signer is not yet created for the token then create new OCI signer object\n        logger.debug(f\"Creating new signer for token ID: {tokenID}\")\n        signer = TokenExchangeSigner(\n            jwt_or_func=token,\n            oci_domain_id=iam_guid.split(\".\")[0] if iam_guid else None,  # This is same as IAM GUID configured for the OCI IAM Domain\n            client_id=client_id,  # This is same as the client ID configured for the OCI IAM Domain Integrated Application\n            client_secret=client_secret,  # This is same as the client secret configured for the OCI IAM Domain Integrated Application\n        )\n        logger.debug(f\"Signer {signer} created for token ID: {tokenID}\")\n\n        #Cache the signer object in memory cache\n        _global_token_cache[tokenID] = signer\n        logger.debug(f\"Signer cached for token ID: {tokenID}\")\n\n        return signer\n\n    mcp = FastMCP(\"My Protected Server\", auth=auth)\n    ```\n\"\"\"\n\nfrom typing import Literal\n\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.oidc_proxy import OIDCProxy\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass OCIProvider(OIDCProxy):\n    \"\"\"An OCI IAM Domain provider implementation for FastMCP.\n\n    This provider is a complete OCI integration that's ready to use with\n    just the configuration URL, client ID, client secret, and base URL.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.oci import OCIProvider\n\n        import os\n\n        # Load configuration from environment\n        auth = OCIProvider(\n            config_url=os.environ.get(\"OCI_CONFIG_URL\"),  # OCI IAM Domain OIDC discovery URL\n            client_id=os.environ.get(\"OCI_CLIENT_ID\"),  # Client ID configured for the OCI IAM Domain Integrated Application\n            client_secret=os.environ.get(\"OCI_CLIENT_SECRET\"),  # Client secret configured for the OCI IAM Domain Integrated Application\n            base_url=\"http://localhost:8000\",\n            required_scopes=[\"openid\", \"profile\", \"email\"],\n            redirect_path=\"/auth/callback\",\n        )\n\n        mcp = FastMCP(\"My Protected Server\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        config_url: AnyHttpUrl | str,\n        client_id: str,\n        client_secret: str,\n        base_url: AnyHttpUrl | str,\n        audience: str | None = None,\n        issuer_url: AnyHttpUrl | str | None = None,\n        required_scopes: list[str] | None = None,\n        redirect_path: str | None = None,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n    ) -> None:\n        \"\"\"Initialize OCI OIDC provider.\n\n        Args:\n            config_url: OCI OIDC Discovery URL\n            client_id: OCI IAM Domain Integrated Application client id\n            client_secret: OCI Integrated Application client secret\n            base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)\n            audience: OCI API audience (optional)\n            issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL.\n            required_scopes: Required OCI scopes (defaults to [\"openid\"])\n            redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is \"/auth/callback\".\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n        \"\"\"\n        # Parse scopes if provided as string\n        oci_required_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else [\"openid\"]\n        )\n\n        super().__init__(\n            config_url=config_url,\n            client_id=client_id,\n            client_secret=client_secret,\n            audience=audience,\n            base_url=base_url,\n            issuer_url=issuer_url,\n            redirect_path=redirect_path,\n            required_scopes=oci_required_scopes,\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n        )\n\n        logger.debug(\n            \"Initialized OCI OAuth provider for client %s with scopes: %s\",\n            client_id,\n            oci_required_scopes,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/propelauth.py",
    "content": "\"\"\"PropelAuth authentication provider for FastMCP.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth.providers.propelauth import PropelAuthProvider\n\n    auth = PropelAuthProvider(\n        auth_url=\"https://auth.yourdomain.com\",\n        introspection_client_id=\"your-client-id\",\n        introspection_client_secret=\"your-client-secret\",\n        base_url=\"https://your-fastmcp-server.com\",\n        required_scopes=[\"read:user_data\"],\n    )\n\n    mcp = FastMCP(\"My App\", auth=auth)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TypedDict\n\nimport httpx\nfrom pydantic import AnyHttpUrl, SecretStr\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.server.auth import AccessToken, RemoteAuthProvider\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass PropelAuthTokenIntrospectionOverrides(TypedDict, total=False):\n    timeout_seconds: int\n    cache_ttl_seconds: int | None\n    max_cache_size: int | None\n    http_client: httpx.AsyncClient | None\n\n\nclass PropelAuthProvider(RemoteAuthProvider):\n    \"\"\"PropelAuth resource server provider using OAuth 2.1 token introspection.\n\n    This provider validates access tokens via PropelAuth's introspection endpoint\n    and forwards authorization server metadata for OAuth discovery.\n\n    Setup:\n        1. Enable MCP authentication in the PropelAuth Dashboard\n        2. Configure scopes on the MCP page\n        3. Select which redirect URIs to enable by picking which clients you support\n        4. Generate introspection credentials (Client ID + Client Secret)\n\n    For detailed setup instructions, see:\n    https://docs.propelauth.com/mcp-authentication/overview\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.propelauth import PropelAuthProvider\n\n        auth = PropelAuthProvider(\n            auth_url=\"https://auth.yourdomain.com\",\n            introspection_client_id=\"your-client-id\",\n            introspection_client_secret=\"your-client-secret\",\n            base_url=\"https://your-fastmcp-server.com\",\n            required_scopes=[\"read:user_data\"],\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        auth_url: AnyHttpUrl | str,\n        introspection_client_id: str,\n        introspection_client_secret: str | SecretStr,\n        base_url: AnyHttpUrl | str,\n        required_scopes: list[str] | None = None,\n        scopes_supported: list[str] | None = None,\n        resource_name: str | None = None,\n        resource_documentation: AnyHttpUrl | None = None,\n        resource: AnyHttpUrl | str | None = None,\n        token_introspection_overrides: (\n            PropelAuthTokenIntrospectionOverrides | None\n        ) = None,\n    ):\n        \"\"\"Initialize PropelAuth provider.\n\n        Args:\n            auth_url: Your PropelAuth Auth URL (from the Backend Integration page)\n            introspection_client_id: Introspection Client ID from the PropelAuth Dashboard\n            introspection_client_secret: Introspection Client Secret from the PropelAuth Dashboard\n            base_url: Public URL of this FastMCP server\n            required_scopes: Optional list of scopes that must be present in tokens\n            scopes_supported: Optional list of scopes to advertise in OAuth metadata.\n                If None, uses required_scopes. Use this when the scopes clients should\n                request differ from the scopes enforced on tokens.\n            resource_name: Optional name for the protected resource metadata.\n            resource_documentation: Optional documentation URL for the protected resource.\n            resource: Optional resource URI (RFC 8707) identifying this MCP server.\n                Use this when multiple MCP servers share the same PropelAuth\n                authorization server (e.g. ``resource=\"https://api.example.com/mcp\"``),\n                so only tokens intended for this MCP server are accepted.\n            token_introspection_overrides: Optional overrides for the underlying\n                IntrospectionTokenVerifier (timeout, caching, http_client)\n        \"\"\"\n        normalized_auth_url = str(auth_url).rstrip(\"/\")\n        introspection_url = f\"{normalized_auth_url}/oauth/2.1/introspect\"\n        authorization_server_url = AnyHttpUrl(f\"{normalized_auth_url}/oauth/2.1\")\n\n        if resource is None:\n            self._resource = None\n            logger.debug(\n                \"PropelAuthProvider: no resource configured, audience checking disabled\"\n            )\n        else:\n            self._resource = str(resource)\n\n        token_verifier = self._create_token_verifier(\n            introspection_url=introspection_url,\n            client_id=introspection_client_id,\n            client_secret=introspection_client_secret,\n            required_scopes=required_scopes,\n            introspection_overrides=token_introspection_overrides,\n        )\n\n        self._normalized_auth_url = normalized_auth_url\n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[authorization_server_url],\n            base_url=base_url,\n            scopes_supported=scopes_supported,\n            resource_name=resource_name,\n            resource_documentation=resource_documentation,\n        )\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get routes for this provider.\n\n        Includes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),\n        and creates an authorization server metadata route that forwards to PropelAuth's route\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata.\n        \"\"\"\n        routes = super().get_routes(mcp_path)\n\n        async def oauth_authorization_server_metadata(request):\n            \"\"\"Forward PropelAuth OAuth authorization server metadata\"\"\"\n            try:\n                async with httpx.AsyncClient() as client:\n                    response = await client.get(\n                        f\"{self._normalized_auth_url}/.well-known/oauth-authorization-server/oauth/2.1\"\n                    )\n                    response.raise_for_status()\n                    metadata = response.json()\n                    return JSONResponse(metadata)\n            except Exception as e:\n                return JSONResponse(\n                    {\n                        \"error\": \"server_error\",\n                        \"error_description\": f\"Failed to fetch PropelAuth metadata: {e}\",\n                    },\n                    status_code=500,\n                )\n\n        routes.append(\n            Route(\n                \"/.well-known/oauth-authorization-server\",\n                endpoint=oauth_authorization_server_metadata,\n                methods=[\"GET\"],\n            )\n        )\n\n        return routes\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token and check the ``aud`` claim against the configured resource.\"\"\"\n        result = await super().verify_token(token)\n        if result is None or self._resource is None:\n            return result\n\n        aud = result.claims.get(\"aud\")\n        if aud != self._resource:\n            logger.debug(\n                \"PropelAuthProvider: token audience %r does not match resource %s\",\n                aud,\n                self._resource,\n            )\n            return None\n\n        return result\n\n    def _create_token_verifier(\n        self,\n        introspection_url: str,\n        client_id: str,\n        client_secret: str | SecretStr,\n        required_scopes: list[str] | None,\n        introspection_overrides: PropelAuthTokenIntrospectionOverrides | None,\n    ) -> IntrospectionTokenVerifier:\n        # Being defensive here, check for only the fields we are expecting\n        safe_overrides: PropelAuthTokenIntrospectionOverrides = {}\n        if introspection_overrides is not None:\n            if \"timeout_seconds\" in introspection_overrides:\n                safe_overrides[\"timeout_seconds\"] = introspection_overrides[\n                    \"timeout_seconds\"\n                ]\n            if \"cache_ttl_seconds\" in introspection_overrides:\n                safe_overrides[\"cache_ttl_seconds\"] = introspection_overrides[\n                    \"cache_ttl_seconds\"\n                ]\n            if \"max_cache_size\" in introspection_overrides:\n                safe_overrides[\"max_cache_size\"] = introspection_overrides[\n                    \"max_cache_size\"\n                ]\n            if \"http_client\" in introspection_overrides:\n                safe_overrides[\"http_client\"] = introspection_overrides[\"http_client\"]\n\n        return IntrospectionTokenVerifier(\n            introspection_url=introspection_url,\n            client_id=client_id,\n            client_secret=client_secret,\n            required_scopes=required_scopes,\n            **safe_overrides,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/scalekit.py",
    "content": "\"\"\"Scalekit authentication provider for FastMCP.\n\nThis module provides ScalekitProvider - a complete authentication solution that integrates\nwith Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server\nauthentication for seamless MCP client authentication.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport httpx\nfrom pydantic import AnyHttpUrl\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.server.auth import RemoteAuthProvider, TokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass ScalekitProvider(RemoteAuthProvider):\n    \"\"\"Scalekit resource server provider for OAuth 2.1 authentication.\n\n    This provider implements Scalekit integration using resource server pattern.\n    FastMCP acts as a protected resource server that validates access tokens issued\n    by Scalekit's authorization server.\n\n    IMPORTANT SETUP REQUIREMENTS:\n\n    1. Create an MCP Server in Scalekit Dashboard:\n       - Go to your [Scalekit Dashboard](https://app.scalekit.com/)\n       - Navigate to MCP Servers section\n       - Register a new MCP Server with appropriate scopes\n       - Ensure the Resource Identifier matches exactly what you configure as MCP URL\n       - Note the Resource ID\n\n    2. Environment Configuration:\n       - Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)\n       - Set SCALEKIT_RESOURCE_ID from your created resource\n       - Set BASE_URL to your FastMCP server's public URL\n\n    For detailed setup instructions, see:\n    https://docs.scalekit.com/mcp/overview/\n\n    Example:\n        ```python\n        from fastmcp.server.auth.providers.scalekit import ScalekitProvider\n\n        # Create Scalekit resource server provider\n        scalekit_auth = ScalekitProvider(\n            environment_url=\"https://your-env.scalekit.com\",\n            resource_id=\"sk_resource_...\",\n            base_url=\"https://your-fastmcp-server.com\",\n        )\n\n        # Use with FastMCP\n        mcp = FastMCP(\"My App\", auth=scalekit_auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        environment_url: AnyHttpUrl | str,\n        resource_id: str,\n        base_url: AnyHttpUrl | str | None = None,\n        mcp_url: AnyHttpUrl | str | None = None,\n        client_id: str | None = None,\n        required_scopes: list[str] | None = None,\n        scopes_supported: list[str] | None = None,\n        resource_name: str | None = None,\n        resource_documentation: AnyHttpUrl | None = None,\n        token_verifier: TokenVerifier | None = None,\n    ):\n        \"\"\"Initialize Scalekit resource server provider.\n\n        Args:\n            environment_url: Your Scalekit environment URL (e.g., \"https://your-env.scalekit.com\")\n            resource_id: Your Scalekit resource ID\n            base_url: Public URL of this FastMCP server (or use mcp_url for backwards compatibility)\n            mcp_url: Deprecated alias for base_url. Will be removed in a future release.\n            client_id: Deprecated parameter, no longer required. Will be removed in a future release.\n            required_scopes: Optional list of scopes that must be present in tokens\n            scopes_supported: Optional list of scopes to advertise in OAuth metadata.\n                If None, uses required_scopes. Use this when the scopes clients should\n                request differ from the scopes enforced on tokens.\n            resource_name: Optional name for the protected resource metadata.\n            resource_documentation: Optional documentation URL for the protected resource.\n            token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit\n        \"\"\"\n        # Resolve base_url from mcp_url if needed (backwards compatibility)\n        resolved_base_url = base_url or mcp_url\n        if not resolved_base_url:\n            raise ValueError(\"Either base_url or mcp_url must be provided\")\n\n        if mcp_url is not None:\n            logger.warning(\n                \"ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. \"\n                \"Rename it to 'base_url'.\"\n            )\n\n        if client_id is not None:\n            logger.warning(\n                \"ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward \"\n                \"compatibility and will be removed in a future release.\"\n            )\n\n        self.environment_url = str(environment_url).rstrip(\"/\")\n        self.resource_id = resource_id\n        parsed_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else []\n        )\n        self.required_scopes = parsed_scopes\n        base_url_value = str(resolved_base_url)\n\n        logger.debug(\n            \"Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s\",\n            self.environment_url,\n            self.resource_id,\n            base_url_value,\n            self.required_scopes,\n        )\n\n        # Create default JWT verifier if none provided\n        if token_verifier is None:\n            logger.debug(\n                \"Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s\",\n                f\"{self.environment_url}/keys\",\n                self.environment_url,\n                self.required_scopes,\n            )\n            token_verifier = JWTVerifier(\n                jwks_uri=f\"{self.environment_url}/keys\",\n                issuer=self.environment_url,\n                algorithm=\"RS256\",\n                audience=self.resource_id,\n                required_scopes=self.required_scopes or None,\n            )\n        else:\n            logger.debug(\"Using custom token verifier for ScalekitProvider\")\n\n        # Initialize RemoteAuthProvider with Scalekit as the authorization server\n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[\n                AnyHttpUrl(f\"{self.environment_url}/resources/{self.resource_id}\")\n            ],\n            base_url=base_url_value,\n            scopes_supported=scopes_supported,\n            resource_name=resource_name,\n            resource_documentation=resource_documentation,\n        )\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get OAuth routes including Scalekit authorization server metadata forwarding.\n\n        This returns the standard protected resource routes plus an authorization server\n        metadata endpoint that forwards Scalekit's OAuth metadata to clients.\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata.\n        \"\"\"\n        # Get the standard protected resource routes from RemoteAuthProvider\n        routes = super().get_routes(mcp_path)\n        logger.debug(\n            \"Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s\",\n            mcp_path,\n            self.resource_id,\n        )\n\n        async def oauth_authorization_server_metadata(request):\n            \"\"\"Forward Scalekit OAuth authorization server metadata with FastMCP customizations.\"\"\"\n            try:\n                metadata_url = f\"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}\"\n                logger.debug(\n                    \"Fetching Scalekit OAuth metadata: metadata_url=%s\", metadata_url\n                )\n                async with httpx.AsyncClient() as client:\n                    response = await client.get(metadata_url)\n                    response.raise_for_status()\n                    metadata = response.json()\n                    logger.debug(\n                        \"Scalekit metadata fetched successfully: metadata_keys=%s\",\n                        list(metadata.keys()),\n                    )\n                    return JSONResponse(metadata)\n            except Exception as e:\n                logger.error(f\"Failed to fetch Scalekit metadata: {e}\")\n                return JSONResponse(\n                    {\n                        \"error\": \"server_error\",\n                        \"error_description\": f\"Failed to fetch Scalekit metadata: {e}\",\n                    },\n                    status_code=500,\n                )\n\n        # Add Scalekit authorization server metadata forwarding\n        routes.append(\n            Route(\n                \"/.well-known/oauth-authorization-server\",\n                endpoint=oauth_authorization_server_metadata,\n                methods=[\"GET\"],\n            )\n        )\n\n        return routes\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/supabase.py",
    "content": "\"\"\"Supabase authentication provider for FastMCP.\n\nThis module provides SupabaseProvider - a complete authentication solution that integrates\nwith Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)\nfor seamless MCP client authentication.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Literal\n\nimport httpx\nfrom pydantic import AnyHttpUrl\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.server.auth import RemoteAuthProvider, TokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass SupabaseProvider(RemoteAuthProvider):\n    \"\"\"Supabase metadata provider for DCR (Dynamic Client Registration).\n\n    This provider implements Supabase Auth integration using metadata forwarding.\n    This approach allows Supabase to handle the OAuth flow directly while FastMCP acts\n    as a resource server, verifying JWTs issued by Supabase Auth.\n\n    IMPORTANT SETUP REQUIREMENTS:\n\n    1. Supabase Project Setup:\n       - Create a Supabase project at https://supabase.com\n       - Note your project URL (e.g., \"https://abc123.supabase.co\")\n       - Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)\n       - Asymmetric keys (RS256/ES256) are recommended for production\n\n    2. JWT Verification:\n       - FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json\n       - JWTs are issued by {project_url}{auth_route}\n       - Default auth_route is \"/auth/v1\" (can be customized for self-hosted setups)\n       - Tokens are cached for up to 10 minutes by Supabase's edge servers\n       - Algorithm must match your Supabase Auth configuration\n\n    3. Authorization:\n       - Supabase uses Row Level Security (RLS) policies for database authorization\n       - OAuth-level scopes are an upcoming feature in Supabase Auth\n       - Both approaches will be supported once scope handling is available\n\n    For detailed setup instructions, see:\n    https://supabase.com/docs/guides/auth/jwts\n\n    Example:\n        ```python\n        from fastmcp.server.auth.providers.supabase import SupabaseProvider\n\n        # Create Supabase metadata provider (JWT verifier created automatically)\n        supabase_auth = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://your-fastmcp-server.com\",\n            algorithm=\"ES256\",  # Match your Supabase Auth configuration\n        )\n\n        # Use with FastMCP\n        mcp = FastMCP(\"My App\", auth=supabase_auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        project_url: AnyHttpUrl | str,\n        base_url: AnyHttpUrl | str,\n        auth_route: str = \"/auth/v1\",\n        algorithm: Literal[\"RS256\", \"ES256\"] = \"ES256\",\n        required_scopes: list[str] | None = None,\n        scopes_supported: list[str] | None = None,\n        resource_name: str | None = None,\n        resource_documentation: AnyHttpUrl | None = None,\n        token_verifier: TokenVerifier | None = None,\n    ):\n        \"\"\"Initialize Supabase metadata provider.\n\n        Args:\n            project_url: Your Supabase project URL (e.g., \"https://abc123.supabase.co\")\n            base_url: Public URL of this FastMCP server\n            auth_route: Supabase Auth route. Defaults to \"/auth/v1\". Can be customized\n                for self-hosted Supabase Auth setups using custom routes.\n            algorithm: JWT signing algorithm (RS256 or ES256). Must match your\n                Supabase Auth configuration. Defaults to ES256.\n            required_scopes: Optional list of scopes to require for all requests.\n                Note: Supabase currently uses RLS policies for authorization. OAuth-level\n                scopes are an upcoming feature.\n            scopes_supported: Optional list of scopes to advertise in OAuth metadata.\n                If None, uses required_scopes. Use this when the scopes clients should\n                request differ from the scopes enforced on tokens.\n            resource_name: Optional name for the protected resource metadata.\n            resource_documentation: Optional documentation URL for the protected resource.\n            token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase\n        \"\"\"\n        self.project_url = str(project_url).rstrip(\"/\")\n        self.base_url = AnyHttpUrl(str(base_url).rstrip(\"/\"))\n        self.auth_route = auth_route.strip(\"/\")\n\n        # Parse scopes if provided as string\n        parsed_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else None\n        )\n\n        # Create default JWT verifier if none provided\n        if token_verifier is None:\n            logger.warning(\n                \"SupabaseProvider cannot validate token audience for the specific resource \"\n                \"because Supabase Auth does not support RFC 8707 resource indicators. \"\n                \"This may leave the server vulnerable to cross-server token replay.\"\n            )\n            token_verifier = JWTVerifier(\n                jwks_uri=f\"{self.project_url}/{self.auth_route}/.well-known/jwks.json\",\n                issuer=f\"{self.project_url}/{self.auth_route}\",\n                algorithm=algorithm,\n                audience=\"authenticated\",\n                required_scopes=parsed_scopes,\n            )\n\n        # Initialize RemoteAuthProvider with Supabase as the authorization server\n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(f\"{self.project_url}/{self.auth_route}\")],\n            base_url=self.base_url,\n            scopes_supported=scopes_supported,\n            resource_name=resource_name,\n            resource_documentation=resource_documentation,\n        )\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get OAuth routes including Supabase authorization server metadata forwarding.\n\n        This returns the standard protected resource routes plus an authorization server\n        metadata endpoint that forwards Supabase's OAuth metadata to clients.\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata.\n        \"\"\"\n        # Get the standard protected resource routes from RemoteAuthProvider\n        routes = super().get_routes(mcp_path)\n\n        async def oauth_authorization_server_metadata(request):\n            \"\"\"Forward Supabase OAuth authorization server metadata with FastMCP customizations.\"\"\"\n            try:\n                async with httpx.AsyncClient() as client:\n                    response = await client.get(\n                        f\"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server\"\n                    )\n                    response.raise_for_status()\n                    metadata = response.json()\n                    return JSONResponse(metadata)\n            except Exception as e:\n                return JSONResponse(\n                    {\n                        \"error\": \"server_error\",\n                        \"error_description\": f\"Failed to fetch Supabase metadata: {e}\",\n                    },\n                    status_code=500,\n                )\n\n        # Add Supabase authorization server metadata forwarding\n        routes.append(\n            Route(\n                \"/.well-known/oauth-authorization-server\",\n                endpoint=oauth_authorization_server_metadata,\n                methods=[\"GET\"],\n            )\n        )\n\n        return routes\n"
  },
  {
    "path": "src/fastmcp/server/auth/providers/workos.py",
    "content": "\"\"\"WorkOS authentication providers for FastMCP.\n\nThis module provides two WorkOS authentication strategies:\n\n1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)\n2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit\n\nChoose based on your WorkOS setup and authentication requirements.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nfrom typing import Literal\n\nimport httpx\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom pydantic import AnyHttpUrl\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.auth import parse_scopes\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\nclass WorkOSTokenVerifier(TokenVerifier):\n    \"\"\"Token verifier for WorkOS OAuth tokens.\n\n    WorkOS AuthKit tokens are opaque, so we verify them by calling\n    the /oauth2/userinfo endpoint to check validity and get user info.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        authkit_domain: str,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize the WorkOS token verifier.\n\n        Args:\n            authkit_domain: WorkOS AuthKit domain (e.g., \"https://your-app.authkit.app\")\n            required_scopes: Required OAuth scopes\n            timeout_seconds: HTTP request timeout\n            http_client: Optional httpx.AsyncClient for connection pooling. When provided,\n                the client is reused across calls and the caller is responsible for its\n                lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        super().__init__(required_scopes=required_scopes)\n        self.authkit_domain = authkit_domain.rstrip(\"/\")\n        self.timeout_seconds = timeout_seconds\n        self._http_client = http_client\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify WorkOS OAuth token by calling userinfo endpoint.\"\"\"\n        try:\n            async with (\n                contextlib.nullcontext(self._http_client)\n                if self._http_client is not None\n                else httpx.AsyncClient(timeout=self.timeout_seconds)\n            ) as client:\n                # Use WorkOS AuthKit userinfo endpoint to validate token\n                response = await client.get(\n                    f\"{self.authkit_domain}/oauth2/userinfo\",\n                    headers={\n                        \"Authorization\": f\"Bearer {token}\",\n                        \"User-Agent\": \"FastMCP-WorkOS-OAuth\",\n                    },\n                )\n\n                if response.status_code != 200:\n                    logger.debug(\n                        \"WorkOS token verification failed: %d - %s\",\n                        response.status_code,\n                        response.text[:200],\n                    )\n                    return None\n\n                user_data = response.json()\n                token_scopes = (\n                    parse_scopes(user_data.get(\"scope\") or user_data.get(\"scopes\"))\n                    or []\n                )\n\n                if self.required_scopes and not all(\n                    scope in token_scopes for scope in self.required_scopes\n                ):\n                    logger.debug(\n                        \"WorkOS token missing required scopes. required=%s actual=%s\",\n                        self.required_scopes,\n                        token_scopes,\n                    )\n                    return None\n\n                # Create AccessToken with WorkOS user info\n                return AccessToken(\n                    token=token,\n                    client_id=str(user_data.get(\"sub\", \"unknown\")),\n                    scopes=token_scopes,\n                    expires_at=None,  # Will be set from token introspection if needed\n                    claims={\n                        \"sub\": user_data.get(\"sub\"),\n                        \"email\": user_data.get(\"email\"),\n                        \"email_verified\": user_data.get(\"email_verified\"),\n                        \"name\": user_data.get(\"name\"),\n                        \"given_name\": user_data.get(\"given_name\"),\n                        \"family_name\": user_data.get(\"family_name\"),\n                    },\n                )\n\n        except httpx.RequestError as e:\n            logger.debug(\"Failed to verify WorkOS token: %s\", e)\n            return None\n        except Exception as e:\n            logger.debug(\"WorkOS token verification error: %s\", e)\n            return None\n\n\nclass WorkOSProvider(OAuthProxy):\n    \"\"\"Complete WorkOS OAuth provider for FastMCP.\n\n    This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.\n    It provides OAuth2 authentication for users through WorkOS Connect applications.\n\n    Features:\n    - Transparent OAuth proxy to WorkOS AuthKit\n    - Automatic token validation via userinfo endpoint\n    - User information extraction from ID tokens\n    - Support for standard OAuth scopes (openid, profile, email)\n\n    Setup Requirements:\n    1. Create a WorkOS Connect application in your dashboard\n    2. Note your AuthKit domain (e.g., \"https://your-app.authkit.app\")\n    3. Configure redirect URI as: http://localhost:8000/auth/callback\n    4. Note your Client ID and Client Secret\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth.providers.workos import WorkOSProvider\n\n        auth = WorkOSProvider(\n            client_id=\"client_123\",\n            client_secret=\"sk_test_456\",\n            authkit_domain=\"https://your-app.authkit.app\",\n            base_url=\"http://localhost:8000\"\n        )\n\n        mcp = FastMCP(\"My App\", auth=auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_id: str,\n        client_secret: str,\n        authkit_domain: str,\n        base_url: AnyHttpUrl | str,\n        issuer_url: AnyHttpUrl | str | None = None,\n        redirect_path: str | None = None,\n        required_scopes: list[str] | None = None,\n        timeout_seconds: int = 10,\n        allowed_client_redirect_uris: list[str] | None = None,\n        client_storage: AsyncKeyValue | None = None,\n        jwt_signing_key: str | bytes | None = None,\n        require_authorization_consent: bool | Literal[\"external\"] = True,\n        consent_csp_policy: str | None = None,\n        http_client: httpx.AsyncClient | None = None,\n    ):\n        \"\"\"Initialize WorkOS OAuth provider.\n\n        Args:\n            client_id: WorkOS client ID\n            client_secret: WorkOS client secret\n            authkit_domain: Your WorkOS AuthKit domain (e.g., \"https://your-app.authkit.app\")\n            base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)\n            issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL\n                to avoid 404s during discovery when mounting under a path.\n            redirect_path: Redirect path configured in WorkOS (defaults to \"/auth/callback\")\n            required_scopes: Required OAuth scopes (no default)\n            timeout_seconds: HTTP request timeout for WorkOS API calls (defaults to 10)\n            allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.\n                If None (default), all URIs are allowed. If empty list, no URIs are allowed.\n            client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).\n                If None, an encrypted file store will be created in the data directory\n                (derived from `platformdirs`).\n            jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,\n                they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not\n                provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.\n            require_authorization_consent: Whether to require user consent before authorizing clients (default True).\n                When True, users see a consent screen before being redirected to WorkOS.\n                When False, authorization proceeds directly without user confirmation.\n                When \"external\", the built-in consent screen is skipped but no warning is\n                logged, indicating that consent is handled externally (e.g. by the upstream IdP).\n                SECURITY WARNING: Only set to False for local development or testing environments.\n            http_client: Optional httpx.AsyncClient for connection pooling in token verification.\n                When provided, the client is reused across verify_token calls and the caller\n                is responsible for its lifecycle. When None (default), a fresh client is created per call.\n        \"\"\"\n        # Apply defaults and ensure authkit_domain is a full URL\n        authkit_domain_str = authkit_domain\n        if not authkit_domain_str.startswith((\"http://\", \"https://\")):\n            authkit_domain_str = f\"https://{authkit_domain_str}\"\n        authkit_domain_final = authkit_domain_str.rstrip(\"/\")\n        scopes_final = (\n            parse_scopes(required_scopes) if required_scopes is not None else []\n        )\n\n        # Create WorkOS token verifier\n        token_verifier = WorkOSTokenVerifier(\n            authkit_domain=authkit_domain_final,\n            required_scopes=scopes_final,\n            timeout_seconds=timeout_seconds,\n            http_client=http_client,\n        )\n\n        # Initialize OAuth proxy with WorkOS AuthKit endpoints\n        super().__init__(\n            upstream_authorization_endpoint=f\"{authkit_domain_final}/oauth2/authorize\",\n            upstream_token_endpoint=f\"{authkit_domain_final}/oauth2/token\",\n            upstream_client_id=client_id,\n            upstream_client_secret=client_secret,\n            token_verifier=token_verifier,\n            base_url=base_url,\n            redirect_path=redirect_path,\n            issuer_url=issuer_url or base_url,  # Default to base_url if not specified\n            allowed_client_redirect_uris=allowed_client_redirect_uris,\n            client_storage=client_storage,\n            jwt_signing_key=jwt_signing_key,\n            require_authorization_consent=require_authorization_consent,\n            consent_csp_policy=consent_csp_policy,\n        )\n\n        logger.debug(\n            \"Initialized WorkOS OAuth provider for client %s with AuthKit domain %s\",\n            client_id,\n            authkit_domain_final,\n        )\n\n\nclass AuthKitProvider(RemoteAuthProvider):\n    \"\"\"AuthKit metadata provider for DCR (Dynamic Client Registration).\n\n    This provider implements AuthKit integration using metadata forwarding\n    instead of OAuth proxying. This is the recommended approach for WorkOS DCR\n    as it allows WorkOS to handle the OAuth flow directly while FastMCP acts\n    as a resource server.\n\n    IMPORTANT SETUP REQUIREMENTS:\n\n    1. Enable Dynamic Client Registration in WorkOS Dashboard:\n       - Go to Applications → Configuration\n       - Toggle \"Dynamic Client Registration\" to enabled\n\n    2. Configure your FastMCP server URL as a callback:\n       - Add your server URL to the Redirects tab in WorkOS dashboard\n       - Example: https://your-fastmcp-server.com/oauth2/callback\n\n    For detailed setup instructions, see:\n    https://workos.com/docs/authkit/mcp/integrating/token-verification\n\n    Example:\n        ```python\n        from fastmcp.server.auth.providers.workos import AuthKitProvider\n\n        # Create AuthKit metadata provider (JWT verifier created automatically)\n        workos_auth = AuthKitProvider(\n            authkit_domain=\"https://your-workos-domain.authkit.app\",\n            base_url=\"https://your-fastmcp-server.com\",\n        )\n\n        # Use with FastMCP\n        mcp = FastMCP(\"My App\", auth=workos_auth)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        authkit_domain: AnyHttpUrl | str,\n        base_url: AnyHttpUrl | str,\n        client_id: str | None = None,\n        required_scopes: list[str] | None = None,\n        scopes_supported: list[str] | None = None,\n        resource_name: str | None = None,\n        resource_documentation: AnyHttpUrl | None = None,\n        token_verifier: TokenVerifier | None = None,\n    ):\n        \"\"\"Initialize AuthKit metadata provider.\n\n        Args:\n            authkit_domain: Your AuthKit domain (e.g., \"https://your-app.authkit.app\")\n            base_url: Public URL of this FastMCP server\n            client_id: Your WorkOS project client ID (e.g., \"client_01ABC...\"). Used to\n                validate the JWT audience claim. Found in your WorkOS Dashboard under\n                API Keys. This is the project-level client ID, not individual MCP client IDs.\n            required_scopes: Optional list of scopes to require for all requests\n            scopes_supported: Optional list of scopes to advertise in OAuth metadata.\n                If None, uses required_scopes. Use this when the scopes clients should\n                request differ from the scopes enforced on tokens.\n            resource_name: Optional name for the protected resource metadata.\n            resource_documentation: Optional documentation URL for the protected resource.\n            token_verifier: Optional token verifier. If None, creates JWT verifier for AuthKit\n        \"\"\"\n        self.authkit_domain = str(authkit_domain).rstrip(\"/\")\n        self.base_url = AnyHttpUrl(str(base_url).rstrip(\"/\"))\n\n        # Parse scopes if provided as string\n        parsed_scopes = (\n            parse_scopes(required_scopes) if required_scopes is not None else None\n        )\n\n        # Create default JWT verifier if none provided\n        if token_verifier is None:\n            logger.warning(\n                \"AuthKitProvider cannot validate token audience for the specific resource \"\n                \"because AuthKit does not support RFC 8707 resource indicators. \"\n                \"This may leave the server vulnerable to cross-server token replay. \"\n                \"Consider using WorkOSProvider (OAuth proxy) for audience-bound tokens.\"\n            )\n            token_verifier = JWTVerifier(\n                jwks_uri=f\"{self.authkit_domain}/oauth2/jwks\",\n                issuer=self.authkit_domain,\n                algorithm=\"RS256\",\n                audience=client_id,\n                required_scopes=parsed_scopes,\n            )\n\n        # Initialize RemoteAuthProvider with AuthKit as the authorization server\n        super().__init__(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(self.authkit_domain)],\n            base_url=self.base_url,\n            scopes_supported=scopes_supported,\n            resource_name=resource_name,\n            resource_documentation=resource_documentation,\n        )\n\n    def get_routes(\n        self,\n        mcp_path: str | None = None,\n    ) -> list[Route]:\n        \"\"\"Get OAuth routes including AuthKit authorization server metadata forwarding.\n\n        This returns the standard protected resource routes plus an authorization server\n        metadata endpoint that forwards AuthKit's OAuth metadata to clients.\n\n        Args:\n            mcp_path: The path where the MCP endpoint is mounted (e.g., \"/mcp\")\n                This is used to advertise the resource URL in metadata.\n        \"\"\"\n        # Get the standard protected resource routes from RemoteAuthProvider\n        routes = super().get_routes(mcp_path)\n\n        async def oauth_authorization_server_metadata(request):\n            \"\"\"Forward AuthKit OAuth authorization server metadata with FastMCP customizations.\"\"\"\n            try:\n                async with httpx.AsyncClient() as client:\n                    response = await client.get(\n                        f\"{self.authkit_domain}/.well-known/oauth-authorization-server\"\n                    )\n                    response.raise_for_status()\n                    metadata = response.json()\n                    return JSONResponse(metadata)\n            except Exception as e:\n                return JSONResponse(\n                    {\n                        \"error\": \"server_error\",\n                        \"error_description\": f\"Failed to fetch AuthKit metadata: {e}\",\n                    },\n                    status_code=500,\n                )\n\n        # Add AuthKit authorization server metadata forwarding\n        routes.append(\n            Route(\n                \"/.well-known/oauth-authorization-server\",\n                endpoint=oauth_authorization_server_metadata,\n                methods=[\"GET\"],\n            )\n        )\n\n        return routes\n"
  },
  {
    "path": "src/fastmcp/server/auth/redirect_validation.py",
    "content": "\"\"\"Utilities for validating client redirect URIs in OAuth flows.\n\nThis module provides secure redirect URI validation with wildcard support,\nprotecting against userinfo-based bypass attacks like http://localhost@evil.com.\n\"\"\"\n\nimport fnmatch\nfrom urllib.parse import urlparse\n\nfrom pydantic import AnyUrl\n\n\ndef _parse_host_port(netloc: str) -> tuple[str | None, str | None]:\n    \"\"\"Parse host and port from netloc, handling wildcards.\n\n    Args:\n        netloc: The netloc component (e.g., \"localhost:8080\" or \"localhost:*\")\n\n    Returns:\n        Tuple of (host, port_str) where port_str may be \"*\" or a number string\n    \"\"\"\n    # Handle userinfo (remove it for parsing, but we check separately)\n    if \"@\" in netloc:\n        netloc = netloc.split(\"@\")[-1]\n\n    # Handle IPv6 addresses [::1]:port\n    if netloc.startswith(\"[\"):\n        bracket_end = netloc.find(\"]\")\n        if bracket_end == -1:\n            return netloc, None\n        host = netloc[1:bracket_end]\n        rest = netloc[bracket_end + 1 :]\n        if rest.startswith(\":\"):\n            return host, rest[1:]\n        return host, None\n\n    # Handle regular host:port\n    if \":\" in netloc:\n        host, port = netloc.rsplit(\":\", 1)\n        return host, port\n\n    return netloc, None\n\n\ndef _match_host(uri_host: str | None, pattern_host: str | None) -> bool:\n    \"\"\"Match host component, supporting *.example.com wildcard patterns.\n\n    Args:\n        uri_host: The host from the URI being validated\n        pattern_host: The host pattern (may start with *.)\n\n    Returns:\n        True if the host matches\n    \"\"\"\n    if not uri_host or not pattern_host:\n        return uri_host == pattern_host\n\n    # Normalize to lowercase for comparison\n    uri_host = uri_host.lower()\n    pattern_host = pattern_host.lower()\n\n    # Handle *.example.com wildcard subdomain patterns\n    if pattern_host.startswith(\"*.\"):\n        suffix = pattern_host[1:]  # .example.com\n        # Only match actual subdomains (foo.example.com), NOT the base domain\n        return uri_host.endswith(suffix) and uri_host != pattern_host[2:]\n\n    return uri_host == pattern_host\n\n\ndef _match_port(\n    uri_port: str | None,\n    pattern_port: str | None,\n    uri_scheme: str,\n) -> bool:\n    \"\"\"Match port component, supporting * wildcard for any port.\n\n    Args:\n        uri_port: The port from the URI (None if default, string otherwise)\n        pattern_port: The port from the pattern (None if default, \"*\" for wildcard)\n        uri_scheme: The URI scheme (http/https) for default port handling\n\n    Returns:\n        True if the port matches\n    \"\"\"\n    # Wildcard matches any port\n    if pattern_port == \"*\":\n        return True\n\n    # Normalize None to default ports\n    default_port = \"443\" if uri_scheme == \"https\" else \"80\"\n    uri_effective = uri_port if uri_port else default_port\n    pattern_effective = pattern_port if pattern_port else default_port\n\n    return uri_effective == pattern_effective\n\n\ndef _match_path(uri_path: str, pattern_path: str) -> bool:\n    \"\"\"Match path component using fnmatch for wildcard support.\n\n    Args:\n        uri_path: The path from the URI\n        pattern_path: The path pattern (may contain * wildcards)\n\n    Returns:\n        True if the path matches\n    \"\"\"\n    # Normalize empty paths to /\n    uri_path = uri_path or \"/\"\n    pattern_path = pattern_path or \"/\"\n\n    # Empty or root pattern path matches any path\n    # This makes http://localhost:* match http://localhost:3000/callback\n    if pattern_path == \"/\":\n        return True\n\n    # Use fnmatch for path wildcards (e.g., /auth/*)\n    return fnmatch.fnmatch(uri_path, pattern_path)\n\n\ndef matches_allowed_pattern(uri: str, pattern: str) -> bool:\n    \"\"\"Securely check if a URI matches an allowed pattern with wildcard support.\n\n    This function parses both the URI and pattern as URLs, comparing each\n    component separately to prevent bypass attacks like userinfo injection.\n\n    Patterns support wildcards:\n    - http://localhost:* matches any localhost port\n    - http://127.0.0.1:* matches any 127.0.0.1 port\n    - https://*.example.com/* matches any subdomain of example.com\n    - https://app.example.com/auth/* matches any path under /auth/\n\n    Security: Rejects URIs with userinfo (user:pass@host) which could bypass\n    naive string matching (e.g., http://localhost@evil.com).\n\n    Args:\n        uri: The redirect URI to validate\n        pattern: The allowed pattern (may contain wildcards)\n\n    Returns:\n        True if the URI matches the pattern\n    \"\"\"\n    try:\n        uri_parsed = urlparse(uri)\n        pattern_parsed = urlparse(pattern)\n    except ValueError:\n        return False\n\n    # SECURITY: Reject URIs with userinfo (user:pass@host)\n    # This prevents bypass attacks like http://localhost@evil.com/callback\n    # which would match http://localhost:* with naive fnmatch\n    if uri_parsed.username is not None or uri_parsed.password is not None:\n        return False\n\n    # Scheme must match exactly\n    if uri_parsed.scheme.lower() != pattern_parsed.scheme.lower():\n        return False\n\n    # Parse host and port manually to handle wildcards\n    uri_host, uri_port = _parse_host_port(uri_parsed.netloc)\n    pattern_host, pattern_port = _parse_host_port(pattern_parsed.netloc)\n\n    # Host must match (with subdomain wildcard support)\n    if not _match_host(uri_host, pattern_host):\n        return False\n\n    # Port must match (with * wildcard support)\n    if not _match_port(uri_port, pattern_port, uri_parsed.scheme.lower()):\n        return False\n\n    # Path must match (with fnmatch wildcards)\n    return _match_path(uri_parsed.path, pattern_parsed.path)\n\n\ndef validate_redirect_uri(\n    redirect_uri: str | AnyUrl | None,\n    allowed_patterns: list[str] | None,\n) -> bool:\n    \"\"\"Validate a redirect URI against allowed patterns.\n\n    Args:\n        redirect_uri: The redirect URI to validate\n        allowed_patterns: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility).\n                         If empty list, no URIs are allowed.\n                         To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS.\n\n    Returns:\n        True if the redirect URI is allowed\n    \"\"\"\n    if redirect_uri is None:\n        return True  # None is allowed (will use client's default)\n\n    uri_str = str(redirect_uri)\n\n    # If no patterns specified, allow all for DCR compatibility\n    # (clients need to dynamically register with their own redirect URIs)\n    if allowed_patterns is None:\n        return True\n\n    # Check if URI matches any allowed pattern\n    for pattern in allowed_patterns:\n        if matches_allowed_pattern(uri_str, pattern):\n            return True\n\n    return False\n\n\n# Default patterns for localhost-only validation\nDEFAULT_LOCALHOST_PATTERNS = [\n    \"http://localhost:*\",\n    \"http://127.0.0.1:*\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/auth/ssrf.py",
    "content": "\"\"\"SSRF-safe HTTP utilities for FastMCP.\n\nThis module provides SSRF-protected HTTP fetching with:\n- DNS resolution and IP validation before requests\n- DNS pinning to prevent rebinding TOCTOU attacks\n- Support for both CIMD and JWKS fetches\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport ipaddress\nimport socket\nimport time\nfrom collections.abc import Mapping\nfrom dataclasses import dataclass\nfrom urllib.parse import urlparse\n\nimport httpx\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\ndef format_ip_for_url(ip_str: str) -> str:\n    \"\"\"Format IP address for use in URL (bracket IPv6 addresses).\n\n    IPv6 addresses must be bracketed in URLs to distinguish the address from\n    the port separator. For example: https://[2001:db8::1]:443/path\n\n    Args:\n        ip_str: IP address string\n\n    Returns:\n        IP string suitable for URL (IPv6 addresses are bracketed)\n    \"\"\"\n    try:\n        ip = ipaddress.ip_address(ip_str)\n        if isinstance(ip, ipaddress.IPv6Address):\n            return f\"[{ip_str}]\"\n        return ip_str\n    except ValueError:\n        return ip_str\n\n\nclass SSRFError(Exception):\n    \"\"\"Raised when an SSRF protection check fails.\"\"\"\n\n\nclass SSRFFetchError(Exception):\n    \"\"\"Raised when SSRF-safe fetch fails.\"\"\"\n\n\ndef is_ip_allowed(ip_str: str) -> bool:\n    \"\"\"Check if an IP address is allowed (must be globally routable unicast).\n\n    Uses ip.is_global which catches:\n    - Private (10.x, 172.16-31.x, 192.168.x)\n    - Loopback (127.x, ::1)\n    - Link-local (169.254.x, fe80::) - includes AWS metadata!\n    - Reserved, unspecified\n    - RFC6598 Carrier-Grade NAT (100.64.0.0/10) - can point to internal networks\n\n    Additionally blocks multicast addresses (not caught by is_global).\n\n    Args:\n        ip_str: IP address string to check\n\n    Returns:\n        True if the IP is allowed (public unicast internet), False if blocked\n    \"\"\"\n    try:\n        ip = ipaddress.ip_address(ip_str)\n    except ValueError:\n        return False\n\n    if not ip.is_global:\n        return False\n\n    # Block multicast (not caught by is_global for some ranges)\n    if ip.is_multicast:\n        return False\n\n    # IPv6-specific checks for embedded IPv4 addresses\n    if isinstance(ip, ipaddress.IPv6Address):\n        if ip.ipv4_mapped:\n            return is_ip_allowed(str(ip.ipv4_mapped))\n        if ip.sixtofour:\n            return is_ip_allowed(str(ip.sixtofour))\n        if ip.teredo:\n            server, client = ip.teredo\n            return is_ip_allowed(str(server)) and is_ip_allowed(str(client))\n\n    return True\n\n\nasync def resolve_hostname(hostname: str, port: int = 443) -> list[str]:\n    \"\"\"Resolve hostname to IP addresses using DNS.\n\n    Args:\n        hostname: Hostname to resolve\n        port: Port number (used for getaddrinfo)\n\n    Returns:\n        List of resolved IP addresses\n\n    Raises:\n        SSRFError: If resolution fails\n    \"\"\"\n    loop = asyncio.get_running_loop()\n    try:\n        infos = await loop.run_in_executor(\n            None,\n            lambda: socket.getaddrinfo(\n                hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM\n            ),\n        )\n        ips = list({info[4][0] for info in infos})\n        if not ips:\n            raise SSRFError(f\"DNS resolution returned no addresses for {hostname}\")\n        return ips\n    except socket.gaierror as e:\n        raise SSRFError(f\"DNS resolution failed for {hostname}: {e}\") from e\n\n\n@dataclass\nclass ValidatedURL:\n    \"\"\"A URL that has been validated for SSRF with resolved IPs.\"\"\"\n\n    original_url: str\n    hostname: str\n    port: int\n    path: str\n    resolved_ips: list[str]\n\n\n@dataclass\nclass SSRFFetchResponse:\n    \"\"\"Response payload from an SSRF-safe fetch.\"\"\"\n\n    content: bytes\n    status_code: int\n    headers: dict[str, str]\n\n\nasync def validate_url(url: str, require_path: bool = False) -> ValidatedURL:\n    \"\"\"Validate URL for SSRF and resolve to IPs.\n\n    Args:\n        url: URL to validate\n        require_path: If True, require non-root path (for CIMD)\n\n    Returns:\n        ValidatedURL with resolved IPs\n\n    Raises:\n        SSRFError: If URL is invalid or resolves to blocked IPs\n    \"\"\"\n    try:\n        parsed = urlparse(url)\n    except (ValueError, AttributeError) as e:\n        raise SSRFError(f\"Invalid URL: {e}\") from e\n\n    if parsed.scheme != \"https\":\n        raise SSRFError(f\"URL must use HTTPS, got: {parsed.scheme}\")\n\n    if not parsed.netloc:\n        raise SSRFError(\"URL must have a host\")\n\n    if require_path and parsed.path in (\"\", \"/\"):\n        raise SSRFError(\"URL must have a non-root path\")\n\n    hostname = parsed.hostname or parsed.netloc\n    port = parsed.port or 443\n\n    # Resolve and validate IPs\n    resolved_ips = await resolve_hostname(hostname, port)\n\n    blocked = [ip for ip in resolved_ips if not is_ip_allowed(ip)]\n    if blocked:\n        raise SSRFError(\n            f\"URL resolves to blocked IP address(es): {blocked}. \"\n            f\"Private, loopback, link-local, and reserved IPs are not allowed.\"\n        )\n\n    return ValidatedURL(\n        original_url=url,\n        hostname=hostname,\n        port=port,\n        path=parsed.path + (\"?\" + parsed.query if parsed.query else \"\"),\n        resolved_ips=resolved_ips,\n    )\n\n\nasync def ssrf_safe_fetch(\n    url: str,\n    *,\n    require_path: bool = False,\n    max_size: int = 5120,\n    timeout: float = 10.0,\n    overall_timeout: float = 30.0,\n) -> bytes:\n    \"\"\"Fetch URL with comprehensive SSRF protection and DNS pinning.\n\n    Security measures:\n    1. HTTPS only\n    2. DNS resolution with IP validation\n    3. Connects to validated IP directly (DNS pinning prevents rebinding)\n    4. Response size limit\n    5. Redirects disabled\n    6. Overall timeout\n\n    Args:\n        url: URL to fetch\n        require_path: If True, require non-root path\n        max_size: Maximum response size in bytes (default 5KB)\n        timeout: Per-operation timeout in seconds\n        overall_timeout: Overall timeout for entire operation\n\n    Returns:\n        Response body as bytes\n\n    Raises:\n        SSRFError: If SSRF validation fails\n        SSRFFetchError: If fetch fails\n    \"\"\"\n    response = await ssrf_safe_fetch_response(\n        url,\n        require_path=require_path,\n        max_size=max_size,\n        timeout=timeout,\n        overall_timeout=overall_timeout,\n        allowed_status_codes={200},\n    )\n    return response.content\n\n\nasync def ssrf_safe_fetch_response(\n    url: str,\n    *,\n    require_path: bool = False,\n    max_size: int = 5120,\n    timeout: float = 10.0,\n    overall_timeout: float = 30.0,\n    request_headers: Mapping[str, str] | None = None,\n    allowed_status_codes: set[int] | None = None,\n) -> SSRFFetchResponse:\n    \"\"\"Fetch URL with SSRF protection and return response metadata.\n\n    This is equivalent to :func:`ssrf_safe_fetch` but returns response headers\n    and status code, and supports conditional request headers.\n    \"\"\"\n    start_time = time.monotonic()\n\n    # Validate URL and resolve DNS\n    validated = await validate_url(url, require_path=require_path)\n\n    last_error: Exception | None = None\n    expected_statuses = allowed_status_codes or {200}\n\n    for pinned_ip in validated.resolved_ips:\n        elapsed = time.monotonic() - start_time\n        if elapsed > overall_timeout:\n            raise SSRFFetchError(f\"Overall timeout exceeded: {url}\")\n        remaining = max(1.0, overall_timeout - elapsed)\n\n        pinned_url = (\n            f\"https://{format_ip_for_url(pinned_ip)}:{validated.port}{validated.path}\"\n        )\n\n        logger.debug(\n            \"SSRF-safe fetch: %s -> %s (pinned to %s)\",\n            url,\n            pinned_url,\n            pinned_ip,\n        )\n\n        headers = {\"Host\": validated.hostname}\n        if request_headers:\n            for key, value in request_headers.items():\n                # Host must remain pinned to the validated hostname.\n                if key.lower() == \"host\":\n                    continue\n                headers[key] = value\n\n        try:\n            # Use httpx with streaming to enforce size limit during download\n            async with (\n                httpx.AsyncClient(\n                    timeout=httpx.Timeout(\n                        connect=min(timeout, remaining),\n                        read=min(timeout, remaining),\n                        write=min(timeout, remaining),\n                        pool=min(timeout, remaining),\n                    ),\n                    follow_redirects=False,\n                    verify=True,\n                ) as client,\n                client.stream(\n                    \"GET\",\n                    pinned_url,\n                    headers=headers,\n                    extensions={\"sni_hostname\": validated.hostname},\n                ) as response,\n            ):\n                if time.monotonic() - start_time > overall_timeout:\n                    raise SSRFFetchError(f\"Overall timeout exceeded: {url}\")\n\n                if response.status_code not in expected_statuses:\n                    raise SSRFFetchError(f\"HTTP {response.status_code} fetching {url}\")\n\n                # Check Content-Length header first if available\n                content_length = response.headers.get(\"content-length\")\n                if content_length:\n                    try:\n                        size = int(content_length)\n                        if size > max_size:\n                            raise SSRFFetchError(\n                                f\"Response too large: {size} bytes (max {max_size})\"\n                            )\n                    except ValueError:\n                        pass\n\n                # Stream the response and enforce size limit during download\n                chunks = []\n                total = 0\n                async for chunk in response.aiter_bytes():\n                    if time.monotonic() - start_time > overall_timeout:\n                        raise SSRFFetchError(f\"Overall timeout exceeded: {url}\")\n                    total += len(chunk)\n                    if total > max_size:\n                        raise SSRFFetchError(\n                            f\"Response too large: exceeded {max_size} bytes\"\n                        )\n                    chunks.append(chunk)\n\n                return SSRFFetchResponse(\n                    content=b\"\".join(chunks),\n                    status_code=response.status_code,\n                    headers=dict(response.headers),\n                )\n\n        except httpx.TimeoutException as e:\n            last_error = e\n            continue\n        except httpx.RequestError as e:\n            last_error = e\n            continue\n\n    if last_error is not None:\n        if isinstance(last_error, httpx.TimeoutException):\n            raise SSRFFetchError(f\"Timeout fetching {url}\") from last_error\n        raise SSRFFetchError(f\"Error fetching {url}: {last_error}\") from last_error\n\n    raise SSRFFetchError(f\"Error fetching {url}: no resolved IPs succeeded\")\n"
  },
  {
    "path": "src/fastmcp/server/context.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport weakref\nfrom collections.abc import Callable, Generator, Mapping, Sequence\nfrom contextlib import contextmanager\nfrom contextvars import ContextVar, Token\nfrom dataclasses import dataclass\nfrom logging import Logger\nfrom typing import Any, Literal, overload\n\nimport mcp.types\nfrom mcp import LoggingLevel, ServerSession\nfrom mcp.server.lowlevel.server import request_ctx\nfrom mcp.shared.context import RequestContext\nfrom mcp.types import (\n    GetPromptResult,\n    ModelPreferences,\n    Root,\n    SamplingMessage,\n)\nfrom mcp.types import Prompt as SDKPrompt\nfrom mcp.types import Resource as SDKResource\nfrom pydantic.networks import AnyUrl\nfrom starlette.requests import Request\nfrom typing_extensions import TypeVar\nfrom uncalled_for import SharedContext\n\nfrom fastmcp.resources.base import ResourceResult\nfrom fastmcp.server.elicitation import (\n    AcceptedElicitation,\n    CancelledElicitation,\n    DeclinedElicitation,\n    handle_elicit_accept,\n    parse_elicit_response_type,\n)\nfrom fastmcp.server.low_level import MiddlewareServerSession\nfrom fastmcp.server.sampling import SampleStep, SamplingResult, SamplingTool\nfrom fastmcp.server.sampling.run import (\n    sample_impl,\n    sample_step_impl,\n)\nfrom fastmcp.server.server import FastMCP, StateValue\nfrom fastmcp.server.transforms.visibility import (\n    Visibility,\n)\nfrom fastmcp.server.transforms.visibility import (\n    disable_components as _disable_components,\n)\nfrom fastmcp.server.transforms.visibility import (\n    enable_components as _enable_components,\n)\nfrom fastmcp.server.transforms.visibility import (\n    get_session_transforms as _get_session_transforms,\n)\nfrom fastmcp.server.transforms.visibility import (\n    get_visibility_rules as _get_visibility_rules,\n)\nfrom fastmcp.server.transforms.visibility import (\n    reset_visibility as _reset_visibility,\n)\nfrom fastmcp.utilities.logging import _clamp_logger, get_logger\nfrom fastmcp.utilities.versions import VersionSpec\n\nlogger: Logger = get_logger(name=__name__)\nto_client_logger: Logger = logger.getChild(suffix=\"to_client\")\n\n# Convert all levels of server -> client messages to debug level\n# This clamp can be undone at runtime by calling `_unclamp_logger` or calling\n# `_clamp_logger` with a different max level.\n_clamp_logger(logger=to_client_logger, max_level=\"DEBUG\")\n\n\nT = TypeVar(\"T\", default=Any)\nResultT = TypeVar(\"ResultT\", default=str)\n\n# Import ToolChoiceOption from sampling module (after other imports)\nfrom fastmcp.server.sampling.run import ToolChoiceOption  # noqa: E402\n\n_current_context: ContextVar[Context | None] = ContextVar(\"context\", default=None)\n\nTransportType = Literal[\"stdio\", \"sse\", \"streamable-http\"]\n_current_transport: ContextVar[TransportType | None] = ContextVar(\n    \"transport\", default=None\n)\n\n\ndef set_transport(\n    transport: TransportType,\n) -> Token[TransportType | None]:\n    \"\"\"Set the current transport type. Returns token for reset.\"\"\"\n    return _current_transport.set(transport)\n\n\ndef reset_transport(token: Token[TransportType | None]) -> None:\n    \"\"\"Reset transport to previous value.\"\"\"\n    _current_transport.reset(token)\n\n\n@dataclass\nclass LogData:\n    \"\"\"Data object for passing log arguments to client-side handlers.\n\n    This provides an interface to match the Python standard library logging,\n    for compatibility with structured logging.\n    \"\"\"\n\n    msg: str\n    extra: Mapping[str, Any] | None = None\n\n\n_mcp_level_to_python_level = {\n    \"debug\": logging.DEBUG,\n    \"info\": logging.INFO,\n    \"notice\": logging.INFO,\n    \"warning\": logging.WARNING,\n    \"error\": logging.ERROR,\n    \"critical\": logging.CRITICAL,\n    \"alert\": logging.CRITICAL,\n    \"emergency\": logging.CRITICAL,\n}\n\n\n@contextmanager\ndef set_context(context: Context) -> Generator[Context, None, None]:\n    token = _current_context.set(context)\n    try:\n        yield context\n    finally:\n        _current_context.reset(token)\n\n\n@dataclass\nclass Context:\n    \"\"\"Context object providing access to MCP capabilities.\n\n    This provides a cleaner interface to MCP's RequestContext functionality.\n    It gets injected into tool and resource functions that request it via type hints.\n\n    To use context in a tool function, add a parameter with the Context type annotation:\n\n    ```python\n    @server.tool\n    async def my_tool(x: int, ctx: Context) -> str:\n        # Log messages to the client\n        await ctx.info(f\"Processing {x}\")\n        await ctx.debug(\"Debug info\")\n        await ctx.warning(\"Warning message\")\n        await ctx.error(\"Error message\")\n\n        # Report progress\n        await ctx.report_progress(50, 100, \"Processing\")\n\n        # Access resources\n        data = await ctx.read_resource(\"resource://data\")\n\n        # Get request info\n        request_id = ctx.request_id\n        client_id = ctx.client_id\n\n        # Manage state across the session (persists across requests)\n        await ctx.set_state(\"key\", \"value\")\n        value = await ctx.get_state(\"key\")\n\n        # Store non-serializable values for the current request only\n        await ctx.set_state(\"client\", http_client, serializable=False)\n\n        return str(x)\n    ```\n\n    State Management:\n    Context provides session-scoped state that persists across requests within\n    the same MCP session. State is automatically keyed by session, ensuring\n    isolation between different clients.\n\n    State set during `on_initialize` middleware will persist to subsequent tool\n    calls when using the same session object (STDIO, SSE, single-server HTTP).\n    For distributed/serverless HTTP deployments where different machines handle\n    the init and tool calls, state is isolated by the mcp-session-id header.\n\n    The context parameter name can be anything as long as it's annotated with Context.\n    The context is optional - tools that don't need it can omit the parameter.\n\n    \"\"\"\n\n    # Default TTL for session state: 1 day in seconds\n    _STATE_TTL_SECONDS: int = 86400\n\n    def __init__(\n        self,\n        fastmcp: FastMCP,\n        session: ServerSession | None = None,\n        *,\n        task_id: str | None = None,\n        origin_request_id: str | None = None,\n    ):\n        self._fastmcp: weakref.ref[FastMCP] = weakref.ref(fastmcp)\n        self._session: ServerSession | None = session  # For state ops during init\n        self._tokens: list[Token] = []\n        # Background task support (SEP-1686)\n        self._task_id: str | None = task_id\n        self._origin_request_id: str | None = origin_request_id\n        # Request-scoped state for non-serializable values (serializable=False)\n        self._request_state: dict[str, Any] = {}\n\n    @property\n    def is_background_task(self) -> bool:\n        \"\"\"True when this context is running in a background task (Docket worker).\n\n        When True, certain operations like elicit() and sample() will use\n        task-aware implementations that can pause the task and wait for\n        client input.\n\n        Example:\n            ```python\n            @server.tool(task=True)\n            async def my_task(ctx: Context) -> str:\n                # Works transparently in both foreground and background task modes\n                result = await ctx.elicit(\"Need input\", str)\n                return str(result)\n            ```\n        \"\"\"\n        return self._task_id is not None\n\n    @property\n    def task_id(self) -> str | None:\n        \"\"\"Get the background task ID if running in a background task.\n\n        Returns None if not running in a background task context.\n        \"\"\"\n        return self._task_id\n\n    @property\n    def origin_request_id(self) -> str | None:\n        \"\"\"Get the request ID that originated this execution, if available.\n\n        In foreground request mode, this is the current request_id.\n        In background task mode, this is the request_id captured when the task\n        was submitted, if one was available.\n        \"\"\"\n        if self.request_context is not None:\n            return str(self.request_context.request_id)\n        return self._origin_request_id\n\n    @property\n    def fastmcp(self) -> FastMCP:\n        \"\"\"Get the FastMCP instance.\"\"\"\n        fastmcp = self._fastmcp()\n        if fastmcp is None:\n            raise RuntimeError(\"FastMCP instance is no longer available\")\n        return fastmcp\n\n    async def __aenter__(self) -> Context:\n        \"\"\"Enter the context manager and set this context as the current context.\"\"\"\n        # Inherit request-scoped state from parent context so middleware\n        # and tool contexts share the same in-memory state dict.\n        parent = _current_context.get(None)\n        if parent is not None:\n            self._request_state = parent._request_state\n\n        # Always set this context and save the token\n        token = _current_context.set(self)\n        self._tokens.append(token)\n\n        # Set current server for dependency injection (use weakref to avoid reference cycles)\n        from fastmcp.server.dependencies import (\n            _current_docket,\n            _current_server,\n            _current_worker,\n            is_docket_available,\n        )\n\n        self._server_token = _current_server.set(weakref.ref(self.fastmcp))\n\n        # Set docket/worker from server instance for this request's context.\n        # This ensures ContextVars work even in ASGI environments (Lambda, FastAPI mount)\n        # where lifespan ContextVars don't propagate to request handlers.\n        server = self.fastmcp\n        if is_docket_available():\n            if server._docket is not None:\n                self._docket_token = _current_docket.set(server._docket)\n            if server._worker is not None:\n                self._worker_token = _current_worker.set(server._worker)\n        else:\n            # Without docket, the lifespan won't provide a SharedContext,\n            # so create one scoped to this Context for Shared() dependencies.\n            self._shared_context = SharedContext()\n            await self._shared_context.__aenter__()\n\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:\n        \"\"\"Exit the context manager and reset the most recent token.\"\"\"\n        from fastmcp.server.dependencies import (\n            _current_docket,\n            _current_server,\n            _current_worker,\n        )\n\n        # Mirror __aenter__: clean up docket/worker tokens or SharedContext\n        if hasattr(self, \"_worker_token\"):\n            _current_worker.reset(self._worker_token)\n            del self._worker_token\n        if hasattr(self, \"_docket_token\"):\n            _current_docket.reset(self._docket_token)\n            del self._docket_token\n        if hasattr(self, \"_shared_context\"):\n            await self._shared_context.__aexit__(exc_type, exc_val, exc_tb)\n            del self._shared_context\n\n        if hasattr(self, \"_server_token\"):\n            _current_server.reset(self._server_token)\n            del self._server_token\n\n        # Reset context token\n        if self._tokens:\n            token = self._tokens.pop()\n            _current_context.reset(token)\n\n    @property\n    def request_context(self) -> RequestContext[ServerSession, Any, Request] | None:\n        \"\"\"Access to the underlying request context.\n\n        Returns None when the MCP session has not been established yet.\n        Returns the full RequestContext once the MCP session is available.\n\n        For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,\n        which works whether or not the MCP session is available.\n\n        Example in middleware:\n        ```python\n        async def on_request(self, context, call_next):\n            ctx = context.fastmcp_context\n            if ctx.request_context:\n                # MCP session available - can access session_id, request_id, etc.\n                session_id = ctx.session_id\n            else:\n                # MCP session not available yet - use HTTP helpers\n                from fastmcp.server.dependencies import get_http_request\n                request = get_http_request()\n            return await call_next(context)\n        ```\n        \"\"\"\n        try:\n            return request_ctx.get()\n        except LookupError:\n            return None\n\n    @property\n    def lifespan_context(self) -> dict[str, Any]:\n        \"\"\"Access the server's lifespan context.\n\n        Returns the context dict yielded by the server's lifespan function.\n        Returns an empty dict if no lifespan was configured or if the MCP\n        session is not yet established.\n\n        In background tasks (Docket workers), where request_context is not\n        available, falls back to reading from the FastMCP server's lifespan\n        result directly.\n\n        Example:\n        ```python\n        @server.tool\n        def my_tool(ctx: Context) -> str:\n            db = ctx.lifespan_context.get(\"db\")\n            if db:\n                return db.query(\"SELECT 1\")\n            return \"No database connection\"\n        ```\n        \"\"\"\n        rc = self.request_context\n        if rc is None:\n            # In background tasks, request_context is not available.\n            # Fall back to the server's lifespan result directly (#3095).\n            result = self.fastmcp._lifespan_result\n            if result is not None:\n                return result\n            return {}\n        return rc.lifespan_context\n\n    async def report_progress(\n        self, progress: float, total: float | None = None, message: str | None = None\n    ) -> None:\n        \"\"\"Report progress for the current operation.\n\n        Works in both foreground (MCP progress notifications) and background\n        (Docket task execution) contexts.\n\n        Args:\n            progress: Current progress value e.g. 24\n            total: Optional total value e.g. 100\n            message: Optional status message describing current progress\n        \"\"\"\n\n        progress_token = (\n            self.request_context.meta.progressToken\n            if self.request_context and self.request_context.meta\n            else None\n        )\n\n        # Foreground: Send MCP progress notification if we have a token\n        if progress_token is not None:\n            await self.session.send_progress_notification(\n                progress_token=progress_token,\n                progress=progress,\n                total=total,\n                message=message,\n                related_request_id=self.request_id,\n            )\n            return\n\n        # Background: Update Docket execution progress (stored in Redis)\n        # This makes progress visible via tasks/get and notifications/tasks/status\n        from fastmcp.server.dependencies import is_docket_available\n\n        if not is_docket_available():\n            return\n\n        try:\n            from docket.dependencies import current_execution\n\n            execution = current_execution.get()\n\n            # Update progress in Redis using Docket's progress API.\n            # Docket only exposes increment() (relative), so we compute\n            # the delta from the last reported value stored on this execution.\n            if total is not None:\n                await execution.progress.set_total(int(total))\n\n            current = int(progress)\n            last: int = getattr(execution, \"_fastmcp_last_progress\", 0)\n            delta = current - last\n            if delta > 0:\n                await execution.progress.increment(delta)\n            execution._fastmcp_last_progress = current  # type: ignore[attr-defined]\n\n            if message is not None:\n                await execution.progress.set_message(message)\n        except LookupError:\n            # Not running in Docket worker context - no progress tracking available\n            pass\n\n    async def _paginate_list(\n        self,\n        request_factory: Callable[[str | None], Any],\n        call_method: Callable[[Any], Any],\n        extract_items: Callable[[Any], list[Any]],\n    ) -> list[Any]:\n        \"\"\"Generic pagination helper for list operations.\n\n        Args:\n            request_factory: Function that creates a request from a cursor\n            call_method: Async method to call with the request\n            extract_items: Function to extract items from the result\n\n        Returns:\n            List of all items across all pages\n        \"\"\"\n        all_items: list[Any] = []\n        cursor: str | None = None\n        seen_cursors: set[str] = set()\n        while True:\n            request = request_factory(cursor)\n            result = await call_method(request)\n            all_items.extend(extract_items(result))\n            if not result.nextCursor:\n                break\n            if result.nextCursor in seen_cursors:\n                break\n            seen_cursors.add(result.nextCursor)\n            cursor = result.nextCursor\n        return all_items\n\n    async def list_resources(self) -> list[SDKResource]:\n        \"\"\"List all available resources from the server.\n\n        Returns:\n            List of Resource objects available on the server\n        \"\"\"\n        return await self._paginate_list(\n            request_factory=lambda cursor: mcp.types.ListResourcesRequest(\n                params=mcp.types.PaginatedRequestParams(cursor=cursor)\n                if cursor\n                else None\n            ),\n            call_method=self.fastmcp._list_resources_mcp,\n            extract_items=lambda result: result.resources,\n        )\n\n    async def list_prompts(self) -> list[SDKPrompt]:\n        \"\"\"List all available prompts from the server.\n\n        Returns:\n            List of Prompt objects available on the server\n        \"\"\"\n        return await self._paginate_list(\n            request_factory=lambda cursor: mcp.types.ListPromptsRequest(\n                params=mcp.types.PaginatedRequestParams(cursor=cursor)\n                if cursor\n                else None\n            ),\n            call_method=self.fastmcp._list_prompts_mcp,\n            extract_items=lambda result: result.prompts,\n        )\n\n    async def get_prompt(\n        self, name: str, arguments: dict[str, Any] | None = None\n    ) -> GetPromptResult:\n        \"\"\"Get a prompt by name with optional arguments.\n\n        Args:\n            name: The name of the prompt to get\n            arguments: Optional arguments to pass to the prompt\n\n        Returns:\n            The prompt result\n        \"\"\"\n        result = await self.fastmcp.render_prompt(name, arguments)\n        if isinstance(result, mcp.types.CreateTaskResult):\n            raise RuntimeError(\n                \"Unexpected CreateTaskResult: Context calls should not have task metadata\"\n            )\n        return result.to_mcp_prompt_result()\n\n    async def read_resource(self, uri: str | AnyUrl) -> ResourceResult:\n        \"\"\"Read a resource by URI.\n\n        Args:\n            uri: Resource URI to read\n\n        Returns:\n            ResourceResult with contents\n        \"\"\"\n        result = await self.fastmcp.read_resource(str(uri))\n        if isinstance(result, mcp.types.CreateTaskResult):\n            raise RuntimeError(\n                \"Unexpected CreateTaskResult: Context calls should not have task metadata\"\n            )\n        return result\n\n    async def log(\n        self,\n        message: str,\n        level: LoggingLevel | None = None,\n        logger_name: str | None = None,\n        extra: Mapping[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Send a log message to the client.\n\n        Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\n\n        Args:\n            message: Log message\n            level: Optional log level. One of \"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\",\n                \"alert\", or \"emergency\". Default is \"info\".\n            logger_name: Optional logger name\n            extra: Optional mapping for additional arguments\n        \"\"\"\n        data = LogData(msg=message, extra=extra)\n        related_request_id = self.origin_request_id\n\n        await _log_to_server_and_client(\n            data=data,\n            session=self.session,\n            level=level or \"info\",\n            logger_name=logger_name,\n            related_request_id=related_request_id,\n        )\n\n    @property\n    def transport(self) -> TransportType | None:\n        \"\"\"Get the current transport type.\n\n        Returns the transport type used to run this server: \"stdio\", \"sse\",\n        or \"streamable-http\". Returns None if called outside of a server context.\n        \"\"\"\n        return _current_transport.get()\n\n    def client_supports_extension(self, extension_id: str) -> bool:\n        \"\"\"Check whether the connected client supports a given MCP extension.\n\n        Inspects the ``extensions`` extra field on ``ClientCapabilities``\n        sent by the client during initialization.\n\n        Returns ``False`` when no session is available (e.g., outside a\n        request context) or when the client did not advertise the extension.\n\n        Example::\n\n            from fastmcp.server.apps import UI_EXTENSION_ID\n\n            @mcp.tool\n            async def my_tool(ctx: Context) -> str:\n                if ctx.client_supports_extension(UI_EXTENSION_ID):\n                    return \"UI-capable client\"\n                return \"text-only client\"\n        \"\"\"\n        rc = self.request_context\n        if rc is None:\n            return False\n        session = rc.session\n        if not isinstance(session, MiddlewareServerSession):\n            return False\n        return session.client_supports_extension(extension_id)\n\n    @property\n    def client_id(self) -> str | None:\n        \"\"\"Get the client ID if available.\"\"\"\n        return (\n            getattr(self.request_context.meta, \"client_id\", None)\n            if self.request_context and self.request_context.meta\n            else None\n        )\n\n    @property\n    def request_id(self) -> str:\n        \"\"\"Get the unique ID for this request.\n\n        Raises RuntimeError if MCP request context is not available.\n        \"\"\"\n        if self.request_context is None:\n            raise RuntimeError(\n                \"request_id is not available because the MCP session has not been established yet. \"\n                \"Check `context.request_context` for None before accessing this attribute.\"\n            )\n        return str(self.request_context.request_id)\n\n    @property\n    def session_id(self) -> str:\n        \"\"\"Get the MCP session ID for ALL transports.\n\n        Returns the session ID that can be used as a key for session-based\n        data storage (e.g., Redis) to share data between tool calls within\n        the same client session.\n\n        Returns:\n            The session ID for StreamableHTTP transports, or a generated ID\n            for other transports.\n\n        Raises:\n            RuntimeError if no session is available.\n\n        Example:\n            ```python\n            @server.tool\n            def store_data(data: dict, ctx: Context) -> str:\n                session_id = ctx.session_id\n                redis_client.set(f\"session:{session_id}:data\", json.dumps(data))\n                return f\"Data stored for session {session_id}\"\n            ```\n        \"\"\"\n        from uuid import uuid4\n\n        # Get session from request context or _session (for on_initialize)\n        request_ctx = self.request_context\n        if request_ctx is not None:\n            session = request_ctx.session\n        elif self._session is not None:\n            session = self._session\n        else:\n            raise RuntimeError(\n                \"session_id is not available because no session exists. \"\n                \"This typically means you're outside a request context.\"\n            )\n\n        # Check for cached session ID\n        session_id = getattr(session, \"_fastmcp_state_prefix\", None)\n        if session_id is not None:\n            return session_id\n\n        # For HTTP, try to get from header\n        if request_ctx is not None:\n            request = request_ctx.request\n            if request:\n                session_id = request.headers.get(\"mcp-session-id\")\n\n        # For STDIO/SSE/in-memory, generate a UUID\n        if session_id is None:\n            session_id = str(uuid4())\n\n        # Cache on session for consistency\n        session._fastmcp_state_prefix = session_id  # type: ignore[attr-defined]\n        return session_id\n\n    @property\n    def session(self) -> ServerSession:\n        \"\"\"Access to the underlying session for advanced usage.\n\n        In request mode: Returns the session from the active request context.\n        In background task mode: Returns the session stored at Context creation.\n\n        Raises RuntimeError if no session is available.\n        \"\"\"\n        # Background task mode: use the stored session\n        if self.is_background_task and self._session is not None:\n            return self._session\n\n        # Request mode: use request context\n        if self.request_context is not None:\n            return self.request_context.session\n\n        # Fallback to stored session (e.g., during on_initialize)\n        if self._session is not None:\n            return self._session\n\n        raise RuntimeError(\n            \"session is not available because the MCP session has not been established yet. \"\n            \"Check `context.request_context` for None before accessing this attribute.\"\n        )\n\n    # Convenience methods for common log levels\n    async def debug(\n        self,\n        message: str,\n        logger_name: str | None = None,\n        extra: Mapping[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Send a `DEBUG`-level message to the connected MCP Client.\n\n        Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\"\"\"\n        await self.log(\n            level=\"debug\",\n            message=message,\n            logger_name=logger_name,\n            extra=extra,\n        )\n\n    async def info(\n        self,\n        message: str,\n        logger_name: str | None = None,\n        extra: Mapping[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Send a `INFO`-level message to the connected MCP Client.\n\n        Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\"\"\"\n        await self.log(\n            level=\"info\",\n            message=message,\n            logger_name=logger_name,\n            extra=extra,\n        )\n\n    async def warning(\n        self,\n        message: str,\n        logger_name: str | None = None,\n        extra: Mapping[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Send a `WARNING`-level message to the connected MCP Client.\n\n        Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\"\"\"\n        await self.log(\n            level=\"warning\",\n            message=message,\n            logger_name=logger_name,\n            extra=extra,\n        )\n\n    async def error(\n        self,\n        message: str,\n        logger_name: str | None = None,\n        extra: Mapping[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Send a `ERROR`-level message to the connected MCP Client.\n\n        Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.\"\"\"\n        await self.log(\n            level=\"error\",\n            message=message,\n            logger_name=logger_name,\n            extra=extra,\n        )\n\n    async def list_roots(self) -> list[Root]:\n        \"\"\"List the roots available to the server, as indicated by the client.\"\"\"\n        result = await self.session.list_roots()\n        return result.roots\n\n    async def send_notification(\n        self, notification: mcp.types.ServerNotificationType\n    ) -> None:\n        \"\"\"Send a notification to the client immediately.\n\n        Args:\n            notification: An MCP notification instance (e.g., ToolListChangedNotification())\n        \"\"\"\n        await self.session.send_notification(mcp.types.ServerNotification(notification))\n\n    async def close_sse_stream(self) -> None:\n        \"\"\"Close the current response stream to trigger client reconnection.\n\n        When using StreamableHTTP transport with an EventStore configured, this\n        method gracefully closes the HTTP connection for the current request.\n        The client will automatically reconnect (after `retry_interval` milliseconds)\n        and resume receiving events from where it left off via the EventStore.\n\n        This is useful for long-running operations to avoid load balancer timeouts.\n        Instead of holding a connection open for minutes, you can periodically close\n        and let the client reconnect.\n\n        Example:\n            ```python\n            @mcp.tool\n            async def long_running_task(ctx: Context) -> str:\n                for i in range(100):\n                    await ctx.report_progress(i, 100)\n\n                    # Close connection every 30 iterations to avoid LB timeouts\n                    if i % 30 == 0 and i > 0:\n                        await ctx.close_sse_stream()\n\n                    await do_work()\n                return \"Done\"\n            ```\n\n        Note:\n            This is a no-op (with a debug log) if not using StreamableHTTP\n            transport with an EventStore configured.\n        \"\"\"\n        if not self.request_context or not self.request_context.close_sse_stream:\n            logger.debug(\n                \"close_sse_stream() called but not applicable \"\n                \"(requires StreamableHTTP transport with event_store)\"\n            )\n            return\n        await self.request_context.close_sse_stream()\n\n    async def sample_step(\n        self,\n        messages: str | Sequence[str | SamplingMessage],\n        *,\n        system_prompt: str | None = None,\n        temperature: float | None = None,\n        max_tokens: int | None = None,\n        model_preferences: ModelPreferences | str | list[str] | None = None,\n        tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,\n        tool_choice: ToolChoiceOption | str | None = None,\n        execute_tools: bool = True,\n        mask_error_details: bool | None = None,\n        tool_concurrency: int | None = None,\n    ) -> SampleStep:\n        \"\"\"\n        Make a single LLM sampling call.\n\n        This is a stateless function that makes exactly one LLM call and optionally\n        executes any requested tools. Use this for fine-grained control over the\n        sampling loop.\n\n        Args:\n            messages: The message(s) to send. Can be a string, list of strings,\n                or list of SamplingMessage objects.\n            system_prompt: Optional system prompt for the LLM.\n            temperature: Optional sampling temperature.\n            max_tokens: Maximum tokens to generate. Defaults to 512.\n            model_preferences: Optional model preferences.\n            tools: Optional list of tools the LLM can use.\n            tool_choice: Tool choice mode (\"auto\", \"required\", or \"none\").\n            execute_tools: If True (default), execute tool calls and append results\n                to history. If False, return immediately with tool_calls available\n                in the step for manual execution.\n            mask_error_details: If True, mask detailed error messages from tool\n                execution. When None (default), uses the global settings value.\n                Tools can raise ToolError to bypass masking.\n            tool_concurrency: Controls parallel execution of tools:\n                - None (default): Sequential execution (one at a time)\n                - 0: Unlimited parallel execution\n                - N > 0: Execute at most N tools concurrently\n                If any tool has sequential=True, all tools execute sequentially\n                regardless of this setting.\n\n        Returns:\n            SampleStep containing:\n            - .response: The raw LLM response\n            - .history: Messages including input, assistant response, and tool results\n            - .is_tool_use: True if the LLM requested tool execution\n            - .tool_calls: List of tool calls (if any)\n            - .text: The text content (if any)\n\n        Example:\n            messages = \"Research X\"\n\n            while True:\n                step = await ctx.sample_step(messages, tools=[search])\n\n                if not step.is_tool_use:\n                    print(step.text)\n                    break\n\n                # Continue with tool results\n                messages = step.history\n        \"\"\"\n        return await sample_step_impl(\n            self,\n            messages=messages,\n            system_prompt=system_prompt,\n            temperature=temperature,\n            max_tokens=max_tokens,\n            model_preferences=model_preferences,\n            tools=tools,\n            tool_choice=tool_choice,\n            auto_execute_tools=execute_tools,\n            mask_error_details=mask_error_details,\n            tool_concurrency=tool_concurrency,\n        )\n\n    @overload\n    async def sample(\n        self,\n        messages: str | Sequence[str | SamplingMessage],\n        *,\n        system_prompt: str | None = None,\n        temperature: float | None = None,\n        max_tokens: int | None = None,\n        model_preferences: ModelPreferences | str | list[str] | None = None,\n        tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,\n        result_type: type[ResultT],\n        mask_error_details: bool | None = None,\n        tool_concurrency: int | None = None,\n    ) -> SamplingResult[ResultT]:\n        \"\"\"Overload: With result_type, returns SamplingResult[ResultT].\"\"\"\n\n    @overload\n    async def sample(\n        self,\n        messages: str | Sequence[str | SamplingMessage],\n        *,\n        system_prompt: str | None = None,\n        temperature: float | None = None,\n        max_tokens: int | None = None,\n        model_preferences: ModelPreferences | str | list[str] | None = None,\n        tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,\n        result_type: None = None,\n        mask_error_details: bool | None = None,\n        tool_concurrency: int | None = None,\n    ) -> SamplingResult[str]:\n        \"\"\"Overload: Without result_type, returns SamplingResult[str].\"\"\"\n\n    async def sample(\n        self,\n        messages: str | Sequence[str | SamplingMessage],\n        *,\n        system_prompt: str | None = None,\n        temperature: float | None = None,\n        max_tokens: int | None = None,\n        model_preferences: ModelPreferences | str | list[str] | None = None,\n        tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,\n        result_type: type[ResultT] | None = None,\n        mask_error_details: bool | None = None,\n        tool_concurrency: int | None = None,\n    ) -> SamplingResult[ResultT] | SamplingResult[str]:\n        \"\"\"\n        Send a sampling request to the client and await the response.\n\n        This method runs to completion automatically. When tools are provided,\n        it executes a tool loop: if the LLM returns a tool use request, the tools\n        are executed and the results are sent back to the LLM. This continues\n        until the LLM provides a final text response.\n\n        When result_type is specified, a synthetic `final_response` tool is\n        created. The LLM calls this tool to provide the structured response,\n        which is validated against the result_type and returned as `.result`.\n\n        For fine-grained control over the sampling loop, use sample_step() instead.\n\n        Args:\n            messages: The message(s) to send. Can be a string, list of strings,\n                or list of SamplingMessage objects.\n            system_prompt: Optional system prompt for the LLM.\n            temperature: Optional sampling temperature.\n            max_tokens: Maximum tokens to generate. Defaults to 512.\n            model_preferences: Optional model preferences.\n            tools: Optional list of tools the LLM can use. Accepts plain\n                functions or SamplingTools.\n            result_type: Optional type for structured output. When specified,\n                a synthetic `final_response` tool is created and the LLM's\n                response is validated against this type.\n            mask_error_details: If True, mask detailed error messages from tool\n                execution. When None (default), uses the global settings value.\n                Tools can raise ToolError to bypass masking.\n            tool_concurrency: Controls parallel execution of tools:\n                - None (default): Sequential execution (one at a time)\n                - 0: Unlimited parallel execution\n                - N > 0: Execute at most N tools concurrently\n                If any tool has sequential=True, all tools execute sequentially\n                regardless of this setting.\n\n        Returns:\n            SamplingResult[T] containing:\n            - .text: The text representation (raw text or JSON for structured)\n            - .result: The typed result (str for text, parsed object for structured)\n            - .history: All messages exchanged during sampling\n\n        Note:\n            Background task support for sampling is planned for a future release.\n            Currently, sampling in background tasks requires using the low-level\n            session.create_message() API directly.\n        \"\"\"\n        # TODO: Add background task support similar to elicit() when is_background_task\n        return await sample_impl(\n            self,\n            messages=messages,\n            system_prompt=system_prompt,\n            temperature=temperature,\n            max_tokens=max_tokens,\n            model_preferences=model_preferences,\n            tools=tools,\n            result_type=result_type,\n            mask_error_details=mask_error_details,\n            tool_concurrency=tool_concurrency,\n        )\n\n    @overload\n    async def elicit(\n        self,\n        message: str,\n        response_type: None,\n    ) -> (\n        AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation\n    ): ...\n\n    \"\"\"When response_type is None, the accepted elicitation will contain an\n    empty dict\"\"\"\n\n    @overload\n    async def elicit(\n        self,\n        message: str,\n        response_type: type[T],\n    ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ...\n\n    \"\"\"When response_type is not None, the accepted elicitation will contain the\n    response data\"\"\"\n\n    @overload\n    async def elicit(\n        self,\n        message: str,\n        response_type: list[str],\n    ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...\n\n    \"\"\"When response_type is a list of strings, the accepted elicitation will\n    contain the selected string response\"\"\"\n\n    @overload\n    async def elicit(\n        self,\n        message: str,\n        response_type: dict[str, dict[str, str]],\n    ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...\n\n    \"\"\"When response_type is a dict mapping keys to title dicts, the accepted\n    elicitation will contain the selected key\"\"\"\n\n    @overload\n    async def elicit(\n        self,\n        message: str,\n        response_type: list[list[str]],\n    ) -> (\n        AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation\n    ): ...\n\n    \"\"\"When response_type is a list containing a list of strings (multi-select),\n    the accepted elicitation will contain a list of selected strings\"\"\"\n\n    @overload\n    async def elicit(\n        self,\n        message: str,\n        response_type: list[dict[str, dict[str, str]]],\n    ) -> (\n        AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation\n    ): ...\n\n    \"\"\"When response_type is a list containing a dict mapping keys to title dicts\n    (multi-select with titles), the accepted elicitation will contain a list of\n    selected keys\"\"\"\n\n    async def elicit(\n        self,\n        message: str,\n        response_type: type[T]\n        | list[str]\n        | dict[str, dict[str, str]]\n        | list[list[str]]\n        | list[dict[str, dict[str, str]]]\n        | None = None,\n    ) -> (\n        AcceptedElicitation[T]\n        | AcceptedElicitation[dict[str, Any]]\n        | AcceptedElicitation[str]\n        | AcceptedElicitation[list[str]]\n        | DeclinedElicitation\n        | CancelledElicitation\n    ):\n        \"\"\"\n        Send an elicitation request to the client and await the response.\n\n        Call this method at any time to request additional information from\n        the user through the client. The client must support elicitation,\n        or the request will error.\n\n        Note that the MCP protocol only supports simple object schemas with\n        primitive types. You can provide a dataclass, TypedDict, or BaseModel to\n        comply. If you provide a primitive type, an object schema with a single\n        \"value\" field will be generated for the MCP interaction and\n        automatically deconstructed into the primitive type upon response.\n\n        If the response_type is None, the generated schema will be that of an\n        empty object in order to comply with the MCP protocol requirements.\n        Clients must send an empty object (\"{}\")in response.\n\n        Args:\n            message: A human-readable message explaining what information is needed\n            response_type: The type of the response, which should be a primitive\n                type or dataclass or BaseModel. If it is a primitive type, an\n                object schema with a single \"value\" field will be generated.\n\n        Note:\n            This method works transparently in both request and background task\n            contexts. In background task mode (SEP-1686), it will set the task\n            status to \"input_required\" and wait for the client to provide input.\n        \"\"\"\n        config = parse_elicit_response_type(response_type)\n\n        if self.is_background_task:\n            # Background task mode: use task-aware elicitation\n            result = await self._elicit_for_task(\n                message=message,\n                schema=config.schema,\n            )\n        else:\n            # Standard request mode: use session.elicit directly\n            result = await self.session.elicit(\n                message=message,\n                requestedSchema=config.schema,\n                related_request_id=self.request_id,\n            )\n\n        if result.action == \"accept\":\n            return handle_elicit_accept(config, result.content)\n        elif result.action == \"decline\":\n            return DeclinedElicitation()\n        elif result.action == \"cancel\":\n            return CancelledElicitation()\n        else:\n            raise ValueError(f\"Unexpected elicitation action: {result.action}\")\n\n    async def _elicit_for_task(\n        self,\n        message: str,\n        schema: dict[str, Any],\n    ) -> mcp.types.ElicitResult:\n        \"\"\"Send an elicitation request from a background task (SEP-1686).\n\n        This method handles elicitation when running in a Docket worker context,\n        where there's no active MCP request. It:\n        1. Sets the task status to \"input_required\"\n        2. Sends the elicitation request with task metadata\n        3. Waits for the client to provide input via tasks/sendInput\n        4. Returns the result and resumes task execution\n\n        Args:\n            message: The message to display to the user\n            schema: The JSON schema for the expected response\n\n        Returns:\n            ElicitResult with the user's response\n\n        Raises:\n            RuntimeError: If not running in a background task context\n        \"\"\"\n        if not self.is_background_task:\n            raise RuntimeError(\n                \"_elicit_for_task called but not in a background task context\"\n            )\n\n        # Import here to avoid circular imports and optional dependency issues\n        from fastmcp.server.tasks.elicitation import elicit_for_task\n\n        return await elicit_for_task(\n            task_id=self._task_id,  # type: ignore[arg-type]\n            session=self._session,\n            message=message,\n            schema=schema,\n            fastmcp=self.fastmcp,\n        )\n\n    def _make_state_key(self, key: str) -> str:\n        \"\"\"Create session-prefixed key for state storage.\"\"\"\n        return f\"{self.session_id}:{key}\"\n\n    async def set_state(\n        self, key: str, value: Any, *, serializable: bool = True\n    ) -> None:\n        \"\"\"Set a value in the state store.\n\n        By default, values are stored in the session-scoped state store and\n        persist across requests within the same MCP session. Values must be\n        JSON-serializable (dicts, lists, strings, numbers, etc.).\n\n        For non-serializable values (e.g., HTTP clients, database connections),\n        pass ``serializable=False``. These values are stored in a request-scoped\n        dict and only live for the current MCP request (tool call, resource\n        read, or prompt render). They will not be available in subsequent\n        requests.\n\n        The key is automatically prefixed with the session identifier.\n        \"\"\"\n        prefixed_key = self._make_state_key(key)\n        if not serializable:\n            self._request_state[prefixed_key] = value\n            return\n        # Clear any request-scoped shadow so the session value is visible\n        self._request_state.pop(prefixed_key, None)\n        try:\n            await self.fastmcp._state_store.put(\n                key=prefixed_key,\n                value=StateValue(value=value),\n                ttl=self._STATE_TTL_SECONDS,\n            )\n        except Exception as e:\n            # Catch serialization errors from Pydantic (ValueError) or\n            # the key_value library (SerializationError). Both contain\n            # \"serialize\" in the message. Other exceptions propagate as-is.\n            if \"serialize\" in str(e).lower():\n                raise TypeError(\n                    f\"Value for state key {key!r} is not serializable. \"\n                    f\"Use set_state({key!r}, value, serializable=False) to store \"\n                    f\"non-serializable values. Note: non-serializable state is \"\n                    f\"request-scoped and will not persist across requests.\"\n                ) from e\n            raise\n\n    async def get_state(self, key: str) -> Any:\n        \"\"\"Get a value from the state store.\n\n        Checks request-scoped state first (set with ``serializable=False``),\n        then falls back to the session-scoped state store.\n\n        Returns None if the key is not found.\n        \"\"\"\n        prefixed_key = self._make_state_key(key)\n        if prefixed_key in self._request_state:\n            return self._request_state[prefixed_key]\n        result = await self.fastmcp._state_store.get(key=prefixed_key)\n        return result.value if result is not None else None\n\n    async def delete_state(self, key: str) -> None:\n        \"\"\"Delete a value from the state store.\n\n        Removes from both request-scoped and session-scoped stores.\n        \"\"\"\n        prefixed_key = self._make_state_key(key)\n        self._request_state.pop(prefixed_key, None)\n        await self.fastmcp._state_store.delete(key=prefixed_key)\n\n    # -------------------------------------------------------------------------\n    # Session visibility control\n    # -------------------------------------------------------------------------\n\n    async def _get_visibility_rules(self) -> list[dict[str, Any]]:\n        \"\"\"Load visibility rule dicts from session state.\"\"\"\n        return await _get_visibility_rules(self)\n\n    async def _get_session_transforms(self) -> list[Visibility]:\n        \"\"\"Get session-specific Visibility transforms from state store.\"\"\"\n        return await _get_session_transforms(self)\n\n    async def enable_components(\n        self,\n        *,\n        names: set[str] | None = None,\n        keys: set[str] | None = None,\n        version: VersionSpec | None = None,\n        tags: set[str] | None = None,\n        components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]]\n        | None = None,\n        match_all: bool = False,\n    ) -> None:\n        \"\"\"Enable components matching criteria for this session only.\n\n        Session rules override global transforms. Rules accumulate - each call\n        adds a new rule to the session. Later marks override earlier ones\n        (Visibility transform semantics).\n\n        Sends notifications to this session only: ToolListChangedNotification,\n        ResourceListChangedNotification, and PromptListChangedNotification.\n\n        Args:\n            names: Component names or URIs to match.\n            keys: Component keys to match (e.g., {\"tool:my_tool@v1\"}).\n            version: Component version spec to match.\n            tags: Tags to match (component must have at least one).\n            components: Component types to match (e.g., {\"tool\", \"prompt\"}).\n            match_all: If True, matches all components regardless of other criteria.\n        \"\"\"\n        await _enable_components(\n            self,\n            names=names,\n            keys=keys,\n            version=version,\n            tags=tags,\n            components=components,\n            match_all=match_all,\n        )\n\n    async def disable_components(\n        self,\n        *,\n        names: set[str] | None = None,\n        keys: set[str] | None = None,\n        version: VersionSpec | None = None,\n        tags: set[str] | None = None,\n        components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]]\n        | None = None,\n        match_all: bool = False,\n    ) -> None:\n        \"\"\"Disable components matching criteria for this session only.\n\n        Session rules override global transforms. Rules accumulate - each call\n        adds a new rule to the session. Later marks override earlier ones\n        (Visibility transform semantics).\n\n        Sends notifications to this session only: ToolListChangedNotification,\n        ResourceListChangedNotification, and PromptListChangedNotification.\n\n        Args:\n            names: Component names or URIs to match.\n            keys: Component keys to match (e.g., {\"tool:my_tool@v1\"}).\n            version: Component version spec to match.\n            tags: Tags to match (component must have at least one).\n            components: Component types to match (e.g., {\"tool\", \"prompt\"}).\n            match_all: If True, matches all components regardless of other criteria.\n        \"\"\"\n        await _disable_components(\n            self,\n            names=names,\n            keys=keys,\n            version=version,\n            tags=tags,\n            components=components,\n            match_all=match_all,\n        )\n\n    async def reset_visibility(self) -> None:\n        \"\"\"Clear all session visibility rules.\n\n        Use this to reset session visibility back to global defaults.\n\n        Sends notifications to this session only: ToolListChangedNotification,\n        ResourceListChangedNotification, and PromptListChangedNotification.\n        \"\"\"\n        await _reset_visibility(self)\n\n\n_MCP_LEVEL_SEVERITY: dict[LoggingLevel, int] = {\n    \"debug\": 0,\n    \"info\": 1,\n    \"notice\": 2,\n    \"warning\": 3,\n    \"error\": 4,\n    \"critical\": 5,\n    \"alert\": 6,\n    \"emergency\": 7,\n}\n\n\nasync def _log_to_server_and_client(\n    data: LogData,\n    session: ServerSession,\n    level: LoggingLevel,\n    logger_name: str | None = None,\n    related_request_id: str | None = None,\n) -> None:\n    \"\"\"Log a message to the server and client.\"\"\"\n    from fastmcp.server.low_level import MiddlewareServerSession\n\n    if isinstance(session, MiddlewareServerSession):\n        min_level = session._minimum_logging_level or session.fastmcp.client_log_level\n        if min_level is not None:\n            if _MCP_LEVEL_SEVERITY[level] < _MCP_LEVEL_SEVERITY[min_level]:\n                return\n\n    msg_prefix = f\"Sending {level.upper()} to client\"\n\n    if logger_name:\n        msg_prefix += f\" ({logger_name})\"\n\n    to_client_logger.log(\n        level=_mcp_level_to_python_level[level],\n        msg=f\"{msg_prefix}: {data.msg}\",\n        extra=data.extra,\n    )\n\n    await session.send_log_message(\n        level=level,\n        data=data,\n        logger=logger_name,\n        related_request_id=related_request_id,\n    )\n"
  },
  {
    "path": "src/fastmcp/server/dependencies.py",
    "content": "\"\"\"Dependency injection for FastMCP.\n\nDI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket\nusing the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,\nCurrentWorker) and background task execution require fastmcp[tasks].\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport inspect\nimport logging\nimport weakref\nfrom collections.abc import AsyncGenerator, Callable\nfrom contextlib import AsyncExitStack, asynccontextmanager\nfrom contextvars import ContextVar, Token\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom functools import lru_cache\nfrom types import TracebackType\nfrom typing import TYPE_CHECKING, Any, Protocol, cast, get_type_hints, runtime_checkable\n\nfrom mcp.server.auth.middleware.auth_context import (\n    get_access_token as _sdk_get_access_token,\n)\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\nfrom mcp.server.auth.provider import (\n    AccessToken as _SDKAccessToken,\n)\nfrom mcp.server.lowlevel.server import request_ctx\nfrom starlette.requests import Request\nfrom uncalled_for import Dependency, get_dependency_parameters\nfrom uncalled_for.resolution import _Depends\n\nfrom fastmcp.exceptions import FastMCPError\nfrom fastmcp.server.auth import AccessToken\nfrom fastmcp.server.http import _current_http_request\nfrom fastmcp.utilities.async_utils import (\n    call_sync_fn_in_threadpool,\n    is_coroutine_function,\n)\nfrom fastmcp.utilities.types import find_kwarg_by_type, is_class_member_of_type\n\n_logger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.worker import Worker\n    from mcp.server.session import ServerSession\n\n    from fastmcp.server.context import Context\n    from fastmcp.server.server import FastMCP\n\n\n__all__ = [\n    \"AccessToken\",\n    \"CurrentAccessToken\",\n    \"CurrentContext\",\n    \"CurrentDocket\",\n    \"CurrentFastMCP\",\n    \"CurrentHeaders\",\n    \"CurrentRequest\",\n    \"CurrentWorker\",\n    \"Progress\",\n    \"TaskContextInfo\",\n    \"TokenClaim\",\n    \"get_access_token\",\n    \"get_context\",\n    \"get_http_headers\",\n    \"get_http_request\",\n    \"get_server\",\n    \"get_task_context\",\n    \"get_task_session\",\n    \"is_docket_available\",\n    \"register_task_session\",\n    \"require_docket\",\n    \"resolve_dependencies\",\n    \"transform_context_annotations\",\n    \"without_injected_parameters\",\n]\n\n\n# --- TaskContextInfo and get_task_context ---\n\n\n@dataclass(frozen=True, slots=True)\nclass TaskContextInfo:\n    \"\"\"Information about the current background task context.\n\n    Returned by ``get_task_context()`` when running inside a Docket worker.\n    Contains identifiers needed to communicate with the MCP session.\n    \"\"\"\n\n    task_id: str\n    \"\"\"The MCP task ID (server-generated UUID).\"\"\"\n\n    session_id: str\n    \"\"\"The session ID that submitted this task.\"\"\"\n\n\ndef get_task_context() -> TaskContextInfo | None:\n    \"\"\"Get the current task context if running inside a background task worker.\n\n    This function extracts task information from the Docket execution context.\n    Returns None if not running in a task context (e.g., foreground execution).\n\n    Returns:\n        TaskContextInfo with task_id and session_id, or None if not in a task.\n    \"\"\"\n    if not is_docket_available():\n        return None\n\n    from docket.dependencies import current_execution\n\n    try:\n        execution = current_execution.get()\n        # Parse the task key: {session_id}:{task_id}:{task_type}:{component}\n        from fastmcp.server.tasks.keys import parse_task_key\n\n        key_parts = parse_task_key(execution.key)\n        return TaskContextInfo(\n            task_id=key_parts[\"client_task_id\"],\n            session_id=key_parts[\"session_id\"],\n        )\n    except LookupError:\n        # Not in worker context\n        return None\n    except (ValueError, KeyError):\n        # Invalid task key format\n        return None\n\n\n# --- Session registry for background task Context ---\n\n\n_task_sessions: dict[str, weakref.ref[ServerSession]] = {}\n\n\ndef register_task_session(session_id: str, session: ServerSession) -> None:\n    \"\"\"Register a session for Context access in background tasks.\n\n    Called automatically when a task is submitted to Docket. The session is\n    stored as a weakref so it doesn't prevent garbage collection when the\n    client disconnects.\n\n    Args:\n        session_id: The session identifier\n        session: The ServerSession instance\n    \"\"\"\n    _task_sessions[session_id] = weakref.ref(session)\n\n\ndef get_task_session(session_id: str) -> ServerSession | None:\n    \"\"\"Get a registered session by ID if still alive.\n\n    Args:\n        session_id: The session identifier\n\n    Returns:\n        The ServerSession if found and alive, None otherwise\n    \"\"\"\n    ref = _task_sessions.get(session_id)\n    if ref is None:\n        return None\n    session = ref()\n    if session is None:\n        # Session was garbage collected, clean up entry\n        _task_sessions.pop(session_id, None)\n    return session\n\n\n# --- ContextVars ---\n\n_current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar(\n    \"server\", default=None\n)\n_current_docket: ContextVar[Docket | None] = ContextVar(\"docket\", default=None)\n_current_worker: ContextVar[Worker | None] = ContextVar(\"worker\", default=None)\n_task_access_token: ContextVar[AccessToken | None] = ContextVar(\n    \"task_access_token\", default=None\n)\n\n\n# --- Docket availability check ---\n\n_DOCKET_AVAILABLE: bool | None = None\n\n\ndef is_docket_available() -> bool:\n    \"\"\"Check if pydocket is installed.\"\"\"\n    global _DOCKET_AVAILABLE\n    if _DOCKET_AVAILABLE is None:\n        try:\n            import docket  # noqa: F401\n\n            _DOCKET_AVAILABLE = True\n        except ImportError:\n            _DOCKET_AVAILABLE = False\n    return _DOCKET_AVAILABLE\n\n\ndef require_docket(feature: str) -> None:\n    \"\"\"Raise ImportError with install instructions if docket not available.\n\n    Args:\n        feature: Description of what requires docket (e.g., \"`task=True`\",\n                 \"CurrentDocket()\"). Will be included in the error message.\n    \"\"\"\n    if not is_docket_available():\n        raise ImportError(\n            f\"FastMCP background tasks require the `tasks` extra. \"\n            f\"Install with: pip install 'fastmcp[tasks]'. \"\n            f\"(Triggered by {feature})\"\n        )\n\n\n# Import Progress separately — it's docket-specific, not part of uncalled-for\ntry:\n    from docket.dependencies import Progress as DocketProgress\nexcept ImportError:\n    DocketProgress = None  # type: ignore[assignment]\n\n\n# --- Context utilities ---\n\n\ndef transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]:\n    \"\"\"Transform ctx: Context into ctx: Context = CurrentContext().\n\n    Transforms ALL params typed as Context to use Docket's DI system,\n    unless they already have a Dependency-based default (like CurrentContext()).\n\n    This unifies the legacy type annotation DI with Docket's Depends() system,\n    allowing both patterns to work through a single resolution path.\n\n    Note: Only POSITIONAL_OR_KEYWORD parameters are reordered (params with defaults\n    after those without). KEYWORD_ONLY parameters keep their position since Python\n    allows them to have defaults in any order.\n\n    Args:\n        fn: Function to transform\n\n    Returns:\n        Function with modified signature (same function object, updated __signature__)\n    \"\"\"\n    from fastmcp.server.context import Context\n\n    # Get the function's signature\n    try:\n        sig = inspect.signature(fn)\n    except (ValueError, TypeError):\n        return fn\n\n    # Get type hints for accurate type checking\n    try:\n        type_hints = get_type_hints(fn, include_extras=True)\n    except Exception:\n        type_hints = getattr(fn, \"__annotations__\", {})\n\n    # First pass: identify which params need transformation\n    params_to_transform: set[str] = set()\n    optional_context_params: set[str] = set()\n    for name, param in sig.parameters.items():\n        annotation = type_hints.get(name, param.annotation)\n        if is_class_member_of_type(annotation, Context):\n            if not isinstance(param.default, Dependency):\n                params_to_transform.add(name)\n                if param.default is None:\n                    optional_context_params.add(name)\n\n    if not params_to_transform:\n        return fn\n\n    # Second pass: build new param list preserving parameter kind structure\n    # Python signature structure: [POSITIONAL_ONLY] / [POSITIONAL_OR_KEYWORD] *args [KEYWORD_ONLY] **kwargs\n    # Within POSITIONAL_ONLY and POSITIONAL_OR_KEYWORD: params without defaults must come first\n    # KEYWORD_ONLY params can have defaults in any order\n    P = inspect.Parameter\n\n    # Group params by section, preserving order within each\n    positional_only_no_default: list[P] = []\n    positional_only_with_default: list[P] = []\n    positional_or_keyword_no_default: list[P] = []\n    positional_or_keyword_with_default: list[P] = []\n    var_positional: list[P] = []  # *args (at most one)\n    keyword_only: list[P] = []  # After * or *args, order preserved\n    var_keyword: list[P] = []  # **kwargs (at most one)\n\n    for name, param in sig.parameters.items():\n        # Transform Context params by adding CurrentContext default\n        if name in params_to_transform:\n            # We use CurrentContext() instead of Depends(get_context) because\n            # get_context() returns the Context which is an AsyncContextManager,\n            # and the DI system would try to enter it again (it's already entered)\n            if name in optional_context_params:\n                param = param.replace(default=OptionalCurrentContext())\n            else:\n                param = param.replace(default=CurrentContext())\n\n        # Sort into buckets based on parameter kind\n        if param.kind == P.POSITIONAL_ONLY:\n            if param.default is P.empty:\n                positional_only_no_default.append(param)\n            else:\n                positional_only_with_default.append(param)\n        elif param.kind == P.POSITIONAL_OR_KEYWORD:\n            if param.default is P.empty:\n                positional_or_keyword_no_default.append(param)\n            else:\n                positional_or_keyword_with_default.append(param)\n        elif param.kind == P.VAR_POSITIONAL:\n            var_positional.append(param)\n        elif param.kind == P.KEYWORD_ONLY:\n            keyword_only.append(param)\n        elif param.kind == P.VAR_KEYWORD:\n            var_keyword.append(param)\n\n    # Reconstruct parameter list maintaining Python's required structure\n    new_params: list[P] = (\n        positional_only_no_default\n        + positional_only_with_default\n        + positional_or_keyword_no_default\n        + positional_or_keyword_with_default\n        + var_positional\n        + keyword_only\n        + var_keyword\n    )\n\n    # Update function's signature in place\n    # Handle methods by setting signature on the underlying function\n    # For bound methods, we need to preserve the 'self' parameter because\n    # inspect.signature(bound_method) automatically removes the first param\n    if inspect.ismethod(fn):\n        # Get the original __func__ signature which includes 'self'\n        func_sig = inspect.signature(fn.__func__)\n        # Insert 'self' at the beginning of our new params\n        self_param = next(iter(func_sig.parameters.values()))  # Should be 'self'\n        new_sig = func_sig.replace(parameters=[self_param, *new_params])\n        fn.__func__.__signature__ = new_sig  # type: ignore[union-attr]\n    else:\n        new_sig = sig.replace(parameters=new_params)\n        fn.__signature__ = new_sig  # type: ignore[attr-defined]\n\n    # Clear caches that may have cached the old signature\n    # This ensures get_dependency_parameters and without_injected_parameters\n    # see the transformed signature\n    _clear_signature_caches(fn)\n\n    return fn\n\n\ndef _clear_signature_caches(fn: Callable[..., Any]) -> None:\n    \"\"\"Clear signature-related caches for a function.\n\n    Called after modifying a function's signature to ensure downstream\n    code sees the updated signature.\n    \"\"\"\n    from uncalled_for.introspection import _parameter_cache, _signature_cache\n\n    _signature_cache.pop(fn, None)\n    _parameter_cache.pop(fn, None)\n\n    if inspect.ismethod(fn):\n        _signature_cache.pop(fn.__func__, None)\n        _parameter_cache.pop(fn.__func__, None)\n\n\ndef get_context() -> Context:\n    \"\"\"Get the current FastMCP Context instance directly.\"\"\"\n    from fastmcp.server.context import _current_context\n\n    context = _current_context.get()\n    if context is None:\n        raise RuntimeError(\"No active context found.\")\n    return context\n\n\ndef get_server() -> FastMCP:\n    \"\"\"Get the current FastMCP server instance directly.\n\n    Returns:\n        The active FastMCP server\n\n    Raises:\n        RuntimeError: If no server in context\n    \"\"\"\n    server_ref = _current_server.get()\n    if server_ref is None:\n        raise RuntimeError(\"No FastMCP server instance in context\")\n    server = server_ref()\n    if server is None:\n        raise RuntimeError(\"FastMCP server instance is no longer available\")\n    return server\n\n\ndef get_http_request() -> Request:\n    \"\"\"Get the current HTTP request.\n\n    Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.\n    \"\"\"\n    # Try MCP SDK's request_ctx first (set during normal MCP request handling)\n    request = None\n    with contextlib.suppress(LookupError):\n        request = request_ctx.get().request\n\n    # Fallback to FastMCP's HTTP context variable\n    # This is needed during `on_initialize` middleware where request_ctx isn't set yet\n    if request is None:\n        request = _current_http_request.get()\n\n    if request is None:\n        raise RuntimeError(\"No active HTTP request found.\")\n    return request\n\n\ndef get_http_headers(\n    include_all: bool = False,\n    include: set[str] | None = None,\n) -> dict[str, str]:\n    \"\"\"Extract headers from the current HTTP request if available.\n\n    Never raises an exception, even if there is no active HTTP request (in which case\n    an empty dict is returned).\n\n    By default, strips problematic headers like `content-length` and `authorization`\n    that cause issues if forwarded to downstream services. If `include_all` is True,\n    all headers are returned.\n\n    The `include` parameter allows specific headers to be included even if they would\n    normally be excluded. This is useful for proxy transports that need to forward\n    authorization headers to upstream MCP servers.\n    \"\"\"\n    if include_all:\n        exclude_headers: set[str] = set()\n    else:\n        exclude_headers = {\n            \"host\",\n            \"content-length\",\n            \"content-type\",\n            \"connection\",\n            \"transfer-encoding\",\n            \"upgrade\",\n            \"te\",\n            \"keep-alive\",\n            \"expect\",\n            \"accept\",\n            \"authorization\",\n            # Proxy-related headers\n            \"proxy-authenticate\",\n            \"proxy-authorization\",\n            \"proxy-connection\",\n            # MCP-related headers\n            \"mcp-session-id\",\n        }\n        if include:\n            exclude_headers -= {h.lower() for h in include}\n        # (just in case)\n        if not all(h.lower() == h for h in exclude_headers):\n            raise ValueError(\"Excluded headers must be lowercase\")\n    headers: dict[str, str] = {}\n\n    try:\n        request = get_http_request()\n        for name, value in request.headers.items():\n            lower_name = name.lower()\n            if lower_name not in exclude_headers:\n                headers[lower_name] = str(value)\n        return headers\n    except RuntimeError:\n        return {}\n\n\ndef get_access_token() -> AccessToken | None:\n    \"\"\"Get the FastMCP access token from the current context.\n\n    This function first tries to get the token from the current HTTP request's scope,\n    which is more reliable for long-lived connections where the SDK's auth_context_var\n    may become stale after token refresh. Falls back to the SDK's context var if no\n    request is available. In background tasks (Docket workers), falls back to the\n    token snapshot stored in Redis at task submission time.\n\n    Returns:\n        The access token if an authenticated user is available, None otherwise.\n    \"\"\"\n    access_token: _SDKAccessToken | None = None\n\n    # First, try to get from current HTTP request's scope (issue #1863)\n    # This is more reliable than auth_context_var for Streamable HTTP sessions\n    # where tokens may be refreshed between MCP messages\n    try:\n        request = get_http_request()\n        user = request.scope.get(\"user\")\n        if isinstance(user, AuthenticatedUser):\n            access_token = user.access_token\n    except RuntimeError:\n        # No HTTP request available, fall back to context var\n        pass\n\n    # Fall back to SDK's context var if we didn't get a token from the request\n    if access_token is None:\n        access_token = _sdk_get_access_token()\n\n    # Fall back to background task snapshot (#3095)\n    # In Docket workers, neither HTTP request nor SDK context var are available.\n    # The token was snapshotted in Redis at submit_to_docket() time and restored\n    # into this ContextVar by _CurrentContext.__aenter__().\n    if access_token is None:\n        task_token = _task_access_token.get()\n        if task_token is not None:\n            # Check expiration: if expires_at is set and past, treat as expired\n            if task_token.expires_at is not None:\n                if task_token.expires_at < int(datetime.now(timezone.utc).timestamp()):\n                    return None\n            return task_token\n\n    if access_token is None or isinstance(access_token, AccessToken):\n        return access_token\n\n    # If the object is not a FastMCP AccessToken, convert it to one if the\n    # fields are compatible (e.g. `claims` is not present in the SDK's AccessToken).\n    # This is a workaround for the case where the SDK or auth provider returns a different type\n    # If it fails, it will raise a TypeError\n    try:\n        access_token_as_dict = access_token.model_dump()\n        return AccessToken(\n            token=access_token_as_dict[\"token\"],\n            client_id=access_token_as_dict[\"client_id\"],\n            scopes=access_token_as_dict[\"scopes\"],\n            # Optional fields\n            expires_at=access_token_as_dict.get(\"expires_at\"),\n            resource=access_token_as_dict.get(\"resource\"),\n            claims=access_token_as_dict.get(\"claims\") or {},\n        )\n    except Exception as e:\n        raise TypeError(\n            f\"Expected fastmcp.server.auth.auth.AccessToken, got {type(access_token).__name__}. \"\n            \"Ensure the SDK is using the correct AccessToken type.\"\n        ) from e\n\n\n# --- Schema generation helper ---\n\n\n@lru_cache(maxsize=5000)\ndef without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:\n    \"\"\"Create a wrapper function without injected parameters.\n\n    Returns a wrapper that excludes Context and Docket dependency parameters,\n    making it safe to use with Pydantic TypeAdapter for schema generation and\n    validation. The wrapper internally handles all dependency resolution and\n    Context injection when called.\n\n    Handles:\n    - Legacy Context injection (always works)\n    - Depends() injection (always works - uses docket or vendored DI engine)\n\n    Args:\n        fn: Original function with Context and/or dependencies\n\n    Returns:\n        Async wrapper function without injected parameters\n    \"\"\"\n    from fastmcp.server.context import Context\n\n    # Identify parameters to exclude\n    context_kwarg = find_kwarg_by_type(fn, Context)\n    dependency_params = get_dependency_parameters(fn)\n\n    exclude = set()\n    if context_kwarg:\n        exclude.add(context_kwarg)\n    if dependency_params:\n        exclude.update(dependency_params.keys())\n\n    if not exclude:\n        return fn\n\n    # Build new signature with only user parameters\n    sig = inspect.signature(fn)\n    user_params = [\n        param for name, param in sig.parameters.items() if name not in exclude\n    ]\n    new_sig = inspect.Signature(user_params)\n\n    # Create async wrapper that handles dependency resolution\n    fn_is_async = is_coroutine_function(fn)\n\n    async def wrapper(**user_kwargs: Any) -> Any:\n        async with resolve_dependencies(fn, user_kwargs) as resolved_kwargs:\n            if fn_is_async:\n                return await fn(**resolved_kwargs)\n            else:\n                # Run sync functions in threadpool to avoid blocking the event loop\n                result = await call_sync_fn_in_threadpool(fn, **resolved_kwargs)\n                # Handle sync wrappers that return awaitables (e.g., partial(async_fn))\n                if inspect.isawaitable(result):\n                    result = await result\n                return result\n\n    # Resolve string annotations (from `from __future__ import annotations`) using\n    # the original function's module context. The wrapper's __globals__ points to\n    # this module (dependencies.py) and is read-only, so some Pydantic versions\n    # can't resolve names like Annotated or Literal from string annotations.\n    try:\n        resolved_hints = get_type_hints(fn, include_extras=True)\n    except Exception:\n        resolved_hints = getattr(fn, \"__annotations__\", {})\n\n    wrapper.__signature__ = new_sig  # type: ignore[attr-defined]\n    wrapper.__annotations__ = {\n        k: v for k, v in resolved_hints.items() if k not in exclude and k != \"return\"\n    }\n    wrapper.__name__ = getattr(fn, \"__name__\", \"wrapper\")\n    wrapper.__doc__ = getattr(fn, \"__doc__\", None)\n    wrapper.__module__ = fn.__module__\n    wrapper.__qualname__ = getattr(fn, \"__qualname__\", wrapper.__qualname__)\n\n    return wrapper\n\n\n# --- Dependency resolution ---\n\n\n@asynccontextmanager\nasync def _resolve_fastmcp_dependencies(\n    fn: Callable[..., Any], arguments: dict[str, Any]\n) -> AsyncGenerator[dict[str, Any], None]:\n    \"\"\"Resolve Docket dependencies for a FastMCP function.\n\n    Sets up the minimal context needed for Docket's Depends() to work:\n    - A cache for resolved dependencies\n    - An AsyncExitStack for managing context manager lifetimes\n\n    The Docket instance (for CurrentDocket dependency) is managed separately\n    by the server's lifespan and made available via ContextVar.\n\n    Note: This does NOT set up Docket's Execution context. If user code needs\n    Docket-specific dependencies like TaskArgument(), TaskKey(), etc., those\n    will fail with clear errors about missing context.\n\n    Args:\n        fn: The function to resolve dependencies for\n        arguments: The arguments passed to the function\n\n    Yields:\n        Dictionary of resolved dependencies merged with provided arguments\n    \"\"\"\n    dependency_params = get_dependency_parameters(fn)\n\n    if not dependency_params:\n        yield arguments\n        return\n\n    # Initialize dependency cache and exit stack\n    cache_token = _Depends.cache.set({})\n    try:\n        async with AsyncExitStack() as stack:\n            stack_token = _Depends.stack.set(stack)\n            try:\n                resolved: dict[str, Any] = {}\n\n                for parameter, dependency in dependency_params.items():\n                    # If argument was explicitly provided, use that instead\n                    if parameter in arguments:\n                        resolved[parameter] = arguments[parameter]\n                        continue\n\n                    # Resolve the dependency\n                    try:\n                        resolved[parameter] = await stack.enter_async_context(\n                            dependency\n                        )\n                    except FastMCPError:\n                        # Let FastMCPError subclasses (ToolError, ResourceError, etc.)\n                        # propagate unchanged so they can be handled appropriately\n                        raise\n                    except Exception as error:\n                        fn_name = getattr(fn, \"__name__\", repr(fn))\n                        raise RuntimeError(\n                            f\"Failed to resolve dependency '{parameter}' for {fn_name}\"\n                        ) from error\n\n                # Merge resolved dependencies with provided arguments\n                final_arguments = {**arguments, **resolved}\n\n                yield final_arguments\n            finally:\n                _Depends.stack.reset(stack_token)\n    finally:\n        _Depends.cache.reset(cache_token)\n\n\n@asynccontextmanager\nasync def resolve_dependencies(\n    fn: Callable[..., Any], arguments: dict[str, Any]\n) -> AsyncGenerator[dict[str, Any], None]:\n    \"\"\"Resolve dependencies for a FastMCP function.\n\n    This function:\n    1. Filters out any dependency parameter names from user arguments (security)\n    2. Resolves Depends() parameters via the DI system\n\n    The filtering prevents external callers from overriding injected parameters by\n    providing values for dependency parameter names. This is a security feature.\n\n    Note: Context injection is handled via transform_context_annotations() which\n    converts `ctx: Context` to `ctx: Context = Depends(get_context)` at registration\n    time, so all injection goes through the unified DI system.\n\n    Args:\n        fn: The function to resolve dependencies for\n        arguments: User arguments (may contain keys that match dependency names,\n                  which will be filtered out)\n\n    Yields:\n        Dictionary of filtered user args + resolved dependencies\n\n    Example:\n        ```python\n        async with resolve_dependencies(my_tool, {\"name\": \"Alice\"}) as kwargs:\n            result = my_tool(**kwargs)\n            if inspect.isawaitable(result):\n                result = await result\n        ```\n    \"\"\"\n    # Filter out dependency parameters from user arguments to prevent override\n    # This is a security measure - external callers should never be able to\n    # provide values for injected parameters\n    dependency_params = get_dependency_parameters(fn)\n    user_args = {k: v for k, v in arguments.items() if k not in dependency_params}\n\n    async with _resolve_fastmcp_dependencies(fn, user_args) as resolved_kwargs:\n        yield resolved_kwargs\n\n\n# --- Dependency classes ---\n# These must inherit from docket.dependencies.Dependency when docket is available\n# so that get_dependency_parameters can detect them.\n\n\nasync def _restore_task_access_token(\n    session_id: str, task_id: str\n) -> Token[AccessToken | None] | None:\n    \"\"\"Restore the access token snapshot from Redis into a ContextVar.\n\n    Called when setting up context in a Docket worker. The token was stored at\n    submit_to_docket() time. The token is restored regardless of expiration;\n    get_access_token() checks expiry when reading from the ContextVar.\n\n    Returns:\n        The ContextVar token for resetting, or None if nothing was restored.\n    \"\"\"\n    docket = _current_docket.get()\n    if docket is None:\n        return None\n\n    token_key = docket.key(f\"fastmcp:task:{session_id}:{task_id}:access_token\")\n    try:\n        async with docket.redis() as redis:\n            token_data = await redis.get(token_key)\n        if token_data is not None:\n            restored = AccessToken.model_validate_json(token_data)\n            return _task_access_token.set(restored)\n    except Exception:\n        _logger.warning(\n            \"Failed to restore access token for task %s:%s\",\n            session_id,\n            task_id,\n            exc_info=True,\n        )\n    return None\n\n\nasync def _restore_task_origin_request_id(session_id: str, task_id: str) -> str | None:\n    \"\"\"Restore the origin request ID snapshot for a background task.\n\n    Returns None if no request ID was captured at submission time.\n    \"\"\"\n    docket = _current_docket.get()\n    if docket is None:\n        return None\n\n    request_id_key = docket.key(\n        f\"fastmcp:task:{session_id}:{task_id}:origin_request_id\"\n    )\n    try:\n        async with docket.redis() as redis:\n            request_id_data = await redis.get(request_id_key)\n        if request_id_data is None:\n            return None\n        if isinstance(request_id_data, bytes):\n            return request_id_data.decode()\n        return str(request_id_data)\n    except Exception:\n        _logger.warning(\n            \"Failed to restore origin request ID for task %s:%s\",\n            session_id,\n            task_id,\n            exc_info=True,\n        )\n        return None\n\n\nclass _CurrentContext(Dependency[\"Context\"]):\n    \"\"\"Async context manager for Context dependency.\n\n    In foreground (request) mode: returns the active context from _current_context.\n    In background (Docket worker) mode: creates a task-aware Context with task_id\n    and restores the access token snapshot from Redis.\n    \"\"\"\n\n    _context: Context | None = None\n    _access_token_cv_token: Token[AccessToken | None] | None = None\n\n    async def __aenter__(self) -> Context:\n        from fastmcp.server.context import Context, _current_context\n\n        # Try foreground context first (normal MCP request)\n        context = _current_context.get()\n        if context is not None:\n            return context\n\n        # Check if we're in a Docket worker context\n        task_info = get_task_context()\n        if task_info is not None:\n            # Get session from registry (registered when task was submitted)\n            session = get_task_session(task_info.session_id)\n            # Get server from ContextVar\n            server = get_server()\n            origin_request_id = await _restore_task_origin_request_id(\n                task_info.session_id, task_info.task_id\n            )\n            # Create task-aware Context\n            self._context = Context(\n                fastmcp=server,\n                session=session,\n                task_id=task_info.task_id,\n                origin_request_id=origin_request_id,\n            )\n            # Enter the context to set up ContextVars\n            await self._context.__aenter__()\n\n            # Restore access token snapshot from Redis (#3095)\n            self._access_token_cv_token = await _restore_task_access_token(\n                task_info.session_id, task_info.task_id\n            )\n\n            return self._context\n\n        # Neither foreground nor background context available\n        raise RuntimeError(\n            \"No active context found. This can happen if:\\n\"\n            \"  - Called outside an MCP request handler\\n\"\n            \"  - Called in a background task before session was registered\\n\"\n            \"Check `context.request_context` for None before accessing.\"\n        )\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        # Clean up access token ContextVar\n        if self._access_token_cv_token is not None:\n            _task_access_token.reset(self._access_token_cv_token)\n            self._access_token_cv_token = None\n        # Clean up if we created a context for background task\n        if self._context is not None:\n            await self._context.__aexit__(exc_type, exc_value, traceback)\n            self._context = None\n\n\nclass _OptionalCurrentContext(Dependency[\"Context | None\"]):\n    \"\"\"Context dependency that degrades to None when no context is active.\n\n    This is implemented as a wrapper (composition), not a subclass of\n    `_CurrentContext`, to avoid overriding `__aenter__` with an incompatible\n    return type.\n    \"\"\"\n\n    _inner: _CurrentContext | None = None\n\n    async def __aenter__(self) -> Context | None:\n        inner = _CurrentContext()\n        try:\n            context = await inner.__aenter__()\n        except RuntimeError as exc:\n            if \"No active context found\" in str(exc):\n                return None\n            raise\n        self._inner = inner\n        return context\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        if self._inner is None:\n            return\n        await self._inner.__aexit__(exc_type, exc_value, traceback)\n        self._inner = None\n\n\ndef CurrentContext() -> Context:\n    \"\"\"Get the current FastMCP Context instance.\n\n    This dependency provides access to the active FastMCP Context for the\n    current MCP operation (tool/resource/prompt call).\n\n    Returns:\n        A dependency that resolves to the active Context instance\n\n    Raises:\n        RuntimeError: If no active context found (during resolution)\n\n    Example:\n        ```python\n        from fastmcp.dependencies import CurrentContext\n\n        @mcp.tool()\n        async def log_progress(ctx: Context = CurrentContext()) -> str:\n            ctx.report_progress(50, 100, \"Halfway done\")\n            return \"Working\"\n        ```\n    \"\"\"\n    return cast(\"Context\", _CurrentContext())\n\n\ndef OptionalCurrentContext() -> Context | None:\n    \"\"\"Get the current FastMCP Context, or None when no context is active.\"\"\"\n    return cast(\"Context | None\", _OptionalCurrentContext())\n\n\nclass _CurrentDocket(Dependency[\"Docket\"]):\n    \"\"\"Async context manager for Docket dependency.\"\"\"\n\n    async def __aenter__(self) -> Docket:\n        require_docket(\"CurrentDocket()\")\n        docket = _current_docket.get()\n        if docket is None:\n            raise RuntimeError(\n                \"No Docket instance found. Docket is only initialized when there are \"\n                \"task-enabled components (task=True). Add task=True to a component \"\n                \"to enable Docket infrastructure.\"\n            )\n        return docket\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n\ndef CurrentDocket() -> Docket:\n    \"\"\"Get the current Docket instance managed by FastMCP.\n\n    This dependency provides access to the Docket instance that FastMCP\n    automatically creates for background task scheduling.\n\n    Returns:\n        A dependency that resolves to the active Docket instance\n\n    Raises:\n        RuntimeError: If not within a FastMCP server context\n        ImportError: If fastmcp[tasks] not installed\n\n    Example:\n        ```python\n        from fastmcp.dependencies import CurrentDocket\n\n        @mcp.tool()\n        async def schedule_task(docket: Docket = CurrentDocket()) -> str:\n            await docket.add(some_function)(arg1, arg2)\n            return \"Scheduled\"\n        ```\n    \"\"\"\n    require_docket(\"CurrentDocket()\")\n    return cast(\"Docket\", _CurrentDocket())\n\n\nclass _CurrentWorker(Dependency[\"Worker\"]):\n    \"\"\"Async context manager for Worker dependency.\"\"\"\n\n    async def __aenter__(self) -> Worker:\n        require_docket(\"CurrentWorker()\")\n        worker = _current_worker.get()\n        if worker is None:\n            raise RuntimeError(\n                \"No Worker instance found. Worker is only initialized when there are \"\n                \"task-enabled components (task=True). Add task=True to a component \"\n                \"to enable Docket infrastructure.\"\n            )\n        return worker\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n\ndef CurrentWorker() -> Worker:\n    \"\"\"Get the current Docket Worker instance managed by FastMCP.\n\n    This dependency provides access to the Worker instance that FastMCP\n    automatically creates for background task processing.\n\n    Returns:\n        A dependency that resolves to the active Worker instance\n\n    Raises:\n        RuntimeError: If not within a FastMCP server context\n        ImportError: If fastmcp[tasks] not installed\n\n    Example:\n        ```python\n        from fastmcp.dependencies import CurrentWorker\n\n        @mcp.tool()\n        async def check_worker_status(worker: Worker = CurrentWorker()) -> str:\n            return f\"Worker: {worker.name}\"\n        ```\n    \"\"\"\n    require_docket(\"CurrentWorker()\")\n    return cast(\"Worker\", _CurrentWorker())\n\n\nclass _CurrentFastMCP(Dependency[\"FastMCP\"]):\n    \"\"\"Async context manager for FastMCP server dependency.\"\"\"\n\n    async def __aenter__(self) -> FastMCP:\n        server_ref = _current_server.get()\n        if server_ref is None:\n            raise RuntimeError(\"No FastMCP server instance in context\")\n        server = server_ref()\n        if server is None:\n            raise RuntimeError(\"FastMCP server instance is no longer available\")\n        return server\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n\ndef CurrentFastMCP() -> FastMCP:\n    \"\"\"Get the current FastMCP server instance.\n\n    This dependency provides access to the active FastMCP server.\n\n    Returns:\n        A dependency that resolves to the active FastMCP server\n\n    Raises:\n        RuntimeError: If no server in context (during resolution)\n\n    Example:\n        ```python\n        from fastmcp.dependencies import CurrentFastMCP\n\n        @mcp.tool()\n        async def introspect(server: FastMCP = CurrentFastMCP()) -> str:\n            return f\"Server: {server.name}\"\n        ```\n    \"\"\"\n    from fastmcp.server.server import FastMCP\n\n    return cast(FastMCP, _CurrentFastMCP())\n\n\nclass _CurrentRequest(Dependency[Request]):\n    \"\"\"Async context manager for HTTP Request dependency.\"\"\"\n\n    async def __aenter__(self) -> Request:\n        return get_http_request()\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n\ndef CurrentRequest() -> Request:\n    \"\"\"Get the current HTTP request.\n\n    This dependency provides access to the Starlette Request object for the\n    current HTTP request. Only available when running over HTTP transports\n    (SSE or Streamable HTTP).\n\n    Returns:\n        A dependency that resolves to the active Starlette Request\n\n    Raises:\n        RuntimeError: If no HTTP request in context (e.g., STDIO transport)\n\n    Example:\n        ```python\n        from fastmcp.server.dependencies import CurrentRequest\n        from starlette.requests import Request\n\n        @mcp.tool()\n        async def get_client_ip(request: Request = CurrentRequest()) -> str:\n            return request.client.host if request.client else \"Unknown\"\n        ```\n    \"\"\"\n    return cast(Request, _CurrentRequest())\n\n\nclass _CurrentHeaders(Dependency[dict[str, str]]):\n    \"\"\"Async context manager for HTTP Headers dependency.\"\"\"\n\n    async def __aenter__(self) -> dict[str, str]:\n        return get_http_headers(include={\"authorization\"})\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n\ndef CurrentHeaders() -> dict[str, str]:\n    \"\"\"Get the current HTTP request headers.\n\n    This dependency provides access to the HTTP headers for the current request,\n    including the authorization header. Returns an empty dictionary when no HTTP\n    request is available, making it safe to use in code that might run over any\n    transport.\n\n    Returns:\n        A dependency that resolves to a dictionary of header name -> value\n\n    Example:\n        ```python\n        from fastmcp.server.dependencies import CurrentHeaders\n\n        @mcp.tool()\n        async def get_auth_type(headers: dict = CurrentHeaders()) -> str:\n            auth = headers.get(\"authorization\", \"\")\n            return \"Bearer\" if auth.startswith(\"Bearer \") else \"None\"\n        ```\n    \"\"\"\n    return cast(dict[str, str], _CurrentHeaders())\n\n\n# --- Progress dependency ---\n\n\n@runtime_checkable\nclass ProgressLike(Protocol):\n    \"\"\"Protocol for progress tracking interface.\n\n    Defines the common interface between InMemoryProgress (server context)\n    and Docket's Progress (worker context).\n    \"\"\"\n\n    @property\n    def current(self) -> int | None:\n        \"\"\"Current progress value.\"\"\"\n        ...\n\n    @property\n    def total(self) -> int:\n        \"\"\"Total/target progress value.\"\"\"\n        ...\n\n    @property\n    def message(self) -> str | None:\n        \"\"\"Current progress message.\"\"\"\n        ...\n\n    async def set_total(self, total: int) -> None:\n        \"\"\"Set the total/target value for progress tracking.\"\"\"\n        ...\n\n    async def increment(self, amount: int = 1) -> None:\n        \"\"\"Atomically increment the current progress value.\"\"\"\n        ...\n\n    async def set_message(self, message: str | None) -> None:\n        \"\"\"Update the progress status message.\"\"\"\n        ...\n\n\nclass InMemoryProgress:\n    \"\"\"In-memory progress tracker for immediate tool execution.\n\n    Provides the same interface as Docket's Progress but stores state in memory\n    instead of Redis. Useful for testing and immediate execution where\n    progress doesn't need to be observable across processes.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._current: int | None = None\n        self._total: int = 1\n        self._message: str | None = None\n\n    async def __aenter__(self) -> InMemoryProgress:\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n    @property\n    def current(self) -> int | None:\n        return self._current\n\n    @property\n    def total(self) -> int:\n        return self._total\n\n    @property\n    def message(self) -> str | None:\n        return self._message\n\n    async def set_total(self, total: int) -> None:\n        \"\"\"Set the total/target value for progress tracking.\"\"\"\n        if total < 1:\n            raise ValueError(\"Total must be at least 1\")\n        self._total = total\n\n    async def increment(self, amount: int = 1) -> None:\n        \"\"\"Atomically increment the current progress value.\"\"\"\n        if amount < 1:\n            raise ValueError(\"Amount must be at least 1\")\n        if self._current is None:\n            self._current = amount\n        else:\n            self._current += amount\n\n    async def set_message(self, message: str | None) -> None:\n        \"\"\"Update the progress status message.\"\"\"\n        self._message = message\n\n\nclass Progress(Dependency[\"Progress\"]):\n    \"\"\"FastMCP Progress dependency that works in both server and worker contexts.\n\n    Handles three execution modes:\n    - In Docket worker: Uses the execution's progress (observable via Redis)\n    - In FastMCP server with Docket: Falls back to in-memory progress\n    - In FastMCP server without Docket: Uses in-memory progress\n\n    This allows tools to use Progress() regardless of whether they're called\n    immediately or as background tasks, and regardless of whether pydocket\n    is installed.\n    \"\"\"\n\n    _impl: ProgressLike | None = None\n\n    async def __aenter__(self) -> Progress:\n        server_ref = _current_server.get()\n        if server_ref is None or server_ref() is None:\n            raise RuntimeError(\"Progress dependency requires a FastMCP server context.\")\n\n        if is_docket_available():\n            from docket.dependencies import Progress as DocketProgress\n\n            try:\n                docket_progress = DocketProgress()\n                self._impl = await docket_progress.__aenter__()\n                return self\n            except LookupError:\n                pass\n\n        self._impl = InMemoryProgress()\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        self._impl = None\n\n    @property\n    def current(self) -> int | None:\n        \"\"\"Current progress value.\"\"\"\n        assert self._impl is not None, \"Progress must be used as a dependency\"\n        return self._impl.current\n\n    @property\n    def total(self) -> int:\n        \"\"\"Total/target progress value.\"\"\"\n        assert self._impl is not None, \"Progress must be used as a dependency\"\n        return self._impl.total\n\n    @property\n    def message(self) -> str | None:\n        \"\"\"Current progress message.\"\"\"\n        assert self._impl is not None, \"Progress must be used as a dependency\"\n        return self._impl.message\n\n    async def set_total(self, total: int) -> None:\n        \"\"\"Set the total/target value for progress tracking.\"\"\"\n        assert self._impl is not None, \"Progress must be used as a dependency\"\n        await self._impl.set_total(total)\n\n    async def increment(self, amount: int = 1) -> None:\n        \"\"\"Atomically increment the current progress value.\"\"\"\n        assert self._impl is not None, \"Progress must be used as a dependency\"\n        await self._impl.increment(amount)\n\n    async def set_message(self, message: str | None) -> None:\n        \"\"\"Update the progress status message.\"\"\"\n        assert self._impl is not None, \"Progress must be used as a dependency\"\n        await self._impl.set_message(message)\n\n\n# --- Access Token dependency ---\n\n\nclass _CurrentAccessToken(Dependency[AccessToken]):\n    \"\"\"Async context manager for AccessToken dependency.\"\"\"\n\n    _access_token_cv_token: Token[AccessToken | None] | None = None\n\n    async def __aenter__(self) -> AccessToken:\n        token = get_access_token()\n\n        # If no token found and we're in a Docket worker, try restoring from\n        # Redis. This handles the case where ctx: Context is not in the\n        # function signature, so _CurrentContext never ran the restoration.\n        if token is None:\n            task_info = get_task_context()\n            if task_info is not None:\n                self._access_token_cv_token = await _restore_task_access_token(\n                    task_info.session_id, task_info.task_id\n                )\n                token = get_access_token()\n\n        if token is None:\n            raise RuntimeError(\n                \"No access token found. Ensure authentication is configured \"\n                \"and the request is authenticated.\"\n            )\n        return token\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        if self._access_token_cv_token is not None:\n            _task_access_token.reset(self._access_token_cv_token)\n            self._access_token_cv_token = None\n\n\ndef CurrentAccessToken() -> AccessToken:\n    \"\"\"Get the current access token for the authenticated user.\n\n    This dependency provides access to the AccessToken for the current\n    authenticated request. Raises an error if no authentication is present.\n\n    Returns:\n        A dependency that resolves to the active AccessToken\n\n    Raises:\n        RuntimeError: If no authenticated user (use get_access_token() for optional)\n\n    Example:\n        ```python\n        from fastmcp.server.dependencies import CurrentAccessToken\n        from fastmcp.server.auth import AccessToken\n\n        @mcp.tool()\n        async def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:\n            return token.claims.get(\"sub\", \"unknown\")\n        ```\n    \"\"\"\n    return cast(AccessToken, _CurrentAccessToken())\n\n\n# --- Token Claim dependency ---\n\n\nclass _TokenClaim(Dependency[str]):\n    \"\"\"Dependency that extracts a specific claim from the access token.\"\"\"\n\n    def __init__(self, claim_name: str):\n        self.claim_name = claim_name\n\n    async def __aenter__(self) -> str:\n        token = get_access_token()\n        if token is None:\n            raise RuntimeError(\n                f\"No access token available. Cannot extract claim '{self.claim_name}'.\"\n            )\n        value = token.claims.get(self.claim_name)\n        if value is None:\n            raise RuntimeError(\n                f\"Claim '{self.claim_name}' not found in access token. \"\n                f\"Available claims: {list(token.claims.keys())}\"\n            )\n        return str(value)\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        pass\n\n\ndef TokenClaim(name: str) -> str:\n    \"\"\"Get a specific claim from the access token.\n\n    This dependency extracts a single claim value from the current access token.\n    It's useful for getting user identifiers, roles, or other token claims\n    without needing the full token object.\n\n    Args:\n        name: The name of the claim to extract (e.g., \"oid\", \"sub\", \"email\")\n\n    Returns:\n        A dependency that resolves to the claim value as a string\n\n    Raises:\n        RuntimeError: If no access token is available or claim is missing\n\n    Example:\n        ```python\n        from fastmcp.server.dependencies import TokenClaim\n\n        @mcp.tool()\n        async def add_expense(\n            user_id: str = TokenClaim(\"oid\"),  # Azure object ID\n            amount: float,\n        ):\n            # user_id is automatically injected from the token\n            await db.insert({\"user_id\": user_id, \"amount\": amount})\n        ```\n    \"\"\"\n    return cast(str, _TokenClaim(name))\n"
  },
  {
    "path": "src/fastmcp/server/elicitation.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Any, Generic, Literal, cast, get_origin\n\nfrom mcp.server.elicitation import (\n    CancelledElicitation,\n    DeclinedElicitation,\n)\nfrom pydantic import BaseModel\nfrom pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue\nfrom pydantic_core import core_schema\nfrom typing_extensions import TypeVar\n\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import get_cached_typeadapter\n\n__all__ = [\n    \"AcceptedElicitation\",\n    \"CancelledElicitation\",\n    \"DeclinedElicitation\",\n    \"ElicitConfig\",\n    \"ScalarElicitationType\",\n    \"get_elicitation_schema\",\n    \"handle_elicit_accept\",\n    \"parse_elicit_response_type\",\n]\n\nlogger = get_logger(__name__)\n\nT = TypeVar(\"T\", default=Any)\n\n\nclass ElicitationJsonSchema(GenerateJsonSchema):\n    \"\"\"Custom JSON schema generator for MCP elicitation that always inlines enums.\n\n    MCP elicitation requires inline enum schemas without $ref/$defs references.\n    This generator ensures enums are always generated inline for compatibility.\n    Optionally adds enumNames for better UI display when available.\n    \"\"\"\n\n    def generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue:  # type: ignore[override]\n        \"\"\"Override to prevent ref generation for enums and handle list schemas.\"\"\"\n        # For enum schemas, bypass the ref mechanism entirely\n        if schema[\"type\"] == \"enum\":\n            # Directly call our custom enum_schema without going through handler\n            # This prevents the ref/defs mechanism from being invoked\n            return self.enum_schema(schema)\n        # For list schemas, check if items are enums\n        if schema[\"type\"] == \"list\":\n            return self.list_schema(schema)\n        # For all other types, use the default implementation\n        return super().generate_inner(schema)\n\n    def list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue:\n        \"\"\"Generate schema for list types, detecting enum items for multi-select.\"\"\"\n        items_schema = schema.get(\"items_schema\")\n\n        # Check if items are enum/Literal\n        if items_schema and items_schema.get(\"type\") == \"enum\":\n            # Generate array with enum items\n            items = self.enum_schema(items_schema)  # type: ignore[arg-type]\n            # If items have oneOf pattern, convert to anyOf for multi-select per SEP-1330\n            if \"oneOf\" in items:\n                items = {\"anyOf\": items[\"oneOf\"]}\n            return {\n                \"type\": \"array\",\n                \"items\": items,  # Will be {\"enum\": [...]} or {\"anyOf\": [...]}\n            }\n\n        # Check if items are Literal (which Pydantic represents differently)\n        if items_schema:\n            # Try to detect Literal patterns\n            items_result = super().generate_inner(items_schema)\n            # If it's a const pattern or enum-like, allow it\n            if (\n                \"const\" in items_result\n                or \"enum\" in items_result\n                or \"oneOf\" in items_result\n            ):\n                # Convert oneOf to anyOf for multi-select\n                if \"oneOf\" in items_result:\n                    items_result = {\"anyOf\": items_result[\"oneOf\"]}\n                return {\n                    \"type\": \"array\",\n                    \"items\": items_result,\n                }\n\n        # Default behavior for non-enum arrays\n        return super().list_schema(schema)\n\n    def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:\n        \"\"\"Generate inline enum schema.\n\n        Always generates enum pattern: `{\"enum\": [value, ...]}`\n        Titled enums are handled separately via dict-based syntax in ctx.elicit().\n        \"\"\"\n        # Get the base schema from parent - always use simple enum pattern\n        return super().enum_schema(schema)\n\n\n# we can't use the low-level AcceptedElicitation because it only works with BaseModels\nclass AcceptedElicitation(BaseModel, Generic[T]):\n    \"\"\"Result when user accepts the elicitation.\"\"\"\n\n    action: Literal[\"accept\"] = \"accept\"\n    data: T\n\n\n@dataclass\nclass ScalarElicitationType(Generic[T]):\n    value: T\n\n\n@dataclass\nclass ElicitConfig:\n    \"\"\"Configuration for an elicitation request.\n\n    Attributes:\n        schema: The JSON schema to send to the client\n        response_type: The type to validate responses with (None for raw schemas)\n        is_raw: True if schema was built directly (extract \"value\" from response)\n    \"\"\"\n\n    schema: dict[str, Any]\n    response_type: type | None\n    is_raw: bool\n\n\ndef parse_elicit_response_type(response_type: Any) -> ElicitConfig:\n    \"\"\"Parse response_type into schema and handling configuration.\n\n    Supports multiple syntaxes:\n    - None: Empty object schema, expect empty response\n    - dict: `{\"low\": {\"title\": \"...\"}}` -> single-select titled enum\n    - list patterns:\n        - `[[\"a\", \"b\"]]` -> multi-select untitled\n        - `[{\"low\": {...}}]` -> multi-select titled\n        - `[\"a\", \"b\"]` -> single-select untitled\n    - `list[X]` type annotation: multi-select with type\n    - Scalar types (bool, int, float, str, Literal, Enum): single value\n    - Other types (dataclass, BaseModel): use directly\n    \"\"\"\n    if response_type is None:\n        return ElicitConfig(\n            schema={\"type\": \"object\", \"properties\": {}},\n            response_type=None,\n            is_raw=False,\n        )\n\n    if isinstance(response_type, dict):\n        return _parse_dict_syntax(response_type)\n\n    if isinstance(response_type, list):\n        return _parse_list_syntax(response_type)\n\n    if get_origin(response_type) is list:\n        return _parse_generic_list(response_type)\n\n    if _is_scalar_type(response_type):\n        return _parse_scalar_type(response_type)\n\n    # Other types (dataclass, BaseModel, etc.) - use directly\n    return ElicitConfig(\n        schema=get_elicitation_schema(response_type),\n        response_type=response_type,\n        is_raw=False,\n    )\n\n\ndef _is_scalar_type(response_type: Any) -> bool:\n    \"\"\"Check if response_type is a scalar type that needs wrapping.\"\"\"\n    return (\n        response_type in {bool, int, float, str}\n        or get_origin(response_type) is Literal\n        or (isinstance(response_type, type) and issubclass(response_type, Enum))\n    )\n\n\ndef _parse_dict_syntax(d: dict[str, Any]) -> ElicitConfig:\n    \"\"\"Parse dict syntax: {\"low\": {\"title\": \"...\"}} -> single-select titled.\"\"\"\n    if not d:\n        raise ValueError(\"Dict response_type cannot be empty.\")\n    enum_schema = _dict_to_enum_schema(d, multi_select=False)\n    return ElicitConfig(\n        schema={\n            \"type\": \"object\",\n            \"properties\": {\"value\": enum_schema},\n            \"required\": [\"value\"],\n        },\n        response_type=None,\n        is_raw=True,\n    )\n\n\ndef _parse_list_syntax(lst: list[Any]) -> ElicitConfig:\n    \"\"\"Parse list patterns: [[...]], [{...}], or [...].\"\"\"\n    # [[\"a\", \"b\", \"c\"]] -> multi-select untitled\n    if (\n        len(lst) == 1\n        and isinstance(lst[0], list)\n        and lst[0]\n        and all(isinstance(item, str) for item in lst[0])\n    ):\n        return ElicitConfig(\n            schema={\n                \"type\": \"object\",\n                \"properties\": {\"value\": {\"type\": \"array\", \"items\": {\"enum\": lst[0]}}},\n                \"required\": [\"value\"],\n            },\n            response_type=None,\n            is_raw=True,\n        )\n\n    # [{\"low\": {\"title\": \"...\"}}] -> multi-select titled\n    if len(lst) == 1 and isinstance(lst[0], dict) and lst[0]:\n        enum_schema = _dict_to_enum_schema(lst[0], multi_select=True)\n        return ElicitConfig(\n            schema={\n                \"type\": \"object\",\n                \"properties\": {\"value\": {\"type\": \"array\", \"items\": enum_schema}},\n                \"required\": [\"value\"],\n            },\n            response_type=None,\n            is_raw=True,\n        )\n\n    # [\"a\", \"b\", \"c\"] -> single-select untitled\n    if lst and all(isinstance(item, str) for item in lst):\n        # Construct Literal type from tuple - use cast since we can't construct Literal dynamically\n        # but we know the values are all strings\n        choice_literal: type[Any] = cast(type[Any], Literal[tuple(lst)])  # type: ignore[valid-type]\n        wrapped = ScalarElicitationType[choice_literal]  # type: ignore[valid-type]\n        return ElicitConfig(\n            schema=get_elicitation_schema(wrapped),\n            response_type=wrapped,\n            is_raw=False,\n        )\n\n    raise ValueError(f\"Invalid list response_type format. Received: {lst}\")\n\n\ndef _parse_generic_list(response_type: Any) -> ElicitConfig:\n    \"\"\"Parse list[X] type annotation -> multi-select.\"\"\"\n    wrapped = ScalarElicitationType[response_type]\n    return ElicitConfig(\n        schema=get_elicitation_schema(wrapped),\n        response_type=wrapped,\n        is_raw=False,\n    )\n\n\ndef _parse_scalar_type(response_type: Any) -> ElicitConfig:\n    \"\"\"Parse scalar types (bool, int, float, str, Literal, Enum).\"\"\"\n    wrapped = ScalarElicitationType[response_type]\n    return ElicitConfig(\n        schema=get_elicitation_schema(wrapped),\n        response_type=wrapped,\n        is_raw=False,\n    )\n\n\ndef handle_elicit_accept(\n    config: ElicitConfig, content: Any\n) -> AcceptedElicitation[Any]:\n    \"\"\"Handle an accepted elicitation response.\n\n    Args:\n        config: The elicitation configuration from parse_elicit_response_type\n        content: The response content from the client\n\n    Returns:\n        AcceptedElicitation with the extracted/validated data\n    \"\"\"\n    # For raw schemas (dict/nested-list syntax), extract value directly\n    if config.is_raw:\n        if not isinstance(content, dict) or \"value\" not in content:\n            raise ValueError(\"Elicitation response missing required 'value' field.\")\n        return AcceptedElicitation[Any](data=content[\"value\"])\n\n    # For typed schemas, validate with Pydantic\n    if config.response_type is not None:\n        type_adapter = get_cached_typeadapter(config.response_type)\n        validated_data = type_adapter.validate_python(content)\n        if isinstance(validated_data, ScalarElicitationType):\n            return AcceptedElicitation[Any](data=validated_data.value)\n        return AcceptedElicitation[Any](data=validated_data)\n\n    # For None response_type, expect empty response\n    if content:\n        raise ValueError(\n            f\"Elicitation expected an empty response, but received: {content}\"\n        )\n    return AcceptedElicitation[dict[str, Any]](data={})\n\n\ndef _dict_to_enum_schema(\n    enum_dict: dict[str, dict[str, str]], multi_select: bool = False\n) -> dict[str, Any]:\n    \"\"\"Convert dict enum to SEP-1330 compliant schema pattern.\n\n    Args:\n        enum_dict: {\"low\": {\"title\": \"Low Priority\"}, \"medium\": {\"title\": \"Medium Priority\"}}\n        multi_select: If True, use anyOf pattern; if False, use oneOf pattern\n\n    Returns:\n        {\"type\": \"string\", \"oneOf\": [...]} for single-select\n        {\"anyOf\": [...]} for multi-select (used as array items)\n    \"\"\"\n    pattern_key = \"anyOf\" if multi_select else \"oneOf\"\n    pattern = []\n    for value, metadata in enum_dict.items():\n        title = metadata.get(\"title\", value)\n        pattern.append({\"const\": value, \"title\": title})\n\n    result: dict[str, Any] = {pattern_key: pattern}\n    if not multi_select:\n        result[\"type\"] = \"string\"\n    return result\n\n\ndef get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:\n    \"\"\"Get the schema for an elicitation response.\n\n    Args:\n        response_type: The type of the response\n    \"\"\"\n\n    # Use custom schema generator that inlines enums for MCP compatibility\n    schema = get_cached_typeadapter(response_type).json_schema(\n        schema_generator=ElicitationJsonSchema\n    )\n    schema = compress_schema(schema)\n\n    # Validate the schema to ensure it follows MCP elicitation requirements\n    validate_elicitation_json_schema(schema)\n\n    return schema\n\n\ndef validate_elicitation_json_schema(schema: dict[str, Any]) -> None:\n    \"\"\"Validate that a JSON schema follows MCP elicitation requirements.\n\n    This ensures the schema is compatible with MCP elicitation requirements:\n    - Must be an object schema\n    - Must only contain primitive field types (string, number, integer, boolean)\n    - Must be flat (no nested objects or arrays of objects)\n    - Allows const fields (for Literal types) and enum fields (for Enum types)\n    - Only primitive types and their nullable variants are allowed\n\n    Args:\n        schema: The JSON schema to validate\n\n    Raises:\n        TypeError: If the schema doesn't meet MCP elicitation requirements\n    \"\"\"\n    ALLOWED_TYPES = {\"string\", \"number\", \"integer\", \"boolean\"}\n\n    # Check that the schema is an object\n    if schema.get(\"type\") != \"object\":\n        raise TypeError(\n            f\"Elicitation schema must be an object schema, got type '{schema.get('type')}'. \"\n            \"Elicitation schemas are limited to flat objects with primitive properties only.\"\n        )\n\n    properties = schema.get(\"properties\", {})\n\n    for prop_name, prop_schema in properties.items():\n        prop_type = prop_schema.get(\"type\")\n\n        # Handle nullable types\n        if isinstance(prop_type, list):\n            if \"null\" in prop_type:\n                prop_type = [t for t in prop_type if t != \"null\"]\n                if len(prop_type) == 1:\n                    prop_type = prop_type[0]\n        elif prop_schema.get(\"nullable\", False):\n            continue  # Nullable with no other type is fine\n\n        # Handle const fields (Literal types)\n        if \"const\" in prop_schema:\n            continue  # const fields are allowed regardless of type\n\n        # Handle enum fields (Enum types)\n        if \"enum\" in prop_schema:\n            continue  # enum fields are allowed regardless of type\n\n        # Handle references to definitions (like Enum types)\n        if \"$ref\" in prop_schema:\n            # Get the referenced definition\n            ref_path = prop_schema[\"$ref\"]\n            if ref_path.startswith(\"#/$defs/\"):\n                def_name = ref_path[8:]  # Remove \"#/$defs/\" prefix\n                ref_def = schema.get(\"$defs\", {}).get(def_name, {})\n                # If the referenced definition has an enum, it's allowed\n                if \"enum\" in ref_def:\n                    continue\n                # If the referenced definition has a type that's allowed, it's allowed\n                ref_type = ref_def.get(\"type\")\n                if ref_type in ALLOWED_TYPES:\n                    continue\n            # If we can't determine what the ref points to, reject it for safety\n            raise TypeError(\n                f\"Elicitation schema field '{prop_name}' contains a reference '{ref_path}' \"\n                \"that could not be validated. Only references to enum types or primitive types are allowed.\"\n            )\n\n        # Handle union types (oneOf/anyOf)\n        if \"oneOf\" in prop_schema or \"anyOf\" in prop_schema:\n            union_schemas = prop_schema.get(\"oneOf\", []) + prop_schema.get(\"anyOf\", [])\n            for union_schema in union_schemas:\n                # Allow const and enum in unions\n                if \"const\" in union_schema or \"enum\" in union_schema:\n                    continue\n                union_type = union_schema.get(\"type\")\n                if union_type not in ALLOWED_TYPES:\n                    raise TypeError(\n                        f\"Elicitation schema field '{prop_name}' has union type '{union_type}' which is not \"\n                        f\"a primitive type. Only {ALLOWED_TYPES} are allowed in elicitation schemas.\"\n                    )\n            continue\n\n        # Check for arrays before checking primitive types\n        if prop_type == \"array\":\n            items_schema = prop_schema.get(\"items\", {})\n            if items_schema.get(\"type\") == \"object\":\n                raise TypeError(\n                    f\"Elicitation schema field '{prop_name}' is an array of objects, but arrays of objects are not allowed. \"\n                    \"Elicitation schemas must be flat objects with primitive properties only.\"\n                )\n\n            # Allow arrays with enum patterns (for multi-select)\n            if \"enum\" in items_schema:\n                continue  # Allowed: {\"type\": \"array\", \"items\": {\"enum\": [...]}}\n\n            # Allow arrays with oneOf/anyOf const patterns (SEP-1330)\n            if \"oneOf\" in items_schema or \"anyOf\" in items_schema:\n                union_schemas = items_schema.get(\"oneOf\", []) + items_schema.get(\n                    \"anyOf\", []\n                )\n                if union_schemas and all(\"const\" in s for s in union_schemas):\n                    continue  # Allowed: {\"type\": \"array\", \"items\": {\"anyOf\": [{\"const\": ...}, ...]}}\n\n            # Reject other array types (e.g., arrays of primitives without enum pattern)\n            raise TypeError(\n                f\"Elicitation schema field '{prop_name}' is an array, but arrays are only allowed \"\n                \"when items are enums (for multi-select). Only enum arrays are supported in elicitation schemas.\"\n            )\n\n        # Check for nested objects (not allowed)\n        if prop_type == \"object\":\n            raise TypeError(\n                f\"Elicitation schema field '{prop_name}' is an object, but nested objects are not allowed. \"\n                \"Elicitation schemas must be flat objects with primitive properties only.\"\n            )\n\n        # Check if it's a primitive type\n        if prop_type not in ALLOWED_TYPES:\n            raise TypeError(\n                f\"Elicitation schema field '{prop_name}' has type '{prop_type}' which is not \"\n                f\"a primitive type. Only {ALLOWED_TYPES} are allowed in elicitation schemas.\"\n            )\n"
  },
  {
    "path": "src/fastmcp/server/event_store.py",
    "content": "\"\"\"EventStore implementation backed by AsyncKeyValue.\n\nThis module provides an EventStore implementation that enables SSE polling/resumability\nfor Streamable HTTP transports. Events are stored using the key_value package's\nAsyncKeyValue protocol, allowing users to configure any compatible backend\n(in-memory, Redis, etc.) following the same pattern as ResponseCachingMiddleware.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom uuid import uuid4\n\nfrom key_value.aio.adapters.pydantic import PydanticAdapter\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.streamable_http import EventCallback, EventId, EventMessage, StreamId\nfrom mcp.server.streamable_http import EventStore as SDKEventStore\nfrom mcp.types import JSONRPCMessage\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import FastMCPBaseModel\n\nlogger = get_logger(__name__)\n\n\nclass EventEntry(FastMCPBaseModel):\n    \"\"\"Stored event entry.\"\"\"\n\n    event_id: str\n    stream_id: str\n    message: dict | None  # JSONRPCMessage serialized to dict\n\n\nclass StreamEventList(FastMCPBaseModel):\n    \"\"\"List of event IDs for a stream.\"\"\"\n\n    event_ids: list[str]\n\n\nclass EventStore(SDKEventStore):\n    \"\"\"EventStore implementation backed by AsyncKeyValue.\n\n    Enables SSE polling/resumability by storing events that can be replayed\n    when clients reconnect. Works with any AsyncKeyValue backend (memory, Redis, etc.)\n    following the same pattern as ResponseCachingMiddleware and OAuthProxy.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.event_store import EventStore\n\n        # Default in-memory storage\n        event_store = EventStore()\n\n        # Or with a custom backend\n        from key_value.aio.stores.redis import RedisStore\n        redis_backend = RedisStore(url=\"redis://localhost\")\n        event_store = EventStore(storage=redis_backend)\n\n        mcp = FastMCP(\"MyServer\")\n        app = mcp.http_app(event_store=event_store, retry_interval=2000)\n        ```\n\n    Args:\n        storage: AsyncKeyValue backend. Defaults to MemoryStore.\n        max_events_per_stream: Maximum events to retain per stream. Default 100.\n        ttl: Event TTL in seconds. Default 3600 (1 hour). Set to None for no expiration.\n    \"\"\"\n\n    def __init__(\n        self,\n        storage: AsyncKeyValue | None = None,\n        max_events_per_stream: int = 100,\n        ttl: int | None = 3600,\n    ):\n        self._storage: AsyncKeyValue = storage or MemoryStore()\n        self._max_events_per_stream = max_events_per_stream\n        self._ttl = ttl\n\n        # PydanticAdapter for type-safe storage (following OAuth proxy pattern)\n        self._event_store: PydanticAdapter[EventEntry] = PydanticAdapter[EventEntry](\n            key_value=self._storage,\n            pydantic_model=EventEntry,\n            default_collection=\"fastmcp_events\",\n        )\n        self._stream_store: PydanticAdapter[StreamEventList] = PydanticAdapter[\n            StreamEventList\n        ](\n            key_value=self._storage,\n            pydantic_model=StreamEventList,\n            default_collection=\"fastmcp_streams\",\n        )\n\n    async def store_event(\n        self, stream_id: StreamId, message: JSONRPCMessage | None\n    ) -> EventId:\n        \"\"\"Store an event and return its ID.\n\n        Args:\n            stream_id: ID of the stream the event belongs to\n            message: The JSON-RPC message to store, or None for priming events\n\n        Returns:\n            The generated event ID for the stored event\n        \"\"\"\n        event_id = str(uuid4())\n\n        # Store the event entry\n        entry = EventEntry(\n            event_id=event_id,\n            stream_id=stream_id,\n            message=message.model_dump(mode=\"json\") if message else None,\n        )\n        await self._event_store.put(key=event_id, value=entry, ttl=self._ttl)\n\n        # Update stream's event list\n        stream_data = await self._stream_store.get(key=stream_id)\n        event_ids = stream_data.event_ids if stream_data else []\n        event_ids.append(event_id)\n\n        # Trim to max events (delete old events)\n        if len(event_ids) > self._max_events_per_stream:\n            for old_id in event_ids[: -self._max_events_per_stream]:\n                await self._event_store.delete(key=old_id)\n            event_ids = event_ids[-self._max_events_per_stream :]\n\n        await self._stream_store.put(\n            key=stream_id,\n            value=StreamEventList(event_ids=event_ids),\n            ttl=self._ttl,\n        )\n\n        return event_id\n\n    async def replay_events_after(\n        self,\n        last_event_id: EventId,\n        send_callback: EventCallback,\n    ) -> StreamId | None:\n        \"\"\"Replay events that occurred after the specified event ID.\n\n        Args:\n            last_event_id: The ID of the last event the client received\n            send_callback: A callback function to send events to the client\n\n        Returns:\n            The stream ID of the replayed events, or None if the event ID was not found\n        \"\"\"\n        # Look up the event to find its stream\n        entry = await self._event_store.get(key=last_event_id)\n        if not entry:\n            logger.warning(f\"Event ID {last_event_id} not found in store\")\n            return None\n\n        stream_id = entry.stream_id\n        stream_data = await self._stream_store.get(key=stream_id)\n        if not stream_data:\n            logger.warning(f\"Stream {stream_id} not found in store\")\n            return None\n\n        event_ids = stream_data.event_ids\n\n        # Find events after last_event_id\n        try:\n            start_idx = event_ids.index(last_event_id) + 1\n        except ValueError:\n            logger.warning(f\"Event ID {last_event_id} not found in stream {stream_id}\")\n            return None\n\n        # Replay events after the last one\n        for event_id in event_ids[start_idx:]:\n            event = await self._event_store.get(key=event_id)\n            if event and event.message:\n                msg = JSONRPCMessage.model_validate(event.message)\n                await send_callback(EventMessage(msg, event.event_id))\n\n        return stream_id\n"
  },
  {
    "path": "src/fastmcp/server/http.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import AsyncGenerator, Callable, Generator\nfrom contextlib import asynccontextmanager, contextmanager\nfrom contextvars import ContextVar\nfrom typing import TYPE_CHECKING\n\nfrom mcp.server.auth.routes import build_resource_metadata_url\nfrom mcp.server.lowlevel.server import LifespanResultT\nfrom mcp.server.sse import SseServerTransport\nfrom mcp.server.streamable_http import (\n    EventStore,\n)\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom starlette.applications import Starlette\nfrom starlette.middleware import Middleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import BaseRoute, Mount, Route\nfrom starlette.types import Lifespan, Receive, Scope, Send\n\nfrom fastmcp.server.auth import AuthProvider\nfrom fastmcp.server.auth.middleware import RequireAuthMiddleware\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\nlogger = get_logger(__name__)\n\n\nclass StreamableHTTPASGIApp:\n    \"\"\"ASGI application wrapper for Streamable HTTP server transport.\"\"\"\n\n    def __init__(self, session_manager):\n        self.session_manager = session_manager\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        try:\n            await self.session_manager.handle_request(scope, receive, send)\n        except RuntimeError as e:\n            if str(e) == \"Task group is not initialized. Make sure to use run().\":\n                logger.error(\n                    f\"Original RuntimeError from mcp library: {e}\", exc_info=True\n                )\n                new_error_message = (\n                    \"FastMCP's StreamableHTTPSessionManager task group was not initialized. \"\n                    \"This commonly occurs when the FastMCP application's lifespan is not \"\n                    \"passed to the parent ASGI application (e.g., FastAPI or Starlette). \"\n                    \"Please ensure you are setting `lifespan=mcp_app.lifespan` in your \"\n                    \"parent app's constructor, where `mcp_app` is the application instance \"\n                    \"returned by `fastmcp_instance.http_app()`. \\\\n\"\n                    \"For more details, see the FastMCP ASGI integration documentation: \"\n                    \"https://gofastmcp.com/deployment/asgi\"\n                )\n                # Raise a new RuntimeError that includes the original error's message\n                # for full context, but leads with the more helpful guidance.\n                raise RuntimeError(f\"{new_error_message}\\\\nOriginal error: {e}\") from e\n            else:\n                # Re-raise other RuntimeErrors if they don't match the specific message\n                raise\n\n\n_current_http_request: ContextVar[Request | None] = ContextVar(\n    \"http_request\",\n    default=None,\n)\n\n\nclass StarletteWithLifespan(Starlette):\n    @property\n    def lifespan(self) -> Lifespan[Starlette]:\n        return self.router.lifespan_context\n\n\n@contextmanager\ndef set_http_request(request: Request) -> Generator[Request, None, None]:\n    token = _current_http_request.set(request)\n    try:\n        yield request\n    finally:\n        _current_http_request.reset(token)\n\n\nclass RequestContextMiddleware:\n    \"\"\"\n    Middleware that stores each request in a ContextVar and sets transport type.\n    \"\"\"\n\n    def __init__(self, app):\n        self.app = app\n\n    async def __call__(self, scope, receive, send):\n        if scope[\"type\"] == \"http\":\n            from fastmcp.server.context import reset_transport, set_transport\n\n            # Get transport type from app state (set during app creation)\n            transport_type = getattr(scope[\"app\"].state, \"transport_type\", None)\n            transport_token = set_transport(transport_type) if transport_type else None\n            try:\n                with set_http_request(Request(scope)):\n                    await self.app(scope, receive, send)\n            finally:\n                if transport_token is not None:\n                    reset_transport(transport_token)\n        else:\n            await self.app(scope, receive, send)\n\n\ndef create_base_app(\n    routes: list[BaseRoute],\n    middleware: list[Middleware],\n    debug: bool = False,\n    lifespan: Callable | None = None,\n) -> StarletteWithLifespan:\n    \"\"\"Create a base Starlette app with common middleware and routes.\n\n    Args:\n        routes: List of routes to include in the app\n        middleware: List of middleware to include in the app\n        debug: Whether to enable debug mode\n        lifespan: Optional lifespan manager for the app\n\n    Returns:\n        A Starlette application\n    \"\"\"\n    # Always add RequestContextMiddleware as the outermost middleware\n    # TODO(ty): remove type ignore when ty supports Starlette Middleware typing\n    middleware.insert(0, Middleware(RequestContextMiddleware))  # type: ignore[arg-type]\n\n    return StarletteWithLifespan(\n        routes=routes,\n        middleware=middleware,\n        debug=debug,\n        lifespan=lifespan,\n    )\n\n\ndef create_sse_app(\n    server: FastMCP[LifespanResultT],\n    message_path: str,\n    sse_path: str,\n    auth: AuthProvider | None = None,\n    debug: bool = False,\n    routes: list[BaseRoute] | None = None,\n    middleware: list[Middleware] | None = None,\n) -> StarletteWithLifespan:\n    \"\"\"Return an instance of the SSE server app.\n\n    Args:\n        server: The FastMCP server instance\n        message_path: Path for SSE messages\n        sse_path: Path for SSE connections\n        auth: Optional authentication provider (AuthProvider)\n        debug: Whether to enable debug mode\n        routes: Optional list of custom routes\n        middleware: Optional list of middleware\n    Returns:\n        A Starlette application with RequestContextMiddleware\n    \"\"\"\n\n    server_routes: list[BaseRoute] = []\n    server_middleware: list[Middleware] = []\n\n    # Set up SSE transport\n    sse = SseServerTransport(message_path)\n\n    # Create handler for SSE connections\n    async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response:\n        async with sse.connect_sse(scope, receive, send) as streams:\n            await server._mcp_server.run(\n                streams[0],\n                streams[1],\n                server._mcp_server.create_initialization_options(),\n            )\n        return Response()\n\n    # Set up auth if enabled\n    if auth:\n        # Get auth middleware from the provider\n        auth_middleware = auth.get_middleware()\n\n        # Get auth provider's own routes (OAuth endpoints, metadata, etc)\n        auth_routes = auth.get_routes(mcp_path=sse_path)\n        server_routes.extend(auth_routes)\n        server_middleware.extend(auth_middleware)\n\n        # Build RFC 9728-compliant metadata URL\n        resource_url = auth._get_resource_url(sse_path)\n        resource_metadata_url = (\n            build_resource_metadata_url(resource_url) if resource_url else None\n        )\n\n        # Create protected SSE endpoint route\n        server_routes.append(\n            Route(\n                sse_path,\n                endpoint=RequireAuthMiddleware(\n                    handle_sse,\n                    auth.required_scopes,\n                    resource_metadata_url,\n                ),\n                methods=[\"GET\"],\n            )\n        )\n\n        # Wrap the SSE message endpoint with RequireAuthMiddleware\n        server_routes.append(\n            Mount(\n                message_path,\n                app=RequireAuthMiddleware(\n                    sse.handle_post_message,\n                    auth.required_scopes,\n                    resource_metadata_url,\n                ),\n            )\n        )\n    else:\n        # No auth required\n        async def sse_endpoint(request: Request) -> Response:\n            return await handle_sse(request.scope, request.receive, request._send)\n\n        server_routes.append(\n            Route(\n                sse_path,\n                endpoint=sse_endpoint,\n                methods=[\"GET\"],\n            )\n        )\n        server_routes.append(\n            Mount(\n                message_path,\n                app=sse.handle_post_message,\n            )\n        )\n\n    # Add custom routes with lowest precedence\n    if routes:\n        server_routes.extend(routes)\n    server_routes.extend(server._get_additional_http_routes())\n\n    # Add middleware\n    if middleware:\n        server_middleware.extend(middleware)\n\n    @asynccontextmanager\n    async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:\n        async with server._lifespan_manager():\n            yield\n\n    # Create and return the app\n    app = create_base_app(\n        routes=server_routes,\n        middleware=server_middleware,\n        debug=debug,\n        lifespan=lifespan,\n    )\n    # Store the FastMCP server instance on the Starlette app state\n    app.state.fastmcp_server = server\n    app.state.path = sse_path\n    app.state.transport_type = \"sse\"\n\n    return app\n\n\ndef create_streamable_http_app(\n    server: FastMCP[LifespanResultT],\n    streamable_http_path: str,\n    event_store: EventStore | None = None,\n    retry_interval: int | None = None,\n    auth: AuthProvider | None = None,\n    json_response: bool = False,\n    stateless_http: bool = False,\n    debug: bool = False,\n    routes: list[BaseRoute] | None = None,\n    middleware: list[Middleware] | None = None,\n) -> StarletteWithLifespan:\n    \"\"\"Return an instance of the StreamableHTTP server app.\n\n    Args:\n        server: The FastMCP server instance\n        streamable_http_path: Path for StreamableHTTP connections\n        event_store: Optional event store for SSE polling/resumability\n        retry_interval: Optional retry interval in milliseconds for SSE polling.\n            Controls how quickly clients should reconnect after server-initiated\n            disconnections. Requires event_store to be set. Defaults to SDK default.\n        auth: Optional authentication provider (AuthProvider)\n        json_response: Whether to use JSON response format\n        stateless_http: Whether to use stateless mode (new transport per request)\n        debug: Whether to enable debug mode\n        routes: Optional list of custom routes\n        middleware: Optional list of middleware\n\n    Returns:\n        A Starlette application with StreamableHTTP support\n    \"\"\"\n    server_routes: list[BaseRoute] = []\n    server_middleware: list[Middleware] = []\n\n    # Create session manager using the provided event store\n    session_manager = StreamableHTTPSessionManager(\n        app=server._mcp_server,\n        event_store=event_store,\n        retry_interval=retry_interval,\n        json_response=json_response,\n        stateless=stateless_http,\n    )\n\n    # Create the ASGI app wrapper\n    streamable_http_app = StreamableHTTPASGIApp(session_manager)\n\n    # Add StreamableHTTP routes with or without auth\n    if auth:\n        # Get auth middleware from the provider\n        auth_middleware = auth.get_middleware()\n\n        # Get auth provider's own routes (OAuth endpoints, metadata, etc)\n        auth_routes = auth.get_routes(mcp_path=streamable_http_path)\n        server_routes.extend(auth_routes)\n        server_middleware.extend(auth_middleware)\n\n        # Build RFC 9728-compliant metadata URL\n        resource_url = auth._get_resource_url(streamable_http_path)\n        resource_metadata_url = (\n            build_resource_metadata_url(resource_url) if resource_url else None\n        )\n\n        # Create protected HTTP endpoint route\n        # Stateless servers have no session tracking, so GET SSE streams\n        # (for server-initiated notifications) serve no purpose.\n        http_methods = (\n            [\"POST\", \"DELETE\"] if stateless_http else [\"GET\", \"POST\", \"DELETE\"]\n        )\n        server_routes.append(\n            Route(\n                streamable_http_path,\n                endpoint=RequireAuthMiddleware(\n                    streamable_http_app,\n                    auth.required_scopes,\n                    resource_metadata_url,\n                ),\n                methods=http_methods,\n            )\n        )\n    else:\n        # No auth required\n        http_methods = [\"POST\", \"DELETE\"] if stateless_http else None\n        server_routes.append(\n            Route(\n                streamable_http_path,\n                endpoint=streamable_http_app,\n                methods=http_methods,\n            )\n        )\n\n    # Add custom routes with lowest precedence\n    if routes:\n        server_routes.extend(routes)\n    server_routes.extend(server._get_additional_http_routes())\n\n    # Add middleware\n    if middleware:\n        server_middleware.extend(middleware)\n\n    # Create a lifespan manager to start and stop the session manager\n    @asynccontextmanager\n    async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:\n        async with server._lifespan_manager(), session_manager.run():\n            yield\n\n    # Create and return the app with lifespan\n    app = create_base_app(\n        routes=server_routes,\n        middleware=server_middleware,\n        debug=debug,\n        lifespan=lifespan,\n    )\n    # Store the FastMCP server instance on the Starlette app state\n    app.state.fastmcp_server = server\n    app.state.path = streamable_http_path\n    app.state.transport_type = \"streamable-http\"\n\n    return app\n"
  },
  {
    "path": "src/fastmcp/server/lifespan.py",
    "content": "\"\"\"Composable lifespans for FastMCP servers.\n\nThis module provides a `@lifespan` decorator for creating composable server lifespans\nthat can be combined using the `|` operator.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.lifespan import lifespan\n\n    @lifespan\n    async def db_lifespan(server):\n        conn = await connect_db()\n        yield {\"db\": conn}\n        await conn.close()\n\n    @lifespan\n    async def cache_lifespan(server):\n        cache = await connect_cache()\n        yield {\"cache\": cache}\n        await cache.close()\n\n    mcp = FastMCP(\"server\", lifespan=db_lifespan | cache_lifespan)\n    ```\n\nTo compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:\n\n    ```python\n    from contextlib import asynccontextmanager\n    from fastmcp.server.lifespan import lifespan, ContextManagerLifespan\n\n    @asynccontextmanager\n    async def legacy_lifespan(server):\n        yield {\"legacy\": True}\n\n    @lifespan\n    async def new_lifespan(server):\n        yield {\"new\": True}\n\n    # Wrap the legacy lifespan explicitly\n    combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncIterator, Callable\nfrom contextlib import AbstractAsyncContextManager, asynccontextmanager\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\n\nLifespanFn = Callable[[\"FastMCP[Any]\"], AsyncIterator[dict[str, Any] | None]]\nLifespanContextManagerFn = Callable[\n    [\"FastMCP[Any]\"], AbstractAsyncContextManager[dict[str, Any] | None]\n]\n\n\nclass Lifespan:\n    \"\"\"Composable lifespan wrapper.\n\n    Wraps an async generator function and enables composition via the `|` operator.\n    The wrapped function should yield a dict that becomes part of the lifespan context.\n    \"\"\"\n\n    def __init__(self, fn: LifespanFn) -> None:\n        \"\"\"Initialize a Lifespan wrapper.\n\n        Args:\n            fn: An async generator function that takes a FastMCP server and yields\n                a dict for the lifespan context.\n        \"\"\"\n        self._fn = fn\n\n    @asynccontextmanager\n    async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:\n        \"\"\"Execute the lifespan as an async context manager.\n\n        Args:\n            server: The FastMCP server instance.\n\n        Yields:\n            The lifespan context dict.\n        \"\"\"\n        async with asynccontextmanager(self._fn)(server) as result:\n            yield result if result is not None else {}\n\n    def __or__(self, other: Lifespan) -> ComposedLifespan:\n        \"\"\"Compose with another lifespan using the | operator.\n\n        Args:\n            other: Another Lifespan instance.\n\n        Returns:\n            A ComposedLifespan that runs both lifespans.\n\n        Raises:\n            TypeError: If other is not a Lifespan instance.\n        \"\"\"\n        if not isinstance(other, Lifespan):\n            raise TypeError(\n                f\"Cannot compose Lifespan with {type(other).__name__}. \"\n                f\"Use @lifespan decorator or wrap with ContextManagerLifespan().\"\n            )\n        return ComposedLifespan(self, other)\n\n\nclass ContextManagerLifespan(Lifespan):\n    \"\"\"Lifespan wrapper for already-wrapped context manager functions.\n\n    Use this for functions already decorated with @asynccontextmanager.\n    \"\"\"\n\n    _fn: LifespanContextManagerFn  # Override type for this subclass\n\n    def __init__(self, fn: LifespanContextManagerFn) -> None:\n        \"\"\"Initialize with a context manager factory function.\"\"\"\n        self._fn = fn\n\n    @asynccontextmanager\n    async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:\n        \"\"\"Execute the lifespan as an async context manager.\n\n        Args:\n            server: The FastMCP server instance.\n\n        Yields:\n            The lifespan context dict.\n        \"\"\"\n        # self._fn is already a context manager factory, just call it\n        async with self._fn(server) as result:\n            yield result if result is not None else {}\n\n\nclass ComposedLifespan(Lifespan):\n    \"\"\"Two lifespans composed together.\n\n    Enters the left lifespan first, then the right. Exits in reverse order.\n    Results are shallow-merged into a single dict.\n    \"\"\"\n\n    def __init__(self, left: Lifespan, right: Lifespan) -> None:\n        \"\"\"Initialize a composed lifespan.\n\n        Args:\n            left: The first lifespan to enter.\n            right: The second lifespan to enter.\n        \"\"\"\n        # Don't call super().__init__ since we override __call__\n        self._left = left\n        self._right = right\n\n    @asynccontextmanager\n    async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:\n        \"\"\"Execute both lifespans, merging their results.\n\n        Args:\n            server: The FastMCP server instance.\n\n        Yields:\n            The merged lifespan context dict from both lifespans.\n        \"\"\"\n        async with (\n            self._left(server) as left_result,\n            self._right(server) as right_result,\n        ):\n            yield {**left_result, **right_result}\n\n\ndef lifespan(fn: LifespanFn) -> Lifespan:\n    \"\"\"Decorator to create a composable lifespan.\n\n    Use this decorator on an async generator function to make it composable\n    with other lifespans using the `|` operator.\n\n    Example:\n        ```python\n        @lifespan\n        async def my_lifespan(server):\n            # Setup\n            resource = await create_resource()\n            yield {\"resource\": resource}\n            # Teardown\n            await resource.close()\n\n        mcp = FastMCP(\"server\", lifespan=my_lifespan | other_lifespan)\n        ```\n\n    Args:\n        fn: An async generator function that takes a FastMCP server and yields\n            a dict for the lifespan context.\n\n    Returns:\n        A composable Lifespan wrapper.\n    \"\"\"\n    return Lifespan(fn)\n"
  },
  {
    "path": "src/fastmcp/server/low_level.py",
    "content": "from __future__ import annotations\n\nimport weakref\nfrom collections.abc import Awaitable, Callable\nfrom contextlib import AsyncExitStack\nfrom typing import TYPE_CHECKING, Any\n\nimport anyio\nimport mcp.types\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom mcp import LoggingLevel, McpError\nfrom mcp.server.lowlevel.server import (\n    LifespanResultT,\n    NotificationOptions,\n    RequestT,\n)\nfrom mcp.server.lowlevel.server import (\n    Server as _Server,\n)\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.server.stdio import stdio_server as stdio_server\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom pydantic import AnyUrl\n\nfrom fastmcp.server.apps import UI_EXTENSION_ID\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\nlogger = get_logger(__name__)\n\n\nclass MiddlewareServerSession(ServerSession):\n    \"\"\"ServerSession that routes initialization requests through FastMCP middleware.\"\"\"\n\n    def __init__(self, fastmcp: FastMCP, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._fastmcp_ref: weakref.ref[FastMCP] = weakref.ref(fastmcp)\n        # Task group for subscription tasks (set during session run)\n        self._subscription_task_group: anyio.TaskGroup | None = None  # type: ignore[valid-type]\n        # Minimum logging level requested by the client via logging/setLevel\n        self._minimum_logging_level: LoggingLevel | None = None\n\n    @property\n    def fastmcp(self) -> FastMCP:\n        \"\"\"Get the FastMCP instance.\"\"\"\n        fastmcp = self._fastmcp_ref()\n        if fastmcp is None:\n            raise RuntimeError(\"FastMCP instance is no longer available\")\n        return fastmcp\n\n    def client_supports_extension(self, extension_id: str) -> bool:\n        \"\"\"Check if the connected client supports a given MCP extension.\n\n        Inspects the ``extensions`` extra field on ``ClientCapabilities``\n        sent by the client during initialization.\n        \"\"\"\n        client_params = self._client_params\n        if client_params is None:\n            return False\n        caps = client_params.capabilities\n        if caps is None:\n            return False\n        # ClientCapabilities uses extra=\"allow\" — extensions is an extra field\n        extras = caps.model_extra or {}\n        extensions: dict[str, Any] | None = extras.get(\"extensions\")\n        if not extensions:\n            return False\n        return extension_id in extensions\n\n    async def _received_request(\n        self,\n        responder: RequestResponder[mcp.types.ClientRequest, mcp.types.ServerResult],\n    ):\n        \"\"\"\n        Override the _received_request method to route special requests\n        through FastMCP middleware.\n\n        Handles initialization requests and SEP-1686 task methods.\n        \"\"\"\n        import fastmcp.server.context\n        from fastmcp.server.middleware.middleware import MiddlewareContext\n\n        if isinstance(responder.request.root, mcp.types.InitializeRequest):\n            # The MCP SDK's ServerSession._received_request() handles the\n            # initialize request internally by calling responder.respond()\n            # to send the InitializeResult directly to the write stream, then\n            # returning None. This bypasses the middleware return path entirely,\n            # so middleware would only see the request, never the response.\n            #\n            # To expose the response to middleware (e.g., for logging server\n            # capabilities), we wrap responder.respond() to capture the\n            # InitializeResult before it's sent, then return it from\n            # call_original_handler so it flows back through the middleware chain.\n            captured_response: mcp.types.ServerResult | None = None\n            original_respond = responder.respond\n\n            async def capturing_respond(\n                response: mcp.types.ServerResult,\n            ) -> None:\n                nonlocal captured_response\n                captured_response = response\n                return await original_respond(response)\n\n            responder.respond = capturing_respond  # type: ignore[method-assign]\n\n            async def call_original_handler(\n                ctx: MiddlewareContext,\n            ) -> mcp.types.InitializeResult | None:\n                await super(MiddlewareServerSession, self)._received_request(responder)\n                if captured_response is not None and isinstance(\n                    captured_response.root, mcp.types.InitializeResult\n                ):\n                    return captured_response.root\n                return None\n\n            async with fastmcp.server.context.Context(\n                fastmcp=self.fastmcp, session=self\n            ) as fastmcp_ctx:\n                # Create the middleware context.\n                mw_context = MiddlewareContext(\n                    message=responder.request.root,\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"initialize\",\n                    fastmcp_context=fastmcp_ctx,\n                )\n\n                try:\n                    return await self.fastmcp._run_middleware(\n                        mw_context, call_original_handler\n                    )\n                except McpError as e:\n                    # McpError can be thrown from middleware in `on_initialize`\n                    # send the error to responder.\n                    if not responder._completed:\n                        with responder:\n                            await responder.respond(e.error)\n                    else:\n                        # Don't re-raise: prevents responding to initialize request twice\n                        logger.warning(\n                            \"Received McpError but responder is already completed. \"\n                            \"Cannot send error response as response was already sent.\",\n                            exc_info=e,\n                        )\n                    return None\n\n        # Fall through to default handling (task methods now handled via registered handlers)\n        return await super()._received_request(responder)\n\n\nclass LowLevelServer(_Server[LifespanResultT, RequestT]):\n    def __init__(self, fastmcp: FastMCP, *args: Any, **kwargs: Any):\n        super().__init__(*args, **kwargs)\n        # Store a weak reference to FastMCP to avoid circular references\n        self._fastmcp_ref: weakref.ref[FastMCP] = weakref.ref(fastmcp)\n\n        # FastMCP servers support notifications for all components\n        self.notification_options = NotificationOptions(\n            prompts_changed=True,\n            resources_changed=True,\n            tools_changed=True,\n        )\n\n    @property\n    def fastmcp(self) -> FastMCP:\n        \"\"\"Get the FastMCP instance.\"\"\"\n        fastmcp = self._fastmcp_ref()\n        if fastmcp is None:\n            raise RuntimeError(\"FastMCP instance is no longer available\")\n        return fastmcp\n\n    def create_initialization_options(\n        self,\n        notification_options: NotificationOptions | None = None,\n        experimental_capabilities: dict[str, dict[str, Any]] | None = None,\n        **kwargs: Any,\n    ) -> InitializationOptions:\n        # ensure we use the FastMCP notification options\n        if notification_options is None:\n            notification_options = self.notification_options\n        return super().create_initialization_options(\n            notification_options=notification_options,\n            experimental_capabilities=experimental_capabilities,\n            **kwargs,\n        )\n\n    def get_capabilities(\n        self,\n        notification_options: NotificationOptions,\n        experimental_capabilities: dict[str, dict[str, Any]],\n    ) -> mcp.types.ServerCapabilities:\n        \"\"\"Override to set capabilities.tasks as a first-class field per SEP-1686.\n\n        This ensures task capabilities appear in capabilities.tasks instead of\n        capabilities.experimental.tasks, which is required by the MCP spec and\n        enables proper task detection by clients like VS Code Copilot 1.107+.\n        \"\"\"\n        from fastmcp.server.tasks.capabilities import get_task_capabilities\n\n        # Get base capabilities from SDK (pass empty dict for experimental)\n        # since we'll set tasks as a first-class field instead\n        capabilities = super().get_capabilities(\n            notification_options,\n            experimental_capabilities or {},\n        )\n\n        # Set tasks as a first-class field (not experimental) per SEP-1686\n        capabilities.tasks = get_task_capabilities()\n\n        # Advertise MCP Apps extension support (io.modelcontextprotocol/ui)\n        # Uses the same extra-field pattern as tasks above — ServerCapabilities\n        # has extra=\"allow\" so this survives serialization.\n        # Merge with any existing extensions to avoid clobbering other features.\n        existing_extensions: dict[str, Any] = (\n            getattr(capabilities, \"extensions\", None) or {}\n        )\n        capabilities.extensions = {**existing_extensions, UI_EXTENSION_ID: {}}\n\n        return capabilities\n\n    async def run(\n        self,\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        initialization_options: InitializationOptions,\n        raise_exceptions: bool = False,\n        stateless: bool = False,\n    ):\n        \"\"\"\n        Overrides the run method to use the MiddlewareServerSession.\n        \"\"\"\n        async with AsyncExitStack() as stack:\n            lifespan_context = await stack.enter_async_context(self.lifespan(self))\n            session = await stack.enter_async_context(\n                MiddlewareServerSession(\n                    self.fastmcp,\n                    read_stream,\n                    write_stream,\n                    initialization_options,\n                    stateless=stateless,\n                )\n            )\n\n            async with anyio.create_task_group() as tg:\n                # Store task group on session for subscription tasks (SEP-1686)\n                session._subscription_task_group = tg\n\n                async for message in session.incoming_messages:\n                    tg.start_soon(\n                        self._handle_message,\n                        message,\n                        session,\n                        lifespan_context,\n                        raise_exceptions,\n                    )\n\n    def read_resource(\n        self,\n    ) -> Callable[\n        [\n            Callable[\n                [AnyUrl],\n                Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],\n            ]\n        ],\n        Callable[\n            [AnyUrl],\n            Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],\n        ],\n    ]:\n        \"\"\"\n        Decorator for registering a read_resource handler with CreateTaskResult support.\n\n        The MCP SDK's read_resource decorator does not support returning CreateTaskResult\n        for background task execution. This decorator wraps the result in ServerResult.\n\n        This decorator can be removed once the MCP SDK adds native CreateTaskResult support\n        for resources.\n        \"\"\"\n\n        def decorator(\n            func: Callable[\n                [AnyUrl],\n                Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],\n            ],\n        ) -> Callable[\n            [AnyUrl],\n            Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],\n        ]:\n            async def handler(\n                req: mcp.types.ReadResourceRequest,\n            ) -> mcp.types.ServerResult:\n                result = await func(req.params.uri)\n                return mcp.types.ServerResult(result)\n\n            self.request_handlers[mcp.types.ReadResourceRequest] = handler\n            return func\n\n        return decorator\n\n    def get_prompt(\n        self,\n    ) -> Callable[\n        [\n            Callable[\n                [str, dict[str, Any] | None],\n                Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],\n            ]\n        ],\n        Callable[\n            [str, dict[str, Any] | None],\n            Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],\n        ],\n    ]:\n        \"\"\"\n        Decorator for registering a get_prompt handler with CreateTaskResult support.\n\n        The MCP SDK's get_prompt decorator does not support returning CreateTaskResult\n        for background task execution. This decorator wraps the result in ServerResult.\n\n        This decorator can be removed once the MCP SDK adds native CreateTaskResult support\n        for prompts.\n        \"\"\"\n\n        def decorator(\n            func: Callable[\n                [str, dict[str, Any] | None],\n                Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],\n            ],\n        ) -> Callable[\n            [str, dict[str, Any] | None],\n            Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],\n        ]:\n            async def handler(\n                req: mcp.types.GetPromptRequest,\n            ) -> mcp.types.ServerResult:\n                result = await func(req.params.name, req.params.arguments)\n                return mcp.types.ServerResult(result)\n\n            self.request_handlers[mcp.types.GetPromptRequest] = handler\n            return func\n\n        return decorator\n"
  },
  {
    "path": "src/fastmcp/server/middleware/__init__.py",
    "content": "from .authorization import AuthMiddleware\nfrom .middleware import (\n    CallNext,\n    Middleware,\n    MiddlewareContext,\n)\nfrom .ping import PingMiddleware\n\n__all__ = [\n    \"AuthMiddleware\",\n    \"CallNext\",\n    \"Middleware\",\n    \"MiddlewareContext\",\n    \"PingMiddleware\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/middleware/authorization.py",
    "content": "\"\"\"Authorization middleware for FastMCP.\n\nThis module provides middleware-based authorization using callable auth checks.\nAuthMiddleware applies auth checks globally to all components on the server.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.auth import require_scopes, restrict_tag\n    from fastmcp.server.middleware import AuthMiddleware\n\n    # Require specific scope for all components\n    mcp = FastMCP(middleware=[\n        AuthMiddleware(auth=require_scopes(\"api\"))\n    ])\n\n    # Tag-based: components tagged \"admin\" require \"admin\" scope\n    mcp = FastMCP(middleware=[\n        AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))\n    ])\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Sequence\n\nimport mcp.types as mt\n\nfrom fastmcp.exceptions import AuthorizationError\nfrom fastmcp.prompts.base import Prompt, PromptResult\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.auth.authorization import (\n    AuthCheck,\n    AuthContext,\n    run_auth_checks,\n)\nfrom fastmcp.server.dependencies import get_access_token\nfrom fastmcp.server.middleware.middleware import (\n    CallNext,\n    Middleware,\n    MiddlewareContext,\n)\nfrom fastmcp.tools.base import Tool, ToolResult\n\nlogger = logging.getLogger(__name__)\n\n\nclass AuthMiddleware(Middleware):\n    \"\"\"Global authorization middleware using callable checks.\n\n    This middleware applies auth checks to all components (tools, resources,\n    prompts) on the server. It uses the same callable API as component-level\n    auth checks.\n\n    The middleware:\n    - Filters tools/resources/prompts from list responses based on auth checks\n    - Checks auth before tool execution, resource read, and prompt render\n    - Skips all auth checks for STDIO transport (no OAuth concept)\n\n    Args:\n        auth: A single auth check function or list of check functions.\n            All checks must pass for authorization to succeed (AND logic).\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.auth import require_scopes\n\n        # Require specific scope for all components\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"api\"))])\n\n        # Multiple scopes (AND logic)\n        mcp = FastMCP(middleware=[\n            AuthMiddleware(auth=require_scopes(\"read\", \"api\"))\n        ])\n        ```\n    \"\"\"\n\n    def __init__(self, auth: AuthCheck | list[AuthCheck]) -> None:\n        self.auth = auth\n\n    async def on_list_tools(\n        self,\n        context: MiddlewareContext[mt.ListToolsRequest],\n        call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],\n    ) -> Sequence[Tool]:\n        \"\"\"Filter tools/list response based on auth checks.\"\"\"\n        tools = await call_next(context)\n\n        # STDIO has no auth concept, skip filtering\n        # Late import to avoid circular import with context.py\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return tools\n\n        token = get_access_token()\n\n        authorized_tools: list[Tool] = []\n        for tool in tools:\n            ctx = AuthContext(token=token, component=tool)\n            try:\n                if await run_auth_checks(self.auth, ctx):\n                    authorized_tools.append(tool)\n            except AuthorizationError:\n                continue\n\n        return authorized_tools\n\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mt.CallToolRequestParams],\n        call_next: CallNext[mt.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        \"\"\"Check auth before tool execution.\"\"\"\n        # STDIO has no auth concept, skip enforcement\n        # Late import to avoid circular import with context.py\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return await call_next(context)\n\n        # Get the tool being called\n        tool_name = context.message.name\n        fastmcp = context.fastmcp_context\n        if fastmcp is None:\n            # Fail closed: deny access when context is missing\n            logger.warning(\n                f\"AuthMiddleware: fastmcp_context is None for tool '{tool_name}'. \"\n                \"Denying access for security.\"\n            )\n            raise AuthorizationError(\n                f\"Authorization failed for tool '{tool_name}': missing context\"\n            )\n\n        # Get tool (component auth is checked in get_tool, raises if unauthorized)\n        tool = await fastmcp.fastmcp.get_tool(tool_name)\n        if tool is None:\n            raise AuthorizationError(\n                f\"Authorization failed for tool '{tool_name}': tool not found\"\n            )\n\n        # Global auth check\n        token = get_access_token()\n        ctx = AuthContext(token=token, component=tool)\n        if not await run_auth_checks(self.auth, ctx):\n            raise AuthorizationError(\n                f\"Authorization failed for tool '{tool_name}': insufficient permissions\"\n            )\n\n        return await call_next(context)\n\n    async def on_list_resources(\n        self,\n        context: MiddlewareContext[mt.ListResourcesRequest],\n        call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],\n    ) -> Sequence[Resource]:\n        \"\"\"Filter resources/list response based on auth checks.\"\"\"\n        resources = await call_next(context)\n\n        # STDIO has no auth concept, skip filtering\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return resources\n\n        token = get_access_token()\n\n        authorized_resources: list[Resource] = []\n        for resource in resources:\n            ctx = AuthContext(token=token, component=resource)\n            try:\n                if await run_auth_checks(self.auth, ctx):\n                    authorized_resources.append(resource)\n            except AuthorizationError:\n                continue\n\n        return authorized_resources\n\n    async def on_read_resource(\n        self,\n        context: MiddlewareContext[mt.ReadResourceRequestParams],\n        call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult],\n    ) -> ResourceResult:\n        \"\"\"Check auth before resource read.\"\"\"\n        # STDIO has no auth concept, skip enforcement\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return await call_next(context)\n\n        # Get the resource being read\n        uri = context.message.uri\n        fastmcp = context.fastmcp_context\n        if fastmcp is None:\n            logger.warning(\n                f\"AuthMiddleware: fastmcp_context is None for resource '{uri}'. \"\n                \"Denying access for security.\"\n            )\n            raise AuthorizationError(\n                f\"Authorization failed for resource '{uri}': missing context\"\n            )\n\n        # Get resource/template (component auth is checked in get_*, raises if unauthorized)\n        component = await fastmcp.fastmcp.get_resource(str(uri))\n        if component is None:\n            component = await fastmcp.fastmcp.get_resource_template(str(uri))\n        if component is None:\n            raise AuthorizationError(\n                f\"Authorization failed for resource '{uri}': resource not found\"\n            )\n\n        # Global auth check\n        token = get_access_token()\n        ctx = AuthContext(token=token, component=component)\n        if not await run_auth_checks(self.auth, ctx):\n            raise AuthorizationError(\n                f\"Authorization failed for resource '{uri}': insufficient permissions\"\n            )\n\n        return await call_next(context)\n\n    async def on_list_resource_templates(\n        self,\n        context: MiddlewareContext[mt.ListResourceTemplatesRequest],\n        call_next: CallNext[\n            mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]\n        ],\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"Filter resource templates/list response based on auth checks.\"\"\"\n        templates = await call_next(context)\n\n        # STDIO has no auth concept, skip filtering\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return templates\n\n        token = get_access_token()\n\n        authorized_templates: list[ResourceTemplate] = []\n        for template in templates:\n            ctx = AuthContext(token=token, component=template)\n            try:\n                if await run_auth_checks(self.auth, ctx):\n                    authorized_templates.append(template)\n            except AuthorizationError:\n                continue\n\n        return authorized_templates\n\n    async def on_list_prompts(\n        self,\n        context: MiddlewareContext[mt.ListPromptsRequest],\n        call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],\n    ) -> Sequence[Prompt]:\n        \"\"\"Filter prompts/list response based on auth checks.\"\"\"\n        prompts = await call_next(context)\n\n        # STDIO has no auth concept, skip filtering\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return prompts\n\n        token = get_access_token()\n\n        authorized_prompts: list[Prompt] = []\n        for prompt in prompts:\n            ctx = AuthContext(token=token, component=prompt)\n            try:\n                if await run_auth_checks(self.auth, ctx):\n                    authorized_prompts.append(prompt)\n            except AuthorizationError:\n                continue\n\n        return authorized_prompts\n\n    async def on_get_prompt(\n        self,\n        context: MiddlewareContext[mt.GetPromptRequestParams],\n        call_next: CallNext[mt.GetPromptRequestParams, PromptResult],\n    ) -> PromptResult:\n        \"\"\"Check auth before prompt render.\"\"\"\n        # STDIO has no auth concept, skip enforcement\n        from fastmcp.server.context import _current_transport\n\n        if _current_transport.get() == \"stdio\":\n            return await call_next(context)\n\n        # Get the prompt being rendered\n        prompt_name = context.message.name\n        fastmcp = context.fastmcp_context\n        if fastmcp is None:\n            logger.warning(\n                f\"AuthMiddleware: fastmcp_context is None for prompt '{prompt_name}'. \"\n                \"Denying access for security.\"\n            )\n            raise AuthorizationError(\n                f\"Authorization failed for prompt '{prompt_name}': missing context\"\n            )\n\n        # Get prompt (component auth is checked in get_prompt, raises if unauthorized)\n        prompt = await fastmcp.fastmcp.get_prompt(prompt_name)\n        if prompt is None:\n            raise AuthorizationError(\n                f\"Authorization failed for prompt '{prompt_name}': prompt not found\"\n            )\n\n        # Global auth check\n        token = get_access_token()\n        ctx = AuthContext(token=token, component=prompt)\n        if not await run_auth_checks(self.auth, ctx):\n            raise AuthorizationError(\n                f\"Authorization failed for prompt '{prompt_name}': insufficient permissions\"\n            )\n\n        return await call_next(context)\n"
  },
  {
    "path": "src/fastmcp/server/middleware/caching.py",
    "content": "\"\"\"A middleware for response caching.\"\"\"\n\nimport hashlib\nfrom collections.abc import Sequence\nfrom logging import Logger\nfrom typing import Any, TypedDict\n\nimport mcp.types\nimport pydantic_core\nfrom key_value.aio.adapters.pydantic import PydanticAdapter\nfrom key_value.aio.protocols.key_value import AsyncKeyValue\nfrom key_value.aio.stores.memory import MemoryStore\nfrom key_value.aio.wrappers.limit_size import LimitSizeWrapper\nfrom key_value.aio.wrappers.statistics import StatisticsWrapper\nfrom key_value.aio.wrappers.statistics.wrapper import (\n    KVStoreCollectionStatistics,\n)\nfrom pydantic import Field\nfrom typing_extensions import NotRequired, Self, override\n\nfrom fastmcp.prompts.base import Message, Prompt, PromptResult\nfrom fastmcp.resources.base import Resource, ResourceContent, ResourceResult\nfrom fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import FastMCPBaseModel\n\nlogger: Logger = get_logger(name=__name__)\n\n# Constants\nONE_HOUR_IN_SECONDS = 3600\nFIVE_MINUTES_IN_SECONDS = 300\n\nONE_MB_IN_BYTES = 1024 * 1024\n\nGLOBAL_KEY = \"__global__\"\n\n\nclass CachableResourceContent(FastMCPBaseModel):\n    \"\"\"A wrapper for ResourceContent that can be cached.\"\"\"\n\n    content: str | bytes\n    mime_type: str | None = None\n    meta: dict[str, Any] | None = None\n\n\nclass CachableResourceResult(FastMCPBaseModel):\n    \"\"\"A wrapper for ResourceResult that can be cached.\"\"\"\n\n    contents: list[CachableResourceContent]\n    meta: dict[str, Any] | None = None\n\n    def get_size(self) -> int:\n        return len(self.model_dump_json())\n\n    @classmethod\n    def wrap(cls, value: ResourceResult) -> Self:\n        return cls(\n            contents=[\n                CachableResourceContent(\n                    content=item.content, mime_type=item.mime_type, meta=item.meta\n                )\n                for item in value.contents\n            ],\n            meta=value.meta,\n        )\n\n    def unwrap(self) -> ResourceResult:\n        return ResourceResult(\n            contents=[\n                ResourceContent(\n                    content=item.content, mime_type=item.mime_type, meta=item.meta\n                )\n                for item in self.contents\n            ],\n            meta=self.meta,\n        )\n\n\nclass CachableToolResult(FastMCPBaseModel):\n    content: list[mcp.types.ContentBlock]\n    structured_content: dict[str, Any] | None\n    meta: dict[str, Any] | None\n\n    @classmethod\n    def wrap(cls, value: ToolResult) -> Self:\n        return cls(\n            content=value.content,\n            structured_content=value.structured_content,\n            meta=value.meta,\n        )\n\n    def unwrap(self) -> ToolResult:\n        return ToolResult(\n            content=self.content,\n            structured_content=self.structured_content,\n            meta=self.meta,\n        )\n\n\nclass CachableMessage(FastMCPBaseModel):\n    \"\"\"A wrapper for Message that can be cached.\"\"\"\n\n    role: str\n    content: (\n        mcp.types.TextContent\n        | mcp.types.ImageContent\n        | mcp.types.AudioContent\n        | mcp.types.EmbeddedResource\n    )\n\n\nclass CachablePromptResult(FastMCPBaseModel):\n    \"\"\"A wrapper for PromptResult that can be cached.\"\"\"\n\n    messages: list[CachableMessage]\n    description: str | None = None\n    meta: dict[str, Any] | None = None\n\n    def get_size(self) -> int:\n        return len(self.model_dump_json())\n\n    @classmethod\n    def wrap(cls, value: PromptResult) -> Self:\n        return cls(\n            messages=[\n                CachableMessage(role=m.role, content=m.content) for m in value.messages\n            ],\n            description=value.description,\n            meta=value.meta,\n        )\n\n    def unwrap(self) -> PromptResult:\n        return PromptResult(\n            messages=[\n                Message(content=m.content, role=m.role)  # type: ignore[arg-type]\n                for m in self.messages\n            ],\n            description=self.description,\n            meta=self.meta,\n        )\n\n\nclass SharedMethodSettings(TypedDict):\n    \"\"\"Shared config for a cache method.\"\"\"\n\n    ttl: NotRequired[int]\n    enabled: NotRequired[bool]\n\n\nclass ListToolsSettings(SharedMethodSettings):\n    \"\"\"Configuration options for Tool-related caching.\"\"\"\n\n\nclass ListResourcesSettings(SharedMethodSettings):\n    \"\"\"Configuration options for Resource-related caching.\"\"\"\n\n\nclass ListPromptsSettings(SharedMethodSettings):\n    \"\"\"Configuration options for Prompt-related caching.\"\"\"\n\n\nclass CallToolSettings(SharedMethodSettings):\n    \"\"\"Configuration options for Tool-related caching.\"\"\"\n\n    included_tools: NotRequired[list[str]]\n    excluded_tools: NotRequired[list[str]]\n\n\nclass ReadResourceSettings(SharedMethodSettings):\n    \"\"\"Configuration options for Resource-related caching.\"\"\"\n\n\nclass GetPromptSettings(SharedMethodSettings):\n    \"\"\"Configuration options for Prompt-related caching.\"\"\"\n\n\nclass ResponseCachingStatistics(FastMCPBaseModel):\n    list_tools: KVStoreCollectionStatistics | None = Field(default=None)\n    list_resources: KVStoreCollectionStatistics | None = Field(default=None)\n    list_prompts: KVStoreCollectionStatistics | None = Field(default=None)\n    read_resource: KVStoreCollectionStatistics | None = Field(default=None)\n    get_prompt: KVStoreCollectionStatistics | None = Field(default=None)\n    call_tool: KVStoreCollectionStatistics | None = Field(default=None)\n\n\nclass ResponseCachingMiddleware(Middleware):\n    \"\"\"The response caching middleware offers a simple way to cache responses to mcp methods. The Middleware\n    supports cache invalidation via notifications from the server. The Middleware implements TTL-based caching\n    but cache implementations may offer additional features like LRU eviction, size limits, and more.\n\n    When items are retrieved from the cache they will no longer be the original objects, but rather no-op objects\n    this means that response caching may not be compatible with other middleware that expects original subclasses.\n\n    Notes:\n    - Caches `tools/call`, `resources/read`, `prompts/get`, `tools/list`, `resources/list`, and `prompts/list` requests.\n    - Cache keys are derived from method name and arguments.\n    \"\"\"\n\n    def __init__(\n        self,\n        cache_storage: AsyncKeyValue | None = None,\n        list_tools_settings: ListToolsSettings | None = None,\n        list_resources_settings: ListResourcesSettings | None = None,\n        list_prompts_settings: ListPromptsSettings | None = None,\n        read_resource_settings: ReadResourceSettings | None = None,\n        get_prompt_settings: GetPromptSettings | None = None,\n        call_tool_settings: CallToolSettings | None = None,\n        max_item_size: int = ONE_MB_IN_BYTES,\n    ):\n        \"\"\"Initialize the response caching middleware.\n\n        Args:\n            cache_storage: The cache backend to use. If None, an in-memory cache is used.\n            list_tools_settings: The settings for the list tools method. If None, the default settings are used (5 minute TTL).\n            list_resources_settings: The settings for the list resources method. If None, the default settings are used (5 minute TTL).\n            list_prompts_settings: The settings for the list prompts method. If None, the default settings are used (5 minute TTL).\n            read_resource_settings: The settings for the read resource method. If None, the default settings are used (1 hour TTL).\n            get_prompt_settings: The settings for the get prompt method. If None, the default settings are used (1 hour TTL).\n            call_tool_settings: The settings for the call tool method. If None, the default settings are used (1 hour TTL).\n            max_item_size: The maximum size of items eligible for caching. Defaults to 1MB.\n        \"\"\"\n\n        self._backend: AsyncKeyValue = cache_storage or MemoryStore()\n\n        # When the size limit is exceeded, the put will silently fail\n        self._size_limiter: LimitSizeWrapper = LimitSizeWrapper(\n            key_value=self._backend, max_size=max_item_size, raise_on_too_large=False\n        )\n        self._stats: StatisticsWrapper = StatisticsWrapper(key_value=self._size_limiter)\n\n        self._list_tools_settings: ListToolsSettings = (\n            list_tools_settings or ListToolsSettings()\n        )\n        self._list_resources_settings: ListResourcesSettings = (\n            list_resources_settings or ListResourcesSettings()\n        )\n        self._list_prompts_settings: ListPromptsSettings = (\n            list_prompts_settings or ListPromptsSettings()\n        )\n\n        self._read_resource_settings: ReadResourceSettings = (\n            read_resource_settings or ReadResourceSettings()\n        )\n        self._get_prompt_settings: GetPromptSettings = (\n            get_prompt_settings or GetPromptSettings()\n        )\n        self._call_tool_settings: CallToolSettings = (\n            call_tool_settings or CallToolSettings()\n        )\n\n        self._list_tools_cache: PydanticAdapter[list[Tool]] = PydanticAdapter(\n            key_value=self._stats,\n            pydantic_model=list[Tool],\n            default_collection=\"tools/list\",\n        )\n\n        self._list_resources_cache: PydanticAdapter[list[Resource]] = PydanticAdapter(\n            key_value=self._stats,\n            pydantic_model=list[Resource],\n            default_collection=\"resources/list\",\n        )\n\n        self._list_prompts_cache: PydanticAdapter[list[Prompt]] = PydanticAdapter(\n            key_value=self._stats,\n            pydantic_model=list[Prompt],\n            default_collection=\"prompts/list\",\n        )\n\n        self._read_resource_cache: PydanticAdapter[CachableResourceResult] = (\n            PydanticAdapter(\n                key_value=self._stats,\n                pydantic_model=CachableResourceResult,\n                default_collection=\"resources/read\",\n            )\n        )\n\n        self._get_prompt_cache: PydanticAdapter[CachablePromptResult] = PydanticAdapter(\n            key_value=self._stats,\n            pydantic_model=CachablePromptResult,\n            default_collection=\"prompts/get\",\n        )\n\n        self._call_tool_cache: PydanticAdapter[CachableToolResult] = PydanticAdapter(\n            key_value=self._stats,\n            pydantic_model=CachableToolResult,\n            default_collection=\"tools/call\",\n        )\n\n    @override\n    async def on_list_tools(\n        self,\n        context: MiddlewareContext[mcp.types.ListToolsRequest],\n        call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]],\n    ) -> Sequence[Tool]:\n        \"\"\"List tools from the cache, if caching is enabled, and the result is in the cache. Otherwise,\n        otherwise call the next middleware and store the result in the cache if caching is enabled.\"\"\"\n        if self._list_tools_settings.get(\"enabled\") is False:\n            return await call_next(context)\n\n        if cached_value := await self._list_tools_cache.get(key=GLOBAL_KEY):\n            return cached_value\n\n        tools: Sequence[Tool] = await call_next(context=context)\n\n        # Turn any subclass of Tool into a Tool\n        cachable_tools: list[Tool] = [\n            Tool(\n                name=tool.name,\n                title=tool.title,\n                description=tool.description,\n                parameters=tool.parameters,\n                output_schema=tool.output_schema,\n                annotations=tool.annotations,\n                meta=tool.meta,\n                tags=tool.tags,\n            )\n            for tool in tools\n        ]\n\n        await self._list_tools_cache.put(\n            key=GLOBAL_KEY,\n            value=cachable_tools,\n            ttl=self._list_tools_settings.get(\"ttl\", FIVE_MINUTES_IN_SECONDS),\n        )\n\n        return cachable_tools\n\n    @override\n    async def on_list_resources(\n        self,\n        context: MiddlewareContext[mcp.types.ListResourcesRequest],\n        call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]],\n    ) -> Sequence[Resource]:\n        \"\"\"List resources from the cache, if caching is enabled, and the result is in the cache. Otherwise,\n        otherwise call the next middleware and store the result in the cache if caching is enabled.\"\"\"\n        if self._list_resources_settings.get(\"enabled\") is False:\n            return await call_next(context)\n\n        if cached_value := await self._list_resources_cache.get(key=GLOBAL_KEY):\n            return cached_value\n\n        resources: Sequence[Resource] = await call_next(context=context)\n\n        # Turn any subclass of Resource into a Resource\n        cachable_resources: list[Resource] = [\n            Resource(\n                name=resource.name,\n                title=resource.title,\n                description=resource.description,\n                tags=resource.tags,\n                meta=resource.meta,\n                mime_type=resource.mime_type,\n                annotations=resource.annotations,\n                uri=resource.uri,\n            )\n            for resource in resources\n        ]\n\n        await self._list_resources_cache.put(\n            key=GLOBAL_KEY,\n            value=cachable_resources,\n            ttl=self._list_resources_settings.get(\"ttl\", FIVE_MINUTES_IN_SECONDS),\n        )\n\n        return cachable_resources\n\n    @override\n    async def on_list_prompts(\n        self,\n        context: MiddlewareContext[mcp.types.ListPromptsRequest],\n        call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]],\n    ) -> Sequence[Prompt]:\n        \"\"\"List prompts from the cache, if caching is enabled, and the result is in the cache. Otherwise,\n        otherwise call the next middleware and store the result in the cache if caching is enabled.\"\"\"\n        if self._list_prompts_settings.get(\"enabled\") is False:\n            return await call_next(context)\n\n        if cached_value := await self._list_prompts_cache.get(key=GLOBAL_KEY):\n            return cached_value\n\n        prompts: Sequence[Prompt] = await call_next(context=context)\n\n        # Turn any subclass of Prompt into a Prompt\n        cachable_prompts: list[Prompt] = [\n            Prompt(\n                name=prompt.name,\n                title=prompt.title,\n                description=prompt.description,\n                tags=prompt.tags,\n                meta=prompt.meta,\n                arguments=prompt.arguments,\n            )\n            for prompt in prompts\n        ]\n\n        await self._list_prompts_cache.put(\n            key=GLOBAL_KEY,\n            value=cachable_prompts,\n            ttl=self._list_prompts_settings.get(\"ttl\", FIVE_MINUTES_IN_SECONDS),\n        )\n\n        return cachable_prompts\n\n    @override\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mcp.types.CallToolRequestParams],\n        call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        \"\"\"Call a tool from the cache, if caching is enabled, and the result is in the cache. Otherwise,\n        otherwise call the next middleware and store the result in the cache if caching is enabled.\"\"\"\n        tool_name = context.message.name\n\n        if self._call_tool_settings.get(\n            \"enabled\"\n        ) is False or not self._matches_tool_cache_settings(tool_name=tool_name):\n            return await call_next(context=context)\n\n        cache_key: str = _make_call_tool_cache_key(msg=context.message)\n\n        if cached_value := await self._call_tool_cache.get(key=cache_key):\n            return cached_value.unwrap()\n\n        tool_result: ToolResult = await call_next(context=context)\n        cachable_tool_result: CachableToolResult = CachableToolResult.wrap(\n            value=tool_result\n        )\n\n        await self._call_tool_cache.put(\n            key=cache_key,\n            value=cachable_tool_result,\n            ttl=self._call_tool_settings.get(\"ttl\", ONE_HOUR_IN_SECONDS),\n        )\n\n        return cachable_tool_result.unwrap()\n\n    @override\n    async def on_read_resource(\n        self,\n        context: MiddlewareContext[mcp.types.ReadResourceRequestParams],\n        call_next: CallNext[mcp.types.ReadResourceRequestParams, ResourceResult],\n    ) -> ResourceResult:\n        \"\"\"Read a resource from the cache, if caching is enabled, and the result is in the cache. Otherwise,\n        otherwise call the next middleware and store the result in the cache if caching is enabled.\"\"\"\n        if self._read_resource_settings.get(\"enabled\") is False:\n            return await call_next(context=context)\n\n        cache_key: str = _make_read_resource_cache_key(msg=context.message)\n        cached_value: CachableResourceResult | None\n\n        if cached_value := await self._read_resource_cache.get(key=cache_key):\n            return cached_value.unwrap()\n\n        value: ResourceResult = await call_next(context=context)\n        cached_value = CachableResourceResult.wrap(value)\n\n        await self._read_resource_cache.put(\n            key=cache_key,\n            value=cached_value,\n            ttl=self._read_resource_settings.get(\"ttl\", ONE_HOUR_IN_SECONDS),\n        )\n\n        return cached_value.unwrap()\n\n    @override\n    async def on_get_prompt(\n        self,\n        context: MiddlewareContext[mcp.types.GetPromptRequestParams],\n        call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult],\n    ) -> PromptResult:\n        \"\"\"Get a prompt from the cache, if caching is enabled, and the result is in the cache. Otherwise,\n        otherwise call the next middleware and store the result in the cache if caching is enabled.\"\"\"\n        if self._get_prompt_settings.get(\"enabled\") is False:\n            return await call_next(context=context)\n\n        cache_key: str = _make_get_prompt_cache_key(msg=context.message)\n\n        if cached_value := await self._get_prompt_cache.get(key=cache_key):\n            return cached_value.unwrap()\n\n        value: PromptResult = await call_next(context=context)\n\n        await self._get_prompt_cache.put(\n            key=cache_key,\n            value=CachablePromptResult.wrap(value),\n            ttl=self._get_prompt_settings.get(\"ttl\", ONE_HOUR_IN_SECONDS),\n        )\n\n        return value\n\n    def _matches_tool_cache_settings(self, tool_name: str) -> bool:\n        \"\"\"Check if the tool matches the cache settings for tool calls.\"\"\"\n\n        if included_tools := self._call_tool_settings.get(\"included_tools\"):\n            if tool_name not in included_tools:\n                return False\n\n        if excluded_tools := self._call_tool_settings.get(\"excluded_tools\"):\n            if tool_name in excluded_tools:\n                return False\n\n        return True\n\n    def statistics(self) -> ResponseCachingStatistics:\n        \"\"\"Get the statistics for the cache.\"\"\"\n        return ResponseCachingStatistics(\n            list_tools=self._stats.statistics.collections.get(\"tools/list\"),\n            list_resources=self._stats.statistics.collections.get(\"resources/list\"),\n            list_prompts=self._stats.statistics.collections.get(\"prompts/list\"),\n            read_resource=self._stats.statistics.collections.get(\"resources/read\"),\n            get_prompt=self._stats.statistics.collections.get(\"prompts/get\"),\n            call_tool=self._stats.statistics.collections.get(\"tools/call\"),\n        )\n\n\ndef _get_arguments_str(arguments: dict[str, Any] | None) -> str:\n    \"\"\"Get a string representation of the arguments.\"\"\"\n\n    if arguments is None:\n        return \"null\"\n\n    try:\n        return pydantic_core.to_json(value=arguments, fallback=str).decode()\n\n    except TypeError:\n        return repr(arguments)\n\n\ndef _hash_cache_key(value: str) -> str:\n    \"\"\"Build a fixed-length SHA-256 cache key from request-derived input.\"\"\"\n\n    return hashlib.sha256(value.encode()).hexdigest()\n\n\ndef _make_call_tool_cache_key(msg: mcp.types.CallToolRequestParams) -> str:\n    \"\"\"Make a cache key for a tool call using a stable hash of name and arguments.\"\"\"\n\n    return _hash_cache_key(f\"{msg.name}:{_get_arguments_str(msg.arguments)}\")\n\n\ndef _make_read_resource_cache_key(msg: mcp.types.ReadResourceRequestParams) -> str:\n    \"\"\"Make a cache key for a resource read using a stable hash of URI.\"\"\"\n\n    return _hash_cache_key(str(msg.uri))\n\n\ndef _make_get_prompt_cache_key(msg: mcp.types.GetPromptRequestParams) -> str:\n    \"\"\"Make a cache key for a prompt get using a stable hash of name and arguments.\"\"\"\n\n    return _hash_cache_key(f\"{msg.name}:{_get_arguments_str(msg.arguments)}\")\n"
  },
  {
    "path": "src/fastmcp/server/middleware/dereference.py",
    "content": "\"\"\"Middleware that dereferences $ref in JSON schemas before sending to clients.\"\"\"\n\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport mcp.types as mt\nfrom typing_extensions import override\n\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.json_schema import dereference_refs\n\n\nclass DereferenceRefsMiddleware(Middleware):\n    \"\"\"Dereferences $ref in component schemas before sending to clients.\n\n    Some MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref\n    properly. This middleware inlines all $ref definitions so schemas are\n    self-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.\n    \"\"\"\n\n    @override\n    async def on_list_tools(\n        self,\n        context: MiddlewareContext[mt.ListToolsRequest],\n        call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],\n    ) -> Sequence[Tool]:\n        tools = await call_next(context)\n        return [_dereference_tool(tool) for tool in tools]\n\n    @override\n    async def on_list_resource_templates(\n        self,\n        context: MiddlewareContext[mt.ListResourceTemplatesRequest],\n        call_next: CallNext[\n            mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]\n        ],\n    ) -> Sequence[ResourceTemplate]:\n        templates = await call_next(context)\n        return [_dereference_resource_template(t) for t in templates]\n\n\ndef _dereference_tool(tool: Tool) -> Tool:\n    \"\"\"Return a copy of the tool with dereferenced schemas.\"\"\"\n    updates: dict[str, object] = {}\n    if \"$defs\" in tool.parameters or _has_ref(tool.parameters):\n        updates[\"parameters\"] = dereference_refs(tool.parameters)\n    if tool.output_schema is not None and (\n        \"$defs\" in tool.output_schema or _has_ref(tool.output_schema)\n    ):\n        updates[\"output_schema\"] = dereference_refs(tool.output_schema)\n    if updates:\n        return tool.model_copy(update=updates)\n    return tool\n\n\ndef _dereference_resource_template(template: ResourceTemplate) -> ResourceTemplate:\n    \"\"\"Return a copy of the template with dereferenced schemas.\"\"\"\n    if \"$defs\" in template.parameters or _has_ref(template.parameters):\n        return template.model_copy(\n            update={\"parameters\": dereference_refs(template.parameters)}\n        )\n    return template\n\n\ndef _has_ref(schema: dict[str, Any]) -> bool:\n    \"\"\"Check if a schema contains any $ref.\"\"\"\n    if \"$ref\" in schema:\n        return True\n    for value in schema.values():\n        if isinstance(value, dict) and _has_ref(value):\n            return True\n        if isinstance(value, list):\n            for item in value:\n                if isinstance(item, dict) and _has_ref(item):\n                    return True\n    return False\n"
  },
  {
    "path": "src/fastmcp/server/middleware/error_handling.py",
    "content": "\"\"\"Error handling middleware for consistent error responses and tracking.\"\"\"\n\nimport asyncio\nimport logging\nimport traceback\nfrom collections.abc import Callable\nfrom typing import Any\n\nimport anyio\nfrom mcp import McpError\nfrom mcp.types import ErrorData\n\nfrom fastmcp.exceptions import NotFoundError\n\nfrom .middleware import CallNext, Middleware, MiddlewareContext\n\n\nclass ErrorHandlingMiddleware(Middleware):\n    \"\"\"Middleware that provides consistent error handling and logging.\n\n    Catches exceptions, logs them appropriately, and converts them to\n    proper MCP error responses. Also tracks error patterns for monitoring.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware\n        import logging\n\n        # Configure logging to see error details\n        logging.basicConfig(level=logging.ERROR)\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(ErrorHandlingMiddleware())\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        logger: logging.Logger | None = None,\n        include_traceback: bool = False,\n        error_callback: Callable[[Exception, MiddlewareContext], None] | None = None,\n        transform_errors: bool = True,\n    ):\n        \"\"\"Initialize error handling middleware.\n\n        Args:\n            logger: Logger instance for error logging. If None, uses 'fastmcp.errors'\n            include_traceback: Whether to include full traceback in error logs\n            error_callback: Optional callback function called for each error\n            transform_errors: Whether to transform non-MCP errors to McpError\n        \"\"\"\n        self.logger = logger or logging.getLogger(\"fastmcp.errors\")\n        self.include_traceback = include_traceback\n        self.error_callback = error_callback\n        self.transform_errors = transform_errors\n        self.error_counts = {}\n\n    def _log_error(self, error: Exception, context: MiddlewareContext) -> None:\n        \"\"\"Log error with appropriate detail level.\"\"\"\n        error_type = type(error).__name__\n        method = context.method or \"unknown\"\n\n        # Track error counts\n        error_key = f\"{error_type}:{method}\"\n        self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1\n\n        base_message = f\"Error in {method}: {error_type}: {error!s}\"\n\n        if self.include_traceback:\n            self.logger.error(f\"{base_message}\\n{traceback.format_exc()}\")\n        else:\n            self.logger.error(base_message)\n\n        # Call custom error callback if provided\n        if self.error_callback:\n            try:\n                self.error_callback(error, context)\n            except Exception as callback_error:\n                self.logger.error(f\"Error in error callback: {callback_error}\")\n\n    def _transform_error(\n        self, error: Exception, context: MiddlewareContext\n    ) -> Exception:\n        \"\"\"Transform non-MCP errors to proper MCP errors.\"\"\"\n        if isinstance(error, McpError):\n            return error\n\n        if not self.transform_errors:\n            return error\n\n        # Map common exceptions to appropriate MCP error codes\n        error_type = type(error.__cause__) if error.__cause__ else type(error)\n\n        if error_type in (ValueError, TypeError):\n            return McpError(\n                ErrorData(code=-32602, message=f\"Invalid params: {error!s}\")\n            )\n        elif error_type in (FileNotFoundError, KeyError, NotFoundError):\n            # MCP spec defines -32002 specifically for resource not found\n            method = context.method or \"\"\n            if method.startswith(\"resources/\"):\n                return McpError(\n                    ErrorData(code=-32002, message=f\"Resource not found: {error!s}\")\n                )\n            return McpError(ErrorData(code=-32001, message=f\"Not found: {error!s}\"))\n        elif error_type is PermissionError:\n            return McpError(\n                ErrorData(code=-32000, message=f\"Permission denied: {error!s}\")\n            )\n        # asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+\n        elif error_type in (TimeoutError, asyncio.TimeoutError):\n            return McpError(\n                ErrorData(code=-32000, message=f\"Request timeout: {error!s}\")\n            )\n        else:\n            return McpError(\n                ErrorData(code=-32603, message=f\"Internal error: {error!s}\")\n            )\n\n    async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:\n        \"\"\"Handle errors for all messages.\"\"\"\n        try:\n            return await call_next(context)\n        except Exception as error:\n            self._log_error(error, context)\n\n            # Transform and re-raise\n            transformed_error = self._transform_error(error, context)\n            raise transformed_error from error\n\n    def get_error_stats(self) -> dict[str, int]:\n        \"\"\"Get error statistics for monitoring.\"\"\"\n        return self.error_counts.copy()\n\n\nclass RetryMiddleware(Middleware):\n    \"\"\"Middleware that implements automatic retry logic for failed requests.\n\n    Retries requests that fail with transient errors, using exponential\n    backoff to avoid overwhelming the server or external dependencies.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.error_handling import RetryMiddleware\n\n        # Retry up to 3 times with exponential backoff\n        retry_middleware = RetryMiddleware(\n            max_retries=3,\n            retry_exceptions=(ConnectionError, TimeoutError)\n        )\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(retry_middleware)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        max_retries: int = 3,\n        base_delay: float = 1.0,\n        max_delay: float = 60.0,\n        backoff_multiplier: float = 2.0,\n        retry_exceptions: tuple[type[Exception], ...] = (ConnectionError, TimeoutError),\n        logger: logging.Logger | None = None,\n    ):\n        \"\"\"Initialize retry middleware.\n\n        Args:\n            max_retries: Maximum number of retry attempts\n            base_delay: Initial delay between retries in seconds\n            max_delay: Maximum delay between retries in seconds\n            backoff_multiplier: Multiplier for exponential backoff\n            retry_exceptions: Tuple of exception types that should trigger retries\n            logger: Logger for retry attempts\n        \"\"\"\n        self.max_retries = max_retries\n        self.base_delay = base_delay\n        self.max_delay = max_delay\n        self.backoff_multiplier = backoff_multiplier\n        self.retry_exceptions = retry_exceptions\n        self.logger = logger or logging.getLogger(\"fastmcp.retry\")\n\n    def _should_retry(self, error: Exception) -> bool:\n        \"\"\"Determine if an error should trigger a retry.\"\"\"\n        return isinstance(error, self.retry_exceptions)\n\n    def _calculate_delay(self, attempt: int) -> float:\n        \"\"\"Calculate delay for the given attempt number.\"\"\"\n        delay = self.base_delay * (self.backoff_multiplier**attempt)\n        return min(delay, self.max_delay)\n\n    async def on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any:\n        \"\"\"Implement retry logic for requests.\"\"\"\n        last_error = None\n\n        for attempt in range(self.max_retries + 1):\n            try:\n                return await call_next(context)\n            except Exception as error:\n                last_error = error\n\n                # Don't retry on the last attempt or if it's not a retryable error\n                if attempt == self.max_retries or not self._should_retry(error):\n                    break\n\n                delay = self._calculate_delay(attempt)\n                self.logger.warning(\n                    f\"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): \"\n                    f\"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s...\"\n                )\n\n                await anyio.sleep(delay)\n\n        # Re-raise the last error if all retries failed\n        if last_error:\n            raise last_error\n"
  },
  {
    "path": "src/fastmcp/server/middleware/logging.py",
    "content": "\"\"\"Comprehensive logging middleware for FastMCP servers.\"\"\"\n\nimport json\nimport logging\nimport time\nfrom collections.abc import Callable\nfrom logging import Logger\nfrom typing import Any\n\nimport pydantic_core\n\nfrom .middleware import CallNext, Middleware, MiddlewareContext\n\n\ndef default_serializer(data: Any) -> str:\n    \"\"\"The default serializer for Payloads in the logging middleware.\"\"\"\n    return pydantic_core.to_json(data, fallback=str).decode()\n\n\nclass BaseLoggingMiddleware(Middleware):\n    \"\"\"Base class for logging middleware.\"\"\"\n\n    logger: Logger\n    log_level: int\n    include_payloads: bool\n    include_payload_length: bool\n    estimate_payload_tokens: bool\n    max_payload_length: int | None\n    methods: list[str] | None\n    structured_logging: bool\n    payload_serializer: Callable[[Any], str] | None\n\n    def _serialize_payload(self, context: MiddlewareContext[Any]) -> str:\n        payload: str\n\n        if not self.payload_serializer:\n            payload = default_serializer(context.message)\n        else:\n            try:\n                payload = self.payload_serializer(context.message)\n            except Exception as e:\n                self.logger.warning(\n                    f\"Failed to serialize payload due to {e}: {context.type} {context.method} {context.source}.\"\n                )\n                payload = default_serializer(context.message)\n\n        return payload\n\n    def _format_message(self, message: dict[str, str | int | float]) -> str:\n        \"\"\"Format a message for logging.\"\"\"\n        if self.structured_logging:\n            return json.dumps(message)\n        else:\n            return \" \".join([f\"{k}={v}\" for k, v in message.items()])\n\n    def _create_before_message(\n        self, context: MiddlewareContext[Any]\n    ) -> dict[str, str | int | float]:\n        message: dict[str, str | int | float] = {\n            \"event\": context.type + \"_start\",\n            \"method\": context.method or \"unknown\",\n            \"source\": context.source,\n        }\n\n        if (\n            self.include_payloads\n            or self.include_payload_length\n            or self.estimate_payload_tokens\n        ):\n            payload = self._serialize_payload(context)\n\n            if self.include_payload_length or self.estimate_payload_tokens:\n                payload_length = len(payload)\n                payload_tokens = payload_length // 4\n                if self.estimate_payload_tokens:\n                    message[\"payload_tokens\"] = payload_tokens\n                if self.include_payload_length:\n                    message[\"payload_length\"] = payload_length\n\n            if self.max_payload_length and len(payload) > self.max_payload_length:\n                payload = payload[: self.max_payload_length] + \"...\"\n\n            if self.include_payloads:\n                message[\"payload\"] = payload\n                message[\"payload_type\"] = type(context.message).__name__\n\n        return message\n\n    def _create_error_message(\n        self,\n        context: MiddlewareContext[Any],\n        start_time: float,\n        error: Exception,\n    ) -> dict[str, str | int | float]:\n        duration_ms: float = _get_duration_ms(start_time)\n        message = {\n            \"event\": context.type + \"_error\",\n            \"method\": context.method or \"unknown\",\n            \"source\": context.source,\n            \"duration_ms\": duration_ms,\n            \"error\": str(object=error),\n        }\n        return message\n\n    def _create_after_message(\n        self,\n        context: MiddlewareContext[Any],\n        start_time: float,\n    ) -> dict[str, str | int | float]:\n        duration_ms: float = _get_duration_ms(start_time)\n        message = {\n            \"event\": context.type + \"_success\",\n            \"method\": context.method or \"unknown\",\n            \"source\": context.source,\n            \"duration_ms\": duration_ms,\n        }\n        return message\n\n    def _log_message(\n        self, message: dict[str, str | int | float], log_level: int | None = None\n    ):\n        self.logger.log(log_level or self.log_level, self._format_message(message))\n\n    async def on_message(\n        self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]\n    ) -> Any:\n        \"\"\"Log messages for configured methods.\"\"\"\n\n        if self.methods and context.method not in self.methods:\n            return await call_next(context)\n\n        self._log_message(self._create_before_message(context))\n\n        start_time = time.perf_counter()\n        try:\n            result = await call_next(context)\n\n            self._log_message(self._create_after_message(context, start_time))\n\n            return result\n        except Exception as e:\n            self._log_message(\n                self._create_error_message(context, start_time, e), logging.ERROR\n            )\n            raise\n\n\nclass LoggingMiddleware(BaseLoggingMiddleware):\n    \"\"\"Middleware that provides comprehensive request and response logging.\n\n    Logs all MCP messages with configurable detail levels. Useful for debugging,\n    monitoring, and understanding server usage patterns.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.logging import LoggingMiddleware\n        import logging\n\n        # Configure logging\n        logging.basicConfig(level=logging.INFO)\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(LoggingMiddleware())\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        logger: logging.Logger | None = None,\n        log_level: int = logging.INFO,\n        include_payloads: bool = False,\n        include_payload_length: bool = False,\n        estimate_payload_tokens: bool = False,\n        max_payload_length: int = 1000,\n        methods: list[str] | None = None,\n        payload_serializer: Callable[[Any], str] | None = None,\n    ):\n        \"\"\"Initialize logging middleware.\n\n        Args:\n            logger: Logger instance to use. If None, creates a logger named 'fastmcp.requests'\n            log_level: Log level for messages (default: INFO)\n            include_payloads: Whether to include message payloads in logs\n            include_payload_length: Whether to include response size in logs\n            estimate_payload_tokens: Whether to estimate response tokens\n            max_payload_length: Maximum length of payload to log (prevents huge logs)\n            methods: List of methods to log. If None, logs all methods.\n            payload_serializer: Callable that converts objects to a JSON string for the\n                payload. If not provided, uses FastMCP's default tool serializer.\n        \"\"\"\n        self.logger: Logger = logger or logging.getLogger(\"fastmcp.middleware.logging\")\n        self.log_level = log_level\n        self.include_payloads: bool = include_payloads\n        self.include_payload_length: bool = include_payload_length\n        self.estimate_payload_tokens: bool = estimate_payload_tokens\n        self.max_payload_length: int = max_payload_length\n        self.methods: list[str] | None = methods\n        self.payload_serializer: Callable[[Any], str] | None = payload_serializer\n        self.structured_logging: bool = False\n\n\nclass StructuredLoggingMiddleware(BaseLoggingMiddleware):\n    \"\"\"Middleware that provides structured JSON logging for better log analysis.\n\n    Outputs structured logs that are easier to parse and analyze with log\n    aggregation tools like ELK stack, Splunk, or cloud logging services.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.logging import StructuredLoggingMiddleware\n        import logging\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(StructuredLoggingMiddleware())\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        logger: logging.Logger | None = None,\n        log_level: int = logging.INFO,\n        include_payloads: bool = False,\n        include_payload_length: bool = False,\n        estimate_payload_tokens: bool = False,\n        methods: list[str] | None = None,\n        payload_serializer: Callable[[Any], str] | None = None,\n    ):\n        \"\"\"Initialize structured logging middleware.\n\n        Args:\n            logger: Logger instance to use. If None, creates a logger named 'fastmcp.structured'\n            log_level: Log level for messages (default: INFO)\n            include_payloads: Whether to include message payloads in logs\n            include_payload_length: Whether to include payload size in logs\n            estimate_payload_tokens: Whether to estimate token count using length // 4\n            methods: List of methods to log. If None, logs all methods.\n            payload_serializer: Callable that converts objects to a JSON string for the\n                payload. If not provided, uses FastMCP's default tool serializer.\n        \"\"\"\n        self.logger: Logger = logger or logging.getLogger(\n            \"fastmcp.middleware.structured_logging\"\n        )\n        self.log_level: int = log_level\n        self.include_payloads: bool = include_payloads\n        self.include_payload_length: bool = include_payload_length\n        self.estimate_payload_tokens: bool = estimate_payload_tokens\n        self.methods: list[str] | None = methods\n        self.payload_serializer: Callable[[Any], str] | None = payload_serializer\n        self.max_payload_length: int | None = None\n        self.structured_logging: bool = True\n\n\ndef _get_duration_ms(start_time: float, /) -> float:\n    return round(number=(time.perf_counter() - start_time) * 1000, ndigits=2)\n"
  },
  {
    "path": "src/fastmcp/server/middleware/middleware.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom collections.abc import Awaitable, Sequence\nfrom dataclasses import dataclass, field, replace\nfrom datetime import datetime, timezone\nfrom functools import partial\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Generic,\n    Literal,\n    Protocol,\n    runtime_checkable,\n)\n\nimport mcp.types as mt\nfrom typing_extensions import TypeVar\n\nfrom fastmcp.prompts.base import Prompt, PromptResult\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.tools.base import Tool, ToolResult\n\nif TYPE_CHECKING:\n    from fastmcp.server.context import Context\n\n__all__ = [\n    \"CallNext\",\n    \"Middleware\",\n    \"MiddlewareContext\",\n]\n\nlogger = logging.getLogger(__name__)\n\n\nT = TypeVar(\"T\", default=Any)\nR = TypeVar(\"R\", covariant=True, default=Any)\n\n\n@runtime_checkable\nclass CallNext(Protocol[T, R]):\n    def __call__(self, context: MiddlewareContext[T]) -> Awaitable[R]: ...\n\n\n@dataclass(kw_only=True, frozen=True)\nclass MiddlewareContext(Generic[T]):\n    \"\"\"\n    Unified context for all middleware operations.\n    \"\"\"\n\n    message: T\n\n    fastmcp_context: Context | None = None\n\n    # Common metadata\n    source: Literal[\"client\", \"server\"] = \"client\"\n    type: Literal[\"request\", \"notification\"] = \"request\"\n    method: str | None = None\n    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))\n\n    def copy(self, **kwargs: Any) -> MiddlewareContext[T]:\n        return replace(self, **kwargs)\n\n\ndef make_middleware_wrapper(\n    middleware: Middleware, call_next: CallNext[T, R]\n) -> CallNext[T, R]:\n    \"\"\"Create a wrapper that applies a single middleware to a context. The\n    closure bakes in the middleware and call_next function, so it can be\n    passed to other functions that expect a call_next function.\"\"\"\n\n    async def wrapper(context: MiddlewareContext[T]) -> R:\n        return await middleware(context, call_next)\n\n    return wrapper\n\n\nclass Middleware:\n    \"\"\"Base class for FastMCP middleware with dispatching hooks.\"\"\"\n\n    async def __call__(\n        self,\n        context: MiddlewareContext[T],\n        call_next: CallNext[T, Any],\n    ) -> Any:\n        \"\"\"Main entry point that orchestrates the pipeline.\"\"\"\n        handler_chain = await self._dispatch_handler(\n            context,\n            call_next=call_next,\n        )\n        return await handler_chain(context)\n\n    async def _dispatch_handler(\n        self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]\n    ) -> CallNext[Any, Any]:\n        \"\"\"Builds a chain of handlers for a given message.\"\"\"\n        handler = call_next\n\n        match context.method:\n            case \"initialize\":\n                handler = partial(self.on_initialize, call_next=handler)\n            case \"tools/call\":\n                handler = partial(self.on_call_tool, call_next=handler)\n            case \"resources/read\":\n                handler = partial(self.on_read_resource, call_next=handler)\n            case \"prompts/get\":\n                handler = partial(self.on_get_prompt, call_next=handler)\n            case \"tools/list\":\n                handler = partial(self.on_list_tools, call_next=handler)\n            case \"resources/list\":\n                handler = partial(self.on_list_resources, call_next=handler)\n            case \"resources/templates/list\":\n                handler = partial(self.on_list_resource_templates, call_next=handler)\n            case \"prompts/list\":\n                handler = partial(self.on_list_prompts, call_next=handler)\n\n        match context.type:\n            case \"request\":\n                handler = partial(self.on_request, call_next=handler)\n            case \"notification\":\n                handler = partial(self.on_notification, call_next=handler)\n\n        handler = partial(self.on_message, call_next=handler)\n\n        return handler\n\n    async def on_message(\n        self,\n        context: MiddlewareContext[Any],\n        call_next: CallNext[Any, Any],\n    ) -> Any:\n        return await call_next(context)\n\n    async def on_request(\n        self,\n        context: MiddlewareContext[mt.Request[Any, Any]],\n        call_next: CallNext[mt.Request[Any, Any], Any],\n    ) -> Any:\n        return await call_next(context)\n\n    async def on_notification(\n        self,\n        context: MiddlewareContext[mt.Notification[Any, Any]],\n        call_next: CallNext[mt.Notification[Any, Any], Any],\n    ) -> Any:\n        return await call_next(context)\n\n    async def on_initialize(\n        self,\n        context: MiddlewareContext[mt.InitializeRequest],\n        call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n    ) -> mt.InitializeResult | None:\n        return await call_next(context)\n\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mt.CallToolRequestParams],\n        call_next: CallNext[mt.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        return await call_next(context)\n\n    async def on_read_resource(\n        self,\n        context: MiddlewareContext[mt.ReadResourceRequestParams],\n        call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult],\n    ) -> ResourceResult:\n        return await call_next(context)\n\n    async def on_get_prompt(\n        self,\n        context: MiddlewareContext[mt.GetPromptRequestParams],\n        call_next: CallNext[mt.GetPromptRequestParams, PromptResult],\n    ) -> PromptResult:\n        return await call_next(context)\n\n    async def on_list_tools(\n        self,\n        context: MiddlewareContext[mt.ListToolsRequest],\n        call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],\n    ) -> Sequence[Tool]:\n        return await call_next(context)\n\n    async def on_list_resources(\n        self,\n        context: MiddlewareContext[mt.ListResourcesRequest],\n        call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],\n    ) -> Sequence[Resource]:\n        return await call_next(context)\n\n    async def on_list_resource_templates(\n        self,\n        context: MiddlewareContext[mt.ListResourceTemplatesRequest],\n        call_next: CallNext[\n            mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]\n        ],\n    ) -> Sequence[ResourceTemplate]:\n        return await call_next(context)\n\n    async def on_list_prompts(\n        self,\n        context: MiddlewareContext[mt.ListPromptsRequest],\n        call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],\n    ) -> Sequence[Prompt]:\n        return await call_next(context)\n"
  },
  {
    "path": "src/fastmcp/server/middleware/ping.py",
    "content": "\"\"\"Ping middleware for keeping client connections alive.\"\"\"\n\nfrom typing import Any\n\nimport anyio\n\nfrom .middleware import CallNext, Middleware, MiddlewareContext\n\n\nclass PingMiddleware(Middleware):\n    \"\"\"Middleware that sends periodic pings to keep client connections alive.\n\n    Starts a background ping task on first message from each session. The task\n    sends server-to-client pings at the configured interval until the session\n    ends.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.middleware import PingMiddleware\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(PingMiddleware(interval_ms=5000))\n        ```\n    \"\"\"\n\n    def __init__(self, interval_ms: int = 30000):\n        \"\"\"Initialize ping middleware.\n\n        Args:\n            interval_ms: Interval between pings in milliseconds (default: 30000)\n\n        Raises:\n            ValueError: If interval_ms is not positive\n        \"\"\"\n        if interval_ms <= 0:\n            raise ValueError(\"interval_ms must be positive\")\n        self.interval_ms = interval_ms\n        self._active_sessions: set[int] = set()\n        self._lock = anyio.Lock()\n\n    async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:\n        \"\"\"Start ping task on first message from a session.\"\"\"\n        if (\n            context.fastmcp_context is None\n            or context.fastmcp_context.request_context is None\n        ):\n            return await call_next(context)\n\n        session = context.fastmcp_context.session\n        session_id = id(session)\n\n        async with self._lock:\n            if session_id not in self._active_sessions:\n                # _subscription_task_group is added by MiddlewareServerSession\n                tg = session._subscription_task_group  # type: ignore[attr-defined]\n                if tg is not None:\n                    self._active_sessions.add(session_id)\n                    tg.start_soon(self._ping_loop, session, session_id)\n\n        return await call_next(context)\n\n    async def _ping_loop(self, session: Any, session_id: int) -> None:\n        \"\"\"Send periodic pings until session ends.\"\"\"\n        try:\n            while True:\n                await anyio.sleep(self.interval_ms / 1000)\n                await session.send_ping()\n        finally:\n            self._active_sessions.discard(session_id)\n"
  },
  {
    "path": "src/fastmcp/server/middleware/rate_limiting.py",
    "content": "\"\"\"Rate limiting middleware for protecting FastMCP servers from abuse.\"\"\"\n\nimport time\nfrom collections import defaultdict, deque\nfrom collections.abc import Callable\nfrom typing import Any\n\nimport anyio\nfrom mcp import McpError\nfrom mcp.types import ErrorData\n\nfrom .middleware import CallNext, Middleware, MiddlewareContext\n\n\nclass RateLimitError(McpError):\n    \"\"\"Error raised when rate limit is exceeded.\"\"\"\n\n    def __init__(self, message: str = \"Rate limit exceeded\"):\n        super().__init__(ErrorData(code=-32000, message=message))\n\n\nclass TokenBucketRateLimiter:\n    \"\"\"Token bucket implementation for rate limiting.\"\"\"\n\n    def __init__(self, capacity: int, refill_rate: float):\n        \"\"\"Initialize token bucket.\n\n        Args:\n            capacity: Maximum number of tokens in the bucket\n            refill_rate: Tokens added per second\n        \"\"\"\n        self.capacity = capacity\n        self.refill_rate = refill_rate\n        self.tokens = capacity\n        self.last_refill = time.time()\n        self._lock = anyio.Lock()\n\n    async def consume(self, tokens: int = 1) -> bool:\n        \"\"\"Try to consume tokens from the bucket.\n\n        Args:\n            tokens: Number of tokens to consume\n\n        Returns:\n            True if tokens were available and consumed, False otherwise\n        \"\"\"\n        async with self._lock:\n            now = time.time()\n            elapsed = now - self.last_refill\n\n            # Add tokens based on elapsed time\n            self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)\n            self.last_refill = now\n\n            if self.tokens >= tokens:\n                self.tokens -= tokens\n                return True\n            return False\n\n\nclass SlidingWindowRateLimiter:\n    \"\"\"Sliding window rate limiter implementation.\"\"\"\n\n    def __init__(self, max_requests: int, window_seconds: int):\n        \"\"\"Initialize sliding window rate limiter.\n\n        Args:\n            max_requests: Maximum requests allowed in the time window\n            window_seconds: Time window in seconds\n        \"\"\"\n        self.max_requests = max_requests\n        self.window_seconds = window_seconds\n        self.requests = deque()\n        self._lock = anyio.Lock()\n\n    async def is_allowed(self) -> bool:\n        \"\"\"Check if a request is allowed.\"\"\"\n        async with self._lock:\n            now = time.time()\n            cutoff = now - self.window_seconds\n\n            # Remove old requests outside the window\n            while self.requests and self.requests[0] < cutoff:\n                self.requests.popleft()\n\n            if len(self.requests) < self.max_requests:\n                self.requests.append(now)\n                return True\n            return False\n\n\nclass RateLimitingMiddleware(Middleware):\n    \"\"\"Middleware that implements rate limiting to prevent server abuse.\n\n    Uses a token bucket algorithm by default, allowing for burst traffic\n    while maintaining a sustainable long-term rate.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware\n\n        # Allow 10 requests per second with bursts up to 20\n        rate_limiter = RateLimitingMiddleware(\n            max_requests_per_second=10,\n            burst_capacity=20\n        )\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(rate_limiter)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        max_requests_per_second: float = 10.0,\n        burst_capacity: int | None = None,\n        get_client_id: Callable[[MiddlewareContext], str] | None = None,\n        global_limit: bool = False,\n    ):\n        \"\"\"Initialize rate limiting middleware.\n\n        Args:\n            max_requests_per_second: Sustained requests per second allowed\n            burst_capacity: Maximum burst capacity. If None, defaults to 2x max_requests_per_second\n            get_client_id: Function to extract client ID from context. If None, uses global limiting\n            global_limit: If True, apply limit globally; if False, per-client\n        \"\"\"\n        self.max_requests_per_second = max_requests_per_second\n        self.burst_capacity = burst_capacity or int(max_requests_per_second * 2)\n        self.get_client_id = get_client_id\n        self.global_limit = global_limit\n\n        # Storage for rate limiters per client\n        self.limiters: dict[str, TokenBucketRateLimiter] = defaultdict(\n            lambda: TokenBucketRateLimiter(\n                self.burst_capacity, self.max_requests_per_second\n            )\n        )\n\n        # Global rate limiter\n        if self.global_limit:\n            self.global_limiter = TokenBucketRateLimiter(\n                self.burst_capacity, self.max_requests_per_second\n            )\n\n    def _get_client_identifier(self, context: MiddlewareContext) -> str:\n        \"\"\"Get client identifier for rate limiting.\"\"\"\n        if self.get_client_id:\n            return self.get_client_id(context)\n        return \"global\"\n\n    async def on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any:\n        \"\"\"Apply rate limiting to requests.\"\"\"\n        if self.global_limit:\n            # Global rate limiting\n            allowed = await self.global_limiter.consume()\n            if not allowed:\n                raise RateLimitError(\"Global rate limit exceeded\")\n        else:\n            # Per-client rate limiting\n            client_id = self._get_client_identifier(context)\n            limiter = self.limiters[client_id]\n            allowed = await limiter.consume()\n            if not allowed:\n                raise RateLimitError(f\"Rate limit exceeded for client: {client_id}\")\n\n        return await call_next(context)\n\n\nclass SlidingWindowRateLimitingMiddleware(Middleware):\n    \"\"\"Middleware that implements sliding window rate limiting.\n\n    Uses a sliding window approach which provides more precise rate limiting\n    but uses more memory to track individual request timestamps.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.rate_limiting import SlidingWindowRateLimitingMiddleware\n\n        # Allow 100 requests per minute\n        rate_limiter = SlidingWindowRateLimitingMiddleware(\n            max_requests=100,\n            window_minutes=1\n        )\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(rate_limiter)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        max_requests: int,\n        window_minutes: int = 1,\n        get_client_id: Callable[[MiddlewareContext], str] | None = None,\n    ):\n        \"\"\"Initialize sliding window rate limiting middleware.\n\n        Args:\n            max_requests: Maximum requests allowed in the time window\n            window_minutes: Time window in minutes\n            get_client_id: Function to extract client ID from context\n        \"\"\"\n        self.max_requests = max_requests\n        self.window_seconds = window_minutes * 60\n        self.get_client_id = get_client_id\n\n        # Storage for rate limiters per client\n        self.limiters: dict[str, SlidingWindowRateLimiter] = defaultdict(\n            lambda: SlidingWindowRateLimiter(self.max_requests, self.window_seconds)\n        )\n\n    def _get_client_identifier(self, context: MiddlewareContext) -> str:\n        \"\"\"Get client identifier for rate limiting.\"\"\"\n        if self.get_client_id:\n            return self.get_client_id(context)\n        return \"global\"\n\n    async def on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any:\n        \"\"\"Apply sliding window rate limiting to requests.\"\"\"\n        client_id = self._get_client_identifier(context)\n        limiter = self.limiters[client_id]\n\n        allowed = await limiter.is_allowed()\n        if not allowed:\n            raise RateLimitError(\n                f\"Rate limit exceeded: {self.max_requests} requests per \"\n                f\"{self.window_seconds // 60} minutes for client: {client_id}\"\n            )\n\n        return await call_next(context)\n"
  },
  {
    "path": "src/fastmcp/server/middleware/response_limiting.py",
    "content": "\"\"\"Response limiting middleware for controlling tool response sizes.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nimport mcp.types as mt\nimport pydantic_core\nfrom mcp.types import TextContent\n\nfrom fastmcp.tools.base import ToolResult\n\nfrom .middleware import CallNext, Middleware, MiddlewareContext\n\n__all__ = [\"ResponseLimitingMiddleware\"]\n\nlogger = logging.getLogger(__name__)\n\n\nclass ResponseLimitingMiddleware(Middleware):\n    \"\"\"Middleware that limits the response size of tool calls.\n\n    Intercepts tool call responses and enforces size limits. If a response\n    exceeds the limit, it extracts text content, truncates it, and returns\n    a single TextContent block.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.middleware.response_limiting import (\n            ResponseLimitingMiddleware,\n        )\n\n        mcp = FastMCP(\"MyServer\")\n\n        # Limit all tool responses to 500KB\n        mcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000))\n\n        # Limit only specific tools\n        mcp.add_middleware(\n            ResponseLimitingMiddleware(\n                max_size=100_000,\n                tools=[\"search\", \"fetch_data\"],\n            )\n        )\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        max_size: int = 1_000_000,\n        truncation_suffix: str = \"\\n\\n[Response truncated due to size limit]\",\n        tools: list[str] | None = None,\n    ) -> None:\n        \"\"\"Initialize response limiting middleware.\n\n        Args:\n            max_size: Maximum response size in bytes. Defaults to 1MB (1,000,000).\n            truncation_suffix: Suffix to append when truncating responses.\n                Defaults to \"\\\\n\\\\n[Response truncated due to size limit]\".\n            tools: List of tool names to apply limiting to. If None, applies to all.\n        \"\"\"\n        if max_size <= 0:\n            raise ValueError(f\"max_size must be positive, got {max_size}\")\n        self.max_size = max_size\n        self.truncation_suffix = truncation_suffix\n        self.tools = set(tools) if tools is not None else None\n\n    def _truncate_to_result(self, text: str) -> ToolResult:\n        \"\"\"Truncate text to fit within max_size and wrap in ToolResult.\"\"\"\n        suffix_bytes = len(self.truncation_suffix.encode(\"utf-8\"))\n        # Account for JSON wrapper overhead: {\"content\":[{\"type\":\"text\",\"text\":\"...\"}]}\n        overhead = 50\n        target_size = self.max_size - suffix_bytes - overhead\n\n        if target_size <= 0:\n            # Edge case: max_size too small for even the suffix\n            truncated = self.truncation_suffix\n        else:\n            # Truncate to target size, preserving UTF-8 boundaries\n            encoded = text.encode(\"utf-8\")\n            if len(encoded) <= target_size:\n                truncated = text + self.truncation_suffix\n            else:\n                truncated = (\n                    encoded[:target_size].decode(\"utf-8\", errors=\"ignore\")\n                    + self.truncation_suffix\n                )\n\n        return ToolResult(content=[TextContent(type=\"text\", text=truncated)])\n\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mt.CallToolRequestParams],\n        call_next: CallNext[mt.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        \"\"\"Intercept tool calls and limit response size.\"\"\"\n        result = await call_next(context)\n\n        # Check if we should limit this tool\n        if self.tools is not None and context.message.name not in self.tools:\n            return result\n\n        # Measure serialized size\n        serialized = pydantic_core.to_json(result, fallback=str)\n        if len(serialized) <= self.max_size:\n            return result\n\n        # Over limit: extract text, truncate, return single TextContent\n        logger.warning(\n            \"Tool %r response exceeds size limit: %d bytes > %d bytes, truncating\",\n            context.message.name,\n            len(serialized),\n            self.max_size,\n        )\n\n        texts = [b.text for b in result.content if isinstance(b, TextContent)]\n        text = (\n            \"\\n\\n\".join(texts)\n            if texts\n            else serialized.decode(\"utf-8\", errors=\"replace\")\n        )\n\n        return self._truncate_to_result(text)\n"
  },
  {
    "path": "src/fastmcp/server/middleware/timing.py",
    "content": "\"\"\"Timing middleware for measuring and logging request performance.\"\"\"\n\nimport logging\nimport time\nfrom typing import Any\n\nfrom .middleware import CallNext, Middleware, MiddlewareContext\n\n\nclass TimingMiddleware(Middleware):\n    \"\"\"Middleware that logs the execution time of requests.\n\n    Only measures and logs timing for request messages (not notifications).\n    Provides insights into performance characteristics of your MCP server.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.timing import TimingMiddleware\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(TimingMiddleware())\n\n        # Now all requests will be timed and logged\n        ```\n    \"\"\"\n\n    def __init__(\n        self, logger: logging.Logger | None = None, log_level: int = logging.INFO\n    ):\n        \"\"\"Initialize timing middleware.\n\n        Args:\n            logger: Logger instance to use. If None, creates a logger named 'fastmcp.timing'\n            log_level: Log level for timing messages (default: INFO)\n        \"\"\"\n        self.logger = logger or logging.getLogger(\"fastmcp.timing\")\n        self.log_level = log_level\n\n    async def on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any:\n        \"\"\"Time request execution and log the results.\"\"\"\n        method = context.method or \"unknown\"\n\n        start_time = time.perf_counter()\n        try:\n            result = await call_next(context)\n            duration_ms = (time.perf_counter() - start_time) * 1000\n            self.logger.log(\n                self.log_level, f\"Request {method} completed in {duration_ms:.2f}ms\"\n            )\n            return result\n        except Exception as e:\n            duration_ms = (time.perf_counter() - start_time) * 1000\n            self.logger.log(\n                self.log_level,\n                f\"Request {method} failed after {duration_ms:.2f}ms: {e}\",\n            )\n            raise\n\n\nclass DetailedTimingMiddleware(Middleware):\n    \"\"\"Enhanced timing middleware with per-operation breakdowns.\n\n    Provides detailed timing information for different types of MCP operations,\n    allowing you to identify performance bottlenecks in specific operations.\n\n    Example:\n        ```python\n        from fastmcp.server.middleware.timing import DetailedTimingMiddleware\n        import logging\n\n        # Configure logging to see the output\n        logging.basicConfig(level=logging.INFO)\n\n        mcp = FastMCP(\"MyServer\")\n        mcp.add_middleware(DetailedTimingMiddleware())\n        ```\n    \"\"\"\n\n    def __init__(\n        self, logger: logging.Logger | None = None, log_level: int = logging.INFO\n    ):\n        \"\"\"Initialize detailed timing middleware.\n\n        Args:\n            logger: Logger instance to use. If None, creates a logger named 'fastmcp.timing.detailed'\n            log_level: Log level for timing messages (default: INFO)\n        \"\"\"\n        self.logger = logger or logging.getLogger(\"fastmcp.timing.detailed\")\n        self.log_level = log_level\n\n    async def _time_operation(\n        self, context: MiddlewareContext, call_next: CallNext, operation_name: str\n    ) -> Any:\n        \"\"\"Helper method to time any operation.\"\"\"\n        start_time = time.perf_counter()\n        try:\n            result = await call_next(context)\n            duration_ms = (time.perf_counter() - start_time) * 1000\n            self.logger.log(\n                self.log_level, f\"{operation_name} completed in {duration_ms:.2f}ms\"\n            )\n            return result\n        except Exception as e:\n            duration_ms = (time.perf_counter() - start_time) * 1000\n            self.logger.log(\n                self.log_level,\n                f\"{operation_name} failed after {duration_ms:.2f}ms: {e}\",\n            )\n            raise\n\n    async def on_call_tool(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time tool execution.\"\"\"\n        tool_name = getattr(context.message, \"name\", \"unknown\")\n        return await self._time_operation(context, call_next, f\"Tool '{tool_name}'\")\n\n    async def on_read_resource(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time resource reading.\"\"\"\n        resource_uri = getattr(context.message, \"uri\", \"unknown\")\n        return await self._time_operation(\n            context, call_next, f\"Resource '{resource_uri}'\"\n        )\n\n    async def on_get_prompt(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time prompt retrieval.\"\"\"\n        prompt_name = getattr(context.message, \"name\", \"unknown\")\n        return await self._time_operation(context, call_next, f\"Prompt '{prompt_name}'\")\n\n    async def on_list_tools(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time tool listing.\"\"\"\n        return await self._time_operation(context, call_next, \"List tools\")\n\n    async def on_list_resources(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time resource listing.\"\"\"\n        return await self._time_operation(context, call_next, \"List resources\")\n\n    async def on_list_resource_templates(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time resource template listing.\"\"\"\n        return await self._time_operation(context, call_next, \"List resource templates\")\n\n    async def on_list_prompts(\n        self, context: MiddlewareContext, call_next: CallNext\n    ) -> Any:\n        \"\"\"Time prompt listing.\"\"\"\n        return await self._time_operation(context, call_next, \"List prompts\")\n"
  },
  {
    "path": "src/fastmcp/server/middleware/tool_injection.py",
    "content": "\"\"\"A middleware for injecting tools into the MCP server context.\"\"\"\n\nimport warnings\nfrom collections.abc import Sequence\nfrom logging import Logger\nfrom typing import Annotated, Any\n\nimport mcp.types\nfrom mcp.types import Prompt\nfrom pydantic import AnyUrl\nfrom typing_extensions import override\n\nimport fastmcp\nfrom fastmcp.resources.base import ResourceResult\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.logging import get_logger\n\nlogger: Logger = get_logger(name=__name__)\n\n\nclass ToolInjectionMiddleware(Middleware):\n    \"\"\"A middleware for injecting tools into the context.\"\"\"\n\n    def __init__(self, tools: Sequence[Tool]):\n        \"\"\"Initialize the tool injection middleware.\"\"\"\n        self._tools_to_inject: Sequence[Tool] = tools\n        self._tools_to_inject_by_name: dict[str, Tool] = {\n            tool.name: tool for tool in tools\n        }\n\n    @override\n    async def on_list_tools(\n        self,\n        context: MiddlewareContext[mcp.types.ListToolsRequest],\n        call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]],\n    ) -> Sequence[Tool]:\n        \"\"\"Inject tools into the response.\"\"\"\n        return [*self._tools_to_inject, *await call_next(context)]\n\n    @override\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mcp.types.CallToolRequestParams],\n        call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        \"\"\"Intercept tool calls to injected tools.\"\"\"\n        if context.message.name in self._tools_to_inject_by_name:\n            tool = self._tools_to_inject_by_name[context.message.name]\n            return await tool.run(arguments=context.message.arguments or {})\n\n        return await call_next(context)\n\n\nasync def list_prompts(context: Context) -> list[Prompt]:\n    \"\"\"List prompts available on the server.\"\"\"\n    return await context.list_prompts()\n\n\nlist_prompts_tool = Tool.from_function(\n    fn=list_prompts,\n)\n\n\nasync def get_prompt(\n    context: Context,\n    name: Annotated[str, \"The name of the prompt to render.\"],\n    arguments: Annotated[\n        dict[str, Any] | None, \"The arguments to pass to the prompt.\"\n    ] = None,\n) -> mcp.types.GetPromptResult:\n    \"\"\"Render a prompt available on the server.\"\"\"\n    return await context.get_prompt(name=name, arguments=arguments)\n\n\nget_prompt_tool = Tool.from_function(\n    fn=get_prompt,\n)\n\n\nclass PromptToolMiddleware(ToolInjectionMiddleware):\n    \"\"\"A middleware for injecting prompts as tools into the context.\n\n    .. deprecated::\n        Use ``fastmcp.server.transforms.PromptsAsTools`` instead.\n    \"\"\"\n\n    def __init__(self) -> None:\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"PromptToolMiddleware is deprecated. Use the PromptsAsTools transform instead: \"\n                \"from fastmcp.server.transforms import PromptsAsTools\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        tools: list[Tool] = [list_prompts_tool, get_prompt_tool]\n        super().__init__(tools=tools)\n\n\nasync def list_resources(context: Context) -> list[mcp.types.Resource]:\n    \"\"\"List resources available on the server.\"\"\"\n    return await context.list_resources()\n\n\nlist_resources_tool = Tool.from_function(\n    fn=list_resources,\n)\n\n\nasync def read_resource(\n    context: Context,\n    uri: Annotated[AnyUrl | str, \"The URI of the resource to read.\"],\n) -> ResourceResult:\n    \"\"\"Read a resource available on the server.\"\"\"\n    return await context.read_resource(uri=uri)\n\n\nread_resource_tool = Tool.from_function(\n    fn=read_resource,\n)\n\n\nclass ResourceToolMiddleware(ToolInjectionMiddleware):\n    \"\"\"A middleware for injecting resources as tools into the context.\n\n    .. deprecated::\n        Use ``fastmcp.server.transforms.ResourcesAsTools`` instead.\n    \"\"\"\n\n    def __init__(self) -> None:\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"ResourceToolMiddleware is deprecated. Use the ResourcesAsTools transform instead: \"\n                \"from fastmcp.server.transforms import ResourcesAsTools\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        tools: list[Tool] = [list_resources_tool, read_resource_tool]\n        super().__init__(tools=tools)\n"
  },
  {
    "path": "src/fastmcp/server/mixins/__init__.py",
    "content": "\"\"\"Server mixins for FastMCP.\"\"\"\n\nfrom fastmcp.server.mixins.lifespan import LifespanMixin\nfrom fastmcp.server.mixins.mcp_operations import MCPOperationsMixin\nfrom fastmcp.server.mixins.transport import TransportMixin\n\n__all__ = [\"LifespanMixin\", \"MCPOperationsMixin\", \"TransportMixin\"]\n"
  },
  {
    "path": "src/fastmcp/server/mixins/lifespan.py",
    "content": "\"\"\"Lifespan and Docket task infrastructure for FastMCP Server.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport weakref\nfrom collections.abc import AsyncIterator\nfrom contextlib import AsyncExitStack, asynccontextmanager, suppress\nfrom typing import TYPE_CHECKING, Any\n\nimport anyio\nfrom uncalled_for import SharedContext\n\nimport fastmcp\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from docket import Docket\n\n    from fastmcp.server.server import FastMCP\n\nlogger = get_logger(__name__)\n\n\nclass LifespanMixin:\n    \"\"\"Mixin providing lifespan and Docket task infrastructure for FastMCP.\"\"\"\n\n    @property\n    def docket(self: FastMCP) -> Docket | None:\n        \"\"\"Get the Docket instance if Docket support is enabled.\n\n        Returns None if Docket is not enabled or server hasn't been started yet.\n        \"\"\"\n        return self._docket\n\n    @asynccontextmanager\n    async def _docket_lifespan(self: FastMCP) -> AsyncIterator[None]:\n        \"\"\"Manage Docket instance and Worker for background task execution.\n\n        Docket infrastructure is only initialized if:\n        1. pydocket is installed (fastmcp[tasks] extra)\n        2. There are task-enabled components (task_config.mode != 'forbidden')\n\n        This means users with pydocket installed but no task-enabled components\n        won't spin up Docket/Worker infrastructure.\n        \"\"\"\n        from fastmcp.server.dependencies import _current_server, is_docket_available\n\n        # Set FastMCP server in ContextVar so CurrentFastMCP can access it\n        # (use weakref to avoid reference cycles)\n        server_token = _current_server.set(weakref.ref(self))\n\n        try:\n            # If docket is not available, skip task infrastructure but still\n            # set up SharedContext so Shared() dependencies work.\n            if not is_docket_available():\n                async with SharedContext():\n                    yield\n                return\n\n            # Collect task-enabled components at startup with all transforms applied.\n            # Components must be available now to be registered with Docket workers;\n            # dynamically added components after startup won't be registered.\n            try:\n                task_components = list(await self.get_tasks())\n            except Exception as e:\n                logger.warning(f\"Failed to get tasks: {e}\")\n                if fastmcp.settings.mounted_components_raise_on_load_error:\n                    raise\n                task_components = []\n\n            # If no task-enabled components, skip Docket infrastructure but still\n            # set up SharedContext so Shared() dependencies work.\n            if not task_components:\n                async with SharedContext():\n                    yield\n                return\n\n            # Docket is available AND there are task-enabled components\n            from docket import Docket, Worker\n\n            from fastmcp import settings\n            from fastmcp.server.dependencies import (\n                _current_docket,\n                _current_worker,\n            )\n\n            # Create Docket instance using configured name and URL\n            async with Docket(\n                name=settings.docket.name,\n                url=settings.docket.url,\n            ) as docket:\n                # Store on server instance for cross-task access (FastMCPTransport)\n                self._docket = docket\n\n                # Register task-enabled components with Docket\n                for component in task_components:\n                    component.register_with_docket(docket)\n\n                # Set Docket in ContextVar so CurrentDocket can access it\n                docket_token = _current_docket.set(docket)\n                try:\n                    # Build worker kwargs from settings\n                    worker_kwargs: dict[str, Any] = {\n                        \"concurrency\": settings.docket.concurrency,\n                        \"redelivery_timeout\": settings.docket.redelivery_timeout,\n                        \"reconnection_delay\": settings.docket.reconnection_delay,\n                        \"minimum_check_interval\": settings.docket.minimum_check_interval,\n                    }\n                    if settings.docket.worker_name:\n                        worker_kwargs[\"name\"] = settings.docket.worker_name\n\n                    # Create and start Worker\n                    async with Worker(docket, **worker_kwargs) as worker:\n                        # Store on server instance for cross-context access\n                        self._worker = worker\n                        # Set Worker in ContextVar so CurrentWorker can access it\n                        worker_token = _current_worker.set(worker)\n                        try:\n                            worker_task = asyncio.create_task(worker.run_forever())\n                            try:\n                                yield\n                            finally:\n                                worker_task.cancel()\n                                with suppress(asyncio.CancelledError):\n                                    await worker_task\n                        finally:\n                            _current_worker.reset(worker_token)\n                            self._worker = None\n                finally:\n                    # Reset ContextVar\n                    _current_docket.reset(docket_token)\n                    # Clear instance attribute\n                    self._docket = None\n        finally:\n            # Reset server ContextVar\n            _current_server.reset(server_token)\n\n    @asynccontextmanager\n    async def _lifespan_manager(self: FastMCP) -> AsyncIterator[None]:\n        async with self._lifespan_lock:\n            if self._lifespan_result_set:\n                self._lifespan_ref_count += 1\n                should_enter_lifespan = False\n            else:\n                self._lifespan_ref_count = 1\n                should_enter_lifespan = True\n\n        if not should_enter_lifespan:\n            try:\n                yield\n            finally:\n                async with self._lifespan_lock:\n                    self._lifespan_ref_count -= 1\n                    if self._lifespan_ref_count == 0:\n                        self._lifespan_result_set = False\n                        self._lifespan_result = None\n            return\n\n        # Use an explicit AsyncExitStack so we can shield teardown from\n        # cancellation. Without this, Ctrl-C causes CancelledError to\n        # propagate into lifespan finally blocks, preventing any async\n        # cleanup (e.g. closing DB connections, flushing buffers).\n        stack = AsyncExitStack()\n        try:\n            user_lifespan_result = await stack.enter_async_context(self._lifespan(self))\n            await stack.enter_async_context(self._docket_lifespan())\n\n            self._lifespan_result = user_lifespan_result\n            self._lifespan_result_set = True\n\n            # Start lifespans for all providers\n            for provider in self.providers:\n                await stack.enter_async_context(provider.lifespan())\n\n            self._started.set()\n            try:\n                yield\n            finally:\n                self._started.clear()\n        finally:\n            try:\n                with anyio.CancelScope(shield=True):\n                    await stack.aclose()\n            finally:\n                async with self._lifespan_lock:\n                    self._lifespan_ref_count -= 1\n                    if self._lifespan_ref_count == 0:\n                        self._lifespan_result_set = False\n                        self._lifespan_result = None\n\n    def _setup_task_protocol_handlers(self: FastMCP) -> None:\n        \"\"\"Register SEP-1686 task protocol handlers with SDK.\n\n        Only registers handlers if docket is installed. Without docket,\n        task protocol requests will return \"method not found\" errors.\n        \"\"\"\n        from fastmcp.server.dependencies import is_docket_available\n\n        if not is_docket_available():\n            return\n\n        from mcp.types import (\n            CancelTaskRequest,\n            GetTaskPayloadRequest,\n            GetTaskRequest,\n            ListTasksRequest,\n            ServerResult,\n        )\n\n        from fastmcp.server.tasks.requests import (\n            tasks_cancel_handler,\n            tasks_get_handler,\n            tasks_list_handler,\n            tasks_result_handler,\n        )\n\n        # Manually register handlers (SDK decorators fail with locally-defined functions)\n        # SDK expects handlers that receive Request objects and return ServerResult\n\n        async def handle_get_task(req: GetTaskRequest) -> ServerResult:\n            params = req.params.model_dump(by_alias=True, exclude_none=True)\n            result = await tasks_get_handler(self, params)\n            return ServerResult(result)\n\n        async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:\n            params = req.params.model_dump(by_alias=True, exclude_none=True)\n            result = await tasks_result_handler(self, params)\n            return ServerResult(result)\n\n        async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:\n            params = (\n                req.params.model_dump(by_alias=True, exclude_none=True)\n                if req.params\n                else {}\n            )\n            result = await tasks_list_handler(self, params)\n            return ServerResult(result)\n\n        async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:\n            params = req.params.model_dump(by_alias=True, exclude_none=True)\n            result = await tasks_cancel_handler(self, params)\n            return ServerResult(result)\n\n        # Register directly with SDK (same as what decorators do internally)\n        self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task\n        self._mcp_server.request_handlers[GetTaskPayloadRequest] = (\n            handle_get_task_result\n        )\n        self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks\n        self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task\n"
  },
  {
    "path": "src/fastmcp/server/mixins/mcp_operations.py",
    "content": "\"\"\"MCP protocol handler setup and wire-format handlers for FastMCP Server.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable, Sequence\nfrom typing import TYPE_CHECKING, Any, TypeVar, cast\n\nimport mcp.types\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import ContentBlock\nfrom pydantic import AnyUrl\n\nfrom fastmcp.exceptions import DisabledError, NotFoundError\nfrom fastmcp.server.tasks.config import TaskMeta\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.pagination import paginate_sequence\nfrom fastmcp.utilities.versions import VersionSpec, dedupe_with_versions\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\nlogger = get_logger(__name__)\n\nPaginateT = TypeVar(\"PaginateT\")\n\n\ndef _apply_pagination(\n    items: Sequence[PaginateT],\n    cursor: str | None,\n    page_size: int | None,\n) -> tuple[list[PaginateT], str | None]:\n    \"\"\"Apply pagination to items, raising McpError for invalid cursors.\n\n    If page_size is None, returns all items without pagination.\n    \"\"\"\n    if page_size is None:\n        return list(items), None\n    try:\n        return paginate_sequence(items, cursor, page_size)\n    except ValueError as e:\n        raise McpError(mcp.types.ErrorData(code=-32602, message=str(e))) from e\n\n\nclass MCPOperationsMixin:\n    \"\"\"Mixin providing MCP protocol handler setup and wire-format handlers.\n\n    Note: Methods registered with SDK decorators (e.g., _list_tools_mcp, _call_tool_mcp)\n    cannot use `self: FastMCP` type hints because the SDK's `get_type_hints()` fails\n    to resolve FastMCP at runtime (it's only available under TYPE_CHECKING). When\n    type hints fail to resolve, the SDK falls back to calling handlers with no arguments.\n    These methods use untyped `self` to avoid this issue.\n    \"\"\"\n\n    def _setup_handlers(self: FastMCP) -> None:\n        \"\"\"Set up core MCP protocol handlers.\n\n        List handlers use SDK decorators that pass the request object to our handler\n        (needed for pagination cursor). The SDK also populates caches like _tool_cache.\n\n        Exception: list_resource_templates SDK decorator doesn't pass the request,\n        so we register that handler directly.\n\n        The call_tool decorator is from the SDK (supports CreateTaskResult + validate_input).\n        The read_resource and get_prompt decorators are from LowLevelServer to add\n        CreateTaskResult support until the SDK provides it natively.\n        \"\"\"\n        self._mcp_server.list_tools()(self._list_tools_mcp)\n        self._mcp_server.list_resources()(self._list_resources_mcp)\n        self._mcp_server.list_prompts()(self._list_prompts_mcp)\n\n        # list_resource_templates SDK decorator doesn't pass the request to handlers,\n        # so we register directly to get cursor access for pagination\n        self._mcp_server.request_handlers[mcp.types.ListResourceTemplatesRequest] = (\n            self._wrap_list_handler(self._list_resource_templates_mcp)\n        )\n\n        self._mcp_server.call_tool(validate_input=self.strict_input_validation)(\n            self._call_tool_mcp\n        )\n        self._mcp_server.read_resource()(self._read_resource_mcp)\n        self._mcp_server.get_prompt()(self._get_prompt_mcp)\n        self._mcp_server.set_logging_level()(self._set_logging_level_mcp)\n\n        # Register SEP-1686 task protocol handlers\n        self._setup_task_protocol_handlers()\n\n    def _wrap_list_handler(\n        self: FastMCP, handler: Callable[..., Awaitable[Any]]\n    ) -> Callable[..., Awaitable[mcp.types.ServerResult]]:\n        \"\"\"Wrap a list handler to pass the request and return ServerResult.\"\"\"\n\n        async def wrapper(request: Any) -> mcp.types.ServerResult:\n            result = await handler(request)\n            return mcp.types.ServerResult(result)\n\n        return wrapper\n\n    async def _list_tools_mcp(\n        self, request: mcp.types.ListToolsRequest\n    ) -> mcp.types.ListToolsResult:\n        \"\"\"\n        List all available tools, in the format expected by the low-level MCP\n        server. Supports pagination when list_page_size is configured.\n        \"\"\"\n        # Cast self to FastMCP for type checking (see class docstring for why\n        # we can't use `self: FastMCP` annotation on SDK-registered handlers)\n        server = cast(\"FastMCP\", self)\n        logger.debug(f\"[{server.name}] Handler called: list_tools\")\n\n        tools = dedupe_with_versions(list(await server.list_tools()), lambda t: t.name)\n        sdk_tools = [tool.to_mcp_tool(name=tool.name) for tool in tools]\n\n        # SDK may pass None for internal cache refresh despite type hint\n        cursor = (\n            request.params.cursor if request is not None and request.params else None\n        )\n        page, next_cursor = _apply_pagination(sdk_tools, cursor, server._list_page_size)\n        return mcp.types.ListToolsResult(tools=page, nextCursor=next_cursor)\n\n    async def _list_resources_mcp(\n        self, request: mcp.types.ListResourcesRequest\n    ) -> mcp.types.ListResourcesResult:\n        \"\"\"\n        List all available resources, in the format expected by the low-level MCP\n        server. Supports pagination when list_page_size is configured.\n        \"\"\"\n        server = cast(\"FastMCP\", self)\n        logger.debug(f\"[{server.name}] Handler called: list_resources\")\n\n        resources = dedupe_with_versions(\n            list(await server.list_resources()), lambda r: str(r.uri)\n        )\n        sdk_resources = [\n            resource.to_mcp_resource(uri=str(resource.uri)) for resource in resources\n        ]\n\n        cursor = request.params.cursor if request.params else None\n        page, next_cursor = _apply_pagination(\n            sdk_resources, cursor, server._list_page_size\n        )\n        return mcp.types.ListResourcesResult(resources=page, nextCursor=next_cursor)\n\n    async def _list_resource_templates_mcp(\n        self, request: mcp.types.ListResourceTemplatesRequest\n    ) -> mcp.types.ListResourceTemplatesResult:\n        \"\"\"\n        List all available resource templates, in the format expected by the low-level MCP\n        server. Supports pagination when list_page_size is configured.\n        \"\"\"\n        server = cast(\"FastMCP\", self)\n        logger.debug(f\"[{server.name}] Handler called: list_resource_templates\")\n\n        templates = dedupe_with_versions(\n            list(await server.list_resource_templates()), lambda t: t.uri_template\n        )\n        sdk_templates = [\n            template.to_mcp_template(uriTemplate=template.uri_template)\n            for template in templates\n        ]\n        cursor = request.params.cursor if request.params else None\n        page, next_cursor = _apply_pagination(\n            sdk_templates, cursor, server._list_page_size\n        )\n        return mcp.types.ListResourceTemplatesResult(\n            resourceTemplates=page, nextCursor=next_cursor\n        )\n\n    async def _list_prompts_mcp(\n        self, request: mcp.types.ListPromptsRequest\n    ) -> mcp.types.ListPromptsResult:\n        \"\"\"\n        List all available prompts, in the format expected by the low-level MCP\n        server. Supports pagination when list_page_size is configured.\n        \"\"\"\n        server = cast(\"FastMCP\", self)\n        logger.debug(f\"[{server.name}] Handler called: list_prompts\")\n\n        prompts = dedupe_with_versions(\n            list(await server.list_prompts()), lambda p: p.name\n        )\n        sdk_prompts = [prompt.to_mcp_prompt(name=prompt.name) for prompt in prompts]\n        cursor = request.params.cursor if request.params else None\n        page, next_cursor = _apply_pagination(\n            sdk_prompts, cursor, server._list_page_size\n        )\n        return mcp.types.ListPromptsResult(prompts=page, nextCursor=next_cursor)\n\n    async def _call_tool_mcp(\n        self, key: str, arguments: dict[str, Any]\n    ) -> (\n        list[ContentBlock]\n        | tuple[list[ContentBlock], dict[str, Any]]\n        | mcp.types.CallToolResult\n        | mcp.types.CreateTaskResult\n    ):\n        \"\"\"\n        Handle MCP 'callTool' requests.\n\n        Extracts task metadata from MCP request context and passes it explicitly\n        to call_tool(). The tool's _run() method handles the backgrounding decision,\n        ensuring middleware runs before Docket.\n\n        Args:\n            key: The name of the tool to call\n            arguments: Arguments to pass to the tool\n\n        Returns:\n            Tool result or CreateTaskResult for background execution\n        \"\"\"\n        server = cast(\"FastMCP\", self)\n        logger.debug(\n            f\"[{server.name}] Handler called: call_tool %s with %s\", key, arguments\n        )\n\n        try:\n            # Extract version and task metadata from request context.\n            # fn_key is set by call_tool() after finding the tool.\n            version_str: str | None = None\n            task_meta: TaskMeta | None = None\n            try:\n                ctx = server._mcp_server.request_context\n                # Extract version from request-level _meta.fastmcp.version\n                if ctx.meta:\n                    meta_dict = ctx.meta.model_dump(exclude_none=True)\n                    version_str = meta_dict.get(\"fastmcp\", {}).get(\"version\")\n                # Extract SEP-1686 task metadata\n                if ctx.experimental.is_task:\n                    mcp_task_meta = ctx.experimental.task_metadata\n                    task_meta_dict = mcp_task_meta.model_dump(exclude_none=True)\n                    task_meta = TaskMeta(ttl=task_meta_dict.get(\"ttl\"))\n            except (AttributeError, LookupError):\n                pass\n\n            version = VersionSpec(eq=version_str) if version_str else None\n            result = await server.call_tool(\n                key, arguments, version=version, task_meta=task_meta\n            )\n\n            if isinstance(result, mcp.types.CreateTaskResult):\n                return result\n            return result.to_mcp_result()\n\n        except DisabledError as e:\n            raise NotFoundError(f\"Unknown tool: {key!r}\") from e\n        except NotFoundError as e:\n            raise NotFoundError(f\"Unknown tool: {key!r}\") from e\n\n    async def _read_resource_mcp(\n        self, uri: AnyUrl | str\n    ) -> mcp.types.ReadResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Handle MCP 'readResource' requests.\n\n        Extracts task metadata from MCP request context and passes it explicitly\n        to read_resource(). The resource's _read() method handles the backgrounding\n        decision, ensuring middleware runs before Docket.\n\n        Args:\n            uri: The resource URI\n\n        Returns:\n            ReadResourceResult or CreateTaskResult for background execution\n        \"\"\"\n        server = cast(\"FastMCP\", self)\n        logger.debug(f\"[{server.name}] Handler called: read_resource %s\", uri)\n\n        try:\n            # Extract version and task metadata from request context.\n            version_str: str | None = None\n            task_meta: TaskMeta | None = None\n            try:\n                ctx = server._mcp_server.request_context\n                # Extract version from _meta.fastmcp.version if provided\n                if ctx.meta:\n                    meta_dict = ctx.meta.model_dump(exclude_none=True)\n                    fastmcp_meta = meta_dict.get(\"fastmcp\") or {}\n                    version_str = fastmcp_meta.get(\"version\")\n                # Extract SEP-1686 task metadata\n                if ctx.experimental.is_task:\n                    mcp_task_meta = ctx.experimental.task_metadata\n                    task_meta_dict = mcp_task_meta.model_dump(exclude_none=True)\n                    task_meta = TaskMeta(ttl=task_meta_dict.get(\"ttl\"))\n            except (AttributeError, LookupError):\n                pass\n\n            version = VersionSpec(eq=version_str) if version_str else None\n            result = await server.read_resource(\n                str(uri), version=version, task_meta=task_meta\n            )\n\n            if isinstance(result, mcp.types.CreateTaskResult):\n                return result\n            return result.to_mcp_result(uri)\n        except DisabledError as e:\n            raise McpError(\n                mcp.types.ErrorData(\n                    code=-32002, message=f\"Resource not found: {str(uri)!r}\"\n                )\n            ) from e\n        except NotFoundError as e:\n            raise McpError(\n                mcp.types.ErrorData(code=-32002, message=f\"Resource not found: {e}\")\n            ) from e\n\n    async def _get_prompt_mcp(\n        self, name: str, arguments: dict[str, Any] | None\n    ) -> mcp.types.GetPromptResult | mcp.types.CreateTaskResult:\n        \"\"\"Handle MCP 'getPrompt' requests.\n\n        Extracts task metadata from MCP request context and passes it explicitly\n        to render_prompt(). The prompt's _render() method handles the backgrounding\n        decision, ensuring middleware runs before Docket.\n\n        Args:\n            name: The prompt name\n            arguments: Prompt arguments\n\n        Returns:\n            GetPromptResult or CreateTaskResult for background execution\n        \"\"\"\n        server = cast(\"FastMCP\", self)\n        logger.debug(\n            f\"[{server.name}] Handler called: get_prompt %s with %s\", name, arguments\n        )\n\n        try:\n            # Extract version and task metadata from request context.\n            # fn_key is set by render_prompt() after finding the prompt.\n            version_str: str | None = None\n            task_meta: TaskMeta | None = None\n            try:\n                ctx = server._mcp_server.request_context\n                # Extract version from request-level _meta.fastmcp.version\n                if ctx.meta:\n                    meta_dict = ctx.meta.model_dump(exclude_none=True)\n                    version_str = meta_dict.get(\"fastmcp\", {}).get(\"version\")\n                # Extract SEP-1686 task metadata\n                if ctx.experimental.is_task:\n                    mcp_task_meta = ctx.experimental.task_metadata\n                    task_meta_dict = mcp_task_meta.model_dump(exclude_none=True)\n                    task_meta = TaskMeta(ttl=task_meta_dict.get(\"ttl\"))\n            except (AttributeError, LookupError):\n                pass\n\n            version = VersionSpec(eq=version_str) if version_str else None\n            result = await server.render_prompt(\n                name, arguments, version=version, task_meta=task_meta\n            )\n\n            if isinstance(result, mcp.types.CreateTaskResult):\n                return result\n            return result.to_mcp_prompt_result()\n        except DisabledError as e:\n            raise NotFoundError(f\"Unknown prompt: {name!r}\") from e\n        except NotFoundError:\n            raise\n\n    async def _set_logging_level_mcp(self, level: mcp.types.LoggingLevel) -> None:\n        \"\"\"Handle MCP 'logging/setLevel' requests.\n\n        Stores the requested minimum log level on the session so that\n        subsequent log messages below this level are suppressed.\n        \"\"\"\n        from fastmcp.server.low_level import MiddlewareServerSession\n\n        server = cast(\"FastMCP\", self)\n        logger.debug(f\"[{server.name}] Handler called: set_logging_level %s\", level)\n        try:\n            ctx = server._mcp_server.request_context\n            session = ctx.session\n            if isinstance(session, MiddlewareServerSession):\n                session._minimum_logging_level = level\n        except LookupError:\n            pass\n"
  },
  {
    "path": "src/fastmcp/server/mixins/transport.py",
    "content": "\"\"\"Transport-related methods for FastMCP Server.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Any, Literal\n\nimport anyio\nimport uvicorn\nfrom mcp.server.lowlevel.server import NotificationOptions\nfrom mcp.server.stdio import stdio_server\nfrom starlette.middleware import Middleware as ASGIMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import BaseRoute, Route\n\nimport fastmcp\nfrom fastmcp.server.event_store import EventStore\nfrom fastmcp.server.http import (\n    StarletteWithLifespan,\n    create_sse_app,\n    create_streamable_http_app,\n)\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.providers.fastmcp_provider import FastMCPProvider\nfrom fastmcp.server.providers.wrapped_provider import _WrappedProvider\nfrom fastmcp.utilities.cli import log_server_banner\nfrom fastmcp.utilities.logging import get_logger, temporary_log_level\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP, Transport\n\nlogger = get_logger(__name__)\n\n\nclass TransportMixin:\n    \"\"\"Mixin providing transport-related methods for FastMCP.\n\n    Includes HTTP/stdio/SSE transport handling and custom HTTP routes.\n    \"\"\"\n\n    async def run_async(\n        self: FastMCP,\n        transport: Transport | None = None,\n        show_banner: bool | None = None,\n        **transport_kwargs: Any,\n    ) -> None:\n        \"\"\"Run the FastMCP server asynchronously.\n\n        Args:\n            transport: Transport protocol to use (\"stdio\", \"http\", \"sse\", or \"streamable-http\")\n            show_banner: Whether to display the server banner. If None, uses the\n                FASTMCP_SHOW_SERVER_BANNER setting (default: True).\n        \"\"\"\n        if show_banner is None:\n            show_banner = fastmcp.settings.show_server_banner\n        if transport is None:\n            transport = fastmcp.settings.transport\n        if transport not in {\"stdio\", \"http\", \"sse\", \"streamable-http\"}:\n            raise ValueError(f\"Unknown transport: {transport}\")\n\n        if transport == \"stdio\":\n            await self.run_stdio_async(\n                show_banner=show_banner,\n                **transport_kwargs,\n            )\n        elif transport in {\"http\", \"sse\", \"streamable-http\"}:\n            await self.run_http_async(\n                transport=transport,\n                show_banner=show_banner,\n                **transport_kwargs,\n            )\n        else:\n            raise ValueError(f\"Unknown transport: {transport}\")\n\n    def run(\n        self: FastMCP,\n        transport: Transport | None = None,\n        show_banner: bool | None = None,\n        **transport_kwargs: Any,\n    ) -> None:\n        \"\"\"Run the FastMCP server. Note this is a synchronous function.\n\n        Args:\n            transport: Transport protocol to use (\"http\", \"stdio\", \"sse\", or \"streamable-http\")\n            show_banner: Whether to display the server banner. If None, uses the\n                FASTMCP_SHOW_SERVER_BANNER setting (default: True).\n        \"\"\"\n\n        anyio.run(\n            partial(\n                self.run_async,\n                transport,\n                show_banner=show_banner,\n                **transport_kwargs,\n            )\n        )\n\n    def custom_route(\n        self: FastMCP,\n        path: str,\n        methods: list[str],\n        name: str | None = None,\n        include_in_schema: bool = True,\n    ) -> Callable[\n        [Callable[[Request], Awaitable[Response]]],\n        Callable[[Request], Awaitable[Response]],\n    ]:\n        \"\"\"\n        Decorator to register a custom HTTP route on the FastMCP server.\n\n        Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,\n        which can be useful for OAuth callbacks, health checks, or admin APIs.\n        The handler function must be an async function that accepts a Starlette\n        Request and returns a Response.\n\n        Args:\n            path: URL path for the route (e.g., \"/auth/callback\")\n            methods: List of HTTP methods to support (e.g., [\"GET\", \"POST\"])\n            name: Optional name for the route (to reference this route with\n                Starlette's reverse URL lookup feature)\n            include_in_schema: Whether to include in OpenAPI schema, defaults to True\n\n        Example:\n            Register a custom HTTP route for a health check endpoint:\n            ```python\n            @server.custom_route(\"/health\", methods=[\"GET\"])\n            async def health_check(request: Request) -> Response:\n                return JSONResponse({\"status\": \"ok\"})\n            ```\n        \"\"\"\n\n        def decorator(\n            fn: Callable[[Request], Awaitable[Response]],\n        ) -> Callable[[Request], Awaitable[Response]]:\n            self._additional_http_routes.append(\n                Route(\n                    path,\n                    endpoint=fn,\n                    methods=methods,\n                    name=name,\n                    include_in_schema=include_in_schema,\n                )\n            )\n            return fn\n\n        return decorator\n\n    def _get_additional_http_routes(self: FastMCP) -> list[BaseRoute]:\n        \"\"\"Get all additional HTTP routes including from mounted servers.\n\n        Collects custom HTTP routes registered via ``@server.custom_route()``\n        from this server **and** from any FastMCP servers reachable through\n        mounted providers (recursively).  This ensures that routes defined on\n        a child server are forwarded to the parent's HTTP app when using\n        ``server.mount(child)``.\n\n        Note:\n            When path collisions occur between a parent and a mounted child,\n            the parent's routes take precedence because they appear first in\n            the returned list.\n\n        Returns:\n            List of Starlette Route objects\n        \"\"\"\n        routes: list[BaseRoute] = list(self._additional_http_routes)\n\n        def _unwrap_provider(provider: Provider) -> Provider:\n            \"\"\"Unwrap _WrappedProvider layers to find the inner provider.\"\"\"\n            while isinstance(provider, _WrappedProvider):\n                provider = provider._inner\n            return provider\n\n        for provider in self.providers:\n            inner = _unwrap_provider(provider)\n            if isinstance(inner, FastMCPProvider):\n                # Recurse into the mounted server to collect its routes\n                # (and any routes from servers mounted on *it*).\n                routes.extend(inner.server._get_additional_http_routes())\n\n        return routes\n\n    async def run_stdio_async(\n        self: FastMCP,\n        show_banner: bool = True,\n        log_level: str | None = None,\n        stateless: bool = False,\n    ) -> None:\n        \"\"\"Run the server using stdio transport.\n\n        Args:\n            show_banner: Whether to display the server banner\n            log_level: Log level for the server\n            stateless: Whether to run in stateless mode (no session initialization)\n        \"\"\"\n        from fastmcp.server.context import reset_transport, set_transport\n\n        # Display server banner\n        if show_banner:\n            log_server_banner(server=self)\n\n        token = set_transport(\"stdio\")\n        try:\n            with temporary_log_level(log_level):\n                async with self._lifespan_manager():\n                    async with stdio_server() as (read_stream, write_stream):\n                        mode = \" (stateless)\" if stateless else \"\"\n                        logger.info(\n                            f\"Starting MCP server {self.name!r} with transport 'stdio'{mode}\"\n                        )\n\n                        await self._mcp_server.run(\n                            read_stream,\n                            write_stream,\n                            self._mcp_server.create_initialization_options(\n                                notification_options=NotificationOptions(\n                                    tools_changed=True\n                                ),\n                            ),\n                            stateless=stateless,\n                        )\n        finally:\n            reset_transport(token)\n\n    async def run_http_async(\n        self: FastMCP,\n        show_banner: bool = True,\n        transport: Literal[\"http\", \"streamable-http\", \"sse\"] = \"http\",\n        host: str | None = None,\n        port: int | None = None,\n        log_level: str | None = None,\n        path: str | None = None,\n        uvicorn_config: dict[str, Any] | None = None,\n        middleware: list[ASGIMiddleware] | None = None,\n        json_response: bool | None = None,\n        stateless_http: bool | None = None,\n        stateless: bool | None = None,\n    ) -> None:\n        \"\"\"Run the server using HTTP transport.\n\n        Args:\n            transport: Transport protocol to use - \"http\" (default), \"streamable-http\", or \"sse\"\n            host: Host address to bind to (defaults to settings.host)\n            port: Port to bind to (defaults to settings.port)\n            log_level: Log level for the server (defaults to settings.log_level)\n            path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)\n            uvicorn_config: Additional configuration for the Uvicorn server\n            middleware: A list of middleware to apply to the app\n            json_response: Whether to use JSON response format (defaults to settings.json_response)\n            stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)\n            stateless: Alias for stateless_http for CLI consistency\n        \"\"\"\n        # Allow stateless as alias for stateless_http\n        if stateless is not None and stateless_http is None:\n            stateless_http = stateless\n\n        # Resolve from settings/env var if not explicitly set\n        if stateless_http is None:\n            stateless_http = fastmcp.settings.stateless_http\n\n        # SSE doesn't support stateless mode\n        if stateless_http and transport == \"sse\":\n            raise ValueError(\"SSE transport does not support stateless mode\")\n\n        host = host or fastmcp.settings.host\n        port = port or fastmcp.settings.port\n        default_log_level_to_use = (log_level or fastmcp.settings.log_level).lower()\n\n        app = self.http_app(\n            path=path,\n            transport=transport,\n            middleware=middleware,\n            json_response=json_response,\n            stateless_http=stateless_http,\n        )\n\n        # Display server banner\n        if show_banner:\n            log_server_banner(server=self)\n        uvicorn_config_from_user = uvicorn_config or {}\n\n        config_kwargs: dict[str, Any] = {\n            \"timeout_graceful_shutdown\": 2,\n            \"lifespan\": \"on\",\n            \"ws\": \"websockets-sansio\",\n        }\n        config_kwargs.update(uvicorn_config_from_user)\n\n        if \"log_config\" not in config_kwargs and \"log_level\" not in config_kwargs:\n            config_kwargs[\"log_level\"] = default_log_level_to_use\n\n        with temporary_log_level(log_level):\n            async with self._lifespan_manager():\n                config = uvicorn.Config(app, host=host, port=port, **config_kwargs)\n                server = uvicorn.Server(config)\n                path = getattr(app.state, \"path\", \"\").lstrip(\"/\")\n                mode = \" (stateless)\" if stateless_http else \"\"\n                logger.info(\n                    f\"Starting MCP server {self.name!r} with transport {transport!r}{mode} on http://{host}:{port}/{path}\"\n                )\n\n                await server.serve()\n\n    def http_app(\n        self: FastMCP,\n        path: str | None = None,\n        middleware: list[ASGIMiddleware] | None = None,\n        json_response: bool | None = None,\n        stateless_http: bool | None = None,\n        transport: Literal[\"http\", \"streamable-http\", \"sse\"] = \"http\",\n        event_store: EventStore | None = None,\n        retry_interval: int | None = None,\n    ) -> StarletteWithLifespan:\n        \"\"\"Create a Starlette app using the specified HTTP transport.\n\n        Args:\n            path: The path for the HTTP endpoint\n            middleware: A list of middleware to apply to the app\n            json_response: Whether to use JSON response format\n            stateless_http: Whether to use stateless mode (new transport per request)\n            transport: Transport protocol to use - \"http\", \"streamable-http\", or \"sse\"\n            event_store: Optional event store for SSE polling/resumability. When set,\n                enables clients to reconnect and resume receiving events after\n                server-initiated disconnections. Only used with streamable-http transport.\n            retry_interval: Optional retry interval in milliseconds for SSE polling.\n                Controls how quickly clients should reconnect after server-initiated\n                disconnections. Requires event_store to be set. Only used with\n                streamable-http transport.\n\n        Returns:\n            A Starlette application configured with the specified transport\n        \"\"\"\n\n        if transport in (\"streamable-http\", \"http\"):\n            return create_streamable_http_app(\n                server=self,\n                streamable_http_path=path or fastmcp.settings.streamable_http_path,\n                event_store=event_store,\n                retry_interval=retry_interval,\n                auth=self.auth,\n                json_response=(\n                    json_response\n                    if json_response is not None\n                    else fastmcp.settings.json_response\n                ),\n                stateless_http=(\n                    stateless_http\n                    if stateless_http is not None\n                    else fastmcp.settings.stateless_http\n                ),\n                debug=fastmcp.settings.debug,\n                middleware=middleware,\n            )\n        elif transport == \"sse\":\n            return create_sse_app(\n                server=self,\n                message_path=fastmcp.settings.message_path,\n                sse_path=path or fastmcp.settings.sse_path,\n                auth=self.auth,\n                debug=fastmcp.settings.debug,\n                middleware=middleware,\n            )\n        else:\n            raise ValueError(f\"Unknown transport: {transport}\")\n"
  },
  {
    "path": "src/fastmcp/server/openapi/__init__.py",
    "content": "\"\"\"OpenAPI server implementation for FastMCP.\n\n.. deprecated::\n    This module is deprecated. Import from fastmcp.server.providers.openapi instead.\n\nThe recommended approach is to use OpenAPIProvider with FastMCP:\n\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.openapi import OpenAPIProvider\n    import httpx\n\n    client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n    provider = OpenAPIProvider(openapi_spec=spec, client=client)\n\n    mcp = FastMCP(\"My API Server\")\n    mcp.add_provider(provider)\n\nFastMCPOpenAPI is still available but deprecated.\n\"\"\"\n\nimport warnings\n\nwarnings.warn(\n    \"fastmcp.server.openapi is deprecated. \"\n    \"Import from fastmcp.server.providers.openapi instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\n# Re-export from new canonical location\nfrom fastmcp.server.providers.openapi import (  # noqa: E402\n    ComponentFn as ComponentFn,\n    MCPType as MCPType,\n    OpenAPIProvider as OpenAPIProvider,\n    OpenAPIResource as OpenAPIResource,\n    OpenAPIResourceTemplate as OpenAPIResourceTemplate,\n    OpenAPITool as OpenAPITool,\n    RouteMap as RouteMap,\n    RouteMapFn as RouteMapFn,\n)\n\n# Keep FastMCPOpenAPI for backwards compat (it has its own deprecation warning)\nfrom fastmcp.server.openapi.server import FastMCPOpenAPI as FastMCPOpenAPI  # noqa: E402\n\n__all__ = [\n    \"ComponentFn\",\n    \"FastMCPOpenAPI\",\n    \"MCPType\",\n    \"OpenAPIProvider\",\n    \"OpenAPIResource\",\n    \"OpenAPIResourceTemplate\",\n    \"OpenAPITool\",\n    \"RouteMap\",\n    \"RouteMapFn\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/openapi/components.py",
    "content": "\"\"\"OpenAPI component implementations - backwards compatibility stub.\n\nThis module is deprecated. Import from fastmcp.server.providers.openapi instead.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport warnings\n\nwarnings.warn(\n    \"fastmcp.server.openapi.components is deprecated. \"\n    \"Import from fastmcp.server.providers.openapi instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\nfrom fastmcp.server.providers.openapi import (  # noqa: E402\n    OpenAPIResource,\n    OpenAPIResourceTemplate,\n    OpenAPITool,\n)\n\n# Export public symbols\n__all__ = [\n    \"OpenAPIResource\",\n    \"OpenAPIResourceTemplate\",\n    \"OpenAPITool\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/openapi/routing.py",
    "content": "\"\"\"Route mapping logic for OpenAPI operations.\n\n.. deprecated::\n    This module is deprecated. Import from fastmcp.server.providers.openapi instead.\n\"\"\"\n\n# ruff: noqa: E402\n\nimport warnings\n\n# Backwards compatibility - export everything that was previously public\n__all__ = [\n    \"DEFAULT_ROUTE_MAPPINGS\",\n    \"ComponentFn\",\n    \"MCPType\",\n    \"RouteMap\",\n    \"RouteMapFn\",\n    \"_determine_route_type\",\n]\n\nwarnings.warn(\n    \"fastmcp.server.openapi.routing is deprecated. \"\n    \"Import from fastmcp.server.providers.openapi instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\n# Re-export from new canonical location\nfrom fastmcp.server.providers.openapi.routing import (\n    DEFAULT_ROUTE_MAPPINGS as DEFAULT_ROUTE_MAPPINGS,\n)\nfrom fastmcp.server.providers.openapi.routing import (\n    ComponentFn as ComponentFn,\n)\nfrom fastmcp.server.providers.openapi.routing import (\n    MCPType as MCPType,\n)\nfrom fastmcp.server.providers.openapi.routing import (\n    RouteMap as RouteMap,\n)\nfrom fastmcp.server.providers.openapi.routing import (\n    RouteMapFn as RouteMapFn,\n)\nfrom fastmcp.server.providers.openapi.routing import (\n    _determine_route_type as _determine_route_type,\n)\n"
  },
  {
    "path": "src/fastmcp/server/openapi/server.py",
    "content": "\"\"\"FastMCPOpenAPI - backwards compatibility wrapper.\n\nThis class is deprecated. Use FastMCP with OpenAPIProvider instead:\n\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.openapi import OpenAPIProvider\n    import httpx\n\n    client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n    provider = OpenAPIProvider(openapi_spec=spec, client=client)\n    mcp = FastMCP(\"My API Server\", providers=[provider])\n\"\"\"\n\nfrom __future__ import annotations\n\nimport warnings\nfrom typing import Any\n\nimport httpx\n\nfrom fastmcp.server.providers.openapi import (\n    ComponentFn,\n    OpenAPIProvider,\n    RouteMap,\n    RouteMapFn,\n)\nfrom fastmcp.server.server import FastMCP\n\n\nclass FastMCPOpenAPI(FastMCP):\n    \"\"\"FastMCP server implementation that creates components from an OpenAPI schema.\n\n    .. deprecated::\n        Use FastMCP with OpenAPIProvider instead. This class will be\n        removed in a future version.\n\n    Example (deprecated):\n        ```python\n        from fastmcp.server.openapi import FastMCPOpenAPI\n        import httpx\n\n        server = FastMCPOpenAPI(\n            openapi_spec=spec,\n            client=httpx.AsyncClient(),\n        )\n        ```\n\n    New approach:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.providers.openapi import OpenAPIProvider\n        import httpx\n\n        client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n        provider = OpenAPIProvider(openapi_spec=spec, client=client)\n        mcp = FastMCP(\"API Server\", providers=[provider])\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        openapi_spec: dict[str, Any],\n        client: httpx.AsyncClient | None = None,\n        name: str | None = None,\n        route_maps: list[RouteMap] | None = None,\n        route_map_fn: RouteMapFn | None = None,\n        mcp_component_fn: ComponentFn | None = None,\n        mcp_names: dict[str, str] | None = None,\n        tags: set[str] | None = None,\n        **settings: Any,\n    ):\n        \"\"\"Initialize a FastMCP server from an OpenAPI schema.\n\n        .. deprecated::\n            Use FastMCP with OpenAPIProvider instead.\n\n        Args:\n            openapi_spec: OpenAPI schema as a dictionary\n            client: Optional httpx AsyncClient for making HTTP requests.\n                If not provided, a default client is created from the spec.\n            name: Optional name for the server\n            route_maps: Optional list of RouteMap objects defining route mappings\n            route_map_fn: Optional callable for advanced route type mapping\n            mcp_component_fn: Optional callable for component customization\n            mcp_names: Optional dictionary mapping operationId to component names\n            tags: Optional set of tags to add to all components\n            **settings: Additional settings for FastMCP\n        \"\"\"\n        warnings.warn(\n            \"FastMCPOpenAPI is deprecated. Use FastMCP with OpenAPIProvider instead:\\n\"\n            \"    provider = OpenAPIProvider(openapi_spec=spec, client=client)\\n\"\n            \"    mcp = FastMCP('name', providers=[provider])\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n\n        super().__init__(name=name or \"OpenAPI FastMCP\", **settings)\n\n        # Store references for backwards compatibility\n        self._client = client\n        self._mcp_component_fn = mcp_component_fn\n\n        # Create provider with the client\n        provider = OpenAPIProvider(\n            openapi_spec=openapi_spec,\n            client=client,\n            route_maps=route_maps,\n            route_map_fn=route_map_fn,\n            mcp_component_fn=mcp_component_fn,\n            mcp_names=mcp_names,\n            tags=tags,\n        )\n\n        self.add_provider(provider)\n\n        # Expose internal attributes for backwards compatibility\n        self._spec = provider._spec\n        self._director = provider._director\n\n\n# Export public symbols\n__all__ = [\n    \"FastMCPOpenAPI\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/providers/__init__.py",
    "content": "\"\"\"Providers for dynamic MCP components.\n\nThis module provides the `Provider` abstraction for providing tools,\nresources, and prompts dynamically at runtime.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.providers import Provider\n    from fastmcp.tools import Tool\n\n    class DatabaseProvider(Provider):\n        def __init__(self, db_url: str):\n            self.db = Database(db_url)\n\n        async def _list_tools(self) -> list[Tool]:\n            rows = await self.db.fetch(\"SELECT * FROM tools\")\n            return [self._make_tool(row) for row in rows]\n\n        async def _get_tool(self, name: str) -> Tool | None:\n            row = await self.db.fetchone(\"SELECT * FROM tools WHERE name = ?\", name)\n            return self._make_tool(row) if row else None\n\n    mcp = FastMCP(\"Server\", providers=[DatabaseProvider(db_url)])\n    ```\n\"\"\"\n\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.server.providers.aggregate import AggregateProvider\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.providers.fastmcp_provider import FastMCPProvider\nfrom fastmcp.server.providers.filesystem import FileSystemProvider\nfrom fastmcp.server.providers.local_provider import LocalProvider\nfrom fastmcp.server.providers.skills import (\n    ClaudeSkillsProvider,\n    SkillProvider,\n    SkillsDirectoryProvider,\n    SkillsProvider,\n)\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.openapi import OpenAPIProvider as OpenAPIProvider\n    from fastmcp.server.providers.proxy import ProxyProvider as ProxyProvider\n\n__all__ = [\n    \"AggregateProvider\",\n    \"ClaudeSkillsProvider\",\n    \"FastMCPProvider\",\n    \"FileSystemProvider\",\n    \"LocalProvider\",\n    \"OpenAPIProvider\",\n    \"Provider\",\n    \"ProxyProvider\",\n    \"SkillProvider\",\n    \"SkillsDirectoryProvider\",\n    \"SkillsProvider\",  # Backwards compatibility alias for SkillsDirectoryProvider\n]\n\n\ndef __getattr__(name: str):\n    \"\"\"Lazy import for providers to avoid circular imports.\"\"\"\n    if name == \"ProxyProvider\":\n        from fastmcp.server.providers.proxy import ProxyProvider\n\n        return ProxyProvider\n    if name == \"OpenAPIProvider\":\n        from fastmcp.server.providers.openapi import OpenAPIProvider\n\n        return OpenAPIProvider\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n"
  },
  {
    "path": "src/fastmcp/server/providers/aggregate.py",
    "content": "\"\"\"AggregateProvider for combining multiple providers into one.\n\nThis module provides `AggregateProvider`, a utility class that presents\nmultiple providers as a single unified provider. Useful when you want to\ncombine custom providers without creating a full FastMCP server.\n\nExample:\n    ```python\n    from fastmcp.server.providers import AggregateProvider\n\n    # Combine multiple providers into one\n    combined = AggregateProvider()\n    combined.add_provider(provider1)\n    combined.add_provider(provider2, namespace=\"api\")  # Tools become \"api_foo\"\n\n    # Use like any other provider\n    tools = await combined.list_tools()\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import AsyncIterator, Sequence\nfrom contextlib import AsyncExitStack, asynccontextmanager\nfrom typing import TYPE_CHECKING, TypeVar\n\nfrom fastmcp.exceptions import NotFoundError\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.transforms import Namespace\nfrom fastmcp.utilities.async_utils import gather\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.versions import VersionSpec, version_sort_key\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.tools.base import Tool\n\nlogger = logging.getLogger(__name__)\n\nT = TypeVar(\"T\")\n\n\nclass AggregateProvider(Provider):\n    \"\"\"Utility provider that combines multiple providers into one.\n\n    Components are aggregated from all providers. For get_* operations,\n    providers are queried in parallel and the highest version is returned.\n\n    When adding providers with a namespace, wrap_transform() is used to apply\n    the Namespace transform. This means namespace transformation is handled\n    by the wrapped provider, not by AggregateProvider.\n\n    Errors from individual providers are logged and skipped (graceful degradation).\n\n    Example:\n        ```python\n        combined = AggregateProvider()\n        combined.add_provider(db_provider)\n        combined.add_provider(api_provider, namespace=\"api\")\n        # db_provider's tools keep original names\n        # api_provider's tools become \"api_foo\", \"api_bar\", etc.\n        ```\n    \"\"\"\n\n    def __init__(self, providers: Sequence[Provider] | None = None) -> None:\n        \"\"\"Initialize with an optional sequence of providers.\n\n        Args:\n            providers: Optional initial providers (without namespacing).\n                For namespaced providers, use add_provider() instead.\n        \"\"\"\n        super().__init__()\n        self.providers: list[Provider] = list(providers or [])\n\n    def add_provider(self, provider: Provider, *, namespace: str = \"\") -> None:\n        \"\"\"Add a provider with optional namespace.\n\n        If the provider is a FastMCP server, it's automatically wrapped in\n        FastMCPProvider to ensure middleware is invoked correctly.\n\n        Args:\n            provider: The provider to add.\n            namespace: Optional namespace prefix. When set:\n                - Tools become \"namespace_toolname\"\n                - Resources become \"protocol://namespace/path\"\n                - Prompts become \"namespace_promptname\"\n        \"\"\"\n        # Import here to avoid circular imports\n        from fastmcp.server.server import FastMCP\n\n        # Auto-wrap FastMCP servers to ensure middleware is invoked\n        if isinstance(provider, FastMCP):\n            from fastmcp.server.providers.fastmcp_provider import FastMCPProvider\n\n            provider = FastMCPProvider(provider)\n\n        # Apply namespace via wrap_transform if specified\n        if namespace:\n            provider = provider.wrap_transform(Namespace(namespace))\n\n        self.providers.append(provider)\n\n    def _collect_list_results(\n        self, results: list[Sequence[T] | BaseException], operation: str\n    ) -> list[T]:\n        \"\"\"Collect successful list results, logging any exceptions.\"\"\"\n        collected: list[T] = []\n        for i, result in enumerate(results):\n            if isinstance(result, BaseException):\n                logger.debug(\n                    f\"Error during {operation} from provider \"\n                    f\"{self.providers[i]}: {result}\"\n                )\n                continue\n            collected.extend(result)\n        return collected\n\n    def _get_highest_version_result(\n        self,\n        results: list[FastMCPComponent | None | BaseException],\n        operation: str,\n    ) -> FastMCPComponent | None:\n        \"\"\"Get the highest version from successful non-None results.\n\n        Used for versioned components where we want the highest version\n        across all providers rather than the first match.\n        \"\"\"\n        valid: list[FastMCPComponent] = []\n        for i, result in enumerate(results):\n            if isinstance(result, BaseException):\n                if not isinstance(result, NotFoundError):\n                    logger.debug(\n                        f\"Error during {operation} from provider \"\n                        f\"{self.providers[i]}: {result}\"\n                    )\n                continue\n            if result is not None:\n                valid.append(result)\n        if not valid:\n            return None\n        return max(valid, key=version_sort_key)\n\n    def __repr__(self) -> str:\n        return f\"AggregateProvider(providers={self.providers!r})\"\n\n    # -------------------------------------------------------------------------\n    # Tools\n    # -------------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"List all tools from all providers.\"\"\"\n        results = await gather(\n            *[p.list_tools() for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._collect_list_results(results, \"list_tools\")\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get tool by name from providers.\"\"\"\n        results = await gather(\n            *[p.get_tool(name, version) for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._get_highest_version_result(results, f\"get_tool({name!r})\")  # type: ignore[return-value]\n\n    # -------------------------------------------------------------------------\n    # Resources\n    # -------------------------------------------------------------------------\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"List all resources from all providers.\"\"\"\n        results = await gather(\n            *[p.list_resources() for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._collect_list_results(results, \"list_resources\")\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get resource by URI from providers.\"\"\"\n        results = await gather(\n            *[p.get_resource(uri, version) for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._get_highest_version_result(results, f\"get_resource({uri!r})\")  # type: ignore[return-value]\n\n    # -------------------------------------------------------------------------\n    # Resource Templates\n    # -------------------------------------------------------------------------\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"List all resource templates from all providers.\"\"\"\n        results = await gather(\n            *[p.list_resource_templates() for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._collect_list_results(results, \"list_resource_templates\")\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get resource template by URI from providers.\"\"\"\n        results = await gather(\n            *[p.get_resource_template(uri, version) for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._get_highest_version_result(\n            list(results), f\"get_resource_template({uri!r})\"\n        )  # type: ignore[return-value]\n\n    # -------------------------------------------------------------------------\n    # Prompts\n    # -------------------------------------------------------------------------\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"List all prompts from all providers.\"\"\"\n        results = await gather(\n            *[p.list_prompts() for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._collect_list_results(results, \"list_prompts\")\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get prompt by name from providers.\"\"\"\n        results = await gather(\n            *[p.get_prompt(name, version) for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._get_highest_version_result(results, f\"get_prompt({name!r})\")  # type: ignore[return-value]\n\n    # -------------------------------------------------------------------------\n    # Tasks\n    # -------------------------------------------------------------------------\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Get all task-eligible components from all providers.\"\"\"\n        results = await gather(\n            *[p.get_tasks() for p in self.providers],\n            return_exceptions=True,\n        )\n        return self._collect_list_results(results, \"get_tasks\")\n\n    # -------------------------------------------------------------------------\n    # Lifecycle\n    # -------------------------------------------------------------------------\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        \"\"\"Combine lifespans of all providers.\"\"\"\n        async with AsyncExitStack() as stack:\n            for p in self.providers:\n                await stack.enter_async_context(p.lifespan())\n            yield\n"
  },
  {
    "path": "src/fastmcp/server/providers/base.py",
    "content": "\"\"\"Base Provider class for dynamic MCP components.\n\nThis module provides the `Provider` abstraction for providing tools,\nresources, and prompts dynamically at runtime.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.providers import Provider\n    from fastmcp.tools import Tool\n\n    class DatabaseProvider(Provider):\n        def __init__(self, db_url: str):\n            super().__init__()\n            self.db = Database(db_url)\n\n        async def _list_tools(self) -> list[Tool]:\n            rows = await self.db.fetch(\"SELECT * FROM tools\")\n            return [self._make_tool(row) for row in rows]\n\n        async def _get_tool(self, name: str) -> Tool | None:\n            row = await self.db.fetchone(\"SELECT * FROM tools WHERE name = ?\", name)\n            return self._make_tool(row) if row else None\n\n    mcp = FastMCP(\"Server\", providers=[DatabaseProvider(db_url)])\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncIterator, Sequence\nfrom contextlib import asynccontextmanager\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Literal, cast\n\nfrom typing_extensions import Self\n\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.transforms.visibility import Visibility\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.async_utils import gather\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.versions import VersionSpec, version_sort_key\n\nif TYPE_CHECKING:\n    from fastmcp.server.transforms import Transform\n\n\nclass Provider:\n    \"\"\"Base class for dynamic component providers.\n\n    Subclass and override whichever methods you need. Default implementations\n    return empty lists / None, so you only need to implement what your provider\n    supports.\n\n    Provider semantics:\n        - Return `None` from `get_*` methods to indicate \"I don't have it\" (search continues)\n        - Static components (registered via decorators) always take precedence over providers\n        - Providers are queried in registration order; first non-None wins\n        - Components execute themselves via run()/read()/render() - providers just source them\n\n    Error handling:\n        - `list_*` methods: Errors are logged and the provider returns empty (graceful degradation).\n          This allows other providers to still contribute their components.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._transforms: list[Transform] = []\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\n    @property\n    def transforms(self) -> list[Transform]:\n        \"\"\"All transforms applied to components from this provider.\"\"\"\n        return list(self._transforms)\n\n    def add_transform(self, transform: Transform) -> None:\n        \"\"\"Add a transform to this provider.\n\n        Transforms modify components (tools, resources, prompts) as they flow\n        through the provider. They're applied in order - first added is innermost.\n\n        Args:\n            transform: The transform to add.\n\n        Example:\n            ```python\n            from fastmcp.server.transforms import Namespace\n\n            provider = MyProvider()\n            provider.add_transform(Namespace(\"api\"))\n            # Tools become \"api_toolname\"\n            ```\n        \"\"\"\n        self._transforms.append(transform)\n\n    def wrap_transform(self, transform: Transform) -> Provider:\n        \"\"\"Return a new provider with this transform applied (immutable).\n\n        Unlike add_transform() which mutates this provider, wrap_transform()\n        returns a new provider that wraps this one. The original provider\n        is unchanged.\n\n        This is useful when you want to apply transforms without side effects,\n        such as adding the same provider to multiple aggregators with different\n        namespaces.\n\n        Args:\n            transform: The transform to apply.\n\n        Returns:\n            A new provider that wraps this one with the transform applied.\n\n        Example:\n            ```python\n            from fastmcp.server.transforms import Namespace\n\n            provider = MyProvider()\n            namespaced = provider.wrap_transform(Namespace(\"api\"))\n            # provider is unchanged\n            # namespaced returns tools as \"api_toolname\"\n            ```\n        \"\"\"\n        # Import here to avoid circular imports\n        from fastmcp.server.providers.wrapped_provider import _WrappedProvider\n\n        return _WrappedProvider(self, transform)\n\n    # -------------------------------------------------------------------------\n    # Internal transform chain building\n    # -------------------------------------------------------------------------\n\n    async def list_tools(self) -> Sequence[Tool]:\n        \"\"\"List tools with all transforms applied.\n\n        Applies transforms sequentially: base → transforms (in order).\n        Each transform receives the result from the previous transform.\n        Components may be marked as disabled but are NOT filtered here -\n        filtering happens at the server level to allow session transforms to override.\n\n        Returns:\n            Transformed sequence of tools (including disabled ones).\n        \"\"\"\n        tools = await self._list_tools()\n        for transform in self.transforms:\n            tools = await transform.list_tools(tools)\n        return tools\n\n    async def get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get tool by transformed name with all transforms applied.\n\n        Note: This method does NOT filter disabled components. The Server\n        (FastMCP) performs enabled filtering after all transforms complete,\n        allowing session-level transforms to override provider-level disables.\n\n        Args:\n            name: The transformed tool name to look up.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The tool if found (may be marked disabled), None if not found.\n        \"\"\"\n\n        async def base(n: str, version: VersionSpec | None = None) -> Tool | None:\n            return await self._get_tool(n, version)\n\n        chain = base\n        for transform in self.transforms:\n            chain = partial(transform.get_tool, call_next=chain)\n\n        return await chain(name, version=version)\n\n    async def list_resources(self) -> Sequence[Resource]:\n        \"\"\"List resources with all transforms applied.\n\n        Components may be marked as disabled but are NOT filtered here.\n        \"\"\"\n        resources = await self._list_resources()\n        for transform in self.transforms:\n            resources = await transform.list_resources(resources)\n        return resources\n\n    async def get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get resource by transformed URI with all transforms applied.\n\n        Note: This method does NOT filter disabled components. The Server\n        (FastMCP) performs enabled filtering after all transforms complete.\n\n        Args:\n            uri: The transformed resource URI to look up.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The resource if found (may be marked disabled), None if not found.\n        \"\"\"\n\n        async def base(u: str, version: VersionSpec | None = None) -> Resource | None:\n            return await self._get_resource(u, version)\n\n        chain = base\n        for transform in self.transforms:\n            chain = partial(transform.get_resource, call_next=chain)\n\n        return await chain(uri, version=version)\n\n    async def list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"List resource templates with all transforms applied.\n\n        Components may be marked as disabled but are NOT filtered here.\n        \"\"\"\n        templates = await self._list_resource_templates()\n        for transform in self.transforms:\n            templates = await transform.list_resource_templates(templates)\n        return templates\n\n    async def get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get resource template by transformed URI with all transforms applied.\n\n        Note: This method does NOT filter disabled components. The Server\n        (FastMCP) performs enabled filtering after all transforms complete.\n\n        Args:\n            uri: The transformed template URI to look up.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The template if found (may be marked disabled), None if not found.\n        \"\"\"\n\n        async def base(\n            u: str, version: VersionSpec | None = None\n        ) -> ResourceTemplate | None:\n            return await self._get_resource_template(u, version)\n\n        chain = base\n        for transform in self.transforms:\n            chain = partial(transform.get_resource_template, call_next=chain)\n\n        return await chain(uri, version=version)\n\n    async def list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"List prompts with all transforms applied.\n\n        Components may be marked as disabled but are NOT filtered here.\n        \"\"\"\n        prompts = await self._list_prompts()\n        for transform in self.transforms:\n            prompts = await transform.list_prompts(prompts)\n        return prompts\n\n    async def get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get prompt by transformed name with all transforms applied.\n\n        Note: This method does NOT filter disabled components. The Server\n        (FastMCP) performs enabled filtering after all transforms complete.\n\n        Args:\n            name: The transformed prompt name to look up.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The prompt if found (may be marked disabled), None if not found.\n        \"\"\"\n\n        async def base(n: str, version: VersionSpec | None = None) -> Prompt | None:\n            return await self._get_prompt(n, version)\n\n        chain = base\n        for transform in self.transforms:\n            chain = partial(transform.get_prompt, call_next=chain)\n\n        return await chain(name, version=version)\n\n    # -------------------------------------------------------------------------\n    # Private list/get methods (override these to provide components)\n    # -------------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"Return all available tools.\n\n        Override to provide tools dynamically. Returns ALL versions of all tools.\n        The server handles deduplication to show one tool per name.\n        \"\"\"\n        return []\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a specific tool by name.\n\n        Default implementation filters _list_tools() and picks the highest version\n        that matches the spec.\n\n        Args:\n            name: The tool name.\n            version: Optional version filter. If None, returns highest version.\n                     If specified, returns highest version matching the spec.\n\n        Returns:\n            The Tool if found, or None to continue searching other providers.\n        \"\"\"\n        tools = await self._list_tools()\n        matching = [t for t in tools if t.name == name]\n        if version:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"Return all available resources.\n\n        Override to provide resources dynamically. Returns ALL versions of all resources.\n        The server handles deduplication to show one resource per URI.\n        \"\"\"\n        return []\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a specific resource by URI.\n\n        Default implementation filters _list_resources() and returns highest\n        version matching the spec.\n\n        Args:\n            uri: The resource URI.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The Resource if found, or None to continue searching other providers.\n        \"\"\"\n        resources = await self._list_resources()\n        matching = [r for r in resources if str(r.uri) == uri]\n        if version:\n            matching = [r for r in matching if version.matches(r.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"Return all available resource templates.\n\n        Override to provide resource templates dynamically. Returns ALL versions.\n        The server handles deduplication.\n        \"\"\"\n        return []\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template that matches the given URI.\n\n        Default implementation lists all templates, finds those whose pattern\n        matches the URI, and returns the highest version matching the spec.\n\n        Args:\n            uri: The URI to match against templates.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The ResourceTemplate if a matching one is found, or None to continue searching.\n        \"\"\"\n        templates = await self._list_resource_templates()\n        matching = [t for t in templates if t.matches(uri) is not None]\n        if version:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"Return all available prompts.\n\n        Override to provide prompts dynamically. Returns ALL versions of all prompts.\n        The server handles deduplication to show one prompt per name.\n        \"\"\"\n        return []\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a specific prompt by name.\n\n        Default implementation filters _list_prompts() and picks the highest version\n        matching the spec.\n\n        Args:\n            name: The prompt name.\n            version: Optional version filter. If None, returns highest version.\n\n        Returns:\n            The Prompt if found, or None to continue searching other providers.\n        \"\"\"\n        prompts = await self._list_prompts()\n        matching = [p for p in prompts if p.name == name]\n        if version:\n            matching = [p for p in matching if version.matches(p.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    # -------------------------------------------------------------------------\n    # Task registration\n    # -------------------------------------------------------------------------\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Return components that should be registered as background tasks.\n\n        Override to customize which components are task-eligible.\n        Default calls list_* methods, applies provider transforms, and filters\n        for components with task_config.mode != 'forbidden'.\n\n        Used by the server during startup to register functions with Docket.\n        \"\"\"\n        # Fetch all component types in parallel\n        results = await gather(\n            self._list_tools(),\n            self._list_resources(),\n            self._list_resource_templates(),\n            self._list_prompts(),\n        )\n        tools = cast(Sequence[Tool], results[0])\n        resources = cast(Sequence[Resource], results[1])\n        templates = cast(Sequence[ResourceTemplate], results[2])\n        prompts = cast(Sequence[Prompt], results[3])\n\n        # Apply provider's own transforms sequentially\n        # For tasks, we need the fully-transformed names\n        for transform in self.transforms:\n            tools = await transform.list_tools(tools)\n            resources = await transform.list_resources(resources)\n            templates = await transform.list_resource_templates(templates)\n            prompts = await transform.list_prompts(prompts)\n\n        return [\n            c\n            for c in [\n                *tools,\n                *resources,\n                *templates,\n                *prompts,\n            ]\n            if c.task_config.supports_tasks()\n        ]\n\n    # -------------------------------------------------------------------------\n    # Lifecycle methods\n    # -------------------------------------------------------------------------\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        \"\"\"User-overridable lifespan for custom setup and teardown.\n\n        Override this method to perform provider-specific initialization\n        like opening database connections, setting up external resources,\n        or other state management needed for the provider's lifetime.\n\n        The lifespan scope matches the server's lifespan - code before yield\n        runs at startup, code after yield runs at shutdown.\n\n        Example:\n            ```python\n            @asynccontextmanager\n            async def lifespan(self):\n                # Setup\n                self.db = await connect_database()\n                try:\n                    yield\n                finally:\n                    # Teardown\n                    await self.db.close()\n            ```\n        \"\"\"\n        yield\n\n    # -------------------------------------------------------------------------\n    # Enable/Disable\n    # -------------------------------------------------------------------------\n\n    def enable(\n        self,\n        *,\n        names: set[str] | None = None,\n        keys: set[str] | None = None,\n        version: VersionSpec | None = None,\n        tags: set[str] | None = None,\n        components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]]\n        | None = None,\n        only: bool = False,\n    ) -> Self:\n        \"\"\"Enable components matching all specified criteria.\n\n        Adds a visibility transform that marks matching components as enabled.\n        Later transforms override earlier ones, so enable after disable makes\n        the component enabled.\n\n        With only=True, switches to allowlist mode - first disables everything,\n        then enables matching components.\n\n        Args:\n            names: Component names or URIs to enable.\n            keys: Component keys to enable (e.g., {\"tool:my_tool@v1\"}).\n            version: Component version spec to enable (e.g., VersionSpec(eq=\"v1\") or\n                VersionSpec(gte=\"v2\")). Unversioned components will not match.\n            tags: Enable components with these tags.\n            components: Component types to include (e.g., {\"tool\", \"prompt\"}).\n            only: If True, ONLY enable matching components (allowlist mode).\n\n        Returns:\n            Self for method chaining.\n        \"\"\"\n        if only:\n            # Allowlist: disable everything, then enable matching\n            # The enable transform runs later on return path, so it overrides\n            self._transforms.append(Visibility(False, match_all=True))\n        self._transforms.append(\n            Visibility(\n                True,\n                names=names,\n                keys=keys,\n                version=version,\n                components=set(components) if components else None,\n                tags=set(tags) if tags else None,\n            )\n        )\n\n        return self\n\n    def disable(\n        self,\n        *,\n        names: set[str] | None = None,\n        keys: set[str] | None = None,\n        version: VersionSpec | None = None,\n        tags: set[str] | None = None,\n        components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]]\n        | None = None,\n    ) -> Self:\n        \"\"\"Disable components matching all specified criteria.\n\n        Adds a visibility transform that marks matching components as disabled.\n        Components can be re-enabled by calling enable() with matching criteria\n        (the later transform wins).\n\n        Args:\n            names: Component names or URIs to disable.\n            keys: Component keys to disable (e.g., {\"tool:my_tool@v1\"}).\n            version: Component version spec to disable (e.g., VersionSpec(eq=\"v1\") or\n                VersionSpec(gte=\"v2\")). Unversioned components will not match.\n            tags: Disable components with these tags.\n            components: Component types to include (e.g., {\"tool\", \"prompt\"}).\n\n        Returns:\n            Self for method chaining.\n        \"\"\"\n        self._transforms.append(\n            Visibility(\n                False,\n                names=names,\n                keys=keys,\n                version=version,\n                components=set(components) if components else None,\n                tags=set(tags) if tags else None,\n            )\n        )\n        return self\n"
  },
  {
    "path": "src/fastmcp/server/providers/fastmcp_provider.py",
    "content": "\"\"\"FastMCPProvider for wrapping FastMCP servers as providers.\n\nThis module provides the `FastMCPProvider` class that wraps a FastMCP server\nand exposes its components through the Provider interface.\n\nIt also provides FastMCPProvider* component classes that delegate execution to\nthe wrapped server's middleware, ensuring middleware runs when components are\nexecuted.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom collections.abc import AsyncIterator, Sequence\nfrom contextlib import asynccontextmanager\nfrom typing import TYPE_CHECKING, Any, overload\nfrom urllib.parse import quote\n\nimport mcp.types\nfrom mcp.types import AnyUrl\n\nfrom fastmcp.prompts.base import Prompt, PromptResult\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.tasks.config import TaskMeta\nfrom fastmcp.server.telemetry import delegate_span\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\n    from fastmcp.server.server import FastMCP\n\n\ndef _expand_uri_template(template: str, params: dict[str, Any]) -> str:\n    \"\"\"Expand a URI template with parameters.\n\n    Handles both {name} path placeholders and RFC 6570 {?param1,param2}\n    query parameter syntax.\n    \"\"\"\n    result = template\n\n    # Replace {name} path placeholders\n    for key, value in params.items():\n        result = re.sub(rf\"\\{{{key}\\}}\", str(value), result)\n\n    # Expand {?param1,param2,...} query parameter blocks\n    def _expand_query_block(match: re.Match[str]) -> str:\n        names = [n.strip() for n in match.group(1).split(\",\")]\n        parts = []\n        for name in names:\n            if name in params:\n                parts.append(f\"{quote(name)}={quote(str(params[name]))}\")\n        if parts:\n            return \"?\" + \"&\".join(parts)\n        return \"\"\n\n    result = re.sub(r\"\\{\\?([^}]+)\\}\", _expand_query_block, result)\n\n    return result\n\n\n# -----------------------------------------------------------------------------\n# FastMCPProvider component classes\n# -----------------------------------------------------------------------------\n\n\nclass FastMCPProviderTool(Tool):\n    \"\"\"Tool that delegates execution to a wrapped server's middleware.\n\n    When `run()` is called, this tool invokes the wrapped server's\n    `_call_tool_middleware()` method, ensuring the server's middleware\n    chain is executed.\n    \"\"\"\n\n    _server: Any = None  # FastMCP, but Any to avoid circular import\n    _original_name: str | None = None\n\n    def __init__(\n        self,\n        server: Any,\n        original_name: str,\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self._server = server\n        self._original_name = original_name\n\n    @classmethod\n    def wrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool:\n        \"\"\"Wrap a Tool to delegate execution to the server's middleware.\"\"\"\n        return cls(\n            server=server,\n            original_name=tool.name,\n            name=tool.name,\n            version=tool.version,\n            description=tool.description,\n            parameters=tool.parameters,\n            output_schema=tool.output_schema,\n            tags=tool.tags,\n            annotations=tool.annotations,\n            task_config=tool.task_config,\n            meta=tool.get_meta(),\n            title=tool.title,\n            icons=tool.icons,\n        )\n\n    @overload\n    async def _run(\n        self,\n        arguments: dict[str, Any],\n        task_meta: None = None,\n    ) -> ToolResult: ...\n\n    @overload\n    async def _run(\n        self,\n        arguments: dict[str, Any],\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _run(\n        self,\n        arguments: dict[str, Any],\n        task_meta: TaskMeta | None = None,\n    ) -> ToolResult | mcp.types.CreateTaskResult:\n        \"\"\"Delegate to child server's call_tool() with task_meta.\n\n        Passes task_meta through to the child server so it can handle\n        backgrounding appropriately. fn_key is already set by the parent\n        server before calling this method.\n        \"\"\"\n        # Pass exact version so child executes the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        with delegate_span(\n            self._original_name or \"\", \"FastMCPProvider\", self._original_name or \"\"\n        ):\n            return await self._server.call_tool(\n                self._original_name, arguments, version=version, task_meta=task_meta\n            )\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        \"\"\"Delegate to child server's call_tool() without task_meta.\n\n        This is called when the tool is used within a TransformedTool\n        forwarding function or other contexts where task_meta is not available.\n        \"\"\"\n        # Pass exact version so child executes the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        result = await self._server.call_tool(\n            self._original_name, arguments, version=version\n        )\n        # Result from call_tool should always be ToolResult when no task_meta\n        if isinstance(result, mcp.types.CreateTaskResult):\n            raise RuntimeError(\n                \"Unexpected CreateTaskResult from call_tool without task_meta\"\n            )\n        return result\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"FastMCPProvider\",\n            \"fastmcp.delegate.original_name\": self._original_name,\n        }\n\n\nclass FastMCPProviderResource(Resource):\n    \"\"\"Resource that delegates reading to a wrapped server's read_resource().\n\n    When `read()` is called, this resource invokes the wrapped server's\n    `read_resource()` method, ensuring the server's middleware chain is executed.\n    \"\"\"\n\n    _server: Any = None  # FastMCP, but Any to avoid circular import\n    _original_uri: str | None = None\n\n    def __init__(\n        self,\n        server: Any,\n        original_uri: str,\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self._server = server\n        self._original_uri = original_uri\n\n    @classmethod\n    def wrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource:\n        \"\"\"Wrap a Resource to delegate reading to the server's middleware.\"\"\"\n        return cls(\n            server=server,\n            original_uri=str(resource.uri),\n            uri=resource.uri,\n            version=resource.version,\n            name=resource.name,\n            description=resource.description,\n            mime_type=resource.mime_type,\n            tags=resource.tags,\n            annotations=resource.annotations,\n            task_config=resource.task_config,\n            meta=resource.get_meta(),\n            title=resource.title,\n            icons=resource.icons,\n        )\n\n    @overload\n    async def _read(self, task_meta: None = None) -> ResourceResult: ...\n\n    @overload\n    async def _read(self, task_meta: TaskMeta) -> mcp.types.CreateTaskResult: ...\n\n    async def _read(\n        self, task_meta: TaskMeta | None = None\n    ) -> ResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Delegate to child server's read_resource() with task_meta.\n\n        Passes task_meta through to the child server so it can handle\n        backgrounding appropriately. fn_key is already set by the parent\n        server before calling this method.\n        \"\"\"\n        # Pass exact version so child reads the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        with delegate_span(\n            self._original_uri or \"\", \"FastMCPProvider\", self._original_uri or \"\"\n        ):\n            return await self._server.read_resource(\n                self._original_uri, version=version, task_meta=task_meta\n            )\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"FastMCPProvider\",\n            \"fastmcp.delegate.original_uri\": self._original_uri,\n        }\n\n\nclass FastMCPProviderPrompt(Prompt):\n    \"\"\"Prompt that delegates rendering to a wrapped server's render_prompt().\n\n    When `render()` is called, this prompt invokes the wrapped server's\n    `render_prompt()` method, ensuring the server's middleware chain is executed.\n    \"\"\"\n\n    _server: Any = None  # FastMCP, but Any to avoid circular import\n    _original_name: str | None = None\n\n    def __init__(\n        self,\n        server: Any,\n        original_name: str,\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self._server = server\n        self._original_name = original_name\n\n    @classmethod\n    def wrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt:\n        \"\"\"Wrap a Prompt to delegate rendering to the server's middleware.\"\"\"\n        return cls(\n            server=server,\n            original_name=prompt.name,\n            name=prompt.name,\n            version=prompt.version,\n            description=prompt.description,\n            arguments=prompt.arguments,\n            tags=prompt.tags,\n            task_config=prompt.task_config,\n            meta=prompt.get_meta(),\n            title=prompt.title,\n            icons=prompt.icons,\n        )\n\n    @overload\n    async def _render(\n        self,\n        arguments: dict[str, Any] | None = None,\n        task_meta: None = None,\n    ) -> PromptResult: ...\n\n    @overload\n    async def _render(\n        self,\n        arguments: dict[str, Any] | None,\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _render(\n        self,\n        arguments: dict[str, Any] | None = None,\n        task_meta: TaskMeta | None = None,\n    ) -> PromptResult | mcp.types.CreateTaskResult:\n        \"\"\"Delegate to child server's render_prompt() with task_meta.\n\n        Passes task_meta through to the child server so it can handle\n        backgrounding appropriately. fn_key is already set by the parent\n        server before calling this method.\n        \"\"\"\n        # Pass exact version so child renders the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        with delegate_span(\n            self._original_name or \"\", \"FastMCPProvider\", self._original_name or \"\"\n        ):\n            return await self._server.render_prompt(\n                self._original_name, arguments, version=version, task_meta=task_meta\n            )\n\n    async def render(self, arguments: dict[str, Any] | None = None) -> PromptResult:\n        \"\"\"Delegate to child server's render_prompt() without task_meta.\n\n        This is called when the prompt is used within a transformed context\n        or other contexts where task_meta is not available.\n        \"\"\"\n        # Pass exact version so child renders the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        result = await self._server.render_prompt(\n            self._original_name, arguments, version=version\n        )\n        # Result from render_prompt should always be PromptResult when no task_meta\n        if isinstance(result, mcp.types.CreateTaskResult):\n            raise RuntimeError(\n                \"Unexpected CreateTaskResult from render_prompt without task_meta\"\n            )\n        return result\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"FastMCPProvider\",\n            \"fastmcp.delegate.original_name\": self._original_name,\n        }\n\n\nclass FastMCPProviderResourceTemplate(ResourceTemplate):\n    \"\"\"Resource template that creates FastMCPProviderResources.\n\n    When `create_resource()` is called, this template creates a\n    FastMCPProviderResource that will invoke the wrapped server's middleware\n    when read.\n    \"\"\"\n\n    _server: Any = None  # FastMCP, but Any to avoid circular import\n    _original_uri_template: str | None = None\n\n    def __init__(\n        self,\n        server: Any,\n        original_uri_template: str,\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self._server = server\n        self._original_uri_template = original_uri_template\n\n    @classmethod\n    def wrap(\n        cls, server: Any, template: ResourceTemplate\n    ) -> FastMCPProviderResourceTemplate:\n        \"\"\"Wrap a ResourceTemplate to create FastMCPProviderResources.\"\"\"\n        return cls(\n            server=server,\n            original_uri_template=template.uri_template,\n            uri_template=template.uri_template,\n            version=template.version,\n            name=template.name,\n            description=template.description,\n            mime_type=template.mime_type,\n            parameters=template.parameters,\n            tags=template.tags,\n            annotations=template.annotations,\n            task_config=template.task_config,\n            meta=template.get_meta(),\n            title=template.title,\n            icons=template.icons,\n        )\n\n    async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:\n        \"\"\"Create a FastMCPProviderResource for the given URI.\n\n        The `uri` is the external/transformed URI (e.g., with namespace prefix).\n        We use `_original_uri_template` with `params` to construct the internal\n        URI that the nested server understands.\n        \"\"\"\n        # Expand the original template with params to get internal URI\n        original_uri = _expand_uri_template(self._original_uri_template or \"\", params)\n        return FastMCPProviderResource(\n            server=self._server,\n            original_uri=original_uri,\n            uri=AnyUrl(uri),\n            name=self.name,\n            description=self.description,\n            mime_type=self.mime_type,\n        )\n\n    @overload\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: None = None\n    ) -> ResourceResult: ...\n\n    @overload\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: TaskMeta\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _read(\n        self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None\n    ) -> ResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Delegate to child server's read_resource() with task_meta.\n\n        Passes task_meta through to the child server so it can handle\n        backgrounding appropriately. fn_key is already set by the parent\n        server before calling this method.\n        \"\"\"\n        # Expand the original template with params to get internal URI\n        original_uri = _expand_uri_template(self._original_uri_template or \"\", params)\n\n        # Pass exact version so child reads the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        with delegate_span(\n            original_uri, \"FastMCPProvider\", self._original_uri_template or \"\"\n        ):\n            return await self._server.read_resource(\n                original_uri, version=version, task_meta=task_meta\n            )\n\n    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:\n        \"\"\"Read the resource content for background task execution.\n\n        Reads the resource via the wrapped server and returns the ResourceResult.\n        This method is called by Docket during background task execution.\n        \"\"\"\n        # Expand the original template with arguments to get internal URI\n        original_uri = _expand_uri_template(\n            self._original_uri_template or \"\", arguments\n        )\n\n        # Pass exact version so child reads the correct version\n        version = VersionSpec(eq=self.version) if self.version else None\n\n        # Read from the wrapped server\n        result = await self._server.read_resource(original_uri, version=version)\n        if isinstance(result, mcp.types.CreateTaskResult):\n            raise RuntimeError(\"Unexpected CreateTaskResult during Docket execution\")\n\n        return result\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"No-op: the child's actual template is registered via get_tasks().\"\"\"\n\n    async def add_to_docket(\n        self,\n        docket: Docket,\n        params: dict[str, Any],\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this template for background execution via docket.\n\n        The child's FunctionResourceTemplate.fn is registered (via get_tasks),\n        and it expects splatted **kwargs, so we splat params here.\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(**params)\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"FastMCPProvider\",\n            \"fastmcp.delegate.original_uri_template\": self._original_uri_template,\n        }\n\n\n# -----------------------------------------------------------------------------\n# FastMCPProvider\n# -----------------------------------------------------------------------------\n\n\nclass FastMCPProvider(Provider):\n    \"\"\"Provider that wraps a FastMCP server.\n\n    This provider enables mounting one FastMCP server onto another, exposing\n    the mounted server's tools, resources, and prompts through the parent\n    server.\n\n    Components returned by this provider are wrapped in FastMCPProvider*\n    classes that delegate execution to the wrapped server's middleware chain.\n    This ensures middleware runs when components are executed.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.providers import FastMCPProvider\n\n        main = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Mount directly - tools accessible by original names\n        main.add_provider(FastMCPProvider(sub))\n\n        # Or with namespace\n        from fastmcp.server.transforms import Namespace\n        provider = FastMCPProvider(sub)\n        provider.add_transform(Namespace(\"sub\"))\n        main.add_provider(provider)\n        ```\n\n    Note:\n        Normally you would use `FastMCP.mount()` which handles proxy conversion\n        and creates the provider with namespace automatically.\n    \"\"\"\n\n    def __init__(self, server: FastMCP[Any]):\n        \"\"\"Initialize a FastMCPProvider.\n\n        Args:\n            server: The FastMCP server to wrap.\n        \"\"\"\n        super().__init__()\n        self.server = server\n\n    # -------------------------------------------------------------------------\n    # Tool methods\n    # -------------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"List all tools from the mounted server as FastMCPProviderTools.\n\n        Runs the mounted server's middleware so filtering/transformation applies.\n        Wraps each tool as a FastMCPProviderTool that delegates execution to\n        the nested server's middleware.\n        \"\"\"\n        raw_tools = await self.server.list_tools()\n        return [FastMCPProviderTool.wrap(self.server, t) for t in raw_tools]\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name as a FastMCPProviderTool.\n\n        Passes the full VersionSpec to the nested server, which handles both\n        exact version matching and range filtering. Uses get_tool to ensure\n        the nested server's transforms are applied.\n        \"\"\"\n        raw_tool = await self.server.get_tool(name, version)\n        if raw_tool is None:\n            return None\n        return FastMCPProviderTool.wrap(self.server, raw_tool)\n\n    # -------------------------------------------------------------------------\n    # Resource methods\n    # -------------------------------------------------------------------------\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"List all resources from the mounted server as FastMCPProviderResources.\n\n        Runs the mounted server's middleware so filtering/transformation applies.\n        Wraps each resource as a FastMCPProviderResource that delegates reading\n        to the nested server's middleware.\n        \"\"\"\n        raw_resources = await self.server.list_resources()\n        return [FastMCPProviderResource.wrap(self.server, r) for r in raw_resources]\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a concrete resource by URI as a FastMCPProviderResource.\n\n        Passes the full VersionSpec to the nested server, which handles both\n        exact version matching and range filtering. Uses get_resource to ensure\n        the nested server's transforms are applied.\n        \"\"\"\n        raw_resource = await self.server.get_resource(uri, version)\n        if raw_resource is None:\n            return None\n        return FastMCPProviderResource.wrap(self.server, raw_resource)\n\n    # -------------------------------------------------------------------------\n    # Resource template methods\n    # -------------------------------------------------------------------------\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"List all resource templates from the mounted server.\n\n        Runs the mounted server's middleware so filtering/transformation applies.\n        Returns FastMCPProviderResourceTemplate instances that create\n        FastMCPProviderResources when materialized.\n        \"\"\"\n        raw_templates = await self.server.list_resource_templates()\n        return [\n            FastMCPProviderResourceTemplate.wrap(self.server, t) for t in raw_templates\n        ]\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template that matches the given URI.\n\n        Passes the full VersionSpec to the nested server, which handles both\n        exact version matching and range filtering. Uses get_resource_template\n        to ensure the nested server's transforms are applied.\n        \"\"\"\n        raw_template = await self.server.get_resource_template(uri, version)\n        if raw_template is None:\n            return None\n        return FastMCPProviderResourceTemplate.wrap(self.server, raw_template)\n\n    # -------------------------------------------------------------------------\n    # Prompt methods\n    # -------------------------------------------------------------------------\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"List all prompts from the mounted server as FastMCPProviderPrompts.\n\n        Runs the mounted server's middleware so filtering/transformation applies.\n        Returns FastMCPProviderPrompt instances that delegate rendering to the\n        wrapped server's middleware.\n        \"\"\"\n        raw_prompts = await self.server.list_prompts()\n        return [FastMCPProviderPrompt.wrap(self.server, p) for p in raw_prompts]\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a prompt by name as a FastMCPProviderPrompt.\n\n        Passes the full VersionSpec to the nested server, which handles both\n        exact version matching and range filtering. Uses get_prompt to ensure\n        the nested server's transforms are applied.\n        \"\"\"\n        raw_prompt = await self.server.get_prompt(name, version)\n        if raw_prompt is None:\n            return None\n        return FastMCPProviderPrompt.wrap(self.server, raw_prompt)\n\n    # -------------------------------------------------------------------------\n    # Task registration\n    # -------------------------------------------------------------------------\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Return task-eligible components from the mounted server.\n\n        Returns the child's ACTUAL components (not wrapped) so their actual\n        functions get registered with Docket. Gets components with child\n        server's transforms applied, then applies this provider's transforms\n        for correct registration keys.\n        \"\"\"\n        # Get tasks with child server's transforms already applied\n        components = list(await self.server.get_tasks())\n\n        # Separate by type for this provider's transform application\n        tools = [c for c in components if isinstance(c, Tool)]\n        resources = [c for c in components if isinstance(c, Resource)]\n        templates = [c for c in components if isinstance(c, ResourceTemplate)]\n        prompts = [c for c in components if isinstance(c, Prompt)]\n\n        # Apply this provider's transforms sequentially\n        for transform in self.transforms:\n            tools = await transform.list_tools(tools)\n            resources = await transform.list_resources(resources)\n            templates = await transform.list_resource_templates(templates)\n            prompts = await transform.list_prompts(prompts)\n\n        # Filter to only task-eligible components (same as base Provider)\n        return [\n            c\n            for c in [\n                *tools,\n                *resources,\n                *templates,\n                *prompts,\n            ]\n            if c.task_config.supports_tasks()\n        ]\n\n    # -------------------------------------------------------------------------\n    # Lifecycle methods\n    # -------------------------------------------------------------------------\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        \"\"\"Start the mounted server's user lifespan.\n\n        This starts only the wrapped server's user-defined lifespan, NOT its\n        full _lifespan_manager() (which includes Docket). The parent server's\n        Docket handles all background tasks.\n        \"\"\"\n        async with self.server._lifespan(self.server):\n            yield\n"
  },
  {
    "path": "src/fastmcp/server/providers/filesystem.py",
    "content": "\"\"\"FileSystemProvider for filesystem-based component discovery.\n\nFileSystemProvider scans a directory for Python files, imports them, and\nregisters any Tool, Resource, ResourceTemplate, or Prompt objects found.\n\nComponents are created using the standalone decorators from fastmcp.tools,\nfastmcp.resources, and fastmcp.prompts:\n\nExample:\n    ```python\n    # In mcp/tools.py\n    from fastmcp.tools import tool\n\n    @tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    # In main.py\n    from pathlib import Path\n\n    from fastmcp import FastMCP\n    from fastmcp.server.providers import FileSystemProvider\n\n    mcp = FastMCP(\"MyServer\", providers=[FileSystemProvider(Path(__file__).parent / \"mcp\")])\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Sequence\nfrom pathlib import Path\n\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.providers.filesystem_discovery import discover_and_import\nfrom fastmcp.server.providers.local_provider import LocalProvider\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.versions import VersionSpec\n\nlogger = get_logger(__name__)\n\n\nclass FileSystemProvider(LocalProvider):\n    \"\"\"Provider that discovers components from the filesystem.\n\n    Scans a directory for Python files and registers any Tool, Resource,\n    ResourceTemplate, or Prompt objects found. Components are created using\n    the standalone decorators:\n    - @tool from fastmcp.tools\n    - @resource from fastmcp.resources\n    - @prompt from fastmcp.prompts\n\n    Args:\n        root: Root directory to scan. Defaults to current directory.\n        reload: If True, re-scan files on every request (dev mode).\n            Defaults to False (scan once at init, cache results).\n\n    Example:\n        ```python\n        # In mcp/tools.py\n        from fastmcp.tools import tool\n\n        @tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # In main.py\n        from pathlib import Path\n\n        from fastmcp import FastMCP\n        from fastmcp.server.providers import FileSystemProvider\n\n        # Path relative to this file\n        mcp = FastMCP(\"MyServer\", providers=[FileSystemProvider(Path(__file__).parent / \"mcp\")])\n\n        # Dev mode - re-scan on every request\n        mcp = FastMCP(\"MyServer\", providers=[FileSystemProvider(Path(__file__).parent / \"mcp\", reload=True)])\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        root: str | Path = \".\",\n        reload: bool = False,\n    ) -> None:\n        super().__init__(on_duplicate=\"replace\")\n        self._root = Path(root).resolve()\n        self._reload = reload\n        self._loaded = False\n        # Track files we've warned about: path -> mtime when warned\n        # Re-warn if file changes (mtime differs)\n        self._warned_files: dict[Path, float] = {}\n        # Lock for serializing reload operations (created lazily)\n        self._reload_lock: asyncio.Lock | None = None\n\n        # Always load once at init to catch errors early\n        self._load_components()\n\n    def _load_components(self) -> None:\n        \"\"\"Discover and register all components from the filesystem.\"\"\"\n        # Clear existing components if reloading\n        if self._loaded:\n            self._components.clear()\n\n        result = discover_and_import(self._root)\n\n        # Log warnings for failed files (only once per file version)\n        for file_path, error in result.failed_files.items():\n            try:\n                current_mtime = file_path.stat().st_mtime\n            except OSError:\n                current_mtime = 0.0\n\n            # Warn if we haven't warned about this file, or if it changed\n            last_warned_mtime = self._warned_files.get(file_path)\n            if last_warned_mtime is None or last_warned_mtime != current_mtime:\n                logger.warning(f\"Failed to import {file_path}: {error}\")\n                self._warned_files[file_path] = current_mtime\n\n        # Clear warnings for files that now import successfully\n        successful_files = {fp for fp, _ in result.components}\n        for fp in successful_files:\n            self._warned_files.pop(fp, None)\n\n        for file_path, component in result.components:\n            try:\n                self._register_component(component)\n            except Exception:\n                logger.exception(\n                    \"Failed to register %s from %s\",\n                    getattr(component, \"name\", repr(component)),\n                    file_path,\n                )\n\n        self._loaded = True\n        logger.debug(\n            f\"FileSystemProvider loaded {len(self._components)} components from {self._root}\"\n        )\n\n    def _register_component(self, component: FastMCPComponent) -> None:\n        \"\"\"Register a single component based on its type.\"\"\"\n        if isinstance(component, Tool):\n            self.add_tool(component)\n        elif isinstance(component, ResourceTemplate):\n            self.add_template(component)\n        elif isinstance(component, Resource):\n            self.add_resource(component)\n        elif isinstance(component, Prompt):\n            self.add_prompt(component)\n        else:\n            logger.debug(\"Ignoring unknown component type: %r\", type(component))\n\n    async def _ensure_loaded(self) -> None:\n        \"\"\"Ensure components are loaded, reloading if in reload mode.\n\n        Uses a lock to serialize concurrent reload operations and runs\n        filesystem I/O off the event loop using asyncio.to_thread.\n        \"\"\"\n        if not self._reload and self._loaded:\n            return\n\n        # Create lock lazily (can't create in __init__ without event loop)\n        if self._reload_lock is None:\n            self._reload_lock = asyncio.Lock()\n\n        async with self._reload_lock:\n            # Double-check after acquiring lock\n            if self._reload or not self._loaded:\n                await asyncio.to_thread(self._load_components)\n\n    # Override provider methods to support reload mode\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"Return all tools, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._list_tools()\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._get_tool(name, version)\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"Return all resources, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._list_resources()\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._get_resource(uri, version)\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"Return all resource templates, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._list_resource_templates()\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._get_resource_template(uri, version)\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"Return all prompts, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._list_prompts()\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a prompt by name, reloading if in reload mode.\"\"\"\n        await self._ensure_loaded()\n        return await super()._get_prompt(name, version)\n\n    def __repr__(self) -> str:\n        return f\"FileSystemProvider(root={self._root!r}, reload={self._reload})\"\n"
  },
  {
    "path": "src/fastmcp/server/providers/filesystem_discovery.py",
    "content": "\"\"\"File discovery and module import utilities for filesystem-based routing.\n\nThis module provides functions to:\n1. Discover Python files in a directory tree\n2. Import modules (as packages if __init__.py exists, else directly)\n3. Extract decorated components (Tool, Resource, Prompt objects) from imported modules\n\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib.util\nimport sys\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom types import ModuleType\n\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n\n@dataclass\nclass DiscoveryResult:\n    \"\"\"Result of filesystem discovery.\"\"\"\n\n    # Components are real objects (Tool, Resource, ResourceTemplate, Prompt)\n    components: list[tuple[Path, FastMCPComponent]] = field(default_factory=list)\n    failed_files: dict[Path, str] = field(default_factory=dict)  # path -> error message\n\n\ndef discover_files(root: Path) -> list[Path]:\n    \"\"\"Recursively discover all Python files under a directory.\n\n    Excludes __init__.py files (they're for package structure, not components).\n\n    Args:\n        root: Root directory to scan.\n\n    Returns:\n        List of .py file paths, sorted for deterministic order.\n    \"\"\"\n    if not root.exists():\n        return []\n\n    if not root.is_dir():\n        # If root is a file, just return it (if it's a .py file)\n        if root.suffix == \".py\" and root.name != \"__init__.py\":\n            return [root]\n        return []\n\n    files: list[Path] = []\n    for path in root.rglob(\"*.py\"):\n        # Skip __init__.py files\n        if path.name == \"__init__.py\":\n            continue\n        # Skip __pycache__ directories\n        if \"__pycache__\" in path.parts:\n            continue\n        files.append(path)\n\n    # Sort for deterministic discovery order\n    return sorted(files)\n\n\ndef _is_package_dir(directory: Path) -> bool:\n    \"\"\"Check if a directory is a Python package (has __init__.py).\"\"\"\n    return (directory / \"__init__.py\").exists()\n\n\ndef _find_package_root(file_path: Path) -> Path | None:\n    \"\"\"Find the root of the package containing this file.\n\n    Walks up the directory tree until we find a directory without __init__.py.\n\n    Returns:\n        The package root directory, or None if not in a package.\n    \"\"\"\n    current = file_path.parent\n    package_root = None\n\n    while current != current.parent:  # Stop at filesystem root\n        if _is_package_dir(current):\n            package_root = current\n            current = current.parent\n        else:\n            break\n\n    return package_root\n\n\ndef _compute_module_name(file_path: Path, package_root: Path) -> str:\n    \"\"\"Compute the dotted module name for a file within a package.\n\n    Args:\n        file_path: Path to the Python file.\n        package_root: Root directory of the package.\n\n    Returns:\n        Dotted module name (e.g., \"mcp.tools.greet\").\n    \"\"\"\n    relative = file_path.relative_to(package_root.parent)\n    parts = list(relative.parts)\n    # Remove .py extension from last part\n    parts[-1] = parts[-1].removesuffix(\".py\")\n    return \".\".join(parts)\n\n\ndef import_module_from_file(file_path: Path) -> ModuleType:\n    \"\"\"Import a Python file as a module.\n\n    If the file is part of a package (directory has __init__.py), imports\n    it as a proper package member (relative imports work). Otherwise,\n    imports directly using spec_from_file_location.\n\n    Args:\n        file_path: Path to the Python file.\n\n    Returns:\n        The imported module.\n\n    Raises:\n        ImportError: If the module cannot be imported.\n    \"\"\"\n    file_path = file_path.resolve()\n\n    # Check if this file is part of a package\n    package_root = _find_package_root(file_path)\n\n    if package_root is not None:\n        # Import as part of a package\n        module_name = _compute_module_name(file_path, package_root)\n\n        # Ensure package root's parent is in sys.path\n        package_parent = str(package_root.parent)\n        if package_parent not in sys.path:\n            sys.path.insert(0, package_parent)\n\n        # Import using standard import machinery\n        # If already imported, reload to pick up changes (for reload mode)\n        try:\n            if module_name in sys.modules:\n                return importlib.reload(sys.modules[module_name])\n            return importlib.import_module(module_name)\n        except ImportError as e:\n            raise ImportError(\n                f\"Failed to import {module_name} from {file_path}: {e}\"\n            ) from e\n    else:\n        # Import directly using spec_from_file_location\n        module_name = file_path.stem\n\n        # Ensure parent directory is in sys.path for imports\n        parent_dir = str(file_path.parent)\n        if parent_dir not in sys.path:\n            sys.path.insert(0, parent_dir)\n\n        spec = importlib.util.spec_from_file_location(module_name, file_path)\n        if spec is None or spec.loader is None:\n            raise ImportError(f\"Cannot load spec for {file_path}\")\n\n        module = importlib.util.module_from_spec(spec)\n        sys.modules[module_name] = module\n\n        try:\n            spec.loader.exec_module(module)\n        except Exception as e:\n            # Clean up sys.modules on failure\n            sys.modules.pop(module_name, None)\n            raise ImportError(f\"Failed to execute module {file_path}: {e}\") from e\n\n        return module\n\n\ndef extract_components(module: ModuleType) -> list[FastMCPComponent]:\n    \"\"\"Extract all MCP components from a module.\n\n    Scans all module attributes for instances of Tool, Resource,\n    ResourceTemplate, or Prompt objects created by standalone decorators,\n    or functions decorated with @tool/@resource/@prompt that have __fastmcp__ metadata.\n\n    Args:\n        module: The imported module to scan.\n\n    Returns:\n        List of component objects (Tool, Resource, ResourceTemplate, Prompt).\n    \"\"\"\n    # Import here to avoid circular imports\n    import inspect\n\n    from fastmcp.decorators import get_fastmcp_meta\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.prompts.function_prompt import PromptMeta\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.function_resource import ResourceMeta\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.server.dependencies import without_injected_parameters\n    from fastmcp.tools.base import Tool\n    from fastmcp.tools.function_tool import ToolMeta\n\n    component_types = (Tool, Resource, ResourceTemplate, Prompt)\n    components: list[FastMCPComponent] = []\n\n    for name in dir(module):\n        # Skip private/magic attributes\n        if name.startswith(\"_\"):\n            continue\n\n        try:\n            obj = getattr(module, name)\n        except AttributeError:\n            continue\n\n        # Check if this object is a component type\n        if isinstance(obj, component_types):\n            components.append(obj)\n            continue\n\n        # Check for functions with __fastmcp__ metadata\n        meta = get_fastmcp_meta(obj)\n        if meta is not None:\n            if isinstance(meta, ToolMeta):\n                resolved_task = meta.task if meta.task is not None else False\n                tool = Tool.from_function(\n                    obj,\n                    name=meta.name,\n                    version=meta.version,\n                    title=meta.title,\n                    description=meta.description,\n                    icons=meta.icons,\n                    tags=meta.tags,\n                    output_schema=meta.output_schema,\n                    annotations=meta.annotations,\n                    meta=meta.meta,\n                    task=resolved_task,\n                    exclude_args=meta.exclude_args,\n                    serializer=meta.serializer,\n                    auth=meta.auth,\n                )\n                components.append(tool)\n            elif isinstance(meta, ResourceMeta):\n                resolved_task = meta.task if meta.task is not None else False\n                has_uri_params = \"{\" in meta.uri and \"}\" in meta.uri\n                wrapper_fn = without_injected_parameters(obj)\n                has_func_params = bool(inspect.signature(wrapper_fn).parameters)\n\n                if has_uri_params or has_func_params:\n                    resource = ResourceTemplate.from_function(\n                        fn=obj,\n                        uri_template=meta.uri,\n                        name=meta.name,\n                        version=meta.version,\n                        title=meta.title,\n                        description=meta.description,\n                        icons=meta.icons,\n                        mime_type=meta.mime_type,\n                        tags=meta.tags,\n                        annotations=meta.annotations,\n                        meta=meta.meta,\n                        task=resolved_task,\n                        auth=meta.auth,\n                    )\n                else:\n                    resource = Resource.from_function(\n                        fn=obj,\n                        uri=meta.uri,\n                        name=meta.name,\n                        version=meta.version,\n                        title=meta.title,\n                        description=meta.description,\n                        icons=meta.icons,\n                        mime_type=meta.mime_type,\n                        tags=meta.tags,\n                        annotations=meta.annotations,\n                        meta=meta.meta,\n                        task=resolved_task,\n                        auth=meta.auth,\n                    )\n                components.append(resource)\n            elif isinstance(meta, PromptMeta):\n                resolved_task = meta.task if meta.task is not None else False\n                prompt = Prompt.from_function(\n                    obj,\n                    name=meta.name,\n                    version=meta.version,\n                    title=meta.title,\n                    description=meta.description,\n                    icons=meta.icons,\n                    tags=meta.tags,\n                    meta=meta.meta,\n                    task=resolved_task,\n                    auth=meta.auth,\n                )\n                components.append(prompt)\n\n    return components\n\n\ndef discover_and_import(root: Path) -> DiscoveryResult:\n    \"\"\"Discover files, import modules, and extract components.\n\n    This is the main entry point for filesystem-based discovery.\n\n    Args:\n        root: Root directory to scan.\n\n    Returns:\n        DiscoveryResult with components and any failed files.\n\n    Note:\n        Files that fail to import are tracked in failed_files, not logged.\n        The caller is responsible for logging/handling failures.\n        Files with no components are silently skipped.\n    \"\"\"\n    result = DiscoveryResult()\n\n    for file_path in discover_files(root):\n        try:\n            module = import_module_from_file(file_path)\n        except ImportError as e:\n            result.failed_files[file_path] = str(e)\n            continue\n        except Exception as e:\n            result.failed_files[file_path] = str(e)\n            continue\n\n        components = extract_components(module)\n        for component in components:\n            result.components.append((file_path, component))\n\n    return result\n"
  },
  {
    "path": "src/fastmcp/server/providers/local_provider/__init__.py",
    "content": "\"\"\"LocalProvider for locally-defined MCP components.\n\nThis module provides the `LocalProvider` class that manages tools, resources,\ntemplates, and prompts registered via decorators or direct methods.\n\"\"\"\n\nfrom fastmcp.server.providers.local_provider.local_provider import (\n    LocalProvider,\n)\n\n__all__ = [\"LocalProvider\"]\n"
  },
  {
    "path": "src/fastmcp/server/providers/local_provider/decorators/__init__.py",
    "content": "\"\"\"Decorator mixins for LocalProvider.\n\nThis module provides mixin classes that add decorator functionality\nto LocalProvider for tools, resources, templates, and prompts.\n\"\"\"\n\nfrom .prompts import PromptDecoratorMixin\nfrom .resources import ResourceDecoratorMixin\nfrom .tools import ToolDecoratorMixin\n\n__all__ = [\n    \"PromptDecoratorMixin\",\n    \"ResourceDecoratorMixin\",\n    \"ToolDecoratorMixin\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/providers/local_provider/decorators/prompts.py",
    "content": "\"\"\"Prompt decorator mixin for LocalProvider.\n\nThis module provides the PromptDecoratorMixin class that adds prompt\nregistration functionality to LocalProvider.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom collections.abc import Callable\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Any, TypeVar, overload\n\nimport mcp.types\nfrom mcp.types import AnyFunction\n\nimport fastmcp\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.prompts.function_prompt import FunctionPrompt\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.tasks.config import TaskConfig\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.local_provider import LocalProvider\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\n\nclass PromptDecoratorMixin:\n    \"\"\"Mixin class providing prompt decorator functionality for LocalProvider.\n\n    This mixin contains all methods related to:\n    - Prompt registration via add_prompt()\n    - Prompt decorator (@provider.prompt)\n    \"\"\"\n\n    def add_prompt(self: LocalProvider, prompt: Prompt | Callable[..., Any]) -> Prompt:\n        \"\"\"Add a prompt to this provider's storage.\n\n        Accepts either a Prompt object or a decorated function with __fastmcp__ metadata.\n        \"\"\"\n        enabled = True\n        if not isinstance(prompt, Prompt):\n            from fastmcp.decorators import get_fastmcp_meta\n            from fastmcp.prompts.function_prompt import PromptMeta\n\n            meta = get_fastmcp_meta(prompt)\n            if meta is not None and isinstance(meta, PromptMeta):\n                resolved_task = meta.task if meta.task is not None else False\n                enabled = meta.enabled\n                prompt = Prompt.from_function(\n                    prompt,\n                    name=meta.name,\n                    version=meta.version,\n                    title=meta.title,\n                    description=meta.description,\n                    icons=meta.icons,\n                    tags=meta.tags,\n                    meta=meta.meta,\n                    task=resolved_task,\n                    auth=meta.auth,\n                )\n            else:\n                raise TypeError(\n                    f\"Expected Prompt or @prompt-decorated function, got {type(prompt).__name__}. \"\n                    \"Use @prompt decorator or pass a Prompt instance.\"\n                )\n        self._add_component(prompt)\n        if not enabled:\n            self.disable(keys={prompt.key})\n        return prompt\n\n    @overload\n    def prompt(\n        self: LocalProvider,\n        name_or_fn: F,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        enabled: bool = True,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> F: ...\n\n    @overload\n    def prompt(\n        self: LocalProvider,\n        name_or_fn: str | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        enabled: bool = True,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> Callable[[F], F]: ...\n\n    def prompt(\n        self: LocalProvider,\n        name_or_fn: str | AnyFunction | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        enabled: bool = True,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> (\n        Callable[[AnyFunction], FunctionPrompt]\n        | FunctionPrompt\n        | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]\n    ):\n        \"\"\"Decorator to register a prompt.\n\n        This decorator supports multiple calling patterns:\n        - @provider.prompt (without parentheses)\n        - @provider.prompt() (with empty parentheses)\n        - @provider.prompt(\"custom_name\") (with name as first argument)\n        - @provider.prompt(name=\"custom_name\") (with name as keyword argument)\n        - provider.prompt(function, name=\"custom_name\") (direct function call)\n\n        Args:\n            name_or_fn: Either a function (when used as @prompt), a string name, or None\n            name: Optional name for the prompt (keyword-only, alternative to name_or_fn)\n            title: Optional title for the prompt\n            description: Optional description of what the prompt does\n            icons: Optional icons for the prompt\n            tags: Optional set of tags for categorizing the prompt\n            enabled: Whether the prompt is enabled (default True). If False, adds to blocklist.\n            meta: Optional meta information about the prompt\n            task: Optional task configuration for background execution\n            auth: Optional authorization checks for the prompt\n\n        Returns:\n            The registered FunctionPrompt or a decorator function.\n\n        Example:\n            ```python\n            provider = LocalProvider()\n\n            @provider.prompt\n            def analyze(topic: str) -> list:\n                return [{\"role\": \"user\", \"content\": f\"Analyze: {topic}\"}]\n\n            @provider.prompt(\"custom_name\")\n            def my_prompt(data: str) -> list:\n                return [{\"role\": \"user\", \"content\": data}]\n            ```\n        \"\"\"\n        if isinstance(name_or_fn, classmethod):\n            raise TypeError(\n                \"To decorate a classmethod, use @classmethod above @prompt. \"\n                \"See https://gofastmcp.com/servers/prompts#using-with-methods\"\n            )\n\n        def decorate_and_register(\n            fn: AnyFunction, prompt_name: str | None\n        ) -> FunctionPrompt | AnyFunction:\n            # Check for unbound method\n            try:\n                params = list(inspect.signature(fn).parameters.keys())\n            except (ValueError, TypeError):\n                params = []\n            if params and params[0] in (\"self\", \"cls\"):\n                fn_name = getattr(fn, \"__name__\", \"function\")\n                raise TypeError(\n                    f\"The function '{fn_name}' has '{params[0]}' as its first parameter. \"\n                    f\"Use the standalone @prompt decorator and register the bound method:\\n\\n\"\n                    f\"    from fastmcp.prompts import prompt\\n\\n\"\n                    f\"    class MyClass:\\n\"\n                    f\"        @prompt\\n\"\n                    f\"        def {fn_name}(...):\\n\"\n                    f\"            ...\\n\\n\"\n                    f\"    obj = MyClass()\\n\"\n                    f\"    mcp.add_prompt(obj.{fn_name})\\n\\n\"\n                    f\"See https://gofastmcp.com/servers/prompts#using-with-methods\"\n                )\n\n            resolved_task: bool | TaskConfig = task if task is not None else False\n\n            if fastmcp.settings.decorator_mode == \"object\":\n                prompt_obj = Prompt.from_function(\n                    fn,\n                    name=prompt_name,\n                    version=version,\n                    title=title,\n                    description=description,\n                    icons=icons,\n                    tags=tags,\n                    meta=meta,\n                    task=resolved_task,\n                    auth=auth,\n                )\n                self._add_component(prompt_obj)\n                if not enabled:\n                    self.disable(keys={prompt_obj.key})\n                return prompt_obj\n            else:\n                from fastmcp.prompts.function_prompt import PromptMeta\n\n                metadata = PromptMeta(\n                    name=prompt_name,\n                    version=version,\n                    title=title,\n                    description=description,\n                    icons=icons,\n                    tags=tags,\n                    meta=meta,\n                    task=task,\n                    auth=auth,\n                    enabled=enabled,\n                )\n                target = fn.__func__ if hasattr(fn, \"__func__\") else fn\n                target.__fastmcp__ = metadata  # type: ignore[attr-defined]\n                self.add_prompt(fn)\n                return fn\n\n        if inspect.isroutine(name_or_fn):\n            return decorate_and_register(name_or_fn, name)\n\n        elif isinstance(name_or_fn, str):\n            if name is not None:\n                raise TypeError(\n                    f\"Cannot specify both a name as first argument and as keyword argument. \"\n                    f\"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both.\"\n                )\n            prompt_name = name_or_fn\n        elif name_or_fn is None:\n            prompt_name = name\n        else:\n            raise TypeError(f\"Invalid first argument: {type(name_or_fn)}\")\n\n        return partial(\n            self.prompt,\n            name=prompt_name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            meta=meta,\n            enabled=enabled,\n            task=task,\n            auth=auth,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/local_provider/decorators/resources.py",
    "content": "\"\"\"Resource decorator mixin for LocalProvider.\n\nThis module provides the ResourceDecoratorMixin class that adds resource\nand template registration functionality to LocalProvider.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any, TypeVar\n\nimport mcp.types\nfrom mcp.types import Annotations, AnyFunction\n\nimport fastmcp\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.function_resource import resource as standalone_resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.tasks.config import TaskConfig\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.local_provider import LocalProvider\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\n\nclass ResourceDecoratorMixin:\n    \"\"\"Mixin class providing resource decorator functionality for LocalProvider.\n\n    This mixin contains all methods related to:\n    - Resource registration via add_resource()\n    - Resource template registration via add_template()\n    - Resource decorator (@provider.resource)\n    \"\"\"\n\n    def add_resource(\n        self: LocalProvider, resource: Resource | ResourceTemplate | Callable[..., Any]\n    ) -> Resource | ResourceTemplate:\n        \"\"\"Add a resource to this provider's storage.\n\n        Accepts either a Resource/ResourceTemplate object or a decorated function with __fastmcp__ metadata.\n        \"\"\"\n        enabled = True\n        if not isinstance(resource, (Resource, ResourceTemplate)):\n            from fastmcp.decorators import get_fastmcp_meta\n            from fastmcp.resources.function_resource import ResourceMeta\n            from fastmcp.server.dependencies import without_injected_parameters\n\n            meta = get_fastmcp_meta(resource)\n            if meta is not None and isinstance(meta, ResourceMeta):\n                resolved_task = meta.task if meta.task is not None else False\n                enabled = meta.enabled\n                has_uri_params = \"{\" in meta.uri and \"}\" in meta.uri\n                wrapper_fn = without_injected_parameters(resource)\n                has_func_params = bool(inspect.signature(wrapper_fn).parameters)\n\n                if has_uri_params or has_func_params:\n                    resource = ResourceTemplate.from_function(\n                        fn=resource,\n                        uri_template=meta.uri,\n                        name=meta.name,\n                        version=meta.version,\n                        title=meta.title,\n                        description=meta.description,\n                        icons=meta.icons,\n                        mime_type=meta.mime_type,\n                        tags=meta.tags,\n                        annotations=meta.annotations,\n                        meta=meta.meta,\n                        task=resolved_task,\n                        auth=meta.auth,\n                    )\n                else:\n                    resource = Resource.from_function(\n                        fn=resource,\n                        uri=meta.uri,\n                        name=meta.name,\n                        version=meta.version,\n                        title=meta.title,\n                        description=meta.description,\n                        icons=meta.icons,\n                        mime_type=meta.mime_type,\n                        tags=meta.tags,\n                        annotations=meta.annotations,\n                        meta=meta.meta,\n                        task=resolved_task,\n                        auth=meta.auth,\n                    )\n            else:\n                raise TypeError(\n                    f\"Expected Resource, ResourceTemplate, or @resource-decorated function, got {type(resource).__name__}. \"\n                    \"Use @resource('uri') decorator or pass a Resource/ResourceTemplate instance.\"\n                )\n        self._add_component(resource)\n        if not enabled:\n            self.disable(keys={resource.key})\n        return resource\n\n    def add_template(\n        self: LocalProvider, template: ResourceTemplate\n    ) -> ResourceTemplate:\n        \"\"\"Add a resource template to this provider's storage.\"\"\"\n        return self._add_component(template)\n\n    def resource(\n        self: LocalProvider,\n        uri: str,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        mime_type: str | None = None,\n        tags: set[str] | None = None,\n        enabled: bool = True,\n        annotations: Annotations | dict[str, Any] | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> Callable[[F], F]:\n        \"\"\"Decorator to register a function as a resource.\n\n        If the URI contains parameters (e.g. \"resource://{param}\") or the function\n        has parameters, it will be registered as a template resource.\n\n        Args:\n            uri: URI for the resource (e.g. \"resource://my-resource\" or \"resource://{param}\")\n            name: Optional name for the resource\n            title: Optional title for the resource\n            description: Optional description of the resource\n            icons: Optional icons for the resource\n            mime_type: Optional MIME type for the resource\n            tags: Optional set of tags for categorizing the resource\n            enabled: Whether the resource is enabled (default True). If False, adds to blocklist.\n            annotations: Optional annotations about the resource's behavior\n            meta: Optional meta information about the resource\n            task: Optional task configuration for background execution\n            auth: Optional authorization checks for the resource\n\n        Returns:\n            A decorator function.\n\n        Example:\n            ```python\n            provider = LocalProvider()\n\n            @provider.resource(\"data://config\")\n            def get_config() -> str:\n                return '{\"setting\": \"value\"}'\n\n            @provider.resource(\"data://{city}/weather\")\n            def get_weather(city: str) -> str:\n                return f\"Weather for {city}\"\n            ```\n        \"\"\"\n        if isinstance(annotations, dict):\n            annotations = Annotations(**annotations)\n\n        if inspect.isroutine(uri):\n            raise TypeError(\n                \"The @resource decorator was used incorrectly. \"\n                \"It requires a URI as the first argument. \"\n                \"Use @resource('uri') instead of @resource\"\n            )\n\n        resolved_task: bool | TaskConfig = task if task is not None else False\n\n        def decorator(fn: AnyFunction) -> Any:\n            # Check for unbound method\n            try:\n                params = list(inspect.signature(fn).parameters.keys())\n            except (ValueError, TypeError):\n                params = []\n            if params and params[0] in (\"self\", \"cls\"):\n                fn_name = getattr(fn, \"__name__\", \"function\")\n                raise TypeError(\n                    f\"The function '{fn_name}' has '{params[0]}' as its first parameter. \"\n                    f\"Use the standalone @resource decorator and register the bound method:\\n\\n\"\n                    f\"    from fastmcp.resources import resource\\n\\n\"\n                    f\"    class MyClass:\\n\"\n                    f\"        @resource('{uri}')\\n\"\n                    f\"        def {fn_name}(...):\\n\"\n                    f\"            ...\\n\\n\"\n                    f\"    obj = MyClass()\\n\"\n                    f\"    mcp.add_resource(obj.{fn_name})\\n\\n\"\n                    f\"See https://gofastmcp.com/servers/resources#using-with-methods\"\n                )\n\n            if fastmcp.settings.decorator_mode == \"object\":\n                create_resource = standalone_resource(\n                    uri,\n                    name=name,\n                    version=version,\n                    title=title,\n                    description=description,\n                    icons=icons,\n                    mime_type=mime_type,\n                    tags=tags,\n                    annotations=annotations,\n                    meta=meta,\n                    task=resolved_task,\n                    auth=auth,\n                )\n                obj = create_resource(fn)\n                # In legacy mode, standalone_resource always returns a component\n                assert isinstance(obj, (Resource, ResourceTemplate))\n                if isinstance(obj, ResourceTemplate):\n                    self.add_template(obj)\n                    if not enabled:\n                        self.disable(keys={obj.key})\n                else:\n                    self.add_resource(obj)\n                    if not enabled:\n                        self.disable(keys={obj.key})\n                return obj\n            else:\n                from fastmcp.resources.function_resource import ResourceMeta\n\n                metadata = ResourceMeta(\n                    uri=uri,\n                    name=name,\n                    version=version,\n                    title=title,\n                    description=description,\n                    icons=icons,\n                    tags=tags,\n                    mime_type=mime_type,\n                    annotations=annotations,\n                    meta=meta,\n                    task=task,\n                    auth=auth,\n                    enabled=enabled,\n                )\n                target = fn.__func__ if hasattr(fn, \"__func__\") else fn\n                target.__fastmcp__ = metadata  # type: ignore[attr-defined]\n                self.add_resource(fn)\n                return fn\n\n        return decorator\n"
  },
  {
    "path": "src/fastmcp/server/providers/local_provider/decorators/tools.py",
    "content": "\"\"\"Tool decorator mixin for LocalProvider.\n\nThis module provides the ToolDecoratorMixin class that adds tool\nregistration functionality to LocalProvider.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport types\nimport warnings\nfrom collections.abc import Callable\nfrom functools import partial\nfrom typing import (\n    TYPE_CHECKING,\n    Annotated,\n    Any,\n    Literal,\n    TypeVar,\n    Union,\n    get_args,\n    get_origin,\n    overload,\n)\n\nimport mcp.types\nfrom mcp.types import AnyFunction, ToolAnnotations\n\nimport fastmcp\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.utilities.types import NotSet, NotSetT\n\ntry:\n    from prefab_ui.app import PrefabApp as _PrefabApp\n    from prefab_ui.components.base import Component as _PrefabComponent\n\n    _HAS_PREFAB = True\nexcept ImportError:\n    _HAS_PREFAB = False\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.local_provider import LocalProvider\n    from fastmcp.tools.base import ToolResultSerializerType\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\nDuplicateBehavior = Literal[\"error\", \"warn\", \"replace\", \"ignore\"]\n\nPREFAB_RENDERER_URI = \"ui://prefab/renderer.html\"\n\n\ndef _is_prefab_type(tp: Any) -> bool:\n    \"\"\"Check if *tp* is or contains a prefab type, recursing through unions and Annotated.\"\"\"\n    if isinstance(tp, type) and issubclass(tp, (_PrefabApp, _PrefabComponent)):\n        return True\n    origin = get_origin(tp)\n    if origin is Union or origin is types.UnionType or origin is Annotated:\n        return any(_is_prefab_type(a) for a in get_args(tp))\n    return False\n\n\ndef _has_prefab_return_type(tool: Tool) -> bool:\n    \"\"\"Check if a FunctionTool's return type annotation is a prefab type.\"\"\"\n    if not _HAS_PREFAB or not isinstance(tool, FunctionTool):\n        return False\n    rt = tool.return_type\n    if rt is None or rt is inspect.Parameter.empty:\n        return False\n    return _is_prefab_type(rt)\n\n\ndef _ensure_prefab_renderer(provider: LocalProvider) -> None:\n    \"\"\"Lazily register the shared prefab renderer as a ui:// resource.\"\"\"\n    from prefab_ui.renderer import get_renderer_csp, get_renderer_html\n\n    from fastmcp.resources.types import TextResource\n    from fastmcp.server.apps import (\n        UI_MIME_TYPE,\n        AppConfig,\n        ResourceCSP,\n        app_config_to_meta_dict,\n    )\n\n    renderer_key = f\"resource:{PREFAB_RENDERER_URI}@\"\n    if renderer_key in provider._components:\n        return\n\n    csp = get_renderer_csp()\n    resource_app = AppConfig(\n        csp=ResourceCSP(\n            resource_domains=csp.get(\"resource_domains\"),\n            connect_domains=csp.get(\"connect_domains\"),\n        )\n    )\n    resource = TextResource(\n        uri=PREFAB_RENDERER_URI,  # type: ignore[arg-type]  # AnyUrl accepts ui:// scheme at runtime\n        name=\"Prefab Renderer\",\n        text=get_renderer_html(),\n        mime_type=UI_MIME_TYPE,\n        meta={\"ui\": app_config_to_meta_dict(resource_app)},\n    )\n    provider._add_component(resource)\n\n\ndef _expand_prefab_ui_meta(tool: Tool) -> None:\n    \"\"\"Expand meta[\"ui\"] = True into the full AppConfig dict for a prefab tool.\"\"\"\n    from prefab_ui.renderer import get_renderer_csp\n\n    from fastmcp.server.apps import AppConfig, ResourceCSP, app_config_to_meta_dict\n\n    csp = get_renderer_csp()\n    app_config = AppConfig(\n        resource_uri=PREFAB_RENDERER_URI,\n        csp=ResourceCSP(\n            resource_domains=csp.get(\"resource_domains\"),\n            connect_domains=csp.get(\"connect_domains\"),\n        ),\n    )\n    meta = dict(tool.meta) if tool.meta else {}\n    meta[\"ui\"] = app_config_to_meta_dict(app_config)\n    tool.meta = meta\n\n\ndef _maybe_apply_prefab_ui(provider: LocalProvider, tool: Tool) -> None:\n    \"\"\"Auto-wire prefab UI metadata and renderer resource if needed.\"\"\"\n    if not _HAS_PREFAB:\n        return\n\n    meta = tool.meta or {}\n    ui = meta.get(\"ui\")\n\n    if ui is True:\n        # Explicit app=True: expand to full AppConfig and register renderer\n        _ensure_prefab_renderer(provider)\n        _expand_prefab_ui_meta(tool)\n    elif ui is None and _has_prefab_return_type(tool):\n        # Inference: return type is a prefab type, auto-wire\n        _ensure_prefab_renderer(provider)\n        _expand_prefab_ui_meta(tool)\n    # If ui is a dict, it's already manually configured — leave it alone\n\n\nclass ToolDecoratorMixin:\n    \"\"\"Mixin class providing tool decorator functionality for LocalProvider.\n\n    This mixin contains all methods related to:\n    - Tool registration via add_tool()\n    - Tool decorator (@provider.tool)\n    \"\"\"\n\n    def add_tool(self: LocalProvider, tool: Tool | Callable[..., Any]) -> Tool:\n        \"\"\"Add a tool to this provider's storage.\n\n        Accepts either a Tool object or a decorated function with __fastmcp__ metadata.\n        \"\"\"\n        enabled = True\n        if not isinstance(tool, Tool):\n            from fastmcp.decorators import get_fastmcp_meta\n            from fastmcp.tools.function_tool import ToolMeta\n\n            fmeta = get_fastmcp_meta(tool)\n            if fmeta is not None and isinstance(fmeta, ToolMeta):\n                resolved_task = fmeta.task if fmeta.task is not None else False\n                enabled = fmeta.enabled\n\n                # Merge ToolMeta.app into the meta dict\n                tool_meta = fmeta.meta\n                if fmeta.app is not None:\n                    from fastmcp.server.apps import app_config_to_meta_dict\n\n                    tool_meta = dict(tool_meta) if tool_meta else {}\n                    if fmeta.app is True:\n                        tool_meta[\"ui\"] = True\n                    else:\n                        tool_meta[\"ui\"] = app_config_to_meta_dict(fmeta.app)\n\n                tool = Tool.from_function(\n                    tool,\n                    name=fmeta.name,\n                    version=fmeta.version,\n                    title=fmeta.title,\n                    description=fmeta.description,\n                    icons=fmeta.icons,\n                    tags=fmeta.tags,\n                    output_schema=fmeta.output_schema,\n                    annotations=fmeta.annotations,\n                    meta=tool_meta,\n                    task=resolved_task,\n                    exclude_args=fmeta.exclude_args,\n                    serializer=fmeta.serializer,\n                    timeout=fmeta.timeout,\n                    auth=fmeta.auth,\n                )\n            else:\n                tool = Tool.from_function(tool)\n        self._add_component(tool)\n        if not enabled:\n            self.disable(keys={tool.key})\n        _maybe_apply_prefab_ui(self, tool)\n        return tool\n\n    @overload\n    def tool(\n        self: LocalProvider,\n        name_or_fn: F,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        annotations: ToolAnnotations | dict[str, Any] | None = None,\n        exclude_args: list[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        enabled: bool = True,\n        task: bool | TaskConfig | None = None,\n        serializer: ToolResultSerializerType | None = None,  # Deprecated\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> F: ...\n\n    @overload\n    def tool(\n        self: LocalProvider,\n        name_or_fn: str | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        annotations: ToolAnnotations | dict[str, Any] | None = None,\n        exclude_args: list[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        enabled: bool = True,\n        task: bool | TaskConfig | None = None,\n        serializer: ToolResultSerializerType | None = None,  # Deprecated\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> Callable[[F], F]: ...\n\n    # NOTE: This method mirrors fastmcp.tools.tool() but adds registration,\n    # the `enabled` param, and supports deprecated params (serializer, exclude_args).\n    # When deprecated params are removed, this should delegate to the standalone\n    # decorator to reduce duplication.\n    def tool(\n        self: LocalProvider,\n        name_or_fn: str | AnyFunction | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        annotations: ToolAnnotations | dict[str, Any] | None = None,\n        exclude_args: list[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        enabled: bool = True,\n        task: bool | TaskConfig | None = None,\n        serializer: ToolResultSerializerType | None = None,  # Deprecated\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> (\n        Callable[[AnyFunction], FunctionTool]\n        | FunctionTool\n        | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]\n    ):\n        \"\"\"Decorator to register a tool.\n\n        This decorator supports multiple calling patterns:\n        - @provider.tool (without parentheses)\n        - @provider.tool() (with empty parentheses)\n        - @provider.tool(\"custom_name\") (with name as first argument)\n        - @provider.tool(name=\"custom_name\") (with name as keyword argument)\n        - provider.tool(function, name=\"custom_name\") (direct function call)\n\n        Args:\n            name_or_fn: Either a function (when used as @tool), a string name, or None\n            name: Optional name for the tool (keyword-only, alternative to name_or_fn)\n            title: Optional title for the tool\n            description: Optional description of what the tool does\n            icons: Optional icons for the tool\n            tags: Optional set of tags for categorizing the tool\n            output_schema: Optional JSON schema for the tool's output\n            annotations: Optional annotations about the tool's behavior\n            exclude_args: Optional list of argument names to exclude from the tool schema\n            meta: Optional meta information about the tool\n            enabled: Whether the tool is enabled (default True). If False, adds to blocklist.\n            task: Optional task configuration for background execution\n            serializer: Deprecated. Return ToolResult from your tools for full control over serialization.\n\n        Returns:\n            The registered FunctionTool or a decorator function.\n\n        Example:\n            ```python\n            provider = LocalProvider()\n\n            @provider.tool\n            def greet(name: str) -> str:\n                return f\"Hello, {name}!\"\n\n            @provider.tool(\"custom_name\")\n            def my_tool(x: int) -> str:\n                return str(x)\n            ```\n        \"\"\"\n        if serializer is not None and fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"The `serializer` parameter is deprecated. \"\n                \"Return ToolResult from your tools for full control over serialization. \"\n                \"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        if isinstance(annotations, dict):\n            annotations = ToolAnnotations(**annotations)\n\n        if isinstance(name_or_fn, classmethod):\n            raise TypeError(\n                \"To decorate a classmethod, use @classmethod above @tool. \"\n                \"See https://gofastmcp.com/servers/tools#using-with-methods\"\n            )\n\n        def decorate_and_register(\n            fn: AnyFunction, tool_name: str | None\n        ) -> FunctionTool | AnyFunction:\n            # Check for unbound method\n            try:\n                params = list(inspect.signature(fn).parameters.keys())\n            except (ValueError, TypeError):\n                params = []\n            if params and params[0] in (\"self\", \"cls\"):\n                fn_name = getattr(fn, \"__name__\", \"function\")\n                raise TypeError(\n                    f\"The function '{fn_name}' has '{params[0]}' as its first parameter. \"\n                    f\"Use the standalone @tool decorator and register the bound method:\\n\\n\"\n                    f\"    from fastmcp.tools import tool\\n\\n\"\n                    f\"    class MyClass:\\n\"\n                    f\"        @tool\\n\"\n                    f\"        def {fn_name}(...):\\n\"\n                    f\"            ...\\n\\n\"\n                    f\"    obj = MyClass()\\n\"\n                    f\"    mcp.add_tool(obj.{fn_name})\\n\\n\"\n                    f\"See https://gofastmcp.com/servers/tools#using-with-methods\"\n                )\n\n            resolved_task: bool | TaskConfig = task if task is not None else False\n\n            if fastmcp.settings.decorator_mode == \"object\":\n                tool_obj = Tool.from_function(\n                    fn,\n                    name=tool_name,\n                    version=version,\n                    title=title,\n                    description=description,\n                    icons=icons,\n                    tags=tags,\n                    output_schema=output_schema,\n                    annotations=annotations,\n                    exclude_args=exclude_args,\n                    meta=meta,\n                    serializer=serializer,\n                    task=resolved_task,\n                    timeout=timeout,\n                    auth=auth,\n                )\n                self._add_component(tool_obj)\n                if not enabled:\n                    self.disable(keys={tool_obj.key})\n                _maybe_apply_prefab_ui(self, tool_obj)\n                return tool_obj\n            else:\n                from fastmcp.tools.function_tool import ToolMeta\n\n                metadata = ToolMeta(\n                    name=tool_name,\n                    version=version,\n                    title=title,\n                    description=description,\n                    icons=icons,\n                    tags=tags,\n                    output_schema=output_schema,\n                    annotations=annotations,\n                    meta=meta,\n                    task=task,\n                    exclude_args=exclude_args,\n                    serializer=serializer,\n                    timeout=timeout,\n                    auth=auth,\n                    enabled=enabled,\n                )\n                target = fn.__func__ if hasattr(fn, \"__func__\") else fn\n                target.__fastmcp__ = metadata  # type: ignore[attr-defined]\n                tool_obj = self.add_tool(fn)\n                return fn\n\n        if inspect.isroutine(name_or_fn):\n            return decorate_and_register(name_or_fn, name)\n\n        elif isinstance(name_or_fn, str):\n            # Case 3: @tool(\"custom_name\") - name passed as first argument\n            if name is not None:\n                raise TypeError(\n                    \"Cannot specify both a name as first argument and as keyword argument. \"\n                    f\"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both.\"\n                )\n            tool_name = name_or_fn\n        elif name_or_fn is None:\n            # Case 4: @tool() or @tool(name=\"something\") - use keyword name\n            tool_name = name\n        else:\n            raise TypeError(\n                f\"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}\"\n            )\n\n        # Return partial for cases where we need to wait for the function\n        return partial(\n            self.tool,\n            name=tool_name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            output_schema=output_schema,\n            annotations=annotations,\n            exclude_args=exclude_args,\n            meta=meta,\n            enabled=enabled,\n            task=task,\n            serializer=serializer,\n            timeout=timeout,\n            auth=auth,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/local_provider/local_provider.py",
    "content": "\"\"\"LocalProvider for locally-defined MCP components.\n\nThis module provides the `LocalProvider` class that manages tools, resources,\ntemplates, and prompts registered via decorators or direct methods.\n\nLocalProvider can be used standalone and attached to multiple servers:\n\n```python\nfrom fastmcp.server.providers import LocalProvider\n\n# Create a reusable provider with tools\nprovider = LocalProvider()\n\n@provider.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# Attach to any server\nfrom fastmcp import FastMCP\nserver1 = FastMCP(\"Server1\", providers=[provider])\nserver2 = FastMCP(\"Server2\", providers=[provider])\n```\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom typing import Literal, TypeVar\n\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.providers.local_provider.decorators import (\n    PromptDecoratorMixin,\n    ResourceDecoratorMixin,\n    ToolDecoratorMixin,\n)\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.versions import VersionSpec, version_sort_key\n\nlogger = get_logger(__name__)\n\nDuplicateBehavior = Literal[\"error\", \"warn\", \"replace\", \"ignore\"]\n\n_C = TypeVar(\"_C\", bound=FastMCPComponent)\n\n\nclass LocalProvider(\n    Provider,\n    ToolDecoratorMixin,\n    ResourceDecoratorMixin,\n    PromptDecoratorMixin,\n):\n    \"\"\"Provider for locally-defined components.\n\n    Supports decorator-based registration (`@provider.tool`, `@provider.resource`,\n    `@provider.prompt`) and direct object registration methods.\n\n    When used standalone, LocalProvider uses default settings. When attached\n    to a FastMCP server via the server's decorators, server-level settings\n    like `_tool_serializer` and `_support_tasks_by_default` are injected.\n\n    Example:\n        ```python\n        from fastmcp.server.providers import LocalProvider\n\n        # Standalone usage\n        provider = LocalProvider()\n\n        @provider.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        @provider.resource(\"data://config\")\n        def get_config() -> str:\n            return '{\"setting\": \"value\"}'\n\n        @provider.prompt\n        def analyze(topic: str) -> list:\n            return [{\"role\": \"user\", \"content\": f\"Analyze: {topic}\"}]\n\n        # Attach to server(s)\n        from fastmcp import FastMCP\n        server = FastMCP(\"MyServer\", providers=[provider])\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        on_duplicate: DuplicateBehavior = \"error\",\n    ) -> None:\n        \"\"\"Initialize a LocalProvider with empty storage.\n\n        Args:\n            on_duplicate: Behavior when adding a component that already exists:\n                - \"error\": Raise ValueError\n                - \"warn\": Log warning and replace\n                - \"replace\": Silently replace\n                - \"ignore\": Keep existing, return it\n        \"\"\"\n        super().__init__()\n        self._on_duplicate = on_duplicate\n        # Unified component storage - keyed by prefixed key (e.g., \"tool:name\", \"resource:uri\")\n        self._components: dict[str, FastMCPComponent] = {}\n\n    # =========================================================================\n    # Storage methods\n    # =========================================================================\n\n    def _get_component_identity(self, component: FastMCPComponent) -> tuple[type, str]:\n        \"\"\"Get the identity (type, name/uri) for a component.\n\n        Returns:\n            A tuple of (component_type, logical_name) where logical_name is\n            the name for tools/prompts or URI for resources/templates.\n        \"\"\"\n        if isinstance(component, Tool):\n            return (Tool, component.name)\n        elif isinstance(component, ResourceTemplate):\n            return (ResourceTemplate, component.uri_template)\n        elif isinstance(component, Resource):\n            return (Resource, str(component.uri))\n        elif isinstance(component, Prompt):\n            return (Prompt, component.name)\n        else:\n            # Fall back to key without version suffix\n            key = component.key\n            base_key = key.rsplit(\"@\", 1)[0] if \"@\" in key else key\n            return (type(component), base_key)\n\n    def _check_version_mixing(self, component: _C) -> None:\n        \"\"\"Check that versioned and unversioned components aren't mixed.\n\n        LocalProvider enforces a simple rule: for any given name/URI, all\n        registered components must either be versioned or unversioned, not both.\n        This prevents confusing situations where unversioned components can't\n        be filtered out by version filters.\n\n        Args:\n            component: The component being added.\n\n        Raises:\n            ValueError: If adding would mix versioned and unversioned components.\n        \"\"\"\n        comp_type, logical_name = self._get_component_identity(component)\n        is_versioned = component.version is not None\n\n        # Check all existing components of the same type and logical name\n        for existing in self._components.values():\n            if not isinstance(existing, comp_type):\n                continue\n\n            _, existing_name = self._get_component_identity(existing)\n            if existing_name != logical_name:\n                continue\n\n            existing_versioned = existing.version is not None\n            if is_versioned != existing_versioned:\n                type_name = comp_type.__name__.lower()\n                if is_versioned:\n                    raise ValueError(\n                        f\"Cannot add versioned {type_name} {logical_name!r} \"\n                        f\"(version={component.version!r}): an unversioned \"\n                        f\"{type_name} with this name already exists. \"\n                        f\"Either version all components or none.\"\n                    )\n                else:\n                    raise ValueError(\n                        f\"Cannot add unversioned {type_name} {logical_name!r}: \"\n                        f\"versioned {type_name}s with this name already exist \"\n                        f\"(e.g., version={existing.version!r}). \"\n                        f\"Either version all components or none.\"\n                    )\n\n    def _add_component(self, component: _C) -> _C:\n        \"\"\"Add a component to unified storage.\n\n        Args:\n            component: The component to add.\n\n        Returns:\n            The component that was added (or existing if on_duplicate=\"ignore\").\n        \"\"\"\n        existing = self._components.get(component.key)\n        if existing:\n            if self._on_duplicate == \"error\":\n                raise ValueError(f\"Component already exists: {component.key}\")\n            elif self._on_duplicate == \"warn\":\n                logger.warning(f\"Component already exists: {component.key}\")\n            elif self._on_duplicate == \"ignore\":\n                return existing  # type: ignore[return-value]\n            # \"replace\" and \"warn\" fall through to add\n\n        # Check for versioned/unversioned mixing before adding\n        self._check_version_mixing(component)\n\n        self._components[component.key] = component\n        return component\n\n    def _remove_component(self, key: str) -> None:\n        \"\"\"Remove a component from unified storage.\n\n        Args:\n            key: The prefixed key of the component.\n\n        Raises:\n            KeyError: If the component is not found.\n        \"\"\"\n        component = self._components.get(key)\n        if component is None:\n            raise KeyError(f\"Component {key!r} not found\")\n\n        del self._components[key]\n\n    def _get_component(self, key: str) -> FastMCPComponent | None:\n        \"\"\"Get a component by its prefixed key.\n\n        Args:\n            key: The prefixed key (e.g., \"tool:name\", \"resource:uri\").\n\n        Returns:\n            The component, or None if not found.\n        \"\"\"\n        return self._components.get(key)\n\n    def remove_tool(self, name: str, version: str | None = None) -> None:\n        \"\"\"Remove tool(s) from this provider's storage.\n\n        Args:\n            name: The tool name.\n            version: If None, removes ALL versions. If specified, removes only that version.\n\n        Raises:\n            KeyError: If no matching tool is found.\n        \"\"\"\n        if version is None:\n            # Remove all versions\n            keys_to_remove = [\n                k\n                for k, c in self._components.items()\n                if isinstance(c, Tool) and c.name == name\n            ]\n            if not keys_to_remove:\n                raise KeyError(f\"Tool {name!r} not found\")\n            for key in keys_to_remove:\n                self._remove_component(key)\n        else:\n            # Remove specific version - key format is \"tool:name@version\"\n            key = f\"{Tool.make_key(name)}@{version}\"\n            if key not in self._components:\n                raise KeyError(f\"Tool {name!r} version {version!r} not found\")\n            self._remove_component(key)\n\n    def remove_resource(self, uri: str, version: str | None = None) -> None:\n        \"\"\"Remove resource(s) from this provider's storage.\n\n        Args:\n            uri: The resource URI.\n            version: If None, removes ALL versions. If specified, removes only that version.\n\n        Raises:\n            KeyError: If no matching resource is found.\n        \"\"\"\n        if version is None:\n            # Remove all versions\n            keys_to_remove = [\n                k\n                for k, c in self._components.items()\n                if isinstance(c, Resource) and str(c.uri) == uri\n            ]\n            if not keys_to_remove:\n                raise KeyError(f\"Resource {uri!r} not found\")\n            for key in keys_to_remove:\n                self._remove_component(key)\n        else:\n            # Remove specific version\n            key = f\"{Resource.make_key(uri)}@{version}\"\n            if key not in self._components:\n                raise KeyError(f\"Resource {uri!r} version {version!r} not found\")\n            self._remove_component(key)\n\n    def remove_template(self, uri_template: str, version: str | None = None) -> None:\n        \"\"\"Remove resource template(s) from this provider's storage.\n\n        Args:\n            uri_template: The template URI pattern.\n            version: If None, removes ALL versions. If specified, removes only that version.\n\n        Raises:\n            KeyError: If no matching template is found.\n        \"\"\"\n        if version is None:\n            # Remove all versions\n            keys_to_remove = [\n                k\n                for k, c in self._components.items()\n                if isinstance(c, ResourceTemplate) and c.uri_template == uri_template\n            ]\n            if not keys_to_remove:\n                raise KeyError(f\"Template {uri_template!r} not found\")\n            for key in keys_to_remove:\n                self._remove_component(key)\n        else:\n            # Remove specific version\n            key = f\"{ResourceTemplate.make_key(uri_template)}@{version}\"\n            if key not in self._components:\n                raise KeyError(\n                    f\"Template {uri_template!r} version {version!r} not found\"\n                )\n            self._remove_component(key)\n\n    def remove_prompt(self, name: str, version: str | None = None) -> None:\n        \"\"\"Remove prompt(s) from this provider's storage.\n\n        Args:\n            name: The prompt name.\n            version: If None, removes ALL versions. If specified, removes only that version.\n\n        Raises:\n            KeyError: If no matching prompt is found.\n        \"\"\"\n        if version is None:\n            # Remove all versions\n            keys_to_remove = [\n                k\n                for k, c in self._components.items()\n                if isinstance(c, Prompt) and c.name == name\n            ]\n            if not keys_to_remove:\n                raise KeyError(f\"Prompt {name!r} not found\")\n            for key in keys_to_remove:\n                self._remove_component(key)\n        else:\n            # Remove specific version\n            key = f\"{Prompt.make_key(name)}@{version}\"\n            if key not in self._components:\n                raise KeyError(f\"Prompt {name!r} version {version!r} not found\")\n            self._remove_component(key)\n\n    # =========================================================================\n    # Provider interface implementation\n    # =========================================================================\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"Return all tools.\"\"\"\n        return [v for v in self._components.values() if isinstance(v, Tool)]\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name.\n\n        Args:\n            name: The tool name.\n            version: Optional version filter. If None, returns highest version.\n        \"\"\"\n        matching = [\n            v\n            for v in self._components.values()\n            if isinstance(v, Tool) and v.name == name\n        ]\n        if version:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"Return all resources.\"\"\"\n        return [v for v in self._components.values() if isinstance(v, Resource)]\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI.\n\n        Args:\n            uri: The resource URI.\n            version: Optional version filter. If None, returns highest version.\n        \"\"\"\n        matching = [\n            v\n            for v in self._components.values()\n            if isinstance(v, Resource) and str(v.uri) == uri\n        ]\n        if version:\n            matching = [r for r in matching if version.matches(r.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"Return all resource templates.\"\"\"\n        return [v for v in self._components.values() if isinstance(v, ResourceTemplate)]\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template that matches the given URI.\n\n        Args:\n            uri: The URI to match against templates.\n            version: Optional version filter. If None, returns highest version.\n        \"\"\"\n        # Find all templates that match the URI\n        matching = [\n            component\n            for component in self._components.values()\n            if isinstance(component, ResourceTemplate)\n            and component.matches(uri) is not None\n        ]\n        if version:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"Return all prompts.\"\"\"\n        return [v for v in self._components.values() if isinstance(v, Prompt)]\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a prompt by name.\n\n        Args:\n            name: The prompt name.\n            version: Optional version filter. If None, returns highest version.\n        \"\"\"\n        matching = [\n            v\n            for v in self._components.values()\n            if isinstance(v, Prompt) and v.name == name\n        ]\n        if version:\n            matching = [p for p in matching if version.matches(p.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    # =========================================================================\n    # Task registration\n    # =========================================================================\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Return components eligible for background task execution.\n\n        Returns components that have task_config.mode != 'forbidden'.\n        This includes both FunctionTool/Resource/Prompt instances created via\n        decorators and custom Tool/Resource/Prompt subclasses.\n        \"\"\"\n        return [c for c in self._components.values() if c.task_config.supports_tasks()]\n\n    # =========================================================================\n    # Decorator methods\n    # =========================================================================\n    # Note: Decorator methods (tool, resource, prompt, add_tool, add_resource,\n    # add_template, add_prompt) are provided by mixin classes:\n    # - ToolDecoratorMixin\n    # - ResourceDecoratorMixin\n    # - PromptDecoratorMixin\n"
  },
  {
    "path": "src/fastmcp/server/providers/openapi/README.md",
    "content": "# OpenAPI Server Implementation (New)\n\nThis directory contains the next-generation FastMCP server implementation for OpenAPI integration, designed to replace the legacy implementation in `/server/openapi.py`.\n\n## Architecture Overview\n\nThe new implementation uses a **stateless request building approach** with `openapi-core` and `RequestDirector`, providing zero-latency startup and robust OpenAPI support optimized for serverless environments.\n\n### Core Components\n\n1. **`server.py`** - `FastMCPOpenAPI` main server class with RequestDirector integration\n2. **`components.py`** - Simplified component implementations using RequestDirector\n3. **`routing.py`** - Route mapping and component selection logic\n\n### Key Architecture Principles\n\n#### 1. Stateless Performance\n- **Zero Startup Latency**: No code generation or heavy initialization\n- **RequestDirector**: Stateless HTTP request building using openapi-core\n- **Pre-calculated Schemas**: All complex processing done during parsing\n\n#### 2. Unified Implementation\n- **Single Code Path**: All components use RequestDirector consistently\n- **No Fallbacks**: Simplified architecture without hybrid complexity\n- **Performance First**: Optimized for cold starts and serverless deployments\n\n#### 3. OpenAPI Compliance\n- **openapi-core Integration**: Leverages proven library for parameter serialization\n- **Full Feature Support**: Complete OpenAPI 3.0/3.1 support including deepObject\n- **Error Handling**: Comprehensive HTTP error mapping to MCP errors\n\n## Component Classes\n\n### RequestDirector-Based Components\n\n#### `OpenAPITool`\n- Executes operations using RequestDirector for HTTP request building\n- Automatic parameter validation and OpenAPI-compliant serialization\n- Built-in error handling and structured response processing\n- **Advantages**: Zero latency, robust, comprehensive OpenAPI support\n\n#### `OpenAPIResource` / `OpenAPIResourceTemplate`  \n- Provides resource access using RequestDirector\n- Consistent parameter handling across all resource types\n- Support for complex parameter patterns and collision resolution\n- **Advantages**: High performance, simplified architecture, reliable error handling\n\n## Server Implementation\n\n### `FastMCPOpenAPI` Class\n\nThe main server class orchestrates the stateless request building approach:\n\n```python\nclass FastMCPOpenAPI(FastMCP):\n    def __init__(self, openapi_spec: dict, client: httpx.AsyncClient, **kwargs):\n        # 1. Parse OpenAPI spec to HTTP routes with pre-calculated schemas\n        self._routes = parse_openapi_to_http_routes(openapi_spec)\n        \n        # 2. Initialize RequestDirector with openapi-core Spec\n        self._spec = Spec.from_dict(openapi_spec)\n        self._director = RequestDirector(self._spec)\n            \n        # 3. Create components using RequestDirector\n        self._create_components()\n```\n\n### Component Creation Logic\n\n```python\ndef _create_tool(self, route: HTTPRoute) -> Tool:\n    # All tools use RequestDirector for consistent, high-performance request building\n    return OpenAPITool(\n        client=self._client, \n        route=route, \n        director=self._director,\n        name=tool_name,\n        description=description,\n        parameters=flat_param_schema\n    )\n```\n\n## Data Flow\n\n### Stateless Request Building\n\n```\nOpenAPI Spec → HTTPRoute with Pre-calculated Fields → RequestDirector → HTTP Request → Structured Response\n```\n\n1. **Spec Parsing**: OpenAPI spec parsed to `HTTPRoute` models with pre-calculated schemas\n2. **RequestDirector Setup**: openapi-core Spec initialized for request building\n3. **Component Creation**: Create components with RequestDirector reference\n4. **Request Building**: RequestDirector builds HTTP request from flat parameters\n5. **Request Execution**: Execute request with httpx client\n6. **Response Processing**: Return structured MCP response\n\n## Key Features\n\n### 1. Enhanced Parameter Handling\n\n#### Parameter Collision Resolution\n- **Automatic Suffixing**: Colliding parameters get location-based suffixes\n- **Example**: `id` in path and body becomes `id__path` and `id`\n- **Transparent**: LLMs see suffixed parameters, implementation routes correctly\n\n#### DeepObject Style Support\n- **Native Support**: Generated client handles all deepObject variations\n- **Explode Handling**: Proper support for explode=true/false\n- **Complex Objects**: Nested object serialization works correctly\n\n### 2. Robust Error Handling\n\n#### HTTP Error Mapping\n- **Status Code Mapping**: HTTP errors mapped to appropriate MCP errors\n- **Structured Responses**: Error details preserved in tool results\n- **Timeout Handling**: Network timeouts handled gracefully\n\n#### Request Building Error Handling\n- **Parameter Validation**: Invalid parameters caught during request building\n- **Schema Validation**: openapi-core validates all OpenAPI constraints\n- **Graceful Degradation**: Missing optional parameters handled smoothly\n\n### 3. Performance Optimizations\n\n#### Efficient Client Reuse\n- **Connection Pooling**: HTTP connections reused across requests\n- **Client Caching**: Generated clients cached for performance\n- **Async Support**: Full async/await throughout\n\n#### Request Optimization\n- **Pre-calculated Schemas**: All complex processing done during initialization\n- **Parameter Mapping**: Collision resolution handled upfront\n- **Zero Latency**: No runtime code generation or complex schema processing\n\n## Configuration\n\n### Server Options\n\n```python\nserver = FastMCPOpenAPI(\n    openapi_spec=spec,           # Required: OpenAPI specification\n    client=httpx_client,         # Required: HTTP client instance\n    name=\"API Server\",           # Optional: Server name\n    route_map=custom_routes,     # Optional: Custom route mappings\n    enable_caching=True,         # Optional: Enable response caching\n)\n```\n\n### Route Mapping Customization\n\n```python\nfrom fastmcp.server.openapi_new.routing import RouteMap\n\ncustom_routes = RouteMap({\n    \"GET:/users\": \"tool\",        # Force specific operations to be tools\n    \"GET:/status\": \"resource\",   # Force specific operations to be resources\n})\n```\n\n## Testing Strategy\n\n### Test Structure\n\nTests are organized by functionality:\n- `test_server.py` - Server integration and RequestDirector behavior\n- `test_parameter_collisions.py` - Parameter collision handling\n- `test_deepobject_style.py` - DeepObject parameter style support\n- `test_openapi_features.py` - General OpenAPI feature compliance\n\n### Testing Philosophy\n\n1. **Real Integration**: Test with real OpenAPI specs and HTTP clients\n2. **Minimal Mocking**: Only mock external API endpoints\n3. **Behavioral Focus**: Test behavior, not implementation details\n4. **Performance Focus**: Test that initialization is fast and stateless\n\n### Example Test Pattern\n\n```python\nasync def test_stateless_request_building():\n    \"\"\"Test that server works with stateless RequestDirector approach.\"\"\"\n    \n    # Test server initialization is fast\n    start_time = time.time()\n    server = FastMCPOpenAPI(spec=valid_spec, client=client)\n    init_time = time.time() - start_time\n    assert init_time < 0.01  # Should be very fast\n    \n    # Verify RequestDirector functionality\n    assert hasattr(server, '_director')\n    assert hasattr(server, '_spec')\n```\n\n## Migration Benefits\n\n### From Legacy Implementation\n\n1. **Eliminated Startup Latency**: Zero code generation overhead (100-200ms improvement)\n2. **Better OpenAPI Compliance**: openapi-core handles all OpenAPI features correctly\n3. **Serverless Friendly**: Perfect for cold-start environments\n4. **Simplified Architecture**: Single RequestDirector approach eliminates complexity\n5. **Enhanced Reliability**: No dynamic code generation failures\n\n### Backward Compatibility\n\n- **Same Interface**: Public API unchanged from legacy implementation\n- **Performance Improvement**: Significantly faster initialization\n- **No Breaking Changes**: Existing code works without modification\n\n## Monitoring and Debugging\n\n### Logging\n\n```python\n# Enable debug logging to see implementation choices\nimport logging\nlogging.getLogger(\"fastmcp.server.openapi_new\").setLevel(logging.DEBUG)\n```\n\n### Key Log Messages\n- **RequestDirector Initialization**: Success/failure of RequestDirector setup\n- **Schema Pre-calculation**: Pre-calculated schema and parameter map status\n- **Request Building**: Parameter mapping and URL construction details\n- **Performance Metrics**: Request timing and error rates\n\n### Debugging Common Issues\n\n1. **RequestDirector Initialization Fails**\n   - Check OpenAPI spec validity with `openapi-core`\n   - Verify spec format is correct JSON/YAML\n   - Ensure all required OpenAPI fields are present\n\n2. **Parameter Issues**\n   - Enable debug logging for parameter processing\n   - Check for parameter collision warnings\n   - Verify OpenAPI spec parameter definitions\n\n3. **Performance Issues**\n   - Monitor RequestDirector request building timing\n   - Check HTTP client configuration\n   - Review response processing timing\n\n## Future Enhancements\n\n### Planned Features\n\n1. **Advanced Caching**: Intelligent response caching with TTL\n2. **Streaming Support**: Handle streaming API responses\n3. **Batch Operations**: Optimize multiple operation calls\n4. **Enhanced Monitoring**: Detailed metrics and health checks\n5. **Configuration Management**: Dynamic configuration updates\n\n### Performance Improvements\n\n1. **Enhanced Schema Caching**: More aggressive schema pre-calculation\n2. **Parallel Processing**: Concurrent operation execution\n3. **Memory Optimization**: Further reduce memory footprint\n4. **Request Optimization**: Smart request batching and deduplication\n\n## Related Documentation\n\n- `/utilities/openapi_new/README.md` - Utility implementation details\n- `/server/openapi/README.md` - Legacy implementation reference\n- `/tests/server/openapi_new/` - Comprehensive test suite\n- Project documentation on OpenAPI integration patterns"
  },
  {
    "path": "src/fastmcp/server/providers/openapi/__init__.py",
    "content": "\"\"\"OpenAPI provider for FastMCP.\n\nThis module provides OpenAPI integration for FastMCP through the Provider pattern.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.openapi import OpenAPIProvider\n    import httpx\n\n    client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n    provider = OpenAPIProvider(openapi_spec=spec, client=client)\n    mcp = FastMCP(\"API Server\", providers=[provider])\n    ```\n\"\"\"\n\nfrom fastmcp.server.providers.openapi.components import (\n    OpenAPIResource,\n    OpenAPIResourceTemplate,\n    OpenAPITool,\n)\nfrom fastmcp.server.providers.openapi.provider import OpenAPIProvider\nfrom fastmcp.server.providers.openapi.routing import (\n    ComponentFn,\n    MCPType,\n    RouteMap,\n    RouteMapFn,\n)\n\n__all__ = [\n    \"ComponentFn\",\n    \"MCPType\",\n    \"OpenAPIProvider\",\n    \"OpenAPIResource\",\n    \"OpenAPIResourceTemplate\",\n    \"OpenAPITool\",\n    \"RouteMap\",\n    \"RouteMapFn\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/providers/openapi/components.py",
    "content": "\"\"\"OpenAPI component classes: Tool, Resource, and ResourceTemplate.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport warnings\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nimport httpx\nfrom mcp.types import ToolAnnotations\nfrom pydantic.networks import AnyUrl\n\nimport fastmcp\nfrom fastmcp.resources import (\n    Resource,\n    ResourceContent,\n    ResourceResult,\n    ResourceTemplate,\n)\nfrom fastmcp.server.dependencies import get_http_headers\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.openapi import HTTPRoute\nfrom fastmcp.utilities.openapi.director import RequestDirector\n\nif TYPE_CHECKING:\n    from fastmcp.server import Context\n\n_SAFE_HEADERS = frozenset(\n    {\n        \"accept\",\n        \"accept-encoding\",\n        \"accept-language\",\n        \"cache-control\",\n        \"connection\",\n        \"content-length\",\n        \"content-type\",\n        \"host\",\n        \"user-agent\",\n    }\n)\n\n\ndef _redact_headers(headers: httpx.Headers) -> dict[str, str]:\n    return {k: v if k.lower() in _SAFE_HEADERS else \"***\" for k, v in headers.items()}\n\n\n__all__ = [\n    \"OpenAPIResource\",\n    \"OpenAPIResourceTemplate\",\n    \"OpenAPITool\",\n    \"_extract_mime_type_from_route\",\n]\n\nlogger = get_logger(__name__)\n\n# Default MIME type when no response content type can be inferred\n_DEFAULT_MIME_TYPE = \"application/json\"\n\n\ndef _extract_mime_type_from_route(route: HTTPRoute) -> str:\n    \"\"\"Extract the primary MIME type from an HTTPRoute's response definitions.\n\n    Looks for the first successful response (2xx) and returns its content type.\n    Prefers JSON-compatible types when multiple are available.\n    Falls back to \"application/json\" when no response content type is declared.\n    \"\"\"\n    if not route.responses:\n        return _DEFAULT_MIME_TYPE\n\n    # Priority order for success status codes\n    success_codes = [\"200\", \"201\", \"202\", \"204\"]\n\n    response_info = None\n    for status_code in success_codes:\n        if status_code in route.responses:\n            response_info = route.responses[status_code]\n            break\n\n    # If no explicit success codes, try any 2xx response\n    if response_info is None:\n        for status_code, resp_info in route.responses.items():\n            if status_code.startswith(\"2\"):\n                response_info = resp_info\n                break\n\n    if response_info is None or not response_info.content_schema:\n        return _DEFAULT_MIME_TYPE\n\n    # If there's only one content type, use it directly\n    content_types = list(response_info.content_schema.keys())\n    if len(content_types) == 1:\n        return content_types[0]\n\n    # When multiple types exist, prefer JSON-compatible types\n    json_compatible_types = [\n        \"application/json\",\n        \"application/vnd.api+json\",\n        \"application/hal+json\",\n        \"application/ld+json\",\n        \"text/json\",\n    ]\n    for ct in json_compatible_types:\n        if ct in response_info.content_schema:\n            return ct\n\n    # Fall back to the first available content type\n    return content_types[0]\n\n\ndef _slugify(text: str) -> str:\n    \"\"\"Convert text to a URL-friendly slug format.\n\n    Only contains lowercase letters, uppercase letters, numbers, and underscores.\n    \"\"\"\n    if not text:\n        return \"\"\n\n    # Replace spaces and common separators with underscores\n    slug = re.sub(r\"[\\s\\-\\.]+\", \"_\", text)\n\n    # Remove non-alphanumeric characters except underscores\n    slug = re.sub(r\"[^a-zA-Z0-9_]\", \"\", slug)\n\n    # Remove multiple consecutive underscores\n    slug = re.sub(r\"_+\", \"_\", slug)\n\n    # Remove leading/trailing underscores\n    slug = slug.strip(\"_\")\n\n    return slug\n\n\nclass OpenAPITool(Tool):\n    \"\"\"Tool implementation for OpenAPI endpoints.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n\n    def __init__(\n        self,\n        client: httpx.AsyncClient,\n        route: HTTPRoute,\n        director: RequestDirector,\n        name: str,\n        description: str,\n        parameters: dict[str, Any],\n        output_schema: dict[str, Any] | None = None,\n        tags: set[str] | None = None,\n        annotations: ToolAnnotations | None = None,\n        serializer: Callable[[Any], str] | None = None,  # Deprecated\n    ):\n        if serializer is not None and fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"The `serializer` parameter is deprecated. \"\n                \"Return ToolResult from your tools for full control over serialization. \"\n                \"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        super().__init__(\n            name=name,\n            description=description,\n            parameters=parameters,\n            output_schema=output_schema,\n            tags=tags or set(),\n            annotations=annotations,\n            serializer=serializer,\n        )\n        self._client = client\n        self._route = route\n        self._director = director\n\n    def __repr__(self) -> str:\n        return f\"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})\"\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        \"\"\"Execute the HTTP request using RequestDirector.\"\"\"\n        # Build the request — errors here are programming/schema issues,\n        # not HTTP failures, so we catch them separately.\n        try:\n            base_url = str(self._client.base_url) or \"http://localhost\"\n            request = self._director.build(self._route, arguments, base_url)\n\n            if self._client.headers:\n                for key, value in self._client.headers.items():\n                    if key not in request.headers:\n                        request.headers[key] = value\n\n            mcp_headers = get_http_headers()\n            if mcp_headers:\n                for key, value in mcp_headers.items():\n                    if key not in request.headers:\n                        request.headers[key] = value\n        except Exception as e:\n            raise ValueError(\n                f\"Error building request for {self._route.method.upper()} \"\n                f\"{self._route.path}: {type(e).__name__}: {e}\"\n            ) from e\n\n        # Send the request and process the response.\n        try:\n            logger.debug(\n                f\"run - sending request; headers: {_redact_headers(request.headers)}\"\n            )\n\n            response = await self._client.send(request)\n            response.raise_for_status()\n\n            # Try to parse as JSON first\n            try:\n                result = response.json()\n\n                # Handle structured content based on output schema\n                if self.output_schema is not None:\n                    if self.output_schema.get(\"x-fastmcp-wrap-result\"):\n                        structured_output = {\"result\": result}\n                    else:\n                        structured_output = result\n                elif not isinstance(result, dict):\n                    structured_output = {\"result\": result}\n                else:\n                    structured_output = result\n\n                # Structured content must be a dict for the MCP protocol.\n                # Wrap non-dict values that slipped through (e.g. a backend\n                # returning an array when the schema declared an object).\n                if not isinstance(structured_output, dict):\n                    structured_output = {\"result\": structured_output}\n\n                return ToolResult(structured_content=structured_output)\n            except json.JSONDecodeError:\n                return ToolResult(content=response.text)\n\n        except httpx.HTTPStatusError as e:\n            error_message = (\n                f\"HTTP error {e.response.status_code}: {e.response.reason_phrase}\"\n            )\n            try:\n                error_data = e.response.json()\n                error_message += f\" - {error_data}\"\n            except (json.JSONDecodeError, ValueError):\n                if e.response.text:\n                    error_message += f\" - {e.response.text}\"\n            raise ValueError(error_message) from e\n\n        except httpx.TimeoutException as e:\n            raise ValueError(f\"HTTP request timed out ({type(e).__name__})\") from e\n\n        except httpx.RequestError as e:\n            raise ValueError(f\"Request error ({type(e).__name__}): {e!s}\") from e\n\n\nclass OpenAPIResource(Resource):\n    \"\"\"Resource implementation for OpenAPI endpoints.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n\n    def __init__(\n        self,\n        client: httpx.AsyncClient,\n        route: HTTPRoute,\n        director: RequestDirector,\n        uri: str,\n        name: str,\n        description: str,\n        mime_type: str = \"application/json\",\n        tags: set[str] | None = None,\n    ):\n        super().__init__(\n            uri=AnyUrl(uri),\n            name=name,\n            description=description,\n            mime_type=mime_type,\n            tags=tags or set(),\n        )\n        self._client = client\n        self._route = route\n        self._director = director\n\n    def __repr__(self) -> str:\n        return f\"OpenAPIResource(name={self.name!r}, uri={self.uri!r}, path={self._route.path})\"\n\n    async def read(self) -> ResourceResult:\n        \"\"\"Fetch the resource data by making an HTTP request.\"\"\"\n        try:\n            path = self._route.path\n            resource_uri = str(self.uri)\n\n            # If this is a templated resource, extract path parameters from the URI\n            if \"{\" in path and \"}\" in path:\n                parts = resource_uri.split(\"/\")\n\n                if len(parts) > 1:\n                    path_params = {}\n                    param_matches = re.findall(r\"\\{([^}]+)\\}\", path)\n                    if param_matches:\n                        param_matches.sort(reverse=True)\n                        expected_param_count = len(parts) - 1\n                        for i, param_name in enumerate(param_matches):\n                            if i < expected_param_count:\n                                param_value = parts[-1 - i]\n                                path_params[param_name] = param_value\n\n                    for param_name, param_value in path_params.items():\n                        path = path.replace(f\"{{{param_name}}}\", str(param_value))\n\n            # Build headers with correct precedence\n            headers: dict[str, str] = {}\n            if self._client.headers:\n                headers.update(self._client.headers)\n            mcp_headers = get_http_headers()\n            if mcp_headers:\n                headers.update(mcp_headers)\n\n            response = await self._client.request(\n                method=self._route.method,\n                url=path,\n                headers=headers,\n            )\n            response.raise_for_status()\n\n            content_type = response.headers.get(\"content-type\", \"\").lower()\n\n            if \"application/json\" in content_type:\n                result = response.json()\n                return ResourceResult(\n                    contents=[\n                        ResourceContent(\n                            content=json.dumps(result), mime_type=\"application/json\"\n                        )\n                    ]\n                )\n            elif any(ct in content_type for ct in [\"text/\", \"application/xml\"]):\n                return ResourceResult(\n                    contents=[\n                        ResourceContent(content=response.text, mime_type=self.mime_type)\n                    ]\n                )\n            else:\n                return ResourceResult(\n                    contents=[\n                        ResourceContent(\n                            content=response.content, mime_type=self.mime_type\n                        )\n                    ]\n                )\n\n        except httpx.HTTPStatusError as e:\n            error_message = (\n                f\"HTTP error {e.response.status_code}: {e.response.reason_phrase}\"\n            )\n            try:\n                error_data = e.response.json()\n                error_message += f\" - {error_data}\"\n            except (json.JSONDecodeError, ValueError):\n                if e.response.text:\n                    error_message += f\" - {e.response.text}\"\n            raise ValueError(error_message) from e\n\n        except httpx.TimeoutException as e:\n            raise ValueError(f\"HTTP request timed out ({type(e).__name__})\") from e\n\n        except httpx.RequestError as e:\n            raise ValueError(f\"Request error ({type(e).__name__}): {e!s}\") from e\n\n\nclass OpenAPIResourceTemplate(ResourceTemplate):\n    \"\"\"Resource template implementation for OpenAPI endpoints.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n\n    def __init__(\n        self,\n        client: httpx.AsyncClient,\n        route: HTTPRoute,\n        director: RequestDirector,\n        uri_template: str,\n        name: str,\n        description: str,\n        parameters: dict[str, Any],\n        tags: set[str] | None = None,\n        mime_type: str = _DEFAULT_MIME_TYPE,\n    ):\n        super().__init__(\n            uri_template=uri_template,\n            name=name,\n            description=description,\n            parameters=parameters,\n            tags=tags or set(),\n            mime_type=mime_type,\n        )\n        self._client = client\n        self._route = route\n        self._director = director\n\n    def __repr__(self) -> str:\n        return f\"OpenAPIResourceTemplate(name={self.name!r}, uri_template={self.uri_template!r}, path={self._route.path})\"\n\n    async def create_resource(\n        self,\n        uri: str,\n        params: dict[str, Any],\n        context: Context | None = None,\n    ) -> Resource:\n        \"\"\"Create a resource with the given parameters.\"\"\"\n        uri_parts = [f\"{key}={value}\" for key, value in params.items()]\n\n        return OpenAPIResource(\n            client=self._client,\n            route=self._route,\n            director=self._director,\n            uri=uri,\n            name=f\"{self.name}-{'-'.join(uri_parts)}\",\n            description=self.description or f\"Resource for {self._route.path}\",\n            mime_type=self.mime_type,\n            tags=set(self._route.tags or []),\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/openapi/provider.py",
    "content": "\"\"\"OpenAPIProvider for creating MCP components from OpenAPI specifications.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections import Counter\nfrom collections.abc import AsyncIterator, Sequence\nfrom contextlib import asynccontextmanager\nfrom typing import Any, Literal, cast\n\nimport httpx\nfrom jsonschema_path import SchemaPath\n\nfrom fastmcp.prompts import Prompt\nfrom fastmcp.resources import Resource, ResourceTemplate\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.providers.openapi.components import (\n    OpenAPIResource,\n    OpenAPIResourceTemplate,\n    OpenAPITool,\n    _extract_mime_type_from_route,\n    _slugify,\n)\nfrom fastmcp.server.providers.openapi.routing import (\n    DEFAULT_ROUTE_MAPPINGS,\n    ComponentFn,\n    MCPType,\n    RouteMap,\n    RouteMapFn,\n    _determine_route_type,\n)\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.openapi import (\n    HTTPRoute,\n    extract_output_schema_from_responses,\n    parse_openapi_to_http_routes,\n)\nfrom fastmcp.utilities.openapi.director import RequestDirector\nfrom fastmcp.utilities.versions import VersionSpec, version_sort_key\n\n__all__ = [\n    \"OpenAPIProvider\",\n]\n\nlogger = get_logger(__name__)\n\nDEFAULT_TIMEOUT: float = 30.0\n\n\nclass OpenAPIProvider(Provider):\n    \"\"\"Provider that creates MCP components from an OpenAPI specification.\n\n    Components are created eagerly during initialization by parsing the OpenAPI\n    spec. Each component makes HTTP calls to the described API endpoints.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.providers.openapi import OpenAPIProvider\n        import httpx\n\n        client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n        provider = OpenAPIProvider(openapi_spec=spec, client=client)\n\n        mcp = FastMCP(\"API Server\")\n        mcp.add_provider(provider)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        openapi_spec: dict[str, Any],\n        client: httpx.AsyncClient | None = None,\n        *,\n        route_maps: list[RouteMap] | None = None,\n        route_map_fn: RouteMapFn | None = None,\n        mcp_component_fn: ComponentFn | None = None,\n        mcp_names: dict[str, str] | None = None,\n        tags: set[str] | None = None,\n        validate_output: bool = True,\n    ):\n        \"\"\"Initialize provider by parsing OpenAPI spec and creating components.\n\n        Args:\n            openapi_spec: OpenAPI schema as a dictionary\n            client: Optional httpx AsyncClient for making HTTP requests.\n                If not provided, a default client is created using the first\n                server URL from the OpenAPI spec with a 30-second timeout.\n                To customize timeout or other settings, pass your own client.\n            route_maps: Optional list of RouteMap objects defining route mappings\n            route_map_fn: Optional callable for advanced route type mapping\n            mcp_component_fn: Optional callable for component customization\n            mcp_names: Optional dictionary mapping operationId to component names\n            tags: Optional set of tags to add to all components\n            validate_output: If True (default), tools use the output schema\n                extracted from the OpenAPI spec for response validation. If\n                False, a permissive schema is used instead, allowing any\n                response structure while still returning structured JSON.\n        \"\"\"\n        super().__init__()\n\n        self._owns_client = client is None\n        if client is None:\n            client = self._create_default_client(openapi_spec)\n        self._client = client\n        self._mcp_component_fn = mcp_component_fn\n        self._validate_output = validate_output\n\n        # Keep track of names to detect collisions\n        self._used_names: dict[str, Counter[str]] = {\n            \"tool\": Counter(),\n            \"resource\": Counter(),\n            \"resource_template\": Counter(),\n            \"prompt\": Counter(),\n        }\n\n        # Pre-created component storage\n        self._tools: dict[str, OpenAPITool] = {}\n        self._resources: dict[str, OpenAPIResource] = {}\n        self._templates: dict[str, OpenAPIResourceTemplate] = {}\n\n        # Create openapi-core Spec and RequestDirector\n        try:\n            self._spec = SchemaPath.from_dict(cast(Any, openapi_spec))\n            self._director = RequestDirector(self._spec)\n        except Exception as e:\n            logger.exception(\"Failed to initialize RequestDirector\")\n            raise ValueError(f\"Invalid OpenAPI specification: {e}\") from e\n\n        http_routes = parse_openapi_to_http_routes(openapi_spec)\n\n        # Process routes\n        route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS\n        for route in http_routes:\n            route_map = _determine_route_type(route, route_maps)\n            route_type = route_map.mcp_type\n\n            if route_map_fn is not None:\n                try:\n                    result = route_map_fn(route, route_type)\n                    if result is not None:\n                        route_type = result\n                        logger.debug(\n                            f\"Route {route.method} {route.path} mapping customized: \"\n                            f\"type={route_type.name}\"\n                        )\n                except Exception as e:\n                    logger.warning(\n                        f\"Error in route_map_fn for {route.method} {route.path}: {e}. \"\n                        f\"Using default values.\"\n                    )\n\n            component_name = self._generate_default_name(route, mcp_names)\n            route_tags = set(route.tags) | route_map.mcp_tags | (tags or set())\n\n            if route_type == MCPType.TOOL:\n                self._create_openapi_tool(route, component_name, tags=route_tags)\n            elif route_type == MCPType.RESOURCE:\n                self._create_openapi_resource(route, component_name, tags=route_tags)\n            elif route_type == MCPType.RESOURCE_TEMPLATE:\n                self._create_openapi_template(route, component_name, tags=route_tags)\n            elif route_type == MCPType.EXCLUDE:\n                logger.debug(f\"Excluding route: {route.method} {route.path}\")\n\n        logger.debug(f\"Created OpenAPIProvider with {len(http_routes)} routes\")\n\n    @classmethod\n    def _create_default_client(cls, openapi_spec: dict[str, Any]) -> httpx.AsyncClient:\n        \"\"\"Create a default httpx client from the OpenAPI spec's server URL.\"\"\"\n        servers = openapi_spec.get(\"servers\", [])\n        if not servers or not servers[0].get(\"url\"):\n            raise ValueError(\n                \"No server URL found in OpenAPI spec. Either add a 'servers' \"\n                \"entry to the spec or provide an httpx.AsyncClient explicitly.\"\n            )\n        base_url = servers[0][\"url\"]\n        return httpx.AsyncClient(base_url=base_url, timeout=DEFAULT_TIMEOUT)\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        \"\"\"Manage the lifecycle of the auto-created httpx client.\"\"\"\n        if self._owns_client:\n            async with self._client:\n                yield\n        else:\n            yield\n\n    def _generate_default_name(\n        self, route: HTTPRoute, mcp_names_map: dict[str, str] | None = None\n    ) -> str:\n        \"\"\"Generate a default name from the route.\"\"\"\n        mcp_names_map = mcp_names_map or {}\n\n        if route.operation_id:\n            if route.operation_id in mcp_names_map:\n                name = mcp_names_map[route.operation_id]\n            else:\n                name = route.operation_id.split(\"__\")[0]\n        else:\n            name = route.summary or f\"{route.method}_{route.path}\"\n\n        name = _slugify(name)\n\n        if len(name) > 56:\n            name = name[:56]\n\n        return name\n\n    def _get_unique_name(\n        self,\n        name: str,\n        component_type: Literal[\"tool\", \"resource\", \"resource_template\", \"prompt\"],\n    ) -> str:\n        \"\"\"Ensure the name is unique by appending numbers if needed.\"\"\"\n        self._used_names[component_type][name] += 1\n        if self._used_names[component_type][name] == 1:\n            return name\n\n        new_name = f\"{name}_{self._used_names[component_type][name]}\"\n        logger.debug(\n            f\"Name collision: '{name}' exists as {component_type}. Using '{new_name}'.\"\n        )\n        return new_name\n\n    def _create_openapi_tool(\n        self,\n        route: HTTPRoute,\n        name: str,\n        tags: set[str],\n    ) -> None:\n        \"\"\"Create and register an OpenAPITool.\"\"\"\n        combined_schema = route.flat_param_schema\n        output_schema = extract_output_schema_from_responses(\n            route.responses,\n            route.response_schemas,\n            route.openapi_version,\n        )\n\n        if not self._validate_output and output_schema is not None:\n            # Use a permissive schema that accepts any object, preserving\n            # the wrap-result flag so non-object responses still get wrapped\n            permissive: dict[str, Any] = {\n                \"type\": \"object\",\n                \"additionalProperties\": True,\n            }\n            if output_schema.get(\"x-fastmcp-wrap-result\"):\n                permissive[\"x-fastmcp-wrap-result\"] = True\n            output_schema = permissive\n\n        tool_name = self._get_unique_name(name, \"tool\")\n        base_description = (\n            route.description\n            or route.summary\n            or f\"Executes {route.method} {route.path}\"\n        )\n\n        tool = OpenAPITool(\n            client=self._client,\n            route=route,\n            director=self._director,\n            name=tool_name,\n            description=base_description,\n            parameters=combined_schema,\n            output_schema=output_schema,\n            tags=set(route.tags or []) | tags,\n        )\n\n        if self._mcp_component_fn is not None:\n            try:\n                self._mcp_component_fn(route, tool)\n                logger.debug(f\"Tool {tool_name} customized by component_fn\")\n            except Exception as e:\n                logger.warning(f\"Error in component_fn for tool {tool_name}: {e}\")\n\n        self._tools[tool.name] = tool\n\n    def _create_openapi_resource(\n        self,\n        route: HTTPRoute,\n        name: str,\n        tags: set[str],\n    ) -> None:\n        \"\"\"Create and register an OpenAPIResource.\"\"\"\n        resource_name = self._get_unique_name(name, \"resource\")\n        resource_uri = f\"resource://{resource_name}\"\n        base_description = (\n            route.description or route.summary or f\"Represents {route.path}\"\n        )\n\n        resource = OpenAPIResource(\n            client=self._client,\n            route=route,\n            director=self._director,\n            uri=resource_uri,\n            name=resource_name,\n            description=base_description,\n            mime_type=_extract_mime_type_from_route(route),\n            tags=set(route.tags or []) | tags,\n        )\n\n        if self._mcp_component_fn is not None:\n            try:\n                self._mcp_component_fn(route, resource)\n                logger.debug(f\"Resource {resource_uri} customized by component_fn\")\n            except Exception as e:\n                logger.warning(\n                    f\"Error in component_fn for resource {resource_uri}: {e}\"\n                )\n\n        self._resources[str(resource.uri)] = resource\n\n    def _create_openapi_template(\n        self,\n        route: HTTPRoute,\n        name: str,\n        tags: set[str],\n    ) -> None:\n        \"\"\"Create and register an OpenAPIResourceTemplate.\"\"\"\n        template_name = self._get_unique_name(name, \"resource_template\")\n\n        path_params = sorted(p.name for p in route.parameters if p.location == \"path\")\n        uri_template_str = f\"resource://{template_name}\"\n        if path_params:\n            uri_template_str += \"/\" + \"/\".join(f\"{{{p}}}\" for p in path_params)\n\n        base_description = (\n            route.description or route.summary or f\"Template for {route.path}\"\n        )\n\n        template_params_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                p.name: {\n                    **(p.schema_.copy() if isinstance(p.schema_, dict) else {}),\n                    **(\n                        {\"description\": p.description}\n                        if p.description\n                        and not (\n                            isinstance(p.schema_, dict) and \"description\" in p.schema_\n                        )\n                        else {}\n                    ),\n                }\n                for p in route.parameters\n                if p.location == \"path\"\n            },\n            \"required\": [\n                p.name for p in route.parameters if p.location == \"path\" and p.required\n            ],\n        }\n\n        template = OpenAPIResourceTemplate(\n            client=self._client,\n            route=route,\n            director=self._director,\n            uri_template=uri_template_str,\n            name=template_name,\n            description=base_description,\n            parameters=template_params_schema,\n            tags=set(route.tags or []) | tags,\n            mime_type=_extract_mime_type_from_route(route),\n        )\n\n        if self._mcp_component_fn is not None:\n            try:\n                self._mcp_component_fn(route, template)\n                logger.debug(f\"Template {uri_template_str} customized by component_fn\")\n            except Exception as e:\n                logger.warning(\n                    f\"Error in component_fn for template {uri_template_str}: {e}\"\n                )\n\n        self._templates[template.uri_template] = template\n\n    # -------------------------------------------------------------------------\n    # Provider interface\n    # -------------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"Return all tools created from the OpenAPI spec.\"\"\"\n        return list(self._tools.values())\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name.\"\"\"\n        tool = self._tools.get(name)\n        if tool is None:\n            return None\n        if version is not None and not version.matches(tool.version):\n            return None\n        return tool\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"Return all resources created from the OpenAPI spec.\"\"\"\n        return list(self._resources.values())\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI.\"\"\"\n        resource = self._resources.get(uri)\n        if resource is None:\n            return None\n        if version is not None and not version.matches(resource.version):\n            return None\n        return resource\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"Return all resource templates created from the OpenAPI spec.\"\"\"\n        return list(self._templates.values())\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template that matches the given URI.\"\"\"\n        matching = [t for t in self._templates.values() if t.matches(uri) is not None]\n        if not matching:\n            return None\n        if version is not None:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"Return empty list - OpenAPI doesn't create prompts.\"\"\"\n        return []\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Return empty list - OpenAPI components don't support tasks.\"\"\"\n        return []\n"
  },
  {
    "path": "src/fastmcp/server/providers/openapi/routing.py",
    "content": "\"\"\"Route mapping logic for OpenAPI operations.\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\nimport re\nfrom collections.abc import Callable\nfrom dataclasses import dataclass, field\nfrom re import Pattern\nfrom typing import TYPE_CHECKING, Literal\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.openapi.components import (\n        OpenAPIResource,\n        OpenAPIResourceTemplate,\n        OpenAPITool,\n    )\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.openapi import HttpMethod, HTTPRoute\n\n__all__ = [\n    \"ComponentFn\",\n    \"MCPType\",\n    \"RouteMap\",\n    \"RouteMapFn\",\n]\n\nlogger = get_logger(__name__)\n\n# Type definitions for the mapping functions\nRouteMapFn = Callable[[HTTPRoute, \"MCPType\"], \"MCPType | None\"]\nComponentFn = Callable[\n    [\n        HTTPRoute,\n        \"OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate\",\n    ],\n    None,\n]\n\n\nclass MCPType(enum.Enum):\n    \"\"\"Type of FastMCP component to create from a route.\n\n    Enum values:\n        TOOL: Convert the route to a callable Tool\n        RESOURCE: Convert the route to a Resource (typically GET endpoints)\n        RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)\n        EXCLUDE: Exclude the route from being converted to any MCP component\n    \"\"\"\n\n    TOOL = \"TOOL\"\n    RESOURCE = \"RESOURCE\"\n    RESOURCE_TEMPLATE = \"RESOURCE_TEMPLATE\"\n    EXCLUDE = \"EXCLUDE\"\n\n\n@dataclass(kw_only=True)\nclass RouteMap:\n    \"\"\"Mapping configuration for HTTP routes to FastMCP component types.\"\"\"\n\n    methods: list[HttpMethod] | Literal[\"*\"] = field(default=\"*\")\n    pattern: Pattern[str] | str = field(default=r\".*\")\n\n    tags: set[str] = field(\n        default_factory=set,\n        metadata={\"description\": \"A set of tags to match. All tags must match.\"},\n    )\n    mcp_type: MCPType = field(\n        metadata={\"description\": \"The type of FastMCP component to create.\"},\n    )\n    mcp_tags: set[str] = field(\n        default_factory=set,\n        metadata={\n            \"description\": \"A set of tags to apply to the generated FastMCP component.\"\n        },\n    )\n\n\n# Default route mapping: all routes become tools.\nDEFAULT_ROUTE_MAPPINGS = [\n    RouteMap(mcp_type=MCPType.TOOL),\n]\n\n\ndef _determine_route_type(\n    route: HTTPRoute,\n    mappings: list[RouteMap],\n) -> RouteMap:\n    \"\"\"Determine the FastMCP component type based on the route and mappings.\"\"\"\n    for route_map in mappings:\n        if route_map.methods == \"*\" or route.method in route_map.methods:\n            if isinstance(route_map.pattern, Pattern):\n                pattern_matches = route_map.pattern.search(route.path)\n            else:\n                pattern_matches = re.search(route_map.pattern, route.path)\n\n            if pattern_matches:\n                if route_map.tags:\n                    route_tags_set = set(route.tags or [])\n                    if not route_map.tags.issubset(route_tags_set):\n                        continue\n\n                logger.debug(\n                    f\"Route {route.method} {route.path} mapped to {route_map.mcp_type.name}\"\n                )\n                return route_map\n\n    return RouteMap(mcp_type=MCPType.TOOL)\n"
  },
  {
    "path": "src/fastmcp/server/providers/proxy.py",
    "content": "\"\"\"ProxyProvider for proxying to remote MCP servers.\n\nThis module provides the `ProxyProvider` class that proxies components from\na remote MCP server via a client factory. It also provides proxy component\nclasses that forward execution to remote servers.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport inspect\nimport time\nfrom collections.abc import Awaitable, Callable, Sequence\nfrom typing import TYPE_CHECKING, Any, cast\nfrom urllib.parse import quote\n\nimport mcp.types\nfrom mcp import ServerSession\nfrom mcp.client.session import ClientSession\nfrom mcp.server.lowlevel.server import request_ctx\nfrom mcp.shared.context import LifespanContextT, RequestContext\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import (\n    METHOD_NOT_FOUND,\n    BlobResourceContents,\n    ElicitRequestFormParams,\n    TextResourceContents,\n)\nfrom pydantic.networks import AnyUrl\n\nfrom fastmcp.client.client import Client, FastMCP1Server\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.client.logging import LogMessage\nfrom fastmcp.client.roots import RootsList\nfrom fastmcp.client.telemetry import client_span\nfrom fastmcp.client.transports import ClientTransportT\nfrom fastmcp.exceptions import ResourceError, ToolError\nfrom fastmcp.mcp_config import MCPConfig\nfrom fastmcp.prompts import Message, Prompt, PromptResult\nfrom fastmcp.prompts.base import PromptArgument\nfrom fastmcp.resources import Resource, ResourceTemplate\nfrom fastmcp.resources.base import ResourceContent, ResourceResult\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.dependencies import get_context\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.components import FastMCPComponent, get_fastmcp_metadata\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.versions import VersionSpec, version_sort_key\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    from fastmcp.client.transports import ClientTransport\n\nlogger = get_logger(__name__)\n\n# Type alias for client factory functions\nClientFactoryT = Callable[[], Client] | Callable[[], Awaitable[Client]]\n\n\n# -----------------------------------------------------------------------------\n# Proxy Component Classes\n# -----------------------------------------------------------------------------\n\n\nclass ProxyTool(Tool):\n    \"\"\"A Tool that represents and executes a tool on a remote server.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n    _backend_name: str | None = None\n\n    def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):\n        super().__init__(**kwargs)\n        self._client_factory = client_factory\n\n    async def _get_client(self) -> Client:\n        \"\"\"Gets a client instance by calling the sync or async factory.\"\"\"\n        client = self._client_factory()\n        if inspect.isawaitable(client):\n            client = cast(Client, await client)\n        return client\n\n    def model_copy(self, **kwargs: Any) -> ProxyTool:\n        \"\"\"Override to preserve _backend_name when name changes.\"\"\"\n        update = kwargs.get(\"update\", {})\n        if \"name\" in update and self._backend_name is None:\n            # First time name is being changed, preserve original for backend calls\n            update = {**update, \"_backend_name\": self.name}\n            kwargs[\"update\"] = update\n        return super().model_copy(**kwargs)\n\n    @classmethod\n    def from_mcp_tool(\n        cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool\n    ) -> ProxyTool:\n        \"\"\"Factory method to create a ProxyTool from a raw MCP tool schema.\"\"\"\n        return cls(\n            client_factory=client_factory,\n            name=mcp_tool.name,\n            title=mcp_tool.title,\n            description=mcp_tool.description,\n            parameters=mcp_tool.inputSchema,\n            annotations=mcp_tool.annotations,\n            output_schema=mcp_tool.outputSchema,\n            icons=mcp_tool.icons,\n            meta=mcp_tool.meta,\n            tags=get_fastmcp_metadata(mcp_tool.meta).get(\"tags\", []),\n        )\n\n    async def run(\n        self,\n        arguments: dict[str, Any],\n        context: Context | None = None,\n    ) -> ToolResult:\n        \"\"\"Executes the tool by making a call through the client.\"\"\"\n        backend_name = self._backend_name or self.name\n        with client_span(\n            f\"tools/call {backend_name}\", \"tools/call\", backend_name\n        ) as span:\n            span.set_attribute(\"fastmcp.provider.type\", \"ProxyProvider\")\n            client = await self._get_client()\n            async with client:\n                ctx = context or get_context()\n                # StatefulProxyClient reuses sessions across requests, so\n                # its receive-loop task has stale ContextVars from the first\n                # request. Stash the current RequestContext in the shared\n                # ref so handlers can restore it before forwarding.\n                if isinstance(client, StatefulProxyClient):\n                    client._proxy_rc_ref[0] = (\n                        ctx.request_context,\n                        ctx._fastmcp,  # weakref to FastMCP, not the Context\n                    )\n                # Build meta dict from request context\n                meta: dict[str, Any] | None = None\n                if hasattr(ctx, \"request_context\"):\n                    req_ctx = ctx.request_context\n                    # Start with existing meta if present\n                    if hasattr(req_ctx, \"meta\") and req_ctx.meta:\n                        meta = dict(req_ctx.meta)\n                    # Add task metadata if this is a task request\n                    if (\n                        hasattr(req_ctx, \"experimental\")\n                        and hasattr(req_ctx.experimental, \"is_task\")\n                        and req_ctx.experimental.is_task\n                    ):\n                        task_metadata = req_ctx.experimental.task_metadata\n                        if task_metadata:\n                            meta = meta or {}\n                            meta[\"modelcontextprotocol.io/task\"] = (\n                                task_metadata.model_dump(exclude_none=True)\n                            )\n\n                result = await client.call_tool_mcp(\n                    name=backend_name, arguments=arguments, meta=meta\n                )\n            if result.isError:\n                raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)\n            # Preserve backend's meta (includes task metadata for background tasks)\n            return ToolResult(\n                content=result.content,\n                structured_content=result.structuredContent,\n                meta=result.meta,\n            )\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"ProxyProvider\",\n            \"fastmcp.proxy.backend_name\": self._backend_name,\n        }\n\n\nclass ProxyResource(Resource):\n    \"\"\"A Resource that represents and reads a resource from a remote server.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n    _cached_content: ResourceResult | None = None\n    _backend_uri: str | None = None\n\n    def __init__(\n        self,\n        client_factory: ClientFactoryT,\n        *,\n        _cached_content: ResourceResult | None = None,\n        **kwargs,\n    ):\n        super().__init__(**kwargs)\n        self._client_factory = client_factory\n        self._cached_content = _cached_content\n\n    async def _get_client(self) -> Client:\n        \"\"\"Gets a client instance by calling the sync or async factory.\"\"\"\n        client = self._client_factory()\n        if inspect.isawaitable(client):\n            client = cast(Client, await client)\n        return client\n\n    def model_copy(self, **kwargs: Any) -> ProxyResource:\n        \"\"\"Override to preserve _backend_uri when uri changes.\"\"\"\n        update = kwargs.get(\"update\", {})\n        if \"uri\" in update and self._backend_uri is None:\n            # First time uri is being changed, preserve original for backend calls\n            update = {**update, \"_backend_uri\": str(self.uri)}\n            kwargs[\"update\"] = update\n        return super().model_copy(**kwargs)\n\n    @classmethod\n    def from_mcp_resource(\n        cls,\n        client_factory: ClientFactoryT,\n        mcp_resource: mcp.types.Resource,\n    ) -> ProxyResource:\n        \"\"\"Factory method to create a ProxyResource from a raw MCP resource schema.\"\"\"\n\n        return cls(\n            client_factory=client_factory,\n            uri=mcp_resource.uri,\n            name=mcp_resource.name,\n            title=mcp_resource.title,\n            description=mcp_resource.description,\n            mime_type=mcp_resource.mimeType or \"text/plain\",\n            icons=mcp_resource.icons,\n            meta=mcp_resource.meta,\n            tags=get_fastmcp_metadata(mcp_resource.meta).get(\"tags\", []),\n            task_config=TaskConfig(mode=\"forbidden\"),\n        )\n\n    async def read(self) -> ResourceResult:\n        \"\"\"Read the resource content from the remote server.\"\"\"\n        if self._cached_content is not None:\n            return self._cached_content\n\n        backend_uri = self._backend_uri or str(self.uri)\n        with client_span(\n            f\"resources/read {backend_uri}\",\n            \"resources/read\",\n            backend_uri,\n            resource_uri=backend_uri,\n        ) as span:\n            span.set_attribute(\"fastmcp.provider.type\", \"ProxyProvider\")\n            client = await self._get_client()\n            async with client:\n                result = await client.read_resource(backend_uri)\n            if not result:\n                raise ResourceError(\n                    f\"Remote server returned empty content for {backend_uri}\"\n                )\n\n            # Process all items in the result list, not just the first one\n            contents: list[ResourceContent] = []\n            for item in result:\n                if isinstance(item, TextResourceContents):\n                    contents.append(\n                        ResourceContent(\n                            content=item.text,\n                            mime_type=item.mimeType,\n                            meta=item.meta,\n                        )\n                    )\n                elif isinstance(item, BlobResourceContents):\n                    contents.append(\n                        ResourceContent(\n                            content=base64.b64decode(item.blob),\n                            mime_type=item.mimeType,\n                            meta=item.meta,\n                        )\n                    )\n                else:\n                    raise ResourceError(f\"Unsupported content type: {type(item)}\")\n\n            return ResourceResult(contents=contents)\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"ProxyProvider\",\n            \"fastmcp.proxy.backend_uri\": self._backend_uri,\n        }\n\n\nclass ProxyTemplate(ResourceTemplate):\n    \"\"\"A ResourceTemplate that represents and creates resources from a remote server template.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n    _backend_uri_template: str | None = None\n\n    def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):\n        super().__init__(**kwargs)\n        self._client_factory = client_factory\n\n    async def _get_client(self) -> Client:\n        \"\"\"Gets a client instance by calling the sync or async factory.\"\"\"\n        client = self._client_factory()\n        if inspect.isawaitable(client):\n            client = cast(Client, await client)\n        return client\n\n    def model_copy(self, **kwargs: Any) -> ProxyTemplate:\n        \"\"\"Override to preserve _backend_uri_template when uri_template changes.\"\"\"\n        update = kwargs.get(\"update\", {})\n        if \"uri_template\" in update and self._backend_uri_template is None:\n            # First time uri_template is being changed, preserve original for backend\n            update = {**update, \"_backend_uri_template\": self.uri_template}\n            kwargs[\"update\"] = update\n        return super().model_copy(**kwargs)\n\n    @classmethod\n    def from_mcp_template(  # type: ignore[override]\n        cls, client_factory: ClientFactoryT, mcp_template: mcp.types.ResourceTemplate\n    ) -> ProxyTemplate:\n        \"\"\"Factory method to create a ProxyTemplate from a raw MCP template schema.\"\"\"\n\n        return cls(\n            client_factory=client_factory,\n            uri_template=mcp_template.uriTemplate,\n            name=mcp_template.name,\n            title=mcp_template.title,\n            description=mcp_template.description,\n            mime_type=mcp_template.mimeType or \"text/plain\",\n            icons=mcp_template.icons,\n            parameters={},  # Remote templates don't have local parameters\n            meta=mcp_template.meta,\n            tags=get_fastmcp_metadata(mcp_template.meta).get(\"tags\", []),\n            task_config=TaskConfig(mode=\"forbidden\"),\n        )\n\n    async def create_resource(\n        self,\n        uri: str,\n        params: dict[str, Any],\n        context: Context | None = None,\n    ) -> ProxyResource:\n        \"\"\"Create a resource from the template by calling the remote server.\"\"\"\n        # don't use the provided uri, because it may not be the same as the\n        # uri_template on the remote server.\n        # quote params to ensure they are valid for the uri_template\n        backend_template = self._backend_uri_template or self.uri_template\n        parameterized_uri = backend_template.format(\n            **{k: quote(v, safe=\"\") for k, v in params.items()}\n        )\n        client = await self._get_client()\n        async with client:\n            result = await client.read_resource(parameterized_uri)\n\n        if not result:\n            raise ResourceError(\n                f\"Remote server returned empty content for {parameterized_uri}\"\n            )\n\n        # Process all items in the result list, not just the first one\n        contents: list[ResourceContent] = []\n        for item in result:\n            if isinstance(item, TextResourceContents):\n                contents.append(\n                    ResourceContent(\n                        content=item.text,\n                        mime_type=item.mimeType,\n                        meta=item.meta,\n                    )\n                )\n            elif isinstance(item, BlobResourceContents):\n                contents.append(\n                    ResourceContent(\n                        content=base64.b64decode(item.blob),\n                        mime_type=item.mimeType,\n                        meta=item.meta,\n                    )\n                )\n            else:\n                raise ResourceError(f\"Unsupported content type: {type(item)}\")\n\n        cached_content = ResourceResult(contents=contents)\n\n        return ProxyResource(\n            client_factory=self._client_factory,\n            uri=parameterized_uri,\n            name=self.name,\n            title=self.title,\n            description=self.description,\n            mime_type=result[\n                0\n            ].mimeType,  # Use first item's mimeType for backward compatibility\n            icons=self.icons,\n            meta=self.meta,\n            tags=get_fastmcp_metadata(self.meta).get(\"tags\", []),\n            _cached_content=cached_content,\n        )\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"ProxyProvider\",\n            \"fastmcp.proxy.backend_uri_template\": self._backend_uri_template,\n        }\n\n\nclass ProxyPrompt(Prompt):\n    \"\"\"A Prompt that represents and renders a prompt from a remote server.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n    _backend_name: str | None = None\n\n    def __init__(self, client_factory: ClientFactoryT, **kwargs):\n        super().__init__(**kwargs)\n        self._client_factory = client_factory\n\n    async def _get_client(self) -> Client:\n        \"\"\"Gets a client instance by calling the sync or async factory.\"\"\"\n        client = self._client_factory()\n        if inspect.isawaitable(client):\n            client = cast(Client, await client)\n        return client\n\n    def model_copy(self, **kwargs: Any) -> ProxyPrompt:\n        \"\"\"Override to preserve _backend_name when name changes.\"\"\"\n        update = kwargs.get(\"update\", {})\n        if \"name\" in update and self._backend_name is None:\n            # First time name is being changed, preserve original for backend calls\n            update = {**update, \"_backend_name\": self.name}\n            kwargs[\"update\"] = update\n        return super().model_copy(**kwargs)\n\n    @classmethod\n    def from_mcp_prompt(\n        cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Prompt\n    ) -> ProxyPrompt:\n        \"\"\"Factory method to create a ProxyPrompt from a raw MCP prompt schema.\"\"\"\n        arguments = [\n            PromptArgument(\n                name=arg.name,\n                description=arg.description,\n                required=arg.required or False,\n            )\n            for arg in mcp_prompt.arguments or []\n        ]\n        return cls(\n            client_factory=client_factory,\n            name=mcp_prompt.name,\n            title=mcp_prompt.title,\n            description=mcp_prompt.description,\n            arguments=arguments,\n            icons=mcp_prompt.icons,\n            meta=mcp_prompt.meta,\n            tags=get_fastmcp_metadata(mcp_prompt.meta).get(\"tags\", []),\n            task_config=TaskConfig(mode=\"forbidden\"),\n        )\n\n    async def render(self, arguments: dict[str, Any]) -> PromptResult:  # type: ignore[override]\n        \"\"\"Render the prompt by making a call through the client.\"\"\"\n        backend_name = self._backend_name or self.name\n        with client_span(\n            f\"prompts/get {backend_name}\", \"prompts/get\", backend_name\n        ) as span:\n            span.set_attribute(\"fastmcp.provider.type\", \"ProxyProvider\")\n            client = await self._get_client()\n            async with client:\n                result = await client.get_prompt(backend_name, arguments)\n            # Convert GetPromptResult to PromptResult, preserving meta from result\n            # (not the static prompt meta which includes fastmcp tags)\n            # Convert PromptMessages to Messages\n            messages = [\n                Message(content=m.content, role=m.role) for m in result.messages\n            ]\n            return PromptResult(\n                messages=messages,\n                description=result.description,\n                meta=result.meta,\n            )\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.provider.type\": \"ProxyProvider\",\n            \"fastmcp.proxy.backend_name\": self._backend_name,\n        }\n\n\n# -----------------------------------------------------------------------------\n# ProxyProvider\n# -----------------------------------------------------------------------------\n\n\nclass _CacheEntry:\n    \"\"\"A cached sequence of components with a monotonic timestamp.\"\"\"\n\n    __slots__ = (\"items\", \"timestamp\")\n\n    def __init__(self, items: Sequence[Any], timestamp: float):\n        self.items = items\n        self.timestamp = timestamp\n\n    def is_fresh(self, ttl: float) -> bool:\n        return (time.monotonic() - self.timestamp) < ttl\n\n\n_DEFAULT_CACHE_TTL: float = 300.0\n\n\nclass ProxyProvider(Provider):\n    \"\"\"Provider that proxies to a remote MCP server via a client factory.\n\n    This provider fetches components from a remote server and returns Proxy*\n    component instances that forward execution to the remote server.\n\n    All components returned by this provider have task_config.mode=\"forbidden\"\n    because tasks cannot be executed through a proxy.\n\n    Component lists (tools, resources, templates, prompts) are cached so that\n    individual lookups (e.g. during ``call_tool``) can resolve from the cache\n    instead of opening a new backend connection.  The cache stores the\n    backend's raw component metadata and is shared across all sessions;\n    per-session visibility and auth filtering are applied after cache lookup\n    by the server layer.  The cache is refreshed whenever a ``list_*`` call\n    is made, and entries expire after ``cache_ttl`` seconds (default 300).\n    Set ``cache_ttl=0`` to disable caching.  Disabling is recommended for\n    backends whose component lists change dynamically.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.providers.proxy import ProxyProvider, ProxyClient\n\n        # Create a proxy provider for a remote server\n        proxy = ProxyProvider(lambda: ProxyClient(\"http://localhost:8000/mcp\"))\n\n        mcp = FastMCP(\"Proxy Server\")\n        mcp.add_provider(proxy)\n\n        # Can also add with namespace\n        mcp.add_provider(proxy.with_namespace(\"remote\"))\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        client_factory: ClientFactoryT,\n        cache_ttl: float | None = None,\n    ):\n        \"\"\"Initialize a ProxyProvider.\n\n        Args:\n            client_factory: A callable that returns a Client instance when called.\n                           This gives you full control over session creation and reuse.\n                           Can be either a synchronous or asynchronous function.\n            cache_ttl: How long (in seconds) to cache component lists for\n                      individual lookups.  Defaults to 300.  Set to 0 to\n                      disable caching.\n        \"\"\"\n        super().__init__()\n        self.client_factory = client_factory\n        self._cache_ttl = cache_ttl if cache_ttl is not None else _DEFAULT_CACHE_TTL\n        self._tools_cache: _CacheEntry[Tool] | None = None\n        self._resources_cache: _CacheEntry[Resource] | None = None\n        self._templates_cache: _CacheEntry[ResourceTemplate] | None = None\n        self._prompts_cache: _CacheEntry[Prompt] | None = None\n\n    async def _get_client(self) -> Client:\n        \"\"\"Gets a client instance by calling the sync or async factory.\"\"\"\n        client = self.client_factory()\n        if inspect.isawaitable(client):\n            client = cast(Client, await client)\n        return client\n\n    # -------------------------------------------------------------------------\n    # Tool methods\n    # -------------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"List all tools from the remote server.\"\"\"\n        try:\n            client = await self._get_client()\n            async with client:\n                mcp_tools = await client.list_tools()\n                tools = [\n                    ProxyTool.from_mcp_tool(self.client_factory, t) for t in mcp_tools\n                ]\n        except McpError as e:\n            if e.error.code == METHOD_NOT_FOUND:\n                tools = []\n            else:\n                raise\n        self._tools_cache = _CacheEntry(tools, time.monotonic())\n        return tools\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        cache = self._tools_cache\n        if cache is None or not cache.is_fresh(self._cache_ttl):\n            await self._list_tools()\n            cache = self._tools_cache\n        assert cache is not None\n        matching = [t for t in cache.items if t.name == name]\n        if version:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    # -------------------------------------------------------------------------\n    # Resource methods\n    # -------------------------------------------------------------------------\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"List all resources from the remote server.\"\"\"\n        try:\n            client = await self._get_client()\n            async with client:\n                mcp_resources = await client.list_resources()\n                resources = [\n                    ProxyResource.from_mcp_resource(self.client_factory, r)\n                    for r in mcp_resources\n                ]\n        except McpError as e:\n            if e.error.code == METHOD_NOT_FOUND:\n                resources = []\n            else:\n                raise\n        self._resources_cache = _CacheEntry(resources, time.monotonic())\n        return resources\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        cache = self._resources_cache\n        if cache is None or not cache.is_fresh(self._cache_ttl):\n            await self._list_resources()\n            cache = self._resources_cache\n        assert cache is not None\n        matching = [r for r in cache.items if str(r.uri) == uri]\n        if version:\n            matching = [r for r in matching if version.matches(r.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    # -------------------------------------------------------------------------\n    # Resource template methods\n    # -------------------------------------------------------------------------\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"List all resource templates from the remote server.\"\"\"\n        try:\n            client = await self._get_client()\n            async with client:\n                mcp_templates = await client.list_resource_templates()\n                templates = [\n                    ProxyTemplate.from_mcp_template(self.client_factory, t)\n                    for t in mcp_templates\n                ]\n        except McpError as e:\n            if e.error.code == METHOD_NOT_FOUND:\n                templates = []\n            else:\n                raise\n        self._templates_cache = _CacheEntry(templates, time.monotonic())\n        return templates\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        cache = self._templates_cache\n        if cache is None or not cache.is_fresh(self._cache_ttl):\n            await self._list_resource_templates()\n            cache = self._templates_cache\n        assert cache is not None\n        matching = [t for t in cache.items if t.matches(uri) is not None]\n        if version:\n            matching = [t for t in matching if version.matches(t.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    # -------------------------------------------------------------------------\n    # Prompt methods\n    # -------------------------------------------------------------------------\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"List all prompts from the remote server.\"\"\"\n        try:\n            client = await self._get_client()\n            async with client:\n                mcp_prompts = await client.list_prompts()\n                prompts = [\n                    ProxyPrompt.from_mcp_prompt(self.client_factory, p)\n                    for p in mcp_prompts\n                ]\n        except McpError as e:\n            if e.error.code == METHOD_NOT_FOUND:\n                prompts = []\n            else:\n                raise\n        self._prompts_cache = _CacheEntry(prompts, time.monotonic())\n        return prompts\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        cache = self._prompts_cache\n        if cache is None or not cache.is_fresh(self._cache_ttl):\n            await self._list_prompts()\n            cache = self._prompts_cache\n        assert cache is not None\n        matching = [p for p in cache.items if p.name == name]\n        if version:\n            matching = [p for p in matching if version.matches(p.version)]\n        if not matching:\n            return None\n        return max(matching, key=version_sort_key)  # type: ignore[type-var]\n\n    # -------------------------------------------------------------------------\n    # Task methods\n    # -------------------------------------------------------------------------\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Return empty list since proxy components don't support tasks.\n\n        Override the base implementation to avoid calling list_tools() during\n        server lifespan initialization, which would open the client before any\n        context is set. All Proxy* components have task_config.mode=\"forbidden\".\n        \"\"\"\n        return []\n\n    # lifespan() uses default implementation (empty context manager)\n    # because client cleanup is handled per-request\n\n\n# -----------------------------------------------------------------------------\n# Factory Functions\n# -----------------------------------------------------------------------------\n\n\ndef _create_client_factory(\n    target: (\n        Client[ClientTransportT]\n        | ClientTransport\n        | FastMCP[Any]\n        | FastMCP1Server\n        | AnyUrl\n        | Path\n        | MCPConfig\n        | dict[str, Any]\n        | str\n    ),\n) -> ClientFactoryT:\n    \"\"\"Create a client factory from the given target.\n\n    Internal helper that handles the session strategy based on the target type:\n    - Connected Client: reuses existing session (with warning about context mixing)\n    - Disconnected Client: creates fresh sessions per request\n    - Other targets: creates ProxyClient and fresh sessions per request\n    \"\"\"\n    if isinstance(target, Client):\n        client = target\n        if client.is_connected() and type(client) is ProxyClient:\n            logger.info(\n                \"Proxy detected connected ProxyClient - creating fresh sessions for each \"\n                \"request to avoid request context leakage.\"\n            )\n\n            def fresh_client_factory() -> Client:\n                return client.new()\n\n            return fresh_client_factory\n\n        if client.is_connected():\n            logger.info(\n                \"Proxy detected connected client - reusing existing session for all requests. \"\n                \"This may cause context mixing in concurrent scenarios.\"\n            )\n\n            def reuse_client_factory() -> Client:\n                return client\n\n            return reuse_client_factory\n\n        def fresh_client_factory() -> Client:\n            return client.new()\n\n        return fresh_client_factory\n    else:\n        # target is not a Client, so it's compatible with ProxyClient.__init__\n        base_client = ProxyClient(cast(Any, target))\n\n        def proxy_client_factory() -> Client:\n            return base_client.new()\n\n        return proxy_client_factory\n\n\n# -----------------------------------------------------------------------------\n# FastMCPProxy - Convenience Wrapper\n# -----------------------------------------------------------------------------\n\n\nclass FastMCPProxy(FastMCP):\n    \"\"\"A FastMCP server that acts as a proxy to a remote MCP-compliant server.\n\n    This is a convenience wrapper that creates a FastMCP server with a\n    ProxyProvider. For more control, use FastMCP with add_provider(ProxyProvider(...)).\n\n    Example:\n        ```python\n        from fastmcp.server import create_proxy\n        from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient\n\n        # Create a proxy server using create_proxy (recommended)\n        proxy = create_proxy(\"http://localhost:8000/mcp\")\n\n        # Or use FastMCPProxy directly with explicit client factory\n        proxy = FastMCPProxy(client_factory=lambda: ProxyClient(\"http://localhost:8000/mcp\"))\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        client_factory: ClientFactoryT,\n        **kwargs,\n    ):\n        \"\"\"Initialize the proxy server.\n\n        FastMCPProxy requires explicit session management via client_factory.\n        Use create_proxy() for convenience with automatic session strategy.\n\n        Args:\n            client_factory: A callable that returns a Client instance when called.\n                           This gives you full control over session creation and reuse.\n                           Can be either a synchronous or asynchronous function.\n            **kwargs: Additional settings for the FastMCP server.\n        \"\"\"\n        super().__init__(**kwargs)\n        self.client_factory = client_factory\n        provider: Provider = ProxyProvider(client_factory)\n        self.add_provider(provider)\n\n\n# -----------------------------------------------------------------------------\n# ProxyClient and Related\n# -----------------------------------------------------------------------------\n\n\nasync def default_proxy_roots_handler(\n    context: RequestContext[ClientSession, LifespanContextT],\n) -> RootsList:\n    \"\"\"Forward list roots request from remote server to proxy's connected clients.\"\"\"\n    ctx = get_context()\n    return await ctx.list_roots()\n\n\nasync def default_proxy_sampling_handler(\n    messages: list[mcp.types.SamplingMessage],\n    params: mcp.types.CreateMessageRequestParams,\n    context: RequestContext[ClientSession, LifespanContextT],\n) -> mcp.types.CreateMessageResult:\n    \"\"\"Forward sampling request from remote server to proxy's connected clients.\"\"\"\n    ctx = get_context()\n    result = await ctx.sample(\n        list(messages),\n        system_prompt=params.systemPrompt,\n        temperature=params.temperature,\n        max_tokens=params.maxTokens,\n        model_preferences=params.modelPreferences,\n    )\n    content = mcp.types.TextContent(type=\"text\", text=result.text or \"\")\n    return mcp.types.CreateMessageResult(\n        role=\"assistant\",\n        model=\"fastmcp-client\",\n        # TODO(ty): remove when ty supports isinstance exclusion narrowing\n        content=content,\n    )\n\n\nasync def default_proxy_elicitation_handler(\n    message: str,\n    response_type: type,\n    params: mcp.types.ElicitRequestParams,\n    context: RequestContext[ClientSession, LifespanContextT],\n) -> ElicitResult:\n    \"\"\"Forward elicitation request from remote server to proxy's connected clients.\"\"\"\n    ctx = get_context()\n    # requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams\n    requested_schema = (\n        params.requestedSchema\n        if isinstance(params, ElicitRequestFormParams)\n        else {\"type\": \"object\", \"properties\": {}}\n    )\n    result = await ctx.session.elicit(\n        message=message,\n        requestedSchema=requested_schema,\n        related_request_id=ctx.request_id,\n    )\n    return ElicitResult(action=result.action, content=result.content)\n\n\nasync def default_proxy_log_handler(message: LogMessage) -> None:\n    \"\"\"Forward log notification from remote server to proxy's connected clients.\"\"\"\n    ctx = get_context()\n    msg = message.data.get(\"msg\")\n    extra = message.data.get(\"extra\")\n    await ctx.log(msg, level=message.level, logger_name=message.logger, extra=extra)\n\n\nasync def default_proxy_progress_handler(\n    progress: float,\n    total: float | None,\n    message: str | None,\n) -> None:\n    \"\"\"Forward progress notification from remote server to proxy's connected clients.\"\"\"\n    ctx = get_context()\n    await ctx.report_progress(progress, total, message)\n\n\ndef _restore_request_context(\n    rc_ref: list[Any],\n) -> None:\n    \"\"\"Set the ``request_ctx`` and ``_current_context`` ContextVars from stashed values.\n\n    Called at the start of proxy handler invocations in\n    ``StatefulProxyClient`` to fix stale ContextVars in the receive-loop\n    task.  Only overrides when the ContextVar is genuinely stale (same\n    session, different request_id) to avoid corrupting the concurrent\n    case where multiple sessions share the same ref via ``copy.copy``.\n\n    We stash a ``(RequestContext, weakref[FastMCP])`` tuple — never a\n    ``Context`` instance — because ``Context`` properties are themselves\n    ContextVar-dependent and would resolve stale values in the receive\n    loop.  Instead we construct a fresh ``Context`` here after restoring\n    ``request_ctx``, so its property accesses read the correct values.\n    \"\"\"\n    from fastmcp.server.context import Context, _current_context\n\n    stashed = rc_ref[0]\n    if stashed is None:\n        return\n\n    rc, fastmcp_ref = stashed\n    try:\n        current_rc = request_ctx.get()\n    except LookupError:\n        request_ctx.set(rc)\n        fastmcp = fastmcp_ref()\n        if fastmcp is not None:\n            _current_context.set(Context(fastmcp))\n        return\n    if current_rc.session is rc.session and current_rc.request_id != rc.request_id:\n        request_ctx.set(rc)\n        fastmcp = fastmcp_ref()\n        if fastmcp is not None:\n            _current_context.set(Context(fastmcp))\n\n\ndef _make_restoring_handler(handler: Callable, rc_ref: list[Any]) -> Callable:\n    \"\"\"Wrap a proxy handler to restore request_ctx before delegating.\n\n    The wrapper is a plain ``async def`` so it passes\n    ``inspect.isfunction()`` checks in handler registration paths\n    (e.g., ``create_roots_callback``).\n    \"\"\"\n\n    async def wrapper(*args: Any, **kwargs: Any) -> Any:\n        _restore_request_context(rc_ref)\n        return await handler(*args, **kwargs)\n\n    return wrapper\n\n\nclass ProxyClient(Client[ClientTransportT]):\n    \"\"\"A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.\n\n    Supports forwarding roots, sampling, elicitation, logging, and progress.\n    \"\"\"\n\n    def __init__(\n        self,\n        transport: ClientTransportT\n        | FastMCP[Any]\n        | FastMCP1Server\n        | AnyUrl\n        | Path\n        | MCPConfig\n        | dict[str, Any]\n        | str,\n        **kwargs,\n    ):\n        if \"name\" not in kwargs:\n            kwargs[\"name\"] = self.generate_name()\n        if \"roots\" not in kwargs:\n            kwargs[\"roots\"] = default_proxy_roots_handler\n        if \"sampling_handler\" not in kwargs:\n            kwargs[\"sampling_handler\"] = default_proxy_sampling_handler\n        if \"elicitation_handler\" not in kwargs:\n            kwargs[\"elicitation_handler\"] = default_proxy_elicitation_handler\n        if \"log_handler\" not in kwargs:\n            kwargs[\"log_handler\"] = default_proxy_log_handler\n        if \"progress_handler\" not in kwargs:\n            kwargs[\"progress_handler\"] = default_proxy_progress_handler\n        super().__init__(**kwargs | {\"transport\": transport})\n\n\nclass StatefulProxyClient(ProxyClient[ClientTransportT]):\n    \"\"\"A proxy client that provides a stateful client factory for the proxy server.\n\n    The stateful proxy client bound its copy to the server session.\n    And it will be disconnected when the session is exited.\n\n    This is useful to proxy a stateful mcp server such as the Playwright MCP server.\n    Note that it is essential to ensure that the proxy server itself is also stateful.\n\n    Because session reuse means the receive-loop task inherits a stale\n    ``request_ctx`` ContextVar snapshot, the default proxy handlers are\n    replaced with versions that restore the ContextVar before forwarding.\n    ``ProxyTool.run`` stashes the current ``RequestContext`` in\n    ``_proxy_rc_ref`` before each backend call, and the handlers consult\n    it to detect (and correct) staleness.\n    \"\"\"\n\n    # Mutable list shared across copies (Client.new() uses copy.copy,\n    # which preserves references to mutable containers).  ProxyTool.run\n    # writes [0] before each backend call; handlers read it to detect\n    # stale ContextVars and restore the correct request_ctx.\n    #\n    # Stores a (RequestContext, weakref[FastMCP]) tuple — never a Context\n    # instance — because Context properties are ContextVar-dependent and\n    # would resolve stale values in the receive loop.  The restore helper\n    # constructs a fresh Context from the weakref after setting request_ctx.\n    _proxy_rc_ref: list[Any]\n\n    def __init__(self, *args: Any, **kwargs: Any):\n        # Install context-restoring handler wrappers BEFORE super().__init__\n        # registers them with the Client's session kwargs.\n        self._proxy_rc_ref = [None]\n        for key, default_fn in (\n            (\"roots\", default_proxy_roots_handler),\n            (\"sampling_handler\", default_proxy_sampling_handler),\n            (\"elicitation_handler\", default_proxy_elicitation_handler),\n            (\"log_handler\", default_proxy_log_handler),\n            (\"progress_handler\", default_proxy_progress_handler),\n        ):\n            if key not in kwargs:\n                kwargs[key] = _make_restoring_handler(default_fn, self._proxy_rc_ref)\n\n        super().__init__(*args, **kwargs)\n        self._caches: dict[ServerSession, Client[ClientTransportT]] = {}\n\n    async def __aexit__(self, exc_type, exc_value, traceback) -> None:  # type: ignore[override]\n        \"\"\"The stateful proxy client will be forced disconnected when the session is exited.\n\n        So we do nothing here.\n        \"\"\"\n\n    async def clear(self):\n        \"\"\"Clear all cached clients and force disconnect them.\"\"\"\n        while self._caches:\n            _, cache = self._caches.popitem()\n            await cache._disconnect(force=True)\n\n    def new_stateful(self) -> Client[ClientTransportT]:\n        \"\"\"Create a new stateful proxy client instance with the same configuration.\n\n        Use this method as the client factory for stateful proxy server.\n        \"\"\"\n        session = get_context().session\n        proxy_client = self._caches.get(session, None)\n\n        if proxy_client is None:\n            proxy_client = self.new()\n            logger.debug(f\"{proxy_client} created for {session}\")\n            self._caches[session] = proxy_client\n\n            async def _on_session_exit():\n                self._caches.pop(session)\n                logger.debug(f\"{proxy_client} will be disconnect\")\n                await proxy_client._disconnect(force=True)\n\n            session._exit_stack.push_async_callback(_on_session_exit)\n\n        return proxy_client\n"
  },
  {
    "path": "src/fastmcp/server/providers/skills/__init__.py",
    "content": "\"\"\"Skills providers for exposing agent skills as MCP resources.\n\nThis module provides a two-layer architecture for skill discovery:\n\n- **SkillProvider**: Handles a single skill folder, exposing its files as resources.\n- **SkillsDirectoryProvider**: Scans a directory, creates a SkillProvider per folder.\n- **Vendor providers**: Platform-specific providers for Claude, Cursor, VS Code, Codex,\n  Gemini, Goose, Copilot, and OpenCode.\n\nExample:\n    ```python\n    from pathlib import Path\n    from fastmcp import FastMCP\n    from fastmcp.server.providers.skills import ClaudeSkillsProvider, SkillProvider\n\n    mcp = FastMCP(\"Skills Server\")\n\n    # Load a single skill\n    mcp.add_provider(SkillProvider(Path.home() / \".claude/skills/pdf-processing\"))\n\n    # Or load all skills in a directory\n    mcp.add_provider(ClaudeSkillsProvider())  # Uses ~/.claude/skills/\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\n# Import providers\nfrom fastmcp.server.providers.skills.claude_provider import ClaudeSkillsProvider\nfrom fastmcp.server.providers.skills.directory_provider import SkillsDirectoryProvider\nfrom fastmcp.server.providers.skills.skill_provider import SkillProvider\nfrom fastmcp.server.providers.skills.vendor_providers import (\n    CodexSkillsProvider,\n    CopilotSkillsProvider,\n    CursorSkillsProvider,\n    GeminiSkillsProvider,\n    GooseSkillsProvider,\n    OpenCodeSkillsProvider,\n    VSCodeSkillsProvider,\n)\n\n\n# Backwards compatibility alias\nSkillsProvider = SkillsDirectoryProvider\n\n\n__all__ = [\n    \"ClaudeSkillsProvider\",\n    \"CodexSkillsProvider\",\n    \"CopilotSkillsProvider\",\n    \"CursorSkillsProvider\",\n    \"GeminiSkillsProvider\",\n    \"GooseSkillsProvider\",\n    \"OpenCodeSkillsProvider\",\n    \"SkillProvider\",\n    \"SkillsDirectoryProvider\",\n    \"SkillsProvider\",  # Backwards compatibility alias\n    \"VSCodeSkillsProvider\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/providers/skills/_common.py",
    "content": "\"\"\"Shared utilities and data structures for skills providers.\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport re\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any\n\n\n@dataclass\nclass SkillFileInfo:\n    \"\"\"Information about a file within a skill.\"\"\"\n\n    path: str  # Relative path within skill directory\n    size: int\n    hash: str  # sha256 hash\n\n\n@dataclass\nclass SkillInfo:\n    \"\"\"Parsed information about a skill.\"\"\"\n\n    name: str  # Directory name (canonical identifier)\n    description: str  # From frontmatter or first line\n    path: Path  # Absolute path to skill directory\n    main_file: str  # Name of main file (e.g., \"SKILL.md\")\n    files: list[SkillFileInfo] = field(default_factory=list)\n    frontmatter: dict[str, Any] = field(default_factory=dict)\n\n\ndef parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:\n    \"\"\"Parse YAML frontmatter from markdown content.\n\n    Args:\n        content: Markdown content potentially starting with ---\n\n    Returns:\n        Tuple of (frontmatter dict, remaining content)\n    \"\"\"\n    if not content.startswith(\"---\"):\n        return {}, content\n\n    # Find the closing ---\n    end_match = re.search(r\"\\n---\\s*\\n\", content[3:])\n    if not end_match:\n        return {}, content\n\n    frontmatter_text = content[3 : 3 + end_match.start()]\n    remaining = content[3 + end_match.end() :]\n\n    # Parse YAML (simple key: value parsing, no complex types)\n    frontmatter: dict[str, Any] = {}\n    for line in frontmatter_text.strip().split(\"\\n\"):\n        if \":\" in line:\n            key, _, value = line.partition(\":\")\n            key = key.strip()\n            value = value.strip()\n\n            # Handle quoted strings\n            if (value.startswith('\"') and value.endswith('\"')) or (\n                value.startswith(\"'\") and value.endswith(\"'\")\n            ):\n                value = value[1:-1]\n\n            # Handle lists [a, b, c]\n            if value.startswith(\"[\") and value.endswith(\"]\"):\n                items = value[1:-1].split(\",\")\n                value = [item.strip().strip(\"\\\"'\") for item in items if item.strip()]\n\n            frontmatter[key] = value\n\n    return frontmatter, remaining\n\n\ndef compute_file_hash(path: Path) -> str:\n    \"\"\"Compute SHA256 hash of a file.\"\"\"\n    sha256 = hashlib.sha256()\n    with open(path, \"rb\") as f:\n        for chunk in iter(lambda: f.read(8192), b\"\"):\n            sha256.update(chunk)\n    return f\"sha256:{sha256.hexdigest()}\"\n\n\ndef scan_skill_files(skill_dir: Path) -> list[SkillFileInfo]:\n    \"\"\"Scan a skill directory for all files.\"\"\"\n    files = []\n    resolved_skill_dir = skill_dir.resolve()\n\n    # Sort for deterministic ordering across platforms\n    for file_path in sorted(skill_dir.rglob(\"*\")):\n        if file_path.is_file():\n            resolved_file_path = file_path.resolve()\n            if not resolved_file_path.is_relative_to(resolved_skill_dir):\n                continue\n\n            rel_path = file_path.relative_to(skill_dir)\n            files.append(\n                SkillFileInfo(\n                    # Use POSIX paths for cross-platform URI consistency\n                    path=rel_path.as_posix(),\n                    size=resolved_file_path.stat().st_size,\n                    hash=compute_file_hash(resolved_file_path),\n                )\n            )\n    return files\n"
  },
  {
    "path": "src/fastmcp/server/providers/skills/claude_provider.py",
    "content": "\"\"\"Claude-specific skills provider for Claude Code skills.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom fastmcp.server.providers.skills.directory_provider import SkillsDirectoryProvider\n\n\nclass ClaudeSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"Provider for Claude Code skills from ~/.claude/skills/.\n\n    A convenience subclass that sets the default root to Claude's skills location.\n\n    Args:\n        reload: If True, re-scan on every request. Defaults to False.\n        supporting_files: How supporting files are exposed:\n            - \"template\": Accessed via ResourceTemplate, hidden from list_resources().\n            - \"resources\": Each file exposed as individual Resource in list_resources().\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.providers.skills import ClaudeSkillsProvider\n\n        mcp = FastMCP(\"Claude Skills\")\n        mcp.add_provider(ClaudeSkillsProvider())  # Uses default location\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".claude\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/skills/directory_provider.py",
    "content": "\"\"\"Directory scanning provider for discovering multiple skills.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.providers.aggregate import AggregateProvider\nfrom fastmcp.server.providers.skills.skill_provider import SkillProvider\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.versions import VersionSpec\n\nlogger = get_logger(__name__)\n\n\nclass SkillsDirectoryProvider(AggregateProvider):\n    \"\"\"Provider that scans directories and creates a SkillProvider per skill folder.\n\n    This extends AggregateProvider to combine multiple SkillProviders into one.\n    Each subdirectory containing a main file (default: SKILL.md) becomes a skill.\n    Can scan multiple root directories - if a skill name appears in multiple roots,\n    the first one found wins.\n\n    Args:\n        roots: Root directory(ies) containing skill folders. Can be a single path\n            or a sequence of paths.\n        reload: If True, re-discover skills on each request. Defaults to False.\n        main_file_name: Name of the main skill file. Defaults to \"SKILL.md\".\n        supporting_files: How supporting files are exposed in child SkillProviders:\n            - \"template\": Accessed via ResourceTemplate, hidden from list_resources().\n            - \"resources\": Each file exposed as individual Resource in list_resources().\n\n    Example:\n        ```python\n        from pathlib import Path\n        from fastmcp import FastMCP\n        from fastmcp.server.providers.skills import SkillsDirectoryProvider\n\n        mcp = FastMCP(\"Skills\")\n        # Single directory\n        mcp.add_provider(SkillsDirectoryProvider(\n            roots=Path.home() / \".claude\" / \"skills\",\n            reload=True,  # Re-scan on each request\n        ))\n        # Multiple directories\n        mcp.add_provider(SkillsDirectoryProvider(\n            roots=[Path(\"/etc/skills\"), Path.home() / \".local\" / \"skills\"],\n        ))\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        roots: str | Path | Sequence[str | Path],\n        reload: bool = False,\n        main_file_name: str = \"SKILL.md\",\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        super().__init__()\n        # Normalize to sequence: single path becomes list\n        if isinstance(roots, (str, Path)):\n            roots = [roots]\n\n        self._roots = [Path(r).resolve() for r in roots]\n        self._reload = reload\n        self._main_file_name = main_file_name\n        self._supporting_files = supporting_files\n        self._discovered = False\n\n        # Discover skills at init\n        self._discover_skills()\n\n    def _discover_skills(self) -> None:\n        \"\"\"Scan root directories and create SkillProvider per valid skill folder.\"\"\"\n        # Clear existing providers if reloading\n        self.providers.clear()\n\n        seen_skill_names: set[str] = set()\n\n        for root in self._roots:\n            if not root.exists():\n                logger.debug(f\"Skills root does not exist: {root}\")\n                continue\n\n            for skill_dir in root.iterdir():\n                if not skill_dir.is_dir():\n                    continue\n\n                main_file = skill_dir / self._main_file_name\n                if not main_file.exists():\n                    continue\n\n                skill_name = skill_dir.name\n                # Skip if we've already seen this skill name (first wins)\n                if skill_name in seen_skill_names:\n                    logger.debug(\n                        f\"Skipping duplicate skill '{skill_name}' from {root} \"\n                        f\"(already found in earlier root)\"\n                    )\n                    continue\n\n                try:\n                    provider = SkillProvider(\n                        skill_path=skill_dir,\n                        main_file_name=self._main_file_name,\n                        supporting_files=self._supporting_files,\n                    )\n                    self.providers.append(provider)\n                    seen_skill_names.add(skill_name)\n                except (FileNotFoundError, PermissionError, OSError):\n                    logger.exception(f\"Failed to load skill: {skill_dir.name}\")\n\n        self._discovered = True\n        logger.debug(\n            f\"SkillsDirectoryProvider loaded {len(self.providers)} skills \"\n            f\"from {len(self._roots)} root(s)\"\n        )\n\n    async def _ensure_discovered(self) -> None:\n        \"\"\"Ensure skills are discovered, rediscovering if reload is enabled.\"\"\"\n        if self._reload or not self._discovered:\n            self._discover_skills()\n\n    # Override list methods to support reload\n    async def _list_resources(self) -> Sequence[Resource]:\n        await self._ensure_discovered()\n        return await super()._list_resources()\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        await self._ensure_discovered()\n        return await super()._list_resource_templates()\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        await self._ensure_discovered()\n        return await super()._get_resource(uri, version)\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        await self._ensure_discovered()\n        return await super()._get_resource_template(uri, version)\n\n    def __repr__(self) -> str:\n        roots_repr = self._roots[0] if len(self._roots) == 1 else self._roots\n        return (\n            f\"SkillsDirectoryProvider(roots={roots_repr!r}, \"\n            f\"reload={self._reload}, skills={len(self.providers)})\"\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/skills/skill_provider.py",
    "content": "\"\"\"Basic skill provider for handling a single skill folder.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport mimetypes\nfrom collections.abc import Sequence\nfrom pathlib import Path\nfrom typing import Any, Literal, cast\n\nfrom pydantic import AnyUrl\n\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.providers.skills._common import (\n    SkillInfo,\n    parse_frontmatter,\n    scan_skill_files,\n)\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.versions import VersionSpec\n\nlogger = get_logger(__name__)\n\n# Ensure .md is recognized as text/markdown on all platforms (Windows may not have this)\nmimetypes.add_type(\"text/markdown\", \".md\")\n\n\n# -----------------------------------------------------------------------------\n# Skill-specific Resource and ResourceTemplate subclasses\n# -----------------------------------------------------------------------------\n\n\nclass SkillResource(Resource):\n    \"\"\"A resource representing a skill's main file or manifest.\"\"\"\n\n    skill_info: SkillInfo\n    is_manifest: bool = False\n\n    def get_meta(self) -> dict[str, Any]:\n        meta = super().get_meta()\n        fastmcp = cast(dict[str, Any], meta[\"fastmcp\"])\n        fastmcp[\"skill\"] = {\n            \"name\": self.skill_info.name,\n            \"is_manifest\": self.is_manifest,\n        }\n        return meta\n\n    async def read(self) -> str | bytes | ResourceResult:\n        \"\"\"Read the resource content.\"\"\"\n        if self.is_manifest:\n            return self._generate_manifest()\n        else:\n            main_file_path = self.skill_info.path / self.skill_info.main_file\n            return main_file_path.read_text()\n\n    def _generate_manifest(self) -> str:\n        \"\"\"Generate JSON manifest for the skill.\"\"\"\n        manifest = {\n            \"skill\": self.skill_info.name,\n            \"files\": [\n                {\"path\": f.path, \"size\": f.size, \"hash\": f.hash}\n                for f in self.skill_info.files\n            ],\n        }\n        return json.dumps(manifest, indent=2)\n\n\nclass SkillFileTemplate(ResourceTemplate):\n    \"\"\"A template for accessing files within a skill.\"\"\"\n\n    skill_info: SkillInfo\n\n    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:\n        \"\"\"Read a file from the skill directory.\"\"\"\n        file_path = arguments.get(\"path\", \"\")\n        full_path = self.skill_info.path / file_path\n\n        # Security: ensure path doesn't escape skill directory\n        try:\n            full_path = full_path.resolve()\n            if not full_path.is_relative_to(self.skill_info.path):\n                raise ValueError(f\"Path {file_path} escapes skill directory\")\n        except ValueError as e:\n            raise ValueError(f\"Invalid path: {e}\") from e\n\n        if not full_path.exists():\n            raise FileNotFoundError(f\"File not found: {file_path}\")\n\n        if not full_path.is_file():\n            raise ValueError(f\"Not a file: {file_path}\")\n\n        # Determine if binary or text based on mime type\n        mime_type, _ = mimetypes.guess_type(str(full_path))\n        if mime_type and mime_type.startswith(\"text/\"):\n            return full_path.read_text()\n        else:\n            return full_path.read_bytes()\n\n    async def _read(  # type: ignore[override]\n        self,\n        uri: str,\n        params: dict[str, Any],\n        task_meta: Any = None,\n    ) -> ResourceResult:\n        \"\"\"Server entry point - read file directly without creating ephemeral resource.\n\n        Note: task_meta is ignored - this template doesn't support background tasks.\n        \"\"\"\n        # Call read() directly and convert to ResourceResult\n        result = await self.read(arguments=params)\n        return self.convert_result(result)\n\n    async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:\n        \"\"\"Create a resource for the given URI and parameters.\n\n        Note: This is not typically used since _read() handles file reading directly.\n        Provided for compatibility with the ResourceTemplate interface.\n        \"\"\"\n        file_path = params.get(\"path\", \"\")\n        full_path = (self.skill_info.path / file_path).resolve()\n\n        # Security: ensure path doesn't escape skill directory\n        if not full_path.is_relative_to(self.skill_info.path):\n            raise ValueError(f\"Path {file_path} escapes skill directory\")\n\n        mime_type, _ = mimetypes.guess_type(str(full_path))\n\n        # Create a SkillFileResource that can read the file\n        return SkillFileResource(\n            uri=AnyUrl(uri),\n            name=f\"{self.skill_info.name}/{file_path}\",\n            description=f\"File from {self.skill_info.name} skill\",\n            mime_type=mime_type or \"application/octet-stream\",\n            skill_info=self.skill_info,\n            file_path=file_path,\n        )\n\n\nclass SkillFileResource(Resource):\n    \"\"\"A resource representing a specific file within a skill.\"\"\"\n\n    skill_info: SkillInfo\n    file_path: str\n\n    def get_meta(self) -> dict[str, Any]:\n        meta = super().get_meta()\n        fastmcp = cast(dict[str, Any], meta[\"fastmcp\"])\n        fastmcp[\"skill\"] = {\n            \"name\": self.skill_info.name,\n        }\n        return meta\n\n    async def read(self) -> str | bytes | ResourceResult:\n        \"\"\"Read the file content.\"\"\"\n        full_path = self.skill_info.path / self.file_path\n\n        # Security check\n        full_path = full_path.resolve()\n        if not full_path.is_relative_to(self.skill_info.path):\n            raise ValueError(f\"Path {self.file_path} escapes skill directory\")\n\n        if not full_path.exists():\n            raise FileNotFoundError(f\"File not found: {self.file_path}\")\n\n        mime_type, _ = mimetypes.guess_type(str(full_path))\n        if mime_type and mime_type.startswith(\"text/\"):\n            return full_path.read_text()\n        else:\n            return full_path.read_bytes()\n\n\n# -----------------------------------------------------------------------------\n# SkillProvider - handles a SINGLE skill folder\n# -----------------------------------------------------------------------------\n\n\nclass SkillProvider(Provider):\n    \"\"\"Provider that exposes a single skill folder as MCP resources.\n\n    Each skill folder must contain a main file (default: SKILL.md) and may\n    contain additional supporting files.\n\n    Exposes:\n    - A Resource for the main file (skill://{name}/SKILL.md)\n    - A Resource for the synthetic manifest (skill://{name}/_manifest)\n    - Supporting files via ResourceTemplate or Resources (configurable)\n\n    Args:\n        skill_path: Path to the skill directory.\n        main_file_name: Name of the main skill file. Defaults to \"SKILL.md\".\n        supporting_files: How supporting files (everything except main file and\n            manifest) are exposed to clients:\n            - \"template\": Accessed via ResourceTemplate, hidden from list_resources().\n              Clients discover files by reading the manifest first.\n            - \"resources\": Each file exposed as individual Resource in list_resources().\n              Full enumeration upfront.\n\n    Example:\n        ```python\n        from pathlib import Path\n        from fastmcp import FastMCP\n        from fastmcp.server.providers.skills import SkillProvider\n\n        mcp = FastMCP(\"My Skill\")\n        mcp.add_provider(SkillProvider(\n            Path.home() / \".claude/skills/pdf-processing\"\n        ))\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        skill_path: str | Path,\n        main_file_name: str = \"SKILL.md\",\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        super().__init__()\n        self._skill_path = Path(skill_path).resolve()\n        self._main_file_name = main_file_name\n        self._supporting_files = supporting_files\n        self._skill_info: SkillInfo | None = None\n\n        # Load at init to catch errors early\n        self._load_skill()\n\n    def _load_skill(self) -> None:\n        \"\"\"Load and parse the skill directory.\"\"\"\n        main_file = self._skill_path / self._main_file_name\n\n        if not self._skill_path.exists():\n            raise FileNotFoundError(f\"Skill directory not found: {self._skill_path}\")\n\n        if not main_file.exists():\n            raise FileNotFoundError(\n                f\"Main skill file not found: {main_file}. \"\n                f\"Expected {self._main_file_name} in {self._skill_path}\"\n            )\n\n        content = main_file.read_text()\n        frontmatter, body = parse_frontmatter(content)\n\n        # Get description from frontmatter or first non-empty line\n        description = frontmatter.get(\"description\", \"\")\n        if not description:\n            for line in body.strip().split(\"\\n\"):\n                line = line.strip()\n                if line and not line.startswith(\"#\"):\n                    description = line[:200]\n                    break\n                elif line.startswith(\"#\"):\n                    description = line.lstrip(\"#\").strip()[:200]\n                    break\n\n        # Scan all files in the skill directory\n        files = scan_skill_files(self._skill_path)\n\n        self._skill_info = SkillInfo(\n            name=self._skill_path.name,\n            description=description or f\"Skill: {self._skill_path.name}\",\n            path=self._skill_path,\n            main_file=self._main_file_name,\n            files=files,\n            frontmatter=frontmatter,\n        )\n\n        logger.debug(f\"SkillProvider loaded skill: {self._skill_info.name}\")\n\n    @property\n    def skill_info(self) -> SkillInfo:\n        \"\"\"Get the loaded skill info.\"\"\"\n        if self._skill_info is None:\n            raise RuntimeError(\"Skill not loaded\")\n        return self._skill_info\n\n    # -------------------------------------------------------------------------\n    # Provider interface implementation\n    # -------------------------------------------------------------------------\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"List skill resources.\"\"\"\n        skill = self.skill_info\n        resources: list[Resource] = []\n\n        # Main skill file\n        resources.append(\n            SkillResource(\n                uri=AnyUrl(f\"skill://{skill.name}/{self._main_file_name}\"),\n                name=f\"{skill.name}/{self._main_file_name}\",\n                description=skill.description,\n                mime_type=\"text/markdown\",\n                skill_info=skill,\n                is_manifest=False,\n            )\n        )\n\n        # Synthetic manifest\n        resources.append(\n            SkillResource(\n                uri=AnyUrl(f\"skill://{skill.name}/_manifest\"),\n                name=f\"{skill.name}/_manifest\",\n                description=f\"File listing for {skill.name}\",\n                mime_type=\"application/json\",\n                skill_info=skill,\n                is_manifest=True,\n            )\n        )\n\n        # If supporting_files=\"resources\", add all supporting files as resources\n        if self._supporting_files == \"resources\":\n            for file_info in skill.files:\n                # Skip main file and manifest (already added)\n                if file_info.path == self._main_file_name:\n                    continue\n\n                mime_type, _ = mimetypes.guess_type(file_info.path)\n                resources.append(\n                    SkillFileResource(\n                        uri=AnyUrl(f\"skill://{skill.name}/{file_info.path}\"),\n                        name=f\"{skill.name}/{file_info.path}\",\n                        description=f\"File from {skill.name} skill\",\n                        mime_type=mime_type or \"application/octet-stream\",\n                        skill_info=skill,\n                        file_path=file_info.path,\n                    )\n                )\n\n        return resources\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI.\"\"\"\n        skill = self.skill_info\n\n        # Parse URI: skill://{skill_name}/{file_path}\n        if not uri.startswith(\"skill://\"):\n            return None\n\n        path_part = uri[len(\"skill://\") :]\n        parts = path_part.split(\"/\", 1)\n        if len(parts) != 2:\n            return None\n\n        skill_name, file_path = parts\n        if skill_name != skill.name:\n            return None\n\n        if file_path == \"_manifest\":\n            return SkillResource(\n                uri=AnyUrl(uri),\n                name=f\"{skill_name}/_manifest\",\n                description=f\"File listing for {skill_name}\",\n                mime_type=\"application/json\",\n                skill_info=skill,\n                is_manifest=True,\n            )\n        elif file_path == self._main_file_name:\n            return SkillResource(\n                uri=AnyUrl(uri),\n                name=f\"{skill_name}/{self._main_file_name}\",\n                description=skill.description,\n                mime_type=\"text/markdown\",\n                skill_info=skill,\n                is_manifest=False,\n            )\n        elif self._supporting_files == \"resources\":\n            # Check if it's a known supporting file\n            for file_info in skill.files:\n                if file_info.path == file_path:\n                    mime_type, _ = mimetypes.guess_type(file_path)\n                    return SkillFileResource(\n                        uri=AnyUrl(uri),\n                        name=f\"{skill_name}/{file_path}\",\n                        description=f\"File from {skill_name} skill\",\n                        mime_type=mime_type or \"application/octet-stream\",\n                        skill_info=skill,\n                        file_path=file_path,\n                    )\n\n        return None\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"List resource templates for accessing files within the skill.\"\"\"\n        # Only expose template if supporting_files=\"template\"\n        if self._supporting_files != \"template\":\n            return []\n\n        skill = self.skill_info\n        return [\n            SkillFileTemplate(\n                uri_template=f\"skill://{skill.name}/{{path*}}\",\n                name=f\"{skill.name}_files\",\n                description=f\"Access files within {skill.name}\",\n                mime_type=\"application/octet-stream\",\n                parameters={\n                    \"type\": \"object\",\n                    \"properties\": {\"path\": {\"type\": \"string\"}},\n                    \"required\": [\"path\"],\n                },\n                skill_info=skill,\n            )\n        ]\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template that matches the given URI.\"\"\"\n        # Only match if supporting_files=\"template\"\n        if self._supporting_files != \"template\":\n            return None\n\n        skill = self.skill_info\n\n        if not uri.startswith(\"skill://\"):\n            return None\n\n        path_part = uri[len(\"skill://\") :]\n        parts = path_part.split(\"/\", 1)\n        if len(parts) != 2:\n            return None\n\n        skill_name, file_path = parts\n        if skill_name != skill.name:\n            return None\n\n        # Don't match known resources (main file, manifest)\n        if file_path == \"_manifest\" or file_path == self._main_file_name:\n            return None\n\n        return SkillFileTemplate(\n            uri_template=f\"skill://{skill.name}/{{path*}}\",\n            name=f\"{skill.name}_files\",\n            description=f\"Access files within {skill.name}\",\n            mime_type=\"application/octet-stream\",\n            parameters={\n                \"type\": \"object\",\n                \"properties\": {\"path\": {\"type\": \"string\"}},\n                \"required\": [\"path\"],\n            },\n            skill_info=skill,\n        )\n\n    def __repr__(self) -> str:\n        return (\n            f\"SkillProvider(skill_path={self._skill_path!r}, \"\n            f\"supporting_files={self._supporting_files!r})\"\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/skills/vendor_providers.py",
    "content": "\"\"\"Vendor-specific skills providers for various AI coding platforms.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom fastmcp.server.providers.skills.directory_provider import SkillsDirectoryProvider\n\n\nclass CursorSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"Cursor skills from ~/.cursor/skills/.\"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".cursor\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n\n\nclass VSCodeSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"VS Code skills from ~/.copilot/skills/.\"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".copilot\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n\n\nclass CodexSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"Codex skills from /etc/codex/skills/ and ~/.codex/skills/.\n\n    Scans both system-level and user-level directories. System skills take\n    precedence if duplicates exist.\n    \"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        system_root = Path(\"/etc/codex/skills\")\n        user_root = Path.home() / \".codex\" / \"skills\"\n\n        # Include both paths (system first, then user)\n        roots = [system_root, user_root]\n\n        super().__init__(\n            roots=roots,\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n\n\nclass GeminiSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"Gemini skills from ~/.gemini/skills/.\"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".gemini\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n\n\nclass GooseSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"Goose skills from ~/.config/agents/skills/.\"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".config\" / \"agents\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n\n\nclass CopilotSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"GitHub Copilot skills from ~/.copilot/skills/.\"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".copilot\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n\n\nclass OpenCodeSkillsProvider(SkillsDirectoryProvider):\n    \"\"\"OpenCode skills from ~/.config/opencode/skills/.\"\"\"\n\n    def __init__(\n        self,\n        reload: bool = False,\n        supporting_files: Literal[\"template\", \"resources\"] = \"template\",\n    ) -> None:\n        root = Path.home() / \".config\" / \"opencode\" / \"skills\"\n\n        super().__init__(\n            roots=[root],\n            reload=reload,\n            main_file_name=\"SKILL.md\",\n            supporting_files=supporting_files,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/providers/wrapped_provider.py",
    "content": "\"\"\"WrappedProvider for immutable transform composition.\n\nThis module provides `_WrappedProvider`, an internal class that wraps a provider\nwith an additional transform. Created by `Provider.wrap_transform()`.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncIterator, Sequence\nfrom contextlib import asynccontextmanager\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.server.transforms import Transform\n    from fastmcp.tools.base import Tool\n    from fastmcp.utilities.components import FastMCPComponent\n\n\nclass _WrappedProvider(Provider):\n    \"\"\"Internal provider that wraps another provider with a transform.\n\n    Created by Provider.wrap_transform(). Delegates all component sourcing\n    to the inner provider's public methods (which apply inner's transforms),\n    then applies the wrapper's transform on top.\n\n    This enables immutable transform composition - the inner provider is\n    unchanged, and the wrapper adds its transform layer.\n    \"\"\"\n\n    def __init__(self, inner: Provider, transform: Transform) -> None:\n        \"\"\"Initialize wrapped provider.\n\n        Args:\n            inner: The provider to wrap.\n            transform: The transform to apply on top of inner's results.\n        \"\"\"\n        super().__init__()\n        self._inner = inner\n        # Add the transform to this provider's transform list\n        # It will be applied via the normal transform chain\n        self._transforms.append(transform)\n\n    def __repr__(self) -> str:\n        return f\"_WrappedProvider({self._inner!r}, transforms={self._transforms!r})\"\n\n    # -------------------------------------------------------------------------\n    # Delegate to inner provider's public methods (which apply inner's transforms)\n    # -------------------------------------------------------------------------\n\n    async def _list_tools(self) -> Sequence[Tool]:\n        \"\"\"Delegate to inner's list_tools (includes inner's transforms).\"\"\"\n        return await self._inner.list_tools()\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Delegate to inner's get_tool (includes inner's transforms).\"\"\"\n        return await self._inner.get_tool(name, version)\n\n    async def _list_resources(self) -> Sequence[Resource]:\n        \"\"\"Delegate to inner's list_resources (includes inner's transforms).\"\"\"\n        return await self._inner.list_resources()\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Delegate to inner's get_resource (includes inner's transforms).\"\"\"\n        return await self._inner.get_resource(uri, version)\n\n    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n        \"\"\"Delegate to inner's list_resource_templates (includes inner's transforms).\"\"\"\n        return await self._inner.list_resource_templates()\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Delegate to inner's get_resource_template (includes inner's transforms).\"\"\"\n        return await self._inner.get_resource_template(uri, version)\n\n    async def _list_prompts(self) -> Sequence[Prompt]:\n        \"\"\"Delegate to inner's list_prompts (includes inner's transforms).\"\"\"\n        return await self._inner.list_prompts()\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Delegate to inner's get_prompt (includes inner's transforms).\"\"\"\n        return await self._inner.get_prompt(name, version)\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Delegate to inner's get_tasks and apply wrapper's transforms.\"\"\"\n        # Import here to avoid circular imports\n        from fastmcp.prompts.base import Prompt\n        from fastmcp.resources.base import Resource\n        from fastmcp.resources.template import ResourceTemplate\n        from fastmcp.tools.base import Tool\n\n        # Get tasks from inner (already has inner's transforms)\n        components = list(await self._inner.get_tasks())\n\n        # Apply this wrapper's transforms to the components\n        # We need to apply transforms per component type\n        tools = [c for c in components if isinstance(c, Tool)]\n        resources = [c for c in components if isinstance(c, Resource)]\n        templates = [c for c in components if isinstance(c, ResourceTemplate)]\n        prompts = [c for c in components if isinstance(c, Prompt)]\n\n        # Apply this wrapper's transforms sequentially\n        for transform in self.transforms:\n            tools = await transform.list_tools(tools)\n            resources = await transform.list_resources(resources)\n            templates = await transform.list_resource_templates(templates)\n            prompts = await transform.list_prompts(prompts)\n\n        return [\n            c\n            for c in [\n                *tools,\n                *resources,\n                *templates,\n                *prompts,\n            ]\n            if c.task_config.supports_tasks()\n        ]\n\n    # -------------------------------------------------------------------------\n    # Lifecycle - combine with inner\n    # -------------------------------------------------------------------------\n\n    @asynccontextmanager\n    async def lifespan(self) -> AsyncIterator[None]:\n        \"\"\"Combine lifespan with inner provider.\"\"\"\n        async with self._inner.lifespan():\n            yield\n"
  },
  {
    "path": "src/fastmcp/server/proxy.py",
    "content": "\"\"\"Backwards compatibility - import from fastmcp.server.providers.proxy instead.\n\nThis module re-exports all proxy-related classes from their new location\nat fastmcp.server.providers.proxy. Direct imports from this module are\ndeprecated and will be removed in a future version.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport warnings\n\nwarnings.warn(\n    \"fastmcp.server.proxy is deprecated. Use fastmcp.server.providers.proxy instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\n# Re-export everything from the new location\nfrom fastmcp.server.providers.proxy import (  # noqa: E402\n    ClientFactoryT,\n    FastMCPProxy,\n    ProxyClient,\n    ProxyPrompt,\n    ProxyProvider,\n    ProxyResource,\n    ProxyTemplate,\n    ProxyTool,\n    StatefulProxyClient,\n)\n\n__all__ = [\n    \"ClientFactoryT\",\n    \"FastMCPProxy\",\n    \"ProxyClient\",\n    \"ProxyPrompt\",\n    \"ProxyProvider\",\n    \"ProxyResource\",\n    \"ProxyTemplate\",\n    \"ProxyTool\",\n    \"StatefulProxyClient\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/sampling/__init__.py",
    "content": "\"\"\"Sampling module for FastMCP servers.\"\"\"\n\nfrom fastmcp.server.sampling.run import SampleStep, SamplingResult\nfrom fastmcp.server.sampling.sampling_tool import SamplingTool\n\n__all__ = [\n    \"SampleStep\",\n    \"SamplingResult\",\n    \"SamplingTool\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/sampling/run.py",
    "content": "\"\"\"Sampling types and helper functions for FastMCP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport json\nfrom collections.abc import Callable, Sequence\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Any, Generic, Literal, cast\n\nimport anyio\nfrom mcp.types import (\n    ClientCapabilities,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    ModelHint,\n    ModelPreferences,\n    SamplingCapability,\n    SamplingMessage,\n    SamplingMessageContentBlock,\n    SamplingToolsCapability,\n    TextContent,\n    ToolChoice,\n    ToolResultContent,\n    ToolUseContent,\n)\nfrom mcp.types import CreateMessageRequestParams as SamplingParams\nfrom mcp.types import Tool as SDKTool\nfrom pydantic import ValidationError\nfrom typing_extensions import TypeVar\n\nfrom fastmcp import settings\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.sampling.sampling_tool import SamplingTool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import TransformedTool\nfrom fastmcp.utilities.async_utils import gather\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import get_cached_typeadapter\n\nlogger = get_logger(__name__)\n\nif TYPE_CHECKING:\n    from fastmcp.server.context import Context\n\nResultT = TypeVar(\"ResultT\")\n\n# Simplified tool choice type - just the mode string instead of the full MCP object\nToolChoiceOption = Literal[\"auto\", \"required\", \"none\"]\n\n\n@dataclass\nclass SamplingResult(Generic[ResultT]):\n    \"\"\"Result of a sampling operation.\n\n    Attributes:\n        text: The text representation of the result (raw text or JSON for structured).\n        result: The typed result (str for text, parsed object for structured output).\n        history: All messages exchanged during sampling.\n    \"\"\"\n\n    text: str | None\n    result: ResultT\n    history: list[SamplingMessage]\n\n\n@dataclass\nclass SampleStep:\n    \"\"\"Result of a single sampling call.\n\n    Represents what the LLM returned in this step plus the message history.\n    \"\"\"\n\n    response: CreateMessageResult | CreateMessageResultWithTools\n    history: list[SamplingMessage]\n\n    @property\n    def is_tool_use(self) -> bool:\n        \"\"\"True if the LLM is requesting tool execution.\"\"\"\n        if isinstance(self.response, CreateMessageResultWithTools):\n            return self.response.stopReason == \"toolUse\"\n        return False\n\n    @property\n    def text(self) -> str | None:\n        \"\"\"Extract text from the response, if available.\"\"\"\n        content = self.response.content\n        if isinstance(content, list):\n            for block in content:\n                if isinstance(block, TextContent):\n                    return block.text\n            return None\n        elif isinstance(content, TextContent):\n            return content.text\n        return None\n\n    @property\n    def tool_calls(self) -> list[ToolUseContent]:\n        \"\"\"Get the list of tool calls from the response.\"\"\"\n        content = self.response.content\n        if isinstance(content, list):\n            return [c for c in content if isinstance(c, ToolUseContent)]\n        elif isinstance(content, ToolUseContent):\n            return [content]\n        return []\n\n\ndef _parse_model_preferences(\n    model_preferences: ModelPreferences | str | list[str] | None,\n) -> ModelPreferences | None:\n    \"\"\"Convert model preferences to ModelPreferences object.\"\"\"\n    if model_preferences is None:\n        return None\n    elif isinstance(model_preferences, ModelPreferences):\n        return model_preferences\n    elif isinstance(model_preferences, str):\n        return ModelPreferences(hints=[ModelHint(name=model_preferences)])\n    elif isinstance(model_preferences, list):\n        if not all(isinstance(h, str) for h in model_preferences):\n            raise ValueError(\"All elements of model_preferences list must be strings.\")\n        return ModelPreferences(hints=[ModelHint(name=h) for h in model_preferences])\n    else:\n        raise ValueError(\n            \"model_preferences must be one of: ModelPreferences, str, list[str], or None.\"\n        )\n\n\n# --- Standalone functions for sample_step() ---\n\n\ndef determine_handler_mode(context: Context, needs_tools: bool) -> bool:\n    \"\"\"Determine whether to use fallback handler or client for sampling.\n\n    Args:\n        context: The MCP context.\n        needs_tools: Whether the sampling request requires tool support.\n\n    Returns:\n        True if fallback handler should be used, False to use client.\n\n    Raises:\n        ValueError: If client lacks required capability and no fallback configured.\n    \"\"\"\n    fastmcp = context.fastmcp\n    session = context.session\n\n    # Check what capabilities the client has\n    has_sampling = session.check_client_capability(\n        capability=ClientCapabilities(sampling=SamplingCapability())\n    )\n    has_tools_capability = session.check_client_capability(\n        capability=ClientCapabilities(\n            sampling=SamplingCapability(tools=SamplingToolsCapability())\n        )\n    )\n\n    if fastmcp.sampling_handler_behavior == \"always\":\n        if fastmcp.sampling_handler is None:\n            raise ValueError(\n                \"sampling_handler_behavior is 'always' but no handler configured\"\n            )\n        return True\n    elif fastmcp.sampling_handler_behavior == \"fallback\":\n        client_sufficient = has_sampling and (not needs_tools or has_tools_capability)\n        if not client_sufficient:\n            if fastmcp.sampling_handler is None:\n                if needs_tools and has_sampling and not has_tools_capability:\n                    raise ValueError(\n                        \"Client does not support sampling with tools. \"\n                        \"The client must advertise the sampling.tools capability.\"\n                    )\n                raise ValueError(\"Client does not support sampling\")\n            return True\n    elif fastmcp.sampling_handler_behavior is not None:\n        raise ValueError(\n            f\"Invalid sampling_handler_behavior: {fastmcp.sampling_handler_behavior!r}. \"\n            \"Must be 'always', 'fallback', or None.\"\n        )\n    elif not has_sampling:\n        raise ValueError(\"Client does not support sampling\")\n    elif needs_tools and not has_tools_capability:\n        raise ValueError(\n            \"Client does not support sampling with tools. \"\n            \"The client must advertise the sampling.tools capability.\"\n        )\n\n    return False\n\n\nasync def call_sampling_handler(\n    context: Context,\n    messages: list[SamplingMessage],\n    *,\n    system_prompt: str | None,\n    temperature: float | None,\n    max_tokens: int,\n    model_preferences: ModelPreferences | str | list[str] | None,\n    sdk_tools: list[SDKTool] | None,\n    tool_choice: ToolChoice | None,\n) -> CreateMessageResult | CreateMessageResultWithTools:\n    \"\"\"Make LLM call using the fallback handler.\n\n    Note: This function expects the caller (sample_step) to have validated that\n    sampling_handler is set via determine_handler_mode(). The checks below are\n    safeguards against internal misuse.\n    \"\"\"\n    if context.fastmcp.sampling_handler is None:\n        raise RuntimeError(\"sampling_handler is None\")\n    if context.request_context is None:\n        raise RuntimeError(\"request_context is None\")\n\n    result = context.fastmcp.sampling_handler(\n        messages,\n        SamplingParams(\n            systemPrompt=system_prompt,\n            messages=messages,\n            temperature=temperature,\n            maxTokens=max_tokens,\n            modelPreferences=_parse_model_preferences(model_preferences),\n            tools=sdk_tools,\n            toolChoice=tool_choice,\n        ),\n        context.request_context,\n    )\n\n    if inspect.isawaitable(result):\n        result = await result\n\n    result = cast(\"str | CreateMessageResult | CreateMessageResultWithTools\", result)\n\n    # Convert string to CreateMessageResult\n    if isinstance(result, str):\n        return CreateMessageResult(\n            role=\"assistant\",\n            content=TextContent(type=\"text\", text=result),\n            model=\"unknown\",\n            stopReason=\"endTurn\",\n        )\n\n    return result\n\n\nasync def execute_tools(\n    tool_calls: list[ToolUseContent],\n    tool_map: dict[str, SamplingTool],\n    mask_error_details: bool = False,\n    tool_concurrency: int | None = None,\n) -> list[ToolResultContent]:\n    \"\"\"Execute tool calls and return results.\n\n    Args:\n        tool_calls: List of tool use requests from the LLM.\n        tool_map: Mapping from tool name to SamplingTool.\n        mask_error_details: If True, mask detailed error messages from tool execution.\n            When masked, only generic error messages are returned to the LLM.\n            Tools can explicitly raise ToolError to bypass masking when they want\n            to provide specific error messages to the LLM.\n        tool_concurrency: Controls parallel execution of tools:\n            - None (default): Sequential execution (one at a time)\n            - 0: Unlimited parallel execution\n            - N > 0: Execute at most N tools concurrently\n            If any tool has sequential=True, all tools execute sequentially\n            regardless of this setting.\n\n    Returns:\n        List of tool result content blocks in the same order as tool_calls.\n    \"\"\"\n    if tool_concurrency is not None and tool_concurrency < 0:\n        raise ValueError(\n            f\"tool_concurrency must be None, 0 (unlimited), or a positive integer, \"\n            f\"got {tool_concurrency}\"\n        )\n\n    async def _execute_single_tool(tool_use: ToolUseContent) -> ToolResultContent:\n        \"\"\"Execute a single tool and return its result.\"\"\"\n        tool = tool_map.get(tool_use.name)\n        if tool is None:\n            return ToolResultContent(\n                type=\"tool_result\",\n                toolUseId=tool_use.id,\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=f\"Error: Unknown tool '{tool_use.name}'\",\n                    )\n                ],\n                isError=True,\n            )\n\n        try:\n            result_value = await tool.run(tool_use.input)\n            return ToolResultContent(\n                type=\"tool_result\",\n                toolUseId=tool_use.id,\n                content=[TextContent(type=\"text\", text=str(result_value))],\n            )\n        except ToolError as e:\n            # ToolError is the escape hatch - always pass message through\n            logger.exception(f\"Error calling sampling tool '{tool_use.name}'\")\n            return ToolResultContent(\n                type=\"tool_result\",\n                toolUseId=tool_use.id,\n                content=[TextContent(type=\"text\", text=str(e))],\n                isError=True,\n            )\n        except Exception as e:\n            # Generic exceptions - mask based on setting\n            logger.exception(f\"Error calling sampling tool '{tool_use.name}'\")\n            if mask_error_details:\n                error_text = f\"Error executing tool '{tool_use.name}'\"\n            else:\n                error_text = f\"Error executing tool '{tool_use.name}': {e}\"\n            return ToolResultContent(\n                type=\"tool_result\",\n                toolUseId=tool_use.id,\n                content=[TextContent(type=\"text\", text=error_text)],\n                isError=True,\n            )\n\n    # Check if any tool requires sequential execution\n    requires_sequential = any(\n        tool.sequential\n        for tool_use in tool_calls\n        if (tool := tool_map.get(tool_use.name)) is not None\n    )\n\n    # Execute sequentially if required or if concurrency is None (default)\n    if tool_concurrency is None or requires_sequential:\n        tool_results: list[ToolResultContent] = []\n        for tool_use in tool_calls:\n            result = await _execute_single_tool(tool_use)\n            tool_results.append(result)\n        return tool_results\n\n    # Execute in parallel\n    if tool_concurrency == 0:\n        # Unlimited parallel execution\n        return await gather(*[_execute_single_tool(tc) for tc in tool_calls])\n    else:\n        # Bounded parallel execution with semaphore\n        semaphore = anyio.Semaphore(tool_concurrency)\n\n        async def bounded_execute(tool_use: ToolUseContent) -> ToolResultContent:\n            async with semaphore:\n                return await _execute_single_tool(tool_use)\n\n        return await gather(*[bounded_execute(tc) for tc in tool_calls])\n\n\n# --- Helper functions for sampling ---\n\n\ndef prepare_messages(\n    messages: str | Sequence[str | SamplingMessage],\n) -> list[SamplingMessage]:\n    \"\"\"Convert various message formats to a list of SamplingMessage objects.\"\"\"\n    if isinstance(messages, str):\n        return [\n            SamplingMessage(\n                content=TextContent(text=messages, type=\"text\"), role=\"user\"\n            )\n        ]\n    else:\n        return [\n            SamplingMessage(content=TextContent(text=m, type=\"text\"), role=\"user\")\n            if isinstance(m, str)\n            else m\n            for m in messages\n        ]\n\n\ndef prepare_tools(\n    tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]\n    | None,\n) -> list[SamplingTool] | None:\n    \"\"\"Convert tools to SamplingTool objects.\n\n    Accepts SamplingTool instances, FunctionTool instances, TransformedTool instances,\n    or plain callable functions. FunctionTool and TransformedTool are converted using\n    from_callable_tool(), while plain functions use from_function().\n\n    Args:\n        tools: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,\n            TransformedTool, or plain callable functions.\n\n    Returns:\n        List of SamplingTool instances, or None if tools is None.\n    \"\"\"\n    if tools is None:\n        return None\n\n    sampling_tools: list[SamplingTool] = []\n    for t in tools:\n        if isinstance(t, SamplingTool):\n            sampling_tools.append(t)\n        elif isinstance(t, (FunctionTool, TransformedTool)):\n            sampling_tools.append(SamplingTool.from_callable_tool(t))\n        elif callable(t):\n            sampling_tools.append(SamplingTool.from_function(t))\n        else:\n            raise TypeError(\n                f\"Expected SamplingTool, FunctionTool, TransformedTool, or callable, got {type(t)}\"\n            )\n\n    return sampling_tools if sampling_tools else None\n\n\ndef extract_tool_calls(\n    response: CreateMessageResult | CreateMessageResultWithTools,\n) -> list[ToolUseContent]:\n    \"\"\"Extract tool calls from a response.\"\"\"\n    content = response.content\n    if isinstance(content, list):\n        return [c for c in content if isinstance(c, ToolUseContent)]\n    elif isinstance(content, ToolUseContent):\n        return [content]\n    return []\n\n\ndef create_final_response_tool(result_type: type) -> SamplingTool:\n    \"\"\"Create a synthetic 'final_response' tool for structured output.\n\n    This tool is used to capture structured responses from the LLM.\n    The tool's schema is derived from the result_type.\n    \"\"\"\n    type_adapter = get_cached_typeadapter(result_type)\n    schema = type_adapter.json_schema()\n    schema = compress_schema(schema, prune_titles=True)\n\n    # Tool parameters must be object-shaped. Wrap primitives in {\"value\": <schema>}\n    if schema.get(\"type\") != \"object\":\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"value\": schema},\n            \"required\": [\"value\"],\n        }\n\n    # The fn just returns the input as-is (validation happens in the loop)\n    def final_response(**kwargs: Any) -> dict[str, Any]:\n        return kwargs\n\n    return SamplingTool(\n        name=\"final_response\",\n        description=(\n            \"Call this tool to provide your final response. \"\n            \"Use this when you have completed the task and are ready to return the result.\"\n        ),\n        parameters=schema,\n        fn=final_response,\n    )\n\n\n# --- Implementation functions for Context methods ---\n\n\nasync def sample_step_impl(\n    context: Context,\n    messages: str | Sequence[str | SamplingMessage],\n    *,\n    system_prompt: str | None = None,\n    temperature: float | None = None,\n    max_tokens: int | None = None,\n    model_preferences: ModelPreferences | str | list[str] | None = None,\n    tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]\n    | None = None,\n    tool_choice: ToolChoiceOption | str | None = None,\n    auto_execute_tools: bool = True,\n    mask_error_details: bool | None = None,\n    tool_concurrency: int | None = None,\n) -> SampleStep:\n    \"\"\"Implementation of Context.sample_step().\n\n    Make a single LLM sampling call. This is a stateless function that makes\n    exactly one LLM call and optionally executes any requested tools.\n    \"\"\"\n    # Convert messages to SamplingMessage objects\n    current_messages = prepare_messages(messages)\n\n    # Convert tools to SamplingTools\n    sampling_tools = prepare_tools(tools)\n    sdk_tools: list[SDKTool] | None = (\n        [t._to_sdk_tool() for t in sampling_tools] if sampling_tools else None\n    )\n    tool_map: dict[str, SamplingTool] = (\n        {t.name: t for t in sampling_tools} if sampling_tools else {}\n    )\n\n    # Determine whether to use fallback handler or client\n    use_fallback = determine_handler_mode(context, bool(sampling_tools))\n\n    # Build tool choice\n    effective_tool_choice: ToolChoice | None = None\n    if tool_choice is not None:\n        if tool_choice not in (\"auto\", \"required\", \"none\"):\n            raise ValueError(\n                f\"Invalid tool_choice: {tool_choice!r}. \"\n                \"Must be 'auto', 'required', or 'none'.\"\n            )\n        effective_tool_choice = ToolChoice(\n            mode=cast(Literal[\"auto\", \"required\", \"none\"], tool_choice)\n        )\n\n    # Effective max_tokens\n    effective_max_tokens = max_tokens if max_tokens is not None else 512\n\n    # Make the LLM call\n    if use_fallback:\n        response = await call_sampling_handler(\n            context,\n            current_messages,\n            system_prompt=system_prompt,\n            temperature=temperature,\n            max_tokens=effective_max_tokens,\n            model_preferences=model_preferences,\n            sdk_tools=sdk_tools,\n            tool_choice=effective_tool_choice,\n        )\n    else:\n        response = await context.session.create_message(\n            messages=current_messages,\n            system_prompt=system_prompt,\n            temperature=temperature,\n            max_tokens=effective_max_tokens,\n            model_preferences=_parse_model_preferences(model_preferences),\n            tools=sdk_tools,\n            tool_choice=effective_tool_choice,\n            related_request_id=context.request_id,\n        )\n\n    # Check if this is a tool use response\n    is_tool_use_response = (\n        isinstance(response, CreateMessageResultWithTools)\n        and response.stopReason == \"toolUse\"\n    )\n\n    # Always include the assistant response in history\n    current_messages.append(SamplingMessage(role=\"assistant\", content=response.content))\n\n    # If not a tool use, return immediately\n    if not is_tool_use_response:\n        return SampleStep(response=response, history=current_messages)\n\n    # If not executing tools, return with assistant message but no tool results\n    if not auto_execute_tools:\n        return SampleStep(response=response, history=current_messages)\n\n    # Execute tools and add results to history\n    step_tool_calls = extract_tool_calls(response)\n    if step_tool_calls:\n        effective_mask = (\n            mask_error_details\n            if mask_error_details is not None\n            else settings.mask_error_details\n        )\n        tool_results: list[ToolResultContent] = await execute_tools(\n            step_tool_calls,\n            tool_map,\n            mask_error_details=effective_mask,\n            tool_concurrency=tool_concurrency,\n        )\n\n        if tool_results:\n            current_messages.append(\n                SamplingMessage(\n                    role=\"user\",\n                    content=cast(list[SamplingMessageContentBlock], tool_results),\n                )\n            )\n\n    return SampleStep(response=response, history=current_messages)\n\n\nasync def sample_impl(\n    context: Context,\n    messages: str | Sequence[str | SamplingMessage],\n    *,\n    system_prompt: str | None = None,\n    temperature: float | None = None,\n    max_tokens: int | None = None,\n    model_preferences: ModelPreferences | str | list[str] | None = None,\n    tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]\n    | None = None,\n    result_type: type[ResultT] | None = None,\n    mask_error_details: bool | None = None,\n    tool_concurrency: int | None = None,\n) -> SamplingResult[ResultT]:\n    \"\"\"Implementation of Context.sample().\n\n    Send a sampling request to the client and await the response. This method\n    runs to completion automatically, executing a tool loop until the LLM\n    provides a final text response.\n    \"\"\"\n    # Safety limit to prevent infinite loops\n    max_iterations = 100\n\n    # Convert tools to SamplingTools\n    sampling_tools = prepare_tools(tools)\n\n    # Handle structured output with result_type\n    tool_choice: str | None = None\n    if result_type is not None and result_type is not str:\n        final_response_tool = create_final_response_tool(result_type)\n        sampling_tools = list(sampling_tools) if sampling_tools else []\n        sampling_tools.append(final_response_tool)\n\n        # Always require tool calls when result_type is set - the LLM must\n        # eventually call final_response (text responses are not accepted)\n        tool_choice = \"required\"\n\n    # Convert messages for the loop\n    current_messages: str | Sequence[str | SamplingMessage] = messages\n\n    for _iteration in range(max_iterations):\n        step = await sample_step_impl(\n            context,\n            messages=current_messages,\n            system_prompt=system_prompt,\n            temperature=temperature,\n            max_tokens=max_tokens,\n            model_preferences=model_preferences,\n            tools=sampling_tools,\n            tool_choice=tool_choice,\n            mask_error_details=mask_error_details,\n            tool_concurrency=tool_concurrency,\n        )\n\n        # Check for final_response tool call for structured output\n        if result_type is not None and result_type is not str and step.is_tool_use:\n            for tool_call in step.tool_calls:\n                if tool_call.name == \"final_response\":\n                    # Validate and return the structured result\n                    type_adapter = get_cached_typeadapter(result_type)\n\n                    # Unwrap if we wrapped primitives (non-object schemas)\n                    input_data = tool_call.input\n                    original_schema = compress_schema(\n                        type_adapter.json_schema(), prune_titles=True\n                    )\n                    if (\n                        original_schema.get(\"type\") != \"object\"\n                        and isinstance(input_data, dict)\n                        and \"value\" in input_data\n                    ):\n                        input_data = input_data[\"value\"]\n\n                    try:\n                        validated_result = type_adapter.validate_python(input_data)\n                        text = json.dumps(\n                            type_adapter.dump_python(validated_result, mode=\"json\")\n                        )\n                        return SamplingResult(\n                            text=text,\n                            result=validated_result,\n                            history=step.history,\n                        )\n                    except ValidationError as e:\n                        # Validation failed - add error as tool result\n                        step.history.append(\n                            SamplingMessage(\n                                role=\"user\",\n                                content=[\n                                    ToolResultContent(\n                                        type=\"tool_result\",\n                                        toolUseId=tool_call.id,\n                                        content=[\n                                            TextContent(\n                                                type=\"text\",\n                                                text=(\n                                                    f\"Validation error: {e}. \"\n                                                    \"Please try again with valid data.\"\n                                                ),\n                                            )\n                                        ],\n                                        isError=True,\n                                    )\n                                ],\n                            )\n                        )\n\n        # If not a tool use response, we're done\n        if not step.is_tool_use:\n            # For structured output, the LLM must use the final_response tool\n            if result_type is not None and result_type is not str:\n                raise RuntimeError(\n                    f\"Expected structured output of type {result_type.__name__}, \"\n                    \"but the LLM returned a text response instead of calling \"\n                    \"the final_response tool.\"\n                )\n            return SamplingResult(\n                text=step.text,\n                result=cast(ResultT, step.text if step.text else \"\"),\n                history=step.history,\n            )\n\n        # Continue with the updated history\n        current_messages = step.history\n\n        # After first iteration, reset tool_choice to auto (unless structured output is required)\n        if result_type is None or result_type is str:\n            tool_choice = None\n\n    raise RuntimeError(f\"Sampling exceeded maximum iterations ({max_iterations})\")\n"
  },
  {
    "path": "src/fastmcp/server/sampling/sampling_tool.py",
    "content": "\"\"\"SamplingTool for use during LLM sampling requests.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom mcp.types import TextContent\nfrom mcp.types import Tool as SDKTool\nfrom pydantic import ConfigDict\n\nfrom fastmcp.exceptions import AuthorizationError\nfrom fastmcp.server.auth.authorization import AuthContext, run_auth_checks\nfrom fastmcp.server.dependencies import get_access_token\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.tools.function_parsing import ParsedFunction\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import TransformedTool\nfrom fastmcp.utilities.types import FastMCPBaseModel\n\n\nclass SamplingTool(FastMCPBaseModel):\n    \"\"\"A tool that can be used during LLM sampling.\n\n    SamplingTools bundle a tool's schema (name, description, parameters) with\n    an executor function, enabling servers to execute agentic workflows where\n    the LLM can request tool calls during sampling.\n\n    In most cases, pass functions directly to ctx.sample():\n\n        def search(query: str) -> str:\n            '''Search the web.'''\n            return web_search(query)\n\n        result = await context.sample(\n            messages=\"Find info about Python\",\n            tools=[search],  # Plain functions work directly\n        )\n\n    Create a SamplingTool explicitly when you need custom name/description:\n\n        tool = SamplingTool.from_function(search, name=\"web_search\")\n    \"\"\"\n\n    name: str\n    description: str | None = None\n    parameters: dict[str, Any]\n    fn: Callable[..., Any]\n    sequential: bool = False\n\n    model_config = ConfigDict(arbitrary_types_allowed=True)\n\n    async def run(self, arguments: dict[str, Any] | None = None) -> Any:\n        \"\"\"Execute the tool with the given arguments.\n\n        Args:\n            arguments: Dictionary of arguments to pass to the tool function.\n\n        Returns:\n            The result of executing the tool function.\n        \"\"\"\n        if arguments is None:\n            arguments = {}\n\n        result = self.fn(**arguments)\n        if inspect.isawaitable(result):\n            result = await result\n        return result\n\n    def _to_sdk_tool(self) -> SDKTool:\n        \"\"\"Convert to an mcp.types.Tool for SDK compatibility.\n\n        This is used internally when passing tools to the MCP SDK's\n        create_message() method.\n        \"\"\"\n        return SDKTool(\n            name=self.name,\n            description=self.description,\n            inputSchema=self.parameters,\n        )\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        description: str | None = None,\n        sequential: bool = False,\n    ) -> SamplingTool:\n        \"\"\"Create a SamplingTool from a function.\n\n        The function's signature is analyzed to generate a JSON schema for\n        the tool's parameters. Type hints are used to determine parameter types.\n\n        Args:\n            fn: The function to create a tool from.\n            name: Optional name override. Defaults to the function's name.\n            description: Optional description override. Defaults to the function's docstring.\n            sequential: If True, this tool requires sequential execution and prevents\n                parallel execution of all tools in the batch. Set to True for tools\n                with shared state, file writes, or other operations that cannot run\n                concurrently. Defaults to False.\n\n        Returns:\n            A SamplingTool wrapping the function.\n\n        Raises:\n            ValueError: If the function is a lambda without a name override.\n        \"\"\"\n        parsed = ParsedFunction.from_function(fn, validate=True)\n\n        if name is None and parsed.name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        return cls(\n            name=name or parsed.name,\n            description=description or parsed.description,\n            parameters=parsed.input_schema,\n            fn=parsed.fn,\n            sequential=sequential,\n        )\n\n    @classmethod\n    def from_callable_tool(\n        cls,\n        tool: FunctionTool | TransformedTool,\n        *,\n        name: str | None = None,\n        description: str | None = None,\n    ) -> SamplingTool:\n        \"\"\"Create a SamplingTool from a FunctionTool or TransformedTool.\n\n        Reuses existing server tools in sampling contexts. For TransformedTool,\n        the tool's .run() method is used to ensure proper argument transformation,\n        and the ToolResult is automatically unwrapped.\n\n        Args:\n            tool: A FunctionTool or TransformedTool to convert.\n            name: Optional name override. Defaults to tool.name.\n            description: Optional description override. Defaults to tool.description.\n\n        Raises:\n            TypeError: If the tool is not a FunctionTool or TransformedTool.\n        \"\"\"\n        # Validate that the tool is a supported type\n        if not isinstance(tool, (FunctionTool, TransformedTool)):\n            raise TypeError(\n                f\"Expected FunctionTool or TransformedTool, got {type(tool).__name__}. \"\n                \"Only callable tools can be converted to SamplingTools.\"\n            )\n\n        # Both FunctionTool and TransformedTool need .run() to ensure proper\n        # result processing (serializers, output_schema, wrap-result flags)\n        async def wrapper(**kwargs: Any) -> Any:\n            # Enforce per-tool auth checks, mirroring what the server\n            # dispatcher does for direct tool calls.  Without this, an\n            # auth-protected tool wrapped as a SamplingTool could be\n            # invoked by the LLM during sampling without authorization.\n            if tool.auth is not None:\n                # Late import to avoid circular import with context.py\n                from fastmcp.server.context import _current_transport\n\n                is_stdio = _current_transport.get() == \"stdio\"\n                if not is_stdio:\n                    token = get_access_token()\n                    ctx = AuthContext(token=token, component=tool)\n                    if not await run_auth_checks(tool.auth, ctx):\n                        raise AuthorizationError(\n                            f\"Authorization failed for tool '{tool.name}': \"\n                            \"insufficient permissions\"\n                        )\n\n            result = await tool.run(kwargs)\n            # Unwrap ToolResult - extract the actual value\n            if isinstance(result, ToolResult):\n                # If there's structured_content, use that\n                if result.structured_content is not None:\n                    # Check tool's schema - this is the source of truth\n                    if tool.output_schema and tool.output_schema.get(\n                        \"x-fastmcp-wrap-result\"\n                    ):\n                        # Tool wraps results: {\"result\": value} -> value\n                        return result.structured_content.get(\"result\")\n                    else:\n                        # No wrapping: use structured_content directly\n                        return result.structured_content\n                # Otherwise, extract from text content\n                if result.content and len(result.content) > 0:\n                    first_content = result.content[0]\n                    if isinstance(first_content, TextContent):\n                        return first_content.text\n            return result\n\n        fn = wrapper\n\n        # Extract the callable function, name, description, and parameters\n        return cls(\n            name=name or tool.name,\n            description=description or tool.description,\n            parameters=tool.parameters,\n            fn=fn,\n        )\n"
  },
  {
    "path": "src/fastmcp/server/server.py",
    "content": "\"\"\"FastMCP - A more ergonomic interface for MCP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport re\nimport secrets\nimport warnings\nfrom collections.abc import (\n    AsyncIterator,\n    Awaitable,\n    Callable,\n    Sequence,\n)\nfrom contextlib import (\n    AbstractAsyncContextManager,\n    asynccontextmanager,\n)\nfrom dataclasses import replace\nfrom functools import partial\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, cast, overload\n\nimport httpx\nimport mcp.types\nfrom key_value.aio.adapters.pydantic import PydanticAdapter\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.lowlevel.server import LifespanResultT\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import (\n    Annotations,\n    AnyFunction,\n    CallToolRequestParams,\n    ToolAnnotations,\n)\nfrom pydantic import AnyUrl\nfrom pydantic import ValidationError as PydanticValidationError\nfrom starlette.routing import BaseRoute\nfrom typing_extensions import Self\n\nimport fastmcp\nimport fastmcp.server\nfrom fastmcp.exceptions import (\n    AuthorizationError,\n    FastMCPError,\n    NotFoundError,\n    PromptError,\n    ResourceError,\n    ToolError,\n    ValidationError,\n)\nfrom fastmcp.mcp_config import MCPConfig\nfrom fastmcp.prompts import Prompt\nfrom fastmcp.prompts.base import PromptResult\nfrom fastmcp.prompts.function_prompt import FunctionPrompt\nfrom fastmcp.resources.base import Resource, ResourceResult\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.apps import (\n    AppConfig,\n    app_config_to_meta_dict,\n    resolve_ui_mime_type,\n)\nfrom fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks\nfrom fastmcp.server.lifespan import Lifespan\nfrom fastmcp.server.low_level import LowLevelServer\nfrom fastmcp.server.middleware import Middleware, MiddlewareContext\nfrom fastmcp.server.mixins import LifespanMixin, MCPOperationsMixin, TransportMixin\nfrom fastmcp.server.providers import LocalProvider, Provider\nfrom fastmcp.server.providers.aggregate import AggregateProvider\nfrom fastmcp.server.tasks.config import TaskConfig, TaskMeta\nfrom fastmcp.server.telemetry import server_span\nfrom fastmcp.server.transforms import (\n    ToolTransform,\n    Transform,\n)\nfrom fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled\nfrom fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\nfrom fastmcp.utilities.components import FastMCPComponent, _coerce_version\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import FastMCPBaseModel, NotSet, NotSetT\nfrom fastmcp.utilities.versions import (\n    VersionSpec,\n    version_sort_key,\n)\n\nif TYPE_CHECKING:\n    from fastmcp.client import Client\n    from fastmcp.client.client import FastMCP1Server\n    from fastmcp.client.sampling import SamplingHandler\n    from fastmcp.client.transports import ClientTransport, ClientTransportT\n    from fastmcp.server.providers.openapi import ComponentFn as OpenAPIComponentFn\n    from fastmcp.server.providers.openapi import RouteMap\n    from fastmcp.server.providers.openapi import RouteMapFn as OpenAPIRouteMapFn\n    from fastmcp.server.providers.proxy import FastMCPProxy\n\nlogger = get_logger(__name__)\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\nDuplicateBehavior = Literal[\"warn\", \"error\", \"replace\", \"ignore\"]\n\n\n_REMOVED_KWARGS: dict[str, str] = {\n    \"host\": \"Pass `host` to `run_http_async()`, or set FASTMCP_HOST.\",\n    \"port\": \"Pass `port` to `run_http_async()`, or set FASTMCP_PORT.\",\n    \"sse_path\": \"Pass `path` to `run_http_async()` or `http_app()`, or set FASTMCP_SSE_PATH.\",\n    \"message_path\": \"Set FASTMCP_MESSAGE_PATH.\",\n    \"streamable_http_path\": \"Pass `path` to `run_http_async()` or `http_app()`, or set FASTMCP_STREAMABLE_HTTP_PATH.\",\n    \"json_response\": \"Pass `json_response` to `run_http_async()` or `http_app()`, or set FASTMCP_JSON_RESPONSE.\",\n    \"stateless_http\": \"Pass `stateless_http` to `run_http_async()` or `http_app()`, or set FASTMCP_STATELESS_HTTP.\",\n    \"debug\": \"Set FASTMCP_DEBUG.\",\n    \"log_level\": \"Pass `log_level` to `run_http_async()`, or set FASTMCP_LOG_LEVEL.\",\n    \"on_duplicate_tools\": \"Use `on_duplicate=` instead.\",\n    \"on_duplicate_resources\": \"Use `on_duplicate=` instead.\",\n    \"on_duplicate_prompts\": \"Use `on_duplicate=` instead.\",\n    \"tool_serializer\": \"Return ToolResult from your tools instead. See https://gofastmcp.com/servers/tools#custom-serialization\",\n    \"include_tags\": \"Use `server.enable(tags=..., only=True)` after creating the server.\",\n    \"exclude_tags\": \"Use `server.disable(tags=...)` after creating the server.\",\n    \"tool_transformations\": \"Use `server.add_transform(ToolTransform(...))` after creating the server.\",\n}\n\n\ndef _check_removed_kwargs(kwargs: dict[str, Any]) -> None:\n    \"\"\"Raise helpful TypeErrors for kwargs removed in v3.\"\"\"\n    for key in kwargs:\n        if key in _REMOVED_KWARGS:\n            raise TypeError(\n                f\"FastMCP() no longer accepts `{key}`. {_REMOVED_KWARGS[key]}\"\n            )\n    if kwargs:\n        raise TypeError(\n            f\"FastMCP() got unexpected keyword argument(s): {', '.join(repr(k) for k in kwargs)}\"\n        )\n\n\nTransport = Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"]\n\n# Compiled URI parsing regex to split a URI into protocol and path components\nURI_PATTERN = re.compile(r\"^([^:]+://)(.*?)$\")\n\n\nLifespanCallable = Callable[\n    [\"FastMCP[LifespanResultT]\"], AbstractAsyncContextManager[LifespanResultT]\n]\n\n\ndef _get_auth_context() -> tuple[bool, Any]:\n    \"\"\"Get auth context for the current request.\n\n    Returns a tuple of (skip_auth, token) where:\n    - skip_auth=True means auth checks should be skipped (STDIO transport)\n    - token is the access token for HTTP transports (may be None if unauthenticated)\n\n    Uses late import to avoid circular import with context.py.\n    \"\"\"\n    from fastmcp.server.context import _current_transport\n\n    is_stdio = _current_transport.get() == \"stdio\"\n    if is_stdio:\n        return (True, None)\n    from fastmcp.server.dependencies import get_access_token\n\n    return (False, get_access_token())\n\n\n@asynccontextmanager\nasync def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:\n    \"\"\"Default lifespan context manager that does nothing.\n\n    Args:\n        server: The server instance this lifespan is managing\n\n    Returns:\n        An empty dictionary as the lifespan result.\n    \"\"\"\n    yield {}\n\n\ndef _lifespan_proxy(\n    fastmcp_server: FastMCP[LifespanResultT],\n) -> Callable[\n    [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]\n]:\n    @asynccontextmanager\n    async def wrap(\n        low_level_server: LowLevelServer[LifespanResultT],\n    ) -> AsyncIterator[LifespanResultT]:\n        if fastmcp_server._lifespan is default_lifespan:\n            yield {}\n            return\n\n        if not fastmcp_server._lifespan_result_set:\n            raise RuntimeError(\n                \"FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. \"\n                + \" Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/PrefectHQ/fastmcp/issues.\"\n            )\n\n        yield fastmcp_server._lifespan_result\n\n    return wrap\n\n\nclass StateValue(FastMCPBaseModel):\n    \"\"\"Wrapper for stored context state values.\"\"\"\n\n    value: Any\n\n\nclass FastMCP(\n    AggregateProvider,\n    LifespanMixin,\n    MCPOperationsMixin,\n    TransportMixin,\n    Generic[LifespanResultT],\n):\n    def __init__(\n        self,\n        name: str | None = None,\n        instructions: str | None = None,\n        *,\n        version: str | int | float | None = None,\n        website_url: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        auth: AuthProvider | None = None,\n        middleware: Sequence[Middleware] | None = None,\n        providers: Sequence[Provider] | None = None,\n        transforms: Sequence[Transform] | None = None,\n        lifespan: LifespanCallable | Lifespan | None = None,\n        tools: Sequence[Tool | Callable[..., Any]] | None = None,\n        on_duplicate: DuplicateBehavior | None = None,\n        mask_error_details: bool | None = None,\n        dereference_schemas: bool = True,\n        strict_input_validation: bool | None = None,\n        list_page_size: int | None = None,\n        tasks: bool | None = None,\n        session_state_store: AsyncKeyValue | None = None,\n        sampling_handler: SamplingHandler | None = None,\n        sampling_handler_behavior: Literal[\"always\", \"fallback\"] | None = None,\n        client_log_level: mcp.types.LoggingLevel | None = None,\n        **kwargs: Any,\n    ):\n        _check_removed_kwargs(kwargs)\n\n        # Initialize Provider (sets up _transforms)\n        super().__init__()\n\n        self._on_duplicate: DuplicateBehaviorSetting = on_duplicate or \"warn\"\n\n        # Resolve server default for background task support\n        self._support_tasks_by_default: bool = tasks if tasks is not None else False\n\n        # Docket and Worker instances (set during lifespan for cross-task access)\n        self._docket = None\n        self._worker = None\n\n        self._additional_http_routes: list[BaseRoute] = []\n\n        # Session-scoped state store (shared across all requests)\n        self._state_storage: AsyncKeyValue = session_state_store or MemoryStore()\n        self._state_store: PydanticAdapter[StateValue] = PydanticAdapter[StateValue](\n            key_value=self._state_storage,\n            pydantic_model=StateValue,\n            default_collection=\"fastmcp_state\",\n        )\n\n        # Create LocalProvider for local components\n        self._local_provider: LocalProvider = LocalProvider(\n            on_duplicate=self._on_duplicate\n        )\n\n        # Add providers using AggregateProvider's add_provider\n        # LocalProvider is always first (no namespace)\n        self.add_provider(self._local_provider)\n        for p in providers or []:\n            self.add_provider(p)\n\n        for t in transforms or []:\n            self.add_transform(t)\n\n        # Store mask_error_details for execution error handling\n        self._mask_error_details: bool = (\n            mask_error_details\n            if mask_error_details is not None\n            else fastmcp.settings.mask_error_details\n        )\n\n        # Store list_page_size for pagination of list operations\n        if list_page_size is not None and list_page_size <= 0:\n            raise ValueError(\"list_page_size must be a positive integer\")\n        self._list_page_size: int | None = list_page_size\n\n        # Handle Lifespan instances (they're callable) or regular lifespan functions\n        if lifespan is not None:\n            self._lifespan: LifespanCallable[LifespanResultT] = cast(\n                LifespanCallable[LifespanResultT], lifespan\n            )\n        else:\n            self._lifespan = cast(LifespanCallable[LifespanResultT], default_lifespan)\n        self._lifespan_result: LifespanResultT | None = None\n        self._lifespan_result_set: bool = False\n        self._lifespan_ref_count: int = 0\n        self._lifespan_lock: asyncio.Lock = asyncio.Lock()\n        self._started: asyncio.Event = asyncio.Event()\n\n        # Generate random ID if no name provided\n        self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[\n            LifespanResultT\n        ](\n            fastmcp=self,\n            name=name or self.generate_name(),\n            version=_coerce_version(version) or fastmcp.__version__,\n            instructions=instructions,\n            website_url=website_url,\n            icons=icons,\n            lifespan=_lifespan_proxy(fastmcp_server=self),\n        )\n\n        self.auth: AuthProvider | None = auth\n\n        if tools:\n            for tool in tools:\n                if not isinstance(tool, Tool):\n                    tool = Tool.from_function(tool)\n                self.add_tool(tool)\n\n        self.strict_input_validation: bool = (\n            strict_input_validation\n            if strict_input_validation is not None\n            else fastmcp.settings.strict_input_validation\n        )\n\n        self.client_log_level: mcp.types.LoggingLevel | None = (\n            client_log_level\n            if client_log_level is not None\n            else fastmcp.settings.client_log_level\n        )\n\n        self.middleware: list[Middleware] = list(middleware or [])\n\n        if dereference_schemas:\n            from fastmcp.server.middleware.dereference import (\n                DereferenceRefsMiddleware,\n            )\n\n            self.middleware.append(DereferenceRefsMiddleware())\n\n        # Set up MCP protocol handlers\n        self._setup_handlers()\n\n        self.sampling_handler: SamplingHandler | None = sampling_handler\n        self.sampling_handler_behavior: Literal[\"always\", \"fallback\"] = (\n            sampling_handler_behavior or \"fallback\"\n        )\n\n    def __repr__(self) -> str:\n        return f\"{type(self).__name__}({self.name!r})\"\n\n    @property\n    def name(self) -> str:\n        return self._mcp_server.name\n\n    @property\n    def instructions(self) -> str | None:\n        return self._mcp_server.instructions\n\n    @instructions.setter\n    def instructions(self, value: str | None) -> None:\n        self._mcp_server.instructions = value\n\n    @property\n    def version(self) -> str | None:\n        return self._mcp_server.version\n\n    @property\n    def website_url(self) -> str | None:\n        return self._mcp_server.website_url\n\n    @property\n    def icons(self) -> list[mcp.types.Icon]:\n        if self._mcp_server.icons is None:\n            return []\n        else:\n            return list(self._mcp_server.icons)\n\n    @property\n    def local_provider(self) -> LocalProvider:\n        \"\"\"The server's local provider, which stores directly-registered components.\n\n        Use this to remove components:\n\n            mcp.local_provider.remove_tool(\"my_tool\")\n            mcp.local_provider.remove_resource(\"data://info\")\n            mcp.local_provider.remove_prompt(\"my_prompt\")\n        \"\"\"\n        return self._local_provider\n\n    async def _run_middleware(\n        self,\n        context: MiddlewareContext[Any],\n        call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],\n    ) -> Any:\n        \"\"\"Builds and executes the middleware chain.\"\"\"\n        chain = call_next\n        for mw in reversed(self.middleware):\n            chain = partial(mw, call_next=chain)\n        return await chain(context)\n\n    def add_middleware(self, middleware: Middleware) -> None:\n        self.middleware.append(middleware)\n\n    def add_provider(self, provider: Provider, *, namespace: str = \"\") -> None:\n        \"\"\"Add a provider for dynamic tools, resources, and prompts.\n\n        Providers are queried in registration order. The first provider to return\n        a non-None result wins. Static components (registered via decorators)\n        always take precedence over providers.\n\n        Args:\n            provider: A Provider instance that will provide components dynamically.\n            namespace: Optional namespace prefix. When set:\n                - Tools become \"namespace_toolname\"\n                - Resources become \"protocol://namespace/path\"\n                - Prompts become \"namespace_promptname\"\n        \"\"\"\n        super().add_provider(provider, namespace=namespace)\n\n    # -------------------------------------------------------------------------\n    # Provider interface overrides - inherited from AggregateProvider\n    # -------------------------------------------------------------------------\n    # _list_tools, _list_resources, _list_resource_templates, _list_prompts\n    # are inherited from AggregateProvider which handles aggregation and namespacing\n\n    async def get_tasks(self) -> Sequence[FastMCPComponent]:\n        \"\"\"Get task-eligible components with all transforms applied.\n\n        Overrides AggregateProvider.get_tasks() to apply server-level transforms\n        after aggregation. AggregateProvider handles provider-level namespacing.\n        \"\"\"\n        # Get tasks from AggregateProvider (handles aggregation and namespacing)\n        components = list(await super().get_tasks())\n\n        # Separate by component type for server-level transform application\n        tools = [c for c in components if isinstance(c, Tool)]\n        resources = [c for c in components if isinstance(c, Resource)]\n        templates = [c for c in components if isinstance(c, ResourceTemplate)]\n        prompts = [c for c in components if isinstance(c, Prompt)]\n\n        # Apply server-level transforms sequentially\n        for transform in self.transforms:\n            tools = await transform.list_tools(tools)\n            resources = await transform.list_resources(resources)\n            templates = await transform.list_resource_templates(templates)\n            prompts = await transform.list_prompts(prompts)\n\n        return [\n            *tools,\n            *resources,\n            *templates,\n            *prompts,\n        ]\n\n    def add_transform(self, transform: Transform) -> None:\n        \"\"\"Add a server-level transform.\n\n        Server-level transforms are applied after all providers are aggregated.\n        They transform tools, resources, and prompts from ALL providers.\n\n        Args:\n            transform: The transform to add.\n\n        Example:\n            ```python\n            from fastmcp.server.transforms import Namespace\n\n            server = FastMCP(\"Server\")\n            server.add_transform(Namespace(\"api\"))\n            # All tools from all providers become \"api_toolname\"\n            ```\n        \"\"\"\n        self._transforms.append(transform)\n\n    def add_tool_transformation(\n        self, tool_name: str, transformation: ToolTransformConfig\n    ) -> None:\n        \"\"\"Add a tool transformation.\n\n        .. deprecated::\n            Use ``add_transform(ToolTransform({...}))`` instead.\n        \"\"\"\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"add_tool_transformation is deprecated. Use \"\n                \"server.add_transform(ToolTransform({tool_name: config})) instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        self.add_transform(ToolTransform({tool_name: transformation}))\n\n    def remove_tool_transformation(self, _tool_name: str) -> None:\n        \"\"\"Remove a tool transformation.\n\n        .. deprecated::\n            Tool transformations are now immutable. Use enable/disable controls instead.\n        \"\"\"\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"remove_tool_transformation is deprecated and has no effect. \"\n                \"Transforms are immutable once added. Use server.disable(keys=[...]) \"\n                \"to hide tools instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n    async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]:\n        \"\"\"List all enabled tools from providers.\n\n        Overrides Provider.list_tools() to add visibility filtering, auth filtering,\n        and middleware execution. Returns all versions (no deduplication).\n        Protocol handlers deduplicate for MCP wire format.\n        \"\"\"\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                mw_context = MiddlewareContext(\n                    message=mcp.types.ListToolsRequest(method=\"tools/list\"),\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"tools/list\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.list_tools(run_middleware=False),\n                )\n\n            # Get all tools, apply session transforms, then filter enabled\n            tools = list(await super().list_tools())\n            tools = await apply_session_transforms(tools)\n            tools = [t for t in tools if is_enabled(t)]\n\n            skip_auth, token = _get_auth_context()\n            authorized: list[Tool] = []\n            for tool in tools:\n                if not skip_auth and tool.auth is not None:\n                    ctx = AuthContext(token=token, component=tool)\n                    try:\n                        if not await run_auth_checks(tool.auth, ctx):\n                            continue\n                    except AuthorizationError:\n                        continue\n                authorized.append(tool)\n            return authorized\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name via aggregation from providers.\n\n        Extends AggregateProvider._get_tool() with component-level auth checks.\n\n        Args:\n            name: The tool name.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The tool if found and authorized, None if not found or unauthorized.\n        \"\"\"\n        # Get tool from AggregateProvider (handles aggregation and namespacing)\n        tool = await super()._get_tool(name, version)\n        if tool is None:\n            return None\n\n        # Component auth - return None if unauthorized (consistent with list filtering)\n        skip_auth, token = _get_auth_context()\n        if not skip_auth and tool.auth is not None:\n            ctx = AuthContext(token=token, component=tool)\n            try:\n                if not await run_auth_checks(tool.auth, ctx):\n                    return None\n            except AuthorizationError:\n                return None\n\n        return tool\n\n    async def get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name, filtering disabled tools.\n\n        Overrides Provider.get_tool() to add visibility filtering after all\n        transforms (including session-level) have been applied. This ensures\n        session transforms can override provider-level disables.\n\n        When the highest version is disabled and no explicit version was\n        requested, falls back to the next-highest enabled version.\n\n        Args:\n            name: The tool name.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The tool if found and enabled, None otherwise.\n        \"\"\"\n        tool = await super().get_tool(name, version)\n        if tool is None:\n            return None\n\n        # Apply session transforms to single item\n        tools = await apply_session_transforms([tool])\n        if tools and is_enabled(tools[0]):\n            return tools[0]\n\n        # The highest version is disabled. If an explicit version was requested,\n        # respect the disable. Otherwise fall back to the next-highest enabled version.\n        if version is not None:\n            return None\n\n        all_tools = [t for t in await super().list_tools() if t.name == name]\n        all_tools = list(await apply_session_transforms(all_tools))\n        enabled = [t for t in all_tools if is_enabled(t)]\n\n        skip_auth, token = _get_auth_context()\n        authorized: list[Tool] = []\n        for t in enabled:\n            if not skip_auth and t.auth is not None:\n                ctx = AuthContext(token=token, component=t)\n                try:\n                    if not await run_auth_checks(t.auth, ctx):\n                        continue\n                except AuthorizationError:\n                    continue\n            authorized.append(t)\n\n        if not authorized:\n            return None\n        return cast(Tool, max(authorized, key=version_sort_key))\n\n    async def list_resources(\n        self, *, run_middleware: bool = True\n    ) -> Sequence[Resource]:\n        \"\"\"List all enabled resources from providers.\n\n        Overrides Provider.list_resources() to add visibility filtering, auth filtering,\n        and middleware execution. Returns all versions (no deduplication).\n        Protocol handlers deduplicate for MCP wire format.\n        \"\"\"\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                mw_context = MiddlewareContext(\n                    message={},\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"resources/list\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.list_resources(run_middleware=False),\n                )\n\n            # Get all resources, apply session transforms, then filter enabled\n            resources = list(await super().list_resources())\n            resources = await apply_session_transforms(resources)\n            resources = [r for r in resources if is_enabled(r)]\n\n            skip_auth, token = _get_auth_context()\n            authorized: list[Resource] = []\n            for resource in resources:\n                if not skip_auth and resource.auth is not None:\n                    ctx = AuthContext(token=token, component=resource)\n                    try:\n                        if not await run_auth_checks(resource.auth, ctx):\n                            continue\n                    except AuthorizationError:\n                        continue\n                authorized.append(resource)\n            return authorized\n\n    async def _get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI via aggregation from providers.\n\n        Extends AggregateProvider._get_resource() with component-level auth checks.\n\n        Args:\n            uri: The resource URI.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The resource if found and authorized, None if not found or unauthorized.\n        \"\"\"\n        # Get resource from AggregateProvider (handles aggregation and namespacing)\n        resource = await super()._get_resource(uri, version)\n        if resource is None:\n            return None\n\n        # Component auth - return None if unauthorized (consistent with list filtering)\n        skip_auth, token = _get_auth_context()\n        if not skip_auth and resource.auth is not None:\n            ctx = AuthContext(token=token, component=resource)\n            try:\n                if not await run_auth_checks(resource.auth, ctx):\n                    return None\n            except AuthorizationError:\n                return None\n\n        return resource\n\n    async def get_resource(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI, filtering disabled resources.\n\n        Overrides Provider.get_resource() to add visibility filtering after all\n        transforms (including session-level) have been applied.\n\n        When the highest version is disabled and no explicit version was\n        requested, falls back to the next-highest enabled version.\n\n        Args:\n            uri: The resource URI.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The resource if found and enabled, None otherwise.\n        \"\"\"\n        resource = await super().get_resource(uri, version)\n        if resource is None:\n            return None\n\n        # Apply session transforms to single item\n        resources = await apply_session_transforms([resource])\n        if resources and is_enabled(resources[0]):\n            return resources[0]\n\n        if version is not None:\n            return None\n\n        all_resources = [r for r in await super().list_resources() if str(r.uri) == uri]\n        all_resources = list(await apply_session_transforms(all_resources))\n        enabled = [r for r in all_resources if is_enabled(r)]\n\n        skip_auth, token = _get_auth_context()\n        authorized: list[Resource] = []\n        for r in enabled:\n            if not skip_auth and r.auth is not None:\n                ctx = AuthContext(token=token, component=r)\n                try:\n                    if not await run_auth_checks(r.auth, ctx):\n                        continue\n                except AuthorizationError:\n                    continue\n            authorized.append(r)\n\n        if not authorized:\n            return None\n        return cast(Resource, max(authorized, key=version_sort_key))\n\n    async def list_resource_templates(\n        self, *, run_middleware: bool = True\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"List all enabled resource templates from providers.\n\n        Overrides Provider.list_resource_templates() to add visibility filtering,\n        auth filtering, and middleware execution. Returns all versions (no deduplication).\n        Protocol handlers deduplicate for MCP wire format.\n        \"\"\"\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                mw_context = MiddlewareContext(\n                    message={},\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"resources/templates/list\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.list_resource_templates(\n                        run_middleware=False\n                    ),\n                )\n\n            # Get all templates, apply session transforms, then filter enabled\n            templates = list(await super().list_resource_templates())\n            templates = await apply_session_transforms(templates)\n            templates = [t for t in templates if is_enabled(t)]\n\n            skip_auth, token = _get_auth_context()\n            authorized: list[ResourceTemplate] = []\n            for template in templates:\n                if not skip_auth and template.auth is not None:\n                    ctx = AuthContext(token=token, component=template)\n                    try:\n                        if not await run_auth_checks(template.auth, ctx):\n                            continue\n                    except AuthorizationError:\n                        continue\n                authorized.append(template)\n            return authorized\n\n    async def _get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template by URI via aggregation from providers.\n\n        Extends AggregateProvider._get_resource_template() with component-level auth checks.\n\n        Args:\n            uri: The template URI to match.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The template if found and authorized, None if not found or unauthorized.\n        \"\"\"\n        # Get template from AggregateProvider (handles aggregation and namespacing)\n        template = await super()._get_resource_template(uri, version)\n        if template is None:\n            return None\n\n        # Component auth - return None if unauthorized (consistent with list filtering)\n        skip_auth, token = _get_auth_context()\n        if not skip_auth and template.auth is not None:\n            ctx = AuthContext(token=token, component=template)\n            try:\n                if not await run_auth_checks(template.auth, ctx):\n                    return None\n            except AuthorizationError:\n                return None\n\n        return template\n\n    async def get_resource_template(\n        self, uri: str, version: VersionSpec | None = None\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template by URI, filtering disabled templates.\n\n        Overrides Provider.get_resource_template() to add visibility filtering after\n        all transforms (including session-level) have been applied.\n\n        When the highest version is disabled and no explicit version was\n        requested, falls back to the next-highest enabled version.\n\n        Args:\n            uri: The template URI.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The template if found and enabled, None otherwise.\n        \"\"\"\n        template = await super().get_resource_template(uri, version)\n        if template is None:\n            return None\n\n        # Apply session transforms to single item\n        templates = await apply_session_transforms([template])\n        if templates and is_enabled(templates[0]):\n            return templates[0]\n\n        if version is not None:\n            return None\n\n        all_templates = [\n            t\n            for t in await super().list_resource_templates()\n            if t.matches(uri) is not None\n        ]\n        all_templates = list(await apply_session_transforms(all_templates))\n        enabled = [t for t in all_templates if is_enabled(t)]\n\n        skip_auth, token = _get_auth_context()\n        authorized: list[ResourceTemplate] = []\n        for t in enabled:\n            if not skip_auth and t.auth is not None:\n                ctx = AuthContext(token=token, component=t)\n                try:\n                    if not await run_auth_checks(t.auth, ctx):\n                        continue\n                except AuthorizationError:\n                    continue\n            authorized.append(t)\n\n        if not authorized:\n            return None\n        return cast(ResourceTemplate, max(authorized, key=version_sort_key))\n\n    async def list_prompts(self, *, run_middleware: bool = True) -> Sequence[Prompt]:\n        \"\"\"List all enabled prompts from providers.\n\n        Overrides Provider.list_prompts() to add visibility filtering, auth filtering,\n        and middleware execution. Returns all versions (no deduplication).\n        Protocol handlers deduplicate for MCP wire format.\n        \"\"\"\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                mw_context = MiddlewareContext(\n                    message={},\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"prompts/list\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.list_prompts(run_middleware=False),\n                )\n\n            # Get all prompts, apply session transforms, then filter enabled\n            prompts = list(await super().list_prompts())\n            prompts = await apply_session_transforms(prompts)\n            prompts = [p for p in prompts if is_enabled(p)]\n\n            skip_auth, token = _get_auth_context()\n            authorized: list[Prompt] = []\n            for prompt in prompts:\n                if not skip_auth and prompt.auth is not None:\n                    ctx = AuthContext(token=token, component=prompt)\n                    try:\n                        if not await run_auth_checks(prompt.auth, ctx):\n                            continue\n                    except AuthorizationError:\n                        continue\n                authorized.append(prompt)\n            return authorized\n\n    async def _get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a prompt by name via aggregation from providers.\n\n        Extends AggregateProvider._get_prompt() with component-level auth checks.\n\n        Args:\n            name: The prompt name.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The prompt if found and authorized, None if not found or unauthorized.\n        \"\"\"\n        # Get prompt from AggregateProvider (handles aggregation and namespacing)\n        prompt = await super()._get_prompt(name, version)\n        if prompt is None:\n            return None\n\n        # Component auth - return None if unauthorized (consistent with list filtering)\n        skip_auth, token = _get_auth_context()\n        if not skip_auth and prompt.auth is not None:\n            ctx = AuthContext(token=token, component=prompt)\n            try:\n                if not await run_auth_checks(prompt.auth, ctx):\n                    return None\n            except AuthorizationError:\n                return None\n\n        return prompt\n\n    async def get_prompt(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a prompt by name, filtering disabled prompts.\n\n        Overrides Provider.get_prompt() to add visibility filtering after all\n        transforms (including session-level) have been applied.\n\n        When the highest version is disabled and no explicit version was\n        requested, falls back to the next-highest enabled version.\n\n        Args:\n            name: The prompt name.\n            version: Version filter (None returns highest version).\n\n        Returns:\n            The prompt if found and enabled, None otherwise.\n        \"\"\"\n        prompt = await super().get_prompt(name, version)\n        if prompt is None:\n            return None\n\n        # Apply session transforms to single item\n        prompts = await apply_session_transforms([prompt])\n        if prompts and is_enabled(prompts[0]):\n            return prompts[0]\n\n        if version is not None:\n            return None\n\n        all_prompts = [p for p in await super().list_prompts() if p.name == name]\n        all_prompts = list(await apply_session_transforms(all_prompts))\n        enabled = [p for p in all_prompts if is_enabled(p)]\n\n        skip_auth, token = _get_auth_context()\n        authorized: list[Prompt] = []\n        for p in enabled:\n            if not skip_auth and p.auth is not None:\n                ctx = AuthContext(token=token, component=p)\n                try:\n                    if not await run_auth_checks(p.auth, ctx):\n                        continue\n                except AuthorizationError:\n                    continue\n            authorized.append(p)\n\n        if not authorized:\n            return None\n        return cast(Prompt, max(authorized, key=version_sort_key))\n\n    @overload\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: None = None,\n    ) -> ToolResult: ...\n\n    @overload\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: TaskMeta | None = None,\n    ) -> ToolResult | mcp.types.CreateTaskResult:\n        \"\"\"Call a tool by name.\n\n        This is the public API for executing tools. By default, middleware is applied.\n\n        Args:\n            name: The tool name\n            arguments: Tool arguments (optional)\n            version: Specific version to call. If None, calls highest version.\n            run_middleware: If True (default), apply the middleware chain.\n                Set to False when called from middleware to avoid re-applying.\n            task_meta: If provided, execute as a background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return ToolResult.\n\n        Returns:\n            ToolResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Raises:\n            NotFoundError: If tool not found or disabled\n            ToolError: If tool execution fails\n            ValidationError: If arguments fail validation\n        \"\"\"\n        # Note: fn_key enrichment happens here after finding the tool.\n        # For mounted servers, the parent's provider sets fn_key to the\n        # namespaced key before delegating, ensuring correct Docket routing.\n\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                mw_context = MiddlewareContext[CallToolRequestParams](\n                    message=mcp.types.CallToolRequestParams(\n                        name=name, arguments=arguments or {}\n                    ),\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"tools/call\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.call_tool(\n                        context.message.name,\n                        context.message.arguments or {},\n                        version=version,\n                        run_middleware=False,\n                        task_meta=task_meta,\n                    ),\n                )\n\n            # Core logic: find and execute tool (providers queried in parallel)\n            # Use get_tool to apply transforms and filter disabled\n            with server_span(\n                f\"tools/call {name}\", \"tools/call\", self.name, \"tool\", name\n            ) as span:\n                # Try normal provider resolution first (applies transforms,\n                # visibility, auth).  Fall back to the global key registry\n                # so that FastMCPApp CallTool references survive namespace\n                # transforms.  Global keys contain a UUID suffix, so they\n                # won't collide with human-written tool names.\n                tool = await self.get_tool(name, version=version)\n                if tool is None:\n                    from fastmcp.server.app import get_global_tool\n\n                    tool = get_global_tool(name)\n                    if tool is not None:\n                        # Auth still applies to global-key tools\n                        skip_auth, token = _get_auth_context()\n                        if not skip_auth and tool.auth is not None:\n                            try:\n                                ctx = AuthContext(token=token, component=tool)\n                                if not await run_auth_checks(tool.auth, ctx):\n                                    raise NotFoundError(f\"Unknown tool: {name!r}\")\n                            except AuthorizationError:\n                                raise NotFoundError(f\"Unknown tool: {name!r}\") from None\n                if tool is None:\n                    raise NotFoundError(f\"Unknown tool: {name!r}\")\n                span.set_attributes(tool.get_span_attributes())\n                if task_meta is not None and task_meta.fn_key is None:\n                    task_meta = replace(task_meta, fn_key=tool.key)\n                try:\n                    return await tool._run(arguments or {}, task_meta=task_meta)\n                except FastMCPError:\n                    logger.exception(f\"Error calling tool {name!r}\")\n                    raise\n                except (ValidationError, PydanticValidationError):\n                    logger.exception(f\"Error validating tool {name!r}\")\n                    raise\n                except Exception as e:\n                    logger.exception(f\"Error calling tool {name!r}\")\n                    # Handle actionable errors that should reach the LLM\n                    # even when masking is enabled\n                    if isinstance(e, httpx.HTTPStatusError):\n                        if e.response.status_code == 429:\n                            raise ToolError(\n                                \"Rate limited by upstream API, please retry later\"\n                            ) from e\n                    if isinstance(e, httpx.TimeoutException):\n                        raise ToolError(\n                            \"Upstream request timed out, please retry\"\n                        ) from e\n                    # Standard masking logic\n                    if self._mask_error_details:\n                        raise ToolError(f\"Error calling tool {name!r}\") from e\n                    raise ToolError(f\"Error calling tool {name!r}: {e}\") from e\n\n    @overload\n    async def read_resource(\n        self,\n        uri: str,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: None = None,\n    ) -> ResourceResult: ...\n\n    @overload\n    async def read_resource(\n        self,\n        uri: str,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def read_resource(\n        self,\n        uri: str,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: TaskMeta | None = None,\n    ) -> ResourceResult | mcp.types.CreateTaskResult:\n        \"\"\"Read a resource by URI.\n\n        This is the public API for reading resources. By default, middleware is applied.\n        Checks concrete resources first, then templates.\n\n        Args:\n            uri: The resource URI\n            version: Specific version to read. If None, reads highest version.\n            run_middleware: If True (default), apply the middleware chain.\n                Set to False when called from middleware to avoid re-applying.\n            task_meta: If provided, execute as a background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return ResourceResult.\n\n        Returns:\n            ResourceResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Raises:\n            NotFoundError: If resource not found or disabled\n            ResourceError: If resource read fails\n        \"\"\"\n        # Note: fn_key enrichment happens here after finding the resource/template.\n        # Resources and templates use different key formats:\n        # - Resources use resource.key (derived from the concrete URI)\n        # - Templates use template.key (the template pattern)\n        # For mounted servers, the parent's provider sets fn_key to the\n        # namespaced key before delegating, ensuring correct Docket routing.\n\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                uri_param = AnyUrl(uri)\n                mw_context = MiddlewareContext(\n                    message=mcp.types.ReadResourceRequestParams(uri=uri_param),\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"resources/read\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.read_resource(\n                        str(context.message.uri),\n                        version=version,\n                        run_middleware=False,\n                        task_meta=task_meta,\n                    ),\n                )\n\n            # Core logic: find and read resource (providers queried in parallel)\n            with server_span(\n                f\"resources/read {uri}\",\n                \"resources/read\",\n                self.name,\n                \"resource\",\n                uri,\n                resource_uri=uri,\n            ) as span:\n                # Try concrete resources first (transforms + auth via _get_resource)\n                resource = await self.get_resource(uri, version=version)\n                if resource is not None:\n                    span.set_attributes(resource.get_span_attributes())\n                    if task_meta is not None and task_meta.fn_key is None:\n                        task_meta = replace(task_meta, fn_key=resource.key)\n                    try:\n                        return await resource._read(task_meta=task_meta)\n                    except (FastMCPError, McpError):\n                        logger.exception(f\"Error reading resource {uri!r}\")\n                        raise\n                    except Exception as e:\n                        logger.exception(f\"Error reading resource {uri!r}\")\n                        # Handle actionable errors that should reach the LLM\n                        if isinstance(e, httpx.HTTPStatusError):\n                            if e.response.status_code == 429:\n                                raise ResourceError(\n                                    \"Rate limited by upstream API, please retry later\"\n                                ) from e\n                        if isinstance(e, httpx.TimeoutException):\n                            raise ResourceError(\n                                \"Upstream request timed out, please retry\"\n                            ) from e\n                        # Standard masking logic\n                        if self._mask_error_details:\n                            raise ResourceError(\n                                f\"Error reading resource {uri!r}\"\n                            ) from e\n                        raise ResourceError(\n                            f\"Error reading resource {uri!r}: {e}\"\n                        ) from e\n\n                # Try templates (transforms + auth via get_resource_template)\n                template = await self.get_resource_template(uri, version=version)\n                if template is None:\n                    if version is None:\n                        raise NotFoundError(f\"Unknown resource: {uri!r}\")\n                    raise NotFoundError(\n                        f\"Unknown resource: {uri!r} version {version!r}\"\n                    )\n                span.set_attributes(template.get_span_attributes())\n                params = template.matches(uri)\n                assert params is not None\n                if task_meta is not None and task_meta.fn_key is None:\n                    task_meta = replace(task_meta, fn_key=template.key)\n                try:\n                    return await template._read(uri, params, task_meta=task_meta)\n                except (FastMCPError, McpError):\n                    logger.exception(f\"Error reading resource {uri!r}\")\n                    raise\n                except Exception as e:\n                    logger.exception(f\"Error reading resource {uri!r}\")\n                    # Handle actionable errors that should reach the LLM\n                    if isinstance(e, httpx.HTTPStatusError):\n                        if e.response.status_code == 429:\n                            raise ResourceError(\n                                \"Rate limited by upstream API, please retry later\"\n                            ) from e\n                    if isinstance(e, httpx.TimeoutException):\n                        raise ResourceError(\n                            \"Upstream request timed out, please retry\"\n                        ) from e\n                    # Standard masking logic\n                    if self._mask_error_details:\n                        raise ResourceError(f\"Error reading resource {uri!r}\") from e\n                    raise ResourceError(f\"Error reading resource {uri!r}: {e}\") from e\n\n    @overload\n    async def render_prompt(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: None = None,\n    ) -> PromptResult: ...\n\n    @overload\n    async def render_prompt(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def render_prompt(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n        task_meta: TaskMeta | None = None,\n    ) -> PromptResult | mcp.types.CreateTaskResult:\n        \"\"\"Render a prompt by name.\n\n        This is the public API for rendering prompts. By default, middleware is applied.\n        Use get_prompt() to retrieve the prompt definition without rendering.\n\n        Args:\n            name: The prompt name\n            arguments: Prompt arguments (optional)\n            version: Specific version to render. If None, renders highest version.\n            run_middleware: If True (default), apply the middleware chain.\n                Set to False when called from middleware to avoid re-applying.\n            task_meta: If provided, execute as a background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return PromptResult.\n\n        Returns:\n            PromptResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Raises:\n            NotFoundError: If prompt not found or disabled\n            PromptError: If prompt rendering fails\n        \"\"\"\n        async with fastmcp.server.context.Context(fastmcp=self) as ctx:\n            if run_middleware:\n                mw_context = MiddlewareContext(\n                    message=mcp.types.GetPromptRequestParams(\n                        name=name, arguments=arguments\n                    ),\n                    source=\"client\",\n                    type=\"request\",\n                    method=\"prompts/get\",\n                    fastmcp_context=ctx,\n                )\n                return await self._run_middleware(\n                    context=mw_context,\n                    call_next=lambda context: self.render_prompt(\n                        context.message.name,\n                        context.message.arguments,\n                        version=version,\n                        run_middleware=False,\n                        task_meta=task_meta,\n                    ),\n                )\n\n            # Core logic: find and render prompt (providers queried in parallel)\n            # Use get_prompt to apply transforms and filter disabled\n            with server_span(\n                f\"prompts/get {name}\", \"prompts/get\", self.name, \"prompt\", name\n            ) as span:\n                prompt = await self.get_prompt(name, version=version)\n                if prompt is None:\n                    raise NotFoundError(f\"Unknown prompt: {name!r}\")\n                span.set_attributes(prompt.get_span_attributes())\n                if task_meta is not None and task_meta.fn_key is None:\n                    task_meta = replace(task_meta, fn_key=prompt.key)\n                try:\n                    return await prompt._render(arguments, task_meta=task_meta)\n                except (FastMCPError, McpError):\n                    logger.exception(f\"Error rendering prompt {name!r}\")\n                    raise\n                except Exception as e:\n                    logger.exception(f\"Error rendering prompt {name!r}\")\n                    if self._mask_error_details:\n                        raise PromptError(f\"Error rendering prompt {name!r}\") from e\n                    raise PromptError(f\"Error rendering prompt {name!r}: {e}\") from e\n\n    def add_tool(self, tool: Tool | Callable[..., Any]) -> Tool:\n        \"\"\"Add a tool to the server.\n\n        The tool function can optionally request a Context object by adding a parameter\n        with the Context type annotation. See the @tool decorator for examples.\n\n        Args:\n            tool: The Tool instance or @tool-decorated function to register\n\n        Returns:\n            The tool instance that was added to the server.\n        \"\"\"\n        return self._local_provider.add_tool(tool)\n\n    def remove_tool(self, name: str, version: str | None = None) -> None:\n        \"\"\"Remove tool(s) from the server.\n\n        .. deprecated::\n            Use ``mcp.local_provider.remove_tool(name)`` instead.\n\n        Args:\n            name: The name of the tool to remove.\n            version: If None, removes ALL versions. If specified, removes only that version.\n\n        Raises:\n            NotFoundError: If no matching tool is found.\n        \"\"\"\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"remove_tool() is deprecated. Use \"\n                \"mcp.local_provider.remove_tool(name) instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        try:\n            self._local_provider.remove_tool(name, version)\n        except KeyError:\n            if version is None:\n                raise NotFoundError(f\"Tool {name!r} not found\") from None\n            raise NotFoundError(\n                f\"Tool {name!r} version {version!r} not found\"\n            ) from None\n\n    @overload\n    def tool(\n        self,\n        name_or_fn: F,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        annotations: ToolAnnotations | dict[str, Any] | None = None,\n        exclude_args: list[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        app: AppConfig | dict[str, Any] | bool | None = None,\n        task: bool | TaskConfig | None = None,\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> F: ...\n\n    @overload\n    def tool(\n        self,\n        name_or_fn: str | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        annotations: ToolAnnotations | dict[str, Any] | None = None,\n        exclude_args: list[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        app: AppConfig | dict[str, Any] | bool | None = None,\n        task: bool | TaskConfig | None = None,\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> Callable[[F], F]: ...\n\n    def tool(\n        self,\n        name_or_fn: str | AnyFunction | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        annotations: ToolAnnotations | dict[str, Any] | None = None,\n        exclude_args: list[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        app: AppConfig | dict[str, Any] | bool | None = None,\n        task: bool | TaskConfig | None = None,\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> (\n        Callable[[AnyFunction], FunctionTool]\n        | FunctionTool\n        | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]\n    ):\n        \"\"\"Decorator to register a tool.\n\n        Tools can optionally request a Context object by adding a parameter with the\n        Context type annotation. The context provides access to MCP capabilities like\n        logging, progress reporting, and resource access.\n\n        This decorator supports multiple calling patterns:\n        - @server.tool (without parentheses)\n        - @server.tool (with empty parentheses)\n        - @server.tool(\"custom_name\") (with name as first argument)\n        - @server.tool(name=\"custom_name\") (with name as keyword argument)\n        - server.tool(function, name=\"custom_name\") (direct function call)\n\n        Args:\n            name_or_fn: Either a function (when used as @tool), a string name, or None\n            name: Optional name for the tool (keyword-only, alternative to name_or_fn)\n            description: Optional description of what the tool does\n            tags: Optional set of tags for categorizing the tool\n            output_schema: Optional JSON schema for the tool's output\n            annotations: Optional annotations about the tool's behavior\n            exclude_args: Optional list of argument names to exclude from the tool schema.\n                Deprecated: Use `Depends()` for dependency injection instead.\n            meta: Optional meta information about the tool\n\n        Examples:\n            Register a tool with a custom name:\n            ```python\n            @server.tool\n            def my_tool(x: int) -> str:\n                return str(x)\n\n            # Register a tool with a custom name\n            @server.tool\n            def my_tool(x: int) -> str:\n                return str(x)\n\n            @server.tool(\"custom_name\")\n            def my_tool(x: int) -> str:\n                return str(x)\n\n            @server.tool(name=\"custom_name\")\n            def my_tool(x: int) -> str:\n                return str(x)\n\n            # Direct function call\n            server.tool(my_function, name=\"custom_name\")\n            ```\n        \"\"\"\n        # Merge app config into meta[\"ui\"] (wire format) before passing to provider\n        if app is not None and app is not False:\n            meta = dict(meta) if meta else {}\n            if app is True:\n                meta[\"ui\"] = True\n            else:\n                meta[\"ui\"] = app_config_to_meta_dict(app)\n\n        # Delegate to LocalProvider with server-level defaults\n        result = self._local_provider.tool(\n            name_or_fn,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            output_schema=output_schema,\n            annotations=annotations,\n            exclude_args=exclude_args,\n            meta=meta,\n            task=task if task is not None else self._support_tasks_by_default,\n            timeout=timeout,\n            auth=auth,\n        )\n\n        return result\n\n    def add_resource(\n        self, resource: Resource | Callable[..., Any]\n    ) -> Resource | ResourceTemplate:\n        \"\"\"Add a resource to the server.\n\n        Args:\n            resource: A Resource instance or @resource-decorated function to add\n\n        Returns:\n            The resource instance that was added to the server.\n        \"\"\"\n        return self._local_provider.add_resource(resource)\n\n    def add_template(self, template: ResourceTemplate) -> ResourceTemplate:\n        \"\"\"Add a resource template to the server.\n\n        Args:\n            template: A ResourceTemplate instance to add\n\n        Returns:\n            The template instance that was added to the server.\n        \"\"\"\n        return self._local_provider.add_template(template)\n\n    def resource(\n        self,\n        uri: str,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        mime_type: str | None = None,\n        tags: set[str] | None = None,\n        annotations: Annotations | dict[str, Any] | None = None,\n        meta: dict[str, Any] | None = None,\n        app: AppConfig | dict[str, Any] | bool | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> Callable[[F], F]:\n        \"\"\"Decorator to register a function as a resource.\n\n        The function will be called when the resource is read to generate its content.\n        The function can return:\n        - str for text content\n        - bytes for binary content\n        - other types will be converted to JSON\n\n        Resources can optionally request a Context object by adding a parameter with the\n        Context type annotation. The context provides access to MCP capabilities like\n        logging, progress reporting, and session information.\n\n        If the URI contains parameters (e.g. \"resource://{param}\") or the function\n        has parameters, it will be registered as a template resource.\n\n        Args:\n            uri: URI for the resource (e.g. \"resource://my-resource\" or \"resource://{param}\")\n            name: Optional name for the resource\n            description: Optional description of the resource\n            mime_type: Optional MIME type for the resource\n            tags: Optional set of tags for categorizing the resource\n            annotations: Optional annotations about the resource's behavior\n            meta: Optional meta information about the resource\n\n        Examples:\n            Register a resource with a custom name:\n            ```python\n            @server.resource(\"resource://my-resource\")\n            def get_data() -> str:\n                return \"Hello, world!\"\n\n            @server.resource(\"resource://my-resource\")\n            async get_data() -> str:\n                data = await fetch_data()\n                return f\"Hello, world! {data}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            def get_weather(city: str) -> str:\n                return f\"Weather for {city}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            async def get_weather_with_context(city: str, ctx: Context) -> str:\n                await ctx.info(f\"Fetching weather for {city}\")\n                return f\"Weather for {city}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            async def get_weather(city: str) -> str:\n                data = await fetch_weather(city)\n                return f\"Weather for {city}: {data}\"\n            ```\n        \"\"\"\n        # Catch incorrect decorator usage early (before any processing)\n        if not isinstance(uri, str):\n            raise TypeError(\n                \"The @resource decorator was used incorrectly. \"\n                \"It requires a URI as the first argument. \"\n                \"Use @resource('uri') instead of @resource\"\n            )\n\n        # Apply default MIME type for ui:// scheme resources\n        mime_type = resolve_ui_mime_type(uri, mime_type)\n\n        # Validate app config for resources — resource_uri and visibility\n        # don't apply since the resource itself is the UI\n        if isinstance(app, AppConfig):\n            if app.resource_uri is not None:\n                raise ValueError(\n                    \"resource_uri cannot be set on resources — \"\n                    \"the resource itself is the UI. \"\n                    \"Use resource_uri on tools to point to a UI resource.\"\n                )\n            if app.visibility is not None:\n                raise ValueError(\n                    \"visibility cannot be set on resources — it only applies to tools.\"\n                )\n\n        # Merge app config into meta[\"ui\"] (wire format) before passing to provider\n        if app is not None and app is not False:\n            meta = dict(meta) if meta else {}\n            if app is True:\n                meta[\"ui\"] = True\n            else:\n                meta[\"ui\"] = app_config_to_meta_dict(app)\n\n        # Delegate to LocalProvider with server-level defaults\n        inner_decorator = self._local_provider.resource(\n            uri,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            mime_type=mime_type,\n            tags=tags,\n            annotations=annotations,\n            meta=meta,\n            task=task if task is not None else self._support_tasks_by_default,\n            auth=auth,\n        )\n\n        return inner_decorator\n\n    def add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt:\n        \"\"\"Add a prompt to the server.\n\n        Args:\n            prompt: A Prompt instance or @prompt-decorated function to add\n\n        Returns:\n            The prompt instance that was added to the server.\n        \"\"\"\n        return self._local_provider.add_prompt(prompt)\n\n    @overload\n    def prompt(\n        self,\n        name_or_fn: F,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> F: ...\n\n    @overload\n    def prompt(\n        self,\n        name_or_fn: str | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> Callable[[F], F]: ...\n\n    def prompt(\n        self,\n        name_or_fn: str | AnyFunction | None = None,\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[mcp.types.Icon] | None = None,\n        tags: set[str] | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> (\n        Callable[[AnyFunction], FunctionPrompt]\n        | FunctionPrompt\n        | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]\n    ):\n        \"\"\"Decorator to register a prompt.\n\n        Prompts can optionally request a Context object by adding a parameter with the\n        Context type annotation. The context provides access to MCP capabilities like\n        logging, progress reporting, and session information.\n\n        This decorator supports multiple calling patterns:\n        - @server.prompt (without parentheses)\n        - @server.prompt() (with empty parentheses)\n        - @server.prompt(\"custom_name\") (with name as first argument)\n        - @server.prompt(name=\"custom_name\") (with name as keyword argument)\n        - server.prompt(function, name=\"custom_name\") (direct function call)\n\n        Args:\n            name_or_fn: Either a function (when used as @prompt), a string name, or None\n            name: Optional name for the prompt (keyword-only, alternative to name_or_fn)\n            description: Optional description of what the prompt does\n            tags: Optional set of tags for categorizing the prompt\n            meta: Optional meta information about the prompt\n\n        Examples:\n\n            ```python\n            @server.prompt\n            def analyze_table(table_name: str) -> list[Message]:\n                schema = read_table_schema(table_name)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": f\"Analyze this schema:\\n{schema}\"\n                    }\n                ]\n\n            @server.prompt()\n            async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:\n                await ctx.info(f\"Analyzing table {table_name}\")\n                schema = read_table_schema(table_name)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": f\"Analyze this schema:\\n{schema}\"\n                    }\n                ]\n\n            @server.prompt(\"custom_name\")\n            async def analyze_file(path: str) -> list[Message]:\n                content = await read_file(path)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": {\n                            \"type\": \"resource\",\n                            \"resource\": {\n                                \"uri\": f\"file://{path}\",\n                                \"text\": content\n                            }\n                        }\n                    }\n                ]\n\n            @server.prompt(name=\"custom_name\")\n            def another_prompt(data: str) -> list[Message]:\n                return [{\"role\": \"user\", \"content\": data}]\n\n            # Direct function call\n            server.prompt(my_function, name=\"custom_name\")\n            ```\n        \"\"\"\n        # Delegate to LocalProvider with server-level defaults\n        return self._local_provider.prompt(\n            name_or_fn,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            meta=meta,\n            task=task if task is not None else self._support_tasks_by_default,\n            auth=auth,\n        )\n\n    def mount(\n        self,\n        server: FastMCP[LifespanResultT],\n        namespace: str | None = None,\n        as_proxy: bool | None = None,\n        tool_names: dict[str, str] | None = None,\n        prefix: str | None = None,  # deprecated, use namespace\n    ) -> None:\n        \"\"\"Mount another FastMCP server on this server with an optional namespace.\n\n        Unlike importing (with import_server), mounting establishes a dynamic connection\n        between servers. When a client interacts with a mounted server's objects through\n        the parent server, requests are forwarded to the mounted server in real-time.\n        This means changes to the mounted server are immediately reflected when accessed\n        through the parent.\n\n        When a server is mounted with a namespace:\n        - Tools from the mounted server are accessible with namespaced names.\n          Example: If server has a tool named \"get_weather\", it will be available as \"namespace_get_weather\".\n        - Resources are accessible with namespaced URIs.\n          Example: If server has a resource with URI \"weather://forecast\", it will be available as\n          \"weather://namespace/forecast\".\n        - Templates are accessible with namespaced URI templates.\n          Example: If server has a template with URI \"weather://location/{id}\", it will be available\n          as \"weather://namespace/location/{id}\".\n        - Prompts are accessible with namespaced names.\n          Example: If server has a prompt named \"weather_prompt\", it will be available as\n          \"namespace_weather_prompt\".\n\n        When a server is mounted without a namespace (namespace=None), its tools, resources, templates,\n        and prompts are accessible with their original names. Multiple servers can be mounted\n        without namespaces, and they will be tried in order until a match is found.\n\n        The mounted server's lifespan is executed when the parent server starts, and its\n        middleware chain is invoked for all operations (tool calls, resource reads, prompts).\n\n        Args:\n            server: The FastMCP server to mount.\n            namespace: Optional namespace to use for the mounted server's objects. If None,\n                the server's objects are accessible with their original names.\n            as_proxy: Deprecated. Mounted servers now always have their lifespan and\n                middleware invoked. To create a proxy server, use create_proxy()\n                explicitly before mounting.\n            tool_names: Optional mapping of original tool names to custom names. Use this\n                to override namespaced names. Keys are the original tool names from the\n                mounted server.\n            prefix: Deprecated. Use namespace instead.\n        \"\"\"\n        import warnings\n\n        from fastmcp.server.providers.fastmcp_provider import FastMCPProvider\n\n        # Handle deprecated prefix parameter\n        if prefix is not None:\n            warnings.warn(\n                \"The 'prefix' parameter is deprecated, use 'namespace' instead\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            if namespace is None:\n                namespace = prefix\n            else:\n                raise ValueError(\"Cannot specify both 'prefix' and 'namespace'\")\n\n        if as_proxy is not None:\n            warnings.warn(\n                \"as_proxy is deprecated and will be removed in a future version. \"\n                \"Mounted servers now always have their lifespan and middleware invoked. \"\n                \"To create a proxy server, use create_proxy() explicitly.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            # Still honor the flag for backward compatibility\n            if as_proxy:\n                from fastmcp.server.providers.proxy import FastMCPProxy\n\n                if not isinstance(server, FastMCPProxy):\n                    server = FastMCP.as_proxy(server)\n\n        # Create provider and add it with namespace\n        provider: Provider = FastMCPProvider(server)\n\n        # Apply tool renames first (scoped to this provider), then namespace\n        # So foo → bar with namespace=\"baz\" becomes baz_bar\n        if tool_names:\n            transforms = {\n                old_name: ToolTransformConfig(name=new_name)\n                for old_name, new_name in tool_names.items()\n            }\n            provider = provider.wrap_transform(ToolTransform(transforms))\n\n        # Use add_provider with namespace (applies namespace in AggregateProvider)\n        self.add_provider(provider, namespace=namespace or \"\")\n\n    async def import_server(\n        self,\n        server: FastMCP[LifespanResultT],\n        prefix: str | None = None,\n    ) -> None:\n        \"\"\"\n        Import the MCP objects from another FastMCP server into this one,\n        optionally with a given prefix.\n\n        .. deprecated::\n            Use :meth:`mount` instead. ``import_server`` will be removed in a\n            future version.\n\n        Note that when a server is *imported*, its objects are immediately\n        registered to the importing server. This is a one-time operation and\n        future changes to the imported server will not be reflected in the\n        importing server. Server-level configurations and lifespans are not imported.\n\n        When a server is imported with a prefix:\n        - The tools are imported with prefixed names\n          Example: If server has a tool named \"get_weather\", it will be\n          available as \"prefix_get_weather\"\n        - The resources are imported with prefixed URIs using the new format\n          Example: If server has a resource with URI \"weather://forecast\", it will\n          be available as \"weather://prefix/forecast\"\n        - The templates are imported with prefixed URI templates using the new format\n          Example: If server has a template with URI \"weather://location/{id}\", it will\n          be available as \"weather://prefix/location/{id}\"\n        - The prompts are imported with prefixed names\n          Example: If server has a prompt named \"weather_prompt\", it will be available as\n          \"prefix_weather_prompt\"\n\n        When a server is imported without a prefix (prefix=None), its tools, resources,\n        templates, and prompts are imported with their original names.\n\n        Args:\n            server: The FastMCP server to import\n            prefix: Optional prefix to use for the imported server's objects. If None,\n                objects are imported with their original names.\n        \"\"\"\n        import warnings\n\n        warnings.warn(\n            \"import_server is deprecated, use mount() instead\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n\n        def add_resource_prefix(uri: str, prefix: str) -> str:\n            \"\"\"Add prefix to resource URI: protocol://path → protocol://prefix/path.\"\"\"\n            match = URI_PATTERN.match(uri)\n            if match:\n                protocol, path = match.groups()\n                return f\"{protocol}{prefix}/{path}\"\n            return uri\n\n        # Import tools from the server\n        for tool in await server.list_tools():\n            if prefix:\n                tool = tool.model_copy(update={\"name\": f\"{prefix}_{tool.name}\"})\n            self.add_tool(tool)\n\n        # Import resources and templates from the server\n        for resource in await server.list_resources():\n            if prefix:\n                new_uri = add_resource_prefix(str(resource.uri), prefix)\n                resource = resource.model_copy(update={\"uri\": new_uri})\n            self.add_resource(resource)\n\n        for template in await server.list_resource_templates():\n            if prefix:\n                new_uri_template = add_resource_prefix(template.uri_template, prefix)\n                template = template.model_copy(\n                    update={\"uri_template\": new_uri_template}\n                )\n            self.add_template(template)\n\n        # Import prompts from the server\n        for prompt in await server.list_prompts():\n            if prefix:\n                prompt = prompt.model_copy(update={\"name\": f\"{prefix}_{prompt.name}\"})\n            self.add_prompt(prompt)\n\n        if server._lifespan != default_lifespan:\n            from warnings import warn\n\n            warn(\n                message=\"When importing from a server with a lifespan, the lifespan from the imported server will not be used.\",\n                category=RuntimeWarning,\n                stacklevel=2,\n            )\n\n        if prefix:\n            logger.debug(\n                f\"[{self.name}] Imported server {server.name} with prefix '{prefix}'\"\n            )\n        else:\n            logger.debug(f\"[{self.name}] Imported server {server.name}\")\n\n    @classmethod\n    def from_openapi(\n        cls,\n        openapi_spec: dict[str, Any],\n        client: httpx.AsyncClient | None = None,\n        name: str = \"OpenAPI Server\",\n        route_maps: list[RouteMap] | None = None,\n        route_map_fn: OpenAPIRouteMapFn | None = None,\n        mcp_component_fn: OpenAPIComponentFn | None = None,\n        mcp_names: dict[str, str] | None = None,\n        tags: set[str] | None = None,\n        validate_output: bool = True,\n        **settings: Any,\n    ) -> Self:\n        \"\"\"\n        Create a FastMCP server from an OpenAPI specification.\n\n        Args:\n            openapi_spec: OpenAPI schema as a dictionary\n            client: Optional httpx AsyncClient for making HTTP requests.\n                If not provided, a default client is created using the first\n                server URL from the OpenAPI spec with a 30-second timeout.\n            name: Name for the MCP server\n            route_maps: Optional list of RouteMap objects defining route mappings\n            route_map_fn: Optional callable for advanced route type mapping\n            mcp_component_fn: Optional callable for component customization\n            mcp_names: Optional dictionary mapping operationId to component names\n            tags: Optional set of tags to add to all components\n            validate_output: If True (default), tools use the output schema\n                extracted from the OpenAPI spec for response validation. If\n                False, a permissive schema is used instead, allowing any\n                response structure while still returning structured JSON.\n            **settings: Additional settings passed to FastMCP\n\n        Returns:\n            A FastMCP server with an OpenAPIProvider attached.\n        \"\"\"\n        from .providers.openapi import OpenAPIProvider\n\n        provider: Provider = OpenAPIProvider(\n            openapi_spec=openapi_spec,\n            client=client,\n            route_maps=route_maps,\n            route_map_fn=route_map_fn,\n            mcp_component_fn=mcp_component_fn,\n            mcp_names=mcp_names,\n            tags=tags,\n            validate_output=validate_output,\n        )\n        return cls(name=name, providers=[provider], **settings)\n\n    @classmethod\n    def from_fastapi(\n        cls,\n        app: Any,\n        name: str | None = None,\n        route_maps: list[RouteMap] | None = None,\n        route_map_fn: OpenAPIRouteMapFn | None = None,\n        mcp_component_fn: OpenAPIComponentFn | None = None,\n        mcp_names: dict[str, str] | None = None,\n        httpx_client_kwargs: dict[str, Any] | None = None,\n        tags: set[str] | None = None,\n        **settings: Any,\n    ) -> Self:\n        \"\"\"\n        Create a FastMCP server from a FastAPI application.\n\n        Args:\n            app: FastAPI application instance\n            name: Name for the MCP server (defaults to app.title)\n            route_maps: Optional list of RouteMap objects defining route mappings\n            route_map_fn: Optional callable for advanced route type mapping\n            mcp_component_fn: Optional callable for component customization\n            mcp_names: Optional dictionary mapping operationId to component names\n            httpx_client_kwargs: Optional kwargs passed to httpx.AsyncClient.\n                Use this to configure timeout and other client settings.\n            tags: Optional set of tags to add to all components\n            **settings: Additional settings passed to FastMCP\n\n        Returns:\n            A FastMCP server with an OpenAPIProvider attached.\n        \"\"\"\n        from .providers.openapi import OpenAPIProvider\n\n        if httpx_client_kwargs is None:\n            httpx_client_kwargs = {}\n        httpx_client_kwargs.setdefault(\"base_url\", \"http://fastapi\")\n\n        client = httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            **httpx_client_kwargs,\n        )\n\n        server_name = name or app.title\n\n        provider: Provider = OpenAPIProvider(\n            openapi_spec=app.openapi(),\n            client=client,\n            route_maps=route_maps,\n            route_map_fn=route_map_fn,\n            mcp_component_fn=mcp_component_fn,\n            mcp_names=mcp_names,\n            tags=tags,\n        )\n        return cls(name=server_name, providers=[provider], **settings)\n\n    @classmethod\n    def as_proxy(\n        cls,\n        backend: (\n            Client[ClientTransportT]\n            | ClientTransport\n            | FastMCP[Any]\n            | FastMCP1Server\n            | AnyUrl\n            | Path\n            | MCPConfig\n            | dict[str, Any]\n            | str\n        ),\n        **settings: Any,\n    ) -> FastMCPProxy:\n        \"\"\"Create a FastMCP proxy server for the given backend.\n\n        .. deprecated::\n            Use :func:`fastmcp.server.create_proxy` instead.\n            This method will be removed in a future version.\n\n        The `backend` argument can be either an existing `fastmcp.client.Client`\n        instance or any value accepted as the `transport` argument of\n        `fastmcp.client.Client`. This mirrors the convenience of the\n        `fastmcp.client.Client` constructor.\n        \"\"\"\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"FastMCP.as_proxy() is deprecated. Use create_proxy() from \"\n                \"fastmcp.server instead: `from fastmcp.server import create_proxy`\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        # Call the module-level create_proxy function directly\n        return create_proxy(backend, **settings)\n\n    @classmethod\n    def generate_name(cls, name: str | None = None) -> str:\n        class_name = cls.__name__\n\n        if name is None:\n            return f\"{class_name}-{secrets.token_hex(2)}\"\n        else:\n            return f\"{class_name}-{name}-{secrets.token_hex(2)}\"\n\n\n# -----------------------------------------------------------------------------\n# Module-level Factory Functions\n# -----------------------------------------------------------------------------\n\n\ndef create_proxy(\n    target: (\n        Client[ClientTransportT]\n        | ClientTransport\n        | FastMCP[Any]\n        | FastMCP1Server\n        | AnyUrl\n        | Path\n        | MCPConfig\n        | dict[str, Any]\n        | str\n    ),\n    **settings: Any,\n) -> FastMCPProxy:\n    \"\"\"Create a FastMCP proxy server for the given target.\n\n    This is the recommended way to create a proxy server. For lower-level control,\n    use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`.\n\n    Args:\n        target: The backend to proxy to. Can be:\n            - A Client instance (connected or disconnected)\n            - A ClientTransport\n            - A FastMCP server instance\n            - A URL string or AnyUrl\n            - A Path to a server script\n            - An MCPConfig or dict\n        **settings: Additional settings passed to FastMCPProxy (name, etc.)\n\n    Returns:\n        A FastMCPProxy server that proxies to the target.\n\n    Example:\n        ```python\n        from fastmcp.server import create_proxy\n\n        # Create a proxy to a remote server\n        proxy = create_proxy(\"http://remote-server/mcp\")\n\n        # Create a proxy to another FastMCP server\n        proxy = create_proxy(other_server)\n        ```\n    \"\"\"\n    from fastmcp.server.providers.proxy import (\n        FastMCPProxy,\n        _create_client_factory,\n    )\n\n    client_factory = _create_client_factory(target)\n    return FastMCPProxy(\n        client_factory=client_factory,\n        **settings,\n    )\n"
  },
  {
    "path": "src/fastmcp/server/tasks/__init__.py",
    "content": "\"\"\"MCP SEP-1686 background tasks support.\n\nThis module implements protocol-level background task execution for MCP servers.\n\"\"\"\n\nfrom fastmcp.server.tasks.capabilities import get_task_capabilities\nfrom fastmcp.server.tasks.config import TaskConfig, TaskMeta, TaskMode\nfrom fastmcp.server.tasks.elicitation import (\n    elicit_for_task,\n    handle_task_input,\n    relay_elicitation,\n)\nfrom fastmcp.server.tasks.keys import (\n    build_task_key,\n    get_client_task_id_from_key,\n    parse_task_key,\n)\nfrom fastmcp.server.tasks.notifications import (\n    ensure_subscriber_running,\n    push_notification,\n    stop_subscriber,\n)\n\n__all__ = [\n    \"TaskConfig\",\n    \"TaskMeta\",\n    \"TaskMode\",\n    \"build_task_key\",\n    \"elicit_for_task\",\n    \"ensure_subscriber_running\",\n    \"get_client_task_id_from_key\",\n    \"get_task_capabilities\",\n    \"handle_task_input\",\n    \"parse_task_key\",\n    \"push_notification\",\n    \"relay_elicitation\",\n    \"stop_subscriber\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/tasks/capabilities.py",
    "content": "\"\"\"SEP-1686 task capabilities declaration.\"\"\"\n\nfrom importlib.util import find_spec\n\nfrom mcp.types import (\n    ServerTasksCapability,\n    ServerTasksRequestsCapability,\n    TasksCallCapability,\n    TasksCancelCapability,\n    TasksListCapability,\n    TasksToolsCapability,\n)\n\n\ndef _is_docket_available() -> bool:\n    \"\"\"Check if pydocket is installed (local to avoid circular import).\"\"\"\n    return find_spec(\"docket\") is not None\n\n\ndef get_task_capabilities() -> ServerTasksCapability | None:\n    \"\"\"Return the SEP-1686 task capabilities.\n\n    Returns task capabilities as a first-class ServerCapabilities field,\n    declaring support for list, cancel, and request operations per SEP-1686.\n\n    Returns None if pydocket is not installed (no task support).\n\n    Note: prompts/resources are passed via extra_data since the SDK types\n    don't include them yet (FastMCP supports them ahead of the spec).\n    \"\"\"\n    if not _is_docket_available():\n        return None\n\n    return ServerTasksCapability(\n        list=TasksListCapability(),\n        cancel=TasksCancelCapability(),\n        requests=ServerTasksRequestsCapability(\n            tools=TasksToolsCapability(call=TasksCallCapability()),\n            prompts={\"get\": {}},  # type: ignore[call-arg]  # extra_data for forward compat\n            resources={\"read\": {}},  # type: ignore[call-arg]  # extra_data for forward compat\n        ),\n    )\n"
  },
  {
    "path": "src/fastmcp/server/tasks/config.py",
    "content": "\"\"\"TaskConfig for MCP SEP-1686 background task execution modes.\n\nThis module defines the configuration for how tools, resources, and prompts\nhandle task-augmented execution as specified in SEP-1686.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport inspect\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom datetime import timedelta\nfrom typing import Any, Literal\n\nfrom fastmcp.utilities.async_utils import is_coroutine_function\n\n# Task execution modes per SEP-1686 / MCP ToolExecution.taskSupport\nTaskMode = Literal[\"forbidden\", \"optional\", \"required\"]\n\n# Default values for task metadata (single source of truth)\nDEFAULT_POLL_INTERVAL = timedelta(seconds=5)  # Default poll interval\nDEFAULT_POLL_INTERVAL_MS = int(DEFAULT_POLL_INTERVAL.total_seconds() * 1000)\nDEFAULT_TTL_MS = 60_000  # Default TTL in milliseconds\n\n\n@dataclass\nclass TaskMeta:\n    \"\"\"Metadata for task-augmented execution requests.\n\n    When passed to call_tool/read_resource/get_prompt, signals that\n    the operation should be submitted as a background task.\n\n    Attributes:\n        ttl: Client-requested TTL in milliseconds. If None, uses server default.\n        fn_key: Docket routing key. Auto-derived from component name if None.\n    \"\"\"\n\n    ttl: int | None = None\n    fn_key: str | None = None\n\n\n@dataclass\nclass TaskConfig:\n    \"\"\"Configuration for MCP background task execution (SEP-1686).\n\n    Controls how a component handles task-augmented requests:\n\n    - \"forbidden\": Component does not support task execution. Clients must not\n      request task augmentation; server returns -32601 if they do.\n    - \"optional\": Component supports both synchronous and task execution.\n      Client may request task augmentation or call normally.\n    - \"required\": Component requires task execution. Clients must request task\n      augmentation; server returns -32601 if they don't.\n\n    Important:\n        Task-enabled components must be available at server startup to be\n        registered with all Docket workers. Components added dynamically after\n        startup will not be registered for background execution.\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.server.tasks import TaskConfig\n\n        mcp = FastMCP(\"MyServer\")\n\n        # Background execution required\n        @mcp.tool(task=TaskConfig(mode=\"required\"))\n        async def long_running_task(): ...\n\n        # Supports both modes (default when task=True)\n        @mcp.tool(task=TaskConfig(mode=\"optional\"))\n        async def flexible_task(): ...\n        ```\n    \"\"\"\n\n    mode: TaskMode = \"optional\"\n    poll_interval: timedelta = DEFAULT_POLL_INTERVAL\n\n    @classmethod\n    def from_bool(cls, value: bool) -> TaskConfig:\n        \"\"\"Convert boolean task flag to TaskConfig.\n\n        Args:\n            value: True for \"optional\" mode, False for \"forbidden\" mode.\n\n        Returns:\n            TaskConfig with appropriate mode.\n        \"\"\"\n        return cls(mode=\"optional\" if value else \"forbidden\")\n\n    def supports_tasks(self) -> bool:\n        \"\"\"Check if this component supports task execution.\n\n        Returns:\n            True if mode is \"optional\" or \"required\", False if \"forbidden\".\n        \"\"\"\n        return self.mode != \"forbidden\"\n\n    def validate_function(self, fn: Callable[..., Any], name: str) -> None:\n        \"\"\"Validate that function is compatible with this task config.\n\n        Task execution requires:\n        1. fastmcp[tasks] to be installed (pydocket)\n        2. Async functions\n\n        Raises ImportError if mode is \"optional\" or \"required\" but pydocket\n        is not installed. Raises ValueError if function is synchronous.\n\n        Args:\n            fn: The function to validate (handles callable classes and staticmethods).\n            name: Name for error messages.\n\n        Raises:\n            ImportError: If task execution is enabled but pydocket not installed.\n            ValueError: If task execution is enabled but function is sync.\n        \"\"\"\n        if not self.supports_tasks():\n            return\n\n        # Check that docket is available for task execution\n        # Lazy import to avoid circular: dependencies.py → http.py → tasks/__init__.py → config.py\n        from fastmcp.server.dependencies import require_docket\n\n        require_docket(f\"`task=True` on function '{name}'\")\n\n        # Unwrap callable classes and staticmethods\n        fn_to_check = fn\n        if (\n            not inspect.isroutine(fn)\n            and not isinstance(fn, functools.partial)\n            and callable(fn)\n        ):\n            fn_to_check = fn.__call__\n        if isinstance(fn_to_check, staticmethod):\n            fn_to_check = fn_to_check.__func__\n\n        if not is_coroutine_function(fn_to_check):\n            raise ValueError(\n                f\"'{name}' uses a sync function but has task execution enabled. \"\n                \"Background tasks require async functions.\"\n            )\n\n        # Note: Context IS now available in background task workers (SEP-1686)\n        # The wiring in _CurrentContext creates a task-aware Context with task_id\n        # and session from the registry. No warning needed.\n"
  },
  {
    "path": "src/fastmcp/server/tasks/elicitation.py",
    "content": "\"\"\"Background task elicitation support (SEP-1686).\n\nThis module provides elicitation capabilities for background tasks running\nin Docket workers. Unlike regular MCP requests, background tasks don't have\nan active request context, so elicitation requires special handling:\n\n1. Set task status to \"input_required\" via Redis\n2. Send notifications/tasks/status with elicitation metadata\n3. Wait for client to send input via tasks/sendInput\n4. Resume task execution with the provided input\n\nThis uses the public MCP SDK APIs where possible, with minimal use of\ninternal APIs for background task coordination.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport mcp.types\nfrom mcp import ServerSession\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\n\n# Redis key patterns for task elicitation state\nELICIT_REQUEST_KEY = \"fastmcp:task:{session_id}:{task_id}:elicit:request\"\nELICIT_RESPONSE_KEY = \"fastmcp:task:{session_id}:{task_id}:elicit:response\"\nELICIT_STATUS_KEY = \"fastmcp:task:{session_id}:{task_id}:elicit:status\"\n\n# TTL for elicitation state (1 hour)\nELICIT_TTL_SECONDS = 3600\n\n\nasync def elicit_for_task(\n    task_id: str,\n    session: ServerSession | None,\n    message: str,\n    schema: dict[str, Any],\n    fastmcp: FastMCP,\n) -> mcp.types.ElicitResult:\n    \"\"\"Send an elicitation request from a background task.\n\n    This function handles the complexity of eliciting user input when running\n    in a Docket worker context where there's no active MCP request.\n\n    Args:\n        task_id: The background task ID\n        session: The MCP ServerSession for this task\n        message: The message to display to the user\n        schema: The JSON schema for the expected response\n        fastmcp: The FastMCP server instance\n\n    Returns:\n        ElicitResult containing the user's response\n\n    Raises:\n        RuntimeError: If Docket is not available\n        McpError: If the elicitation request fails\n    \"\"\"\n    docket = fastmcp._docket\n    if docket is None:\n        raise RuntimeError(\n            \"Background task elicitation requires Docket. \"\n            \"Ensure 'fastmcp[tasks]' is installed and the server has task-enabled components.\"\n        )\n\n    # Generate a unique request ID for this elicitation\n    request_id = str(uuid.uuid4())\n\n    # Get session ID from task context (authoritative source for background tasks)\n    # This is extracted from the Docket execution key: {session_id}:{task_id}:...\n    from fastmcp.server.dependencies import get_task_context\n\n    task_context = get_task_context()\n    if task_context is not None:\n        session_id = task_context.session_id\n    else:\n        # Fallback: try to get from session attribute (shouldn't happen in background)\n        session_id = getattr(session, \"_fastmcp_state_prefix\", None)\n        if session_id is None:\n            raise RuntimeError(\n                \"Cannot determine session_id for elicitation. \"\n                \"This typically means elicit_for_task() was called outside a Docket worker context.\"\n            )\n\n    # Store elicitation request in Redis\n    request_key = ELICIT_REQUEST_KEY.format(session_id=session_id, task_id=task_id)\n    response_key = ELICIT_RESPONSE_KEY.format(session_id=session_id, task_id=task_id)\n    status_key = ELICIT_STATUS_KEY.format(session_id=session_id, task_id=task_id)\n\n    elicit_request = {\n        \"request_id\": request_id,\n        \"message\": message,\n        \"schema\": schema,\n    }\n\n    async with docket.redis() as redis:\n        # Store the elicitation request\n        await redis.set(\n            docket.key(request_key),\n            json.dumps(elicit_request),\n            ex=ELICIT_TTL_SECONDS,\n        )\n        # Set status to \"waiting\"\n        await redis.set(\n            docket.key(status_key),\n            \"waiting\",\n            ex=ELICIT_TTL_SECONDS,\n        )\n\n    # Send task status update notification with input_required status.\n    # Use notifications/tasks/status so typed MCP clients can consume it.\n    #\n    # NOTE: We use the distributed notification queue instead of session.send_notification()\n    # This enables notifications to work when workers run in separate processes\n    # (Azure Web PubSub / Service Bus inspired pattern)\n    timestamp = datetime.now(timezone.utc).isoformat()\n    notification_dict = {\n        \"method\": \"notifications/tasks/status\",\n        \"params\": {\n            \"taskId\": task_id,\n            \"status\": \"input_required\",\n            \"statusMessage\": message,\n            \"createdAt\": timestamp,\n            \"lastUpdatedAt\": timestamp,\n            \"ttl\": ELICIT_TTL_SECONDS * 1000,\n        },\n        \"_meta\": {\n            \"io.modelcontextprotocol/related-task\": {\n                \"taskId\": task_id,\n                \"status\": \"input_required\",\n                \"statusMessage\": message,\n                \"elicitation\": {\n                    \"requestId\": request_id,\n                    \"message\": message,\n                    \"requestedSchema\": schema,\n                },\n            }\n        },\n    }\n\n    # Push notification to Redis queue (works from any process)\n    # Server's subscriber loop will forward to client\n    from fastmcp.server.tasks.notifications import push_notification\n\n    try:\n        await push_notification(session_id, notification_dict, docket)\n    except Exception as e:\n        # Fail fast: if notification can't be queued, client won't know to respond\n        # Return cancel immediately rather than waiting for 1-hour timeout\n        logger.warning(\n            \"Failed to queue input_required notification for task %s, cancelling elicitation: %s\",\n            task_id,\n            e,\n        )\n        # Best-effort cleanup\n        try:\n            async with docket.redis() as redis:\n                await redis.delete(\n                    docket.key(request_key),\n                    docket.key(status_key),\n                )\n        except Exception:\n            pass  # Keys will expire via TTL\n        return mcp.types.ElicitResult(action=\"cancel\", content=None)\n\n    # Wait for response using BLPOP (blocking pop)\n    # This is much more efficient than polling - single Redis round-trip\n    # that blocks until a response is pushed, vs 7,200 round-trips/hour with polling\n    max_wait_seconds = ELICIT_TTL_SECONDS\n\n    try:\n        async with docket.redis() as redis:\n            # BLPOP blocks until an item is pushed to the list or timeout\n            # Returns tuple of (key, value) or None on timeout\n            result = await cast(\n                Any,\n                redis.blpop(\n                    [docket.key(response_key)],\n                    timeout=max_wait_seconds,\n                ),\n            )\n\n            if result:\n                # result is (key, value) tuple\n                _key, response_data = result\n                response = json.loads(response_data)\n\n                # Clean up Redis keys\n                await redis.delete(\n                    docket.key(request_key),\n                    docket.key(status_key),\n                )\n\n                # Convert to ElicitResult\n                return mcp.types.ElicitResult(\n                    action=response.get(\"action\", \"accept\"),\n                    content=response.get(\"content\"),\n                )\n    except Exception as e:\n        logger.warning(\n            \"BLPOP failed for task %s elicitation, falling back to cancel: %s\",\n            task_id,\n            e,\n        )\n\n    # Timeout or error - treat as cancellation\n    # Best-effort cleanup - if Redis is unavailable, keys will expire via TTL\n    try:\n        async with docket.redis() as redis:\n            await redis.delete(\n                docket.key(request_key),\n                docket.key(response_key),\n                docket.key(status_key),\n            )\n    except Exception as cleanup_error:\n        logger.debug(\n            \"Failed to clean up elicitation keys for task %s (will expire via TTL): %s\",\n            task_id,\n            cleanup_error,\n        )\n\n    return mcp.types.ElicitResult(action=\"cancel\", content=None)\n\n\nasync def relay_elicitation(\n    session: ServerSession,\n    session_id: str,\n    task_id: str,\n    elicitation: dict[str, Any],\n    fastmcp: FastMCP,\n) -> None:\n    \"\"\"Relay elicitation from a background task worker to the client.\n\n    Called by the notification subscriber when it detects an input_required\n    notification with elicitation metadata. Sends a standard elicitation/create\n    request to the client session, then uses handle_task_input() to push the\n    response to Redis so the blocked worker can resume.\n\n    Args:\n        session: MCP ServerSession\n        session_id: Session identifier\n        task_id: Background task ID\n        elicitation: Elicitation metadata (message, requestedSchema)\n        fastmcp: FastMCP server instance\n    \"\"\"\n    try:\n        result = await session.elicit(\n            message=elicitation[\"message\"],\n            requestedSchema=elicitation[\"requestedSchema\"],\n        )\n        await handle_task_input(\n            task_id=task_id,\n            session_id=session_id,\n            action=result.action,\n            content=result.content,\n            fastmcp=fastmcp,\n        )\n        logger.debug(\n            \"Relayed elicitation response for task %s (action=%s)\",\n            task_id,\n            result.action,\n        )\n    except Exception as e:\n        logger.warning(\"Failed to relay elicitation for task %s: %s\", task_id, e)\n        # Push a cancel response so the worker's BLPOP doesn't block forever\n        success = await handle_task_input(\n            task_id=task_id,\n            session_id=session_id,\n            action=\"cancel\",\n            content=None,\n            fastmcp=fastmcp,\n        )\n        if not success:\n            logger.warning(\n                \"Failed to push cancel response for task %s \"\n                \"(worker may block until TTL)\",\n                task_id,\n            )\n\n\nasync def handle_task_input(\n    task_id: str,\n    session_id: str,\n    action: str,\n    content: dict[str, Any] | None,\n    fastmcp: FastMCP,\n) -> bool:\n    \"\"\"Handle input sent to a background task via tasks/sendInput.\n\n    This is called when a client sends input in response to an elicitation\n    request from a background task.\n\n    Args:\n        task_id: The background task ID\n        session_id: The MCP session ID\n        action: The elicitation action (\"accept\", \"decline\", \"cancel\")\n        content: The response content (for \"accept\" action)\n        fastmcp: The FastMCP server instance\n\n    Returns:\n        True if the input was successfully stored, False otherwise\n    \"\"\"\n    docket = fastmcp._docket\n    if docket is None:\n        return False\n\n    response_key = ELICIT_RESPONSE_KEY.format(session_id=session_id, task_id=task_id)\n    status_key = ELICIT_STATUS_KEY.format(session_id=session_id, task_id=task_id)\n\n    response = {\n        \"action\": action,\n        \"content\": content,\n    }\n\n    async with docket.redis() as redis:\n        # Check if there's a pending elicitation\n        status = await redis.get(docket.key(status_key))\n        if status is None or status.decode(\"utf-8\") != \"waiting\":\n            return False\n\n        # Push response to list - this wakes up the BLPOP in elicit_for_task\n        # Using LPUSH instead of SET enables the efficient blocking wait pattern\n        await redis.lpush(  # type: ignore[invalid-await]  # redis-py union type (sync/async)\n            docket.key(response_key),\n            json.dumps(response),\n        )\n        # Set TTL on the response list (in case BLPOP doesn't consume it)\n        await redis.expire(docket.key(response_key), ELICIT_TTL_SECONDS)\n\n        # Update status to \"responded\"\n        await redis.set(\n            docket.key(status_key),\n            \"responded\",\n            ex=ELICIT_TTL_SECONDS,\n        )\n\n    return True\n"
  },
  {
    "path": "src/fastmcp/server/tasks/handlers.py",
    "content": "\"\"\"SEP-1686 task execution handlers.\n\nHandles queuing tool/prompt/resource executions to Docket as background tasks.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport uuid\nfrom contextlib import suppress\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Any, Literal\n\nimport mcp.types\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import INTERNAL_ERROR, ErrorData\n\nfrom fastmcp.server.dependencies import _current_docket, get_access_token, get_context\nfrom fastmcp.server.tasks.config import TaskMeta\nfrom fastmcp.server.tasks.keys import build_task_key\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.tools.base import Tool\n\nlogger = get_logger(__name__)\n\n# Redis mapping TTL buffer: Add 15 minutes to Docket's execution_ttl\nTASK_MAPPING_TTL_BUFFER_SECONDS = 15 * 60\n\n\nasync def submit_to_docket(\n    task_type: Literal[\"tool\", \"resource\", \"template\", \"prompt\"],\n    key: str,\n    component: Tool | Resource | ResourceTemplate | Prompt,\n    arguments: dict[str, Any] | None = None,\n    task_meta: TaskMeta | None = None,\n) -> mcp.types.CreateTaskResult:\n    \"\"\"Submit any component to Docket for background execution (SEP-1686).\n\n    Unified handler for all component types. Called by component's internal\n    methods (_run, _read, _render) when task metadata is present and mode allows.\n\n    Queues the component's method to Docket, stores raw return values,\n    and converts to MCP types on retrieval.\n\n    Args:\n        task_type: Component type for task key construction\n        key: The component key as seen by MCP layer (with namespace prefix)\n        component: The component instance (Tool, Resource, ResourceTemplate, Prompt)\n        arguments: Arguments/params (None for Resource which has no args)\n        task_meta: Task execution metadata. If task_meta.ttl is provided, it\n            overrides the server default (docket.execution_ttl).\n\n    Returns:\n        CreateTaskResult: Task stub with proper Task object\n    \"\"\"\n    # Generate server-side task ID per SEP-1686 final spec (line 375-377)\n    # Server MUST generate task IDs, clients no longer provide them\n    server_task_id = str(uuid.uuid4())\n\n    # Record creation timestamp per SEP-1686 final spec (line 430)\n    created_at = datetime.now(timezone.utc)\n\n    # Get session ID - use \"internal\" for programmatic calls without MCP session\n    ctx = get_context()\n    try:\n        session_id = ctx.session_id\n    except RuntimeError:\n        session_id = \"internal\"\n\n    docket = _current_docket.get()\n    if docket is None:\n        raise McpError(\n            ErrorData(\n                code=INTERNAL_ERROR,\n                message=\"Background tasks require a running FastMCP server context\",\n            )\n        )\n\n    # Build full task key with embedded metadata\n    task_key = build_task_key(session_id, server_task_id, task_type, key)\n\n    # Determine TTL: use task_meta.ttl if provided, else docket default\n    if task_meta is not None and task_meta.ttl is not None:\n        ttl_ms = task_meta.ttl\n    else:\n        ttl_ms = int(docket.execution_ttl.total_seconds() * 1000)\n    ttl_seconds = int(ttl_ms / 1000) + TASK_MAPPING_TTL_BUFFER_SECONDS\n\n    # Store task metadata in Redis for protocol handlers\n    task_meta_key = docket.key(f\"fastmcp:task:{session_id}:{server_task_id}\")\n    created_at_key = docket.key(\n        f\"fastmcp:task:{session_id}:{server_task_id}:created_at\"\n    )\n    poll_interval_key = docket.key(\n        f\"fastmcp:task:{session_id}:{server_task_id}:poll_interval\"\n    )\n    origin_request_id_key = docket.key(\n        f\"fastmcp:task:{session_id}:{server_task_id}:origin_request_id\"\n    )\n    poll_interval_ms = int(component.task_config.poll_interval.total_seconds() * 1000)\n    origin_request_id = (\n        str(ctx.request_context.request_id) if ctx.request_context is not None else None\n    )\n\n    # Snapshot the current access token (if any) for background task access (#3095)\n    access_token = get_access_token()\n    access_token_key = docket.key(\n        f\"fastmcp:task:{session_id}:{server_task_id}:access_token\"\n    )\n\n    async with docket.redis() as redis:\n        await redis.set(task_meta_key, task_key, ex=ttl_seconds)\n        await redis.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)\n        await redis.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)\n        if origin_request_id is not None:\n            await redis.set(origin_request_id_key, origin_request_id, ex=ttl_seconds)\n        if access_token is not None:\n            await redis.set(\n                access_token_key, access_token.model_dump_json(), ex=ttl_seconds\n            )\n\n    # Register session for Context access in background workers (SEP-1686)\n    # This enables elicitation/sampling from background tasks via weakref\n    # Skip for \"internal\" sessions (programmatic calls without MCP session)\n    if session_id != \"internal\":\n        from fastmcp.server.dependencies import register_task_session\n\n        register_task_session(session_id, ctx.session)\n\n    # Send an initial tasks/status notification before queueing.\n    # This guarantees clients can observe task creation immediately.\n    notification = mcp.types.TaskStatusNotification.model_validate(\n        {\n            \"method\": \"notifications/tasks/status\",\n            \"params\": {\n                \"taskId\": server_task_id,\n                \"status\": \"working\",\n                \"statusMessage\": \"Task submitted\",\n                \"createdAt\": created_at,\n                \"lastUpdatedAt\": created_at,\n                \"ttl\": ttl_ms,\n                \"pollInterval\": poll_interval_ms,\n            },\n            \"_meta\": {\n                \"io.modelcontextprotocol/related-task\": {\n                    \"taskId\": server_task_id,\n                }\n            },\n        }\n    )\n    server_notification = mcp.types.ServerNotification(notification)\n    with suppress(Exception):\n        # Don't let notification failures break task creation\n        await ctx.session.send_notification(server_notification)\n\n    # Queue function to Docket by key (result storage via execution_ttl)\n    # Use component.add_to_docket() which handles calling conventions\n    # `fn_key` is the function lookup key (e.g., \"child_multiply\")\n    # `task_key` is the task result key (e.g., \"fastmcp:task:{session}:{task_id}:tool:child_multiply\")\n    # Resources don't take arguments; tools/prompts/templates always pass arguments (even if None/empty)\n    if task_type == \"resource\":\n        await component.add_to_docket(docket, fn_key=key, task_key=task_key)  # type: ignore[call-arg]\n    else:\n        await component.add_to_docket(docket, arguments, fn_key=key, task_key=task_key)  # type: ignore[call-arg]\n\n    # Spawn subscription task to send status notifications (SEP-1686 optional feature)\n    from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates\n\n    # Start subscription in session's task group (persists for connection lifetime)\n    if hasattr(ctx.session, \"_subscription_task_group\"):\n        tg = ctx.session._subscription_task_group\n        if tg:\n            tg.start_soon(  # type: ignore[union-attr]\n                subscribe_to_task_updates,\n                server_task_id,\n                task_key,\n                ctx.session,\n                docket,\n                poll_interval_ms,\n            )\n\n    # Start notification subscriber for distributed elicitation (idempotent)\n    # This enables ctx.elicit() to work when workers run in separate processes\n    # Subscriber forwards notifications from Redis queue to client session\n    from fastmcp.server.tasks.notifications import (\n        ensure_subscriber_running,\n        stop_subscriber,\n    )\n\n    try:\n        await ensure_subscriber_running(session_id, ctx.session, docket, ctx.fastmcp)\n\n        # Register cleanup callback on session exit (once per session)\n        # This ensures subscriber is stopped when the session disconnects\n        if (\n            hasattr(ctx.session, \"_exit_stack\")\n            and ctx.session._exit_stack is not None\n            and not getattr(ctx.session, \"_notification_cleanup_registered\", False)\n        ):\n\n            async def _cleanup_subscriber() -> None:\n                await stop_subscriber(session_id)\n\n            ctx.session._exit_stack.push_async_callback(_cleanup_subscriber)\n            ctx.session._notification_cleanup_registered = True  # type: ignore[attr-defined]\n    except Exception as e:\n        # Non-fatal: elicitation will still work via polling fallback\n        logger.debug(\"Failed to start notification subscriber: %s\", e)\n\n    # Return CreateTaskResult with proper Task object\n    # Tasks MUST begin in \"working\" status per SEP-1686 final spec (line 381)\n    return mcp.types.CreateTaskResult(\n        task=mcp.types.Task(\n            taskId=server_task_id,\n            status=\"working\",\n            createdAt=created_at,\n            lastUpdatedAt=created_at,\n            ttl=ttl_ms,\n            pollInterval=poll_interval_ms,\n        )\n    )\n"
  },
  {
    "path": "src/fastmcp/server/tasks/keys.py",
    "content": "\"\"\"Task key management for SEP-1686 background tasks.\n\nTask keys encode security scoping and metadata in the Docket key format:\n    `{session_id}:{client_task_id}:{task_type}:{component_identifier}`\n\nThis format provides:\n- Session-based security scoping (prevents cross-session access)\n- Task type identification (tool/prompt/resource)\n- Component identification (name or URI for result conversion)\n\"\"\"\n\nfrom urllib.parse import quote, unquote\n\n\ndef build_task_key(\n    session_id: str,\n    client_task_id: str,\n    task_type: str,\n    component_identifier: str,\n) -> str:\n    \"\"\"Build Docket task key with embedded metadata.\n\n    Format: `{session_id}:{client_task_id}:{task_type}:{component_identifier}`\n\n    The component_identifier is URI-encoded to handle special characters (colons, slashes, etc.).\n\n    Args:\n        session_id: Session ID for security scoping\n        client_task_id: Client-provided task ID\n        task_type: Type of task (\"tool\", \"prompt\", \"resource\")\n        component_identifier: Tool name, prompt name, or resource URI\n\n    Returns:\n        Encoded task key for Docket\n\n    Examples:\n        >>> build_task_key(\"session123\", \"task456\", \"tool\", \"my_tool\")\n        'session123:task456:tool:my_tool'\n\n        >>> build_task_key(\"session123\", \"task456\", \"resource\", \"file://data.txt\")\n        'session123:task456:resource:file%3A%2F%2Fdata.txt'\n    \"\"\"\n    encoded_identifier = quote(component_identifier, safe=\"\")\n    return f\"{session_id}:{client_task_id}:{task_type}:{encoded_identifier}\"\n\n\ndef parse_task_key(task_key: str) -> dict[str, str]:\n    \"\"\"Parse Docket task key to extract metadata.\n\n    Args:\n        task_key: Encoded task key from Docket\n\n    Returns:\n        Dict with keys: session_id, client_task_id, task_type, component_identifier\n\n    Examples:\n        >>> parse_task_key(\"session123:task456:tool:my_tool\")\n        `{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`\n\n        >>> parse_task_key(\"session123:task456:resource:file%3A%2F%2Fdata.txt\")\n        `{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'resource', 'component_identifier': 'file://data.txt'}`\n    \"\"\"\n    parts = task_key.split(\":\", 3)\n    if len(parts) != 4:\n        raise ValueError(\n            f\"Invalid task key format: {task_key}. \"\n            f\"Expected: {{session_id}}:{{client_task_id}}:{{task_type}}:{{component_identifier}}\"\n        )\n\n    return {\n        \"session_id\": parts[0],\n        \"client_task_id\": parts[1],\n        \"task_type\": parts[2],\n        \"component_identifier\": unquote(parts[3]),\n    }\n\n\ndef get_client_task_id_from_key(task_key: str) -> str:\n    \"\"\"Extract just the client task ID from a task key.\n\n    Args:\n        task_key: Full encoded task key\n\n    Returns:\n        Client-provided task ID (second segment)\n\n    Example:\n        >>> get_client_task_id_from_key(\"session123:task456:tool:my_tool\")\n        'task456'\n    \"\"\"\n    return task_key.split(\":\", 3)[1]\n"
  },
  {
    "path": "src/fastmcp/server/tasks/notifications.py",
    "content": "\"\"\"Distributed notification queue for background task events (SEP-1686).\n\nEnables distributed Docket workers to send MCP notifications to clients\nwithout holding session references. Workers push to a Redis queue,\nthe MCP server process subscribes and forwards to the client's session.\n\nPattern: Fire-and-forward with retry\n- One queue per session_id\n- LPUSH/BRPOP for reliable ordered delivery\n- Retry up to 3 times on delivery failure, then discard\n- TTL-based expiration for stale messages\n\nNote: Docket's execution.subscribe() handles task state/progress events via\nRedis Pub/Sub. This module handles elicitation-specific notifications that\nrequire reliable delivery (input_required prompts, cancel signals).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport weakref\nfrom contextlib import suppress\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport mcp.types\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from mcp.server.session import ServerSession\n\n    from fastmcp.server.server import FastMCP\n\nlogger = logging.getLogger(__name__)\n\n# Redis key patterns\nNOTIFICATION_QUEUE_KEY = \"fastmcp:notifications:{session_id}\"\nNOTIFICATION_ACTIVE_KEY = \"fastmcp:notifications:{session_id}:active\"\n\n# Configuration\nNOTIFICATION_TTL_SECONDS = 300  # 5 minute message TTL (elicitation response window)\nMAX_DELIVERY_ATTEMPTS = 3  # Retry failed deliveries before discarding\nSUBSCRIBER_TIMEOUT_SECONDS = 30  # BRPOP timeout (also heartbeat interval)\n\n\nasync def push_notification(\n    session_id: str,\n    notification: dict[str, Any],\n    docket: Docket,\n) -> None:\n    \"\"\"Push notification to session's queue (called from Docket worker).\n\n    Used for elicitation-specific notifications (input_required, cancel)\n    that need reliable delivery across distributed processes.\n\n    Args:\n        session_id: Target session's identifier\n        notification: MCP notification dict (method, params, _meta)\n        docket: Docket instance for Redis access\n    \"\"\"\n    key = docket.key(NOTIFICATION_QUEUE_KEY.format(session_id=session_id))\n    message = json.dumps(\n        {\n            \"notification\": notification,\n            \"attempt\": 0,\n            \"enqueued_at\": datetime.now(timezone.utc).isoformat(),\n        }\n    )\n    async with docket.redis() as redis:\n        await redis.lpush(key, message)  # type: ignore[invalid-await]  # redis-py union type (sync/async)\n        await redis.expire(key, NOTIFICATION_TTL_SECONDS)\n\n\nasync def notification_subscriber_loop(\n    session_id: str,\n    session: ServerSession,\n    docket: Docket,\n    fastmcp: FastMCP,\n) -> None:\n    \"\"\"Subscribe to notification queue and forward to session.\n\n    Runs in the MCP server process. Bridges distributed workers to clients.\n\n    This loop:\n    1. Maintains a heartbeat (active subscriber marker for debugging)\n    2. Blocks on BRPOP waiting for notifications\n    3. Forwards notifications to the client's session\n    4. Retries failed deliveries, then discards (no dead-letter queue)\n\n    Args:\n        session_id: Session identifier to subscribe to\n        session: MCP ServerSession for sending notifications\n        docket: Docket instance for Redis access\n        fastmcp: FastMCP server instance (for elicitation relay)\n    \"\"\"\n    queue_key = docket.key(NOTIFICATION_QUEUE_KEY.format(session_id=session_id))\n    active_key = docket.key(NOTIFICATION_ACTIVE_KEY.format(session_id=session_id))\n\n    logger.debug(\"Starting notification subscriber for session %s\", session_id)\n\n    while True:\n        try:\n            async with docket.redis() as redis:\n                # Heartbeat: mark subscriber as active (for distributed debugging)\n                await redis.set(active_key, \"1\", ex=SUBSCRIBER_TIMEOUT_SECONDS * 2)\n\n                # Blocking wait for notification (timeout refreshes heartbeat)\n                # Using BRPOP (right pop) for FIFO order with LPUSH (left push)\n                result = await cast(\n                    Any, redis.brpop([queue_key], timeout=SUBSCRIBER_TIMEOUT_SECONDS)\n                )\n                if not result:\n                    continue  # Timeout - refresh heartbeat and retry\n\n                _, message_bytes = result\n                message = json.loads(message_bytes)\n                notification_dict = message[\"notification\"]\n                attempt = message.get(\"attempt\", 0)\n\n                try:\n                    # Reconstruct and send MCP notification\n                    await _send_mcp_notification(\n                        session, notification_dict, session_id, docket, fastmcp\n                    )\n                    logger.debug(\n                        \"Delivered notification to session %s (attempt %d)\",\n                        session_id,\n                        attempt + 1,\n                    )\n                except Exception as send_error:\n                    # Delivery failed - retry or discard\n                    if attempt < MAX_DELIVERY_ATTEMPTS - 1:\n                        # Re-queue with incremented attempt (back of queue)\n                        message[\"attempt\"] = attempt + 1\n                        message[\"last_error\"] = str(send_error)\n                        await redis.lpush(queue_key, json.dumps(message))  # type: ignore[invalid-await]\n                        logger.debug(\n                            \"Requeued notification for session %s (attempt %d): %s\",\n                            session_id,\n                            attempt + 2,\n                            send_error,\n                        )\n                    else:\n                        # Discard after max attempts (session likely disconnected)\n                        logger.warning(\n                            \"Discarding notification for session %s after %d attempts: %s\",\n                            session_id,\n                            MAX_DELIVERY_ATTEMPTS,\n                            send_error,\n                        )\n\n        except asyncio.CancelledError:\n            # Graceful shutdown - leave pending messages in queue for reconnect\n            logger.debug(\"Notification subscriber cancelled for session %s\", session_id)\n            break\n        except Exception as e:\n            logger.debug(\n                \"Notification subscriber error for session %s: %s\", session_id, e\n            )\n            await asyncio.sleep(1)  # Backoff on error\n\n\nasync def _send_mcp_notification(\n    session: ServerSession,\n    notification_dict: dict[str, Any],\n    session_id: str,\n    docket: Docket,\n    fastmcp: FastMCP,\n) -> None:\n    \"\"\"Reconstruct MCP notification from dict and send to session.\n\n    For input_required notifications with elicitation metadata, also sends\n    a standard elicitation/create request to the client and relays the\n    response back to the worker via Redis.\n\n    Args:\n        session: MCP ServerSession\n        notification_dict: Notification as dict (method, params, _meta)\n        session_id: Session identifier (for elicitation relay)\n        docket: Docket instance (for notification delivery)\n        fastmcp: FastMCP server instance (for elicitation relay)\n    \"\"\"\n    method = notification_dict.get(\"method\", \"notifications/tasks/status\")\n    if method != \"notifications/tasks/status\":\n        raise ValueError(f\"Unsupported notification method for subscriber: {method}\")\n\n    notification = mcp.types.TaskStatusNotification.model_validate(\n        {\n            \"method\": \"notifications/tasks/status\",\n            \"params\": notification_dict.get(\"params\", {}),\n            \"_meta\": notification_dict.get(\"_meta\"),\n        }\n    )\n    server_notification = mcp.types.ServerNotification(notification)\n\n    await session.send_notification(server_notification)\n\n    # If this is an input_required notification with elicitation metadata,\n    # relay the elicitation to the client via standard elicitation/create\n    params = notification_dict.get(\"params\", {})\n    if params.get(\"status\") == \"input_required\":\n        meta = notification_dict.get(\"_meta\", {})\n        related_task = meta.get(\"io.modelcontextprotocol/related-task\", {})\n        elicitation = related_task.get(\"elicitation\")\n        if elicitation:\n            task_id = params.get(\"taskId\")\n            if not task_id:\n                logger.warning(\n                    \"input_required notification missing taskId, skipping relay\"\n                )\n                return\n            from fastmcp.server.tasks.elicitation import relay_elicitation\n\n            task = asyncio.create_task(\n                relay_elicitation(session, session_id, task_id, elicitation, fastmcp),\n                name=f\"elicitation-relay-{task_id[:8]}\",\n            )\n            _background_tasks.add(task)\n            task.add_done_callback(_background_tasks.discard)\n\n\n# =============================================================================\n# Subscriber Management\n# =============================================================================\n\n# Strong references to fire-and-forget relay tasks (prevent GC mid-flight)\n_background_tasks: set[asyncio.Task[None]] = set()\n\n# Registry of active subscribers per session (prevents duplicates)\n# Uses weakref to session to detect disconnects\n_active_subscribers: dict[\n    str, tuple[asyncio.Task[None], weakref.ref[ServerSession]]\n] = {}\n\n\nasync def ensure_subscriber_running(\n    session_id: str,\n    session: ServerSession,\n    docket: Docket,\n    fastmcp: FastMCP,\n) -> None:\n    \"\"\"Start notification subscriber if not already running (idempotent).\n\n    Subscriber is created on first task submission and cleaned up on disconnect.\n    Safe to call multiple times for the same session.\n\n    Args:\n        session_id: Session identifier\n        session: MCP ServerSession\n        docket: Docket instance\n        fastmcp: FastMCP server instance (for elicitation relay)\n    \"\"\"\n    # Check if subscriber already running for this session\n    if session_id in _active_subscribers:\n        task, session_ref = _active_subscribers[session_id]\n        # Check if task is still running AND session is still alive\n        if not task.done() and session_ref() is not None:\n            return  # Already running\n\n        # Task finished or session dead - clean up\n        if not task.done():\n            task.cancel()\n            with suppress(asyncio.CancelledError):\n                await task\n        del _active_subscribers[session_id]\n\n    # Start new subscriber task\n    task = asyncio.create_task(\n        notification_subscriber_loop(session_id, session, docket, fastmcp),\n        name=f\"notification-subscriber-{session_id[:8]}\",\n    )\n    _active_subscribers[session_id] = (task, weakref.ref(session))\n    logger.debug(\"Started notification subscriber for session %s\", session_id)\n\n\nasync def stop_subscriber(session_id: str) -> None:\n    \"\"\"Stop notification subscriber for a session.\n\n    Called when session disconnects. Pending messages remain in queue\n    for delivery if client reconnects (with TTL expiration).\n\n    Args:\n        session_id: Session identifier\n    \"\"\"\n    if session_id not in _active_subscribers:\n        return\n\n    task, _ = _active_subscribers.pop(session_id)\n    if not task.done():\n        task.cancel()\n        with suppress(asyncio.CancelledError):\n            await task\n    logger.debug(\"Stopped notification subscriber for session %s\", session_id)\n\n\ndef get_subscriber_count() -> int:\n    \"\"\"Get number of active subscribers (for monitoring).\"\"\"\n    return len(_active_subscribers)\n"
  },
  {
    "path": "src/fastmcp/server/tasks/requests.py",
    "content": "\"\"\"SEP-1686 task request handlers.\n\nHandles MCP task protocol requests: tasks/get, tasks/result, tasks/list, tasks/cancel.\nThese handlers query and manage existing tasks (contrast with handlers.py which creates tasks).\n\nThis module requires fastmcp[tasks] (pydocket). It is only imported when docket is available.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timedelta, timezone\nfrom typing import TYPE_CHECKING, Any, Literal\n\nimport mcp.types\nfrom docket.execution import ExecutionState\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import (\n    INTERNAL_ERROR,\n    INVALID_PARAMS,\n    CancelTaskResult,\n    ErrorData,\n    GetTaskResult,\n    ListTasksResult,\n)\n\nimport fastmcp.server.context\nfrom fastmcp.exceptions import NotFoundError\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.tasks.config import DEFAULT_POLL_INTERVAL_MS, DEFAULT_TTL_MS\nfrom fastmcp.server.tasks.keys import parse_task_key\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\n\n# Map Docket execution states to MCP task status strings\n# Per SEP-1686 final spec (line 381): tasks MUST begin in \"working\" status\nDOCKET_TO_MCP_STATE: dict[ExecutionState, str] = {\n    ExecutionState.SCHEDULED: \"working\",  # Initial state per spec\n    ExecutionState.QUEUED: \"working\",  # Initial state per spec\n    ExecutionState.RUNNING: \"working\",\n    ExecutionState.COMPLETED: \"completed\",\n    ExecutionState.FAILED: \"failed\",\n    ExecutionState.CANCELLED: \"cancelled\",\n}\n\n\ndef _parse_key_version(key_suffix: str) -> tuple[str, str | None]:\n    \"\"\"Parse a key suffix into (name_or_uri, version).\n\n    Keys always contain @ as a version delimiter (sentinel pattern):\n    - \"add@1.0\" → (\"add\", \"1.0\")  # versioned\n    - \"add@\" → (\"add\", None)      # unversioned\n    - \"user@example.com@1.0\" → (\"user@example.com\", \"1.0\")  # @ in URI\n\n    Uses rsplit to split on the LAST @ which is always the version delimiter.\n    Falls back to treating the whole string as the name if @ is not present\n    (for backwards compatibility with legacy task keys).\n    \"\"\"\n    if \"@\" not in key_suffix:\n        # Legacy key without version sentinel - treat as unversioned\n        return key_suffix, None\n    name_or_uri, version = key_suffix.rsplit(\"@\", 1)\n    return name_or_uri, version if version else None\n\n\nasync def _lookup_task_execution(\n    docket: Any,\n    session_id: str,\n    client_task_id: str,\n) -> tuple[Any, str | None, int]:\n    \"\"\"Look up task execution and metadata from Redis.\n\n    Consolidates the common pattern of fetching task metadata from Redis,\n    validating it exists, and retrieving the Docket execution.\n\n    Args:\n        docket: Docket instance\n        session_id: Session ID\n        client_task_id: Client-provided task ID\n\n    Returns:\n        Tuple of (execution, created_at, poll_interval_ms)\n\n    Raises:\n        McpError: If task not found or execution not found\n    \"\"\"\n    task_meta_key = docket.key(f\"fastmcp:task:{session_id}:{client_task_id}\")\n    created_at_key = docket.key(\n        f\"fastmcp:task:{session_id}:{client_task_id}:created_at\"\n    )\n    poll_interval_key = docket.key(\n        f\"fastmcp:task:{session_id}:{client_task_id}:poll_interval\"\n    )\n\n    # Fetch metadata (single round-trip with mget)\n    async with docket.redis() as redis:\n        task_key_bytes, created_at_bytes, poll_interval_bytes = await redis.mget(\n            task_meta_key, created_at_key, poll_interval_key\n        )\n\n    # Decode and validate task_key\n    task_key = task_key_bytes.decode(\"utf-8\") if task_key_bytes else None\n    if not task_key:\n        raise McpError(\n            ErrorData(code=INVALID_PARAMS, message=f\"Task {client_task_id} not found\")\n        )\n\n    # Get execution\n    execution = await docket.get_execution(task_key)\n    if not execution:\n        raise McpError(\n            ErrorData(\n                code=INVALID_PARAMS,\n                message=f\"Task {client_task_id} execution not found\",\n            )\n        )\n\n    # Parse metadata with defaults\n    created_at = created_at_bytes.decode(\"utf-8\") if created_at_bytes else None\n    try:\n        poll_interval_ms = (\n            int(poll_interval_bytes.decode(\"utf-8\"))\n            if poll_interval_bytes\n            else DEFAULT_POLL_INTERVAL_MS\n        )\n    except (ValueError, UnicodeDecodeError):\n        poll_interval_ms = DEFAULT_POLL_INTERVAL_MS\n\n    return execution, created_at, poll_interval_ms\n\n\nasync def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskResult:\n    \"\"\"Handle MCP 'tasks/get' request (SEP-1686).\n\n    Args:\n        server: FastMCP server instance\n        params: Request params containing taskId\n\n    Returns:\n        GetTaskResult: Task status response with spec-compliant fields\n    \"\"\"\n    async with fastmcp.server.context.Context(fastmcp=server) as ctx:\n        client_task_id = params.get(\"taskId\")\n        if not client_task_id:\n            raise McpError(\n                ErrorData(\n                    code=INVALID_PARAMS, message=\"Missing required parameter: taskId\"\n                )\n            )\n\n        # Get session ID from Context\n        session_id = ctx.session_id\n\n        # Get Docket instance\n        docket = server._docket\n        if docket is None:\n            raise McpError(\n                ErrorData(\n                    code=INTERNAL_ERROR,\n                    message=\"Background tasks require Docket\",\n                )\n            )\n\n        # Look up task execution and metadata\n        execution, created_at, poll_interval_ms = await _lookup_task_execution(\n            docket, session_id, client_task_id\n        )\n\n        # Sync state from Redis\n        await execution.sync()\n\n        # Map Docket state to MCP state\n        state_map = DOCKET_TO_MCP_STATE\n        mcp_state: Literal[\n            \"working\", \"input_required\", \"completed\", \"failed\", \"cancelled\"\n        ] = state_map.get(execution.state, \"failed\")  # type: ignore[assignment]\n\n        # Build response (use default ttl since we don't track per-task values)\n        # createdAt is REQUIRED per SEP-1686 final spec (line 430)\n        # Per spec lines 447-448: SHOULD NOT include related-task metadata in tasks/get\n        error_message = None\n        status_message = None\n\n        if execution.state == ExecutionState.FAILED:\n            try:\n                await execution.get_result(timeout=timedelta(seconds=0))\n            except Exception as error:\n                error_message = str(error)\n                status_message = f\"Task failed: {error_message}\"\n        elif execution.progress and execution.progress.message:\n            # Extract progress message from Docket if available (spec line 403)\n            status_message = execution.progress.message\n\n        # createdAt is required per spec, but can be None from Redis\n        # Parse ISO string to datetime, or use current time as fallback\n        if created_at:\n            try:\n                created_at_dt = datetime.fromisoformat(\n                    created_at.replace(\"Z\", \"+00:00\")\n                )\n            except (ValueError, AttributeError):\n                created_at_dt = datetime.now(timezone.utc)\n        else:\n            created_at_dt = datetime.now(timezone.utc)\n\n        return GetTaskResult(\n            taskId=client_task_id,\n            status=mcp_state,\n            createdAt=created_at_dt,\n            lastUpdatedAt=datetime.now(timezone.utc),\n            ttl=DEFAULT_TTL_MS,\n            pollInterval=poll_interval_ms,\n            statusMessage=status_message,\n        )\n\n\nasync def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:\n    \"\"\"Handle MCP 'tasks/result' request (SEP-1686).\n\n    Converts raw task return values to MCP types based on task type.\n\n    Args:\n        server: FastMCP server instance\n        params: Request params containing taskId\n\n    Returns:\n        MCP result (CallToolResult, GetPromptResult, or ReadResourceResult)\n    \"\"\"\n    async with fastmcp.server.context.Context(fastmcp=server) as ctx:\n        client_task_id = params.get(\"taskId\")\n        if not client_task_id:\n            raise McpError(\n                ErrorData(\n                    code=INVALID_PARAMS, message=\"Missing required parameter: taskId\"\n                )\n            )\n\n        # Get session ID from Context\n        session_id = ctx.session_id\n\n        # Get execution from Docket (use instance attribute for cross-task access)\n        docket = server._docket\n        if docket is None:\n            raise McpError(\n                ErrorData(\n                    code=INTERNAL_ERROR,\n                    message=\"Background tasks require Docket\",\n                )\n            )\n\n        # Look up full task key from Redis\n        task_meta_key = docket.key(f\"fastmcp:task:{session_id}:{client_task_id}\")\n        async with docket.redis() as redis:\n            task_key_bytes = await redis.get(task_meta_key)\n\n        task_key = None if task_key_bytes is None else task_key_bytes.decode(\"utf-8\")\n\n        if task_key is None:\n            raise McpError(\n                ErrorData(\n                    code=INVALID_PARAMS,\n                    message=f\"Invalid taskId: {client_task_id} not found\",\n                )\n            )\n\n        execution = await docket.get_execution(task_key)\n        if execution is None:\n            raise McpError(\n                ErrorData(\n                    code=INVALID_PARAMS,\n                    message=f\"Invalid taskId: {client_task_id} not found\",\n                )\n            )\n\n        # Sync state from Redis\n        await execution.sync()\n\n        # Check if completed\n        state_map = DOCKET_TO_MCP_STATE\n        if execution.state not in (ExecutionState.COMPLETED, ExecutionState.FAILED):\n            mcp_state = state_map.get(execution.state, \"failed\")\n            raise McpError(\n                ErrorData(\n                    code=INVALID_PARAMS,\n                    message=f\"Task not completed yet (current state: {mcp_state})\",\n                )\n            )\n\n        # Get result from Docket\n        try:\n            raw_value = await execution.get_result(timeout=timedelta(seconds=0))\n        except Exception as error:\n            # Task failed - return error result\n            return mcp.types.CallToolResult(\n                content=[mcp.types.TextContent(type=\"text\", text=str(error))],\n                isError=True,\n                _meta={  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                    \"io.modelcontextprotocol/related-task\": {\n                        \"taskId\": client_task_id,\n                    }\n                },\n            )\n\n        # Parse task key to get component key\n        key_parts = parse_task_key(task_key)\n        component_key = key_parts[\"component_identifier\"]\n\n        # Look up component by its prefixed key (inlined from deleted get_component)\n        component: Tool | Resource | ResourceTemplate | Prompt | None = None\n        try:\n            if component_key.startswith(\"tool:\"):\n                name, version_str = _parse_key_version(component_key[5:])\n                version = VersionSpec(eq=version_str) if version_str else None\n                component = await server.get_tool(name, version)\n            elif component_key.startswith(\"resource:\"):\n                uri, version_str = _parse_key_version(component_key[9:])\n                version = VersionSpec(eq=version_str) if version_str else None\n                component = await server.get_resource(uri, version)\n            elif component_key.startswith(\"template:\"):\n                uri, version_str = _parse_key_version(component_key[9:])\n                version = VersionSpec(eq=version_str) if version_str else None\n                component = await server.get_resource_template(uri, version)\n            elif component_key.startswith(\"prompt:\"):\n                name, version_str = _parse_key_version(component_key[7:])\n                version = VersionSpec(eq=version_str) if version_str else None\n                component = await server.get_prompt(name, version)\n        except NotFoundError:\n            component = None\n\n        if component is None:\n            raise McpError(\n                ErrorData(\n                    code=INTERNAL_ERROR,\n                    message=f\"Component not found for task: {component_key}\",\n                )\n            )\n\n        # Build related-task metadata\n        related_task_meta = {\n            \"io.modelcontextprotocol/related-task\": {\n                \"taskId\": client_task_id,\n            }\n        }\n\n        # Convert based on component type.\n        # Each branch merges related_task_meta with any existing _meta\n        # (e.g. fastmcp.wrap_result) rather than overwriting it.\n        if isinstance(component, Tool):\n            fastmcp_result = component.convert_result(raw_value)\n            mcp_result = fastmcp_result.to_mcp_result()\n            if isinstance(mcp_result, mcp.types.CallToolResult):\n                merged = {**(mcp_result.meta or {}), **related_task_meta}\n                mcp_result._meta = merged  # type: ignore[attr-defined]\n            elif isinstance(mcp_result, tuple):\n                content, structured_content = mcp_result\n                mcp_result = mcp.types.CallToolResult(\n                    content=content,\n                    structuredContent=structured_content,\n                    _meta=related_task_meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                )\n            else:\n                mcp_result = mcp.types.CallToolResult(\n                    content=mcp_result,\n                    _meta=related_task_meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                )\n            return mcp_result\n\n        elif isinstance(component, Prompt):\n            fastmcp_result = component.convert_result(raw_value)\n            mcp_result = fastmcp_result.to_mcp_prompt_result()\n            merged = {**(mcp_result.meta or {}), **related_task_meta}\n            mcp_result._meta = merged  # type: ignore[attr-defined]\n            return mcp_result\n\n        elif isinstance(component, ResourceTemplate):\n            fastmcp_result = component.convert_result(raw_value)\n            mcp_result = fastmcp_result.to_mcp_result(component.uri_template)\n            merged = {**(mcp_result.meta or {}), **related_task_meta}\n            mcp_result._meta = merged  # type: ignore[attr-defined]\n            return mcp_result\n\n        elif isinstance(component, Resource):\n            fastmcp_result = component.convert_result(raw_value)\n            mcp_result = fastmcp_result.to_mcp_result(str(component.uri))\n            merged = {**(mcp_result.meta or {}), **related_task_meta}\n            mcp_result._meta = merged  # type: ignore[attr-defined]\n            return mcp_result\n\n        else:\n            raise McpError(\n                ErrorData(\n                    code=INTERNAL_ERROR,\n                    message=f\"Internal error: Unknown component type: {type(component).__name__}\",\n                )\n            )\n\n\nasync def tasks_list_handler(\n    server: FastMCP, params: dict[str, Any]\n) -> ListTasksResult:\n    \"\"\"Handle MCP 'tasks/list' request (SEP-1686).\n\n    Note: With client-side tracking, this returns minimal info.\n\n    Args:\n        server: FastMCP server instance\n        params: Request params (cursor, limit)\n\n    Returns:\n        ListTasksResult: Response with tasks list and pagination\n    \"\"\"\n    # Return empty list - client tracks tasks locally\n    return ListTasksResult(tasks=[], nextCursor=None)\n\n\nasync def tasks_cancel_handler(\n    server: FastMCP, params: dict[str, Any]\n) -> CancelTaskResult:\n    \"\"\"Handle MCP 'tasks/cancel' request (SEP-1686).\n\n    Cancels a running task, transitioning it to cancelled state.\n\n    Args:\n        server: FastMCP server instance\n        params: Request params containing taskId\n\n    Returns:\n        CancelTaskResult: Task status response showing cancelled state\n    \"\"\"\n    async with fastmcp.server.context.Context(fastmcp=server) as ctx:\n        client_task_id = params.get(\"taskId\")\n        if not client_task_id:\n            raise McpError(\n                ErrorData(\n                    code=INVALID_PARAMS, message=\"Missing required parameter: taskId\"\n                )\n            )\n\n        # Get session ID from Context\n        session_id = ctx.session_id\n\n        # Get Docket instance\n        docket = server._docket\n        if docket is None:\n            raise McpError(\n                ErrorData(\n                    code=INTERNAL_ERROR,\n                    message=\"Background tasks require Docket\",\n                )\n            )\n\n        # Look up task execution and metadata\n        execution, created_at, poll_interval_ms = await _lookup_task_execution(\n            docket, session_id, client_task_id\n        )\n\n        # Cancel via Docket (now sets CANCELLED state natively)\n        # Note: We need to get task_key from execution.key for cancellation\n        await docket.cancel(execution.key)\n\n        # Return task status with cancelled state\n        # createdAt is REQUIRED per SEP-1686 final spec (line 430)\n        # Per spec lines 447-448: SHOULD NOT include related-task metadata in tasks/cancel\n        return CancelTaskResult(\n            taskId=client_task_id,\n            status=\"cancelled\",\n            createdAt=datetime.fromisoformat(created_at)\n            if created_at\n            else datetime.now(timezone.utc),\n            lastUpdatedAt=datetime.now(timezone.utc),\n            ttl=DEFAULT_TTL_MS,\n            pollInterval=poll_interval_ms,\n            statusMessage=\"Task cancelled\",\n        )\n"
  },
  {
    "path": "src/fastmcp/server/tasks/routing.py",
    "content": "\"\"\"Task routing helper for MCP components.\n\nProvides unified task mode enforcement and docket routing logic.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, Literal\n\nimport mcp.types\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import METHOD_NOT_FOUND, ErrorData\n\nfrom fastmcp.server.tasks.config import TaskMeta\nfrom fastmcp.server.tasks.handlers import submit_to_docket\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.tools.base import Tool\n\nTaskType = Literal[\"tool\", \"resource\", \"template\", \"prompt\"]\n\n\nasync def check_background_task(\n    component: Tool | Resource | ResourceTemplate | Prompt,\n    task_type: TaskType,\n    arguments: dict[str, Any] | None = None,\n    task_meta: TaskMeta | None = None,\n) -> mcp.types.CreateTaskResult | None:\n    \"\"\"Check task mode and submit to background if requested.\n\n    Args:\n        component: The MCP component\n        task_type: Type of task (\"tool\", \"resource\", \"template\", \"prompt\")\n        arguments: Arguments for tool/prompt/template execution\n        task_meta: Task execution metadata. If provided, execute as background task.\n\n    Returns:\n        CreateTaskResult if submitted to docket, None for sync execution\n\n    Raises:\n        McpError: If mode=\"required\" but no task metadata, or mode=\"forbidden\"\n                  but task metadata is present\n    \"\"\"\n    task_config = component.task_config\n\n    # Infer label from component\n    entity_label = f\"{type(component).__name__} '{component.title or component.key}'\"\n\n    # Enforce mode=\"required\" - must have task metadata\n    if task_config.mode == \"required\" and not task_meta:\n        raise McpError(\n            ErrorData(\n                code=METHOD_NOT_FOUND,\n                message=f\"{entity_label} requires task-augmented execution\",\n            )\n        )\n\n    # Enforce mode=\"forbidden\" - cannot be called with task metadata\n    if not task_config.supports_tasks() and task_meta:\n        raise McpError(\n            ErrorData(\n                code=METHOD_NOT_FOUND,\n                message=f\"{entity_label} does not support task-augmented execution\",\n            )\n        )\n\n    # No task metadata - synchronous execution\n    if not task_meta:\n        return None\n\n    # fn_key is expected to be set; fall back to component.key for direct calls\n    fn_key = task_meta.fn_key or component.key\n    return await submit_to_docket(task_type, fn_key, component, arguments, task_meta)\n"
  },
  {
    "path": "src/fastmcp/server/tasks/subscriptions.py",
    "content": "\"\"\"Task subscription helpers for sending MCP notifications (SEP-1686).\n\nSubscribes to Docket execution state changes and sends notifications/tasks/status\nto clients when their tasks change state.\n\nThis module requires fastmcp[tasks] (pydocket). It is only imported when docket is available.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom contextlib import suppress\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING\n\nfrom docket.execution import ExecutionState\nfrom mcp.types import TaskStatusNotification, TaskStatusNotificationParams\n\nfrom fastmcp.server.tasks.config import DEFAULT_TTL_MS\nfrom fastmcp.server.tasks.keys import parse_task_key\nfrom fastmcp.server.tasks.requests import DOCKET_TO_MCP_STATE\nfrom fastmcp.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n    from mcp.server.session import ServerSession\n\nlogger = get_logger(__name__)\n\n\nasync def subscribe_to_task_updates(\n    task_id: str,\n    task_key: str,\n    session: ServerSession,\n    docket: Docket,\n    poll_interval_ms: int = 5000,\n) -> None:\n    \"\"\"Subscribe to Docket execution events and send MCP notifications.\n\n    Per SEP-1686 lines 436-444, servers MAY send notifications/tasks/status\n    when task state changes. This is an optional optimization that reduces\n    client polling frequency.\n\n    Args:\n        task_id: Client-visible task ID (server-generated UUID)\n        task_key: Internal Docket execution key (includes session, type, component)\n        session: MCP ServerSession for sending notifications\n        docket: Docket instance for subscribing to execution events\n        poll_interval_ms: Poll interval in milliseconds to include in notifications\n    \"\"\"\n    try:\n        execution = await docket.get_execution(task_key)\n        if execution is None:\n            logger.warning(f\"No execution found for task {task_id}\")\n            return\n\n        # Subscribe to state and progress events from Docket\n        terminal_states = {\n            ExecutionState.COMPLETED,\n            ExecutionState.FAILED,\n            ExecutionState.CANCELLED,\n        }\n        async for event in execution.subscribe():\n            if event[\"type\"] == \"state\":\n                state = ExecutionState(event[\"state\"])\n                # Send notifications/tasks/status when state changes\n                await _send_status_notification(\n                    session=session,\n                    task_id=task_id,\n                    task_key=task_key,\n                    docket=docket,\n                    state=state,\n                    poll_interval_ms=poll_interval_ms,\n                )\n                # Stop subscribing once the task reaches a terminal state\n                if state in terminal_states:\n                    break\n            elif event[\"type\"] == \"progress\":\n                # Send notification when progress message changes\n                await _send_progress_notification(\n                    session=session,\n                    task_id=task_id,\n                    task_key=task_key,\n                    docket=docket,\n                    execution=execution,\n                    poll_interval_ms=poll_interval_ms,\n                )\n\n    except Exception as e:\n        logger.warning(f\"Subscription task failed for {task_id}: {e}\", exc_info=True)\n\n\nasync def _send_status_notification(\n    session: ServerSession,\n    task_id: str,\n    task_key: str,\n    docket: Docket,\n    state: ExecutionState,\n    poll_interval_ms: int = 5000,\n) -> None:\n    \"\"\"Send notifications/tasks/status to client.\n\n    Per SEP-1686 line 454: notification SHOULD NOT include related-task metadata\n    (taskId is already in params).\n\n    Args:\n        session: MCP ServerSession\n        task_id: Client-visible task ID\n        task_key: Internal task key (for metadata lookup)\n        docket: Docket instance\n        state: Docket execution state (enum)\n        poll_interval_ms: Poll interval in milliseconds\n    \"\"\"\n    # Map Docket state to MCP status\n    state_map = DOCKET_TO_MCP_STATE\n    mcp_status = state_map.get(state, \"failed\")\n\n    # Extract session_id from task_key for Redis lookup\n    key_parts = parse_task_key(task_key)\n    session_id = key_parts[\"session_id\"]\n\n    created_at_key = docket.key(f\"fastmcp:task:{session_id}:{task_id}:created_at\")\n    async with docket.redis() as redis:\n        created_at_bytes = await redis.get(created_at_key)\n\n    created_at = (\n        created_at_bytes.decode(\"utf-8\")\n        if created_at_bytes\n        else datetime.now(timezone.utc).isoformat()\n    )\n\n    # Build status message\n    status_message = None\n    if state == ExecutionState.COMPLETED:\n        status_message = \"Task completed successfully\"\n    elif state == ExecutionState.FAILED:\n        status_message = \"Task failed\"\n    elif state == ExecutionState.CANCELLED:\n        status_message = \"Task cancelled\"\n\n    params_dict = {\n        \"taskId\": task_id,\n        \"status\": mcp_status,\n        \"createdAt\": created_at,\n        \"lastUpdatedAt\": datetime.now(timezone.utc).isoformat(),\n        \"ttl\": DEFAULT_TTL_MS,\n        \"pollInterval\": poll_interval_ms,\n    }\n\n    if status_message:\n        params_dict[\"statusMessage\"] = status_message\n\n    # Create notification (no related-task metadata per spec line 454)\n    notification = TaskStatusNotification(\n        params=TaskStatusNotificationParams.model_validate(params_dict),\n    )\n\n    # Send notification (don't let failures break the subscription)\n    with suppress(Exception):\n        await session.send_notification(notification)  # type: ignore[arg-type]\n\n\nasync def _send_progress_notification(\n    session: ServerSession,\n    task_id: str,\n    task_key: str,\n    docket: Docket,\n    execution: Execution,\n    poll_interval_ms: int = 5000,\n) -> None:\n    \"\"\"Send notifications/tasks/status when progress updates.\n\n    Args:\n        session: MCP ServerSession\n        task_id: Client-visible task ID\n        task_key: Internal task key\n        docket: Docket instance\n        execution: Execution object with current progress\n        poll_interval_ms: Poll interval in milliseconds\n    \"\"\"\n    # Sync execution to get latest progress\n    await execution.sync()\n\n    # Only send if there's a progress message\n    if not execution.progress or not execution.progress.message:\n        return\n\n    # Map Docket state to MCP status\n    state_map = DOCKET_TO_MCP_STATE\n    mcp_status = state_map.get(execution.state, \"failed\")\n\n    # Extract session_id from task_key for Redis lookup\n    key_parts = parse_task_key(task_key)\n    session_id = key_parts[\"session_id\"]\n\n    created_at_key = docket.key(f\"fastmcp:task:{session_id}:{task_id}:created_at\")\n    async with docket.redis() as redis:\n        created_at_bytes = await redis.get(created_at_key)\n\n    created_at = (\n        created_at_bytes.decode(\"utf-8\")\n        if created_at_bytes\n        else datetime.now(timezone.utc).isoformat()\n    )\n\n    params_dict = {\n        \"taskId\": task_id,\n        \"status\": mcp_status,\n        \"createdAt\": created_at,\n        \"lastUpdatedAt\": datetime.now(timezone.utc).isoformat(),\n        \"ttl\": DEFAULT_TTL_MS,\n        \"pollInterval\": poll_interval_ms,\n        \"statusMessage\": execution.progress.message,\n    }\n\n    # Create and send notification\n    notification = TaskStatusNotification(\n        params=TaskStatusNotificationParams.model_validate(params_dict),\n    )\n\n    with suppress(Exception):\n        await session.send_notification(notification)  # type: ignore[arg-type]\n"
  },
  {
    "path": "src/fastmcp/server/telemetry.py",
    "content": "\"\"\"Server-side telemetry helpers.\"\"\"\n\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\n\nfrom mcp.server.lowlevel.server import request_ctx\nfrom opentelemetry.context import Context\nfrom opentelemetry.trace import Span, SpanKind, Status, StatusCode\n\nfrom fastmcp.telemetry import extract_trace_context, get_tracer\n\n\ndef get_auth_span_attributes() -> dict[str, str]:\n    \"\"\"Get auth attributes for the current request, if authenticated.\"\"\"\n    from fastmcp.server.dependencies import get_access_token\n\n    attrs: dict[str, str] = {}\n    try:\n        token = get_access_token()\n        if token:\n            if token.client_id:\n                attrs[\"enduser.id\"] = token.client_id\n            if token.scopes:\n                attrs[\"enduser.scope\"] = \" \".join(token.scopes)\n    except RuntimeError:\n        pass\n    return attrs\n\n\ndef get_session_span_attributes() -> dict[str, str]:\n    \"\"\"Get session attributes for the current request.\"\"\"\n    from fastmcp.server.dependencies import get_context\n\n    attrs: dict[str, str] = {}\n    try:\n        ctx = get_context()\n        if ctx.request_context is not None and ctx.session_id is not None:\n            attrs[\"mcp.session.id\"] = ctx.session_id\n    except RuntimeError:\n        pass\n    return attrs\n\n\ndef _get_parent_trace_context() -> Context | None:\n    \"\"\"Get parent trace context from request meta for distributed tracing.\"\"\"\n    try:\n        req_ctx = request_ctx.get()\n        if req_ctx and hasattr(req_ctx, \"meta\") and req_ctx.meta:\n            return extract_trace_context(dict(req_ctx.meta))\n    except LookupError:\n        pass\n    return None\n\n\n@contextmanager\ndef server_span(\n    name: str,\n    method: str,\n    server_name: str,\n    component_type: str,\n    component_key: str,\n    resource_uri: str | None = None,\n) -> Generator[Span, None, None]:\n    \"\"\"Create a SERVER span with standard MCP attributes and auth context.\n\n    Automatically records any exception on the span and sets error status.\n    \"\"\"\n    tracer = get_tracer()\n    with tracer.start_as_current_span(\n        name,\n        context=_get_parent_trace_context(),\n        kind=SpanKind.SERVER,\n    ) as span:\n        attrs: dict[str, str] = {\n            # RPC semantic conventions\n            \"rpc.system\": \"mcp\",\n            \"rpc.service\": server_name,\n            \"rpc.method\": method,\n            # MCP semantic conventions\n            \"mcp.method.name\": method,\n            # FastMCP-specific attributes\n            \"fastmcp.server.name\": server_name,\n            \"fastmcp.component.type\": component_type,\n            \"fastmcp.component.key\": component_key,\n            **get_auth_span_attributes(),\n            **get_session_span_attributes(),\n        }\n        if resource_uri is not None:\n            attrs[\"mcp.resource.uri\"] = resource_uri\n        span.set_attributes(attrs)\n        try:\n            yield span\n        except Exception as e:\n            span.record_exception(e)\n            span.set_status(Status(StatusCode.ERROR))\n            raise\n\n\n@contextmanager\ndef delegate_span(\n    name: str,\n    provider_type: str,\n    component_key: str,\n) -> Generator[Span, None, None]:\n    \"\"\"Create an INTERNAL span for provider delegation.\n\n    Used by FastMCPProvider when delegating to mounted servers.\n    Automatically records any exception on the span and sets error status.\n    \"\"\"\n    tracer = get_tracer()\n    with tracer.start_as_current_span(f\"delegate {name}\") as span:\n        span.set_attributes(\n            {\n                \"fastmcp.provider.type\": provider_type,\n                \"fastmcp.component.key\": component_key,\n            }\n        )\n        try:\n            yield span\n        except Exception as e:\n            span.record_exception(e)\n            span.set_status(Status(StatusCode.ERROR))\n            raise\n\n\n__all__ = [\n    \"delegate_span\",\n    \"get_auth_span_attributes\",\n    \"get_session_span_attributes\",\n    \"server_span\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/transforms/__init__.py",
    "content": "\"\"\"Transform system for component transformations.\n\nTransforms modify components (tools, resources, prompts). List operations use a pure\nfunction pattern where transforms receive sequences and return transformed sequences.\nGet operations use a middleware pattern with `call_next` to chain lookups.\n\nUnlike middleware (which operates on requests), transforms are observable by the\nsystem for task registration, tag filtering, and component introspection.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms import Namespace\n\n    server = FastMCP(\"Server\")\n    mount = server.mount(other_server)\n    mount.add_transform(Namespace(\"api\"))  # Tools become api_toolname\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Awaitable, Sequence\nfrom typing import TYPE_CHECKING, Protocol\n\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.tools.base import Tool\n\n\n# Get methods use Protocol to express keyword-only version parameter\nclass GetToolNext(Protocol):\n    \"\"\"Protocol for get_tool call_next functions.\"\"\"\n\n    def __call__(\n        self, name: str, *, version: VersionSpec | None = None\n    ) -> Awaitable[Tool | None]: ...\n\n\nclass GetResourceNext(Protocol):\n    \"\"\"Protocol for get_resource call_next functions.\"\"\"\n\n    def __call__(\n        self, uri: str, *, version: VersionSpec | None = None\n    ) -> Awaitable[Resource | None]: ...\n\n\nclass GetResourceTemplateNext(Protocol):\n    \"\"\"Protocol for get_resource_template call_next functions.\"\"\"\n\n    def __call__(\n        self, uri: str, *, version: VersionSpec | None = None\n    ) -> Awaitable[ResourceTemplate | None]: ...\n\n\nclass GetPromptNext(Protocol):\n    \"\"\"Protocol for get_prompt call_next functions.\"\"\"\n\n    def __call__(\n        self, name: str, *, version: VersionSpec | None = None\n    ) -> Awaitable[Prompt | None]: ...\n\n\nclass Transform:\n    \"\"\"Base class for component transformations.\n\n    List operations use a pure function pattern: transforms receive sequences\n    and return transformed sequences. Get operations use a middleware pattern\n    with `call_next` to chain lookups.\n\n    Example:\n        ```python\n        class MyTransform(Transform):\n            async def list_tools(self, tools):\n                return [transform(t) for t in tools]  # Transform sequence\n\n            async def get_tool(self, name, call_next, *, version=None):\n                original = self.reverse_name(name)  # Map to original name\n                tool = await call_next(original, version=version)  # Get from downstream\n                return transform(tool) if tool else None\n        ```\n    \"\"\"\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\n    # -------------------------------------------------------------------------\n    # Tools\n    # -------------------------------------------------------------------------\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"List tools with transformation applied.\n\n        Args:\n            tools: Sequence of tools to transform.\n\n        Returns:\n            Transformed sequence of tools.\n        \"\"\"\n        return tools\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name.\n\n        Args:\n            name: The requested tool name (may be transformed).\n            call_next: Callable to get tool from downstream.\n            version: Optional version filter to apply.\n\n        Returns:\n            The tool if found, None otherwise.\n        \"\"\"\n        return await call_next(name, version=version)\n\n    # -------------------------------------------------------------------------\n    # Resources\n    # -------------------------------------------------------------------------\n\n    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:\n        \"\"\"List resources with transformation applied.\n\n        Args:\n            resources: Sequence of resources to transform.\n\n        Returns:\n            Transformed sequence of resources.\n        \"\"\"\n        return resources\n\n    async def get_resource(\n        self,\n        uri: str,\n        call_next: GetResourceNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> Resource | None:\n        \"\"\"Get a resource by URI.\n\n        Args:\n            uri: The requested resource URI (may be transformed).\n            call_next: Callable to get resource from downstream.\n            version: Optional version filter to apply.\n\n        Returns:\n            The resource if found, None otherwise.\n        \"\"\"\n        return await call_next(uri, version=version)\n\n    # -------------------------------------------------------------------------\n    # Resource Templates\n    # -------------------------------------------------------------------------\n\n    async def list_resource_templates(\n        self, templates: Sequence[ResourceTemplate]\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"List resource templates with transformation applied.\n\n        Args:\n            templates: Sequence of resource templates to transform.\n\n        Returns:\n            Transformed sequence of resource templates.\n        \"\"\"\n        return templates\n\n    async def get_resource_template(\n        self,\n        uri: str,\n        call_next: GetResourceTemplateNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> ResourceTemplate | None:\n        \"\"\"Get a resource template by URI.\n\n        Args:\n            uri: The requested template URI (may be transformed).\n            call_next: Callable to get template from downstream.\n            version: Optional version filter to apply.\n\n        Returns:\n            The resource template if found, None otherwise.\n        \"\"\"\n        return await call_next(uri, version=version)\n\n    # -------------------------------------------------------------------------\n    # Prompts\n    # -------------------------------------------------------------------------\n\n    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:\n        \"\"\"List prompts with transformation applied.\n\n        Args:\n            prompts: Sequence of prompts to transform.\n\n        Returns:\n            Transformed sequence of prompts.\n        \"\"\"\n        return prompts\n\n    async def get_prompt(\n        self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get a prompt by name.\n\n        Args:\n            name: The requested prompt name (may be transformed).\n            call_next: Callable to get prompt from downstream.\n            version: Optional version filter to apply.\n\n        Returns:\n            The prompt if found, None otherwise.\n        \"\"\"\n        return await call_next(name, version=version)\n\n\n# Re-export built-in transforms (must be after Transform class to avoid circular imports)\nfrom fastmcp.server.transforms.visibility import Visibility, is_enabled  # noqa: E402\nfrom fastmcp.server.transforms.namespace import Namespace  # noqa: E402\nfrom fastmcp.server.transforms.prompts_as_tools import PromptsAsTools  # noqa: E402\nfrom fastmcp.server.transforms.resources_as_tools import ResourcesAsTools  # noqa: E402\nfrom fastmcp.server.transforms.tool_transform import ToolTransform  # noqa: E402\nfrom fastmcp.server.transforms.version_filter import VersionFilter  # noqa: E402\n\n__all__ = [\n    \"Namespace\",\n    \"PromptsAsTools\",\n    \"ResourcesAsTools\",\n    \"ToolTransform\",\n    \"Transform\",\n    \"VersionFilter\",\n    \"VersionSpec\",\n    \"Visibility\",\n    \"is_enabled\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/transforms/catalog.py",
    "content": "\"\"\"Base class for transforms that need to read the real component catalog.\n\nSome transforms replace ``list_tools()`` output with synthetic components\n(e.g. a search interface) while still needing access to the *real*\n(auth-filtered) catalog at call time.  ``CatalogTransform`` provides the\nbypass machinery so subclasses can call ``get_tool_catalog()`` without\ntriggering their own replacement logic.\n\nRe-entrancy problem\n-------------------\n\nWhen a synthetic tool handler calls ``get_tool_catalog()``, that calls\n``ctx.fastmcp.list_tools()`` which re-enters the transform pipeline —\nincluding *this* transform's ``list_tools()``.  If the subclass overrides\n``list_tools()`` directly, the re-entrant call would hit the subclass's\nreplacement logic again (returning synthetic tools instead of the real\ncatalog).  A ``super()`` call can't prevent this because Python can't\nshort-circuit a method after ``super()`` returns.\n\nSolution: ``CatalogTransform`` owns ``list_tools()`` and uses a\nper-instance ``ContextVar`` to detect re-entrant calls.  During bypass,\nit passes through to the base ``Transform.list_tools()`` (a no-op).\nOtherwise, it delegates to ``transform_tools()`` — the subclass hook\nwhere replacement logic lives.  Same pattern for resources, prompts,\nand resource templates.\n\nThis is *not* the same as the ``Provider._list_tools()`` convention\n(which produces raw components with no arguments).  ``transform_tools()``\nreceives the current catalog and returns a transformed version.  The\ndistinct name avoids confusion between the two patterns.\n\nUsage::\n\n    class MyTransform(CatalogTransform):\n        async def transform_tools(self, tools):\n            return [self._make_search_tool()]\n\n        def _make_search_tool(self):\n            async def search(ctx: Context = None):\n                real_tools = await self.get_tool_catalog(ctx)\n                ...\n            return Tool.from_function(fn=search, name=\"search\")\n\"\"\"\n\nfrom __future__ import annotations\n\nimport itertools\nfrom collections.abc import Sequence\nfrom contextvars import ContextVar\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.server.transforms import Transform\nfrom fastmcp.utilities.versions import dedupe_with_versions\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.server.context import Context\n    from fastmcp.tools.base import Tool\n\n_instance_counter = itertools.count()\n\n\nclass CatalogTransform(Transform):\n    \"\"\"Transform that needs access to the real component catalog.\n\n    Subclasses override ``transform_tools()`` / ``transform_resources()``\n    / ``transform_prompts()`` / ``transform_resource_templates()``\n    instead of the ``list_*()`` methods.  The base class owns\n    ``list_*()`` and handles re-entrant bypass automatically — subclasses\n    never see re-entrant calls from ``get_*_catalog()``.\n\n    The ``get_*_catalog()`` methods fetch the real (auth-filtered) catalog\n    by temporarily setting a bypass flag so that this transform's\n    ``list_*()`` passes through without calling the subclass hook.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._instance_id: int = next(_instance_counter)\n        self._bypass: ContextVar[bool] = ContextVar(\n            f\"_catalog_bypass_{self._instance_id}\", default=False\n        )\n\n    # ------------------------------------------------------------------\n    # list_* (bypass-aware — subclasses override transform_* instead)\n    # ------------------------------------------------------------------\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        if self._bypass.get():\n            return await super().list_tools(tools)\n        return await self.transform_tools(tools)\n\n    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:\n        if self._bypass.get():\n            return await super().list_resources(resources)\n        return await self.transform_resources(resources)\n\n    async def list_resource_templates(\n        self, templates: Sequence[ResourceTemplate]\n    ) -> Sequence[ResourceTemplate]:\n        if self._bypass.get():\n            return await super().list_resource_templates(templates)\n        return await self.transform_resource_templates(templates)\n\n    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:\n        if self._bypass.get():\n            return await super().list_prompts(prompts)\n        return await self.transform_prompts(prompts)\n\n    # ------------------------------------------------------------------\n    # Subclass hooks (override these, not list_*)\n    # ------------------------------------------------------------------\n\n    async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Transform the tool catalog.\n\n        Override this method to replace, filter, or augment the tool listing.\n        The default implementation passes through unchanged.\n\n        Do NOT override ``list_tools()`` directly — the base class uses it\n        to handle re-entrant bypass when ``get_tool_catalog()`` reads the\n        real catalog.\n        \"\"\"\n        return tools\n\n    async def transform_resources(\n        self, resources: Sequence[Resource]\n    ) -> Sequence[Resource]:\n        \"\"\"Transform the resource catalog.\n\n        Override this method to replace, filter, or augment the resource listing.\n        The default implementation passes through unchanged.\n\n        Do NOT override ``list_resources()`` directly — the base class uses it\n        to handle re-entrant bypass when ``get_resource_catalog()`` reads the\n        real catalog.\n        \"\"\"\n        return resources\n\n    async def transform_resource_templates(\n        self, templates: Sequence[ResourceTemplate]\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"Transform the resource template catalog.\n\n        Override this method to replace, filter, or augment the template listing.\n        The default implementation passes through unchanged.\n\n        Do NOT override ``list_resource_templates()`` directly — the base class\n        uses it to handle re-entrant bypass when\n        ``get_resource_template_catalog()`` reads the real catalog.\n        \"\"\"\n        return templates\n\n    async def transform_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:\n        \"\"\"Transform the prompt catalog.\n\n        Override this method to replace, filter, or augment the prompt listing.\n        The default implementation passes through unchanged.\n\n        Do NOT override ``list_prompts()`` directly — the base class uses it\n        to handle re-entrant bypass when ``get_prompt_catalog()`` reads the\n        real catalog.\n        \"\"\"\n        return prompts\n\n    # ------------------------------------------------------------------\n    # Catalog accessors\n    # ------------------------------------------------------------------\n\n    async def get_tool_catalog(\n        self, ctx: Context, *, run_middleware: bool = True\n    ) -> Sequence[Tool]:\n        \"\"\"Fetch the real tool catalog, bypassing this transform.\n\n        The result is deduplicated by name so that only the highest version\n        of each tool is returned — matching what protocol handlers expose\n        on the wire.\n\n        Args:\n            ctx: The current request context.\n            run_middleware: Whether to run middleware on the inner call.\n                Defaults to True because this is typically called from a\n                tool handler where list_tools middleware has not yet run.\n        \"\"\"\n        token = self._bypass.set(True)\n        try:\n            tools = await ctx.fastmcp.list_tools(run_middleware=run_middleware)\n        finally:\n            self._bypass.reset(token)\n        return dedupe_with_versions(tools, lambda t: t.name)\n\n    async def get_resource_catalog(\n        self, ctx: Context, *, run_middleware: bool = True\n    ) -> Sequence[Resource]:\n        \"\"\"Fetch the real resource catalog, bypassing this transform.\n\n        Args:\n            ctx: The current request context.\n            run_middleware: Whether to run middleware on the inner call.\n                Defaults to True because this is typically called from a\n                tool handler where list_resources middleware has not yet run.\n        \"\"\"\n        token = self._bypass.set(True)\n        try:\n            return await ctx.fastmcp.list_resources(run_middleware=run_middleware)\n        finally:\n            self._bypass.reset(token)\n\n    async def get_prompt_catalog(\n        self, ctx: Context, *, run_middleware: bool = True\n    ) -> Sequence[Prompt]:\n        \"\"\"Fetch the real prompt catalog, bypassing this transform.\n\n        Args:\n            ctx: The current request context.\n            run_middleware: Whether to run middleware on the inner call.\n                Defaults to True because this is typically called from a\n                tool handler where list_prompts middleware has not yet run.\n        \"\"\"\n        token = self._bypass.set(True)\n        try:\n            return await ctx.fastmcp.list_prompts(run_middleware=run_middleware)\n        finally:\n            self._bypass.reset(token)\n\n    async def get_resource_template_catalog(\n        self, ctx: Context, *, run_middleware: bool = True\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"Fetch the real resource template catalog, bypassing this transform.\n\n        Args:\n            ctx: The current request context.\n            run_middleware: Whether to run middleware on the inner call.\n                Defaults to True because this is typically called from a\n                tool handler where list_resource_templates middleware has\n                not yet run.\n        \"\"\"\n        token = self._bypass.set(True)\n        try:\n            return await ctx.fastmcp.list_resource_templates(\n                run_middleware=run_middleware\n            )\n        finally:\n            self._bypass.reset(token)\n"
  },
  {
    "path": "src/fastmcp/server/transforms/namespace.py",
    "content": "\"\"\"Namespace transform for prefixing component names.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.server.transforms import (\n    GetPromptNext,\n    GetResourceNext,\n    GetResourceTemplateNext,\n    GetToolNext,\n    Transform,\n)\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.tools.base import Tool\n\n# Pattern for matching URIs: protocol://path\n_URI_PATTERN = re.compile(r\"^([^:]+://)(.*?)$\")\n\n\nclass Namespace(Transform):\n    \"\"\"Prefixes component names with a namespace.\n\n    - Tools: name → namespace_name\n    - Prompts: name → namespace_name\n    - Resources: protocol://path → protocol://namespace/path\n    - Resource Templates: same as resources\n\n    Example:\n        ```python\n        transform = Namespace(\"math\")\n        # Tool \"add\" becomes \"math_add\"\n        # Resource \"file://data.txt\" becomes \"file://math/data.txt\"\n        ```\n    \"\"\"\n\n    def __init__(self, prefix: str) -> None:\n        \"\"\"Initialize Namespace transform.\n\n        Args:\n            prefix: The namespace prefix to apply.\n        \"\"\"\n        self._prefix = prefix\n        self._name_prefix = f\"{prefix}_\"\n\n    def __repr__(self) -> str:\n        return f\"Namespace({self._prefix!r})\"\n\n    # -------------------------------------------------------------------------\n    # Name transformation helpers\n    # -------------------------------------------------------------------------\n\n    def _transform_name(self, name: str) -> str:\n        \"\"\"Apply namespace prefix to a name.\"\"\"\n        return f\"{self._name_prefix}{name}\"\n\n    def _reverse_name(self, name: str) -> str | None:\n        \"\"\"Remove namespace prefix from a name, or None if no match.\"\"\"\n        if name.startswith(self._name_prefix):\n            return name[len(self._name_prefix) :]\n        return None\n\n    # -------------------------------------------------------------------------\n    # URI transformation helpers\n    # -------------------------------------------------------------------------\n\n    def _transform_uri(self, uri: str) -> str:\n        \"\"\"Apply namespace to a URI: protocol://path → protocol://namespace/path.\"\"\"\n        match = _URI_PATTERN.match(uri)\n        if match:\n            protocol, path = match.groups()\n            return f\"{protocol}{self._prefix}/{path}\"\n        return uri\n\n    def _reverse_uri(self, uri: str) -> str | None:\n        \"\"\"Remove namespace from a URI, or None if no match.\"\"\"\n        match = _URI_PATTERN.match(uri)\n        if match:\n            protocol, path = match.groups()\n            prefix = f\"{self._prefix}/\"\n            if path.startswith(prefix):\n                return f\"{protocol}{path[len(prefix) :]}\"\n            return None\n        return None\n\n    # -------------------------------------------------------------------------\n    # Tools\n    # -------------------------------------------------------------------------\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Prefix tool names with namespace.\"\"\"\n        return [\n            t.model_copy(update={\"name\": self._transform_name(t.name)}) for t in tools\n        ]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get tool by namespaced name.\"\"\"\n        original = self._reverse_name(name)\n        if original is None:\n            return None\n        tool = await call_next(original, version=version)\n        if tool:\n            return tool.model_copy(update={\"name\": name})\n        return None\n\n    # -------------------------------------------------------------------------\n    # Resources\n    # -------------------------------------------------------------------------\n\n    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:\n        \"\"\"Add namespace path segment to resource URIs.\"\"\"\n        return [\n            r.model_copy(update={\"uri\": self._transform_uri(str(r.uri))})\n            for r in resources\n        ]\n\n    async def get_resource(\n        self,\n        uri: str,\n        call_next: GetResourceNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> Resource | None:\n        \"\"\"Get resource by namespaced URI.\"\"\"\n        original = self._reverse_uri(uri)\n        if original is None:\n            return None\n        resource = await call_next(original, version=version)\n        if resource:\n            return resource.model_copy(update={\"uri\": uri})\n        return None\n\n    # -------------------------------------------------------------------------\n    # Resource Templates\n    # -------------------------------------------------------------------------\n\n    async def list_resource_templates(\n        self, templates: Sequence[ResourceTemplate]\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"Add namespace path segment to template URIs.\"\"\"\n        return [\n            t.model_copy(update={\"uri_template\": self._transform_uri(t.uri_template)})\n            for t in templates\n        ]\n\n    async def get_resource_template(\n        self,\n        uri: str,\n        call_next: GetResourceTemplateNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> ResourceTemplate | None:\n        \"\"\"Get resource template by namespaced URI.\"\"\"\n        original = self._reverse_uri(uri)\n        if original is None:\n            return None\n        template = await call_next(original, version=version)\n        if template:\n            return template.model_copy(\n                update={\"uri_template\": self._transform_uri(template.uri_template)}\n            )\n        return None\n\n    # -------------------------------------------------------------------------\n    # Prompts\n    # -------------------------------------------------------------------------\n\n    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:\n        \"\"\"Prefix prompt names with namespace.\"\"\"\n        return [\n            p.model_copy(update={\"name\": self._transform_name(p.name)}) for p in prompts\n        ]\n\n    async def get_prompt(\n        self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Get prompt by namespaced name.\"\"\"\n        original = self._reverse_name(name)\n        if original is None:\n            return None\n        prompt = await call_next(original, version=version)\n        if prompt:\n            return prompt.model_copy(update={\"name\": name})\n        return None\n"
  },
  {
    "path": "src/fastmcp/server/transforms/prompts_as_tools.py",
    "content": "\"\"\"Transform that exposes prompts as tools.\n\nThis transform generates tools for listing and getting prompts, enabling\nclients that only support tools to access prompt functionality.\n\nThe generated tools route through `ctx.fastmcp` at runtime, so all server\nmiddleware (auth, visibility, rate limiting, etc.) applies to prompt\noperations exactly as it would for direct `prompts/get` calls.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms import PromptsAsTools\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_transform(PromptsAsTools(mcp))\n    # Now has list_prompts and get_prompt tools\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING, Annotated, Any\n\nfrom mcp.types import TextContent\n\nfrom fastmcp.server.dependencies import get_context\nfrom fastmcp.server.transforms import GetToolNext, Transform\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.base import Provider\n\n\nclass PromptsAsTools(Transform):\n    \"\"\"Transform that adds tools for listing and getting prompts.\n\n    Generates two tools:\n    - `list_prompts`: Lists all prompts\n    - `get_prompt`: Gets a specific prompt with optional arguments\n\n    The generated tools route through the server at runtime, so auth,\n    middleware, and visibility apply automatically.\n\n    This transform should be applied to a FastMCP server instance, not\n    a raw Provider, because the generated tools need the server's\n    middleware chain for auth and visibility filtering.\n\n    Example:\n        ```python\n        mcp = FastMCP(\"Server\")\n        mcp.add_transform(PromptsAsTools(mcp))\n        # Now has list_prompts and get_prompt tools\n        ```\n    \"\"\"\n\n    def __init__(self, provider: Provider) -> None:\n        from fastmcp.server.server import FastMCP\n\n        if not isinstance(provider, FastMCP):\n            raise TypeError(\n                \"PromptsAsTools requires a FastMCP server instance, not a\"\n                f\" {type(provider).__name__}. The generated tools route through\"\n                \" the server's middleware chain at runtime for auth and\"\n                \" visibility. Pass your FastMCP server: PromptsAsTools(mcp)\"\n            )\n        self._provider = provider\n\n    def __repr__(self) -> str:\n        return f\"PromptsAsTools({self._provider!r})\"\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Add prompt tools to the tool list.\"\"\"\n        return [\n            *tools,\n            self._make_list_prompts_tool(),\n            self._make_get_prompt_tool(),\n        ]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name, including generated prompt tools.\"\"\"\n        if name == \"list_prompts\":\n            return self._make_list_prompts_tool()\n        if name == \"get_prompt\":\n            return self._make_get_prompt_tool()\n        return await call_next(name, version=version)\n\n    def _make_list_prompts_tool(self) -> Tool:\n        \"\"\"Create the list_prompts tool.\"\"\"\n\n        async def list_prompts() -> str:\n            \"\"\"List all available prompts.\n\n            Returns JSON with prompt metadata including name, description,\n            and optional arguments.\n            \"\"\"\n            ctx = get_context()\n            prompts = await ctx.fastmcp.list_prompts()\n\n            result: list[dict[str, Any]] = []\n            for p in prompts:\n                result.append(\n                    {\n                        \"name\": p.name,\n                        \"description\": p.description,\n                        \"arguments\": [\n                            {\n                                \"name\": arg.name,\n                                \"description\": arg.description,\n                                \"required\": arg.required,\n                            }\n                            for arg in (p.arguments or [])\n                        ],\n                    }\n                )\n\n            return json.dumps(result, indent=2)\n\n        return Tool.from_function(fn=list_prompts)\n\n    def _make_get_prompt_tool(self) -> Tool:\n        \"\"\"Create the get_prompt tool.\"\"\"\n\n        async def get_prompt(\n            name: Annotated[str, \"The name of the prompt to get\"],\n            arguments: Annotated[\n                dict[str, Any] | None,\n                \"Optional arguments for the prompt\",\n            ] = None,\n        ) -> str:\n            \"\"\"Get a prompt by name with optional arguments.\n\n            Returns the rendered prompt as JSON with a messages array.\n            Arguments should be provided as a dict mapping argument names\n            to values.\n            \"\"\"\n            ctx = get_context()\n            result = await ctx.fastmcp.render_prompt(name, arguments=arguments or {})\n            return _format_prompt_result(result)\n\n        return Tool.from_function(fn=get_prompt)\n\n\ndef _format_prompt_result(result: Any) -> str:\n    \"\"\"Format PromptResult for tool output.\n\n    Returns JSON with the messages array. Preserves embedded resources\n    as structured JSON objects.\n    \"\"\"\n    messages = []\n    for msg in result.messages:\n        if isinstance(msg.content, TextContent):\n            content = msg.content.text\n        else:\n            content = msg.content.model_dump(mode=\"json\", exclude_none=True)\n\n        messages.append(\n            {\n                \"role\": msg.role,\n                \"content\": content,\n            }\n        )\n\n    return json.dumps({\"messages\": messages}, indent=2)\n"
  },
  {
    "path": "src/fastmcp/server/transforms/resources_as_tools.py",
    "content": "\"\"\"Transform that exposes resources as tools.\n\nThis transform generates tools for listing and reading resources, enabling\nclients that only support tools to access resource functionality.\n\nThe generated tools route through `ctx.fastmcp` at runtime, so all server\nmiddleware (auth, visibility, rate limiting, etc.) applies to resource\noperations exactly as it would for direct `resources/read` calls.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms import ResourcesAsTools\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_transform(ResourcesAsTools(mcp))\n    # Now has list_resources and read_resource tools\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING, Annotated, Any\n\nfrom mcp.types import ToolAnnotations\n\nfrom fastmcp.server.dependencies import get_context\nfrom fastmcp.server.transforms import GetToolNext, Transform\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.versions import VersionSpec\n\n_DEFAULT_ANNOTATIONS = ToolAnnotations(readOnlyHint=True)\n\nif TYPE_CHECKING:\n    from fastmcp.server.providers.base import Provider\n\n\nclass ResourcesAsTools(Transform):\n    \"\"\"Transform that adds tools for listing and reading resources.\n\n    Generates two tools:\n    - `list_resources`: Lists all resources and templates\n    - `read_resource`: Reads a resource by URI\n\n    The generated tools route through the server at runtime, so auth,\n    middleware, and visibility apply automatically.\n\n    This transform should be applied to a FastMCP server instance, not\n    a raw Provider, because the generated tools need the server's\n    middleware chain for auth and visibility filtering.\n\n    Example:\n        ```python\n        mcp = FastMCP(\"Server\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n        # Now has list_resources and read_resource tools\n        ```\n    \"\"\"\n\n    def __init__(self, provider: Provider) -> None:\n        from fastmcp.server.server import FastMCP\n\n        if not isinstance(provider, FastMCP):\n            raise TypeError(\n                \"ResourcesAsTools requires a FastMCP server instance, not a\"\n                f\" {type(provider).__name__}. The generated tools route through\"\n                \" the server's middleware chain at runtime for auth and\"\n                \" visibility. Pass your FastMCP server: ResourcesAsTools(mcp)\"\n            )\n        self._provider = provider\n\n    def __repr__(self) -> str:\n        return f\"ResourcesAsTools({self._provider!r})\"\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Add resource tools to the tool list.\"\"\"\n        return [\n            *tools,\n            self._make_list_resources_tool(),\n            self._make_read_resource_tool(),\n        ]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get a tool by name, including generated resource tools.\"\"\"\n        if name == \"list_resources\":\n            return self._make_list_resources_tool()\n        if name == \"read_resource\":\n            return self._make_read_resource_tool()\n        return await call_next(name, version=version)\n\n    def _make_list_resources_tool(self) -> Tool:\n        \"\"\"Create the list_resources tool.\"\"\"\n\n        async def list_resources() -> str:\n            \"\"\"List all available resources and resource templates.\n\n            Returns JSON with resource metadata. Static resources have a\n            'uri' field, while templates have a 'uri_template' field with\n            placeholders like {name}.\n            \"\"\"\n            ctx = get_context()\n            resources = await ctx.fastmcp.list_resources()\n            templates = await ctx.fastmcp.list_resource_templates()\n\n            result: list[dict[str, Any]] = []\n\n            for r in resources:\n                result.append(\n                    {\n                        \"uri\": str(r.uri),\n                        \"name\": r.name,\n                        \"description\": r.description,\n                        \"mime_type\": r.mime_type,\n                    }\n                )\n\n            for t in templates:\n                result.append(\n                    {\n                        \"uri_template\": t.uri_template,\n                        \"name\": t.name,\n                        \"description\": t.description,\n                    }\n                )\n\n            return json.dumps(result, indent=2)\n\n        return Tool.from_function(fn=list_resources, annotations=_DEFAULT_ANNOTATIONS)\n\n    def _make_read_resource_tool(self) -> Tool:\n        \"\"\"Create the read_resource tool.\"\"\"\n\n        async def read_resource(\n            uri: Annotated[str, \"The URI of the resource to read\"],\n        ) -> str:\n            \"\"\"Read a resource by its URI.\n\n            For static resources, provide the exact URI. For templated\n            resources, provide the URI with template parameters filled in.\n\n            Returns the resource content as a string. Binary content is\n            base64-encoded.\n            \"\"\"\n            ctx = get_context()\n            result = await ctx.fastmcp.read_resource(uri)\n            return _format_result(result)\n\n        return Tool.from_function(fn=read_resource, annotations=_DEFAULT_ANNOTATIONS)\n\n\ndef _format_result(result: Any) -> str:\n    \"\"\"Format ResourceResult for tool output.\n\n    Single text content is returned as-is. Single binary content is\n    base64-encoded. Multiple contents are JSON-encoded.\n    \"\"\"\n    if len(result.contents) == 1:\n        content = result.contents[0].content\n        if isinstance(content, bytes):\n            return base64.b64encode(content).decode()\n        return content\n\n    return json.dumps(\n        [\n            {\n                \"content\": (\n                    c.content\n                    if isinstance(c.content, str)\n                    else base64.b64encode(c.content).decode()\n                ),\n                \"mime_type\": c.mime_type,\n            }\n            for c in result.contents\n        ]\n    )\n"
  },
  {
    "path": "src/fastmcp/server/transforms/search/__init__.py",
    "content": "\"\"\"Search transforms for tool discovery.\n\nSearch transforms collapse a large tool catalog into a search interface,\nletting LLMs discover tools on demand instead of seeing the full list.\n\nExample:\n    ```python\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms.search import RegexSearchTransform\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_transform(RegexSearchTransform())\n    # list_tools now returns only search_tools + call_tool\n    ```\n\"\"\"\n\nfrom fastmcp.server.transforms.search.base import (\n    SearchResultSerializer,\n    serialize_tools_for_output_json,\n    serialize_tools_for_output_markdown,\n)\nfrom fastmcp.server.transforms.search.bm25 import BM25SearchTransform\nfrom fastmcp.server.transforms.search.regex import RegexSearchTransform\n\n__all__ = [\n    \"BM25SearchTransform\",\n    \"RegexSearchTransform\",\n    \"SearchResultSerializer\",\n    \"serialize_tools_for_output_json\",\n    \"serialize_tools_for_output_markdown\",\n]\n"
  },
  {
    "path": "src/fastmcp/server/transforms/search/base.py",
    "content": "\"\"\"Base class for search transforms.\n\nSearch transforms replace ``list_tools()`` output with a small set of\nsynthetic tools — a search tool and a call-tool proxy — so LLMs can\ndiscover tools on demand instead of receiving the full catalog.\n\nAll concrete search transforms (``RegexSearchTransform``,\n``BM25SearchTransform``, etc.) inherit from ``BaseSearchTransform`` and\nimplement ``_make_search_tool()`` and ``_search()`` to provide their\nspecific search strategy.\n\nExample::\n\n    from fastmcp import FastMCP\n    from fastmcp.server.transforms.search import RegexSearchTransform\n\n    mcp = FastMCP(\"Server\")\n\n    @mcp.tool\n    def add(a: int, b: int) -> int: ...\n\n    @mcp.tool\n    def multiply(x: float, y: float) -> float: ...\n\n    # Clients now see only ``search_tools`` and ``call_tool``.\n    # The original tools are discoverable via search.\n    mcp.add_transform(RegexSearchTransform())\n\"\"\"\n\nfrom abc import abstractmethod\nfrom collections.abc import Awaitable, Callable, Sequence\nfrom typing import Annotated, Any\n\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.transforms import GetToolNext\nfrom fastmcp.server.transforms.catalog import CatalogTransform\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.versions import VersionSpec\n\n\ndef _extract_searchable_text(tool: Tool) -> str:\n    \"\"\"Combine tool name, description, and parameter info into searchable text.\"\"\"\n    parts = [tool.name]\n    if tool.description:\n        parts.append(tool.description)\n\n    schema = tool.parameters\n    if schema:\n        properties = schema.get(\"properties\", {})\n        for param_name, param_info in properties.items():\n            parts.append(param_name)\n            if isinstance(param_info, dict):\n                desc = param_info.get(\"description\", \"\")\n                if desc:\n                    parts.append(desc)\n\n    return \" \".join(parts)\n\n\ndef serialize_tools_for_output_json(tools: Sequence[Tool]) -> list[dict[str, Any]]:\n    \"\"\"Serialize tools to the same dict format as ``list_tools`` output.\"\"\"\n    return [\n        tool.to_mcp_tool().model_dump(mode=\"json\", exclude_none=True) for tool in tools\n    ]\n\n\nSearchResultSerializer = Callable[[Sequence[Tool]], Any | Awaitable[Any]]\n\n\nasync def _invoke_serializer(\n    serializer: SearchResultSerializer, tools: Sequence[Tool]\n) -> Any:\n    \"\"\"Call a serializer and await the result if it returns a coroutine.\"\"\"\n    result = serializer(tools)\n    if isinstance(result, Awaitable):\n        return await result\n    return result\n\n\ndef _union_type(branches: list[Any]) -> str:\n    branch_types = list(dict.fromkeys(_schema_type(b) for b in branches))\n    if \"null\" not in branch_types:\n        return \" | \".join(branch_types) if branch_types else \"any\"\n    non_null = [b for b in branch_types if b != \"null\"]\n    if not non_null:\n        return \"null\"\n    return f\"{' | '.join(non_null)}?\"\n\n\ndef _schema_type(schema: Any) -> str:\n    # Intentionally heuristic: the goal is a concise readable label, not a\n    # complete type system. Malformed schemas (e.g. {\"type\": \"\"}) → \"any\".\n    if not isinstance(schema, dict):\n        return \"any\"\n    t = schema.get(\"type\")\n    if isinstance(t, str) and t:\n        if t == \"array\":\n            return f\"{_schema_type(schema.get('items'))}[]\"\n        if t == \"null\":\n            return \"null\"\n        return t\n    if \"$ref\" in schema:\n        return \"object\"\n    if \"anyOf\" in schema:\n        return _union_type(schema[\"anyOf\"])\n    if \"oneOf\" in schema:\n        return _union_type(schema[\"oneOf\"])\n    if \"allOf\" in schema:\n        # allOf = intersection / Pydantic composed model → always an object\n        return \"object\"\n    return \"object\" if \"properties\" in schema else \"any\"\n\n\ndef _schema_section(schema: dict[str, Any] | None, title: str) -> list[str]:\n    lines = [f\"**{title}**\"]\n    if not isinstance(schema, dict):\n        lines.append(\"- `value` (any)\")\n        return lines\n\n    props = schema.get(\"properties\")\n    raw_required = schema.get(\"required\")\n    req = set(raw_required) if isinstance(raw_required, list) else set()\n    if props is None:\n        # Not a properties-based schema — treat as a single unnamed value.\n        lines.append(f\"- `value` ({_schema_type(schema)})\")\n        return lines\n    if not props:\n        # Object schema with no properties — zero-argument tool.\n        lines.append(\"*(no parameters)*\")\n        return lines\n\n    for name, field in props.items():\n        required = \", required\" if name in req else \"\"\n        lines.append(f\"- `{name}` ({_schema_type(field)}{required})\")\n    return lines\n\n\ndef serialize_tools_for_output_markdown(tools: Sequence[Tool]) -> str:\n    \"\"\"Serialize tools to compact markdown, using ~65-70% fewer tokens than JSON.\"\"\"\n    if not tools:\n        return \"No tools matched the query.\"\n    blocks: list[str] = []\n    for tool in tools:\n        lines = [f\"### {tool.name}\"]\n        if tool.description:\n            lines.extend([\"\", tool.description.strip()])\n        lines.extend([\"\", *_schema_section(tool.parameters, \"Parameters\")])\n        if tool.output_schema is not None:\n            lines.extend([\"\", *_schema_section(tool.output_schema, \"Returns\")])\n        blocks.append(\"\\n\".join(lines))\n    return \"\\n\\n\".join(blocks)\n\n\nclass BaseSearchTransform(CatalogTransform):\n    \"\"\"Replace the tool listing with a search interface.\n\n    When this transform is active, ``list_tools()`` returns only:\n\n    * Any tools listed in ``always_visible`` (pinned).\n    * A **search tool** that finds tools matching a query.\n    * A **call_tool** proxy that executes tools discovered via search.\n\n    Hidden tools remain callable — ``get_tool()`` delegates unknown\n    names downstream, so direct calls and the call-tool proxy both work.\n\n    Search results respect the full auth pipeline: middleware, visibility\n    transforms, and component-level auth checks all apply.\n\n    Args:\n        max_results: Maximum number of tools returned per search.\n        always_visible: Tool names that stay in the ``list_tools``\n            output alongside the synthetic search/call tools.\n        search_tool_name: Name of the generated search tool.\n        call_tool_name: Name of the generated call-tool proxy.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        max_results: int = 5,\n        always_visible: list[str] | None = None,\n        search_tool_name: str = \"search_tools\",\n        call_tool_name: str = \"call_tool\",\n        search_result_serializer: SearchResultSerializer | None = None,\n    ) -> None:\n        super().__init__()\n        self._max_results = max_results\n        self._always_visible = set(always_visible or [])\n        self._search_tool_name = search_tool_name\n        self._call_tool_name = call_tool_name\n        self._search_result_serializer: SearchResultSerializer = (\n            search_result_serializer or serialize_tools_for_output_json\n        )\n\n    # ------------------------------------------------------------------\n    # Transform interface\n    # ------------------------------------------------------------------\n\n    async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Replace the catalog with pinned + synthetic search/call tools.\"\"\"\n        pinned = [t for t in tools if t.name in self._always_visible]\n        return [*pinned, self._make_search_tool(), self._make_call_tool()]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Intercept synthetic tool names; delegate everything else.\"\"\"\n        if name == self._search_tool_name:\n            return self._make_search_tool()\n        if name == self._call_tool_name:\n            return self._make_call_tool()\n        return await call_next(name, version=version)\n\n    # ------------------------------------------------------------------\n    # Synthetic tools\n    # ------------------------------------------------------------------\n\n    @abstractmethod\n    def _make_search_tool(self) -> Tool:\n        \"\"\"Create the search tool. Subclasses define the parameter schema.\"\"\"\n        ...\n\n    def _make_call_tool(self) -> Tool:\n        \"\"\"Create the call_tool proxy that executes discovered tools.\"\"\"\n        transform = self\n\n        async def call_tool(\n            name: Annotated[str, \"The name of the tool to call\"],\n            arguments: Annotated[\n                dict[str, Any] | None, \"Arguments to pass to the tool\"\n            ] = None,\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> ToolResult:\n            \"\"\"Call a tool by name with the given arguments.\n\n            Use this to execute tools discovered via search_tools.\n            \"\"\"\n            if name in {transform._call_tool_name, transform._search_tool_name}:\n                raise ValueError(\n                    f\"'{name}' is a synthetic search tool and cannot be called via the call_tool proxy\"\n                )\n            return await ctx.fastmcp.call_tool(name, arguments)\n\n        return Tool.from_function(fn=call_tool, name=self._call_tool_name)\n\n    # ------------------------------------------------------------------\n    # Serialization\n    # ------------------------------------------------------------------\n\n    async def _render_results(self, tools: Sequence[Tool]) -> Any:\n        return await _invoke_serializer(self._search_result_serializer, tools)\n\n    # ------------------------------------------------------------------\n    # Catalog access\n    # ------------------------------------------------------------------\n\n    async def _get_visible_tools(self, ctx: Context) -> Sequence[Tool]:\n        \"\"\"Get the auth-filtered tool catalog, excluding pinned tools.\"\"\"\n        tools = await self.get_tool_catalog(ctx)\n        return [t for t in tools if t.name not in self._always_visible]\n\n    # ------------------------------------------------------------------\n    # Abstract search\n    # ------------------------------------------------------------------\n\n    @abstractmethod\n    async def _search(self, tools: Sequence[Tool], query: str) -> Sequence[Tool]:\n        \"\"\"Search the given tools and return matches.\"\"\"\n        ...\n"
  },
  {
    "path": "src/fastmcp/server/transforms/search/bm25.py",
    "content": "\"\"\"BM25-based search transform.\"\"\"\n\nimport hashlib\nimport math\nimport re\nfrom collections.abc import Sequence\nfrom typing import Annotated, Any\n\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.transforms.search.base import (\n    BaseSearchTransform,\n    SearchResultSerializer,\n    _extract_searchable_text,\n)\nfrom fastmcp.tools.base import Tool\n\n\ndef _tokenize(text: str) -> list[str]:\n    \"\"\"Lowercase, split on non-alphanumeric, filter short tokens.\"\"\"\n    return [t for t in re.split(r\"[^a-z0-9]+\", text.lower()) if len(t) > 1]\n\n\nclass _BM25Index:\n    \"\"\"Self-contained BM25 Okapi index.\"\"\"\n\n    def __init__(self, k1: float = 1.5, b: float = 0.75) -> None:\n        self.k1 = k1\n        self.b = b\n        self._doc_tokens: list[list[str]] = []\n        self._doc_lengths: list[int] = []\n        self._avg_dl: float = 0.0\n        self._df: dict[str, int] = {}\n        self._tf: list[dict[str, int]] = []\n        self._n: int = 0\n\n    def build(self, documents: list[str]) -> None:\n        self._doc_tokens = [_tokenize(doc) for doc in documents]\n        self._doc_lengths = [len(tokens) for tokens in self._doc_tokens]\n        self._n = len(documents)\n        self._avg_dl = sum(self._doc_lengths) / self._n if self._n else 0.0\n\n        self._df = {}\n        self._tf = []\n        for tokens in self._doc_tokens:\n            tf: dict[str, int] = {}\n            seen: set[str] = set()\n            for token in tokens:\n                tf[token] = tf.get(token, 0) + 1\n                if token not in seen:\n                    self._df[token] = self._df.get(token, 0) + 1\n                    seen.add(token)\n            self._tf.append(tf)\n\n    def query(self, text: str, top_k: int) -> list[int]:\n        \"\"\"Return indices of top_k documents sorted by BM25 score.\"\"\"\n        query_tokens = _tokenize(text)\n        if not query_tokens or not self._n:\n            return []\n\n        scores: list[float] = [0.0] * self._n\n        for token in query_tokens:\n            if token not in self._df:\n                continue\n            idf = math.log(\n                (self._n - self._df[token] + 0.5) / (self._df[token] + 0.5) + 1.0\n            )\n            for i in range(self._n):\n                tf = self._tf[i].get(token, 0)\n                if tf == 0:\n                    continue\n                dl = self._doc_lengths[i]\n                numerator = tf * (self.k1 + 1)\n                denominator = tf + self.k1 * (1 - self.b + self.b * dl / self._avg_dl)\n                scores[i] += idf * numerator / denominator\n\n        ranked = sorted(range(self._n), key=lambda i: scores[i], reverse=True)\n        return [i for i in ranked[:top_k] if scores[i] > 0]\n\n\ndef _catalog_hash(tools: Sequence[Tool]) -> str:\n    \"\"\"SHA256 hash of sorted tool searchable text for staleness detection.\"\"\"\n    key = \"|\".join(sorted(_extract_searchable_text(t) for t in tools))\n    return hashlib.sha256(key.encode()).hexdigest()\n\n\nclass BM25SearchTransform(BaseSearchTransform):\n    \"\"\"Search transform using BM25 Okapi relevance ranking.\n\n    Maintains an in-memory index that is lazily rebuilt when the tool\n    catalog changes (detected via a hash of tool names).\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        max_results: int = 5,\n        always_visible: list[str] | None = None,\n        search_tool_name: str = \"search_tools\",\n        call_tool_name: str = \"call_tool\",\n        search_result_serializer: SearchResultSerializer | None = None,\n    ) -> None:\n        super().__init__(\n            max_results=max_results,\n            always_visible=always_visible,\n            search_tool_name=search_tool_name,\n            call_tool_name=call_tool_name,\n            search_result_serializer=search_result_serializer,\n        )\n        self._index = _BM25Index()\n        self._indexed_tools: Sequence[Tool] = ()\n        self._last_hash: str = \"\"\n\n    def _make_search_tool(self) -> Tool:\n        transform = self\n\n        async def search_tools(\n            query: Annotated[str, \"Natural language query to search for tools\"],\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str | list[dict[str, Any]]:\n            \"\"\"Search for tools using natural language.\n\n            Returns matching tool definitions ranked by relevance,\n            in the same format as list_tools.\n            \"\"\"\n            hidden = await transform._get_visible_tools(ctx)\n            results = await transform._search(hidden, query)\n            return await transform._render_results(results)\n\n        return Tool.from_function(fn=search_tools, name=self._search_tool_name)\n\n    async def _search(self, tools: Sequence[Tool], query: str) -> Sequence[Tool]:\n        current_hash = _catalog_hash(tools)\n        if current_hash != self._last_hash:\n            documents = [_extract_searchable_text(t) for t in tools]\n            new_index = _BM25Index(self._index.k1, self._index.b)\n            new_index.build(documents)\n            self._index, self._indexed_tools, self._last_hash = (\n                new_index,\n                tools,\n                current_hash,\n            )\n\n        indices = self._index.query(query, self._max_results)\n        return [self._indexed_tools[i] for i in indices]\n"
  },
  {
    "path": "src/fastmcp/server/transforms/search/regex.py",
    "content": "\"\"\"Regex-based search transform.\"\"\"\n\nimport re\nfrom collections.abc import Sequence\nfrom typing import Annotated, Any\n\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.transforms.search.base import (\n    BaseSearchTransform,\n    _extract_searchable_text,\n)\nfrom fastmcp.tools.base import Tool\n\n\nclass RegexSearchTransform(BaseSearchTransform):\n    \"\"\"Search transform using regex pattern matching.\n\n    Tools are matched against their name, description, and parameter\n    information using ``re.search`` with ``re.IGNORECASE``.\n    \"\"\"\n\n    def _make_search_tool(self) -> Tool:\n        transform = self\n\n        async def search_tools(\n            pattern: Annotated[\n                str,\n                \"Regex pattern to match against tool names, descriptions, and parameters\",\n            ],\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str | list[dict[str, Any]]:\n            \"\"\"Search for tools matching a regex pattern.\n\n            Returns matching tool definitions in the same format as list_tools.\n            \"\"\"\n            hidden = await transform._get_visible_tools(ctx)\n            results = await transform._search(hidden, pattern)\n            return await transform._render_results(results)\n\n        return Tool.from_function(fn=search_tools, name=self._search_tool_name)\n\n    async def _search(self, tools: Sequence[Tool], query: str) -> Sequence[Tool]:\n        try:\n            compiled = re.compile(query, re.IGNORECASE)\n        except re.error:\n            return []\n\n        matches: list[Tool] = []\n        for tool in tools:\n            text = _extract_searchable_text(tool)\n            if compiled.search(text):\n                matches.append(tool)\n                if len(matches) >= self._max_results:\n                    break\n        return matches\n"
  },
  {
    "path": "src/fastmcp/server/transforms/tool_transform.py",
    "content": "\"\"\"Transform for applying tool transformations.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.server.transforms import GetToolNext, Transform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.tools.base import Tool\n\n\nclass ToolTransform(Transform):\n    \"\"\"Applies tool transformations to modify tool schemas.\n\n    Wraps ToolTransformConfig to apply argument renames, schema changes,\n    hidden arguments, and other transformations at the transform level.\n\n    Example:\n        ```python\n        transform = ToolTransform({\n            \"my_tool\": ToolTransformConfig(\n                name=\"renamed_tool\",\n                arguments={\"old_arg\": ArgTransformConfig(name=\"new_arg\")}\n            )\n        })\n        ```\n    \"\"\"\n\n    def __init__(self, transforms: dict[str, ToolTransformConfig]) -> None:\n        \"\"\"Initialize ToolTransform.\n\n        Args:\n            transforms: Map of original tool name → transform config.\n        \"\"\"\n        self._transforms = transforms\n\n        # Build reverse mapping: final_name → original_name\n        self._name_reverse: dict[str, str] = {}\n        for original_name, config in transforms.items():\n            final_name = config.name if config.name else original_name\n            self._name_reverse[final_name] = original_name\n\n        # Validate no duplicate target names\n        seen_targets: dict[str, str] = {}\n        for original_name, config in transforms.items():\n            target = config.name if config.name else original_name\n            if target in seen_targets:\n                raise ValueError(\n                    f\"ToolTransform has duplicate target name {target!r}: \"\n                    f\"both {seen_targets[target]!r} and {original_name!r} map to it\"\n                )\n            seen_targets[target] = original_name\n\n    def __repr__(self) -> str:\n        names = list(self._transforms.keys())\n        if len(names) <= 3:\n            return f\"ToolTransform({names!r})\"\n        return f\"ToolTransform({names[:3]!r}... +{len(names) - 3} more)\"\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Apply transforms to matching tools.\"\"\"\n        result: list[Tool] = []\n        for tool in tools:\n            if tool.name in self._transforms:\n                transformed = self._transforms[tool.name].apply(tool)\n                result.append(transformed)\n            else:\n                result.append(tool)\n        return result\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Get tool by transformed name.\"\"\"\n        # Check if this name is a transformed name\n        original_name = self._name_reverse.get(name, name)\n\n        # Get the original tool\n        tool = await call_next(original_name, version=version)\n        if tool is None:\n            return None\n\n        # Apply transform if applicable\n        if original_name in self._transforms:\n            transformed = self._transforms[original_name].apply(tool)\n            # Only return if requested name matches transformed name\n            if transformed.name == name:\n                return transformed\n            return None\n\n        # No transform, return as-is only if name matches\n        return tool if tool.name == name else None\n"
  },
  {
    "path": "src/fastmcp/server/transforms/version_filter.py",
    "content": "\"\"\"Version filter transform for filtering components by version range.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING\n\nfrom fastmcp.server.transforms import (\n    GetPromptNext,\n    GetResourceNext,\n    GetResourceTemplateNext,\n    GetToolNext,\n    Transform,\n)\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.resources.base import Resource\n    from fastmcp.resources.template import ResourceTemplate\n    from fastmcp.tools.base import Tool\n\n\nclass VersionFilter(Transform):\n    \"\"\"Filters components by version range.\n\n    When applied to a provider or server, components within the version range\n    are visible, and unversioned components are included by default. Within\n    that filtered set, the highest version of each component is exposed to\n    clients (standard deduplication behavior). Set\n    ``include_unversioned=False`` to exclude unversioned components.\n\n    Parameters mirror comparison operators for clarity:\n\n        # Versions < 3.0 (v1 and v2)\n        server.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n        # Versions >= 2.0 and < 3.0 (only v2.x)\n        server.add_transform(VersionFilter(version_gte=\"2.0\", version_lt=\"3.0\"))\n\n    Works with any version string - PEP 440 (1.0, 2.0) or dates (2025-01-01).\n\n    Args:\n        version_gte: Versions >= this value pass through.\n        version_lt: Versions < this value pass through.\n        include_unversioned: Whether unversioned components (``version=None``)\n            should pass through the filter. Defaults to True.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        version_gte: str | None = None,\n        version_lt: str | None = None,\n        include_unversioned: bool = True,\n    ) -> None:\n        if version_gte is None and version_lt is None:\n            raise ValueError(\n                \"At least one of version_gte or version_lt must be specified\"\n            )\n        self.version_gte = version_gte\n        self.version_lt = version_lt\n        self.include_unversioned = include_unversioned\n        self._spec = VersionSpec(gte=version_gte, lt=version_lt)\n\n    def __repr__(self) -> str:\n        parts = []\n        if self.version_gte:\n            parts.append(f\"version_gte={self.version_gte!r}\")\n        if self.version_lt:\n            parts.append(f\"version_lt={self.version_lt!r}\")\n        if not self.include_unversioned:\n            parts.append(\"include_unversioned=False\")\n        return f\"VersionFilter({', '.join(parts)})\"\n\n    # -------------------------------------------------------------------------\n    # Tools\n    # -------------------------------------------------------------------------\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        return [\n            t\n            for t in tools\n            if self._spec.matches(t.version, match_none=self.include_unversioned)\n        ]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        return await call_next(name, version=self._spec.intersect(version))\n\n    # -------------------------------------------------------------------------\n    # Resources\n    # -------------------------------------------------------------------------\n\n    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:\n        return [\n            r\n            for r in resources\n            if self._spec.matches(r.version, match_none=self.include_unversioned)\n        ]\n\n    async def get_resource(\n        self,\n        uri: str,\n        call_next: GetResourceNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> Resource | None:\n        return await call_next(uri, version=self._spec.intersect(version))\n\n    # -------------------------------------------------------------------------\n    # Resource Templates\n    # -------------------------------------------------------------------------\n\n    async def list_resource_templates(\n        self, templates: Sequence[ResourceTemplate]\n    ) -> Sequence[ResourceTemplate]:\n        return [\n            t\n            for t in templates\n            if self._spec.matches(t.version, match_none=self.include_unversioned)\n        ]\n\n    async def get_resource_template(\n        self,\n        uri: str,\n        call_next: GetResourceTemplateNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> ResourceTemplate | None:\n        return await call_next(uri, version=self._spec.intersect(version))\n\n    # -------------------------------------------------------------------------\n    # Prompts\n    # -------------------------------------------------------------------------\n\n    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:\n        return [\n            p\n            for p in prompts\n            if self._spec.matches(p.version, match_none=self.include_unversioned)\n        ]\n\n    async def get_prompt(\n        self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        return await call_next(name, version=self._spec.intersect(version))\n"
  },
  {
    "path": "src/fastmcp/server/transforms/visibility.py",
    "content": "\"\"\"Visibility transform for marking component visibility state.\n\nEach Visibility instance marks components via internal metadata. Multiple\nvisibility transforms can be stacked - later transforms override earlier ones.\nFinal filtering happens at the Provider level.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING, Any, Literal, TypeVar\n\nimport mcp.types\n\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.transforms import (\n    GetPromptNext,\n    GetResourceNext,\n    GetResourceTemplateNext,\n    GetToolNext,\n    Transform,\n)\nfrom fastmcp.utilities.versions import VersionSpec\n\nif TYPE_CHECKING:\n    from fastmcp.prompts.base import Prompt\n    from fastmcp.server.context import Context\n    from fastmcp.tools.base import Tool\n    from fastmcp.utilities.components import FastMCPComponent\n\nT = TypeVar(\"T\", bound=\"FastMCPComponent\")\n\n# Visibility state stored at meta[\"fastmcp\"][\"_internal\"][\"visibility\"]\n_FASTMCP_KEY = \"fastmcp\"\n_INTERNAL_KEY = \"_internal\"\n\n\nclass Visibility(Transform):\n    \"\"\"Sets visibility state on matching components.\n\n    Does NOT filter inline - just marks components with visibility state.\n    Later transforms in the chain can override earlier marks.\n    Final filtering happens at the Provider level after all transforms run.\n\n    Example:\n        ```python\n        # Disable components tagged \"internal\"\n        Visibility(False, tags={\"internal\"})\n\n        # Re-enable specific tool (override earlier disable)\n        Visibility(True, names={\"safe_tool\"})\n\n        # Allowlist via composition:\n        Visibility(False, match_all=True)  # disable everything\n        Visibility(True, tags={\"public\"})  # enable public\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        enabled: bool,\n        *,\n        names: set[str] | None = None,\n        keys: set[str] | None = None,\n        version: VersionSpec | None = None,\n        tags: set[str] | None = None,\n        components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]]\n        | None = None,\n        match_all: bool = False,\n    ) -> None:\n        \"\"\"Initialize a visibility marker.\n\n        Args:\n            enabled: If True, mark matching as enabled; if False, mark as disabled.\n            names: Component names or URIs to match.\n            keys: Component keys to match (e.g., {\"tool:my_tool@v1\"}).\n            version: Component version spec to match. Unversioned components (version=None)\n                will NOT match a version spec.\n            tags: Tags to match (component must have at least one).\n            components: Component types to match (e.g., {\"tool\", \"prompt\"}).\n            match_all: If True, matches all components regardless of other criteria.\n        \"\"\"\n        self._enabled = enabled\n        self.names = names\n        self.keys = keys\n        self.version = version\n        self.tags = tags  # e.g., {\"internal\", \"deprecated\"}\n        self.components = components  # e.g., {\"tool\", \"prompt\"}\n        self.match_all = match_all\n\n    def __repr__(self) -> str:\n        action = \"enable\" if self._enabled else \"disable\"\n        if self.match_all:\n            return f\"Visibility({self._enabled}, match_all=True)\"\n        parts = []\n        if self.names:\n            parts.append(f\"names={set(self.names)}\")\n        if self.keys:\n            parts.append(f\"keys={set(self.keys)}\")\n        if self.version:\n            parts.append(f\"version={self.version!r}\")\n        if self.components:\n            parts.append(f\"components={set(self.components)}\")\n        if self.tags:\n            parts.append(f\"tags={set(self.tags)}\")\n        if parts:\n            return f\"Visibility({action}, {', '.join(parts)})\"\n        return f\"Visibility({action})\"\n\n    def _matches(self, component: FastMCPComponent) -> bool:\n        \"\"\"Check if this transform applies to the component.\n\n        All specified criteria must match (intersection semantics).\n        An empty rule (no criteria) matches nothing.\n        Use match_all=True to match everything.\n\n        Args:\n            component: Component to check.\n\n        Returns:\n            True if this transform should mark the component.\n        \"\"\"\n        # Match-all flag matches everything\n        if self.match_all:\n            return True\n\n        # Empty criteria matches nothing (safe default)\n        if (\n            self.names is None\n            and self.keys is None\n            and self.version is None\n            and self.components is None\n            and self.tags is None\n        ):\n            return False\n\n        # Check component type if specified\n        if self.components is not None:\n            component_type = component.key.split(\":\")[\n                0\n            ]  # e.g., \"tool\" from \"tool:foo@\"\n            if component_type not in self.components:\n                return False\n\n        # Check keys if specified (exact match only)\n        if self.keys is not None:\n            if component.key not in self.keys:\n                return False\n\n        # Check names if specified\n        if self.names is not None:\n            # For resources, also check URI; for templates, check uri_template\n            matches_name = component.name in self.names\n            matches_uri = False\n            if isinstance(component, Resource):\n                matches_uri = str(component.uri) in self.names\n            elif isinstance(component, ResourceTemplate):\n                matches_uri = component.uri_template in self.names\n            if not (matches_name or matches_uri):\n                return False\n\n        # Check version if specified\n        # Note: match_none=False means unversioned components don't match a version spec\n        if self.version is not None and not self.version.matches(\n            component.version, match_none=False\n        ):\n            return False\n\n        # Check tags if specified (component must have at least one matching tag)\n        return self.tags is None or bool(component.tags & self.tags)\n\n    def _mark_component(self, component: T) -> T:\n        \"\"\"Set visibility state in component metadata if rule matches.\n\n        Returns a copy of the component with updated metadata to avoid\n        mutating shared objects cached in providers.\n        \"\"\"\n        if not self._matches(component):\n            return component\n\n        if component.meta is None:\n            new_meta = {_FASTMCP_KEY: {_INTERNAL_KEY: {\"visibility\": self._enabled}}}\n        else:\n            old_fastmcp = component.meta.get(_FASTMCP_KEY, {})\n            old_internal = old_fastmcp.get(_INTERNAL_KEY, {})\n            new_internal = {**old_internal, \"visibility\": self._enabled}\n            new_fastmcp = {**old_fastmcp, _INTERNAL_KEY: new_internal}\n            new_meta = {**component.meta, _FASTMCP_KEY: new_fastmcp}\n        return component.model_copy(update={\"meta\": new_meta})\n\n    # -------------------------------------------------------------------------\n    # Transform methods (mark components, don't filter)\n    # -------------------------------------------------------------------------\n\n    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        \"\"\"Mark tools by visibility state.\"\"\"\n        return [self._mark_component(t) for t in tools]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        \"\"\"Mark tool if found.\"\"\"\n        tool = await call_next(name, version=version)\n        if tool is None:\n            return None\n        return self._mark_component(tool)\n\n    # -------------------------------------------------------------------------\n    # Resources\n    # -------------------------------------------------------------------------\n\n    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:\n        \"\"\"Mark resources by visibility state.\"\"\"\n        return [self._mark_component(r) for r in resources]\n\n    async def get_resource(\n        self,\n        uri: str,\n        call_next: GetResourceNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> Resource | None:\n        \"\"\"Mark resource if found.\"\"\"\n        resource = await call_next(uri, version=version)\n        if resource is None:\n            return None\n        return self._mark_component(resource)\n\n    # -------------------------------------------------------------------------\n    # Resource Templates\n    # -------------------------------------------------------------------------\n\n    async def list_resource_templates(\n        self, templates: Sequence[ResourceTemplate]\n    ) -> Sequence[ResourceTemplate]:\n        \"\"\"Mark resource templates by visibility state.\"\"\"\n        return [self._mark_component(t) for t in templates]\n\n    async def get_resource_template(\n        self,\n        uri: str,\n        call_next: GetResourceTemplateNext,\n        *,\n        version: VersionSpec | None = None,\n    ) -> ResourceTemplate | None:\n        \"\"\"Mark resource template if found.\"\"\"\n        template = await call_next(uri, version=version)\n        if template is None:\n            return None\n        return self._mark_component(template)\n\n    # -------------------------------------------------------------------------\n    # Prompts\n    # -------------------------------------------------------------------------\n\n    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:\n        \"\"\"Mark prompts by visibility state.\"\"\"\n        return [self._mark_component(p) for p in prompts]\n\n    async def get_prompt(\n        self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None\n    ) -> Prompt | None:\n        \"\"\"Mark prompt if found.\"\"\"\n        prompt = await call_next(name, version=version)\n        if prompt is None:\n            return None\n        return self._mark_component(prompt)\n\n\ndef is_enabled(component: FastMCPComponent) -> bool:\n    \"\"\"Check if component is enabled.\n\n    Returns True if:\n    - No visibility mark exists (default is enabled)\n    - Visibility mark is True\n\n    Returns False if visibility mark is False.\n\n    Args:\n        component: Component to check.\n\n    Returns:\n        True if component should be enabled/visible to clients.\n    \"\"\"\n    meta = component.meta or {}\n    fastmcp = meta.get(_FASTMCP_KEY, {})\n    internal = fastmcp.get(_INTERNAL_KEY, {})\n    return internal.get(\"visibility\", True)  # Default True if not set\n\n\n# -------------------------------------------------------------------------\n# Session visibility control\n# -------------------------------------------------------------------------\n\nif TYPE_CHECKING:\n    from fastmcp.server.context import Context\n\n\nasync def get_visibility_rules(context: Context) -> list[dict[str, Any]]:\n    \"\"\"Load visibility rule dicts from session state.\"\"\"\n    return await context.get_state(\"_visibility_rules\") or []\n\n\nasync def save_visibility_rules(\n    context: Context,\n    rules: list[dict[str, Any]],\n    *,\n    components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]] | None = None,\n) -> None:\n    \"\"\"Save visibility rule dicts to session state and send notifications.\n\n    Args:\n        context: The context to save rules for.\n        rules: The visibility rules to save.\n        components: Optional hint about which component types are affected.\n            If None, sends notifications for all types (safe default).\n            If provided, only sends notifications for specified types.\n    \"\"\"\n    await context.set_state(\"_visibility_rules\", rules)\n\n    # Send notifications based on components hint\n    # Note: MCP has no separate template notification - templates use ResourceListChangedNotification\n    if components is None or \"tool\" in components:\n        await context.send_notification(mcp.types.ToolListChangedNotification())\n    if components is None or \"resource\" in components or \"template\" in components:\n        await context.send_notification(mcp.types.ResourceListChangedNotification())\n    if components is None or \"prompt\" in components:\n        await context.send_notification(mcp.types.PromptListChangedNotification())\n\n\ndef create_visibility_transforms(rules: list[dict[str, Any]]) -> list[Visibility]:\n    \"\"\"Convert rule dicts to Visibility transforms.\"\"\"\n    transforms = []\n    for params in rules:\n        version = None\n        if params.get(\"version\"):\n            version_dict = params[\"version\"]\n            version = VersionSpec(\n                gte=version_dict.get(\"gte\"),\n                lt=version_dict.get(\"lt\"),\n                eq=version_dict.get(\"eq\"),\n            )\n        transforms.append(\n            Visibility(\n                params[\"enabled\"],\n                names=set(params[\"names\"]) if params.get(\"names\") else None,\n                keys=set(params[\"keys\"]) if params.get(\"keys\") else None,\n                version=version,\n                tags=set(params[\"tags\"]) if params.get(\"tags\") else None,\n                components=(\n                    set(params[\"components\"]) if params.get(\"components\") else None\n                ),\n                match_all=params.get(\"match_all\", False),\n            )\n        )\n    return transforms\n\n\nasync def get_session_transforms(context: Context) -> list[Visibility]:\n    \"\"\"Get session-specific Visibility transforms from state store.\"\"\"\n    try:\n        # Will raise RuntimeError if no session available\n        _ = context.session_id\n    except RuntimeError:\n        return []\n\n    rules = await get_visibility_rules(context)\n    return create_visibility_transforms(rules)\n\n\nasync def enable_components(\n    context: Context,\n    *,\n    names: set[str] | None = None,\n    keys: set[str] | None = None,\n    version: VersionSpec | None = None,\n    tags: set[str] | None = None,\n    components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]] | None = None,\n    match_all: bool = False,\n) -> None:\n    \"\"\"Enable components matching criteria for this session only.\n\n    Session rules override global transforms. Rules accumulate - each call\n    adds a new rule to the session. Later marks override earlier ones\n    (Visibility transform semantics).\n\n    Sends notifications to this session only: ToolListChangedNotification,\n    ResourceListChangedNotification, and PromptListChangedNotification.\n\n    Args:\n        context: The context for this session.\n        names: Component names or URIs to match.\n        keys: Component keys to match (e.g., {\"tool:my_tool@v1\"}).\n        version: Component version spec to match.\n        tags: Tags to match (component must have at least one).\n        components: Component types to match (e.g., {\"tool\", \"prompt\"}).\n        match_all: If True, matches all components regardless of other criteria.\n    \"\"\"\n    # Normalize empty sets to None (empty = match all)\n    components = components if components else None\n\n    # Load current rules\n    rules = await get_visibility_rules(context)\n\n    # Create new rule dict\n    rule: dict[str, Any] = {\n        \"enabled\": True,\n        \"names\": list(names) if names else None,\n        \"keys\": list(keys) if keys else None,\n        \"version\": (\n            {\"gte\": version.gte, \"lt\": version.lt, \"eq\": version.eq}\n            if version\n            else None\n        ),\n        \"tags\": list(tags) if tags else None,\n        \"components\": list(components) if components else None,\n        \"match_all\": match_all,\n    }\n\n    # Add and save (notifications sent by save_visibility_rules)\n    rules.append(rule)\n    await save_visibility_rules(context, rules, components=components)\n\n\nasync def disable_components(\n    context: Context,\n    *,\n    names: set[str] | None = None,\n    keys: set[str] | None = None,\n    version: VersionSpec | None = None,\n    tags: set[str] | None = None,\n    components: set[Literal[\"tool\", \"resource\", \"template\", \"prompt\"]] | None = None,\n    match_all: bool = False,\n) -> None:\n    \"\"\"Disable components matching criteria for this session only.\n\n    Session rules override global transforms. Rules accumulate - each call\n    adds a new rule to the session. Later marks override earlier ones\n    (Visibility transform semantics).\n\n    Sends notifications to this session only: ToolListChangedNotification,\n    ResourceListChangedNotification, and PromptListChangedNotification.\n\n    Args:\n        context: The context for this session.\n        names: Component names or URIs to match.\n        keys: Component keys to match (e.g., {\"tool:my_tool@v1\"}).\n        version: Component version spec to match.\n        tags: Tags to match (component must have at least one).\n        components: Component types to match (e.g., {\"tool\", \"prompt\"}).\n        match_all: If True, matches all components regardless of other criteria.\n    \"\"\"\n    # Normalize empty sets to None (empty = match all)\n    components = components if components else None\n\n    # Load current rules\n    rules = await get_visibility_rules(context)\n\n    # Create new rule dict\n    rule: dict[str, Any] = {\n        \"enabled\": False,\n        \"names\": list(names) if names else None,\n        \"keys\": list(keys) if keys else None,\n        \"version\": (\n            {\"gte\": version.gte, \"lt\": version.lt, \"eq\": version.eq}\n            if version\n            else None\n        ),\n        \"tags\": list(tags) if tags else None,\n        \"components\": list(components) if components else None,\n        \"match_all\": match_all,\n    }\n\n    # Add and save (notifications sent by save_visibility_rules)\n    rules.append(rule)\n    await save_visibility_rules(context, rules, components=components)\n\n\nasync def reset_visibility(context: Context) -> None:\n    \"\"\"Clear all session visibility rules.\n\n    Use this to reset session visibility back to global defaults.\n\n    Sends notifications to this session only: ToolListChangedNotification,\n    ResourceListChangedNotification, and PromptListChangedNotification.\n\n    Args:\n        context: The context for this session.\n    \"\"\"\n    await save_visibility_rules(context, [])\n\n\nComponentT = TypeVar(\"ComponentT\", bound=\"FastMCPComponent\")\n\n\nasync def apply_session_transforms(\n    components: Sequence[ComponentT],\n) -> Sequence[ComponentT]:\n    \"\"\"Apply session-specific visibility transforms to components.\n\n    This helper applies session-level enable/disable rules by marking\n    components with their visibility state. Session transforms override\n    global transforms due to mark-based semantics (later marks win).\n\n    Args:\n        components: The components to apply session transforms to.\n\n    Returns:\n        The components with session transforms applied.\n    \"\"\"\n    from fastmcp.server.context import _current_context\n\n    current_ctx = _current_context.get()\n    if current_ctx is None:\n        return components\n\n    session_transforms = await get_session_transforms(current_ctx)\n    if not session_transforms:\n        return components\n\n    # Apply each transform's marking to each component\n    result = list(components)\n    for transform in session_transforms:\n        result = [transform._mark_component(c) for c in result]\n    return result\n"
  },
  {
    "path": "src/fastmcp/settings.py",
    "content": "from __future__ import annotations as _annotations\n\nimport inspect\nimport os\nfrom datetime import timedelta\nfrom pathlib import Path\nfrom typing import Annotated, Any, Literal\n\nfrom platformdirs import user_data_dir\nfrom pydantic import Field, field_validator\nfrom pydantic_settings import (\n    BaseSettings,\n    SettingsConfigDict,\n)\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nENV_FILE = os.getenv(\"FASTMCP_ENV_FILE\", \".env\")\n\nLOG_LEVEL = Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n\nMCP_LOG_LEVEL = Literal[\n    \"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\", \"alert\", \"emergency\"\n]\n\nDuplicateBehavior = Literal[\"warn\", \"error\", \"replace\", \"ignore\"]\n\nTEN_MB_IN_BYTES = 1024 * 1024 * 10\n\n\nclass DocketSettings(BaseSettings):\n    \"\"\"Docket worker configuration.\"\"\"\n\n    model_config = SettingsConfigDict(\n        env_prefix=\"FASTMCP_DOCKET_\",\n        extra=\"ignore\",\n    )\n\n    name: Annotated[\n        str,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Name for the Docket queue. All servers/workers sharing the same name\n                and backend URL will share a task queue.\n                \"\"\"\n            ),\n        ),\n    ] = \"fastmcp\"\n\n    url: Annotated[\n        str,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                URL for the Docket backend. Supports:\n                - memory:// - In-memory backend (single process only)\n                - redis://host:port/db - Redis/Valkey backend (distributed, multi-process)\n\n                Example: redis://localhost:6379/0\n\n                Default is memory:// for single-process scenarios. Use Redis or Valkey\n                when coordinating tasks across multiple processes (e.g., additional\n                workers via the fastmcp tasks CLI).\n                \"\"\"\n            ),\n        ),\n    ] = \"memory://\"\n\n    worker_name: Annotated[\n        str | None,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Name for the Docket worker. If None, Docket will auto-generate\n                a unique worker name.\n                \"\"\"\n            ),\n        ),\n    ] = None\n\n    concurrency: Annotated[\n        int,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Maximum number of tasks the worker can process concurrently.\n                \"\"\"\n            ),\n        ),\n    ] = 10\n\n    redelivery_timeout: Annotated[\n        timedelta,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Task redelivery timeout. If a worker doesn't complete\n                a task within this time, the task will be redelivered to another\n                worker.\n                \"\"\"\n            ),\n        ),\n    ] = timedelta(seconds=300)\n\n    reconnection_delay: Annotated[\n        timedelta,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Delay between reconnection attempts when the worker\n                loses connection to the Docket backend.\n                \"\"\"\n            ),\n        ),\n    ] = timedelta(seconds=5)\n\n    minimum_check_interval: Annotated[\n        timedelta,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                How frequently the worker polls for new tasks. Lower\n                values reduce latency for task pickup at the cost of\n                more CPU usage. The default of 50ms is a good balance;\n                increase for high-volume production deployments where\n                tasks are long-running.\n                \"\"\"\n            ),\n        ),\n    ] = timedelta(milliseconds=50)\n\n\nclass Settings(BaseSettings):\n    \"\"\"FastMCP settings.\"\"\"\n\n    model_config = SettingsConfigDict(\n        env_prefix=\"FASTMCP_\",\n        env_file=ENV_FILE,\n        extra=\"ignore\",\n        env_nested_delimiter=\"__\",\n        nested_model_default_partial_update=True,\n        validate_assignment=True,\n    )\n\n    def get_setting(self, attr: str) -> Any:\n        \"\"\"\n        Get a setting. If the setting contains one or more `__`, it will be\n        treated as a nested setting.\n        \"\"\"\n        settings = self\n        while \"__\" in attr:\n            parent_attr, attr = attr.split(\"__\", 1)\n            if not hasattr(settings, parent_attr):\n                raise AttributeError(f\"Setting {parent_attr} does not exist.\")\n            settings = getattr(settings, parent_attr)\n        return getattr(settings, attr)\n\n    def set_setting(self, attr: str, value: Any) -> None:\n        \"\"\"\n        Set a setting. If the setting contains one or more `__`, it will be\n        treated as a nested setting.\n        \"\"\"\n        settings = self\n        while \"__\" in attr:\n            parent_attr, attr = attr.split(\"__\", 1)\n            if not hasattr(settings, parent_attr):\n                raise AttributeError(f\"Setting {parent_attr} does not exist.\")\n            settings = getattr(settings, parent_attr)\n        setattr(settings, attr, value)\n\n    home: Path = Path(user_data_dir(\"fastmcp\", appauthor=False))\n\n    test_mode: bool = False\n\n    log_enabled: bool = True\n    log_level: LOG_LEVEL = \"INFO\"\n\n    @field_validator(\"log_level\", mode=\"before\")\n    @classmethod\n    def normalize_log_level(cls, v):\n        if isinstance(v, str):\n            return v.upper()\n        return v\n\n    docket: DocketSettings = DocketSettings()\n\n    enable_rich_logging: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                If True, will use rich formatting for log output. If False,\n                will use standard Python logging without rich formatting.\n                \"\"\"\n            )\n        ),\n    ] = True\n\n    enable_rich_tracebacks: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                If True, will use rich tracebacks for logging.\n                \"\"\"\n            )\n        ),\n    ] = True\n\n    deprecation_warnings: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Whether to show deprecation warnings. You can completely reset\n                Python's warning behavior by running `warnings.resetwarnings()`.\n                Note this will NOT apply to deprecation warnings from the\n                settings class itself.\n                \"\"\",\n            )\n        ),\n    ] = True\n\n    client_raise_first_exceptiongroup_error: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Many MCP components operate in anyio taskgroups, and raise\n                ExceptionGroups instead of exceptions. If this setting is True, FastMCP Clients\n                will `raise` the first error in any ExceptionGroup instead of raising\n                the ExceptionGroup as a whole. This is useful for debugging, but may\n                mask other errors.\n                \"\"\"\n            ),\n        ),\n    ] = True\n\n    client_init_timeout: Annotated[\n        float | None,\n        Field(\n            description=\"The timeout for the client's initialization handshake, in seconds. Set to None or 0 to disable.\",\n        ),\n    ] = None\n\n    client_disconnect_timeout: Annotated[\n        float,\n        Field(\n            description=\"Maximum time to wait for a clean disconnect before giving up, in seconds.\",\n        ),\n    ] = 5\n\n    # Transport settings\n    transport: Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"] = \"stdio\"\n\n    # HTTP settings\n    host: str = \"127.0.0.1\"\n    port: int = 8000\n    sse_path: str = \"/sse\"\n    message_path: str = \"/messages/\"\n    streamable_http_path: str = \"/mcp\"\n    debug: bool = False\n\n    # error handling\n    mask_error_details: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                If True, error details from user-supplied functions (tool, resource, prompt)\n                will be masked before being sent to clients. Only error messages from explicitly\n                raised ToolError, ResourceError, or PromptError will be included in responses.\n                If False (default), all error details will be included in responses, but prefixed\n                with appropriate context.\n                \"\"\"\n            ),\n        ),\n    ] = False\n\n    client_log_level: Annotated[\n        MCP_LOG_LEVEL | None,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Default minimum log level for messages sent to MCP clients.\n                When set, log messages below this level are suppressed.\n                Individual clients can override this per-session using the\n                MCP logging/setLevel request.\n                \"\"\"\n            ),\n        ),\n    ] = None\n\n    strict_input_validation: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                If True, tool inputs are strictly validated against the input\n                JSON schema. For example, providing the string \\\"10\\\" to an\n                integer field will raise an error. If False, compatible inputs\n                will be coerced to match the schema, which can increase\n                compatibility. For example, providing the string \\\"10\\\" to an\n                integer field will be coerced to 10. Defaults to False.\n                \"\"\"\n            ),\n        ),\n    ] = False\n\n    server_dependencies: list[str] = Field(\n        default_factory=list,\n        description=\"List of dependencies to install in the server environment\",\n    )\n\n    # StreamableHTTP settings\n    json_response: bool = False\n    stateless_http: bool = (\n        False  # If True, uses true stateless mode (new transport per request)\n    )\n\n    mounted_components_raise_on_load_error: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                If True, errors encountered when loading mounted components (tools, resources, prompts)\n                will be raised instead of logged as warnings. This is useful for debugging\n                but will interrupt normal operation.\n                \"\"\"\n            ),\n        ),\n    ] = False\n\n    show_server_banner: Annotated[\n        bool,\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                If True, the server banner will be displayed when running the server.\n                This setting can be overridden by the --no-banner CLI flag or by\n                passing show_banner=False to server.run().\n                Set to False via FASTMCP_SHOW_SERVER_BANNER=false to suppress the banner.\n                \"\"\"\n            ),\n        ),\n    ] = True\n\n    check_for_updates: Annotated[\n        Literal[\"stable\", \"prerelease\", \"off\"],\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Controls update checking when displaying the CLI banner.\n                - \"stable\": Check for stable releases only (default)\n                - \"prerelease\": Also check for pre-release versions (alpha, beta, rc)\n                - \"off\": Disable update checking entirely\n                Set via FASTMCP_CHECK_FOR_UPDATES environment variable.\n                \"\"\"\n            ),\n        ),\n    ] = \"stable\"\n\n    decorator_mode: Annotated[\n        Literal[\"function\", \"object\"],\n        Field(\n            description=inspect.cleandoc(\n                \"\"\"\n                Controls what decorators (@tool, @resource, @prompt) return.\n\n                - \"function\" (default): Decorators return the original function unchanged.\n                  The function remains callable and is registered with the server normally.\n                - \"object\" (deprecated): Decorators return component objects (FunctionTool,\n                  FunctionResource, FunctionPrompt). This was the default behavior in v2 and\n                  will be removed in a future version.\n                \"\"\"\n            ),\n        ),\n    ] = \"function\"\n"
  },
  {
    "path": "src/fastmcp/telemetry.py",
    "content": "\"\"\"OpenTelemetry instrumentation for FastMCP.\n\nThis module provides native OpenTelemetry integration for FastMCP servers and clients.\nIt uses only the opentelemetry-api package, so telemetry is a no-op unless the user\ninstalls an OpenTelemetry SDK and configures exporters.\n\nExample usage with SDK:\n    ```python\n    from opentelemetry import trace\n    from opentelemetry.sdk.trace import TracerProvider\n    from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor\n\n    # Configure the SDK (user responsibility)\n    provider = TracerProvider()\n    provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))\n    trace.set_tracer_provider(provider)\n\n    # Now FastMCP will emit traces\n    from fastmcp import FastMCP\n    mcp = FastMCP(\"my-server\")\n    ```\n\"\"\"\n\nfrom typing import Any\n\nfrom opentelemetry import context as otel_context\nfrom opentelemetry import propagate, trace\nfrom opentelemetry.context import Context\nfrom opentelemetry.trace import Span, Status, StatusCode, Tracer\nfrom opentelemetry.trace import get_tracer as otel_get_tracer\n\nINSTRUMENTATION_NAME = \"fastmcp\"\n\nTRACE_PARENT_KEY = \"traceparent\"\nTRACE_STATE_KEY = \"tracestate\"\n\n\ndef get_tracer(version: str | None = None) -> Tracer:\n    \"\"\"Get the FastMCP tracer for creating spans.\n\n    Args:\n        version: Optional version string for the instrumentation\n\n    Returns:\n        A tracer instance. Returns a no-op tracer if no SDK is configured.\n    \"\"\"\n    return otel_get_tracer(INSTRUMENTATION_NAME, version)\n\n\ndef inject_trace_context(\n    meta: dict[str, Any] | None = None,\n) -> dict[str, Any] | None:\n    \"\"\"Inject current trace context into a meta dict for MCP request propagation.\n\n    Args:\n        meta: Optional existing meta dict to merge with trace context\n\n    Returns:\n        A new dict containing the original meta (if any) plus trace context keys,\n        or None if no trace context to inject and meta was None\n    \"\"\"\n    carrier: dict[str, str] = {}\n    propagate.inject(carrier)\n\n    trace_meta: dict[str, Any] = {}\n    if \"traceparent\" in carrier:\n        trace_meta[TRACE_PARENT_KEY] = carrier[\"traceparent\"]\n    if \"tracestate\" in carrier:\n        trace_meta[TRACE_STATE_KEY] = carrier[\"tracestate\"]\n\n    if trace_meta:\n        return {**(meta or {}), **trace_meta}\n    return meta\n\n\ndef record_span_error(span: Span, exception: BaseException) -> None:\n    \"\"\"Record an exception on a span and set error status.\"\"\"\n    span.record_exception(exception)\n    span.set_status(Status(StatusCode.ERROR))\n\n\ndef extract_trace_context(meta: dict[str, Any] | None) -> Context:\n    \"\"\"Extract trace context from an MCP request meta dict.\n\n    If already in a valid trace (e.g., from HTTP propagation), the existing\n    trace context is preserved and meta is not used.\n\n    Args:\n        meta: The meta dict from an MCP request (ctx.request_context.meta)\n\n    Returns:\n        An OpenTelemetry Context with the extracted trace context,\n        or the current context if no trace context found or already in a trace\n    \"\"\"\n    # Don't override existing trace context (e.g., from HTTP propagation)\n    current_span = trace.get_current_span()\n    if current_span.get_span_context().is_valid:\n        return otel_context.get_current()\n\n    if not meta:\n        return otel_context.get_current()\n\n    carrier: dict[str, str] = {}\n    if TRACE_PARENT_KEY in meta:\n        carrier[\"traceparent\"] = str(meta[TRACE_PARENT_KEY])\n    if TRACE_STATE_KEY in meta:\n        carrier[\"tracestate\"] = str(meta[TRACE_STATE_KEY])\n\n    if carrier:\n        return propagate.extract(carrier)\n    return otel_context.get_current()\n\n\n__all__ = [\n    \"INSTRUMENTATION_NAME\",\n    \"TRACE_PARENT_KEY\",\n    \"TRACE_STATE_KEY\",\n    \"extract_trace_context\",\n    \"get_tracer\",\n    \"inject_trace_context\",\n    \"record_span_error\",\n]\n"
  },
  {
    "path": "src/fastmcp/tools/__init__.py",
    "content": "import sys\n\nfrom .function_tool import FunctionTool, tool\nfrom .base import Tool, ToolResult\nfrom .tool_transform import forward, forward_raw\n\n# Backward compat: tool.py was renamed to base.py to stop Pyright from resolving\n# `from fastmcp.tools import tool` as the submodule instead of the decorator function.\n# This shim keeps `from fastmcp.tools.tool import Tool` working at runtime.\n# Safe to remove once we're confident no external code imports from the old path.\nsys.modules[f\"{__name__}.tool\"] = sys.modules[f\"{__name__}.base\"]\n\n__all__ = [\n    \"FunctionTool\",\n    \"Tool\",\n    \"ToolResult\",\n    \"forward\",\n    \"forward_raw\",\n    \"tool\",\n]\n"
  },
  {
    "path": "src/fastmcp/tools/base.py",
    "content": "from __future__ import annotations\n\nimport warnings\nfrom collections.abc import Callable\nfrom typing import (\n    TYPE_CHECKING,\n    Annotated,\n    Any,\n    ClassVar,\n    TypeAlias,\n    overload,\n)\n\nimport mcp.types\nimport pydantic_core\nfrom mcp.shared.tool_name_validation import validate_and_warn_tool_name\nfrom mcp.types import (\n    CallToolResult,\n    ContentBlock,\n    Icon,\n    TextContent,\n    ToolAnnotations,\n    ToolExecution,\n)\nfrom mcp.types import Tool as MCPTool\nfrom pydantic import BaseModel, Field, model_validator\nfrom pydantic.json_schema import SkipJsonSchema\n\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.tasks.config import TaskConfig, TaskMeta\nfrom fastmcp.utilities.components import FastMCPComponent\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import (\n    Audio,\n    File,\n    Image,\n    NotSet,\n    NotSetT,\n)\n\ntry:\n    from prefab_ui.app import PrefabApp as _PrefabApp\n    from prefab_ui.components.base import Component as _PrefabComponent\n\n    _HAS_PREFAB = True\nexcept ImportError:\n    _HAS_PREFAB = False\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\n    from fastmcp.tools.function_tool import FunctionTool\n    from fastmcp.tools.tool_transform import ArgTransform, TransformedTool\n\n# Re-export from function_tool module\n\nlogger = get_logger(__name__)\n\n\nToolResultSerializerType: TypeAlias = Callable[[Any], str]\n\n\ndef default_serializer(data: Any) -> str:\n    return pydantic_core.to_json(data, fallback=str).decode()\n\n\nclass ToolResult(BaseModel):\n    content: list[ContentBlock] = Field(\n        description=\"List of content blocks for the tool result\"\n    )\n    structured_content: dict[str, Any] | None = Field(\n        default=None, description=\"Structured content matching the tool's output schema\"\n    )\n    meta: dict[str, Any] | None = Field(\n        default=None, description=\"Runtime metadata about the tool execution\"\n    )\n\n    def __init__(\n        self,\n        content: list[ContentBlock] | Any | None = None,\n        structured_content: dict[str, Any] | Any | None = None,\n        meta: dict[str, Any] | None = None,\n    ):\n        if content is None and structured_content is None:\n            raise ValueError(\"Either content or structured_content must be provided\")\n        elif content is None:\n            content = structured_content\n\n        converted_content: list[ContentBlock] = _convert_to_content(result=content)\n\n        if structured_content is not None:\n            # Convert Prefab types to their wire-format envelope before\n            # generic serialization, so the renderer gets the right shape.\n            if _HAS_PREFAB:\n                if isinstance(structured_content, _PrefabApp):\n                    structured_content = _prefab_to_json(structured_content)\n                elif isinstance(structured_content, _PrefabComponent):\n                    structured_content = _prefab_to_json(\n                        _PrefabApp(view=structured_content)\n                    )\n\n            try:\n                structured_content = pydantic_core.to_jsonable_python(\n                    value=structured_content\n                )\n            except pydantic_core.PydanticSerializationError as e:\n                logger.error(\n                    f\"Could not serialize structured content. If this is unexpected, set your tool's output_schema to None to disable automatic serialization: {e}\"\n                )\n                raise\n            if not isinstance(structured_content, dict):\n                raise ValueError(\n                    \"structured_content must be a dict or None. \"\n                    f\"Got {type(structured_content).__name__}: {structured_content!r}. \"\n                    \"Tools should wrap non-dict values based on their output_schema.\"\n                )\n\n        super().__init__(\n            content=converted_content, structured_content=structured_content, meta=meta\n        )\n\n    def to_mcp_result(\n        self,\n    ) -> (\n        list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult\n    ):\n        if self.meta is not None:\n            return CallToolResult(\n                structuredContent=self.structured_content,\n                content=self.content,\n                _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n            )\n        if self.structured_content is None:\n            return self.content\n        return self.content, self.structured_content\n\n\nclass Tool(FastMCPComponent):\n    \"\"\"Internal tool registration info.\"\"\"\n\n    KEY_PREFIX: ClassVar[str] = \"tool\"\n\n    parameters: Annotated[\n        dict[str, Any], Field(description=\"JSON schema for tool parameters\")\n    ]\n    output_schema: Annotated[\n        dict[str, Any] | None, Field(description=\"JSON schema for tool output\")\n    ] = None\n    annotations: Annotated[\n        ToolAnnotations | None,\n        Field(description=\"Additional annotations about the tool\"),\n    ] = None\n    execution: Annotated[\n        ToolExecution | None,\n        Field(description=\"Task execution configuration (SEP-1686)\"),\n    ] = None\n    serializer: Annotated[\n        SkipJsonSchema[ToolResultSerializerType | None],\n        Field(\n            description=\"Deprecated. Return ToolResult from your tools for full control over serialization.\"\n        ),\n    ] = None\n    auth: Annotated[\n        SkipJsonSchema[AuthCheck | list[AuthCheck] | None],\n        Field(description=\"Authorization checks for this tool\", exclude=True),\n    ] = None\n    timeout: Annotated[\n        float | None,\n        Field(\n            description=\"Execution timeout in seconds. If None, no timeout is applied.\"\n        ),\n    ] = None\n\n    @model_validator(mode=\"after\")\n    def _validate_tool_name(self) -> Tool:\n        \"\"\"Validate tool name according to MCP specification (SEP-986).\"\"\"\n        validate_and_warn_tool_name(self.name)\n        return self\n\n    def to_mcp_tool(\n        self,\n        **overrides: Any,\n    ) -> MCPTool:\n        \"\"\"Convert the FastMCP tool to an MCP tool.\"\"\"\n        title = None\n\n        if self.title:\n            title = self.title\n        elif self.annotations and self.annotations.title:\n            title = self.annotations.title\n\n        return MCPTool(\n            name=overrides.get(\"name\", self.name),\n            title=overrides.get(\"title\", title),\n            description=overrides.get(\"description\", self.description),\n            inputSchema=overrides.get(\"inputSchema\", self.parameters),\n            outputSchema=overrides.get(\"outputSchema\", self.output_schema),\n            icons=overrides.get(\"icons\", self.icons),\n            annotations=overrides.get(\"annotations\", self.annotations),\n            execution=overrides.get(\"execution\", self.execution),\n            _meta=overrides.get(  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field\n                \"_meta\", self.get_meta()\n            ),\n        )\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        tags: set[str] | None = None,\n        annotations: ToolAnnotations | None = None,\n        exclude_args: list[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        serializer: ToolResultSerializerType | None = None,  # Deprecated\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionTool:\n        \"\"\"Create a Tool from a function.\"\"\"\n        from fastmcp.tools.function_tool import FunctionTool\n\n        return FunctionTool.from_function(\n            fn=fn,\n            name=name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            annotations=annotations,\n            exclude_args=exclude_args,\n            output_schema=output_schema,\n            serializer=serializer,\n            meta=meta,\n            task=task,\n            timeout=timeout,\n            auth=auth,\n        )\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        \"\"\"\n        Run the tool with arguments.\n\n        This method is not implemented in the base Tool class and must be\n        implemented by subclasses.\n\n        `run()` can EITHER return a list of ContentBlocks, or a tuple of\n        (list of ContentBlocks, dict of structured output).\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement run()\")\n\n    def convert_result(self, raw_value: Any) -> ToolResult:\n        \"\"\"Convert a raw result to ToolResult.\n\n        Handles ToolResult passthrough and converts raw values using the tool's\n        attributes (serializer, output_schema) for proper conversion.\n        \"\"\"\n        if isinstance(raw_value, ToolResult):\n            return raw_value\n\n        if _HAS_PREFAB:\n            if isinstance(raw_value, _PrefabApp):\n                return _prefab_to_tool_result(raw_value)\n            if isinstance(raw_value, _PrefabComponent):\n                return _prefab_to_tool_result(_PrefabApp(view=raw_value))\n\n        content = _convert_to_content(raw_value, serializer=self.serializer)\n\n        # Skip structured content for ContentBlock types only if no output_schema\n        # (if output_schema exists, MCP SDK requires structured_content)\n        if self.output_schema is None and (\n            isinstance(raw_value, ContentBlock | Audio | Image | File)\n            or (\n                isinstance(raw_value, list | tuple)\n                and any(isinstance(item, ContentBlock) for item in raw_value)\n            )\n        ):\n            return ToolResult(content=content)\n\n        try:\n            structured = pydantic_core.to_jsonable_python(raw_value)\n        except pydantic_core.PydanticSerializationError:\n            return ToolResult(content=content)\n\n        if self.output_schema is None:\n            # No schema - only use structured_content for dicts\n            if isinstance(structured, dict):\n                return ToolResult(content=content, structured_content=structured)\n            return ToolResult(content=content)\n\n        # Has output_schema - wrap if x-fastmcp-wrap-result is set\n        wrap_result = self.output_schema.get(\"x-fastmcp-wrap-result\")\n        return ToolResult(\n            content=content,\n            structured_content={\"result\": structured} if wrap_result else structured,\n            meta={\"fastmcp\": {\"wrap_result\": True}} if wrap_result else None,\n        )\n\n    @overload\n    async def _run(\n        self,\n        arguments: dict[str, Any],\n        task_meta: None = None,\n    ) -> ToolResult: ...\n\n    @overload\n    async def _run(\n        self,\n        arguments: dict[str, Any],\n        task_meta: TaskMeta,\n    ) -> mcp.types.CreateTaskResult: ...\n\n    async def _run(\n        self,\n        arguments: dict[str, Any],\n        task_meta: TaskMeta | None = None,\n    ) -> ToolResult | mcp.types.CreateTaskResult:\n        \"\"\"Server entry point that handles task routing.\n\n        This allows ANY Tool subclass to support background execution by setting\n        task_config.mode to \"supported\" or \"required\". The server calls this\n        method instead of run() directly.\n\n        Args:\n            arguments: Tool arguments\n            task_meta: If provided, execute as background task and return\n                CreateTaskResult. If None (default), execute synchronously and\n                return ToolResult.\n\n        Returns:\n            ToolResult when task_meta is None.\n            CreateTaskResult when task_meta is provided.\n\n        Subclasses can override this to customize task routing behavior.\n        For example, FastMCPProviderTool overrides to delegate to child\n        middleware without submitting to Docket.\n        \"\"\"\n        from fastmcp.server.tasks.routing import check_background_task\n\n        task_result = await check_background_task(\n            component=self,\n            task_type=\"tool\",\n            arguments=arguments,\n            task_meta=task_meta,\n        )\n        if task_result:\n            return task_result\n\n        return await self.run(arguments)\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this tool with docket for background execution.\"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.run, names=[self.key])\n\n    async def add_to_docket(  # type: ignore[override]\n        self,\n        docket: Docket,\n        arguments: dict[str, Any],\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this tool for background execution via docket.\n\n        Args:\n            docket: The Docket instance\n            arguments: Tool arguments\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(arguments)\n\n    @classmethod\n    def from_tool(\n        cls,\n        tool: Tool | Callable[..., Any],\n        *,\n        name: str | None = None,\n        title: str | NotSetT | None = NotSet,\n        description: str | NotSetT | None = NotSet,\n        tags: set[str] | None = None,\n        annotations: ToolAnnotations | NotSetT | None = NotSet,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        serializer: ToolResultSerializerType | None = None,  # Deprecated\n        meta: dict[str, Any] | NotSetT | None = NotSet,\n        transform_args: dict[str, ArgTransform] | None = None,\n        transform_fn: Callable[..., Any] | None = None,\n    ) -> TransformedTool:\n        from fastmcp.tools.tool_transform import TransformedTool\n\n        tool = cls._ensure_tool(tool)\n\n        return TransformedTool.from_tool(\n            tool=tool,\n            transform_fn=transform_fn,\n            name=name,\n            title=title,\n            transform_args=transform_args,\n            description=description,\n            tags=tags,\n            annotations=annotations,\n            output_schema=output_schema,\n            serializer=serializer,\n            meta=meta,\n        )\n\n    @classmethod\n    def _ensure_tool(cls, tool: Tool | Callable[..., Any]) -> Tool:\n        \"\"\"Coerce a callable into a Tool, respecting @tool decorator metadata.\"\"\"\n        if isinstance(tool, Tool):\n            return tool\n\n        from fastmcp.decorators import get_fastmcp_meta\n        from fastmcp.tools.function_tool import FunctionTool, ToolMeta\n\n        fmeta = get_fastmcp_meta(tool)\n        if isinstance(fmeta, ToolMeta):\n            return FunctionTool.from_function(tool, metadata=fmeta)\n\n        return cls.from_function(tool)\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        return super().get_span_attributes() | {\n            \"fastmcp.component.type\": \"tool\",\n            \"fastmcp.provider.type\": \"LocalProvider\",\n        }\n\n\ndef _serialize_with_fallback(\n    result: Any, serializer: ToolResultSerializerType | None = None\n) -> str:\n    if serializer is not None:\n        try:\n            return serializer(result)\n        except Exception as e:\n            logger.warning(\n                \"Error serializing tool result: %s\",\n                e,\n                exc_info=True,\n            )\n\n    return default_serializer(result)\n\n\ndef _convert_to_single_content_block(\n    item: Any,\n    serializer: ToolResultSerializerType | None = None,\n) -> ContentBlock:\n    if isinstance(item, ContentBlock):\n        return item\n\n    if isinstance(item, Image):\n        return item.to_image_content()\n\n    if isinstance(item, Audio):\n        return item.to_audio_content()\n\n    if isinstance(item, File):\n        return item.to_resource_content()\n\n    if isinstance(item, str):\n        return TextContent(type=\"text\", text=item)\n\n    return TextContent(type=\"text\", text=_serialize_with_fallback(item, serializer))\n\n\n_PREFAB_TEXT_FALLBACK = \"[Rendered Prefab UI]\"\n\n\ndef _get_tool_resolver() -> Callable[..., str] | None:\n    \"\"\"Get the FastMCPApp callable resolver, if available.\"\"\"\n    try:\n        from fastmcp.server.app import _resolve_tool_ref\n\n        return _resolve_tool_ref\n    except ImportError:\n        return None\n\n\ndef _prefab_to_json(app: Any) -> dict[str, Any]:\n    \"\"\"Call PrefabApp.to_json() with the FastMCPApp callable resolver.\"\"\"\n    return app.to_json(tool_resolver=_get_tool_resolver())\n\n\ndef _prefab_to_tool_result(app: Any) -> ToolResult:\n    \"\"\"Convert a PrefabApp to a FastMCP ToolResult.\"\"\"\n    return ToolResult(\n        content=[TextContent(type=\"text\", text=_PREFAB_TEXT_FALLBACK)],\n        structured_content=_prefab_to_json(app),\n    )\n\n\ndef _convert_to_content(\n    result: Any,\n    serializer: ToolResultSerializerType | None = None,\n) -> list[ContentBlock]:\n    \"\"\"Convert a result to a sequence of content objects.\"\"\"\n\n    if result is None:\n        return []\n\n    if not isinstance(result, (list | tuple)):\n        return [_convert_to_single_content_block(result, serializer)]\n\n    # If all items are ContentBlocks, return them as is\n    if all(isinstance(item, ContentBlock) for item in result):\n        return result\n\n    # If any item is a ContentBlock, convert non-ContentBlock items to TextContent\n    # without aggregating them\n    if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):\n        return [\n            _convert_to_single_content_block(item, serializer)\n            if not isinstance(item, ContentBlock)\n            else item\n            for item in result\n        ]\n    # If none of the items are ContentBlocks, aggregate all items into a single TextContent\n    return [TextContent(type=\"text\", text=_serialize_with_fallback(result, serializer))]\n\n\n__all__ = [\"Tool\", \"ToolResult\"]\n\n\ndef __getattr__(name: str) -> Any:\n    \"\"\"Deprecated re-exports for backwards compatibility.\"\"\"\n    deprecated_exports = {\n        \"FunctionTool\": \"FunctionTool\",\n        \"ParsedFunction\": \"ParsedFunction\",\n        \"tool\": \"tool\",\n    }\n\n    if name in deprecated_exports:\n        import fastmcp\n\n        if fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                f\"Importing {name} from fastmcp.tools.tool is deprecated. \"\n                f\"Import from fastmcp.tools.function_tool instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        from fastmcp.tools import function_tool\n\n        return getattr(function_tool, name)\n\n    raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n"
  },
  {
    "path": "src/fastmcp/tools/function_parsing.py",
    "content": "\"\"\"Function introspection and schema generation for FastMCP tools.\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport inspect\nimport types\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import Annotated, Any, Generic, Union, get_args, get_origin, get_type_hints\n\nimport mcp.types\nfrom pydantic import PydanticSchemaGenerationError\nfrom typing_extensions import TypeVar as TypeVarExt\n\nfrom fastmcp.server.dependencies import (\n    transform_context_annotations,\n    without_injected_parameters,\n)\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import (\n    Audio,\n    File,\n    Image,\n    create_function_without_params,\n    get_cached_typeadapter,\n    is_class_member_of_type,\n    replace_type,\n)\n\ntry:\n    from prefab_ui.app import PrefabApp as _PrefabApp\n    from prefab_ui.components.base import Component as _PrefabComponent\n\n    _PREFAB_TYPES: tuple[type, ...] = (_PrefabApp, _PrefabComponent)\nexcept ImportError:\n    _PREFAB_TYPES = ()\n\n\ndef _contains_prefab_type(tp: Any) -> bool:\n    \"\"\"Check if *tp* is or contains a prefab type, recursing through unions and Annotated.\"\"\"\n    if isinstance(tp, type) and issubclass(tp, _PREFAB_TYPES):\n        return True\n    origin = get_origin(tp)\n    if origin is Union or origin is types.UnionType or origin is Annotated:\n        return any(_contains_prefab_type(a) for a in get_args(tp))\n    return False\n\n\nT = TypeVarExt(\"T\", default=Any)\n\nlogger = get_logger(__name__)\n\n\n@dataclass\nclass _WrappedResult(Generic[T]):\n    \"\"\"Generic wrapper for non-object return types.\"\"\"\n\n    result: T\n\n\nclass _UnserializableType:\n    pass\n\n\ndef _is_object_schema(\n    schema: dict[str, Any],\n    *,\n    _root_schema: dict[str, Any] | None = None,\n    _seen_refs: set[str] | None = None,\n) -> bool:\n    \"\"\"Check if a JSON schema represents an object type.\"\"\"\n    root_schema = _root_schema or schema\n    seen_refs = _seen_refs or set()\n\n    # Direct object type\n    if schema.get(\"type\") == \"object\":\n        return True\n\n    # Schema with properties but no explicit type is treated as object\n    if \"properties\" in schema:\n        return True\n\n    # Resolve local $ref definitions and recurse into the target schema.\n    ref = schema.get(\"$ref\")\n    if not isinstance(ref, str) or not ref.startswith(\"#/\"):\n        return False\n\n    if ref in seen_refs:\n        return False\n\n    # Walk the JSON Pointer path from the root schema, unescaping each\n    # token per RFC 6901 (~1 → /, ~0 → ~).\n    pointer = ref.removeprefix(\"#/\")\n    segments = pointer.split(\"/\")\n    target: Any = root_schema\n    for segment in segments:\n        unescaped = segment.replace(\"~1\", \"/\").replace(\"~0\", \"~\")\n        if not isinstance(target, dict) or unescaped not in target:\n            return False\n        target = target[unescaped]\n\n    target_schema = target\n    if not isinstance(target_schema, dict):\n        return False\n\n    return _is_object_schema(\n        target_schema,\n        _root_schema=root_schema,\n        _seen_refs=seen_refs | {ref},\n    )\n\n\n@dataclass\nclass ParsedFunction:\n    fn: Callable[..., Any]\n    name: str\n    description: str | None\n    input_schema: dict[str, Any]\n    output_schema: dict[str, Any] | None\n    return_type: Any = None\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        exclude_args: list[str] | None = None,\n        validate: bool = True,\n        wrap_non_object_output_schema: bool = True,\n    ) -> ParsedFunction:\n        if validate:\n            sig = inspect.signature(fn)\n            # Reject functions with *args or **kwargs\n            for param in sig.parameters.values():\n                if param.kind == inspect.Parameter.VAR_POSITIONAL:\n                    raise ValueError(\"Functions with *args are not supported as tools\")\n                if param.kind == inspect.Parameter.VAR_KEYWORD:\n                    raise ValueError(\n                        \"Functions with **kwargs are not supported as tools\"\n                    )\n\n            # Reject exclude_args that don't exist in the function or don't have a default value\n            if exclude_args:\n                for arg_name in exclude_args:\n                    if arg_name not in sig.parameters:\n                        raise ValueError(\n                            f\"Parameter '{arg_name}' in exclude_args does not exist in function.\"\n                        )\n                    param = sig.parameters[arg_name]\n                    if param.default == inspect.Parameter.empty:\n                        raise ValueError(\n                            f\"Parameter '{arg_name}' in exclude_args must have a default value.\"\n                        )\n\n        # collect name and doc before we potentially modify the function\n        fn_name = getattr(fn, \"__name__\", None) or fn.__class__.__name__\n        fn_doc = inspect.getdoc(fn)\n\n        # if the fn is a callable class, we need to get the __call__ method from here out\n        if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):\n            fn = fn.__call__\n        # if the fn is a staticmethod, we need to work with the underlying function\n        if isinstance(fn, staticmethod):\n            fn = fn.__func__\n\n        # Transform Context type annotations to Depends() for unified DI\n        fn = transform_context_annotations(fn)\n\n        # Handle injected parameters (Context, Docket dependencies)\n        wrapper_fn = without_injected_parameters(fn)\n\n        # Also handle exclude_args with non-serializable types (issue #2431)\n        # This must happen before Pydantic tries to serialize the parameters\n        if exclude_args:\n            wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args))\n\n        input_type_adapter = get_cached_typeadapter(wrapper_fn)\n        input_schema = input_type_adapter.json_schema()\n\n        # Compress and handle exclude_args\n        prune_params = list(exclude_args) if exclude_args else None\n        input_schema = compress_schema(\n            input_schema, prune_params=prune_params, prune_titles=True\n        )\n\n        output_schema = None\n        # Get the return annotation from the signature\n        sig = inspect.signature(fn)\n        output_type = sig.return_annotation\n\n        # If the annotation is a string (from __future__ annotations), resolve it\n        if isinstance(output_type, str):\n            try:\n                # Use get_type_hints to resolve the return type\n                # include_extras=True preserves Annotated metadata\n                type_hints = get_type_hints(fn, include_extras=True)\n                output_type = type_hints.get(\"return\", output_type)\n            except Exception as e:\n                # If resolution fails, keep the string annotation\n                logger.debug(\"Failed to resolve type hint for return annotation: %s\", e)\n\n        # Save original for return_type before any schema-related replacement\n        original_output_type = output_type\n\n        if output_type not in (inspect._empty, None, Any, ...):\n            # Prefab component subclasses (Column, Card, etc.) shouldn't\n            # produce output schemas — replace_type only does exact matching,\n            # so we handle subclass matching explicitly here.  We also need\n            # to handle composite types like ``Column | None`` and\n            # ``Annotated[PrefabApp, ...]`` by recursing into their args.\n            if _PREFAB_TYPES and _contains_prefab_type(output_type):\n                output_type = _UnserializableType\n\n            # ToolResult subclasses should suppress schema generation just\n            # like ToolResult itself — replace_type only does exact matching.\n            if is_class_member_of_type(output_type, ToolResult):\n                output_type = _UnserializableType\n\n            # there are a variety of types that we don't want to attempt to\n            # serialize because they are either used by FastMCP internally,\n            # or are MCP content types that explicitly don't form structured\n            # content. By replacing them with an explicitly unserializable type,\n            # we ensure that no output schema is automatically generated.\n            clean_output_type = replace_type(\n                output_type,\n                dict.fromkeys(\n                    (\n                        Image,\n                        Audio,\n                        File,\n                        ToolResult,\n                        mcp.types.TextContent,\n                        mcp.types.ImageContent,\n                        mcp.types.AudioContent,\n                        mcp.types.ResourceLink,\n                        mcp.types.EmbeddedResource,\n                        *_PREFAB_TYPES,\n                    ),\n                    _UnserializableType,\n                ),\n            )\n\n            try:\n                type_adapter = get_cached_typeadapter(clean_output_type)\n                base_schema = type_adapter.json_schema(mode=\"serialization\")\n\n                # Generate schema for wrapped type if it's non-object\n                # because MCP requires that output schemas are objects\n                # Check if schema is an object type, resolving $ref references\n                # (self-referencing types use $ref at root level)\n                if wrap_non_object_output_schema and not _is_object_schema(base_schema):\n                    # Use the wrapped result schema directly\n                    wrapped_type = _WrappedResult[clean_output_type]\n                    wrapped_adapter = get_cached_typeadapter(wrapped_type)\n                    output_schema = wrapped_adapter.json_schema(mode=\"serialization\")\n                    output_schema[\"x-fastmcp-wrap-result\"] = True\n                else:\n                    output_schema = base_schema\n\n                output_schema = compress_schema(output_schema, prune_titles=True)\n\n            except PydanticSchemaGenerationError as e:\n                if \"_UnserializableType\" not in str(e):\n                    logger.debug(f\"Unable to generate schema for type {output_type!r}\")\n\n        return cls(\n            fn=fn,\n            name=fn_name,\n            description=fn_doc,\n            input_schema=input_schema,\n            output_schema=output_schema or None,\n            return_type=original_output_type,\n        )\n"
  },
  {
    "path": "src/fastmcp/tools/function_tool.py",
    "content": "\"\"\"Standalone @tool decorator for FastMCP.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport warnings\nfrom collections.abc import Callable\nfrom dataclasses import dataclass, field\nfrom typing import (\n    TYPE_CHECKING,\n    Annotated,\n    Any,\n    Literal,\n    Protocol,\n    TypeVar,\n    overload,\n    runtime_checkable,\n)\n\nimport anyio\nimport mcp.types\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import ErrorData, Icon, ToolAnnotations, ToolExecution\nfrom pydantic import Field\nfrom pydantic.json_schema import SkipJsonSchema\n\nimport fastmcp\nfrom fastmcp.decorators import resolve_task_config\nfrom fastmcp.server.auth.authorization import AuthCheck\nfrom fastmcp.server.dependencies import without_injected_parameters\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.tools.base import (\n    Tool,\n    ToolResult,\n    ToolResultSerializerType,\n)\nfrom fastmcp.tools.function_parsing import ParsedFunction, _is_object_schema\nfrom fastmcp.utilities.async_utils import (\n    call_sync_fn_in_threadpool,\n    is_coroutine_function,\n)\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import (\n    NotSet,\n    NotSetT,\n    get_cached_typeadapter,\n)\n\nlogger = get_logger(__name__)\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\n\n\n@runtime_checkable\nclass DecoratedTool(Protocol):\n    \"\"\"Protocol for functions decorated with @tool.\"\"\"\n\n    __fastmcp__: ToolMeta\n\n    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...\n\n\n@dataclass(frozen=True, kw_only=True)\nclass ToolMeta:\n    \"\"\"Metadata attached to functions by the @tool decorator.\"\"\"\n\n    type: Literal[\"tool\"] = field(default=\"tool\", init=False)\n    name: str | None = None\n    version: str | int | None = None\n    title: str | None = None\n    description: str | None = None\n    icons: list[Icon] | None = None\n    tags: set[str] | None = None\n    output_schema: dict[str, Any] | NotSetT | None = NotSet\n    annotations: ToolAnnotations | None = None\n    meta: dict[str, Any] | None = None\n    app: Any = None\n    task: bool | TaskConfig | None = None\n    exclude_args: list[str] | None = None\n    serializer: Any | None = None\n    timeout: float | None = None\n    auth: AuthCheck | list[AuthCheck] | None = None\n    enabled: bool = True\n\n\nclass FunctionTool(Tool):\n    fn: SkipJsonSchema[Callable[..., Any]]\n    return_type: Annotated[SkipJsonSchema[Any], Field(exclude=True)] = None\n\n    def to_mcp_tool(\n        self,\n        **overrides: Any,\n    ) -> mcp.types.Tool:\n        \"\"\"Convert the FastMCP tool to an MCP tool.\n\n        Extends the base implementation to add task execution mode if enabled.\n        \"\"\"\n        # Get base MCP tool from parent\n        mcp_tool = super().to_mcp_tool(**overrides)\n\n        # Add task execution mode per SEP-1686\n        # Only set execution if not overridden and task execution is supported\n        if self.task_config.supports_tasks() and \"execution\" not in overrides:\n            mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode)\n\n        return mcp_tool\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        *,\n        metadata: ToolMeta | None = None,\n        # Keep individual params for backwards compat\n        name: str | None = None,\n        version: str | int | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        tags: set[str] | None = None,\n        annotations: ToolAnnotations | None = None,\n        exclude_args: list[str] | None = None,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        serializer: ToolResultSerializerType | None = None,\n        meta: dict[str, Any] | None = None,\n        task: bool | TaskConfig | None = None,\n        timeout: float | None = None,\n        auth: AuthCheck | list[AuthCheck] | None = None,\n    ) -> FunctionTool:\n        \"\"\"Create a FunctionTool from a function.\n\n        Args:\n            fn: The function to wrap\n            metadata: ToolMeta object with all configuration. If provided,\n                individual parameters must not be passed.\n            name, title, etc.: Individual parameters for backwards compatibility.\n                Cannot be used together with metadata parameter.\n        \"\"\"\n        # Check mutual exclusion\n        individual_params_provided = (\n            any(\n                x is not None and x is not NotSet\n                for x in [\n                    name,\n                    version,\n                    title,\n                    description,\n                    icons,\n                    tags,\n                    annotations,\n                    meta,\n                    task,\n                    serializer,\n                    timeout,\n                    auth,\n                ]\n            )\n            or output_schema is not NotSet\n            or exclude_args is not None\n        )\n\n        if metadata is not None and individual_params_provided:\n            raise TypeError(\n                \"Cannot pass both 'metadata' and individual parameters to from_function(). \"\n                \"Use metadata alone or individual parameters alone.\"\n            )\n\n        # Build metadata from kwargs if not provided\n        if metadata is None:\n            metadata = ToolMeta(\n                name=name,\n                version=version,\n                title=title,\n                description=description,\n                icons=icons,\n                tags=tags,\n                output_schema=output_schema,\n                annotations=annotations,\n                meta=meta,\n                task=task,\n                exclude_args=exclude_args,\n                serializer=serializer,\n                timeout=timeout,\n                auth=auth,\n            )\n\n        if metadata.serializer is not None and fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"The `serializer` parameter is deprecated. \"\n                \"Return ToolResult from your tools for full control over serialization. \"\n                \"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        if metadata.exclude_args and fastmcp.settings.deprecation_warnings:\n            warnings.warn(\n                \"The `exclude_args` parameter is deprecated as of FastMCP 2.14. \"\n                \"Use dependency injection with `Depends()` instead for better lifecycle management. \"\n                \"See https://gofastmcp.com/servers/dependency-injection#using-depends for examples.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        parsed_fn = ParsedFunction.from_function(fn, exclude_args=metadata.exclude_args)\n        func_name = metadata.name or parsed_fn.name\n\n        if func_name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        # Normalize task to TaskConfig\n        task_value = metadata.task\n        if task_value is None:\n            task_config = TaskConfig(mode=\"forbidden\")\n        elif isinstance(task_value, bool):\n            task_config = TaskConfig.from_bool(task_value)\n        else:\n            task_config = task_value\n        task_config.validate_function(fn, func_name)\n\n        # Handle output_schema\n        if isinstance(metadata.output_schema, NotSetT):\n            final_output_schema = parsed_fn.output_schema\n        else:\n            final_output_schema = metadata.output_schema\n\n        if final_output_schema is not None and isinstance(final_output_schema, dict):\n            if not _is_object_schema(final_output_schema):\n                raise ValueError(\n                    f\"Output schemas must represent object types due to MCP spec limitations. \"\n                    f\"Received: {final_output_schema!r}\"\n                )\n\n        return cls(\n            fn=parsed_fn.fn,\n            return_type=parsed_fn.return_type,\n            name=metadata.name or parsed_fn.name,\n            version=str(metadata.version) if metadata.version is not None else None,\n            title=metadata.title,\n            description=metadata.description or parsed_fn.description,\n            icons=metadata.icons,\n            parameters=parsed_fn.input_schema,\n            output_schema=final_output_schema,\n            annotations=metadata.annotations,\n            tags=metadata.tags or set(),\n            serializer=metadata.serializer,\n            meta=metadata.meta,\n            task_config=task_config,\n            timeout=metadata.timeout,\n            auth=metadata.auth,\n        )\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        \"\"\"Run the tool with arguments.\"\"\"\n        wrapper_fn = without_injected_parameters(self.fn)\n        type_adapter = get_cached_typeadapter(wrapper_fn)\n\n        # Apply timeout if configured\n        if self.timeout is not None:\n            try:\n                with anyio.fail_after(self.timeout):\n                    # Thread pool execution for sync functions, direct await for async\n                    if is_coroutine_function(wrapper_fn):\n                        result = await type_adapter.validate_python(arguments)\n                    else:\n                        # Sync function: run in threadpool to avoid blocking\n                        result = await call_sync_fn_in_threadpool(\n                            type_adapter.validate_python, arguments\n                        )\n                        # Handle sync wrappers that return awaitables\n                        if inspect.isawaitable(result):\n                            result = await result\n            except TimeoutError:\n                logger.warning(\n                    f\"Tool '{self.name}' timed out after {self.timeout}s. \"\n                    f\"Consider using task=True for long-running operations. \"\n                    f\"See https://gofastmcp.com/servers/tasks\"\n                )\n                raise McpError(\n                    ErrorData(\n                        code=-32000,\n                        message=f\"Tool '{self.name}' execution timed out after {self.timeout}s\",\n                    )\n                ) from None\n        else:\n            # No timeout: use existing execution path\n            if is_coroutine_function(wrapper_fn):\n                result = await type_adapter.validate_python(arguments)\n            else:\n                result = await call_sync_fn_in_threadpool(\n                    type_adapter.validate_python, arguments\n                )\n                if inspect.isawaitable(result):\n                    result = await result\n\n        return self.convert_result(result)\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this tool with docket for background execution.\n\n        FunctionTool registers the underlying function, which has the user's\n        Depends parameters for docket to resolve.\n        \"\"\"\n        if not self.task_config.supports_tasks():\n            return\n        docket.register(self.fn, names=[self.key])\n\n    async def add_to_docket(\n        self,\n        docket: Docket,\n        arguments: dict[str, Any],\n        *,\n        fn_key: str | None = None,\n        task_key: str | None = None,\n        **kwargs: Any,\n    ) -> Execution:\n        \"\"\"Schedule this tool for background execution via docket.\n\n        FunctionTool splats the arguments dict since .fn expects **kwargs.\n\n        Args:\n            docket: The Docket instance\n            arguments: Tool arguments\n            fn_key: Function lookup key in Docket registry (defaults to self.key)\n            task_key: Redis storage key for the result\n            **kwargs: Additional kwargs passed to docket.add()\n        \"\"\"\n        lookup_key = fn_key or self.key\n        if task_key:\n            kwargs[\"key\"] = task_key\n        return await docket.add(lookup_key, **kwargs)(**arguments)\n\n\n@overload\ndef tool(fn: F) -> F: ...\n@overload\ndef tool(\n    name_or_fn: str,\n    *,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    tags: set[str] | None = None,\n    output_schema: dict[str, Any] | NotSetT | None = NotSet,\n    annotations: ToolAnnotations | dict[str, Any] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    exclude_args: list[str] | None = None,\n    serializer: Any | None = None,\n    timeout: float | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Callable[[F], F]: ...\n@overload\ndef tool(\n    name_or_fn: None = None,\n    *,\n    name: str | None = None,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    tags: set[str] | None = None,\n    output_schema: dict[str, Any] | NotSetT | None = NotSet,\n    annotations: ToolAnnotations | dict[str, Any] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    exclude_args: list[str] | None = None,\n    serializer: Any | None = None,\n    timeout: float | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Callable[[F], F]: ...\n\n\ndef tool(\n    name_or_fn: str | Callable[..., Any] | None = None,\n    *,\n    name: str | None = None,\n    version: str | int | None = None,\n    title: str | None = None,\n    description: str | None = None,\n    icons: list[Icon] | None = None,\n    tags: set[str] | None = None,\n    output_schema: dict[str, Any] | NotSetT | None = NotSet,\n    annotations: ToolAnnotations | dict[str, Any] | None = None,\n    meta: dict[str, Any] | None = None,\n    task: bool | TaskConfig | None = None,\n    exclude_args: list[str] | None = None,\n    serializer: Any | None = None,\n    timeout: float | None = None,\n    auth: AuthCheck | list[AuthCheck] | None = None,\n) -> Any:\n    \"\"\"Standalone decorator to mark a function as an MCP tool.\n\n    Returns the original function with metadata attached. Register with a server\n    using mcp.add_tool().\n    \"\"\"\n    if isinstance(annotations, dict):\n        annotations = ToolAnnotations(**annotations)\n\n    if isinstance(name_or_fn, classmethod):\n        raise TypeError(\n            \"To decorate a classmethod, use @classmethod above @tool. \"\n            \"See https://gofastmcp.com/servers/tools#using-with-methods\"\n        )\n\n    def create_tool(fn: Callable[..., Any], tool_name: str | None) -> FunctionTool:\n        # Create metadata first, then pass it\n        tool_meta = ToolMeta(\n            name=tool_name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            output_schema=output_schema,\n            annotations=annotations,\n            meta=meta,\n            task=resolve_task_config(task),\n            exclude_args=exclude_args,\n            serializer=serializer,\n            timeout=timeout,\n            auth=auth,\n        )\n        return FunctionTool.from_function(fn, metadata=tool_meta)\n\n    def attach_metadata(fn: F, tool_name: str | None) -> F:\n        metadata = ToolMeta(\n            name=tool_name,\n            version=version,\n            title=title,\n            description=description,\n            icons=icons,\n            tags=tags,\n            output_schema=output_schema,\n            annotations=annotations,\n            meta=meta,\n            task=task,\n            exclude_args=exclude_args,\n            serializer=serializer,\n            timeout=timeout,\n            auth=auth,\n        )\n        target = fn.__func__ if hasattr(fn, \"__func__\") else fn\n        target.__fastmcp__ = metadata\n        return fn\n\n    def decorator(fn: F, tool_name: str | None) -> F:\n        if fastmcp.settings.decorator_mode == \"object\":\n            warnings.warn(\n                \"decorator_mode='object' is deprecated and will be removed in a future version. \"\n                \"Decorators now return the original function with metadata attached.\",\n                DeprecationWarning,\n                stacklevel=4,\n            )\n            return create_tool(fn, tool_name)  # type: ignore[return-value]\n        return attach_metadata(fn, tool_name)\n\n    if inspect.isroutine(name_or_fn):\n        return decorator(name_or_fn, name)\n    elif isinstance(name_or_fn, str):\n        if name is not None:\n            raise TypeError(\"Cannot specify name both as first argument and keyword\")\n        tool_name = name_or_fn\n    elif name_or_fn is None:\n        tool_name = name\n    else:\n        raise TypeError(f\"Invalid first argument: {type(name_or_fn)}\")\n\n    def wrapper(fn: F) -> F:\n        return decorator(fn, tool_name)\n\n    return wrapper\n"
  },
  {
    "path": "src/fastmcp/tools/tool_transform.py",
    "content": "from __future__ import annotations\n\nimport inspect\nimport warnings\nfrom collections.abc import Callable\nfrom contextvars import ContextVar\nfrom copy import deepcopy\nfrom dataclasses import dataclass\nfrom typing import Annotated, Any, Literal, cast\n\nimport pydantic_core\nfrom mcp.types import ToolAnnotations\nfrom pydantic import ConfigDict\nfrom pydantic.fields import Field\nfrom pydantic.functional_validators import BeforeValidator\nfrom pydantic.json_schema import SkipJsonSchema\n\nimport fastmcp\nfrom fastmcp.tools.base import Tool, ToolResult, _convert_to_content\nfrom fastmcp.tools.function_parsing import ParsedFunction\nfrom fastmcp.utilities.components import _convert_set_default_none\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.types import (\n    FastMCPBaseModel,\n    NotSet,\n    NotSetT,\n    get_cached_typeadapter,\n    issubclass_safe,\n)\n\nlogger = get_logger(__name__)\n\n\n# Context variable to store current transformed tool\n_current_tool: ContextVar[TransformedTool | None] = ContextVar(\n    \"_current_tool\", default=None\n)\n\n\nasync def forward(**kwargs: Any) -> ToolResult:\n    \"\"\"Forward to parent tool with argument transformation applied.\n\n    This function can only be called from within a transformed tool's custom\n    function. It applies argument transformation (renaming, validation) before\n    calling the parent tool.\n\n    For example, if the parent tool has args `x` and `y`, but the transformed\n    tool has args `a` and `b`, and an `transform_args` was provided that maps `x` to\n    `a` and `y` to `b`, then `forward(a=1, b=2)` will call the parent tool with\n    `x=1` and `y=2`.\n\n    Args:\n        **kwargs: Arguments to forward to the parent tool (using transformed names).\n\n    Returns:\n        The ToolResult from the parent tool execution.\n\n    Raises:\n        RuntimeError: If called outside a transformed tool context.\n        TypeError: If provided arguments don't match the transformed schema.\n    \"\"\"\n    tool = _current_tool.get()\n    if tool is None:\n        raise RuntimeError(\"forward() can only be called within a transformed tool\")\n\n    # Use the forwarding function that handles mapping\n    return await tool.forwarding_fn(**kwargs)\n\n\nasync def forward_raw(**kwargs: Any) -> ToolResult:\n    \"\"\"Forward directly to parent tool without transformation.\n\n    This function bypasses all argument transformation and validation, calling the parent\n    tool directly with the provided arguments. Use this when you need to call the parent\n    with its original parameter names and structure.\n\n    For example, if the parent tool has args `x` and `y`, then `forward_raw(x=1,\n    y=2)` will call the parent tool with `x=1` and `y=2`.\n\n    Args:\n        **kwargs: Arguments to pass directly to the parent tool (using original names).\n\n    Returns:\n        The ToolResult from the parent tool execution.\n\n    Raises:\n        RuntimeError: If called outside a transformed tool context.\n    \"\"\"\n    tool = _current_tool.get()\n    if tool is None:\n        raise RuntimeError(\"forward_raw() can only be called within a transformed tool\")\n\n    return await tool.parent_tool.run(kwargs)\n\n\n@dataclass(kw_only=True)\nclass ArgTransform:\n    \"\"\"Configuration for transforming a parent tool's argument.\n\n    This class allows fine-grained control over how individual arguments are transformed\n    when creating a new tool from an existing one. You can rename arguments, change their\n    descriptions, add default values, or hide them from clients while passing constants.\n\n    Attributes:\n        name: New name for the argument. Use None to keep original name, or ... for no change.\n        description: New description for the argument. Use None to remove description, or ... for no change.\n        default: New default value for the argument. Use ... for no change.\n        default_factory: Callable that returns a default value. Cannot be used with default.\n        type: New type for the argument. Use ... for no change.\n        hide: If True, hide this argument from clients but pass a constant value to parent.\n        required: If True, make argument required (remove default). Use ... for no change.\n        examples: Examples for the argument. Use ... for no change.\n\n    Examples:\n        Rename argument 'old_name' to 'new_name'\n        ```python\n        ArgTransform(name=\"new_name\")\n        ```\n\n        Change description only\n        ```python\n        ArgTransform(description=\"Updated description\")\n        ```\n\n        Add a default value (makes argument optional)\n        ```python\n        ArgTransform(default=42)\n        ```\n\n        Add a default factory (makes argument optional)\n        ```python\n        ArgTransform(default_factory=lambda: time.time())\n        ```\n\n        Change the type\n        ```python\n        ArgTransform(type=str)\n        ```\n\n        Hide the argument entirely from clients\n        ```python\n        ArgTransform(hide=True)\n        ```\n\n        Hide argument but pass a constant value to parent\n        ```python\n        ArgTransform(hide=True, default=\"constant_value\")\n        ```\n\n        Hide argument but pass a factory-generated value to parent\n        ```python\n        ArgTransform(hide=True, default_factory=lambda: uuid.uuid4().hex)\n        ```\n\n        Make an optional parameter required (removes any default)\n        ```python\n        ArgTransform(required=True)\n        ```\n\n        Combine multiple transformations\n        ```python\n        ArgTransform(name=\"new_name\", description=\"New desc\", default=None, type=int)\n        ```\n    \"\"\"\n\n    name: str | NotSetT = NotSet\n    description: str | NotSetT = NotSet\n    default: Any | NotSetT = NotSet\n    default_factory: Callable[[], Any] | NotSetT = NotSet\n    type: Any | NotSetT = NotSet\n    hide: bool = False\n    required: Literal[True] | NotSetT = NotSet\n    examples: Any | NotSetT = NotSet\n\n    def __post_init__(self):\n        \"\"\"Validate that only one of default or default_factory is provided.\"\"\"\n        has_default = self.default is not NotSet\n        has_factory = self.default_factory is not NotSet\n\n        if has_default and has_factory:\n            raise ValueError(\n                \"Cannot specify both 'default' and 'default_factory' in ArgTransform. \"\n                \"Use either 'default' for a static value or 'default_factory' for a callable.\"\n            )\n\n        if has_factory and not self.hide:\n            raise ValueError(\n                \"default_factory can only be used with hide=True. \"\n                \"Visible parameters must use static 'default' values since JSON schema \"\n                \"cannot represent dynamic factories.\"\n            )\n\n        if self.required is True and (has_default or has_factory):\n            raise ValueError(\n                \"Cannot specify 'required=True' with 'default' or 'default_factory'. \"\n                \"Required parameters cannot have defaults.\"\n            )\n\n        if self.hide and self.required is True:\n            raise ValueError(\n                \"Cannot specify both 'hide=True' and 'required=True'. \"\n                \"Hidden parameters cannot be required since clients cannot provide them.\"\n            )\n\n        if self.required is False:\n            raise ValueError(\n                \"Cannot specify 'required=False'. Set a default value instead.\"\n            )\n\n\nclass ArgTransformConfig(FastMCPBaseModel):\n    \"\"\"A model for requesting a single argument transform.\"\"\"\n\n    name: str | None = Field(default=None, description=\"The new name for the argument.\")\n    description: str | None = Field(\n        default=None, description=\"The new description for the argument.\"\n    )\n    default: str | int | float | bool | None = Field(\n        default=None, description=\"The new default value for the argument.\"\n    )\n    hide: bool = Field(\n        default=False, description=\"Whether to hide the argument from the tool.\"\n    )\n    required: Literal[True] | None = Field(\n        default=None, description=\"Whether the argument is required.\"\n    )\n    examples: Any | None = Field(default=None, description=\"Examples of the argument.\")\n\n    def to_arg_transform(self) -> ArgTransform:\n        \"\"\"Convert the argument transform to a FastMCP argument transform.\"\"\"\n\n        return ArgTransform(**self.model_dump(exclude_unset=True))  # pyright: ignore[reportAny]\n\n\nclass TransformedTool(Tool):\n    \"\"\"A tool that is transformed from another tool.\n\n    This class represents a tool that has been created by transforming another tool.\n    It supports argument renaming, schema modification, custom function injection,\n    structured output control, and provides context for the forward() and forward_raw() functions.\n\n    The transformation can be purely schema-based (argument renaming, dropping, etc.)\n    or can include a custom function that uses forward() to call the parent tool\n    with transformed arguments. Output schemas and structured outputs are automatically\n    inherited from the parent tool but can be overridden or disabled.\n\n    Attributes:\n        parent_tool: The original tool that this tool was transformed from.\n        fn: The function to execute when this tool is called (either the forwarding\n            function for pure transformations or a custom user function).\n        forwarding_fn: Internal function that handles argument transformation and\n            validation when forward() is called from custom functions.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"allow\", arbitrary_types_allowed=True)\n\n    parent_tool: SkipJsonSchema[Tool]\n    fn: SkipJsonSchema[Callable[..., Any]]\n    forwarding_fn: SkipJsonSchema[\n        Callable[..., Any]\n    ]  # Always present, handles arg transformation\n    transform_args: dict[str, ArgTransform]\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        \"\"\"Run the tool with context set for forward() functions.\n\n        This method executes the tool's function while setting up the context\n        that allows forward() and forward_raw() to work correctly within custom\n        functions.\n\n        Args:\n            arguments: Dictionary of arguments to pass to the tool's function.\n\n        Returns:\n            ToolResult object containing content and optional structured output.\n        \"\"\"\n\n        # Fill in missing arguments with schema defaults to ensure\n        # ArgTransform defaults take precedence over function defaults\n        arguments = arguments.copy()\n        properties = self.parameters.get(\"properties\", {})\n\n        for param_name, param_schema in properties.items():\n            if param_name not in arguments and \"default\" in param_schema:\n                # Check if this parameter has a default_factory from transform_args\n                # We need to call the factory for each run, not use the cached schema value\n                has_factory_default = False\n                if self.transform_args:\n                    # Find the original parameter name that maps to this param_name\n                    for orig_name, transform in self.transform_args.items():\n                        transform_name = (\n                            transform.name\n                            if transform.name is not NotSet\n                            else orig_name\n                        )\n                        if (\n                            transform_name == param_name\n                            and transform.default_factory is not NotSet\n                        ):\n                            # Type check to ensure default_factory is callable\n                            if callable(transform.default_factory):\n                                arguments[param_name] = transform.default_factory()\n                                has_factory_default = True\n                                break\n\n                if not has_factory_default:\n                    arguments[param_name] = param_schema[\"default\"]\n\n        token = _current_tool.set(self)\n        try:\n            result = await self.fn(**arguments)\n\n            # If transform function returns ToolResult, respect our output_schema setting\n            if isinstance(result, ToolResult):\n                if self.output_schema is None:\n                    return result\n                elif self.output_schema.get(\n                    \"type\"\n                ) != \"object\" and not self.output_schema.get(\"x-fastmcp-wrap-result\"):\n                    # Non-object explicit schemas disable structured content\n                    return ToolResult(\n                        content=result.content,\n                        structured_content=None,\n                    )\n                else:\n                    return result\n\n            # Otherwise convert to content and create ToolResult with proper structured content\n\n            unstructured_result = _convert_to_content(\n                result, serializer=self.serializer\n            )\n\n            structured_output = None\n            # First handle structured content based on output schema, if any\n            if self.output_schema is not None:\n                if self.output_schema.get(\"x-fastmcp-wrap-result\"):\n                    # Schema says wrap - always wrap in result key\n                    structured_output = {\"result\": result}\n                else:\n                    structured_output = result\n            # If no output schema, try to serialize the result. If it is a dict, use\n            # it as structured content. If it is not a dict, ignore it.\n            if structured_output is None:\n                try:\n                    structured_output = pydantic_core.to_jsonable_python(result)\n                    if not isinstance(structured_output, dict):\n                        structured_output = None\n                except Exception:\n                    pass\n\n            return ToolResult(\n                content=unstructured_result,\n                structured_content=structured_output,\n            )\n        finally:\n            _current_tool.reset(token)\n\n    @classmethod\n    def from_tool(\n        cls,\n        tool: Tool | Callable[..., Any],\n        name: str | None = None,\n        version: str | NotSetT | None = NotSet,\n        title: str | NotSetT | None = NotSet,\n        description: str | NotSetT | None = NotSet,\n        tags: set[str] | None = None,\n        transform_fn: Callable[..., Any] | None = None,\n        transform_args: dict[str, ArgTransform] | None = None,\n        annotations: ToolAnnotations | NotSetT | None = NotSet,\n        output_schema: dict[str, Any] | NotSetT | None = NotSet,\n        serializer: Callable[[Any], str] | NotSetT | None = NotSet,  # Deprecated\n        meta: dict[str, Any] | NotSetT | None = NotSet,\n    ) -> TransformedTool:\n        \"\"\"Create a transformed tool from a parent tool.\n\n        Args:\n            tool: The parent tool to transform.\n            transform_fn: Optional custom function. Can use forward() and forward_raw()\n                to call the parent tool. Functions with **kwargs receive transformed\n                argument names.\n            name: New name for the tool. Defaults to parent tool's name.\n            version: New version for the tool. Defaults to parent tool's version.\n            title: New title for the tool. Defaults to parent tool's title.\n            transform_args: Optional transformations for parent tool arguments.\n                Only specified arguments are transformed, others pass through unchanged:\n                - Simple rename (str)\n                - Complex transformation (rename/description/default/drop) (ArgTransform)\n                - Drop the argument (None)\n            description: New description. Defaults to parent's description.\n            tags: New tags. Defaults to parent's tags.\n            annotations: New annotations. Defaults to parent's annotations.\n            output_schema: Control output schema for structured outputs:\n                - None (default): Inherit from transform_fn if available, then parent tool\n                - dict: Use custom output schema\n                - False: Disable output schema and structured outputs\n            serializer: Deprecated. Return ToolResult from your tools for full control over serialization.\n            meta: Control meta information:\n                - NotSet (default): Inherit from parent tool\n                - dict: Use custom meta information\n                - None: Remove meta information\n\n        Returns:\n            TransformedTool with the specified transformations.\n\n        Examples:\n            # Transform specific arguments only\n            ```python\n            Tool.from_tool(parent, transform_args={\"old\": \"new\"})  # Others unchanged\n            ```\n\n            # Custom function with partial transforms\n            ```python\n            async def custom(x: int, y: int) -> str:\n                result = await forward(x=x, y=y)\n                return f\"Custom: {result}\"\n\n            Tool.from_tool(parent, transform_fn=custom, transform_args={\"a\": \"x\", \"b\": \"y\"})\n            ```\n\n            # Using **kwargs (gets all args, transformed and untransformed)\n            ```python\n            async def flexible(**kwargs) -> str:\n                result = await forward(**kwargs)\n                return f\"Got: {kwargs}\"\n\n            Tool.from_tool(parent, transform_fn=flexible, transform_args={\"a\": \"x\"})\n            ```\n\n            # Control structured outputs and schemas\n            ```python\n            # Custom output schema\n            Tool.from_tool(parent, output_schema={\n                \"type\": \"object\",\n                \"properties\": {\"status\": {\"type\": \"string\"}}\n            })\n\n            # Disable structured outputs\n            Tool.from_tool(parent, output_schema=None)\n\n            # Return ToolResult for full control\n            async def custom_output(**kwargs) -> ToolResult:\n                result = await forward(**kwargs)\n                return ToolResult(\n                    content=[TextContent(text=\"Summary\")],\n                    structured_content={\"processed\": True}\n                )\n            ```\n        \"\"\"\n        tool = Tool._ensure_tool(tool)\n\n        if (\n            serializer is not NotSet\n            and serializer is not None\n            and fastmcp.settings.deprecation_warnings\n        ):\n            warnings.warn(\n                \"The `serializer` parameter is deprecated. \"\n                \"Return ToolResult from your tools for full control over serialization. \"\n                \"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        transform_args = transform_args or {}\n\n        if transform_fn is not None:\n            parsed_fn = ParsedFunction.from_function(transform_fn, validate=False)\n        else:\n            parsed_fn = None\n\n        # Validate transform_args\n        parent_params = set(tool.parameters.get(\"properties\", {}).keys())\n        unknown_args = set(transform_args.keys()) - parent_params\n        if unknown_args:\n            raise ValueError(\n                f\"Unknown arguments in transform_args: {', '.join(sorted(unknown_args))}. \"\n                f\"Parent tool `{tool.name}` has: {', '.join(sorted(parent_params))}\"\n            )\n\n        # Always create the forwarding transform\n        schema, forwarding_fn = cls._create_forwarding_transform(tool, transform_args)\n\n        # Handle output schema\n        if output_schema is NotSet:\n            # Use smart fallback: try custom function, then parent\n            if transform_fn is not None:\n                # parsed fn is not none here\n                final_output_schema = cast(ParsedFunction, parsed_fn).output_schema\n                if final_output_schema is None:\n                    # Check if function returns ToolResult (or subclass) - if so, don't fall back to parent.\n                    # Use parsed_fn.return_type (resolved via get_type_hints) instead of\n                    # inspect.signature, which returns strings under `from __future__ import annotations`.\n                    return_type = cast(ParsedFunction, parsed_fn).return_type\n                    if issubclass_safe(return_type, ToolResult):\n                        final_output_schema = None\n                    else:\n                        final_output_schema = tool.output_schema\n            else:\n                final_output_schema = tool.output_schema\n        else:\n            final_output_schema = cast(dict | None, output_schema)\n\n        if transform_fn is None:\n            # User wants pure transformation - use forwarding_fn as the main function\n            final_fn = forwarding_fn\n            final_schema = schema\n        else:\n            # parsed fn is not none here\n            parsed_fn = cast(ParsedFunction, parsed_fn)\n            # User provided custom function - merge schemas\n            final_fn = transform_fn\n\n            has_kwargs = cls._function_has_kwargs(transform_fn)\n\n            # Validate function parameters against transformed schema\n            fn_params = set(parsed_fn.input_schema.get(\"properties\", {}).keys())\n            transformed_params = set(schema.get(\"properties\", {}).keys())\n\n            if not has_kwargs:\n                # Without **kwargs, function must declare all transformed params\n                # Check if function is missing any parameters required after transformation\n                missing_params = transformed_params - fn_params\n                if missing_params:\n                    raise ValueError(\n                        f\"Function missing parameters required after transformation: \"\n                        f\"{', '.join(sorted(missing_params))}. \"\n                        f\"Function declares: {', '.join(sorted(fn_params))}\"\n                    )\n\n                # ArgTransform takes precedence over function signature\n                # Start with function schema as base, then override with transformed schema\n                final_schema = cls._merge_schema_with_precedence(\n                    parsed_fn.input_schema, schema\n                )\n            else:\n                # With **kwargs, function can access all transformed params\n                # ArgTransform takes precedence over function signature\n                # No validation needed - kwargs makes everything accessible\n\n                # Start with function schema as base, then override with transformed schema\n                final_schema = cls._merge_schema_with_precedence(\n                    parsed_fn.input_schema, schema\n                )\n\n        # Additional validation: check for naming conflicts after transformation\n        if transform_args:\n            new_names = []\n            for old_name in parent_params:\n                transform = transform_args.get(old_name, ArgTransform())\n\n                if transform.hide:\n                    continue\n\n                if transform.name is not NotSet:\n                    new_names.append(transform.name)\n                else:\n                    new_names.append(old_name)\n\n            # Check for duplicate names after transformation\n            name_counts = {}\n            for arg_name in new_names:\n                name_counts[arg_name] = name_counts.get(arg_name, 0) + 1\n\n            duplicates = [\n                arg_name for arg_name, count in name_counts.items() if count > 1\n            ]\n            if duplicates:\n                raise ValueError(\n                    f\"Multiple arguments would be mapped to the same names: \"\n                    f\"{', '.join(sorted(duplicates))}\"\n                )\n\n        final_name = name or tool.name\n        final_version = version if not isinstance(version, NotSetT) else tool.version\n        final_description = (\n            description if not isinstance(description, NotSetT) else tool.description\n        )\n        final_title = title if not isinstance(title, NotSetT) else tool.title\n        final_meta = meta if not isinstance(meta, NotSetT) else tool.meta\n        final_annotations = (\n            annotations if not isinstance(annotations, NotSetT) else tool.annotations\n        )\n        final_serializer = (\n            serializer if not isinstance(serializer, NotSetT) else tool.serializer\n        )\n\n        transformed_tool = cls(\n            fn=final_fn,\n            forwarding_fn=forwarding_fn,\n            parent_tool=tool,\n            name=final_name,\n            version=final_version,\n            title=final_title,\n            description=final_description,\n            parameters=final_schema,\n            output_schema=final_output_schema,\n            tags=tags or tool.tags,\n            annotations=final_annotations,\n            serializer=final_serializer,\n            meta=final_meta,\n            transform_args=transform_args,\n            auth=tool.auth,\n        )\n\n        return transformed_tool\n\n    @classmethod\n    def _create_forwarding_transform(\n        cls,\n        parent_tool: Tool,\n        transform_args: dict[str, ArgTransform] | None,\n    ) -> tuple[dict[str, Any], Callable[..., Any]]:\n        \"\"\"Create schema and forwarding function that encapsulates all transformation logic.\n\n        This method builds a new JSON schema for the transformed tool and creates a\n        forwarding function that validates arguments against the new schema and maps\n        them back to the parent tool's expected arguments.\n\n        Args:\n            parent_tool: The original tool to transform.\n            transform_args: Dictionary defining how to transform each argument.\n\n        Returns:\n            A tuple containing:\n            - The new JSON schema for the transformed tool as a dictionary\n            - Async function that validates and forwards calls to the parent tool\n        \"\"\"\n\n        # Build transformed schema and mapping\n        # Deep copy to prevent compress_schema from mutating parent tool's $defs\n        parent_defs = deepcopy(parent_tool.parameters.get(\"$defs\", {}))\n        parent_props = parent_tool.parameters.get(\"properties\", {}).copy()\n        parent_required = set(parent_tool.parameters.get(\"required\", []))\n\n        new_props = {}\n        new_required = set()\n        new_to_old = {}\n        hidden_defaults = {}  # Track hidden parameters with constant values\n\n        for old_name, old_schema in parent_props.items():\n            # Check if parameter is in transform_args\n            if transform_args and old_name in transform_args:\n                transform = transform_args[old_name]\n            else:\n                # Default behavior - pass through (no transformation)\n                transform = ArgTransform()  # Default ArgTransform with no changes\n\n            # Handle hidden parameters with defaults\n            if transform.hide:\n                # Validate that hidden parameters without user defaults have parent defaults\n                has_user_default = (\n                    transform.default is not NotSet\n                    or transform.default_factory is not NotSet\n                )\n                if not has_user_default and old_name in parent_required:\n                    raise ValueError(\n                        f\"Hidden parameter '{old_name}' has no default value in parent tool \"\n                        f\"and no default or default_factory provided in ArgTransform. Either provide a default \"\n                        f\"or default_factory in ArgTransform or don't hide required parameters.\"\n                    )\n                if has_user_default:\n                    # Store info for later factory calling or direct value\n                    hidden_defaults[old_name] = transform\n                # Skip adding to schema (not exposed to clients)\n                continue\n\n            transform_result = cls._apply_single_transform(\n                old_name,\n                old_schema,\n                transform,\n                old_name in parent_required,\n            )\n\n            if transform_result:\n                new_name, new_schema, is_required = transform_result\n                new_props[new_name] = new_schema\n                new_to_old[new_name] = old_name\n                if is_required:\n                    new_required.add(new_name)\n\n        schema = {\n            \"type\": \"object\",\n            \"properties\": new_props,\n            \"required\": list(new_required),\n            \"additionalProperties\": False,\n        }\n\n        if parent_defs:\n            schema[\"$defs\"] = parent_defs\n            schema = compress_schema(schema)\n\n        # Create forwarding function that closes over everything it needs\n        async def _forward(**kwargs: Any):\n            # Validate arguments\n            valid_args = set(new_props.keys())\n            provided_args = set(kwargs.keys())\n            unknown_args = provided_args - valid_args\n\n            if unknown_args:\n                raise TypeError(\n                    f\"Got unexpected keyword argument(s): {', '.join(sorted(unknown_args))}\"\n                )\n\n            # Check required arguments\n            missing_args = new_required - provided_args\n            if missing_args:\n                raise TypeError(\n                    f\"Missing required argument(s): {', '.join(sorted(missing_args))}\"\n                )\n\n            # Map arguments to parent names\n            parent_args = {}\n            for new_name, value in kwargs.items():\n                old_name = new_to_old.get(new_name, new_name)\n                parent_args[old_name] = value\n\n            # Add hidden defaults (constant values for hidden parameters)\n            for old_name, transform in hidden_defaults.items():\n                if transform.default is not NotSet:\n                    parent_args[old_name] = transform.default\n                elif transform.default_factory is not NotSet:\n                    # Type check to ensure default_factory is callable\n                    if callable(transform.default_factory):\n                        parent_args[old_name] = transform.default_factory()\n\n            return await parent_tool.run(parent_args)\n\n        return schema, _forward\n\n    @staticmethod\n    def _apply_single_transform(\n        old_name: str,\n        old_schema: dict[str, Any],\n        transform: ArgTransform,\n        is_required: bool,\n    ) -> tuple[str, dict[str, Any], bool] | None:\n        \"\"\"Apply transformation to a single parameter.\n\n        This method handles the transformation of a single argument according to\n        the specified transformation rules.\n\n        Args:\n            old_name: Original name of the parameter.\n            old_schema: Original JSON schema for the parameter.\n            transform: ArgTransform object specifying how to transform the parameter.\n            is_required: Whether the original parameter was required.\n\n        Returns:\n            Tuple of (new_name, new_schema, new_is_required) if parameter should be kept,\n            None if parameter should be dropped.\n        \"\"\"\n        if transform.hide:\n            return None\n\n        # Handle name transformation - ensure we always have a string\n        if transform.name is not NotSet:\n            new_name = transform.name if transform.name is not None else old_name\n        else:\n            new_name = old_name\n\n        # Ensure new_name is always a string\n        if not isinstance(new_name, str):\n            new_name = old_name\n\n        new_schema = old_schema.copy()\n\n        # Handle description transformation\n        if transform.description is not NotSet:\n            if transform.description is None:\n                new_schema.pop(\"description\", None)  # Remove description\n            else:\n                new_schema[\"description\"] = transform.description\n\n        # Handle required transformation first\n        if transform.required is not NotSet:\n            is_required = bool(transform.required)\n            if transform.required is True:\n                # Remove any existing default when making required\n                new_schema.pop(\"default\", None)\n\n        # Handle default value transformation (only if not making required)\n        if transform.default is not NotSet and transform.required is not True:\n            new_schema[\"default\"] = transform.default\n            is_required = False\n\n        # Handle type transformation\n        if transform.type is not NotSet:\n            # Use TypeAdapter to get proper JSON schema for the type\n            type_schema = get_cached_typeadapter(transform.type).json_schema()\n            # Update the schema with the type information from TypeAdapter\n            new_schema.update(type_schema)\n\n        # Handle examples transformation\n        if transform.examples is not NotSet:\n            new_schema[\"examples\"] = transform.examples\n\n        return new_name, new_schema, is_required\n\n    @staticmethod\n    def _merge_schema_with_precedence(\n        base_schema: dict[str, Any], override_schema: dict[str, Any]\n    ) -> dict[str, Any]:\n        \"\"\"Merge two schemas, with the override schema taking precedence.\n\n        Args:\n            base_schema: Base schema to start with\n            override_schema: Schema that takes precedence for overlapping properties\n\n        Returns:\n            Merged schema with override taking precedence\n        \"\"\"\n        merged_props = base_schema.get(\"properties\", {}).copy()\n        merged_required = set(base_schema.get(\"required\", []))\n\n        override_props = override_schema.get(\"properties\", {})\n        override_required = set(override_schema.get(\"required\", []))\n\n        # Override properties\n        for param_name, param_schema in override_props.items():\n            if param_name in merged_props:\n                # Merge the schemas, with override taking precedence\n                base_param = merged_props[param_name].copy()\n                base_param.update(param_schema)\n                merged_props[param_name] = base_param\n            else:\n                merged_props[param_name] = param_schema.copy()\n\n        # Handle required parameters - override takes complete precedence\n        # Start with override's required set\n        final_required = override_required.copy()\n\n        # For parameters not in override, inherit base requirement status\n        # but only if they don't have a default in the final merged properties\n        for param_name in merged_required:\n            if param_name not in override_props:\n                # Parameter not mentioned in override, keep base requirement status\n                final_required.add(param_name)\n            elif (\n                param_name in override_props\n                and \"default\" not in merged_props[param_name]\n            ):\n                # Parameter in override but no default, keep required if it was required in base\n                if param_name not in override_required:\n                    # Override doesn't specify it as required, and it has no default,\n                    # so inherit from base\n                    final_required.add(param_name)\n\n        # Remove any parameters that have defaults (they become optional)\n        for param_name, param_schema in merged_props.items():\n            if \"default\" in param_schema:\n                final_required.discard(param_name)\n\n        # Merge $defs from both schemas, with override taking precedence\n        merged_defs = base_schema.get(\"$defs\", {}).copy()\n        override_defs = override_schema.get(\"$defs\", {})\n\n        for def_name, def_schema in override_defs.items():\n            if def_name in merged_defs:\n                base_def = merged_defs[def_name].copy()\n                base_def.update(def_schema)\n                merged_defs[def_name] = base_def\n            else:\n                merged_defs[def_name] = def_schema.copy()\n\n        result = {\n            \"type\": \"object\",\n            \"properties\": merged_props,\n            \"required\": list(final_required),\n            \"additionalProperties\": False,\n        }\n\n        if merged_defs:\n            result[\"$defs\"] = merged_defs\n            result = compress_schema(result)\n\n        return result\n\n    @staticmethod\n    def _function_has_kwargs(fn: Callable[..., Any]) -> bool:\n        \"\"\"Check if function accepts **kwargs.\n\n        This determines whether a custom function can accept arbitrary keyword arguments,\n        which affects how schemas are merged during tool transformation.\n\n        Args:\n            fn: Function to inspect.\n\n        Returns:\n            True if the function has a **kwargs parameter, False otherwise.\n        \"\"\"\n        sig = inspect.signature(fn)\n        return any(\n            p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()\n        )\n\n\ndef _set_visibility_metadata(tool: Tool, *, enabled: bool) -> None:\n    \"\"\"Set visibility state in tool metadata.\n\n    This uses the same metadata format as the Visibility transform,\n    so tools marked here will be filtered by the standard visibility system.\n\n    Args:\n        tool: Tool to mark.\n        enabled: Whether the tool should be visible to clients.\n    \"\"\"\n    # Import here to avoid circular imports\n    from fastmcp.server.transforms.visibility import _FASTMCP_KEY, _INTERNAL_KEY\n\n    if tool.meta is None:\n        tool.meta = {_FASTMCP_KEY: {_INTERNAL_KEY: {\"visibility\": enabled}}}\n    else:\n        old_fastmcp = tool.meta.get(_FASTMCP_KEY, {})\n        old_internal = old_fastmcp.get(_INTERNAL_KEY, {})\n        new_internal = {**old_internal, \"visibility\": enabled}\n        new_fastmcp = {**old_fastmcp, _INTERNAL_KEY: new_internal}\n        tool.meta = {**tool.meta, _FASTMCP_KEY: new_fastmcp}\n\n\nclass ToolTransformConfig(FastMCPBaseModel):\n    \"\"\"Provides a way to transform a tool.\"\"\"\n\n    name: str | None = Field(default=None, description=\"The new name for the tool.\")\n    version: str | None = Field(\n        default=None, description=\"The new version for the tool.\"\n    )\n    title: str | None = Field(\n        default=None,\n        description=\"The new title of the tool.\",\n    )\n    description: str | None = Field(\n        default=None,\n        description=\"The new description of the tool.\",\n    )\n    tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(\n        default_factory=set,\n        description=\"The new tags for the tool.\",\n    )\n    meta: dict[str, Any] | None = Field(\n        default=None,\n        description=\"The new meta information for the tool.\",\n    )\n    enabled: bool = Field(\n        default=True,\n        description=\"Whether the tool is enabled. If False, the tool will be hidden from clients.\",\n    )\n\n    arguments: dict[str, ArgTransformConfig] = Field(\n        default_factory=dict,\n        description=\"A dictionary of argument transforms to apply to the tool.\",\n    )\n\n    def apply(self, tool: Tool) -> TransformedTool:\n        \"\"\"Create a TransformedTool from a provided tool and this transformation configuration.\"\"\"\n\n        tool_changes: dict[str, Any] = self.model_dump(\n            exclude_unset=True, exclude={\"arguments\", \"enabled\"}\n        )\n\n        transformed = TransformedTool.from_tool(\n            tool=tool,\n            **tool_changes,\n            transform_args={k: v.to_arg_transform() for k, v in self.arguments.items()},\n        )\n\n        # Set visibility metadata if enabled was explicitly provided.\n        # This allows enabled=True to override an earlier disable (later transforms win).\n        if \"enabled\" in self.model_fields_set:\n            _set_visibility_metadata(transformed, enabled=self.enabled)\n\n        return transformed\n\n\ndef apply_transformations_to_tools(\n    tools: dict[str, Tool],\n    transformations: dict[str, ToolTransformConfig],\n) -> dict[str, Tool]:\n    \"\"\"Apply a list of transformations to a list of tools. Tools that do not have any transformations\n    are left unchanged.\n\n    Note: tools dict is keyed by prefixed key (e.g., \"tool:my_tool\"),\n    but transformations are keyed by tool name (e.g., \"my_tool\").\n    \"\"\"\n\n    transformed_tools: dict[str, Tool] = {}\n\n    for tool_key, tool in tools.items():\n        # Look up transformation by tool name, not prefixed key\n        if transformation := transformations.get(tool.name):\n            transformed = transformation.apply(tool)\n            transformed_tools[transformed.key] = transformed\n            continue\n\n        transformed_tools[tool_key] = tool\n\n    return transformed_tools\n"
  },
  {
    "path": "src/fastmcp/utilities/__init__.py",
    "content": "\"\"\"FastMCP utility modules.\"\"\"\n"
  },
  {
    "path": "src/fastmcp/utilities/async_utils.py",
    "content": "\"\"\"Async utilities for FastMCP.\"\"\"\n\nimport asyncio\nimport functools\nimport inspect\nfrom collections.abc import Awaitable, Callable\nfrom typing import Any, Literal, TypeVar, overload\n\nimport anyio\nfrom anyio.to_thread import run_sync as run_sync_in_threadpool\n\nT = TypeVar(\"T\")\n\n\ndef is_coroutine_function(fn: Any) -> bool:\n    \"\"\"Check if a callable is a coroutine function, unwrapping functools.partial.\n\n    ``inspect.iscoroutinefunction`` returns ``False`` for\n    ``functools.partial`` objects wrapping an async function on Python < 3.12.\n    This helper unwraps any layers of ``partial`` before checking.\n    \"\"\"\n    while isinstance(fn, functools.partial):\n        fn = fn.func\n    return inspect.iscoroutinefunction(fn) or asyncio.iscoroutinefunction(fn)\n\n\nasync def call_sync_fn_in_threadpool(\n    fn: Callable[..., Any], *args: Any, **kwargs: Any\n) -> Any:\n    \"\"\"Call a sync function in a threadpool to avoid blocking the event loop.\n\n    Uses anyio.to_thread.run_sync which properly propagates contextvars,\n    making this safe for functions that depend on context (like dependency injection).\n    \"\"\"\n    return await run_sync_in_threadpool(functools.partial(fn, *args, **kwargs))\n\n\n@overload\nasync def gather(\n    *awaitables: Awaitable[T],\n    return_exceptions: Literal[True],\n) -> list[T | BaseException]: ...\n\n\n@overload\nasync def gather(\n    *awaitables: Awaitable[T],\n    return_exceptions: Literal[False] = ...,\n) -> list[T]: ...\n\n\nasync def gather(\n    *awaitables: Awaitable[T],\n    return_exceptions: bool = False,\n) -> list[T] | list[T | BaseException]:\n    \"\"\"Run awaitables concurrently and return results in order.\n\n    Uses anyio TaskGroup for structured concurrency.\n\n    Args:\n        *awaitables: Awaitables to run concurrently\n        return_exceptions: If True, exceptions are returned in results.\n                          If False, first exception cancels all and raises.\n\n    Returns:\n        List of results in the same order as input awaitables.\n    \"\"\"\n    results: list[T | BaseException] = [None] * len(awaitables)  # type: ignore[assignment]\n\n    async def run_at(i: int, aw: Awaitable[T]) -> None:\n        try:\n            results[i] = await aw\n        except BaseException as e:\n            if return_exceptions:\n                results[i] = e\n            else:\n                raise\n\n    async with anyio.create_task_group() as tg:\n        for i, aw in enumerate(awaitables):\n            tg.start_soon(run_at, i, aw)\n\n    return results\n"
  },
  {
    "path": "src/fastmcp/utilities/auth.py",
    "content": "\"\"\"Authentication utility helpers.\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nfrom typing import Any\n\n\ndef _decode_jwt_part(token: str, part_index: int) -> dict[str, Any]:\n    \"\"\"Decode a JWT part (header or payload) without signature verification.\n\n    Args:\n        token: JWT token string (header.payload.signature)\n        part_index: 0 for header, 1 for payload\n\n    Returns:\n        Decoded part as a dictionary\n\n    Raises:\n        ValueError: If token is not a valid JWT format\n    \"\"\"\n    parts = token.split(\".\")\n    if len(parts) != 3:\n        raise ValueError(\"Invalid JWT format (expected 3 parts)\")\n\n    part_b64 = parts[part_index]\n    part_b64 += \"=\" * (-len(part_b64) % 4)  # Add padding\n    return json.loads(base64.urlsafe_b64decode(part_b64))\n\n\ndef decode_jwt_header(token: str) -> dict[str, Any]:\n    \"\"\"Decode JWT header without signature verification.\n\n    Useful for extracting the key ID (kid) for JWKS lookup.\n\n    Args:\n        token: JWT token string (header.payload.signature)\n\n    Returns:\n        Decoded header as a dictionary\n\n    Raises:\n        ValueError: If token is not a valid JWT format\n    \"\"\"\n    return _decode_jwt_part(token, 0)\n\n\ndef decode_jwt_payload(token: str) -> dict[str, Any]:\n    \"\"\"Decode JWT payload without signature verification.\n\n    Use only for tokens received directly from trusted sources (e.g., IdP token endpoints).\n\n    Args:\n        token: JWT token string (header.payload.signature)\n\n    Returns:\n        Decoded payload as a dictionary\n\n    Raises:\n        ValueError: If token is not a valid JWT format\n    \"\"\"\n    return _decode_jwt_part(token, 1)\n\n\ndef parse_scopes(value: Any) -> list[str] | None:\n    \"\"\"Parse scopes from environment variables or settings values.\n\n    Accepts either a JSON array string, a comma- or space-separated string,\n    a list of strings, or ``None``. Returns a list of scopes or ``None`` if\n    no value is provided.\n    \"\"\"\n    if value is None or value == \"\":\n        return None if value is None else []\n    if isinstance(value, list):\n        return [str(v).strip() for v in value if str(v).strip()]\n    if isinstance(value, str):\n        value = value.strip()\n        if not value:\n            return []\n        # Try JSON array first\n        if value.startswith(\"[\"):\n            try:\n                data = json.loads(value)\n                if isinstance(data, list):\n                    return [str(v).strip() for v in data if str(v).strip()]\n            except Exception:\n                pass\n        # Fallback to comma/space separated list\n        return [s.strip() for s in value.replace(\",\", \" \").split() if s.strip()]\n    return value\n"
  },
  {
    "path": "src/fastmcp/utilities/cli.py",
    "content": "from __future__ import annotations\n\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import ValidationError\nfrom rich.align import Align\nfrom rich.console import Console, Group\nfrom rich.panel import Panel\nfrom rich.table import Table\nfrom rich.text import Text\n\nimport fastmcp\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config import MCPServerConfig\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\nfrom fastmcp.utilities.types import get_cached_typeadapter\nfrom fastmcp.utilities.version_check import check_for_newer_version\n\nif TYPE_CHECKING:\n    from fastmcp import FastMCP\n\nlogger = get_logger(\"cli.config\")\n\n\ndef is_already_in_uv_subprocess() -> bool:\n    \"\"\"Check if we're already running in a FastMCP uv subprocess.\"\"\"\n    return bool(os.environ.get(\"FASTMCP_UV_SPAWNED\"))\n\n\ndef load_and_merge_config(\n    server_spec: str | None,\n    **cli_overrides,\n) -> tuple[MCPServerConfig, str]:\n    \"\"\"Load config from server_spec and apply CLI overrides.\n\n    This consolidates the config parsing logic that was duplicated across\n    run, inspect, and dev commands.\n\n    Args:\n        server_spec: Python file, config file, URL, or None to auto-detect\n        cli_overrides: CLI arguments that override config values\n\n    Returns:\n        Tuple of (MCPServerConfig, resolved_server_spec)\n    \"\"\"\n    config = None\n    config_path = None\n\n    # Auto-detect fastmcp.json if no server_spec provided\n    if server_spec is None:\n        config_path = Path(\"fastmcp.json\")\n        if not config_path.exists():\n            found_config = MCPServerConfig.find_config()\n            if found_config:\n                config_path = found_config\n            else:\n                logger.error(\n                    \"No server specification provided and no fastmcp.json found in current directory.\\n\"\n                    \"Please specify a server file or create a fastmcp.json configuration.\"\n                )\n                raise FileNotFoundError(\"No server specification or fastmcp.json found\")\n\n        resolved_spec = str(config_path)\n        logger.info(f\"Using configuration from {config_path}\")\n    else:\n        resolved_spec = server_spec\n\n    # Load config if server_spec is a .json file\n    if resolved_spec.endswith(\".json\"):\n        config_path = Path(resolved_spec)\n        if config_path.exists():\n            try:\n                with open(config_path) as f:\n                    data = json.load(f)\n\n                # Check if it's an MCPConfig first (has canonical mcpServers key)\n                if \"mcpServers\" in data:\n                    # MCPConfig - we don't process these here, just pass through\n                    pass\n                else:\n                    # Try to parse as MCPServerConfig\n                    try:\n                        adapter = get_cached_typeadapter(MCPServerConfig)\n                        config = adapter.validate_python(data)\n\n                        # Apply deployment settings\n                        if config.deployment:\n                            config.deployment.apply_runtime_settings(config_path)\n\n                    except ValidationError:\n                        # Not a valid MCPServerConfig, just pass through\n                        pass\n            except (json.JSONDecodeError, FileNotFoundError):\n                # Not a valid JSON file, just pass through\n                pass\n\n    # If we don't have a config object yet, create one from filesystem source\n    if config is None:\n        source = FileSystemSource(path=resolved_spec)\n        config = MCPServerConfig(source=source)\n\n    # Convert to dict for immutable transformation\n    config_dict = config.model_dump()\n\n    # Apply CLI overrides to config's environment (always exists due to default_factory)\n    if python_override := cli_overrides.get(\"python\"):\n        config_dict[\"environment\"][\"python\"] = python_override\n    if packages_override := cli_overrides.get(\"with_packages\"):\n        # Merge packages - CLI packages are added to config packages\n        existing = config_dict[\"environment\"].get(\"dependencies\") or []\n        config_dict[\"environment\"][\"dependencies\"] = packages_override + existing\n    if requirements_override := cli_overrides.get(\"with_requirements\"):\n        config_dict[\"environment\"][\"requirements\"] = str(requirements_override)\n    if project_override := cli_overrides.get(\"project\"):\n        config_dict[\"environment\"][\"project\"] = str(project_override)\n    if editable_override := cli_overrides.get(\"editable\"):\n        config_dict[\"environment\"][\"editable\"] = editable_override\n\n    # Apply deployment CLI overrides (always exists due to default_factory)\n    if transport_override := cli_overrides.get(\"transport\"):\n        config_dict[\"deployment\"][\"transport\"] = transport_override\n    if host_override := cli_overrides.get(\"host\"):\n        config_dict[\"deployment\"][\"host\"] = host_override\n    if port_override := cli_overrides.get(\"port\"):\n        config_dict[\"deployment\"][\"port\"] = port_override\n    if path_override := cli_overrides.get(\"path\"):\n        config_dict[\"deployment\"][\"path\"] = path_override\n    if log_level_override := cli_overrides.get(\"log_level\"):\n        config_dict[\"deployment\"][\"log_level\"] = log_level_override\n    if server_args_override := cli_overrides.get(\"server_args\"):\n        config_dict[\"deployment\"][\"args\"] = server_args_override\n\n    # Create new config from modified dict\n    new_config = MCPServerConfig(**config_dict)\n    return new_config, resolved_spec\n\n\nLOGO_ASCII_1 = r\"\"\"\n    _ __ ___  _____           __  __  _____________    ____    ____ \n   _ __ ___ .'____/___ ______/ /_/  |/  / ____/ __ \\  |___ \\  / __ \\\n  _ __ ___ / /_  / __ `/ ___/ __/ /|_/ / /   / /_/ /  ___/ / / / / /\n _ __ ___ / __/ / /_/ (__  ) /_/ /  / / /___/ ____/  /  __/_/ /_/ / \n_ __ ___ /_/    \\____/____/\\__/_/  /_/\\____/_/      /_____(*)____/  \n\n\"\"\".lstrip(\"\\n\")\n\n# This prints the below in a blue gradient\n#  █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█\n#  █▀  █▀█ ▄▄█  █  █ ▀ █ █▄▄ █▀▀\nLOGO_ASCII_2 = (\n    \"\\x1b[38;2;0;198;255m \\x1b[38;2;0;195;255m█\\x1b[38;2;0;192;255m▀\\x1b[38;2;0;189;255m▀\\x1b[38;2;0;186;255m \"\n    \"\\x1b[38;2;0;184;255m▄\\x1b[38;2;0;181;255m▀\\x1b[38;2;0;178;255m█\\x1b[38;2;0;175;255m \"\n    \"\\x1b[38;2;0;172;255m█\\x1b[38;2;0;169;255m▀\\x1b[38;2;0;166;255m▀\\x1b[38;2;0;163;255m \"\n    \"\\x1b[38;2;0;160;255m▀\\x1b[38;2;0;157;255m█\\x1b[38;2;0;155;255m▀\\x1b[38;2;0;152;255m \"\n    \"\\x1b[38;2;0;149;255m█\\x1b[38;2;0;146;255m▀\\x1b[38;2;0;143;255m▄\\x1b[38;2;0;140;255m▀\\x1b[38;2;0;137;255m█\\x1b[38;2;0;134;255m \"\n    \"\\x1b[38;2;0;131;255m█\\x1b[38;2;0;128;255m▀\\x1b[38;2;0;126;255m▀\\x1b[38;2;0;123;255m \"\n    \"\\x1b[38;2;0;120;255m█\\x1b[38;2;0;117;255m▀\\x1b[38;2;0;114;255m█\\x1b[39m\\n\"\n    \"\\x1b[38;2;0;198;255m \\x1b[38;2;0;195;255m█\\x1b[38;2;0;192;255m▀\\x1b[38;2;0;189;255m \\x1b[38;2;0;186;255m \"\n    \"\\x1b[38;2;0;184;255m█\\x1b[38;2;0;181;255m▀\\x1b[38;2;0;178;255m█\\x1b[38;2;0;175;255m \"\n    \"\\x1b[38;2;0;172;255m▄\\x1b[38;2;0;169;255m▄\\x1b[38;2;0;166;255m█\\x1b[38;2;0;163;255m \"\n    \"\\x1b[38;2;0;160;255m \\x1b[38;2;0;157;255m█\\x1b[38;2;0;155;255m \\x1b[38;2;0;152;255m \"\n    \"\\x1b[38;2;0;149;255m█\\x1b[38;2;0;146;255m \\x1b[38;2;0;143;255m▀\\x1b[38;2;0;140;255m \\x1b[38;2;0;137;255m█\\x1b[38;2;0;134;255m \"\n    \"\\x1b[38;2;0;131;255m█\\x1b[38;2;0;128;255m▄\\x1b[38;2;0;126;255m▄\\x1b[38;2;0;123;255m \"\n    \"\\x1b[38;2;0;120;255m█\\x1b[38;2;0;117;255m▀\\x1b[38;2;0;114;255m▀\\x1b[39m\"\n).strip()\n\n# Prints the below in a blue gradient - stylized F\n#  ▄▀▀▀\n#  █▀▀\n# ▀\nLOGO_ASCII_3 = (\n    \" \\x1b[38;2;0;170;255m▄\\x1b[38;2;0;142;255m▀\\x1b[38;2;0;114;255m▀\\x1b[38;2;0;86;255m▀\\x1b[39m\\n\"\n    \" \\x1b[38;2;0;170;255m█\\x1b[38;2;0;142;255m▀\\x1b[38;2;0;114;255m▀\\x1b[39m\\n\"\n    \"\\x1b[38;2;0;170;255m▀\\x1b[39m\\n\"\n    \"\\x1b[0m\"\n)\n\n# Prints the below in a blue gradient - block logo with slightly stylized F\n#  ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█\n#  █▀  █▀█ ▄▄█  █  █ ▀ █ █▄▄ █▀▀\n\nLOGO_ASCII_4 = (\n    \"\\x1b[38;2;0;198;255m \\x1b[38;2;0;195;255m▄\\x1b[38;2;0;192;255m▀\\x1b[38;2;0;189;255m▀\\x1b[38;2;0;186;255m \\x1b[38;2;0;184;255m▄\\x1b[38;2;0;181;255m▀\\x1b[38;2;0;178;255m█\\x1b[38;2;0;175;255m \"\n    \"\\x1b[38;2;0;172;255m█\\x1b[38;2;0;169;255m▀\\x1b[38;2;0;166;255m▀\\x1b[38;2;0;163;255m \"\n    \"\\x1b[38;2;0;160;255m▀\\x1b[38;2;0;157;255m█\\x1b[38;2;0;155;255m▀\\x1b[38;2;0;152;255m \"\n    \"\\x1b[38;2;0;149;255m█\\x1b[38;2;0;146;255m▀\\x1b[38;2;0;143;255m▄\\x1b[38;2;0;140;255m▀\\x1b[38;2;0;137;255m█\\x1b[38;2;0;134;255m \"\n    \"\\x1b[38;2;0;131;255m█\\x1b[38;2;0;128;255m▀\\x1b[38;2;0;126;255m▀\\x1b[38;2;0;123;255m \"\n    \"\\x1b[38;2;0;120;255m█\\x1b[38;2;0;117;255m▀\\x1b[38;2;0;114;255m█\\x1b[39m\\n\"\n    \"\\x1b[38;2;0;198;255m \\x1b[38;2;0;195;255m█\\x1b[38;2;0;192;255m▀\\x1b[38;2;0;189;255m \\x1b[38;2;0;186;255m \\x1b[38;2;0;184;255m█\\x1b[38;2;0;181;255m▀\\x1b[38;2;0;178;255m█\\x1b[38;2;0;175;255m \"\n    \"\\x1b[38;2;0;172;255m▄\\x1b[38;2;0;169;255m▄\\x1b[38;2;0;166;255m█\\x1b[38;2;0;163;255m \"\n    \"\\x1b[38;2;0;160;255m \\x1b[38;2;0;157;255m█\\x1b[38;2;0;155;255m \\x1b[38;2;0;152;255m \"\n    \"\\x1b[38;2;0;149;255m█\\x1b[38;2;0;146;255m \\x1b[38;2;0;143;255m▀\\x1b[38;2;0;140;255m \\x1b[38;2;0;137;255m█\\x1b[38;2;0;134;255m \"\n    \"\\x1b[38;2;0;131;255m█\\x1b[38;2;0;128;255m▄\\x1b[38;2;0;126;255m▄\\x1b[38;2;0;123;255m \"\n    \"\\x1b[38;2;0;120;255m█\\x1b[38;2;0;117;255m▀\\x1b[38;2;0;114;255m▀\\x1b[39m\\n\"\n)\n\n\ndef log_server_banner(server: FastMCP[Any]) -> None:\n    \"\"\"Creates and logs a formatted banner with server information and logo.\"\"\"\n\n    # Check for updates (non-blocking, fails silently)\n    newer_version = check_for_newer_version()\n\n    # Create the logo text\n    # Use Text with no_wrap and markup disabled to preserve ANSI escape codes\n    logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)\n\n    # Create the main title\n    title_text = Text(f\"FastMCP {fastmcp.__version__}\", style=\"bold blue\")\n\n    # Create the information table\n    info_table = Table.grid(padding=(0, 1))\n    info_table.add_column(style=\"bold\", justify=\"center\")  # Emoji column\n    info_table.add_column(style=\"cyan\", justify=\"left\")  # Label column\n    info_table.add_column(style=\"dim\", justify=\"left\")  # Value column\n\n    server_info = server.name\n    if server.version:\n        server_info += f\", {server.version}\"\n    info_table.add_row(\"🖥\", \"Server:\", Text(server_info, style=\"dim\"))\n    info_table.add_row(\"🚀\", \"Deploy free:\", \"https://horizon.prefect.io\")\n\n    # Create panel with logo, title, and information using Group\n    docs_url = Text(\"https://gofastmcp.com\", style=\"dim\")\n    panel_content = Group(\n        \"\",\n        Align.center(logo_text),\n        \"\",\n        \"\",\n        Align.center(title_text),\n        Align.center(docs_url),\n        \"\",\n        Align.center(info_table),\n    )\n\n    panel = Panel(\n        panel_content,\n        border_style=\"dim\",\n        padding=(1, 4),\n        # expand=False,\n        width=80,  # Set max width for the panel\n    )\n\n    console = Console(stderr=True)\n\n    # Build output elements\n    output_elements: list[Align | Panel | str] = [\"\\n\", Align.center(panel)]\n\n    # Add update notice if a newer version is available (shown last for visibility)\n    if newer_version:\n        update_line1 = Text.assemble(\n            (\"🎉 Update available: \", \"bold\"),\n            (newer_version, \"bold green\"),\n        )\n        update_line2 = Text(\"Run: pip install --upgrade fastmcp\", style=\"dim\")\n        update_notice = Panel(\n            Group(Align.center(update_line1), Align.center(update_line2)),\n            border_style=\"blue\",\n            padding=(0, 2),\n            width=80,\n        )\n        output_elements.append(Align.center(update_notice))\n\n    output_elements.append(\"\\n\")\n\n    console.print(Group(*output_elements))\n"
  },
  {
    "path": "src/fastmcp/utilities/components.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypedDict, cast\n\nfrom mcp.types import Icon\nfrom pydantic import BeforeValidator, Field\nfrom typing_extensions import Self, TypeVar\n\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.utilities.types import FastMCPBaseModel\n\nif TYPE_CHECKING:\n    from docket import Docket\n    from docket.execution import Execution\n\nT = TypeVar(\"T\", default=Any)\n\n\nclass FastMCPMeta(TypedDict, total=False):\n    tags: list[str]\n    version: str\n    versions: list[str]\n\n\ndef get_fastmcp_metadata(meta: dict[str, Any] | None) -> FastMCPMeta:\n    \"\"\"Extract FastMCP metadata from a component's meta dict.\n\n    Handles both the current `fastmcp` namespace and the legacy `_fastmcp`\n    namespace for compatibility with older FastMCP servers.\n    \"\"\"\n    if not meta:\n        return {}\n\n    for key in (\"fastmcp\", \"_fastmcp\"):\n        metadata = meta.get(key)\n        if isinstance(metadata, dict):\n            return cast(FastMCPMeta, metadata)\n\n    return {}\n\n\ndef _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:\n    \"\"\"Convert a sequence to a set, defaulting to an empty set if None.\"\"\"\n    if maybe_set is None:\n        return set()\n    if isinstance(maybe_set, set):\n        return maybe_set\n    return set(maybe_set)\n\n\ndef _coerce_version(v: str | int | float | None) -> str | None:\n    \"\"\"Coerce version to string, accepting int, float, or str.\n\n    Raises TypeError for non-scalar types (list, dict, set, etc.).\n    Raises ValueError if version contains '@' (used as key delimiter).\n    \"\"\"\n    if v is None:\n        return None\n    if isinstance(v, bool):\n        raise TypeError(f\"Version must be a string, int, or float, got bool: {v!r}\")\n    if not isinstance(v, (str, int, float)):\n        raise TypeError(\n            f\"Version must be a string, int, or float, got {type(v).__name__}: {v!r}\"\n        )\n    version = str(v)\n    if \"@\" in version:\n        raise ValueError(\n            f\"Version string cannot contain '@' (used as key delimiter): {version!r}\"\n        )\n    return version\n\n\nclass FastMCPComponent(FastMCPBaseModel):\n    \"\"\"Base class for FastMCP tools, prompts, resources, and resource templates.\"\"\"\n\n    KEY_PREFIX: ClassVar[str] = \"\"\n\n    def __init_subclass__(cls, **kwargs: Any) -> None:\n        super().__init_subclass__(**kwargs)\n        # Warn if a subclass doesn't define KEY_PREFIX (inherited or its own)\n        if not cls.KEY_PREFIX:\n            import warnings\n\n            warnings.warn(\n                f\"{cls.__name__} does not define KEY_PREFIX. \"\n                f\"Component keys will not be type-prefixed, which may cause collisions.\",\n                UserWarning,\n                stacklevel=2,\n            )\n\n    name: str = Field(\n        description=\"The name of the component.\",\n    )\n    version: Annotated[str | None, BeforeValidator(_coerce_version)] = Field(\n        default=None,\n        description=\"Optional version identifier for this component. \"\n        \"Multiple versions of the same component (same name) can coexist.\",\n    )\n    title: str | None = Field(\n        default=None,\n        description=\"The title of the component for display purposes.\",\n    )\n    description: str | None = Field(\n        default=None,\n        description=\"The description of the component.\",\n    )\n    icons: list[Icon] | None = Field(\n        default=None,\n        description=\"Optional list of icons for this component to display in user interfaces.\",\n    )\n    tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(\n        default_factory=set,\n        description=\"Tags for the component.\",\n    )\n    meta: dict[str, Any] | None = Field(\n        default=None, description=\"Meta information about the component\"\n    )\n    task_config: Annotated[\n        TaskConfig,\n        Field(description=\"Background task execution configuration (SEP-1686).\"),\n    ] = Field(default_factory=lambda: TaskConfig(mode=\"forbidden\"))\n\n    @classmethod\n    def make_key(cls, identifier: str) -> str:\n        \"\"\"Construct the lookup key for this component type.\n\n        Args:\n            identifier: The raw identifier (name for tools/prompts, uri for resources)\n\n        Returns:\n            A prefixed key like \"tool:name\" or \"resource:uri\"\n        \"\"\"\n        if cls.KEY_PREFIX:\n            return f\"{cls.KEY_PREFIX}:{identifier}\"\n        return identifier\n\n    @property\n    def key(self) -> str:\n        \"\"\"The globally unique lookup key for this component.\n\n        Format: \"{key_prefix}:{identifier}@{version}\" or \"{key_prefix}:{identifier}@\"\n        e.g. \"tool:my_tool@v2\", \"tool:my_tool@\", \"resource:file://x.txt@\"\n\n        The @ suffix is ALWAYS present to enable unambiguous parsing of keys\n        (URIs may contain @ characters, so we always include the delimiter).\n\n        Subclasses should override this to use their specific identifier.\n        Base implementation uses name.\n        \"\"\"\n        base_key = self.make_key(self.name)\n        return f\"{base_key}@{self.version or ''}\"\n\n    def get_meta(self) -> dict[str, Any]:\n        \"\"\"Get the meta information about the component.\n\n        Returns a dict that always includes a `fastmcp` key containing:\n        - `tags`: sorted list of component tags\n        - `version`: component version (only if set)\n\n        Internal keys (prefixed with `_`) are stripped from the fastmcp namespace.\n        \"\"\"\n        meta = dict(self.meta) if self.meta else {}\n\n        fastmcp_meta: FastMCPMeta = {\"tags\": sorted(self.tags)}\n        if self.version is not None:\n            fastmcp_meta[\"version\"] = self.version\n\n        # Merge with upstream fastmcp meta, stripping internal keys\n        if (upstream_meta := meta.get(\"fastmcp\")) is not None:\n            if not isinstance(upstream_meta, dict):\n                raise TypeError(\"meta['fastmcp'] must be a dict\")\n            # Filter out internal keys (e.g., _internal used for enabled state)\n            public_upstream = {\n                k: v for k, v in upstream_meta.items() if not k.startswith(\"_\")\n            }\n            fastmcp_meta = cast(FastMCPMeta, public_upstream | fastmcp_meta)\n        meta[\"fastmcp\"] = fastmcp_meta\n\n        return meta\n\n    def __eq__(self, other: object) -> bool:\n        if type(self) is not type(other):\n            return False\n        if not isinstance(other, type(self)):\n            return False\n        return self.model_dump() == other.model_dump()\n\n    def __repr__(self) -> str:\n        parts = [f\"name={self.name!r}\"]\n        if self.version:\n            parts.append(f\"version={self.version!r}\")\n        parts.extend(\n            [\n                f\"title={self.title!r}\",\n                f\"description={self.description!r}\",\n                f\"tags={self.tags}\",\n            ]\n        )\n        return f\"{self.__class__.__name__}({', '.join(parts)})\"\n\n    def enable(self) -> None:\n        \"\"\"Removed in 3.0. Use server.enable(keys=[...]) instead.\"\"\"\n        raise NotImplementedError(\n            f\"Component.enable() was removed in FastMCP 3.0. \"\n            f\"Use server.enable(keys=['{self.key}']) instead.\"\n        )\n\n    def disable(self) -> None:\n        \"\"\"Removed in 3.0. Use server.disable(keys=[...]) instead.\"\"\"\n        raise NotImplementedError(\n            f\"Component.disable() was removed in FastMCP 3.0. \"\n            f\"Use server.disable(keys=['{self.key}']) instead.\"\n        )\n\n    def copy(self) -> Self:  # type: ignore[override]\n        \"\"\"Create a copy of the component.\"\"\"\n        return self.model_copy()\n\n    def register_with_docket(self, docket: Docket) -> None:\n        \"\"\"Register this component with docket for background execution.\n\n        No-ops if task_config.mode is \"forbidden\". Subclasses override to\n        register their callable (self.run, self.read, self.render, or self.fn).\n        \"\"\"\n        # Base implementation: no-op (subclasses override)\n\n    async def add_to_docket(\n        self, docket: Docket, *args: Any, **kwargs: Any\n    ) -> Execution:\n        \"\"\"Schedule this component for background execution via docket.\n\n        Subclasses override this to handle their specific calling conventions:\n        - Tool: add_to_docket(docket, arguments: dict, **kwargs)\n        - Resource: add_to_docket(docket, **kwargs)\n        - ResourceTemplate: add_to_docket(docket, params: dict, **kwargs)\n        - Prompt: add_to_docket(docket, arguments: dict | None, **kwargs)\n\n        The **kwargs are passed through to docket.add() (e.g., key=task_key).\n        \"\"\"\n        if not self.task_config.supports_tasks():\n            raise RuntimeError(\n                f\"Cannot add {self.__class__.__name__} '{self.name}' to docket: \"\n                f\"task execution not supported\"\n            )\n        raise NotImplementedError(\n            f\"{self.__class__.__name__} does not implement add_to_docket()\"\n        )\n\n    def get_span_attributes(self) -> dict[str, Any]:\n        \"\"\"Return span attributes for telemetry.\n\n        Subclasses should call super() and merge their specific attributes.\n        \"\"\"\n        return {\"fastmcp.component.key\": self.key}\n"
  },
  {
    "path": "src/fastmcp/utilities/exceptions.py",
    "content": "from collections.abc import Callable, Iterable, Mapping\nfrom typing import Any\n\nimport httpx\nimport mcp.types\nfrom exceptiongroup import BaseExceptionGroup\nfrom mcp import McpError\n\nimport fastmcp\n\n\ndef iter_exc(group: BaseExceptionGroup):\n    for exc in group.exceptions:\n        if isinstance(exc, BaseExceptionGroup):\n            yield from iter_exc(exc)\n        else:\n            yield exc\n\n\ndef _exception_handler(group: BaseExceptionGroup):\n    for leaf in iter_exc(group):\n        if isinstance(leaf, httpx.ConnectTimeout):\n            raise McpError(\n                error=mcp.types.ErrorData(\n                    code=httpx.codes.REQUEST_TIMEOUT,\n                    message=\"Timed out while waiting for response.\",\n                )\n            )\n        raise leaf\n\n\n# this catch handler is used to catch taskgroup exception groups and raise the\n# first exception. This allows more sane debugging.\n_catch_handlers: Mapping[\n    type[BaseException] | Iterable[type[BaseException]],\n    Callable[[BaseExceptionGroup[Any]], Any],\n] = {\n    Exception: _exception_handler,\n}\n\n\ndef get_catch_handlers() -> Mapping[\n    type[BaseException] | Iterable[type[BaseException]],\n    Callable[[BaseExceptionGroup[Any]], Any],\n]:\n    if fastmcp.settings.client_raise_first_exceptiongroup_error:\n        return _catch_handlers\n    else:\n        return {}\n"
  },
  {
    "path": "src/fastmcp/utilities/http.py",
    "content": "import socket\n\n\ndef find_available_port() -> int:\n    \"\"\"Find an available port by letting the OS assign one.\"\"\"\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n"
  },
  {
    "path": "src/fastmcp/utilities/inspect.py",
    "content": "\"\"\"Utilities for inspecting FastMCP instances.\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib.metadata\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Any, Literal, cast\n\nimport pydantic_core\nfrom mcp.server.fastmcp import FastMCP as FastMCP1x\n\nimport fastmcp\nfrom fastmcp import Client\nfrom fastmcp.server.server import FastMCP\n\n\n@dataclass\nclass ToolInfo:\n    \"\"\"Information about a tool.\"\"\"\n\n    key: str\n    name: str\n    description: str | None\n    input_schema: dict[str, Any]\n    output_schema: dict[str, Any] | None = None\n    annotations: dict[str, Any] | None = None\n    tags: list[str] | None = None\n    title: str | None = None\n    icons: list[dict[str, Any]] | None = None\n    meta: dict[str, Any] | None = None\n\n\n@dataclass\nclass PromptInfo:\n    \"\"\"Information about a prompt.\"\"\"\n\n    key: str\n    name: str\n    description: str | None\n    arguments: list[dict[str, Any]] | None = None\n    tags: list[str] | None = None\n    title: str | None = None\n    icons: list[dict[str, Any]] | None = None\n    meta: dict[str, Any] | None = None\n\n\n@dataclass\nclass ResourceInfo:\n    \"\"\"Information about a resource.\"\"\"\n\n    key: str\n    uri: str\n    name: str | None\n    description: str | None\n    mime_type: str | None = None\n    annotations: dict[str, Any] | None = None\n    tags: list[str] | None = None\n    title: str | None = None\n    icons: list[dict[str, Any]] | None = None\n    meta: dict[str, Any] | None = None\n\n\n@dataclass\nclass TemplateInfo:\n    \"\"\"Information about a resource template.\"\"\"\n\n    key: str\n    uri_template: str\n    name: str | None\n    description: str | None\n    mime_type: str | None = None\n    parameters: dict[str, Any] | None = None\n    annotations: dict[str, Any] | None = None\n    tags: list[str] | None = None\n    title: str | None = None\n    icons: list[dict[str, Any]] | None = None\n    meta: dict[str, Any] | None = None\n\n\n@dataclass\nclass FastMCPInfo:\n    \"\"\"Information extracted from a FastMCP instance.\"\"\"\n\n    name: str\n    instructions: str | None\n    version: str | None  # The server's own version string (if specified)\n    website_url: str | None\n    icons: list[dict[str, Any]] | None\n    fastmcp_version: str  # Version of FastMCP generating this manifest\n    mcp_version: str  # Version of MCP protocol library\n    server_generation: int  # Server generation: 1 (mcp package) or 2 (fastmcp)\n    tools: list[ToolInfo]\n    prompts: list[PromptInfo]\n    resources: list[ResourceInfo]\n    templates: list[TemplateInfo]\n    capabilities: dict[str, Any]\n\n\nasync def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:\n    \"\"\"Extract information from a FastMCP v2.x instance.\n\n    Args:\n        mcp: The FastMCP v2.x instance to inspect\n\n    Returns:\n        FastMCPInfo dataclass containing the extracted information\n    \"\"\"\n    # Get all components (list_* includes middleware, enabled/auth filtering)\n    tools_list = await mcp.list_tools()\n    prompts_list = await mcp.list_prompts()\n    resources_list = await mcp.list_resources()\n    templates_list = await mcp.list_resource_templates()\n\n    # Extract detailed tool information\n    tool_infos = []\n    for tool in tools_list:\n        mcp_tool = tool.to_mcp_tool(name=tool.name)\n        tool_infos.append(\n            ToolInfo(\n                key=tool.key,\n                name=tool.name or tool.key,\n                description=tool.description,\n                input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},\n                output_schema=tool.output_schema,\n                annotations=tool.annotations.model_dump() if tool.annotations else None,\n                tags=list(tool.tags) if tool.tags else None,\n                title=tool.title,\n                icons=[icon.model_dump() for icon in tool.icons]\n                if tool.icons\n                else None,\n                meta=tool.meta,\n            )\n        )\n\n    # Extract detailed prompt information\n    prompt_infos = []\n    for prompt in prompts_list:\n        prompt_infos.append(\n            PromptInfo(\n                key=prompt.key,\n                name=prompt.name or prompt.key,\n                description=prompt.description,\n                arguments=[arg.model_dump() for arg in prompt.arguments]\n                if prompt.arguments\n                else None,\n                tags=list(prompt.tags) if prompt.tags else None,\n                title=prompt.title,\n                icons=[icon.model_dump() for icon in prompt.icons]\n                if prompt.icons\n                else None,\n                meta=prompt.meta,\n            )\n        )\n\n    # Extract detailed resource information\n    resource_infos = []\n    for resource in resources_list:\n        resource_infos.append(\n            ResourceInfo(\n                key=resource.key,\n                uri=str(resource.uri),\n                name=resource.name,\n                description=resource.description,\n                mime_type=resource.mime_type,\n                annotations=resource.annotations.model_dump()\n                if resource.annotations\n                else None,\n                tags=list(resource.tags) if resource.tags else None,\n                title=resource.title,\n                icons=[icon.model_dump() for icon in resource.icons]\n                if resource.icons\n                else None,\n                meta=resource.meta,\n            )\n        )\n\n    # Extract detailed template information\n    template_infos = []\n    for template in templates_list:\n        template_infos.append(\n            TemplateInfo(\n                key=template.key,\n                uri_template=template.uri_template,\n                name=template.name,\n                description=template.description,\n                mime_type=template.mime_type,\n                parameters=template.parameters,\n                annotations=template.annotations.model_dump()\n                if template.annotations\n                else None,\n                tags=list(template.tags) if template.tags else None,\n                title=template.title,\n                icons=[icon.model_dump() for icon in template.icons]\n                if template.icons\n                else None,\n                meta=template.meta,\n            )\n        )\n\n    # Basic MCP capabilities that FastMCP supports\n    capabilities = {\n        \"tools\": {\"listChanged\": True},\n        \"resources\": {\"subscribe\": False, \"listChanged\": False},\n        \"prompts\": {\"listChanged\": False},\n        \"logging\": {},\n    }\n\n    # Extract server-level icons and website_url\n    server_icons = (\n        [icon.model_dump() for icon in mcp._mcp_server.icons]\n        if hasattr(mcp._mcp_server, \"icons\") and mcp._mcp_server.icons\n        else None\n    )\n    server_website_url = (\n        mcp._mcp_server.website_url if hasattr(mcp._mcp_server, \"website_url\") else None\n    )\n\n    return FastMCPInfo(\n        name=mcp.name,\n        instructions=mcp.instructions,\n        version=(mcp.version if hasattr(mcp, \"version\") else mcp._mcp_server.version),\n        website_url=server_website_url,\n        icons=server_icons,\n        fastmcp_version=fastmcp.__version__,\n        mcp_version=importlib.metadata.version(\"mcp\"),\n        server_generation=2,  # FastMCP v2\n        tools=tool_infos,\n        prompts=prompt_infos,\n        resources=resource_infos,\n        templates=template_infos,\n        capabilities=capabilities,\n    )\n\n\nasync def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:\n    \"\"\"Extract information from a FastMCP v1.x instance using a Client.\n\n    Args:\n        mcp: The FastMCP v1.x instance to inspect\n\n    Returns:\n        FastMCPInfo dataclass containing the extracted information\n    \"\"\"\n    # Use a client to interact with the FastMCP1x server\n    async with Client(mcp) as client:\n        # Get components via client calls (these return MCP objects)\n        mcp_tools = await client.list_tools()\n        mcp_prompts = await client.list_prompts()\n        mcp_resources = await client.list_resources()\n\n        # Try to get resource templates (FastMCP 1.x does have templates)\n        try:\n            mcp_templates = await client.list_resource_templates()\n        except Exception:\n            mcp_templates = []\n\n        # Extract detailed tool information from MCP Tool objects\n        tool_infos = []\n        for mcp_tool in mcp_tools:\n            tool_infos.append(\n                ToolInfo(\n                    key=mcp_tool.name,\n                    name=mcp_tool.name,\n                    description=mcp_tool.description,\n                    input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},\n                    output_schema=None,  # v1 doesn't have output_schema\n                    annotations=None,  # v1 doesn't have annotations\n                    tags=None,  # v1 doesn't have tags\n                    title=None,  # v1 doesn't have title\n                    icons=[icon.model_dump() for icon in mcp_tool.icons]\n                    if hasattr(mcp_tool, \"icons\") and mcp_tool.icons\n                    else None,\n                    meta=None,  # v1 doesn't have meta field\n                )\n            )\n\n        # Extract detailed prompt information from MCP Prompt objects\n        prompt_infos = []\n        for mcp_prompt in mcp_prompts:\n            # Convert arguments if they exist\n            arguments = None\n            if hasattr(mcp_prompt, \"arguments\") and mcp_prompt.arguments:\n                arguments = [arg.model_dump() for arg in mcp_prompt.arguments]\n\n            prompt_infos.append(\n                PromptInfo(\n                    key=mcp_prompt.name,\n                    name=mcp_prompt.name,\n                    description=mcp_prompt.description,\n                    arguments=arguments,\n                    tags=None,  # v1 doesn't have tags\n                    title=None,  # v1 doesn't have title\n                    icons=[icon.model_dump() for icon in mcp_prompt.icons]\n                    if hasattr(mcp_prompt, \"icons\") and mcp_prompt.icons\n                    else None,\n                    meta=None,  # v1 doesn't have meta field\n                )\n            )\n\n        # Extract detailed resource information from MCP Resource objects\n        resource_infos = []\n        for mcp_resource in mcp_resources:\n            resource_infos.append(\n                ResourceInfo(\n                    key=str(mcp_resource.uri),\n                    uri=str(mcp_resource.uri),\n                    name=mcp_resource.name,\n                    description=mcp_resource.description,\n                    mime_type=mcp_resource.mimeType,\n                    annotations=None,  # v1 doesn't have annotations\n                    tags=None,  # v1 doesn't have tags\n                    title=None,  # v1 doesn't have title\n                    icons=[icon.model_dump() for icon in mcp_resource.icons]\n                    if hasattr(mcp_resource, \"icons\") and mcp_resource.icons\n                    else None,\n                    meta=None,  # v1 doesn't have meta field\n                )\n            )\n\n        # Extract detailed template information from MCP ResourceTemplate objects\n        template_infos = []\n        for mcp_template in mcp_templates:\n            template_infos.append(\n                TemplateInfo(\n                    key=str(mcp_template.uriTemplate),\n                    uri_template=str(mcp_template.uriTemplate),\n                    name=mcp_template.name,\n                    description=mcp_template.description,\n                    mime_type=mcp_template.mimeType,\n                    parameters=None,  # v1 doesn't expose template parameters\n                    annotations=None,  # v1 doesn't have annotations\n                    tags=None,  # v1 doesn't have tags\n                    title=None,  # v1 doesn't have title\n                    icons=[icon.model_dump() for icon in mcp_template.icons]\n                    if hasattr(mcp_template, \"icons\") and mcp_template.icons\n                    else None,\n                    meta=None,  # v1 doesn't have meta field\n                )\n            )\n\n        # Basic MCP capabilities\n        capabilities = {\n            \"tools\": {\"listChanged\": True},\n            \"resources\": {\"subscribe\": False, \"listChanged\": False},\n            \"prompts\": {\"listChanged\": False},\n            \"logging\": {},\n        }\n\n        # Extract server-level icons and website_url from serverInfo\n        server_info = client.initialize_result.serverInfo\n        server_icons = (\n            [icon.model_dump() for icon in server_info.icons]\n            if hasattr(server_info, \"icons\") and server_info.icons\n            else None\n        )\n        server_website_url = (\n            server_info.websiteUrl if hasattr(server_info, \"websiteUrl\") else None\n        )\n\n        return FastMCPInfo(\n            name=mcp._mcp_server.name,\n            instructions=mcp._mcp_server.instructions,\n            version=mcp._mcp_server.version,\n            website_url=server_website_url,\n            icons=server_icons,\n            fastmcp_version=fastmcp.__version__,  # Version generating this manifest\n            mcp_version=importlib.metadata.version(\"mcp\"),\n            server_generation=1,  # MCP v1\n            tools=tool_infos,\n            prompts=prompt_infos,\n            resources=resource_infos,\n            templates=template_infos,\n            capabilities=capabilities,\n        )\n\n\nasync def inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo:\n    \"\"\"Extract information from a FastMCP instance into a dataclass.\n\n    This function automatically detects whether the instance is FastMCP v1.x or v2.x\n    and uses the appropriate extraction method.\n\n    Args:\n        mcp: The FastMCP instance to inspect (v1.x or v2.x)\n\n    Returns:\n        FastMCPInfo dataclass containing the extracted information\n    \"\"\"\n    if isinstance(mcp, FastMCP1x):\n        return await inspect_fastmcp_v1(mcp)\n    else:\n        return await inspect_fastmcp_v2(cast(FastMCP[Any], mcp))\n\n\nclass InspectFormat(str, Enum):\n    \"\"\"Output format for inspect command.\"\"\"\n\n    FASTMCP = \"fastmcp\"\n    MCP = \"mcp\"\n\n\ndef format_fastmcp_info(info: FastMCPInfo) -> bytes:\n    \"\"\"Format FastMCPInfo as FastMCP-specific JSON.\n\n    This includes FastMCP-specific fields like tags, enabled, annotations, etc.\n    \"\"\"\n    # Build the output dict with nested structure\n    result = {\n        \"server\": {\n            \"name\": info.name,\n            \"instructions\": info.instructions,\n            \"version\": info.version,\n            \"website_url\": info.website_url,\n            \"icons\": info.icons,\n            \"generation\": info.server_generation,\n            \"capabilities\": info.capabilities,\n        },\n        \"environment\": {\n            \"fastmcp\": info.fastmcp_version,\n            \"mcp\": info.mcp_version,\n        },\n        \"tools\": info.tools,\n        \"prompts\": info.prompts,\n        \"resources\": info.resources,\n        \"templates\": info.templates,\n    }\n\n    return pydantic_core.to_json(result, indent=2)\n\n\nasync def format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes:\n    \"\"\"Format server info as standard MCP protocol JSON.\n\n    Uses Client to get the standard MCP protocol format with camelCase fields.\n    Includes version metadata at the top level.\n    \"\"\"\n    async with Client(mcp) as client:\n        # Get all the MCP protocol objects\n        tools_result = await client.list_tools_mcp()\n        prompts_result = await client.list_prompts_mcp()\n        resources_result = await client.list_resources_mcp()\n        templates_result = await client.list_resource_templates_mcp()\n\n        # Get server info from the initialize result\n        server_info = client.initialize_result.serverInfo\n\n        # Combine into MCP protocol structure with environment metadata\n        result = {\n            \"environment\": {\n                \"fastmcp\": fastmcp.__version__,  # Version generating this manifest\n                \"mcp\": importlib.metadata.version(\"mcp\"),  # MCP protocol version\n            },\n            \"serverInfo\": server_info,\n            \"capabilities\": {},  # MCP format doesn't include capabilities at top level\n            \"tools\": tools_result.tools,\n            \"prompts\": prompts_result.prompts,\n            \"resources\": resources_result.resources,\n            \"resourceTemplates\": templates_result.resourceTemplates,\n        }\n\n        return pydantic_core.to_json(result, indent=2)\n\n\nasync def format_info(\n    mcp: FastMCP[Any] | FastMCP1x,\n    format: InspectFormat | Literal[\"fastmcp\", \"mcp\"],\n    info: FastMCPInfo | None = None,\n) -> bytes:\n    \"\"\"Format server information according to the specified format.\n\n    Args:\n        mcp: The FastMCP instance\n        format: Output format (\"fastmcp\" or \"mcp\")\n        info: Pre-extracted FastMCPInfo (optional, will be extracted if not provided)\n\n    Returns:\n        JSON bytes in the requested format\n    \"\"\"\n    # Convert string to enum if needed\n    if isinstance(format, str):\n        format = InspectFormat(format)\n\n    if format == InspectFormat.MCP:\n        # MCP format doesn't need FastMCPInfo, it uses Client directly\n        return await format_mcp_info(mcp)\n    elif format == InspectFormat.FASTMCP:\n        # For FastMCP format, we need the FastMCPInfo\n        # This works for both v1 and v2 servers\n        if info is None:\n            info = await inspect_fastmcp(mcp)\n        return format_fastmcp_info(info)\n    else:\n        raise ValueError(f\"Unknown format: {format}\")\n"
  },
  {
    "path": "src/fastmcp/utilities/json_schema.py",
    "content": "from __future__ import annotations\n\nfrom collections import defaultdict\nfrom typing import Any\n\nfrom jsonref import JsonRefError, replace_refs\n\n\ndef _defs_have_cycles(defs: dict[str, Any]) -> bool:\n    \"\"\"Check whether any definitions in ``$defs`` form a reference cycle.\n\n    A cycle means a definition directly or transitively references itself\n    (e.g. Node → children → Node, or A → B → A).  ``jsonref.replace_refs``\n    silently produces Python-level object cycles for these, which Pydantic's\n    serializer rejects.\n    \"\"\"\n    if not defs:\n        return False\n\n    # Build adjacency: def_name -> set of def_names it references.\n    edges: dict[str, set[str]] = defaultdict(set)\n\n    def _collect_refs(obj: Any, source: str) -> None:\n        if isinstance(obj, dict):\n            ref = obj.get(\"$ref\")\n            if isinstance(ref, str) and ref.startswith(\"#/$defs/\"):\n                edges[source].add(ref.split(\"/\")[-1])\n            for v in obj.values():\n                _collect_refs(v, source)\n        elif isinstance(obj, list):\n            for item in obj:\n                _collect_refs(item, source)\n\n    for name, definition in defs.items():\n        _collect_refs(definition, name)\n\n    # DFS cycle detection.\n    UNVISITED, IN_STACK, DONE = 0, 1, 2\n    state: dict[str, int] = defaultdict(int)\n\n    def _has_cycle(node: str) -> bool:\n        state[node] = IN_STACK\n        for neighbor in edges.get(node, ()):\n            if neighbor not in defs:\n                continue\n            if state[neighbor] == IN_STACK:\n                return True\n            if state[neighbor] == UNVISITED and _has_cycle(neighbor):\n                return True\n        state[node] = DONE\n        return False\n\n    return any(state[name] == UNVISITED and _has_cycle(name) for name in defs)\n\n\ndef _strip_remote_refs(obj: Any) -> Any:\n    \"\"\"Return a deep copy of *obj* with non-local ``$ref`` values removed.\n\n    Local refs (starting with ``#``) are kept intact.  Remote refs\n    (``http://``, ``https://``, ``file://``, or any other URI scheme) are\n    stripped so that ``jsonref.replace_refs`` never attempts to fetch an\n    external resource.  This prevents SSRF / LFI when proxying schemas\n    from untrusted servers.\n    \"\"\"\n    if isinstance(obj, dict):\n        ref = obj.get(\"$ref\")\n        if isinstance(ref, str) and not ref.startswith(\"#\"):\n            # Drop the remote $ref key; keep all other keys.\n            return {k: _strip_remote_refs(v) for k, v in obj.items() if k != \"$ref\"}\n        return {k: _strip_remote_refs(v) for k, v in obj.items()}\n    if isinstance(obj, list):\n        return [_strip_remote_refs(item) for item in obj]\n    return obj\n\n\ndef dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Resolve all $ref references in a JSON schema by inlining definitions.\n\n    This function resolves $ref references that point to $defs, replacing them\n    with the actual definition content while preserving sibling keywords (like\n    description, default, examples) that Pydantic places alongside $ref.\n\n    This is necessary because some MCP clients (e.g., VS Code Copilot) don't\n    properly handle $ref in tool input schemas.\n\n    For self-referencing/circular schemas where full dereferencing is not possible,\n    this function falls back to resolving only the root-level $ref while preserving\n    $defs for nested references.\n\n    Only local ``$ref`` values (those starting with ``#``) are resolved.\n    Remote URIs (``http://``, ``file://``, etc.) are stripped before\n    resolution to prevent SSRF / local-file-inclusion attacks when proxying\n    schemas from untrusted servers.\n\n    Args:\n        schema: JSON schema dict that may contain $ref references\n\n    Returns:\n        A new schema dict with $ref resolved where possible and $defs removed\n        when no longer needed\n\n    Example:\n        >>> schema = {\n        ...     \"$defs\": {\"Category\": {\"enum\": [\"a\", \"b\"], \"type\": \"string\"}},\n        ...     \"properties\": {\"cat\": {\"$ref\": \"#/$defs/Category\", \"default\": \"a\"}}\n        ... }\n        >>> resolved = dereference_refs(schema)\n        >>> # Result: {\"properties\": {\"cat\": {\"enum\": [\"a\", \"b\"], \"type\": \"string\", \"default\": \"a\"}}}\n    \"\"\"\n    # Strip any remote $ref values before processing to prevent SSRF / LFI.\n    schema = _strip_remote_refs(schema)\n\n    # Circular $defs can't be fully inlined — jsonref.replace_refs produces\n    # Python dicts with object-identity cycles that Pydantic's model_dump\n    # rejects with \"Circular reference detected (id repeated)\".\n    # Detect cycles up front and fall back to root-only resolution.\n    if _defs_have_cycles(schema.get(\"$defs\", {})):\n        return resolve_root_ref(schema)\n\n    try:\n        # Use jsonref to resolve all $ref references\n        # proxies=False returns plain dicts (not proxy objects)\n        # lazy_load=False resolves immediately\n        dereferenced = replace_refs(schema, proxies=False, lazy_load=False)\n\n        # Merge sibling keywords that were lost during dereferencing\n        # Pydantic puts description, default, examples as siblings to $ref\n        defs = schema.get(\"$defs\", {})\n        merged = _merge_ref_siblings(schema, dereferenced, defs)\n        # Type assertion: top-level schema is always a dict\n        assert isinstance(merged, dict)\n        dereferenced = merged\n\n        # Remove $defs since all references have been resolved\n        if \"$defs\" in dereferenced:\n            dereferenced = {k: v for k, v in dereferenced.items() if k != \"$defs\"}\n\n        return dereferenced\n\n    except JsonRefError:\n        # Self-referencing/circular schemas can't be fully dereferenced\n        # Fall back to resolving only root-level $ref (for MCP spec compliance)\n        return resolve_root_ref(schema)\n\n\ndef _merge_ref_siblings(\n    original: Any,\n    dereferenced: Any,\n    defs: dict[str, Any],\n    visited: set[str] | None = None,\n) -> Any:\n    \"\"\"Merge sibling keywords from original $ref nodes into dereferenced schema.\n\n    When jsonref resolves $ref, it replaces the entire node with the referenced\n    definition, losing any sibling keywords like description, default, or examples.\n    This function walks both trees in parallel and merges those siblings back.\n\n    Args:\n        original: The original schema with $ref and potential siblings\n        dereferenced: The schema after jsonref processing\n        defs: The $defs from the original schema, for looking up referenced definitions\n        visited: Set of definition names already being processed (prevents cycles)\n\n    Returns:\n        The dereferenced schema with sibling keywords restored\n    \"\"\"\n    if visited is None:\n        visited = set()\n\n    if isinstance(original, dict) and isinstance(dereferenced, dict):\n        # Check if original had a $ref\n        if \"$ref\" in original:\n            ref = original[\"$ref\"]\n            siblings = {k: v for k, v in original.items() if k not in (\"$ref\", \"$defs\")}\n\n            # Look up the referenced definition to process its nested siblings\n            if isinstance(ref, str) and ref.startswith(\"#/$defs/\"):\n                def_name = ref.split(\"/\")[-1]\n                # Prevent infinite recursion on circular references\n                if def_name in defs and def_name not in visited:\n                    # Recursively process the definition's content for nested siblings\n                    dereferenced = _merge_ref_siblings(\n                        defs[def_name], dereferenced, defs, visited | {def_name}\n                    )\n\n            if siblings:\n                # Merge local siblings, which take precedence\n                merged = dict(dereferenced)\n                merged.update(siblings)\n                return merged\n            return dereferenced\n\n        # Recurse into nested structures\n        result = {}\n        for key, value in dereferenced.items():\n            if key in original:\n                result[key] = _merge_ref_siblings(original[key], value, defs, visited)\n            else:\n                result[key] = value\n        return result\n\n    elif isinstance(original, list) and isinstance(dereferenced, list):\n        # Process list items in parallel\n        min_len = min(len(original), len(dereferenced))\n        return [\n            _merge_ref_siblings(o, d, defs, visited)\n            for o, d in zip(original[:min_len], dereferenced[:min_len], strict=False)\n        ] + dereferenced[min_len:]\n\n    return dereferenced\n\n\ndef resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Resolve $ref at root level to meet MCP spec requirements.\n\n    MCP specification requires outputSchema to have \"type\": \"object\" at the root level.\n    When Pydantic generates schemas for self-referential models, it uses $ref at the\n    root level pointing to $defs. This function resolves such references by inlining\n    the referenced definition while preserving $defs for nested references.\n\n    Args:\n        schema: JSON schema dict that may have $ref at root level\n\n    Returns:\n        A new schema dict with root-level $ref resolved, or the original schema\n        if no resolution is needed\n\n    Example:\n        >>> schema = {\n        ...     \"$defs\": {\"Node\": {\"type\": \"object\", \"properties\": {...}}},\n        ...     \"$ref\": \"#/$defs/Node\"\n        ... }\n        >>> resolved = resolve_root_ref(schema)\n        >>> # Result: {\"type\": \"object\", \"properties\": {...}, \"$defs\": {...}}\n    \"\"\"\n    # Only resolve if we have $ref at root level with $defs but no explicit type\n    if \"$ref\" in schema and \"$defs\" in schema and \"type\" not in schema:\n        ref = schema[\"$ref\"]\n        # Only handle local $defs references\n        if isinstance(ref, str) and ref.startswith(\"#/$defs/\"):\n            def_name = ref.split(\"/\")[-1]\n            defs = schema[\"$defs\"]\n            if def_name in defs:\n                # Create a new schema by copying the referenced definition\n                resolved = dict(defs[def_name])\n                # Preserve $defs for nested references (other fields may still use them)\n                resolved[\"$defs\"] = defs\n                return resolved\n    return schema\n\n\ndef _prune_param(schema: dict[str, Any], param: str) -> dict[str, Any]:\n    \"\"\"Return a new schema with *param* removed from `properties`, `required`,\n    and (if no longer referenced) `$defs`.\n    \"\"\"\n\n    # ── 1. drop from properties/required ──────────────────────────────\n    props = schema.get(\"properties\", {})\n    removed = props.pop(param, None)\n    if removed is None:  # nothing to do\n        return schema\n\n    # Keep empty properties object rather than removing it entirely\n    schema[\"properties\"] = props\n    if param in schema.get(\"required\", []):\n        schema[\"required\"].remove(param)\n        if not schema[\"required\"]:\n            schema.pop(\"required\")\n\n    return schema\n\n\ndef _single_pass_optimize(\n    schema: dict[str, Any],\n    prune_titles: bool = False,\n    prune_additional_properties: bool = False,\n    prune_defs: bool = True,\n) -> dict[str, Any]:\n    \"\"\"\n    Optimize JSON schemas in a single traversal for better performance.\n\n    This function combines three schema cleanup operations that would normally require\n    separate tree traversals:\n\n    1. **Remove unused definitions** (prune_defs): Finds and removes `$defs` entries\n       that aren't referenced anywhere in the schema, reducing schema size.\n\n    2. **Remove titles** (prune_titles): Strips `title` fields throughout the schema\n       to reduce verbosity while preserving functional information.\n\n    3. **Remove restrictive additionalProperties** (prune_additional_properties):\n       Removes `\"additionalProperties\": false` constraints to make schemas more flexible.\n\n    **Performance Benefits:**\n    - Single tree traversal instead of multiple passes (2-3x faster)\n    - Immutable design prevents shared reference bugs\n    - Early termination prevents runaway recursion on deeply nested schemas\n\n    **Algorithm Overview:**\n    1. Traverse main schema, collecting $ref references and applying cleanups\n    2. Traverse $defs section to map inter-definition dependencies\n    3. Remove unused definitions based on reference analysis\n\n    Args:\n        schema: JSON schema dict to optimize (not modified)\n        prune_titles: Remove title fields for cleaner output\n        prune_additional_properties: Remove \"additionalProperties\": false constraints\n        prune_defs: Remove unused $defs entries to reduce size\n\n    Returns:\n        A new optimized schema dict\n\n    Example:\n        >>> schema = {\n        ...     \"type\": \"object\",\n        ...     \"title\": \"MySchema\",\n        ...     \"additionalProperties\": False,\n        ...     \"$defs\": {\"UnusedDef\": {\"type\": \"string\"}}\n        ... }\n        >>> result = _single_pass_optimize(schema, prune_titles=True, prune_defs=True)\n        >>> # Result: {\"type\": \"object\", \"additionalProperties\": False}\n    \"\"\"\n    if not (prune_defs or prune_titles or prune_additional_properties):\n        return schema  # Nothing to do\n\n    # Phase 1: Collect references and apply simple cleanups\n    # Track which $defs are referenced from the main schema and from other $defs\n    root_refs: set[str] = set()  # $defs referenced directly from main schema\n    def_dependencies: defaultdict[str, list[str]] = defaultdict(\n        list\n    )  # def A references def B\n    defs = schema.get(\"$defs\")\n\n    def traverse_and_clean(\n        node: object,\n        current_def_name: str | None = None,\n        skip_defs_section: bool = False,\n        depth: int = 0,\n    ) -> None:\n        \"\"\"Traverse schema tree, collecting $ref info and applying cleanups.\"\"\"\n        if depth > 50:  # Prevent infinite recursion\n            return\n\n        if isinstance(node, dict):\n            # Collect $ref references for unused definition removal\n            if prune_defs:\n                ref = node.get(\"$ref\")  # type: ignore\n                if isinstance(ref, str) and ref.startswith(\"#/$defs/\"):\n                    referenced_def = ref.split(\"/\")[-1]\n                    if current_def_name:\n                        # We're inside a $def, so this is a def->def reference\n                        def_dependencies[referenced_def].append(current_def_name)\n                    else:\n                        # We're in the main schema, so this is a root reference\n                        root_refs.add(referenced_def)\n\n            # Apply cleanups\n            # Only remove \"title\" if it's a schema metadata field\n            # Schema objects have keywords like \"type\", \"properties\", \"$ref\", etc.\n            # If we see these, then \"title\" is metadata, not a property name\n            if prune_titles and \"title\" in node:\n                # Check if this looks like a schema node\n                if any(\n                    k in node\n                    for k in [\n                        \"type\",\n                        \"properties\",\n                        \"$ref\",\n                        \"items\",\n                        \"allOf\",\n                        \"oneOf\",\n                        \"anyOf\",\n                        \"required\",\n                    ]\n                ):\n                    node.pop(\"title\")  # type: ignore\n\n            if (\n                prune_additional_properties\n                and node.get(\"additionalProperties\") is False  # type: ignore\n            ):\n                node.pop(\"additionalProperties\")  # type: ignore\n\n            # Recursive traversal\n            for key, value in node.items():\n                if skip_defs_section and key == \"$defs\":\n                    continue  # Skip $defs during main schema traversal\n\n                # Handle schema composition keywords with special traversal\n                if key in [\"allOf\", \"oneOf\", \"anyOf\"] and isinstance(value, list):\n                    for item in value:\n                        traverse_and_clean(item, current_def_name, depth=depth + 1)\n                else:\n                    traverse_and_clean(value, current_def_name, depth=depth + 1)\n\n        elif isinstance(node, list):\n            for item in node:\n                traverse_and_clean(item, current_def_name, depth=depth + 1)\n\n    # Phase 2: Traverse main schema (excluding $defs section)\n    traverse_and_clean(schema, skip_defs_section=True)\n\n    # Phase 3: Traverse $defs to find inter-definition references\n    if prune_defs and defs:\n        for def_name, def_schema in defs.items():\n            traverse_and_clean(def_schema, current_def_name=def_name)\n\n        # Phase 4: Remove unused definitions\n        def is_def_used(def_name: str, visiting: set[str] | None = None) -> bool:\n            \"\"\"Check if a definition is used, handling circular references.\"\"\"\n            if def_name in root_refs:\n                return True  # Used directly from main schema\n\n            # Check if any definition that references this one is itself used\n            referencing_defs = def_dependencies.get(def_name, [])\n            if referencing_defs:\n                if visiting is None:\n                    visiting = set()\n\n                # Avoid infinite recursion on circular references\n                if def_name in visiting:\n                    return False\n                visiting = visiting | {def_name}\n\n                # If any referencing def is used, then this def is used\n                for referencing_def in referencing_defs:\n                    if referencing_def not in visiting and is_def_used(\n                        referencing_def, visiting\n                    ):\n                        return True\n\n            return False\n\n        # Remove unused definitions\n        for def_name in list(defs.keys()):\n            if not is_def_used(def_name):\n                defs.pop(def_name)\n\n        # Clean up empty $defs section\n        if not defs:\n            schema.pop(\"$defs\", None)\n\n    return schema\n\n\ndef compress_schema(\n    schema: dict[str, Any],\n    prune_params: list[str] | None = None,\n    prune_additional_properties: bool = False,\n    prune_titles: bool = False,\n    dereference: bool = False,\n) -> dict[str, Any]:\n    \"\"\"\n    Compress and optimize a JSON schema for MCP compatibility.\n\n    Args:\n        schema: The schema to compress\n        prune_params: List of parameter names to remove from properties\n        prune_additional_properties: Whether to remove additionalProperties: false.\n            Defaults to False to maintain MCP client compatibility, as some clients\n            (e.g., Claude) require additionalProperties: false for strict validation.\n        prune_titles: Whether to remove title fields from the schema\n        dereference: Whether to dereference $ref by inlining definitions.\n            Defaults to False; dereferencing is typically handled by\n            middleware at serve-time instead.\n    \"\"\"\n    if dereference:\n        schema = dereference_refs(schema)\n\n    # Resolve root-level $ref for MCP spec compliance (requires type: object at root)\n    schema = resolve_root_ref(schema)\n\n    # Remove specific parameters if requested\n    for param in prune_params or []:\n        schema = _prune_param(schema, param=param)\n\n    # Apply combined optimizations in a single tree traversal.\n    # Always prune unused $defs to keep schemas clean after parameter removal.\n    schema = _single_pass_optimize(\n        schema,\n        prune_titles=prune_titles,\n        prune_additional_properties=prune_additional_properties,\n        prune_defs=True,\n    )\n\n    return schema\n"
  },
  {
    "path": "src/fastmcp/utilities/json_schema_type.py",
    "content": "\"\"\"Convert JSON Schema to Python types with validation.\n\nThe json_schema_to_type function converts a JSON Schema into a Python type that can be used\nfor validation with Pydantic. It supports:\n\n- Basic types (string, number, integer, boolean, null)\n- Complex types (arrays, objects)\n- Format constraints (date-time, email, uri)\n- Numeric constraints (minimum, maximum, multipleOf)\n- String constraints (minLength, maxLength, pattern)\n- Array constraints (minItems, maxItems, uniqueItems)\n- Object properties with defaults\n- References and recursive schemas\n- Enums and constants\n- Union types\n\nExample:\n    ```python\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"type\": \"string\", \"minLength\": 1},\n            \"age\": {\"type\": \"integer\", \"minimum\": 0},\n            \"email\": {\"type\": \"string\", \"format\": \"email\"}\n        },\n        \"required\": [\"name\", \"age\"]\n    }\n\n    # Name is optional and will be inferred from schema's \"title\" property if not provided\n    Person = json_schema_to_type(schema)\n    # Creates a validated dataclass with name, age, and optional email fields\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nimport re\nfrom collections.abc import Callable, Mapping\nfrom copy import deepcopy\nfrom dataclasses import MISSING, field, make_dataclass\nfrom datetime import datetime\nfrom typing import (\n    Annotated,\n    Any,\n    ForwardRef,\n    Literal,\n    Union,\n    cast,\n)\n\nfrom pydantic import (\n    AnyUrl,\n    BaseModel,\n    ConfigDict,\n    EmailStr,\n    Field,\n    Json,\n    StringConstraints,\n    model_validator,\n)\nfrom typing_extensions import NotRequired, TypedDict\n\n__all__ = [\"JSONSchema\", \"json_schema_to_type\"]\n\n\nFORMAT_TYPES: dict[str, Any] = {\n    \"date-time\": datetime,\n    \"email\": EmailStr,\n    \"uri\": AnyUrl,\n    \"json\": Json,\n}\n\n_classes: dict[tuple[str, Any], type | None] = {}\n\n\nclass JSONSchema(TypedDict):\n    type: NotRequired[str | list[str]]\n    properties: NotRequired[dict[str, JSONSchema]]\n    required: NotRequired[list[str]]\n    additionalProperties: NotRequired[bool | JSONSchema]\n    items: NotRequired[JSONSchema | list[JSONSchema]]\n    enum: NotRequired[list[Any]]\n    const: NotRequired[Any]\n    default: NotRequired[Any]\n    description: NotRequired[str]\n    title: NotRequired[str]\n    examples: NotRequired[list[Any]]\n    format: NotRequired[str]\n    allOf: NotRequired[list[JSONSchema]]\n    anyOf: NotRequired[list[JSONSchema]]\n    oneOf: NotRequired[list[JSONSchema]]\n    not_: NotRequired[JSONSchema]\n    definitions: NotRequired[dict[str, JSONSchema]]\n    dependencies: NotRequired[dict[str, JSONSchema | list[str]]]\n    pattern: NotRequired[str]\n    minLength: NotRequired[int]\n    maxLength: NotRequired[int]\n    minimum: NotRequired[int | float]\n    maximum: NotRequired[int | float]\n    exclusiveMinimum: NotRequired[int | float]\n    exclusiveMaximum: NotRequired[int | float]\n    multipleOf: NotRequired[int | float]\n    uniqueItems: NotRequired[bool]\n    minItems: NotRequired[int]\n    maxItems: NotRequired[int]\n    additionalItems: NotRequired[bool | JSONSchema]\n\n\ndef json_schema_to_type(\n    schema: Mapping[str, Any],\n    name: str | None = None,\n) -> type:\n    \"\"\"Convert JSON schema to appropriate Python type with validation.\n\n    Args:\n        schema: A JSON Schema dictionary defining the type structure and validation rules\n        name: Optional name for object schemas. Only allowed when schema type is \"object\".\n            If not provided for objects, name will be inferred from schema's \"title\"\n            property or default to \"Root\".\n\n    Returns:\n        A Python type (typically a dataclass for objects) with Pydantic validation\n\n    Raises:\n        ValueError: If a name is provided for a non-object schema\n\n    Examples:\n        Create a dataclass from an object schema:\n        ```python\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"Person\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\", \"minLength\": 1},\n                \"age\": {\"type\": \"integer\", \"minimum\": 0},\n                \"email\": {\"type\": \"string\", \"format\": \"email\"}\n            },\n            \"required\": [\"name\", \"age\"]\n        }\n\n        Person = json_schema_to_type(schema)\n        # Creates a dataclass with name, age, and optional email fields:\n        # @dataclass\n        # class Person:\n        #     name: str\n        #     age: int\n        #     email: str | None = None\n        ```\n        Person(name=\"John\", age=30)\n\n        Create a scalar type with constraints:\n        ```python\n        schema = {\n            \"type\": \"string\",\n            \"minLength\": 3,\n            \"pattern\": \"^[A-Z][a-z]+$\"\n        }\n\n        NameType = json_schema_to_type(schema)\n        # Creates Annotated[str, StringConstraints(min_length=3, pattern=\"^[A-Z][a-z]+$\")]\n\n        @dataclass\n        class Name:\n            name: NameType\n        ```\n    \"\"\"\n    # Always use the top-level schema for references\n    if schema.get(\"type\") == \"object\":\n        # If no properties defined but has additionalProperties, return typed dict\n        if not schema.get(\"properties\") and schema.get(\"additionalProperties\"):\n            additional_props = schema[\"additionalProperties\"]\n            if additional_props is True:\n                return dict[str, Any]\n            else:\n                # Handle typed dictionaries like dict[str, str]\n                value_type = _schema_to_type(additional_props, schemas=schema)\n                # value_type might be ForwardRef or type - cast to Any for dynamic type construction\n                return cast(type[Any], dict[str, value_type])  # type: ignore[valid-type]\n        # If no properties and no additionalProperties, default to dict[str, Any] for safety\n        elif not schema.get(\"properties\") and not schema.get(\"additionalProperties\"):\n            return dict[str, Any]\n        # If has properties AND additionalProperties is True, use Pydantic BaseModel\n        elif schema.get(\"properties\") and schema.get(\"additionalProperties\") is True:\n            return _create_pydantic_model(schema, name, schemas=schema)\n        # Otherwise use fast dataclass\n        return _create_dataclass(schema, name, schemas=schema)\n    elif name:\n        raise ValueError(f\"Can not apply name to non-object schema: {name}\")\n    result = _schema_to_type(schema, schemas=schema)\n    return result  # type: ignore[return-value]\n\n\ndef _hash_schema(schema: Mapping[str, Any]) -> str:\n    \"\"\"Generate a deterministic hash for schema caching.\"\"\"\n    return hashlib.sha256(json.dumps(schema, sort_keys=True).encode()).hexdigest()\n\n\ndef _resolve_ref(ref: str, schemas: Mapping[str, Any]) -> Mapping[str, Any]:\n    \"\"\"Resolve JSON Schema reference to target schema.\"\"\"\n    path = ref.replace(\"#/\", \"\").split(\"/\")\n    current = schemas\n    for part in path:\n        current = current.get(part, {})\n    return current\n\n\ndef _create_string_type(schema: Mapping[str, Any]) -> type | Annotated[Any, ...]:\n    \"\"\"Create string type with optional constraints.\"\"\"\n    if \"const\" in schema:\n        return Literal[schema[\"const\"]]  # type: ignore\n\n    if fmt := schema.get(\"format\"):\n        if fmt == \"uri\":\n            return AnyUrl\n        elif fmt == \"uri-reference\":\n            return str\n        return FORMAT_TYPES.get(fmt, str)\n\n    constraints = {\n        k: v\n        for k, v in {\n            \"min_length\": schema.get(\"minLength\"),\n            \"max_length\": schema.get(\"maxLength\"),\n            \"pattern\": schema.get(\"pattern\"),\n        }.items()\n        if v is not None\n    }\n\n    return Annotated[str, StringConstraints(**constraints)] if constraints else str\n\n\ndef _create_numeric_type(\n    base: type[int | float], schema: Mapping[str, Any]\n) -> type | Annotated[Any, ...]:\n    \"\"\"Create numeric type with optional constraints.\"\"\"\n    if \"const\" in schema:\n        return Literal[schema[\"const\"]]  # type: ignore\n\n    constraints = {\n        k: v\n        for k, v in {\n            \"gt\": schema.get(\"exclusiveMinimum\"),\n            \"ge\": schema.get(\"minimum\"),\n            \"lt\": schema.get(\"exclusiveMaximum\"),\n            \"le\": schema.get(\"maximum\"),\n            \"multiple_of\": schema.get(\"multipleOf\"),\n        }.items()\n        if v is not None\n    }\n\n    return Annotated[base, Field(**constraints)] if constraints else base  # type: ignore[return-value]\n\n\ndef _create_enum(name: str, values: list[Any]) -> type:\n    \"\"\"Create enum type from list of values.\"\"\"\n    # Always return Literal for enum fields to preserve the literal nature\n    return Literal[tuple(values)]  # type: ignore[return-value]\n\n\ndef _create_array_type(\n    schema: Mapping[str, Any], schemas: Mapping[str, Any]\n) -> type | Annotated[Any, ...]:\n    \"\"\"Create list/set type with optional constraints.\"\"\"\n    items = schema.get(\"items\", {})\n    if isinstance(items, list):\n        # Handle positional item schemas\n        item_types = [_schema_to_type(s, schemas) for s in items]\n        combined = Union[tuple(item_types)]  # noqa: UP007\n        base = list[combined]  # type: ignore[valid-type]\n    else:\n        # Handle single item schema\n        item_type = _schema_to_type(items, schemas)\n        base_class = set if schema.get(\"uniqueItems\") else list\n        base = base_class[item_type]\n\n    constraints = {\n        k: v\n        for k, v in {\n            \"min_length\": schema.get(\"minItems\"),\n            \"max_length\": schema.get(\"maxItems\"),\n        }.items()\n        if v is not None\n    }\n\n    return Annotated[base, Field(**constraints)] if constraints else base  # type: ignore[return-value]\n\n\ndef _return_Any() -> Any:\n    return Any\n\n\ndef _get_from_type_handler(\n    schema: Mapping[str, Any], schemas: Mapping[str, Any]\n) -> Callable[..., Any]:\n    \"\"\"Get the appropriate type handler for the schema.\"\"\"\n\n    type_handlers: dict[str, Callable[..., Any]] = {  # TODO\n        \"string\": lambda s: _create_string_type(s),\n        \"integer\": lambda s: _create_numeric_type(int, s),\n        \"number\": lambda s: _create_numeric_type(float, s),\n        \"boolean\": lambda _: bool,\n        \"null\": lambda _: type(None),\n        \"array\": lambda s: _create_array_type(s, schemas),\n        \"object\": lambda s: (\n            _create_pydantic_model(s, s.get(\"title\"), schemas)\n            if s.get(\"properties\") and s.get(\"additionalProperties\") is True\n            else _create_dataclass(s, s.get(\"title\"), schemas)\n        ),\n    }\n    return type_handlers.get(schema.get(\"type\", None), _return_Any)\n\n\ndef _schema_to_type(\n    schema: Mapping[str, Any],\n    schemas: Mapping[str, Any],\n) -> type | ForwardRef:\n    \"\"\"Convert schema to appropriate Python type.\"\"\"\n    if not schema:\n        return object\n\n    if \"type\" not in schema and \"properties\" in schema:\n        return _create_dataclass(schema, schema.get(\"title\", \"<unknown>\"), schemas)\n\n    # Handle references first\n    if \"$ref\" in schema:\n        ref = schema[\"$ref\"]\n        # Handle self-reference\n        if ref == \"#\":\n            return ForwardRef(schema.get(\"title\", \"Root\"))\n        return _schema_to_type(_resolve_ref(ref, schemas), schemas)\n\n    if \"const\" in schema:\n        return Literal[schema[\"const\"]]  # type: ignore\n\n    if \"enum\" in schema:\n        return _create_enum(f\"Enum_{len(_classes)}\", schema[\"enum\"])\n\n    # Handle anyOf unions\n    if \"anyOf\" in schema:\n        types: list[type | Any] = []\n        for subschema in schema[\"anyOf\"]:\n            # Special handling for dict-like objects in unions\n            if (\n                subschema.get(\"type\") == \"object\"\n                and not subschema.get(\"properties\")\n                and subschema.get(\"additionalProperties\")\n            ):\n                # This is a dict type, handle it directly\n                additional_props = subschema[\"additionalProperties\"]\n                if additional_props is True:\n                    types.append(dict[str, Any])\n                else:\n                    value_type = _schema_to_type(additional_props, schemas)\n                    types.append(dict[str, value_type])  # type: ignore\n            else:\n                types.append(_schema_to_type(subschema, schemas))\n\n        # Check if one of the types is None (null)\n        has_null = type(None) in types\n        types = [t for t in types if t is not type(None)]\n\n        if len(types) == 0:\n            return type(None)\n        elif len(types) == 1:\n            if has_null:\n                return types[0] | None  # type: ignore\n            else:\n                return types[0]\n        else:\n            if has_null:\n                return Union[(*types, type(None))]  # type: ignore\n            else:\n                return Union[tuple(types)]  # type: ignore # noqa: UP007\n\n    schema_type = schema.get(\"type\")\n    if not schema_type:\n        return Any\n\n    if isinstance(schema_type, list):\n        # Create a copy of the schema for each type, but keep all constraints\n        types: list[type | Any] = []\n        for t in schema_type:\n            type_schema = dict(schema)\n            type_schema[\"type\"] = t\n            types.append(_schema_to_type(type_schema, schemas))\n        has_null = type(None) in types\n        types = [t for t in types if t is not type(None)]\n        if has_null:\n            if len(types) == 1:\n                return types[0] | None  # type: ignore\n            else:\n                return Union[(*types, type(None))]  # type: ignore\n        return Union[tuple(types)]  # type: ignore # noqa: UP007\n\n    return _get_from_type_handler(schema, schemas)(schema)\n\n\ndef _sanitize_name(name: str) -> str:\n    \"\"\"Convert string to valid Python identifier.\"\"\"\n    original_name = name\n    # Step 1: replace everything except [0-9a-zA-Z_] with underscores\n    cleaned = re.sub(r\"[^0-9a-zA-Z_]\", \"_\", name)\n    # Step 2: deduplicate underscores\n    cleaned = re.sub(r\"__+\", \"_\", cleaned)\n    # Step 3: if the first char of original name isn't a letter or underscore, prepend field_\n    if not name or not re.match(r\"[a-zA-Z_]\", name[0]):\n        cleaned = f\"field_{cleaned}\"\n    # Step 4: deduplicate again\n    cleaned = re.sub(r\"__+\", \"_\", cleaned)\n    # Step 5: only strip trailing underscores if they weren't in the original name\n    if not original_name.endswith(\"_\"):\n        cleaned = cleaned.rstrip(\"_\")\n    return cleaned\n\n\ndef _get_default_value(\n    schema: dict[str, Any],\n    prop_name: str,\n    parent_default: dict[str, Any] | None = None,\n) -> Any:\n    \"\"\"Get default value with proper priority ordering.\n    1. Value from parent's default if it exists\n    2. Property's own default if it exists\n    3. None\n    \"\"\"\n    if parent_default is not None and prop_name in parent_default:\n        return parent_default[prop_name]\n    return schema.get(\"default\")\n\n\ndef _create_field_with_default(\n    field_type: type,\n    default_value: Any,\n    schema: dict[str, Any],\n) -> Any:\n    \"\"\"Create a field with simplified default handling.\"\"\"\n    # Always use None as default for complex types\n    if isinstance(default_value, dict | list) or default_value is None:\n        return field(default=None)\n\n    # For simple types, use the value directly\n    return field(default=default_value)\n\n\ndef _create_pydantic_model(\n    schema: Mapping[str, Any],\n    name: str | None = None,\n    schemas: Mapping[str, Any] | None = None,\n) -> type:\n    \"\"\"Create Pydantic BaseModel from object schema with additionalProperties.\"\"\"\n    name = name or schema.get(\"title\", \"Root\")\n    if name is None:\n        raise ValueError(\"Name is required\")\n    sanitized_name = _sanitize_name(name)\n    schema_hash = _hash_schema(schema)\n    cache_key = (schema_hash, sanitized_name)\n\n    # Return existing class if already built\n    if cache_key in _classes:\n        existing = _classes[cache_key]\n        if existing is None:\n            return ForwardRef(sanitized_name)  # type: ignore[return-value]\n        return existing\n\n    # Place placeholder for recursive references\n    _classes[cache_key] = None\n\n    properties = schema.get(\"properties\", {})\n    required = schema.get(\"required\", [])\n\n    # Build field annotations and defaults\n    annotations = {}\n    defaults = {}\n\n    for prop_name, prop_schema in properties.items():\n        field_type = _schema_to_type(prop_schema, schemas or {})\n\n        # Handle defaults\n        default_value = prop_schema.get(\"default\", MISSING)\n        if default_value is not MISSING:\n            defaults[prop_name] = default_value\n            annotations[prop_name] = field_type\n        elif prop_name in required:\n            annotations[prop_name] = field_type\n        else:\n            annotations[prop_name] = Union[field_type, type(None)]  # type: ignore[misc]  # noqa: UP007\n            defaults[prop_name] = None\n\n    # Create Pydantic model class\n    cls_dict = {\n        \"__annotations__\": annotations,\n        \"model_config\": ConfigDict(extra=\"allow\"),\n        **defaults,\n    }\n\n    cls = type(sanitized_name, (BaseModel,), cls_dict)\n\n    # Store completed class\n    _classes[cache_key] = cls\n    return cls\n\n\ndef _create_dataclass(\n    schema: Mapping[str, Any],\n    name: str | None = None,\n    schemas: Mapping[str, Any] | None = None,\n) -> type:\n    \"\"\"Create dataclass from object schema.\"\"\"\n    name = name or schema.get(\"title\", \"Root\")\n    # Sanitize name for class creation\n    if name is None:\n        raise ValueError(\"Name is required\")\n    sanitized_name = _sanitize_name(name)\n    schema_hash = _hash_schema(schema)\n    cache_key = (schema_hash, sanitized_name)\n    original_schema = dict(schema)  # Store copy for validator\n\n    # Return existing class if already built\n    if cache_key in _classes:\n        existing = _classes[cache_key]\n        if existing is None:\n            return ForwardRef(sanitized_name)  # type: ignore[return-value]\n        return existing\n\n    # Place placeholder for recursive references\n    _classes[cache_key] = None\n\n    if \"$ref\" in schema:\n        ref = schema[\"$ref\"]\n        if ref == \"#\":\n            return ForwardRef(sanitized_name)  # type: ignore[return-value]\n        schema = _resolve_ref(ref, schemas or {})\n\n    properties = schema.get(\"properties\", {})\n    required = schema.get(\"required\", [])\n\n    fields: list[tuple[Any, ...]] = []\n    for prop_name, prop_schema in properties.items():\n        field_name = _sanitize_name(prop_name)\n\n        # Check for self-reference in property\n        if prop_schema.get(\"$ref\") == \"#\":\n            field_type = ForwardRef(sanitized_name)\n        else:\n            field_type = _schema_to_type(prop_schema, schemas or {})\n\n        default_val = prop_schema.get(\"default\", MISSING)\n        is_required = prop_name in required\n\n        # Include alias in field metadata\n        meta = {\"alias\": prop_name}\n\n        if default_val is not MISSING:\n            if isinstance(default_val, dict | list):\n                field_def = field(\n                    default_factory=lambda d=default_val: deepcopy(d), metadata=meta\n                )\n            else:\n                field_def = field(default=default_val, metadata=meta)\n        else:\n            if is_required:\n                field_def = field(metadata=meta)\n            else:\n                field_def = field(default=None, metadata=meta)\n\n        if is_required or default_val is not MISSING:\n            fields.append((field_name, field_type, field_def))\n        else:\n            fields.append((field_name, Union[field_type, type(None)], field_def))  # type: ignore[misc]  # noqa: UP007\n\n    cls = make_dataclass(sanitized_name, fields, kw_only=True)\n\n    # Add model validator for defaults\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _apply_defaults(cls, data: Mapping[str, Any]):\n        if isinstance(data, dict):\n            return _merge_defaults(data, original_schema)\n        return data\n\n    cls._apply_defaults = _apply_defaults  # type: ignore[attr-defined]\n\n    # Store completed class\n    _classes[cache_key] = cls\n    return cls\n\n\ndef _merge_defaults(\n    data: Mapping[str, Any],\n    schema: Mapping[str, Any],\n    parent_default: Mapping[str, Any] | None = None,\n) -> dict[str, Any]:\n    \"\"\"Merge defaults with provided data at all levels.\"\"\"\n    # If we have no data\n    if not data:\n        # Start with parent default if available\n        if parent_default:\n            result = dict(parent_default)\n        # Otherwise use schema default if available\n        elif \"default\" in schema:\n            result = dict(schema[\"default\"])\n        # Otherwise start empty\n        else:\n            result = {}\n    # If we have data and a parent default, merge them\n    elif parent_default:\n        result = dict(parent_default)\n        for key, value in data.items():\n            if (\n                isinstance(value, dict)\n                and key in result\n                and isinstance(result[key], dict)\n            ):\n                # recursively merge nested dicts\n                result[key] = _merge_defaults(value, {\"properties\": {}}, result[key])\n            else:\n                result[key] = value\n    # Otherwise just use the data\n    else:\n        result = dict(data)\n\n    # For each property in the schema\n    for prop_name, prop_schema in schema.get(\"properties\", {}).items():\n        # If property is missing, apply defaults in priority order\n        if prop_name not in result:\n            if parent_default and prop_name in parent_default:\n                result[prop_name] = parent_default[prop_name]\n            elif \"default\" in prop_schema:\n                result[prop_name] = prop_schema[\"default\"]\n\n        # If property exists and is an object, recursively merge\n        if (\n            prop_name in result\n            and isinstance(result[prop_name], dict)\n            and prop_schema.get(\"type\") == \"object\"\n        ):\n            # Get the appropriate default for this nested object\n            nested_default = None\n            if parent_default and prop_name in parent_default:\n                nested_default = parent_default[prop_name]\n            elif \"default\" in prop_schema:\n                nested_default = prop_schema[\"default\"]\n\n            result[prop_name] = _merge_defaults(\n                result[prop_name], prop_schema, nested_default\n            )\n\n    return result\n"
  },
  {
    "path": "src/fastmcp/utilities/lifespan.py",
    "content": "\"\"\"Lifespan utilities for combining async context manager lifespans.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncIterator, Callable, Mapping\nfrom contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager\nfrom typing import Any, TypeVar\n\nAppT = TypeVar(\"AppT\")\n\n\ndef combine_lifespans(\n    *lifespans: Callable[[AppT], AbstractAsyncContextManager[Mapping[str, Any] | None]],\n) -> Callable[[AppT], AbstractAsyncContextManager[dict[str, Any]]]:\n    \"\"\"Combine multiple lifespans into a single lifespan.\n\n    Useful when mounting FastMCP into FastAPI and you need to run\n    both your app's lifespan and the MCP server's lifespan.\n\n    Works with both FastAPI-style lifespans (yield None) and FastMCP-style\n    lifespans (yield dict). Results are merged; later lifespans override\n    earlier ones on key conflicts.\n\n    Lifespans are entered in order and exited in reverse order (LIFO).\n\n    Example:\n        ```python\n        from fastmcp import FastMCP\n        from fastmcp.utilities.lifespan import combine_lifespans\n        from fastapi import FastAPI\n\n        mcp = FastMCP(\"Tools\")\n        mcp_app = mcp.http_app()\n\n        app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))\n        app.mount(\"/mcp\", mcp_app)  # MCP endpoint at /mcp\n        ```\n\n    Args:\n        *lifespans: Lifespan context manager factories to combine.\n\n    Returns:\n        A combined lifespan context manager factory.\n    \"\"\"\n\n    @asynccontextmanager\n    async def combined(app: AppT) -> AsyncIterator[dict[str, Any]]:\n        merged: dict[str, Any] = {}\n        async with AsyncExitStack() as stack:\n            for ls in lifespans:\n                result = await stack.enter_async_context(ls(app))\n                if result is not None:\n                    merged.update(result)\n            yield merged\n\n    return combined\n"
  },
  {
    "path": "src/fastmcp/utilities/logging.py",
    "content": "\"\"\"Logging utilities for FastMCP.\"\"\"\n\nimport contextlib\nimport logging\nfrom typing import Any, Literal, cast\n\nfrom rich.console import Console\nfrom rich.logging import RichHandler\nfrom typing_extensions import override\n\nimport fastmcp\n\n\ndef get_logger(name: str) -> logging.Logger:\n    \"\"\"Get a logger nested under FastMCP namespace.\n\n    Args:\n        name: the name of the logger, which will be prefixed with 'FastMCP.'\n\n    Returns:\n        a configured logger instance\n    \"\"\"\n    if name.startswith(\"fastmcp.\"):\n        return logging.getLogger(name=name)\n\n    return logging.getLogger(name=f\"fastmcp.{name}\")\n\n\ndef configure_logging(\n    level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] | int = \"INFO\",\n    logger: logging.Logger | None = None,\n    enable_rich_tracebacks: bool | None = None,\n    **rich_kwargs: Any,\n) -> None:\n    \"\"\"\n    Configure logging for FastMCP.\n\n    Args:\n        logger: the logger to configure\n        level: the log level to use\n        rich_kwargs: the parameters to use for creating RichHandler\n    \"\"\"\n    # Check if logging is disabled in settings\n    if not fastmcp.settings.log_enabled:\n        return\n\n    # Use settings default if not specified\n    if enable_rich_tracebacks is None:\n        enable_rich_tracebacks = fastmcp.settings.enable_rich_tracebacks\n\n    if logger is None:\n        logger = logging.getLogger(\"fastmcp\")\n\n    formatter = logging.Formatter(\"%(message)s\")\n\n    # Don't propagate to the root logger\n    logger.propagate = False\n    logger.setLevel(level)\n\n    # Remove any existing handlers to avoid duplicates on reconfiguration\n    for hdlr in logger.handlers[:]:\n        logger.removeHandler(hdlr)\n\n    # Use standard logging handlers if rich logging is disabled\n    if not fastmcp.settings.enable_rich_logging:\n        # Create a standard StreamHandler for stderr\n        handler = logging.StreamHandler()\n        handler.setFormatter(logging.Formatter(\"%(levelname)s: %(message)s\"))\n        logger.addHandler(handler)\n        return\n\n    # Configure the handler for normal logs\n    handler = RichHandler(\n        console=Console(stderr=True),\n        **rich_kwargs,\n    )\n    handler.setFormatter(formatter)\n\n    # filter to exclude tracebacks\n    handler.addFilter(lambda record: record.exc_info is None)\n\n    # Configure the handler for tracebacks, for tracebacks we use a compressed format:\n    # no path or level name to maximize width available for the traceback\n    # suppress framework frames and limit the number of frames to 3\n\n    import mcp\n    import pydantic\n\n    # Build traceback kwargs with defaults that can be overridden\n    traceback_kwargs = {\n        \"console\": Console(stderr=True),\n        \"show_path\": False,\n        \"show_level\": False,\n        \"rich_tracebacks\": enable_rich_tracebacks,\n        \"tracebacks_max_frames\": 3,\n        \"tracebacks_suppress\": [fastmcp, mcp, pydantic],\n    }\n    # Override defaults with user-provided values\n    traceback_kwargs.update(rich_kwargs)\n\n    traceback_handler = RichHandler(**traceback_kwargs)  # type: ignore[arg-type]\n    traceback_handler.setFormatter(formatter)\n\n    traceback_handler.addFilter(lambda record: record.exc_info is not None)\n\n    logger.addHandler(handler)\n    logger.addHandler(traceback_handler)\n\n\n@contextlib.contextmanager\ndef temporary_log_level(\n    level: str | None,\n    logger: logging.Logger | None = None,\n    enable_rich_tracebacks: bool | None = None,\n    **rich_kwargs: Any,\n):\n    \"\"\"Context manager to temporarily set log level and restore it afterwards.\n\n    Args:\n        level: The temporary log level to set (e.g., \"DEBUG\", \"INFO\")\n        logger: Optional logger to configure (defaults to FastMCP logger)\n        enable_rich_tracebacks: Whether to enable rich tracebacks\n        **rich_kwargs: Additional parameters for RichHandler\n\n    Usage:\n        with temporary_log_level(\"DEBUG\"):\n            # Code that runs with DEBUG logging\n            pass\n        # Original log level is restored here\n    \"\"\"\n    if level:\n        # Get the original log level from settings\n        original_level = fastmcp.settings.log_level\n\n        # Configure with new level\n        # Cast to proper type for type checker\n        log_level_literal = cast(\n            Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"],\n            level.upper(),\n        )\n        configure_logging(\n            level=log_level_literal,\n            logger=logger,\n            enable_rich_tracebacks=enable_rich_tracebacks,\n            **rich_kwargs,\n        )\n        try:\n            yield\n        finally:\n            # Restore original configuration using configure_logging\n            # This will respect the log_enabled setting\n            configure_logging(\n                level=original_level,\n                logger=logger,\n                enable_rich_tracebacks=enable_rich_tracebacks,\n                **rich_kwargs,\n            )\n    else:\n        yield\n\n\n_level_to_no: dict[\n    Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] | None, int | None\n] = {\n    \"DEBUG\": logging.DEBUG,\n    \"INFO\": logging.INFO,\n    \"WARNING\": logging.WARNING,\n    \"ERROR\": logging.ERROR,\n    \"CRITICAL\": logging.CRITICAL,\n    None: None,\n}\n\n\nclass _ClampedLogFilter(logging.Filter):\n    min_level: tuple[int, str] | None\n    max_level: tuple[int, str] | None\n\n    def __init__(\n        self,\n        min_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n        | None = None,\n        max_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n        | None = None,\n    ):\n        self.min_level = None\n        self.max_level = None\n\n        if min_level_no := _level_to_no.get(min_level):\n            self.min_level = (min_level_no, str(min_level))\n        if max_level_no := _level_to_no.get(max_level):\n            self.max_level = (max_level_no, str(max_level))\n\n        super().__init__()\n\n    @override\n    def filter(self, record: logging.LogRecord) -> bool:\n        if self.max_level:\n            max_level_no, max_level_name = self.max_level\n\n            if record.levelno > max_level_no:\n                record.levelno = max_level_no\n                record.levelname = max_level_name\n                return True\n\n        if self.min_level:\n            min_level_no, min_level_name = self.min_level\n            if record.levelno < min_level_no:\n                record.levelno = min_level_no\n                record.levelname = min_level_name\n                return True\n\n        return True\n\n\ndef _clamp_logger(\n    logger: logging.Logger,\n    min_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] | None = None,\n    max_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] | None = None,\n) -> None:\n    \"\"\"Clamp the logger to a minimum and maximum level.\n\n    If min_level is provided, messages logged at a lower level than `min_level` will have their level increased to `min_level`.\n    If max_level is provided, messages logged at a higher level than `max_level` will have their level decreased to `max_level`.\n\n    Args:\n        min_level: The lower bound of the clamp\n        max_level: The upper bound of the clamp\n    \"\"\"\n    _unclamp_logger(logger=logger)\n\n    logger.addFilter(filter=_ClampedLogFilter(min_level=min_level, max_level=max_level))\n\n\ndef _unclamp_logger(logger: logging.Logger) -> None:\n    \"\"\"Remove all clamped log filters from the logger.\"\"\"\n    for filter in logger.filters[:]:\n        if isinstance(filter, _ClampedLogFilter):\n            logger.removeFilter(filter)\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/__init__.py",
    "content": "\"\"\"FastMCP Configuration module.\n\nThis module provides versioned configuration support for FastMCP servers.\nThe current version is v1, which is re-exported here for convenience.\n\"\"\"\n\nfrom fastmcp.utilities.mcp_server_config.v1.environments.base import Environment\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\nfrom fastmcp.utilities.mcp_server_config.v1.mcp_server_config import (\n    Deployment,\n    MCPServerConfig,\n    generate_schema,\n)\nfrom fastmcp.utilities.mcp_server_config.v1.sources.base import Source\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\n__all__ = [\n    \"Deployment\",\n    \"Environment\",\n    \"FileSystemSource\",\n    \"MCPServerConfig\",\n    \"Source\",\n    \"UVEnvironment\",\n    \"generate_schema\",\n]\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/__init__.py",
    "content": ""
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/environments/__init__.py",
    "content": "\"\"\"Environment configuration for MCP servers.\"\"\"\n\nfrom fastmcp.utilities.mcp_server_config.v1.environments.base import Environment\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\n__all__ = [\"Environment\", \"UVEnvironment\"]\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/environments/base.py",
    "content": "from abc import ABC, abstractmethod\nfrom pathlib import Path\n\nfrom pydantic import BaseModel, Field\n\n\nclass Environment(BaseModel, ABC):\n    \"\"\"Base class for environment configuration.\"\"\"\n\n    type: str = Field(description=\"Environment type identifier\")\n\n    @abstractmethod\n    def build_command(self, command: list[str]) -> list[str]:\n        \"\"\"Build the full command with environment setup.\n\n        Args:\n            command: Base command to wrap with environment setup\n\n        Returns:\n            Full command ready for subprocess execution\n        \"\"\"\n\n    async def prepare(self, output_dir: Path | None = None) -> None:\n        \"\"\"Prepare the environment (optional, can be no-op).\n\n        Args:\n            output_dir: Directory for persistent environment setup\n        \"\"\"\n        # Default no-op implementation\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py",
    "content": "import shutil\nimport subprocess\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom pydantic import Field\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.base import Environment\n\nlogger = get_logger(\"cli.config\")\n\n\nclass UVEnvironment(Environment):\n    \"\"\"Configuration for Python environment setup.\"\"\"\n\n    type: Literal[\"uv\"] = \"uv\"\n\n    python: str | None = Field(\n        default=None,\n        description=\"Python version constraint\",\n        examples=[\"3.10\", \"3.11\", \"3.12\"],\n    )\n\n    dependencies: list[str] | None = Field(\n        default=None,\n        description=\"Python packages to install with PEP 508 specifiers\",\n        examples=[[\"fastmcp>=2.0,<3\", \"httpx\", \"pandas>=2.0\"]],\n    )\n\n    requirements: Path | None = Field(\n        default=None,\n        description=\"Path to requirements.txt file\",\n        examples=[\"requirements.txt\", \"../requirements/prod.txt\"],\n    )\n\n    project: Path | None = Field(\n        default=None,\n        description=\"Path to project directory containing pyproject.toml\",\n        examples=[\".\", \"../my-project\"],\n    )\n\n    editable: list[Path] | None = Field(\n        default=None,\n        description=\"Directories to install in editable mode\",\n        examples=[[\".\", \"../my-package\"], [\"/path/to/package\"]],\n    )\n\n    def build_command(self, command: list[str]) -> list[str]:\n        \"\"\"Build complete uv run command with environment args and command to execute.\n\n        Args:\n            command: Command to execute (e.g., [\"fastmcp\", \"run\", \"server.py\"])\n\n        Returns:\n            Complete command ready for subprocess.run, including \"uv\" prefix if needed.\n            If no environment configuration is set, returns the command unchanged.\n        \"\"\"\n        # If no environment setup is needed, return command as-is\n        if not self._must_run_with_uv():\n            return command\n\n        args = [\"uv\", \"run\"]\n\n        # Add project if specified\n        if self.project:\n            args.extend([\"--project\", str(self.project.resolve())])\n\n        # Add Python version if specified (only if no project, as project has its own Python)\n        if self.python and not self.project:\n            args.extend([\"--python\", self.python])\n\n        # Always add dependencies, requirements, and editable packages\n        # These work with --project to add additional packages on top of the project env\n        if self.dependencies:\n            for dep in sorted(set(self.dependencies)):\n                args.extend([\"--with\", dep])\n\n        # Add requirements file\n        if self.requirements:\n            args.extend([\"--with-requirements\", str(self.requirements.resolve())])\n\n        # Add editable packages\n        if self.editable:\n            for editable_path in self.editable:\n                args.extend([\"--with-editable\", str(editable_path.resolve())])\n\n        # Add the command\n        args.extend(command)\n\n        return args\n\n    def _must_run_with_uv(self) -> bool:\n        \"\"\"Check if this environment config requires uv to set up.\n\n        Returns:\n            True if any environment settings require uv run\n        \"\"\"\n        return any(\n            [\n                self.python is not None,\n                self.dependencies is not None,\n                self.requirements is not None,\n                self.project is not None,\n                self.editable is not None,\n            ]\n        )\n\n    async def prepare(self, output_dir: Path | None = None) -> None:\n        \"\"\"Prepare the Python environment using uv.\n\n        Args:\n            output_dir: Directory where the persistent uv project will be created.\n                       If None, creates a temporary directory for ephemeral use.\n        \"\"\"\n\n        # Check if uv is available\n        if not shutil.which(\"uv\"):\n            raise RuntimeError(\n                \"uv is not installed. Please install it with: \"\n                \"curl -LsSf https://astral.sh/uv/install.sh | sh\"\n            )\n\n        # Only prepare environment if there are actual settings to apply\n        if not self._must_run_with_uv():\n            logger.debug(\"No environment settings configured, skipping preparation\")\n            return\n\n        # Handle None case for ephemeral use\n        if output_dir is None:\n            import tempfile\n\n            output_dir = Path(tempfile.mkdtemp(prefix=\"fastmcp-env-\"))\n            logger.info(f\"Creating ephemeral environment in {output_dir}\")\n        else:\n            logger.info(f\"Creating persistent environment in {output_dir}\")\n            output_dir = Path(output_dir).resolve()\n\n        # Initialize the project\n        logger.debug(f\"Initializing uv project in {output_dir}\")\n        try:\n            subprocess.run(\n                [\n                    \"uv\",\n                    \"init\",\n                    \"--project\",\n                    str(output_dir),\n                    \"--name\",\n                    \"fastmcp-env\",\n                ],\n                check=True,\n                capture_output=True,\n                text=True,\n            )\n        except subprocess.CalledProcessError as e:\n            # If project already exists, that's fine - continue\n            if \"already initialized\" in e.stderr.lower():\n                logger.debug(\n                    f\"Project already initialized at {output_dir}, continuing...\"\n                )\n            else:\n                logger.error(f\"Failed to initialize project: {e.stderr}\")\n                raise RuntimeError(f\"Failed to initialize project: {e.stderr}\") from e\n\n        # Pin Python version if specified\n        if self.python:\n            logger.debug(f\"Pinning Python version to {self.python}\")\n            try:\n                subprocess.run(\n                    [\n                        \"uv\",\n                        \"python\",\n                        \"pin\",\n                        self.python,\n                        \"--project\",\n                        str(output_dir),\n                    ],\n                    check=True,\n                    capture_output=True,\n                    text=True,\n                )\n            except subprocess.CalledProcessError as e:\n                logger.error(f\"Failed to pin Python version: {e.stderr}\")\n                raise RuntimeError(f\"Failed to pin Python version: {e.stderr}\") from e\n\n        # Add dependencies with --no-sync to defer installation\n        # dependencies ALWAYS include fastmcp; this is compatible with\n        # specific fastmcp versions that might be in the dependencies list\n        dependencies = (self.dependencies or []) + [\"fastmcp\"]\n        logger.debug(f\"Adding dependencies: {', '.join(dependencies)}\")\n        try:\n            subprocess.run(\n                [\n                    \"uv\",\n                    \"add\",\n                    *dependencies,\n                    \"--no-sync\",\n                    \"--project\",\n                    str(output_dir),\n                ],\n                check=True,\n                capture_output=True,\n                text=True,\n            )\n        except subprocess.CalledProcessError as e:\n            logger.error(f\"Failed to add dependencies: {e.stderr}\")\n            raise RuntimeError(f\"Failed to add dependencies: {e.stderr}\") from e\n\n        # Add requirements file if specified\n        if self.requirements:\n            logger.debug(f\"Adding requirements from {self.requirements}\")\n            # Resolve requirements path relative to current directory\n            req_path = Path(self.requirements).resolve()\n            try:\n                subprocess.run(\n                    [\n                        \"uv\",\n                        \"add\",\n                        \"-r\",\n                        str(req_path),\n                        \"--no-sync\",\n                        \"--project\",\n                        str(output_dir),\n                    ],\n                    check=True,\n                    capture_output=True,\n                    text=True,\n                )\n            except subprocess.CalledProcessError as e:\n                logger.error(f\"Failed to add requirements: {e.stderr}\")\n                raise RuntimeError(f\"Failed to add requirements: {e.stderr}\") from e\n\n        # Add editable packages if specified\n        if self.editable:\n            editable_paths = [str(Path(e).resolve()) for e in self.editable]\n            logger.debug(f\"Adding editable packages: {', '.join(editable_paths)}\")\n            try:\n                subprocess.run(\n                    [\n                        \"uv\",\n                        \"add\",\n                        \"--editable\",\n                        *editable_paths,\n                        \"--no-sync\",\n                        \"--project\",\n                        str(output_dir),\n                    ],\n                    check=True,\n                    capture_output=True,\n                    text=True,\n                )\n            except subprocess.CalledProcessError as e:\n                logger.error(f\"Failed to add editable packages: {e.stderr}\")\n                raise RuntimeError(\n                    f\"Failed to add editable packages: {e.stderr}\"\n                ) from e\n\n        # Final sync to install everything\n        logger.info(\"Installing dependencies...\")\n        try:\n            subprocess.run(\n                [\"uv\", \"sync\", \"--project\", str(output_dir)],\n                check=True,\n                capture_output=True,\n                text=True,\n            )\n        except subprocess.CalledProcessError as e:\n            logger.error(f\"Failed to sync dependencies: {e.stderr}\")\n            raise RuntimeError(f\"Failed to sync dependencies: {e.stderr}\") from e\n\n        logger.info(f\"Environment prepared successfully in {output_dir}\")\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py",
    "content": "\"\"\"FastMCP Configuration File Support.\n\nThis module provides support for fastmcp.json configuration files that allow\nusers to specify server settings in a declarative format instead of using\ncommand-line arguments.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast, overload\n\nfrom pydantic import BaseModel, Field, field_validator\n\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\nfrom fastmcp.utilities.mcp_server_config.v1.sources.base import Source\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\nlogger = get_logger(\"cli.config\")\n\n# JSON Schema for IDE support\nFASTMCP_JSON_SCHEMA = \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n\n\n# Type alias for source union (will expand with GitSource, etc. in future)\nSourceType: TypeAlias = FileSystemSource\n\n# Type alias for environment union (will expand with other environments in future)\nEnvironmentType: TypeAlias = UVEnvironment\n\n\nclass Deployment(BaseModel):\n    \"\"\"Configuration for server deployment and runtime settings.\"\"\"\n\n    transport: Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"] | None = Field(\n        default=None,\n        description=\"Transport protocol to use\",\n    )\n\n    host: str | None = Field(\n        default=None,\n        description=\"Host to bind to when using HTTP transport\",\n        examples=[\"127.0.0.1\", \"0.0.0.0\", \"localhost\"],\n    )\n\n    port: int | None = Field(\n        default=None,\n        description=\"Port to bind to when using HTTP transport\",\n        examples=[8000, 3000, 5000],\n    )\n\n    path: str | None = Field(\n        default=None,\n        description=\"URL path for the server endpoint\",\n        examples=[\"/mcp/\", \"/api/mcp/\", \"/sse/\"],\n    )\n\n    log_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] | None = Field(\n        default=None,\n        description=\"Log level for the server\",\n    )\n\n    cwd: str | None = Field(\n        default=None,\n        description=\"Working directory for the server process\",\n        examples=[\".\", \"./src\", \"/app\"],\n    )\n\n    env: dict[str, str] | None = Field(\n        default=None,\n        description=\"Environment variables to set when running the server\",\n        examples=[{\"API_KEY\": \"secret\", \"DEBUG\": \"true\"}],\n    )\n\n    args: list[str] | None = Field(\n        default=None,\n        description=\"Arguments to pass to the server (after --)\",\n        examples=[[\"--config\", \"config.json\", \"--debug\"]],\n    )\n\n    def apply_runtime_settings(self, config_path: Path | None = None) -> None:\n        \"\"\"Apply runtime settings like environment variables and working directory.\n\n        Args:\n            config_path: Path to config file for resolving relative paths\n\n        Environment variables support interpolation with ${VAR_NAME} syntax.\n        For example: \"API_URL\": \"https://api.${ENVIRONMENT}.example.com\"\n        will substitute the value of the ENVIRONMENT variable at runtime.\n        \"\"\"\n        import os\n        from pathlib import Path\n\n        # Set environment variables with interpolation support\n        if self.env:\n            for key, value in self.env.items():\n                # Interpolate environment variables in the value\n                interpolated_value = self._interpolate_env_vars(value)\n                os.environ[key] = interpolated_value\n\n        # Change working directory\n        if self.cwd:\n            cwd_path = Path(self.cwd)\n            if not cwd_path.is_absolute() and config_path:\n                cwd_path = (config_path.parent / cwd_path).resolve()\n            os.chdir(cwd_path)\n\n    def _interpolate_env_vars(self, value: str) -> str:\n        \"\"\"Interpolate environment variables in a string.\n\n        Replaces ${VAR_NAME} with the value of VAR_NAME from the environment.\n        If the variable is not set, the placeholder is left unchanged.\n\n        Args:\n            value: String potentially containing ${VAR_NAME} placeholders\n\n        Returns:\n            String with environment variables interpolated\n        \"\"\"\n\n        def replace_var(match: re.Match) -> str:\n            var_name = match.group(1)\n            # Return the environment variable value if it exists, otherwise keep the placeholder\n            return os.environ.get(var_name, match.group(0))\n\n        # Match ${VAR_NAME} pattern and replace with environment variable values\n        return re.sub(r\"\\$\\{([^}]+)\\}\", replace_var, value)\n\n\nclass MCPServerConfig(BaseModel):\n    \"\"\"Configuration for a FastMCP server.\n\n    This configuration file allows you to specify all settings needed to run\n    a FastMCP server in a declarative format.\n    \"\"\"\n\n    # Schema field for IDE support\n    schema_: str | None = Field(\n        default=\"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n        alias=\"$schema\",\n        description=\"JSON schema for IDE support and validation\",\n    )\n\n    # Server source - defines where and how to load the server\n    source: SourceType = Field(\n        description=\"Source configuration for the server\",\n        examples=[\n            {\"path\": \"server.py\"},\n            {\"path\": \"server.py\", \"entrypoint\": \"app\"},\n            {\"type\": \"filesystem\", \"path\": \"src/server.py\", \"entrypoint\": \"mcp\"},\n        ],\n    )\n\n    # Environment configuration\n    environment: EnvironmentType = Field(\n        default_factory=lambda: UVEnvironment(),\n        description=\"Python environment setup configuration\",\n    )\n\n    # Deployment configuration\n    deployment: Deployment = Field(\n        default_factory=lambda: Deployment(),\n        description=\"Server deployment and runtime settings\",\n    )\n\n    # purely for static type checkers to avoid issues with providing dict source\n    if TYPE_CHECKING:\n\n        @overload\n        def __init__(self, *, source: dict | FileSystemSource, **data) -> None: ...\n        @overload\n        def __init__(self, *, environment: dict | UVEnvironment, **data) -> None: ...\n        @overload\n        def __init__(self, *, deployment: dict | Deployment, **data) -> None: ...\n        def __init__(self, **data) -> None: ...\n\n    @field_validator(\"source\", mode=\"before\")\n    @classmethod\n    def validate_source(cls, v: dict | Source) -> SourceType:\n        \"\"\"Validate and convert source to proper format.\n\n        Supports:\n        - Dict format: `{\"path\": \"server.py\", \"entrypoint\": \"app\"}`\n        - FileSystemSource instance (passed through)\n\n        No string parsing happens here - that's only at CLI boundaries.\n        MCPServerConfig works only with properly typed objects.\n        \"\"\"\n        if isinstance(v, dict):\n            return FileSystemSource(**v)\n        return v  # type: ignore[return-value]\n\n    @field_validator(\"environment\", mode=\"before\")\n    @classmethod\n    def validate_environment(cls, v: dict | Any) -> EnvironmentType:\n        \"\"\"Ensure environment has a type field for discrimination.\n\n        For backward compatibility, if no type is specified, default to \"uv\".\n        \"\"\"\n        if isinstance(v, dict):\n            return UVEnvironment(**v)\n        return v\n\n    @field_validator(\"deployment\", mode=\"before\")\n    @classmethod\n    def validate_deployment(cls, v: dict | Deployment) -> Deployment:\n        \"\"\"Validate and convert deployment to Deployment.\n\n        Accepts:\n        - Deployment instance\n        - dict that can be converted to Deployment\n\n        \"\"\"\n        if isinstance(v, dict):\n            return Deployment(**v)\n        return cast(Deployment, v)  # type: ignore[return-value]\n\n    @classmethod\n    def from_file(cls, file_path: Path) -> MCPServerConfig:\n        \"\"\"Load configuration from a JSON file.\n\n        Args:\n            file_path: Path to the configuration file\n\n        Returns:\n            MCPServerConfig instance\n\n        Raises:\n            FileNotFoundError: If the file doesn't exist\n            json.JSONDecodeError: If the file is not valid JSON\n            pydantic.ValidationError: If the configuration is invalid\n        \"\"\"\n        if not file_path.exists():\n            raise FileNotFoundError(f\"Configuration file not found: {file_path}\")\n\n        with file_path.open(\"r\", encoding=\"utf-8\") as f:\n            data = json.load(f)\n\n        return cls.model_validate(data)\n\n    @classmethod\n    def from_cli_args(\n        cls,\n        source: FileSystemSource,\n        transport: Literal[\"stdio\", \"http\", \"sse\", \"streamable-http\"] | None = None,\n        host: str | None = None,\n        port: int | None = None,\n        path: str | None = None,\n        log_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n        | None = None,\n        python: str | None = None,\n        dependencies: list[str] | None = None,\n        requirements: str | None = None,\n        project: str | None = None,\n        editable: str | None = None,\n        env: dict[str, str] | None = None,\n        cwd: str | None = None,\n        args: list[str] | None = None,\n    ) -> MCPServerConfig:\n        \"\"\"Create a config from CLI arguments.\n\n        This allows us to have a single code path where everything\n        goes through a config object.\n\n        Args:\n            source: Server source (FileSystemSource instance)\n            transport: Transport protocol\n            host: Host for HTTP transport\n            port: Port for HTTP transport\n            path: URL path for server\n            log_level: Logging level\n            python: Python version\n            dependencies: Python packages to install\n            requirements: Path to requirements file\n            project: Path to project directory\n            editable: Path to install in editable mode\n            env: Environment variables\n            cwd: Working directory\n            args: Server arguments\n\n        Returns:\n            MCPServerConfig instance\n        \"\"\"\n        # Build environment config if any env args provided\n        environment = None\n        if any([python, dependencies, requirements, project, editable]):\n            environment = UVEnvironment(\n                python=python,\n                dependencies=dependencies,\n                requirements=Path(requirements) if requirements else None,\n                project=Path(project) if project else None,\n                editable=[Path(editable)] if editable else None,\n            )\n\n        # Build deployment config if any deployment args provided\n        deployment = None\n        if any([transport, host, port, path, log_level, env, cwd, args]):\n            # Convert streamable-http to http for backward compatibility\n            if transport == \"streamable-http\":\n                transport = \"http\"\n            deployment = Deployment(\n                transport=transport,\n                host=host,\n                port=port,\n                path=path,\n                log_level=log_level,\n                env=env,\n                cwd=cwd,\n                args=args,\n            )\n\n        return cls(\n            source=source,\n            environment=environment,\n            deployment=deployment,\n        )\n\n    @classmethod\n    def find_config(cls, start_path: Path | None = None) -> Path | None:\n        \"\"\"Find a fastmcp.json file in the specified directory.\n\n        Args:\n            start_path: Directory to look in (defaults to current directory)\n\n        Returns:\n            Path to the configuration file, or None if not found\n        \"\"\"\n        if start_path is None:\n            start_path = Path.cwd()\n\n        config_path = start_path / \"fastmcp.json\"\n        if config_path.exists():\n            logger.debug(f\"Found configuration file: {config_path}\")\n            return config_path\n\n        return None\n\n    async def prepare(\n        self,\n        skip_source: bool = False,\n        output_dir: Path | None = None,\n    ) -> None:\n        \"\"\"Prepare environment and source for execution.\n\n        When output_dir is provided, creates a persistent uv project.\n        When output_dir is None, does ephemeral caching (for backwards compatibility).\n\n        Args:\n            skip_source: Skip source preparation if True\n            output_dir: Directory to create the persistent uv project in (optional)\n        \"\"\"\n        # Prepare environment (persistent if output_dir provided, ephemeral otherwise)\n        if self.environment:\n            await self.prepare_environment(output_dir=output_dir)\n\n        if not skip_source:\n            await self.prepare_source()\n\n    async def prepare_environment(self, output_dir: Path | None = None) -> None:\n        \"\"\"Prepare the Python environment.\n\n        Args:\n            output_dir: If provided, creates a persistent uv project in this directory.\n                       If None, just populates uv's cache for ephemeral use.\n\n        Delegates to the environment's prepare() method\n        \"\"\"\n        await self.environment.prepare(output_dir=output_dir)\n\n    async def prepare_source(self) -> None:\n        \"\"\"Prepare the source for loading.\n\n        Delegates to the source's prepare() method.\n        \"\"\"\n        await self.source.prepare()\n\n    async def run_server(self, **kwargs: Any) -> None:\n        \"\"\"Load and run the server with this configuration.\n\n        Args:\n            **kwargs: Additional arguments to pass to server.run_async()\n                     These override config settings\n        \"\"\"\n        # Apply deployment settings (env vars, cwd)\n        if self.deployment:\n            self.deployment.apply_runtime_settings()\n\n        # Load the server\n        server = await self.source.load_server()\n\n        # Build run arguments from config\n        run_args = {}\n        if self.deployment:\n            if self.deployment.transport:\n                run_args[\"transport\"] = self.deployment.transport\n            if self.deployment.host:\n                run_args[\"host\"] = self.deployment.host\n            if self.deployment.port:\n                run_args[\"port\"] = self.deployment.port\n            if self.deployment.path:\n                run_args[\"path\"] = self.deployment.path\n            if self.deployment.log_level:\n                run_args[\"log_level\"] = self.deployment.log_level\n\n        # Override with any provided kwargs\n        run_args.update(kwargs)\n\n        # Run the server\n        await server.run_async(**run_args)\n\n\ndef generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None:\n    \"\"\"Generate JSON schema for fastmcp.json files.\n\n    This is used to create the schema file that IDEs can use for\n    validation and auto-completion.\n\n    Args:\n        output_path: Optional path to write the schema to. If provided,\n                    writes the schema and returns None. If not provided,\n                    returns the schema as a dictionary.\n\n    Returns:\n        JSON schema as a dictionary if output_path is None, otherwise None\n    \"\"\"\n    schema = MCPServerConfig.model_json_schema()\n\n    # Add some metadata\n    schema[\"$id\"] = FASTMCP_JSON_SCHEMA\n    schema[\"title\"] = \"FastMCP Configuration\"\n    schema[\"description\"] = \"Configuration file for FastMCP servers\"\n\n    if output_path:\n        import json\n\n        output = Path(output_path)\n        output.parent.mkdir(parents=True, exist_ok=True)\n        with open(output, \"w\") as f:\n            json.dump(schema, f, indent=2)\n            f.write(\"\\n\")  # Add trailing newline\n        return None\n\n    return schema\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/schema.json",
    "content": "{\n  \"$defs\": {\n    \"Deployment\": {\n      \"description\": \"Configuration for server deployment and runtime settings.\",\n      \"properties\": {\n        \"transport\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"stdio\",\n                \"http\",\n                \"sse\",\n                \"streamable-http\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Transport protocol to use\",\n          \"title\": \"Transport\"\n        },\n        \"host\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Host to bind to when using HTTP transport\",\n          \"examples\": [\n            \"127.0.0.1\",\n            \"0.0.0.0\",\n            \"localhost\"\n          ],\n          \"title\": \"Host\"\n        },\n        \"port\": {\n          \"anyOf\": [\n            {\n              \"type\": \"integer\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Port to bind to when using HTTP transport\",\n          \"examples\": [\n            8000,\n            3000,\n            5000\n          ],\n          \"title\": \"Port\"\n        },\n        \"path\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"URL path for the server endpoint\",\n          \"examples\": [\n            \"/mcp/\",\n            \"/api/mcp/\",\n            \"/sse/\"\n          ],\n          \"title\": \"Path\"\n        },\n        \"log_level\": {\n          \"anyOf\": [\n            {\n              \"enum\": [\n                \"DEBUG\",\n                \"INFO\",\n                \"WARNING\",\n                \"ERROR\",\n                \"CRITICAL\"\n              ],\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Log level for the server\",\n          \"title\": \"Log Level\"\n        },\n        \"cwd\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Working directory for the server process\",\n          \"examples\": [\n            \".\",\n            \"./src\",\n            \"/app\"\n          ],\n          \"title\": \"Cwd\"\n        },\n        \"env\": {\n          \"anyOf\": [\n            {\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"object\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Environment variables to set when running the server\",\n          \"examples\": [\n            {\n              \"API_KEY\": \"secret\",\n              \"DEBUG\": \"true\"\n            }\n          ],\n          \"title\": \"Env\"\n        },\n        \"args\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Arguments to pass to the server (after --)\",\n          \"examples\": [\n            [\n              \"--config\",\n              \"config.json\",\n              \"--debug\"\n            ]\n          ],\n          \"title\": \"Args\"\n        }\n      },\n      \"title\": \"Deployment\",\n      \"type\": \"object\"\n    },\n    \"FileSystemSource\": {\n      \"description\": \"Source for local Python files.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"filesystem\",\n          \"default\": \"filesystem\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"path\": {\n          \"description\": \"Path to Python file containing the server\",\n          \"title\": \"Path\",\n          \"type\": \"string\"\n        },\n        \"entrypoint\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Name of server instance or factory function (a no-arg function that returns a FastMCP server)\",\n          \"title\": \"Entrypoint\"\n        }\n      },\n      \"required\": [\n        \"path\"\n      ],\n      \"title\": \"FileSystemSource\",\n      \"type\": \"object\"\n    },\n    \"UVEnvironment\": {\n      \"description\": \"Configuration for Python environment setup.\",\n      \"properties\": {\n        \"type\": {\n          \"const\": \"uv\",\n          \"default\": \"uv\",\n          \"title\": \"Type\",\n          \"type\": \"string\"\n        },\n        \"python\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python version constraint\",\n          \"examples\": [\n            \"3.10\",\n            \"3.11\",\n            \"3.12\"\n          ],\n          \"title\": \"Python\"\n        },\n        \"dependencies\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Python packages to install with PEP 508 specifiers\",\n          \"examples\": [\n            [\n              \"fastmcp>=2.0,<3\",\n              \"httpx\",\n              \"pandas>=2.0\"\n            ]\n          ],\n          \"title\": \"Dependencies\"\n        },\n        \"requirements\": {\n          \"anyOf\": [\n            {\n              \"format\": \"path\",\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to requirements.txt file\",\n          \"examples\": [\n            \"requirements.txt\",\n            \"../requirements/prod.txt\"\n          ],\n          \"title\": \"Requirements\"\n        },\n        \"project\": {\n          \"anyOf\": [\n            {\n              \"format\": \"path\",\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Path to project directory containing pyproject.toml\",\n          \"examples\": [\n            \".\",\n            \"../my-project\"\n          ],\n          \"title\": \"Project\"\n        },\n        \"editable\": {\n          \"anyOf\": [\n            {\n              \"items\": {\n                \"format\": \"path\",\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ],\n          \"default\": null,\n          \"description\": \"Directories to install in editable mode\",\n          \"examples\": [\n            [\n              \".\",\n              \"../my-package\"\n            ],\n            [\n              \"/path/to/package\"\n            ]\n          ],\n          \"title\": \"Editable\"\n        }\n      },\n      \"title\": \"UVEnvironment\",\n      \"type\": \"object\"\n    }\n  },\n  \"description\": \"Configuration file for FastMCP servers\",\n  \"properties\": {\n    \"$schema\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ],\n      \"default\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n      \"description\": \"JSON schema for IDE support and validation\",\n      \"title\": \"$Schema\"\n    },\n    \"source\": {\n      \"$ref\": \"#/$defs/FileSystemSource\",\n      \"description\": \"Source configuration for the server\",\n      \"examples\": [\n        {\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"app\",\n          \"path\": \"server.py\"\n        },\n        {\n          \"entrypoint\": \"mcp\",\n          \"path\": \"src/server.py\",\n          \"type\": \"filesystem\"\n        }\n      ]\n    },\n    \"environment\": {\n      \"$ref\": \"#/$defs/UVEnvironment\",\n      \"description\": \"Python environment setup configuration\"\n    },\n    \"deployment\": {\n      \"$ref\": \"#/$defs/Deployment\",\n      \"description\": \"Server deployment and runtime settings\"\n    }\n  },\n  \"required\": [\n    \"source\"\n  ],\n  \"title\": \"FastMCP Configuration\",\n  \"type\": \"object\",\n  \"$id\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n}\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/sources/__init__.py",
    "content": ""
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/sources/base.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\nclass Source(BaseModel, ABC):\n    \"\"\"Abstract base class for all source types.\"\"\"\n\n    type: str = Field(description=\"Source type identifier\")\n\n    async def prepare(self) -> None:\n        \"\"\"Prepare the source (download, clone, install, etc).\n\n        For sources that need preparation (e.g., git clone, download),\n        this method performs that preparation. For sources that don't\n        need preparation (e.g., local files), this is a no-op.\n        \"\"\"\n        # Default implementation for sources that don't need preparation\n\n    @abstractmethod\n    async def load_server(self) -> Any:\n        \"\"\"Load and return the FastMCP server instance.\n\n        Must be called after prepare() if the source requires preparation.\n        All information needed to load the server should be available\n        as attributes on the source instance.\n        \"\"\"\n        ...\n"
  },
  {
    "path": "src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py",
    "content": "import importlib.util\nimport inspect\nimport sys\nfrom pathlib import Path\nfrom typing import Any, Literal\n\nfrom pydantic import Field, field_validator\n\nfrom fastmcp.utilities.async_utils import is_coroutine_function\nfrom fastmcp.utilities.logging import get_logger\nfrom fastmcp.utilities.mcp_server_config.v1.sources.base import Source\n\nlogger = get_logger(__name__)\n\n\nclass FileSystemSource(Source):\n    \"\"\"Source for local Python files.\"\"\"\n\n    type: Literal[\"filesystem\"] = \"filesystem\"\n\n    path: str = Field(description=\"Path to Python file containing the server\")\n    entrypoint: str | None = Field(\n        default=None,\n        description=\"Name of server instance or factory function (a no-arg function that returns a FastMCP server)\",\n    )\n\n    @field_validator(\"path\", mode=\"before\")\n    @classmethod\n    def parse_path_with_object(cls, v: str) -> str:\n        \"\"\"Parse path:object syntax and extract the object name.\n\n        This validator runs before the model is created, allowing us to\n        handle the \"file.py:object\" syntax at the model boundary.\n        \"\"\"\n        if isinstance(v, str) and \":\" in v:\n            # Check if it's a Windows path (e.g., C:\\...)\n            has_windows_drive = len(v) > 1 and v[1] == \":\"\n\n            # Only split if colon is not part of Windows drive\n            if \":\" in (v[2:] if has_windows_drive else v):\n                # This path has an object specification\n                # We'll handle it in __init__ by setting entrypoint\n                return v\n        return v\n\n    def __init__(self, **data: Any) -> None:\n        \"\"\"Initialize FileSystemSource, handling path:object syntax.\"\"\"\n        # Check if path contains an object specification\n        if \"path\" in data and isinstance(data[\"path\"], str) and \":\" in data[\"path\"]:\n            path_str = data[\"path\"]\n            # Check if it's a Windows path (e.g., C:\\...)\n            has_windows_drive = len(path_str) > 1 and path_str[1] == \":\"\n\n            # Only split if colon is not part of Windows drive\n            if \":\" in (path_str[2:] if has_windows_drive else path_str):\n                file_str, obj = path_str.rsplit(\":\", 1)\n                data[\"path\"] = file_str\n                # Only set entrypoint if not already provided\n                if \"entrypoint\" not in data or data[\"entrypoint\"] is None:\n                    data[\"entrypoint\"] = obj\n\n        super().__init__(**data)\n\n    async def load_server(self) -> Any:\n        \"\"\"Load server from filesystem.\"\"\"\n        # Resolve the file path\n        file_path = Path(self.path).expanduser().resolve()\n        if not file_path.exists():\n            logger.error(f\"File not found: {file_path}\")\n            sys.exit(1)\n        if not file_path.is_file():\n            logger.error(f\"Not a file: {file_path}\")\n            sys.exit(1)\n\n        # Import the module\n        module = self._import_module(file_path)\n\n        # Find the server object\n        server = await self._find_server_object(module, file_path)\n\n        return server\n\n    def _import_module(self, file_path: Path) -> Any:\n        \"\"\"Import a Python module from a file path.\n\n        Args:\n            file_path: Path to the Python file\n\n        Returns:\n            The imported module\n        \"\"\"\n        # Add parent directory to Python path so imports can be resolved\n        file_dir = str(file_path.parent)\n        if file_dir not in sys.path:\n            sys.path.insert(0, file_dir)\n\n        # Import the module\n        spec = importlib.util.spec_from_file_location(\"server_module\", file_path)\n        if not spec or not spec.loader:\n            logger.error(\"Could not load module\", extra={\"file\": str(file_path)})\n            sys.exit(1)\n\n        module = importlib.util.module_from_spec(spec)\n        sys.modules[\"server_module\"] = module  # Register in sys.modules\n        spec.loader.exec_module(module)\n\n        return module\n\n    async def _find_server_object(self, module: Any, file_path: Path) -> Any:\n        \"\"\"Find the server object in the module.\n\n        Args:\n            module: The imported Python module\n            file_path: Path to the file (for error messages)\n\n        Returns:\n            The server object (or result of calling a factory function)\n        \"\"\"\n        # Avoid circular import by importing here\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.server.server import FastMCP\n\n        # If entrypoint is specified, use it\n        if self.entrypoint:\n            # Handle module:object syntax (though this is legacy)\n            if \":\" in self.entrypoint:\n                module_name, object_name = self.entrypoint.split(\":\", 1)\n                try:\n                    import importlib\n\n                    server_module = importlib.import_module(module_name)\n                    obj = getattr(server_module, object_name, None)\n                except ImportError:\n                    logger.error(\n                        f\"Could not import module '{module_name}'\",\n                        extra={\"file\": str(file_path)},\n                    )\n                    sys.exit(1)\n            else:\n                # Just object name\n                obj = getattr(module, self.entrypoint, None)\n\n            if obj is None:\n                logger.error(\n                    f\"Server object '{self.entrypoint}' not found\",\n                    extra={\"file\": str(file_path)},\n                )\n                sys.exit(1)\n\n            return await self._resolve_factory(obj, file_path, self.entrypoint)\n\n        # No entrypoint specified, try common server names\n        for name in [\"mcp\", \"server\", \"app\"]:\n            if hasattr(module, name):\n                obj = getattr(module, name)\n                if isinstance(obj, FastMCP | FastMCP1x):\n                    return await self._resolve_factory(obj, file_path, name)\n\n        # No server found\n        logger.error(\n            f\"No server object found in {file_path}. Please either:\\n\"\n            \"1. Use a standard variable name (mcp, server, or app)\\n\"\n            \"2. Specify the entrypoint name in fastmcp.json or use `file.py:object` syntax as your path.\",\n            extra={\"file\": str(file_path)},\n        )\n        sys.exit(1)\n\n    async def _resolve_factory(self, obj: Any, file_path: Path, name: str) -> Any:\n        \"\"\"Resolve a server object or factory function to a server instance.\n\n        Args:\n            obj: The object that might be a server or factory function\n            file_path: Path to the file for error messages\n            name: Name of the object for error messages\n\n        Returns:\n            A server instance\n        \"\"\"\n        # Avoid circular import by importing here\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.server.server import FastMCP\n\n        # Check if it's a function or coroutine function\n        if inspect.isfunction(obj) or is_coroutine_function(obj):\n            logger.debug(f\"Found factory function '{name}' in {file_path}\")\n\n            try:\n                if is_coroutine_function(obj):\n                    # Async factory function\n                    server = await obj()\n                else:\n                    # Sync factory function\n                    server = obj()\n\n                # Validate the result is a FastMCP server\n                if not isinstance(server, FastMCP | FastMCP1x):\n                    logger.error(\n                        f\"Factory function '{name}' must return a FastMCP server instance, \"\n                        f\"got {type(server).__name__}\",\n                        extra={\"file\": str(file_path)},\n                    )\n                    sys.exit(1)\n\n                logger.debug(f\"Factory function '{name}' created server: {server.name}\")\n                return server\n\n            except Exception as e:\n                logger.error(\n                    f\"Failed to call factory function '{name}': {e}\",\n                    extra={\"file\": str(file_path)},\n                )\n                sys.exit(1)\n\n        # Not a function, return as-is (should be a server instance)\n        return obj\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/README.md",
    "content": "# OpenAPI Utilities\n\nThis directory contains the OpenAPI integration utilities for FastMCP.\n\n## Architecture Overview\n\nThe implementation follows a **stateless request building strategy** using `openapi-core` for high-performance, per-request HTTP request construction, eliminating startup latency while maintaining robust OpenAPI compliance.\n\n### Core Components\n\n1. **`director.py`** - `RequestDirector` for stateless HTTP request building\n2. **`parser.py`** - OpenAPI spec parsing and route extraction with pre-calculated schemas\n3. **`schemas.py`** - Schema processing with parameter mapping for collision handling\n4. **`models.py`** - Enhanced data models with pre-calculated fields for performance\n5. **`formatters.py`** - Response formatting and processing utilities\n\n### Key Architecture Principles\n\n#### 1. Stateless Request Building\n- Uses `openapi-core` library for robust OpenAPI parameter serialization\n- Builds HTTP requests on-demand with zero startup latency\n- Offloads OpenAPI compliance to a well-tested library without code generation overhead\n\n#### 2. Pre-calculated Optimization\n- **Schema Pre-calculation**: Combined schemas calculated once during parsing\n- **Parameter Mapping**: Collision resolution mapping calculated upfront\n- **Zero Runtime Overhead**: All complex processing done during initialization\n\n#### 3. Performance-First Design\n- **No Code Generation**: Eliminates 100-200ms startup latency\n- **Serverless Friendly**: Ideal for cold-start environments\n- **Minimal Dependencies**: Uses lightweight `openapi-core` instead of full client generation\n\n## Data Flow\n\n### Initialization Process\n\n```\nOpenAPI Spec → Parser → HTTPRoute with Pre-calculated Fields → RequestDirector + SchemaPath\n```\n\n1. **Input**: Raw OpenAPI specification (dict)\n2. **Parsing**: Extract operations to `HTTPRoute` models\n3. **Pre-calculation**: Generate combined schemas and parameter maps during parsing\n4. **Director Setup**: Create `RequestDirector` with `SchemaPath` for request building\n\n### Request Processing\n\n```\nMCP Tool Call → RequestDirector.build() → httpx.Request → HTTP Response → Structured Output\n```\n\n1. **Tool Invocation**: FastMCP receives tool call with parameters\n2. **Request Building**: RequestDirector builds HTTP request using parameter map\n3. **Parameter Handling**: openapi-core handles all OpenAPI serialization rules\n4. **Response Processing**: Parse response into structured format with proper error handling\n\n## Key Features\n\n### 1. High-Performance Request Building\n- Zero startup latency - no code generation required\n- Stateless request building scales infinitely\n- Uses proven `openapi-core` library for OpenAPI compliance\n- Perfect for serverless and cold-start environments\n\n### 2. Comprehensive Parameter Support\n- **Parameter Collisions**: Intelligent collision resolution with suffixing\n- **DeepObject Style**: Full support for deepObject parameters with explode=true/false\n- **Complex Schemas**: Handles nested objects, arrays, and all OpenAPI types\n- **Pre-calculated Mapping**: Parameter location mapping done upfront for performance\n\n### 3. Enhanced Error Handling\n- HTTP status code mapping to MCP errors\n- Structured error responses with detailed information\n- Graceful handling of network timeouts and connection errors\n- Proper error context preservation\n\n### 4. Advanced Schema Processing\n- **Pre-calculated Schemas**: Combined parameter and body schemas calculated once\n- **Collision-aware**: Automatically handles parameter name collisions\n- **Type Safety**: Full Pydantic model validation\n- **Performance**: Zero runtime schema processing overhead\n\n## Component Integration\n\n### Server Components (`/server/openapi/`)\n\n1. **`OpenAPITool`** - Simplified tool implementation using RequestDirector\n2. **`OpenAPIResource`** - Resource implementation with RequestDirector\n3. **`OpenAPIResourceTemplate`** - Resource template with RequestDirector support\n4. **`FastMCPOpenAPI`** - Main server class with stateless request building\n\n### RequestDirector Integration\n\nAll components use the same RequestDirector approach:\n- Consistent parameter handling across all component types\n- Uniform error handling and response processing\n- Simplified architecture without fallback complexity\n- High performance for all operation types\n\n## Usage Examples\n\n### Basic Server Setup\n\n```python\nimport httpx\nfrom fastmcp.server.openapi import FastMCPOpenAPI\n\n# OpenAPI spec (can be loaded from file/URL)\nopenapi_spec = {...}\n\n# Create HTTP client\nasync with httpx.AsyncClient() as client:\n    # Create server with stateless request building\n    server = FastMCPOpenAPI(\n        openapi_spec=openapi_spec,\n        client=client,\n        name=\"My API Server\"\n    )\n    \n    # Server automatically creates RequestDirector and pre-calculates schemas\n```\n\n### Direct RequestDirector Usage\n\n```python\nfrom fastmcp.utilities.openapi.director import RequestDirector\nfrom jsonschema_path import SchemaPath\n\n# Create RequestDirector manually\nspec = SchemaPath.from_dict(openapi_spec)\ndirector = RequestDirector(spec)\n\n# Build HTTP request\nrequest = director.build(route, flat_arguments, base_url)\n\n# Execute with httpx\nasync with httpx.AsyncClient() as client:\n    response = await client.send(request)\n```\n\n## Testing Strategy\n\nTests are located in `/tests/server/openapi/`:\n\n### Test Categories\n\n1. **Core Functionality**\n   - `test_server.py` - Server initialization and RequestDirector integration\n\n2. **OpenAPI Features**  \n   - `test_parameter_collisions.py` - Parameter name collision handling\n   - `test_deepobject_style.py` - DeepObject parameter style support\n   - `test_openapi_features.py` - General OpenAPI feature compliance\n\n### Testing Philosophy\n\n- **Real Objects**: Use real HTTPRoute models and OpenAPI specifications\n- **Minimal Mocking**: Only mock external HTTP endpoints\n- **Performance Focus**: Test that initialization is fast and stateless\n- **Behavioral Testing**: Verify OpenAPI compliance without implementation details\n\n## Future Enhancements\n\n### Planned Features\n\n1. **Response Streaming**: Handle streaming API responses\n2. **Enhanced Authentication**: More auth provider integrations\n3. **Advanced Metrics**: Detailed request/response monitoring\n4. **Schema Validation**: Enhanced input/output validation\n5. **Batch Operations**: Optimized multi-operation requests\n\n### Performance Improvements\n\n1. **Schema Caching**: More aggressive schema pre-calculation\n2. **Memory Optimization**: Further reduce memory footprint\n3. **Request Batching**: Smart batching for bulk operations\n4. **Connection Optimization**: Enhanced connection pooling strategies\n\n## Troubleshooting\n\n### Common Issues\n\n1. **RequestDirector Initialization Fails**\n   - Check OpenAPI spec validity with `jsonschema-path`\n   - Verify spec format is correct JSON/YAML\n   - Ensure all required OpenAPI fields are present\n\n2. **Parameter Mapping Issues**\n   - Check parameter collision resolution in debug logs\n   - Verify parameter names match OpenAPI spec exactly\n   - Review pre-calculated parameter map in HTTPRoute\n\n3. **Request Building Errors**\n   - Check network connectivity to target API\n   - Verify base URL configuration\n   - Review parameter validation and type mismatches\n\n### Debugging\n\n- Enable debug logging: `logger.setLevel(logging.DEBUG)`\n- Check RequestDirector initialization logs\n- Review parameter mapping in HTTPRoute models\n- Monitor request building and API response patterns\n\n## Dependencies\n\n- `openapi-core` - OpenAPI specification processing and validation\n- `httpx` - HTTP client library\n- `pydantic` - Data validation and serialization\n- `urllib.parse` - URL building and manipulation"
  },
  {
    "path": "src/fastmcp/utilities/openapi/__init__.py",
    "content": "\"\"\"OpenAPI utilities for FastMCP - refactored for better maintainability.\"\"\"\n\n# Import from models\nfrom .models import (\n    HTTPRoute,\n    HttpMethod,\n    JsonSchema,\n    ParameterInfo,\n    ParameterLocation,\n    RequestBodyInfo,\n    ResponseInfo,\n)\n\n# Import from parser\nfrom .parser import parse_openapi_to_http_routes\n\n# Import from formatters\nfrom .formatters import (\n    format_array_parameter,\n    format_deep_object_parameter,\n    format_description_with_responses,\n    format_json_for_description,\n    generate_example_from_schema,\n)\n\n# Import from schemas\nfrom .schemas import (\n    _combine_schemas,\n    extract_output_schema_from_responses,\n    clean_schema_for_display,\n    _make_optional_parameter_nullable,\n)\n\n# Import from json_schema_converter\nfrom .json_schema_converter import (\n    convert_openapi_schema_to_json_schema,\n    convert_schema_definitions,\n)\n\n# Export public symbols - maintaining backward compatibility\n__all__ = [\n    \"HTTPRoute\",\n    \"HttpMethod\",\n    \"JsonSchema\",\n    \"ParameterInfo\",\n    \"ParameterLocation\",\n    \"RequestBodyInfo\",\n    \"ResponseInfo\",\n    \"_combine_schemas\",\n    \"_make_optional_parameter_nullable\",\n    \"clean_schema_for_display\",\n    \"convert_openapi_schema_to_json_schema\",\n    \"convert_schema_definitions\",\n    \"extract_output_schema_from_responses\",\n    \"format_array_parameter\",\n    \"format_deep_object_parameter\",\n    \"format_description_with_responses\",\n    \"format_json_for_description\",\n    \"generate_example_from_schema\",\n    \"parse_openapi_to_http_routes\",\n]\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/director.py",
    "content": "\"\"\"Request director using openapi-core for stateless HTTP request building.\"\"\"\n\nfrom typing import Any\nfrom urllib.parse import quote, urljoin\n\nimport httpx\nfrom jsonschema_path import SchemaPath\n\nfrom fastmcp.utilities.logging import get_logger\n\nfrom .models import HTTPRoute\n\nlogger = get_logger(__name__)\n\n\nclass RequestDirector:\n    \"\"\"Builds httpx.Request objects from HTTPRoute and arguments using openapi-core.\"\"\"\n\n    def __init__(self, spec: SchemaPath):\n        \"\"\"Initialize with a parsed SchemaPath object.\"\"\"\n        self._spec = spec\n\n    def build(\n        self,\n        route: HTTPRoute,\n        flat_args: dict[str, Any],\n        base_url: str = \"http://localhost\",\n    ) -> httpx.Request:\n        \"\"\"\n        Constructs a final httpx.Request object, handling all OpenAPI serialization.\n\n        Args:\n            route: HTTPRoute containing OpenAPI operation details\n            flat_args: Flattened arguments from LLM (may include suffixed parameters)\n            base_url: Base URL for the request\n\n        Returns:\n            httpx.Request: Properly formatted HTTP request\n        \"\"\"\n        logger.debug(\n            f\"Building request for {route.method} {route.path} with args: {flat_args}\"\n        )\n\n        # Step 1: Un-flatten arguments into path, query, body, etc. using parameter map\n        path_params, query_params, header_params, body = self._unflatten_arguments(\n            route, flat_args\n        )\n\n        logger.debug(\n            f\"Unflattened - path: {path_params}, query: {query_params}, headers: {header_params}, body: {body}\"\n        )\n\n        # Step 2: Build base URL with path parameters\n        url = self._build_url(route.path, path_params, base_url)\n\n        # Step 3: Prepare request data\n        method: str = route.method.upper()\n        params = query_params if query_params else None\n        headers = header_params if header_params else None\n        json_body: dict[str, Any] | list[Any] | None = None\n        content: str | bytes | None = None\n\n        # Step 4: Handle request body\n        if body is not None:\n            if isinstance(body, dict | list):\n                json_body = body\n            else:\n                content = body\n\n        # Step 5: Create httpx.Request\n        return httpx.Request(\n            method=method,\n            url=url,\n            params=params,\n            headers=headers,\n            json=json_body,\n            content=content,\n        )\n\n    def _unflatten_arguments(\n        self, route: HTTPRoute, flat_args: dict[str, Any]\n    ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], Any]:\n        \"\"\"\n        Maps flat arguments back to their OpenAPI locations using the parameter map.\n\n        Args:\n            route: HTTPRoute with parameter_map containing location mappings\n            flat_args: Flat arguments from LLM call\n\n        Returns:\n            Tuple of (path_params, query_params, header_params, body)\n        \"\"\"\n        path_params = {}\n        query_params = {}\n        header_params = {}\n        body_props = {}\n\n        # Use parameter map to route arguments to correct locations\n        if hasattr(route, \"parameter_map\") and route.parameter_map:\n            for arg_name, value in flat_args.items():\n                if value is None:\n                    continue  # Skip None values for optional parameters\n\n                if arg_name not in route.parameter_map:\n                    logger.warning(\n                        f\"Argument '{arg_name}' not found in parameter map for {route.operation_id}\"\n                    )\n                    continue\n\n                mapping = route.parameter_map[arg_name]\n                location = mapping[\"location\"]\n                openapi_name = mapping[\"openapi_name\"]\n\n                if location == \"path\":\n                    path_params[openapi_name] = value\n                elif location == \"query\":\n                    query_params[openapi_name] = value\n                elif location == \"header\":\n                    header_params[openapi_name] = value\n                elif location == \"body\":\n                    body_props[openapi_name] = value\n                else:\n                    logger.warning(\n                        f\"Unknown parameter location '{location}' for {arg_name}\"\n                    )\n        else:\n            # Fallback: try to map arguments based on parameter definitions\n            logger.debug(\"No parameter map available, using fallback mapping\")\n\n            # Create a mapping from parameter names to their locations\n            param_locations = {}\n            for param in route.parameters:\n                param_locations[param.name] = param.location\n\n            # Map arguments to locations\n            for arg_name, value in flat_args.items():\n                if value is None:\n                    continue\n\n                # Check if it's a suffixed parameter (e.g., id__path)\n                if \"__\" in arg_name:\n                    base_name, location = arg_name.rsplit(\"__\", 1)\n                    if location in [\"path\", \"query\", \"header\"]:\n                        if location == \"path\":\n                            path_params[base_name] = value\n                        elif location == \"query\":\n                            query_params[base_name] = value\n                        elif location == \"header\":\n                            header_params[base_name] = value\n                        continue\n\n                # Check if it's a known parameter\n                if arg_name in param_locations:\n                    location = param_locations[arg_name]\n                    if location == \"path\":\n                        path_params[arg_name] = value\n                    elif location == \"query\":\n                        query_params[arg_name] = value\n                    elif location == \"header\":\n                        header_params[arg_name] = value\n                else:\n                    # Assume it's a body property\n                    body_props[arg_name] = value\n\n        # Handle body construction\n        body = None\n        if body_props:\n            # If we have body properties, construct the body object\n            if (\n                route.request_body\n                and route.request_body.content_schema\n                and len(route.request_body.content_schema) > 0\n            ):\n                content_type = next(iter(route.request_body.content_schema))\n                body_schema = route.request_body.content_schema[content_type]\n\n                if (\n                    isinstance(body_schema, dict)\n                    and body_schema.get(\"type\") == \"object\"\n                ):\n                    body = body_props\n                elif len(body_props) == 1:\n                    # If body schema is not an object and we have exactly one property,\n                    # use the property value directly\n                    body = next(iter(body_props.values()))\n                else:\n                    # Multiple properties but schema is not object - wrap in object\n                    body = body_props\n            else:\n                body = body_props\n\n        return path_params, query_params, header_params, body\n\n    def _build_url(\n        self, path_template: str, path_params: dict[str, Any], base_url: str\n    ) -> str:\n        \"\"\"\n        Build URL by substituting path parameters in the template.\n\n        Args:\n            path_template: OpenAPI path template (e.g., \"/users/{id}\")\n            path_params: Path parameter values\n            base_url: Base URL to prepend\n\n        Returns:\n            Complete URL with path parameters substituted\n        \"\"\"\n        # Substitute path parameters with URL-encoding to prevent\n        # path traversal and SSRF via crafted parameter values\n        url_path = path_template\n        for param_name, param_value in path_params.items():\n            placeholder = f\"{{{param_name}}}\"\n            if placeholder in url_path:\n                safe_value = quote(str(param_value), safe=\"\").replace(\".\", \"%2E\")\n                url_path = url_path.replace(placeholder, safe_value)\n\n        # Combine with base URL\n        return urljoin(base_url.rstrip(\"/\") + \"/\", url_path.lstrip(\"/\"))\n\n\n# Export public symbols\n__all__ = [\"RequestDirector\"]\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/formatters.py",
    "content": "\"\"\"Parameter formatting functions for OpenAPI operations.\"\"\"\n\nimport json\nimport logging\nfrom typing import Any\n\nfrom .models import JsonSchema, ParameterInfo, RequestBodyInfo\n\nlogger = logging.getLogger(__name__)\n\n\ndef format_array_parameter(\n    values: list, parameter_name: str, is_query_parameter: bool = False\n) -> str | list:\n    \"\"\"\n    Format an array parameter according to OpenAPI specifications.\n\n    Args:\n        values: List of values to format\n        parameter_name: Name of the parameter (for error messages)\n        is_query_parameter: If True, can return list for explode=True behavior\n\n    Returns:\n        String (comma-separated) or list (for query params with explode=True)\n    \"\"\"\n    # For arrays of simple types (strings, numbers, etc.), join with commas\n    if all(isinstance(item, str | int | float | bool) for item in values):\n        return \",\".join(str(v) for v in values)\n\n    # For complex types, try to create a simpler representation\n    try:\n        # Try to create a simple string representation\n        formatted_parts = []\n        for item in values:\n            if isinstance(item, dict):\n                # For objects, serialize key-value pairs\n                item_parts = []\n                for k, v in item.items():\n                    item_parts.append(f\"{k}:{v}\")\n                formatted_parts.append(\".\".join(item_parts))\n            else:\n                formatted_parts.append(str(item))\n\n        return \",\".join(formatted_parts)\n    except Exception as e:\n        param_type = \"query\" if is_query_parameter else \"path\"\n        logger.warning(\n            f\"Failed to format complex array {param_type} parameter '{parameter_name}': {e}\"\n        )\n\n        if is_query_parameter:\n            # For query parameters, fallback to original list\n            return values\n        else:\n            # For path parameters, fallback to string representation without Python syntax\n            str_value = (\n                str(values)\n                .replace(\"[\", \"\")\n                .replace(\"]\", \"\")\n                .replace(\"'\", \"\")\n                .replace('\"', \"\")\n            )\n            return str_value\n\n\ndef format_deep_object_parameter(\n    param_value: dict, parameter_name: str\n) -> dict[str, str]:\n    \"\"\"\n    Format a dictionary parameter for deep-object style serialization.\n\n    According to OpenAPI 3.0 spec, deepObject style with explode=true serializes\n    object properties as separate query parameters with bracket notation.\n\n    For example, `{\"id\": \"123\", \"type\": \"user\"}` becomes\n    `param[id]=123&param[type]=user`.\n\n    Args:\n        param_value: Dictionary value to format\n        parameter_name: Name of the parameter\n\n    Returns:\n        Dictionary with bracketed parameter names as keys\n    \"\"\"\n    if not isinstance(param_value, dict):\n        logger.warning(\n            f\"Deep-object style parameter '{parameter_name}' expected dict, got {type(param_value)}\"\n        )\n        return {}\n\n    result = {}\n    for key, value in param_value.items():\n        # Format as param[key]=value\n        bracketed_key = f\"{parameter_name}[{key}]\"\n        result[bracketed_key] = str(value)\n\n    return result\n\n\ndef generate_example_from_schema(schema: JsonSchema | None) -> Any:\n    \"\"\"\n    Generate a simple example value from a JSON schema dictionary.\n    Very basic implementation focusing on types.\n    \"\"\"\n    if not schema or not isinstance(schema, dict):\n        return \"unknown\"  # Or None?\n\n    # Use default value if provided\n    if \"default\" in schema:\n        return schema[\"default\"]\n    # Use first enum value if provided\n    if \"enum\" in schema and isinstance(schema[\"enum\"], list) and schema[\"enum\"]:\n        return schema[\"enum\"][0]\n    # Use first example if provided\n    if (\n        \"examples\" in schema\n        and isinstance(schema[\"examples\"], list)\n        and schema[\"examples\"]\n    ):\n        return schema[\"examples\"][0]\n    if \"example\" in schema:\n        return schema[\"example\"]\n\n    schema_type = schema.get(\"type\")\n\n    if schema_type == \"object\":\n        result = {}\n        properties = schema.get(\"properties\", {})\n        if isinstance(properties, dict):\n            # Generate example for first few properties or required ones? Limit complexity.\n            required_props = set(schema.get(\"required\", []))\n            props_to_include = list(properties.keys())[\n                :3\n            ]  # Limit to first 3 for brevity\n            for prop_name in props_to_include:\n                if prop_name in properties:\n                    result[prop_name] = generate_example_from_schema(\n                        properties[prop_name]\n                    )\n            # Ensure required props are present if possible\n            for req_prop in required_props:\n                if req_prop not in result and req_prop in properties:\n                    result[req_prop] = generate_example_from_schema(\n                        properties[req_prop]\n                    )\n        return result if result else {\"key\": \"value\"}  # Basic object if no props\n\n    elif schema_type == \"array\":\n        items_schema = schema.get(\"items\")\n        if isinstance(items_schema, dict):\n            # Generate one example item\n            item_example = generate_example_from_schema(items_schema)\n            return [item_example] if item_example is not None else []\n        return [\"example_item\"]  # Fallback\n\n    elif schema_type == \"string\":\n        format_type = schema.get(\"format\")\n        if format_type == \"date-time\":\n            return \"2024-01-01T12:00:00Z\"\n        if format_type == \"date\":\n            return \"2024-01-01\"\n        if format_type == \"email\":\n            return \"user@example.com\"\n        if format_type == \"uuid\":\n            return \"123e4567-e89b-12d3-a456-426614174000\"\n        if format_type == \"byte\":\n            return \"ZXhhbXBsZQ==\"  # \"example\" base64\n        return \"string\"\n\n    elif schema_type == \"integer\":\n        return 1\n    elif schema_type == \"number\":\n        return 1.5\n    elif schema_type == \"boolean\":\n        return True\n    elif schema_type == \"null\":\n        return None\n\n    # Fallback if type is unknown or missing\n    return \"unknown_type\"\n\n\ndef format_json_for_description(data: Any, indent: int = 2) -> str:\n    \"\"\"Formats Python data as a JSON string block for Markdown.\"\"\"\n    try:\n        json_str = json.dumps(data, indent=indent)\n        return f\"```json\\n{json_str}\\n```\"\n    except TypeError:\n        return f\"```\\nCould not serialize to JSON: {data}\\n```\"\n\n\ndef format_description_with_responses(\n    base_description: str,\n    responses: dict[\n        str, Any\n    ],  # Changed from specific ResponseInfo type to avoid circular imports\n    parameters: list[ParameterInfo] | None = None,  # Add parameters parameter\n    request_body: RequestBodyInfo | None = None,  # Add request_body parameter\n) -> str:\n    \"\"\"\n    Formats the base description string with response, parameter, and request body information.\n\n    Args:\n        base_description (str): The initial description to be formatted.\n        responses (dict[str, Any]): A dictionary of response information, keyed by status code.\n        parameters (list[ParameterInfo] | None, optional): A list of parameter information,\n            including path and query parameters. Each parameter includes details such as name,\n            location, whether it is required, and a description.\n        request_body (RequestBodyInfo | None, optional): Information about the request body,\n            including its description, whether it is required, and its content schema.\n\n    Returns:\n        str: The formatted description string with additional details about responses, parameters,\n        and the request body.\n    \"\"\"\n    desc_parts = [base_description]\n\n    # Add parameter information\n    if parameters:\n        # Process path parameters\n        path_params = [p for p in parameters if p.location == \"path\"]\n        if path_params:\n            param_section = \"\\n\\n**Path Parameters:**\"\n            desc_parts.append(param_section)\n            for param in path_params:\n                required_marker = \" (Required)\" if param.required else \"\"\n                param_desc = f\"\\n- **{param.name}**{required_marker}: {param.description or 'No description.'}\"\n                desc_parts.append(param_desc)\n\n        # Process query parameters\n        query_params = [p for p in parameters if p.location == \"query\"]\n        if query_params:\n            param_section = \"\\n\\n**Query Parameters:**\"\n            desc_parts.append(param_section)\n            for param in query_params:\n                required_marker = \" (Required)\" if param.required else \"\"\n                param_desc = f\"\\n- **{param.name}**{required_marker}: {param.description or 'No description.'}\"\n                desc_parts.append(param_desc)\n\n    # Add request body information if present\n    if request_body and request_body.description:\n        req_body_section = \"\\n\\n**Request Body:**\"\n        desc_parts.append(req_body_section)\n        required_marker = \" (Required)\" if request_body.required else \"\"\n        desc_parts.append(f\"\\n{request_body.description}{required_marker}\")\n\n        # Add request body property descriptions if available\n        if request_body.content_schema:\n            media_type = (\n                \"application/json\"\n                if \"application/json\" in request_body.content_schema\n                else next(iter(request_body.content_schema), None)\n            )\n            if media_type:\n                schema = request_body.content_schema.get(media_type, {})\n                if isinstance(schema, dict) and \"properties\" in schema:\n                    desc_parts.append(\"\\n\\n**Request Properties:**\")\n                    for prop_name, prop_schema in schema[\"properties\"].items():\n                        if (\n                            isinstance(prop_schema, dict)\n                            and \"description\" in prop_schema\n                        ):\n                            required = prop_name in schema.get(\"required\", [])\n                            req_mark = \" (Required)\" if required else \"\"\n                            desc_parts.append(\n                                f\"\\n- **{prop_name}**{req_mark}: {prop_schema['description']}\"\n                            )\n\n    # Add response information\n    if responses:\n        response_section = \"\\n\\n**Responses:**\"\n        added_response_section = False\n\n        # Determine success codes (common ones)\n        success_codes = {\"200\", \"201\", \"202\", \"204\"}  # As strings\n        success_status = next((s for s in success_codes if s in responses), None)\n\n        # Process all responses\n        responses_to_process = responses.items()\n\n        for status_code, resp_info in sorted(responses_to_process):\n            if not added_response_section:\n                desc_parts.append(response_section)\n                added_response_section = True\n\n            status_marker = \" (Success)\" if status_code == success_status else \"\"\n            desc_parts.append(\n                f\"\\n- **{status_code}**{status_marker}: {resp_info.description or 'No description.'}\"\n            )\n\n            # Process content schemas for this response\n            if resp_info.content_schema:\n                # Prioritize json, then take first available\n                media_type = (\n                    \"application/json\"\n                    if \"application/json\" in resp_info.content_schema\n                    else next(iter(resp_info.content_schema), None)\n                )\n\n                if media_type:\n                    schema = resp_info.content_schema.get(media_type)\n                    desc_parts.append(f\"  - Content-Type: `{media_type}`\")\n\n                    # Add response property descriptions\n                    if isinstance(schema, dict):\n                        # Handle array responses\n                        if schema.get(\"type\") == \"array\" and \"items\" in schema:\n                            items_schema = schema[\"items\"]\n                            if (\n                                isinstance(items_schema, dict)\n                                and \"properties\" in items_schema\n                            ):\n                                desc_parts.append(\"\\n  - **Response Item Properties:**\")\n                                for prop_name, prop_schema in items_schema[\n                                    \"properties\"\n                                ].items():\n                                    if (\n                                        isinstance(prop_schema, dict)\n                                        and \"description\" in prop_schema\n                                    ):\n                                        desc_parts.append(\n                                            f\"\\n    - **{prop_name}**: {prop_schema['description']}\"\n                                        )\n                        # Handle object responses\n                        elif \"properties\" in schema:\n                            desc_parts.append(\"\\n  - **Response Properties:**\")\n                            for prop_name, prop_schema in schema[\"properties\"].items():\n                                if (\n                                    isinstance(prop_schema, dict)\n                                    and \"description\" in prop_schema\n                                ):\n                                    desc_parts.append(\n                                        f\"\\n    - **{prop_name}**: {prop_schema['description']}\"\n                                    )\n\n                    # Generate Example\n                    if schema:\n                        example = generate_example_from_schema(schema)\n                        if example != \"unknown_type\" and example is not None:\n                            desc_parts.append(\"\\n  - **Example:**\")\n                            desc_parts.append(\n                                format_json_for_description(example, indent=2)\n                            )\n\n    return \"\\n\".join(desc_parts)\n\n\n# Export public symbols\n__all__ = [\n    \"format_array_parameter\",\n    \"format_deep_object_parameter\",\n    \"format_description_with_responses\",\n    \"format_json_for_description\",\n    \"generate_example_from_schema\",\n]\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/json_schema_converter.py",
    "content": "\"\"\"\nClean OpenAPI 3.0 to JSON Schema converter for the experimental parser.\n\nThis module provides a systematic approach to converting OpenAPI 3.0 schemas\nto JSON Schema, inspired by py-openapi-schema-to-json-schema but optimized\nfor our specific use case.\n\"\"\"\n\nfrom typing import Any\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\n# OpenAPI-specific fields that should be removed from JSON Schema\nOPENAPI_SPECIFIC_FIELDS = {\n    \"nullable\",  # Handled by converting to type arrays\n    \"discriminator\",  # OpenAPI-specific\n    \"readOnly\",  # OpenAPI-specific metadata\n    \"writeOnly\",  # OpenAPI-specific metadata\n    \"xml\",  # OpenAPI-specific metadata\n    \"externalDocs\",  # OpenAPI-specific metadata\n    \"deprecated\",  # Can be kept but not part of JSON Schema core\n}\n\n# Fields that should be recursively processed\nRECURSIVE_FIELDS = {\n    \"properties\": dict,\n    \"items\": dict,\n    \"additionalProperties\": dict,\n    \"allOf\": list,\n    \"anyOf\": list,\n    \"oneOf\": list,\n    \"not\": dict,\n}\n\n\ndef convert_openapi_schema_to_json_schema(\n    schema: dict[str, Any],\n    openapi_version: str | None = None,\n    remove_read_only: bool = False,\n    remove_write_only: bool = False,\n    convert_one_of_to_any_of: bool = True,\n) -> dict[str, Any]:\n    \"\"\"\n    Convert an OpenAPI schema to JSON Schema format.\n\n    This is a clean, systematic approach that:\n    1. Removes OpenAPI-specific fields\n    2. Converts nullable fields to type arrays (for OpenAPI 3.0 only)\n    3. Converts oneOf to anyOf for overlapping union handling\n    4. Recursively processes nested schemas\n    5. Optionally removes readOnly/writeOnly properties\n\n    Args:\n        schema: OpenAPI schema dictionary\n        openapi_version: OpenAPI version for optimization\n        remove_read_only: Whether to remove readOnly properties\n        remove_write_only: Whether to remove writeOnly properties\n        convert_one_of_to_any_of: Whether to convert oneOf to anyOf\n\n    Returns:\n        JSON Schema-compatible dictionary\n    \"\"\"\n    if not isinstance(schema, dict):\n        return schema\n\n    # Early exit optimization - check if conversion is needed\n    needs_conversion = (\n        any(field in schema for field in OPENAPI_SPECIFIC_FIELDS)\n        or (remove_read_only and _has_read_only_properties(schema))\n        or (remove_write_only and _has_write_only_properties(schema))\n        or (convert_one_of_to_any_of and \"oneOf\" in schema)\n        or _needs_recursive_processing(\n            schema,\n            openapi_version,\n            remove_read_only,\n            remove_write_only,\n            convert_one_of_to_any_of,\n        )\n    )\n\n    if not needs_conversion:\n        return schema\n\n    # Work on a copy to avoid mutation\n    result = schema.copy()\n\n    # Step 1: Handle nullable field conversion (OpenAPI 3.0 only)\n    if openapi_version and openapi_version.startswith(\"3.0\"):\n        result = _convert_nullable_field(result)\n\n    # Step 2: Convert oneOf to anyOf if requested\n    if convert_one_of_to_any_of and \"oneOf\" in result:\n        result[\"anyOf\"] = result.pop(\"oneOf\")\n\n    # Step 3: Remove OpenAPI-specific fields\n    for field in OPENAPI_SPECIFIC_FIELDS:\n        result.pop(field, None)\n\n    # Step 4: Handle readOnly/writeOnly property removal\n    if remove_read_only or remove_write_only:\n        result = _filter_properties_by_access(\n            result, remove_read_only, remove_write_only\n        )\n\n    # Step 5: Recursively process nested schemas\n    for field_name, field_type in RECURSIVE_FIELDS.items():\n        if field_name in result:\n            if field_type is dict and isinstance(result[field_name], dict):\n                if field_name == \"properties\":\n                    # Handle properties specially - each property is a schema\n                    result[field_name] = {\n                        prop_name: convert_openapi_schema_to_json_schema(\n                            prop_schema,\n                            openapi_version,\n                            remove_read_only,\n                            remove_write_only,\n                            convert_one_of_to_any_of,\n                        )\n                        if isinstance(prop_schema, dict)\n                        else prop_schema\n                        for prop_name, prop_schema in result[field_name].items()\n                    }\n                else:\n                    result[field_name] = convert_openapi_schema_to_json_schema(\n                        result[field_name],\n                        openapi_version,\n                        remove_read_only,\n                        remove_write_only,\n                        convert_one_of_to_any_of,\n                    )\n            elif field_type is list and isinstance(result[field_name], list):\n                result[field_name] = [\n                    convert_openapi_schema_to_json_schema(\n                        item,\n                        openapi_version,\n                        remove_read_only,\n                        remove_write_only,\n                        convert_one_of_to_any_of,\n                    )\n                    if isinstance(item, dict)\n                    else item\n                    for item in result[field_name]\n                ]\n\n    return result\n\n\ndef _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Convert OpenAPI nullable field to JSON Schema type array.\"\"\"\n    if \"nullable\" not in schema:\n        return schema\n\n    result = schema.copy()\n    nullable_value = result.pop(\"nullable\")\n\n    # Only convert if nullable is True and we have a type structure\n    if not nullable_value:\n        return result\n\n    if \"type\" in result:\n        current_type = result[\"type\"]\n        if isinstance(current_type, str):\n            result[\"type\"] = [current_type, \"null\"]\n        elif isinstance(current_type, list) and \"null\" not in current_type:\n            result[\"type\"] = [*current_type, \"null\"]\n    elif \"oneOf\" in result:\n        # Convert oneOf to anyOf with null\n        result[\"anyOf\"] = [*result.pop(\"oneOf\"), {\"type\": \"null\"}]\n    elif \"anyOf\" in result:\n        # Add null to anyOf if not present\n        if not any(item.get(\"type\") == \"null\" for item in result[\"anyOf\"]):\n            result[\"anyOf\"].append({\"type\": \"null\"})\n    elif \"allOf\" in result:\n        # Wrap allOf in anyOf with null option\n        result[\"anyOf\"] = [{\"allOf\": result.pop(\"allOf\")}, {\"type\": \"null\"}]\n\n    # Handle enum fields - add null to enum values if present\n    if \"enum\" in result and None not in result[\"enum\"]:\n        result[\"enum\"] = result[\"enum\"] + [None]\n\n    return result\n\n\ndef _has_read_only_properties(schema: dict[str, Any]) -> bool:\n    \"\"\"Quick check if schema has any readOnly properties.\"\"\"\n    if \"properties\" not in schema:\n        return False\n    return any(\n        isinstance(prop, dict) and prop.get(\"readOnly\")\n        for prop in schema[\"properties\"].values()\n    )\n\n\ndef _has_write_only_properties(schema: dict[str, Any]) -> bool:\n    \"\"\"Quick check if schema has any writeOnly properties.\"\"\"\n    if \"properties\" not in schema:\n        return False\n    return any(\n        isinstance(prop, dict) and prop.get(\"writeOnly\")\n        for prop in schema[\"properties\"].values()\n    )\n\n\ndef _needs_recursive_processing(\n    schema: dict[str, Any],\n    openapi_version: str | None,\n    remove_read_only: bool,\n    remove_write_only: bool,\n    convert_one_of_to_any_of: bool,\n) -> bool:\n    \"\"\"Check if the schema needs recursive processing (smarter than just checking for recursive fields).\"\"\"\n    for field_name, field_type in RECURSIVE_FIELDS.items():\n        if field_name in schema:\n            if field_type is dict and isinstance(schema[field_name], dict):\n                if field_name == \"properties\":\n                    # Check if any property needs conversion\n                    for prop_schema in schema[field_name].values():\n                        if isinstance(prop_schema, dict):\n                            nested_needs_conversion = (\n                                any(\n                                    field in prop_schema\n                                    for field in OPENAPI_SPECIFIC_FIELDS\n                                )\n                                or (remove_read_only and prop_schema.get(\"readOnly\"))\n                                or (remove_write_only and prop_schema.get(\"writeOnly\"))\n                                or (convert_one_of_to_any_of and \"oneOf\" in prop_schema)\n                                or _needs_recursive_processing(\n                                    prop_schema,\n                                    openapi_version,\n                                    remove_read_only,\n                                    remove_write_only,\n                                    convert_one_of_to_any_of,\n                                )\n                            )\n                            if nested_needs_conversion:\n                                return True\n                else:\n                    # Check if nested schema needs conversion\n                    nested_needs_conversion = (\n                        any(\n                            field in schema[field_name]\n                            for field in OPENAPI_SPECIFIC_FIELDS\n                        )\n                        or (\n                            remove_read_only\n                            and _has_read_only_properties(schema[field_name])\n                        )\n                        or (\n                            remove_write_only\n                            and _has_write_only_properties(schema[field_name])\n                        )\n                        or (convert_one_of_to_any_of and \"oneOf\" in schema[field_name])\n                        or _needs_recursive_processing(\n                            schema[field_name],\n                            openapi_version,\n                            remove_read_only,\n                            remove_write_only,\n                            convert_one_of_to_any_of,\n                        )\n                    )\n                    if nested_needs_conversion:\n                        return True\n            elif field_type is list and isinstance(schema[field_name], list):\n                # Check if any list item needs conversion\n                for item in schema[field_name]:\n                    if isinstance(item, dict):\n                        nested_needs_conversion = (\n                            any(field in item for field in OPENAPI_SPECIFIC_FIELDS)\n                            or (remove_read_only and _has_read_only_properties(item))\n                            or (remove_write_only and _has_write_only_properties(item))\n                            or (convert_one_of_to_any_of and \"oneOf\" in item)\n                            or _needs_recursive_processing(\n                                item,\n                                openapi_version,\n                                remove_read_only,\n                                remove_write_only,\n                                convert_one_of_to_any_of,\n                            )\n                        )\n                        if nested_needs_conversion:\n                            return True\n    return False\n\n\ndef _filter_properties_by_access(\n    schema: dict[str, Any], remove_read_only: bool, remove_write_only: bool\n) -> dict[str, Any]:\n    \"\"\"Remove readOnly and/or writeOnly properties from schema.\"\"\"\n    if \"properties\" not in schema:\n        return schema\n\n    result = schema.copy()\n    filtered_properties = {}\n\n    for prop_name, prop_schema in result[\"properties\"].items():\n        if not isinstance(prop_schema, dict):\n            filtered_properties[prop_name] = prop_schema\n            continue\n\n        should_remove = (remove_read_only and prop_schema.get(\"readOnly\")) or (\n            remove_write_only and prop_schema.get(\"writeOnly\")\n        )\n\n        if not should_remove:\n            filtered_properties[prop_name] = prop_schema\n\n    result[\"properties\"] = filtered_properties\n\n    # Clean up required array if properties were removed\n    if \"required\" in result and filtered_properties:\n        result[\"required\"] = [\n            prop for prop in result[\"required\"] if prop in filtered_properties\n        ]\n        if not result[\"required\"]:\n            result.pop(\"required\")\n\n    return result\n\n\ndef convert_schema_definitions(\n    schema_definitions: dict[str, Any] | None,\n    openapi_version: str | None = None,\n    **kwargs,\n) -> dict[str, Any]:\n    \"\"\"\n    Convert a dictionary of OpenAPI schema definitions to JSON Schema.\n\n    Args:\n        schema_definitions: Dictionary of schema definitions\n        openapi_version: OpenAPI version for optimization\n        **kwargs: Additional arguments passed to convert_openapi_schema_to_json_schema\n\n    Returns:\n        Dictionary of converted schema definitions\n    \"\"\"\n    if not schema_definitions:\n        return {}\n\n    return {\n        name: convert_openapi_schema_to_json_schema(schema, openapi_version, **kwargs)\n        for name, schema in schema_definitions.items()\n    }\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/models.py",
    "content": "\"\"\"Intermediate Representation (IR) models for OpenAPI operations.\"\"\"\n\nfrom typing import Any, Literal\n\nfrom pydantic import Field\n\nfrom fastmcp.utilities.types import FastMCPBaseModel\n\n# Type definitions\nHttpMethod = Literal[\n    \"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\", \"HEAD\", \"TRACE\"\n]\nParameterLocation = Literal[\"path\", \"query\", \"header\", \"cookie\"]\nJsonSchema = dict[str, Any]\n\n\nclass ParameterInfo(FastMCPBaseModel):\n    \"\"\"Represents a single parameter for an HTTP operation in our IR.\"\"\"\n\n    name: str\n    location: ParameterLocation  # Mapped from 'in' field of openapi-pydantic Parameter\n    required: bool = False\n    schema_: JsonSchema = Field(..., alias=\"schema\")  # Target name in IR\n    description: str | None = None\n    explode: bool | None = None  # OpenAPI explode property for array parameters\n    style: str | None = None  # OpenAPI style property for parameter serialization\n\n\nclass RequestBodyInfo(FastMCPBaseModel):\n    \"\"\"Represents the request body for an HTTP operation in our IR.\"\"\"\n\n    required: bool = False\n    content_schema: dict[str, JsonSchema] = Field(\n        default_factory=dict\n    )  # Key: media type\n    description: str | None = None\n\n\nclass ResponseInfo(FastMCPBaseModel):\n    \"\"\"Represents response information in our IR.\"\"\"\n\n    description: str | None = None\n    # Store schema per media type, key is media type\n    content_schema: dict[str, JsonSchema] = Field(default_factory=dict)\n\n\nclass HTTPRoute(FastMCPBaseModel):\n    \"\"\"Intermediate Representation for a single OpenAPI operation.\"\"\"\n\n    path: str\n    method: HttpMethod\n    operation_id: str | None = None\n    summary: str | None = None\n    description: str | None = None\n    tags: list[str] = Field(default_factory=list)\n    parameters: list[ParameterInfo] = Field(default_factory=list)\n    request_body: RequestBodyInfo | None = None\n    responses: dict[str, ResponseInfo] = Field(\n        default_factory=dict\n    )  # Key: status code str\n    request_schemas: dict[str, JsonSchema] = Field(\n        default_factory=dict\n    )  # Store schemas needed for input (parameters/request body)\n    response_schemas: dict[str, JsonSchema] = Field(\n        default_factory=dict\n    )  # Store schemas needed for output (responses)\n    extensions: dict[str, Any] = Field(default_factory=dict)\n    openapi_version: str | None = None\n\n    # Pre-calculated fields for performance\n    flat_param_schema: JsonSchema = Field(\n        default_factory=dict\n    )  # Combined schema for MCP tools\n    parameter_map: dict[str, dict[str, str]] = Field(\n        default_factory=dict\n    )  # Maps flat args to locations\n\n\n# Export public symbols\n__all__ = [\n    \"HTTPRoute\",\n    \"HttpMethod\",\n    \"JsonSchema\",\n    \"ParameterInfo\",\n    \"ParameterLocation\",\n    \"RequestBodyInfo\",\n    \"ResponseInfo\",\n]\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/parser.py",
    "content": "\"\"\"OpenAPI parsing logic for converting OpenAPI specs to HTTPRoute objects.\"\"\"\n\nfrom typing import Any, Generic, TypeVar, cast\n\nfrom openapi_pydantic import (\n    OpenAPI,\n    Operation,\n    Parameter,\n    PathItem,\n    Reference,\n    RequestBody,\n    Response,\n    Schema,\n)\n\n# Import OpenAPI 3.0 models as well\nfrom openapi_pydantic.v3.v3_0 import OpenAPI as OpenAPI_30\nfrom openapi_pydantic.v3.v3_0 import Operation as Operation_30\nfrom openapi_pydantic.v3.v3_0 import Parameter as Parameter_30\nfrom openapi_pydantic.v3.v3_0 import PathItem as PathItem_30\nfrom openapi_pydantic.v3.v3_0 import Reference as Reference_30\nfrom openapi_pydantic.v3.v3_0 import RequestBody as RequestBody_30\nfrom openapi_pydantic.v3.v3_0 import Response as Response_30\nfrom openapi_pydantic.v3.v3_0 import Schema as Schema_30\nfrom pydantic import BaseModel, ValidationError\n\nfrom fastmcp.utilities.logging import get_logger\n\nfrom .models import (\n    HTTPRoute,\n    JsonSchema,\n    ParameterInfo,\n    ParameterLocation,\n    RequestBodyInfo,\n    ResponseInfo,\n)\nfrom .schemas import (\n    _combine_schemas_and_map_params,\n    _replace_ref_with_defs,\n)\n\nlogger = get_logger(__name__)\n\n# Type variables for generic parser\nTOpenAPI = TypeVar(\"TOpenAPI\", OpenAPI, OpenAPI_30)\nTSchema = TypeVar(\"TSchema\", Schema, Schema_30)\nTReference = TypeVar(\"TReference\", Reference, Reference_30)\nTParameter = TypeVar(\"TParameter\", Parameter, Parameter_30)\nTRequestBody = TypeVar(\"TRequestBody\", RequestBody, RequestBody_30)\nTResponse = TypeVar(\"TResponse\", Response, Response_30)\nTOperation = TypeVar(\"TOperation\", Operation, Operation_30)\nTPathItem = TypeVar(\"TPathItem\", PathItem, PathItem_30)\n\n\ndef parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute]:\n    \"\"\"\n    Parses an OpenAPI schema dictionary into a list of HTTPRoute objects\n    using the openapi-pydantic library.\n\n    Supports both OpenAPI 3.0.x and 3.1.x versions.\n    \"\"\"\n    # Check OpenAPI version to use appropriate model\n    openapi_version = openapi_dict.get(\"openapi\", \"\")\n\n    try:\n        if openapi_version.startswith(\"3.0\"):\n            # Use OpenAPI 3.0 models\n            openapi_30 = OpenAPI_30.model_validate(openapi_dict)\n            logger.debug(\n                f\"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}\"\n            )\n            parser = OpenAPIParser(\n                openapi_30,\n                Reference_30,\n                Schema_30,\n                Parameter_30,\n                RequestBody_30,\n                Response_30,\n                Operation_30,\n                PathItem_30,\n                openapi_version,\n            )\n            return parser.parse()\n        else:\n            # Default to OpenAPI 3.1 models\n            openapi_31 = OpenAPI.model_validate(openapi_dict)\n            logger.debug(\n                f\"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}\"\n            )\n            parser = OpenAPIParser(\n                openapi_31,\n                Reference,\n                Schema,\n                Parameter,\n                RequestBody,\n                Response,\n                Operation,\n                PathItem,\n                openapi_version,\n            )\n            return parser.parse()\n    except ValidationError as e:\n        logger.error(f\"OpenAPI schema validation failed: {e}\")\n        error_details = e.errors()\n        logger.error(f\"Validation errors: {error_details}\")\n        raise ValueError(f\"Invalid OpenAPI schema: {error_details}\") from e\n\n\nclass OpenAPIParser(\n    Generic[\n        TOpenAPI,\n        TReference,\n        TSchema,\n        TParameter,\n        TRequestBody,\n        TResponse,\n        TOperation,\n        TPathItem,\n    ]\n):\n    \"\"\"Unified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1.\"\"\"\n\n    def __init__(\n        self,\n        openapi: TOpenAPI,\n        reference_cls: type[TReference],\n        schema_cls: type[TSchema],\n        parameter_cls: type[TParameter],\n        request_body_cls: type[TRequestBody],\n        response_cls: type[TResponse],\n        operation_cls: type[TOperation],\n        path_item_cls: type[TPathItem],\n        openapi_version: str,\n    ):\n        \"\"\"Initialize the parser with the OpenAPI schema and type classes.\"\"\"\n        self.openapi = openapi\n        self.reference_cls = reference_cls\n        self.schema_cls = schema_cls\n        self.parameter_cls = parameter_cls\n        self.request_body_cls = request_body_cls\n        self.response_cls = response_cls\n        self.operation_cls = operation_cls\n        self.path_item_cls = path_item_cls\n        self.openapi_version = openapi_version\n\n    def _convert_to_parameter_location(self, param_in: str) -> ParameterLocation:\n        \"\"\"Convert string parameter location to our ParameterLocation type.\"\"\"\n        if param_in in [\"path\", \"query\", \"header\", \"cookie\"]:\n            return cast(ParameterLocation, param_in)\n        logger.warning(f\"Unknown parameter location: {param_in}, defaulting to 'query'\")\n        return cast(ParameterLocation, \"query\")\n\n    def _resolve_ref(self, item: Any) -> Any:\n        \"\"\"Resolves a reference to its target definition.\"\"\"\n        if isinstance(item, self.reference_cls):\n            ref_str = item.ref\n            # Ensure ref_str is a string before calling startswith()\n            if not isinstance(ref_str, str):\n                return item\n            try:\n                if not ref_str.startswith(\"#/\"):\n                    raise ValueError(\n                        f\"External or non-local reference not supported: {ref_str}\"\n                    )\n\n                parts = ref_str.strip(\"#/\").split(\"/\")\n                target = self.openapi\n\n                for part in parts:\n                    if part.isdigit() and isinstance(target, list):\n                        target = target[int(part)]\n                    elif isinstance(target, BaseModel):\n                        # Check class fields first, then model_extra\n                        if part in target.__class__.model_fields:\n                            target = getattr(target, part, None)\n                        elif target.model_extra and part in target.model_extra:\n                            target = target.model_extra[part]\n                        else:\n                            # Special handling for components\n                            if part == \"components\" and hasattr(target, \"components\"):\n                                target = target.components\n                            elif hasattr(target, part):  # Fallback check\n                                target = getattr(target, part, None)\n                            else:\n                                target = None  # Part not found\n                    elif isinstance(target, dict):\n                        target = target.get(part)\n                    else:\n                        raise ValueError(\n                            f\"Cannot traverse part '{part}' in reference '{ref_str}'\"\n                        )\n\n                    if target is None:\n                        raise ValueError(\n                            f\"Reference part '{part}' not found in path '{ref_str}'\"\n                        )\n\n                # Handle nested references\n                if isinstance(target, self.reference_cls):\n                    return self._resolve_ref(target)\n\n                return target\n            except (AttributeError, KeyError, IndexError, TypeError, ValueError) as e:\n                raise ValueError(f\"Failed to resolve reference '{ref_str}': {e}\") from e\n\n        return item\n\n    def _extract_schema_as_dict(self, schema_obj: Any) -> JsonSchema:\n        \"\"\"Resolves a schema and returns it as a dictionary.\"\"\"\n        try:\n            resolved_schema = self._resolve_ref(schema_obj)\n\n            if isinstance(resolved_schema, self.schema_cls):\n                # Convert schema to dictionary\n                result = resolved_schema.model_dump(\n                    mode=\"json\", by_alias=True, exclude_none=True\n                )\n            elif isinstance(resolved_schema, dict):\n                result = resolved_schema\n            else:\n                logger.warning(\n                    f\"Expected Schema after resolving, got {type(resolved_schema)}. Returning empty dict.\"\n                )\n                result = {}\n\n            # Convert refs from OpenAPI format to JSON Schema format using recursive approach\n\n            result = _replace_ref_with_defs(result)\n            return result\n        except ValueError as e:\n            # Re-raise ValueError for external reference errors and other validation issues\n            if \"External or non-local reference not supported\" in str(e):\n                raise\n            logger.error(f\"Failed to extract schema as dict: {e}\", exc_info=False)\n            return {}\n        except Exception as e:\n            logger.error(f\"Failed to extract schema as dict: {e}\", exc_info=False)\n            return {}\n\n    def _extract_parameters(\n        self,\n        operation_params: list[Any] | None = None,\n        path_item_params: list[Any] | None = None,\n    ) -> list[ParameterInfo]:\n        \"\"\"Extract and resolve parameters from operation and path item.\"\"\"\n        extracted_params: list[ParameterInfo] = []\n        seen_params: dict[\n            tuple[str, str], bool\n        ] = {}  # Use tuple of (name, location) as key\n        all_params = (operation_params or []) + (path_item_params or [])\n\n        for param_or_ref in all_params:\n            try:\n                parameter = self._resolve_ref(param_or_ref)\n\n                if not isinstance(parameter, self.parameter_cls):\n                    logger.warning(\n                        f\"Expected Parameter after resolving, got {type(parameter)}. Skipping.\"\n                    )\n                    continue\n\n                # Extract parameter info - handle both 3.0 and 3.1 parameter models\n                param_in = parameter.param_in  # Both use param_in\n                # Handle enum or string parameter locations\n                from enum import Enum\n\n                param_in_str = (\n                    param_in.value if isinstance(param_in, Enum) else param_in\n                )\n                param_location = self._convert_to_parameter_location(param_in_str)\n                param_schema_obj = parameter.param_schema  # Both use param_schema\n\n                # Skip duplicate parameters (same name and location)\n                param_key = (parameter.name, param_in_str)\n                if param_key in seen_params:\n                    continue\n                seen_params[param_key] = True\n\n                # Extract schema\n                param_schema_dict = {}\n                if param_schema_obj:\n                    # Process schema object\n                    param_schema_dict = self._extract_schema_as_dict(param_schema_obj)\n\n                    # Handle default value\n                    resolved_schema = self._resolve_ref(param_schema_obj)\n                    if (\n                        not isinstance(resolved_schema, self.reference_cls)\n                        and hasattr(resolved_schema, \"default\")\n                        and resolved_schema.default is not None\n                    ):\n                        param_schema_dict[\"default\"] = resolved_schema.default\n\n                elif hasattr(parameter, \"content\") and parameter.content:\n                    # Handle content-based parameters\n                    first_media_type = next(iter(parameter.content.values()), None)\n                    if (\n                        first_media_type\n                        and hasattr(first_media_type, \"media_type_schema\")\n                        and first_media_type.media_type_schema\n                    ):\n                        media_schema = first_media_type.media_type_schema\n                        param_schema_dict = self._extract_schema_as_dict(media_schema)\n\n                        # Handle default value in content schema\n                        resolved_media_schema = self._resolve_ref(media_schema)\n                        if (\n                            not isinstance(resolved_media_schema, self.reference_cls)\n                            and hasattr(resolved_media_schema, \"default\")\n                            and resolved_media_schema.default is not None\n                        ):\n                            param_schema_dict[\"default\"] = resolved_media_schema.default\n\n                # Extract explode and style properties if present\n                explode = getattr(parameter, \"explode\", None)\n                style = getattr(parameter, \"style\", None)\n\n                # Create parameter info object\n                param_info = ParameterInfo(\n                    name=parameter.name,\n                    location=param_location,\n                    required=parameter.required,\n                    schema=param_schema_dict,\n                    description=parameter.description,\n                    explode=explode,\n                    style=style,\n                )\n                extracted_params.append(param_info)\n            except Exception as e:\n                param_name = getattr(\n                    param_or_ref, \"name\", getattr(param_or_ref, \"ref\", \"unknown\")\n                )\n                logger.error(\n                    f\"Failed to extract parameter '{param_name}': {e}\", exc_info=False\n                )\n\n        return extracted_params\n\n    def _extract_request_body(self, request_body_or_ref: Any) -> RequestBodyInfo | None:\n        \"\"\"Extract and resolve request body information.\"\"\"\n        if not request_body_or_ref:\n            return None\n\n        try:\n            request_body = self._resolve_ref(request_body_or_ref)\n\n            if not isinstance(request_body, self.request_body_cls):\n                logger.warning(\n                    f\"Expected RequestBody after resolving, got {type(request_body)}. Returning None.\"\n                )\n                return None\n\n            # Create request body info\n            request_body_info = RequestBodyInfo(\n                required=request_body.required,\n                description=request_body.description,\n            )\n\n            # Extract content schemas\n            if hasattr(request_body, \"content\") and request_body.content:\n                for media_type_str, media_type_obj in request_body.content.items():\n                    if (\n                        media_type_obj\n                        and hasattr(media_type_obj, \"media_type_schema\")\n                        and media_type_obj.media_type_schema\n                    ):\n                        try:\n                            schema_dict = self._extract_schema_as_dict(\n                                media_type_obj.media_type_schema\n                            )\n                            request_body_info.content_schema[media_type_str] = (\n                                schema_dict\n                            )\n                        except ValueError as e:\n                            # Re-raise ValueError for external reference errors\n                            if \"External or non-local reference not supported\" in str(\n                                e\n                            ):\n                                raise\n                            logger.error(\n                                f\"Failed to extract schema for media type '{media_type_str}': {e}\"\n                            )\n                        except Exception as e:\n                            logger.error(\n                                f\"Failed to extract schema for media type '{media_type_str}': {e}\"\n                            )\n\n            return request_body_info\n        except ValueError as e:\n            # Re-raise ValueError for external reference errors\n            if \"External or non-local reference not supported\" in str(e):\n                raise\n            ref_name = getattr(request_body_or_ref, \"ref\", \"unknown\")\n            logger.error(\n                f\"Failed to extract request body '{ref_name}': {e}\", exc_info=False\n            )\n            return None\n        except Exception as e:\n            ref_name = getattr(request_body_or_ref, \"ref\", \"unknown\")\n            logger.error(\n                f\"Failed to extract request body '{ref_name}': {e}\", exc_info=False\n            )\n            return None\n\n    def _is_success_status_code(self, status_code: str) -> bool:\n        \"\"\"Check if a status code represents a successful response (2xx).\"\"\"\n        try:\n            code_int = int(status_code)\n            return 200 <= code_int < 300\n        except (ValueError, TypeError):\n            # Handle special cases like 'default' or other non-numeric codes\n            return status_code.lower() in [\"default\", \"2xx\"]\n\n    def _get_primary_success_response(\n        self, operation_responses: dict[str, Any]\n    ) -> tuple[str, Any] | None:\n        \"\"\"Get the primary success response for an MCP tool. We only need one success response.\"\"\"\n        if not operation_responses:\n            return None\n\n        # Priority order: 200, 201, 202, 204, 207, then any other 2xx\n        priority_codes = [\"200\", \"201\", \"202\", \"204\", \"207\"]\n\n        # First check priority codes\n        for code in priority_codes:\n            if code in operation_responses:\n                return (code, operation_responses[code])\n\n        # Then check any other 2xx codes\n        for status_code, resp_or_ref in operation_responses.items():\n            if self._is_success_status_code(status_code):\n                return (status_code, resp_or_ref)\n\n        # If no success codes found, return None (tool will have no output schema)\n        return None\n\n    def _extract_responses(\n        self, operation_responses: dict[str, Any] | None\n    ) -> dict[str, ResponseInfo]:\n        \"\"\"Extract and resolve response information. Only includes the primary success response for MCP tools.\"\"\"\n        extracted_responses: dict[str, ResponseInfo] = {}\n\n        if not operation_responses:\n            return extracted_responses\n\n        # For MCP tools, we only need the primary success response\n        primary_response = self._get_primary_success_response(operation_responses)\n        if not primary_response:\n            logger.debug(\"No success responses found, tool will have no output schema\")\n            return extracted_responses\n\n        status_code, resp_or_ref = primary_response\n        logger.debug(f\"Using primary success response: {status_code}\")\n\n        try:\n            response = self._resolve_ref(resp_or_ref)\n\n            if not isinstance(response, self.response_cls):\n                logger.warning(\n                    f\"Expected Response after resolving for status code {status_code}, \"\n                    f\"got {type(response)}. Returning empty responses.\"\n                )\n                return extracted_responses\n\n            # Create response info\n            resp_info = ResponseInfo(description=response.description)\n\n            # Extract content schemas\n            if hasattr(response, \"content\") and response.content:\n                for media_type_str, media_type_obj in response.content.items():\n                    if (\n                        media_type_obj\n                        and hasattr(media_type_obj, \"media_type_schema\")\n                        and media_type_obj.media_type_schema\n                    ):\n                        try:\n                            # Track if this is a top-level $ref before resolution\n                            top_level_schema_name = None\n                            media_schema = media_type_obj.media_type_schema\n                            if isinstance(media_schema, self.reference_cls):\n                                ref_str = media_schema.ref\n                                if isinstance(ref_str, str) and ref_str.startswith(\n                                    \"#/components/schemas/\"\n                                ):\n                                    top_level_schema_name = ref_str.split(\"/\")[-1]\n\n                            schema_dict = self._extract_schema_as_dict(media_schema)\n                            # Add marker for top-level schema if it was a ref\n                            if top_level_schema_name:\n                                schema_dict[\"x-fastmcp-top-level-schema\"] = (\n                                    top_level_schema_name\n                                )\n                            resp_info.content_schema[media_type_str] = schema_dict\n                        except ValueError as e:\n                            # Re-raise ValueError for external reference errors\n                            if \"External or non-local reference not supported\" in str(\n                                e\n                            ):\n                                raise\n                            logger.error(\n                                f\"Failed to extract schema for media type '{media_type_str}' \"\n                                f\"in response {status_code}: {e}\"\n                            )\n                        except Exception as e:\n                            logger.error(\n                                f\"Failed to extract schema for media type '{media_type_str}' \"\n                                f\"in response {status_code}: {e}\"\n                            )\n                    else:\n                        # Record the media type even without a schema so MIME\n                        # type inference can still use the declared content type.\n                        resp_info.content_schema.setdefault(media_type_str, {})\n\n            extracted_responses[str(status_code)] = resp_info\n        except ValueError as e:\n            # Re-raise ValueError for external reference errors\n            if \"External or non-local reference not supported\" in str(e):\n                raise\n            ref_name = getattr(resp_or_ref, \"ref\", \"unknown\")\n            logger.error(\n                f\"Failed to extract response for status code {status_code} \"\n                f\"from reference '{ref_name}': {e}\",\n                exc_info=False,\n            )\n        except Exception as e:\n            ref_name = getattr(resp_or_ref, \"ref\", \"unknown\")\n            logger.error(\n                f\"Failed to extract response for status code {status_code} \"\n                f\"from reference '{ref_name}': {e}\",\n                exc_info=False,\n            )\n\n        return extracted_responses\n\n    def _extract_schema_dependencies(\n        self,\n        schema: dict,\n        all_schemas: dict[str, Any],\n        collected: set[str] | None = None,\n    ) -> set[str]:\n        \"\"\"\n        Extract all schema names referenced by a schema (including transitive dependencies).\n\n        Args:\n            schema: The schema to analyze\n            all_schemas: All available schema definitions\n            collected: Set of already collected schema names (for recursion)\n\n        Returns:\n            Set of schema names that are referenced\n        \"\"\"\n        if collected is None:\n            collected = set()\n\n        def find_refs(obj):\n            \"\"\"Recursively find all $ref references.\"\"\"\n            if isinstance(obj, dict):\n                if \"$ref\" in obj and isinstance(obj[\"$ref\"], str):\n                    ref = obj[\"$ref\"]\n                    # Handle both converted and unconverted refs\n                    if ref.startswith((\"#/$defs/\", \"#/components/schemas/\")):\n                        schema_name = ref.split(\"/\")[-1]\n                    else:\n                        return\n\n                    # Add this schema and recursively find its dependencies\n                    if (\n                        collected is not None\n                        and schema_name not in collected\n                        and schema_name in all_schemas\n                    ):\n                        collected.add(schema_name)\n                        # Recursively find dependencies of this schema\n                        find_refs(all_schemas[schema_name])\n\n                # Continue searching in all values\n                for value in obj.values():\n                    find_refs(value)\n            elif isinstance(obj, list):\n                for item in obj:\n                    find_refs(item)\n\n        find_refs(schema)\n        return collected\n\n    def _extract_input_schema_dependencies(\n        self,\n        parameters: list[ParameterInfo],\n        request_body: RequestBodyInfo | None,\n        all_schemas: dict[str, Any],\n    ) -> dict[str, Any]:\n        \"\"\"\n        Extract only the schema definitions needed for input (parameters and request body).\n\n        Args:\n            parameters: Route parameters\n            request_body: Route request body\n            all_schemas: All available schema definitions\n\n        Returns:\n            Dictionary containing only the schemas needed for input\n        \"\"\"\n        needed_schemas = set()\n\n        # Check parameters for schema references\n        for param in parameters:\n            if param.schema_:\n                deps = self._extract_schema_dependencies(param.schema_, all_schemas)\n                needed_schemas.update(deps)\n\n        # Check request body for schema references\n        if request_body and request_body.content_schema:\n            for content_schema in request_body.content_schema.values():\n                deps = self._extract_schema_dependencies(content_schema, all_schemas)\n                needed_schemas.update(deps)\n\n        # Return only the needed input schemas\n        return {\n            name: all_schemas[name] for name in needed_schemas if name in all_schemas\n        }\n\n    def _extract_output_schema_dependencies(\n        self,\n        responses: dict[str, ResponseInfo],\n        all_schemas: dict[str, Any],\n    ) -> dict[str, Any]:\n        \"\"\"\n        Extract only the schema definitions needed for outputs (responses).\n\n        Args:\n            responses: Route responses\n            all_schemas: All available schema definitions\n\n        Returns:\n            Dictionary containing only the schemas needed for outputs\n        \"\"\"\n        if not responses or not all_schemas:\n            return {}\n\n        needed_schemas: set[str] = set()\n\n        for response in responses.values():\n            if not response.content_schema:\n                continue\n\n            for content_schema in response.content_schema.values():\n                deps = self._extract_schema_dependencies(content_schema, all_schemas)\n                needed_schemas.update(deps)\n\n                schema_name = content_schema.get(\"x-fastmcp-top-level-schema\")\n                if isinstance(schema_name, str) and schema_name in all_schemas:\n                    needed_schemas.add(schema_name)\n                    self._extract_schema_dependencies(\n                        all_schemas[schema_name],\n                        all_schemas,\n                        collected=needed_schemas,\n                    )\n\n        return {\n            name: all_schemas[name] for name in needed_schemas if name in all_schemas\n        }\n\n    def parse(self) -> list[HTTPRoute]:\n        \"\"\"Parse the OpenAPI schema into HTTP routes.\"\"\"\n        routes: list[HTTPRoute] = []\n\n        if not hasattr(self.openapi, \"paths\") or not self.openapi.paths:\n            logger.warning(\"OpenAPI schema has no paths defined.\")\n            return []\n\n        # Extract component schemas\n        schema_definitions = {}\n        if hasattr(self.openapi, \"components\") and self.openapi.components:\n            components = self.openapi.components\n            if hasattr(components, \"schemas\") and components.schemas:\n                for name, schema in components.schemas.items():\n                    try:\n                        if isinstance(schema, self.reference_cls):\n                            resolved_schema = self._resolve_ref(schema)\n                            schema_definitions[name] = self._extract_schema_as_dict(\n                                resolved_schema\n                            )\n                        else:\n                            schema_definitions[name] = self._extract_schema_as_dict(\n                                schema\n                            )\n                    except Exception as e:\n                        logger.warning(\n                            f\"Failed to extract schema definition '{name}': {e}\"\n                        )\n\n        # Convert schema definitions refs from OpenAPI to JSON Schema format (once)\n        if schema_definitions:\n            # Convert each schema definition recursively\n            for name, schema in schema_definitions.items():\n                if isinstance(schema, dict):\n                    schema_definitions[name] = _replace_ref_with_defs(schema)\n\n        # Process paths and operations\n        for path_str, path_item_obj in self.openapi.paths.items():\n            if not isinstance(path_item_obj, self.path_item_cls):\n                logger.warning(\n                    f\"Skipping invalid path item for path '{path_str}' (type: {type(path_item_obj)})\"\n                )\n                continue\n\n            path_level_params = (\n                path_item_obj.parameters\n                if hasattr(path_item_obj, \"parameters\")\n                else None\n            )\n\n            # Get HTTP methods from the path item class fields\n            http_methods = [\n                \"get\",\n                \"put\",\n                \"post\",\n                \"delete\",\n                \"options\",\n                \"head\",\n                \"patch\",\n                \"trace\",\n            ]\n            for method_lower in http_methods:\n                operation = getattr(path_item_obj, method_lower, None)\n\n                if operation and isinstance(operation, self.operation_cls):\n                    # Cast method to HttpMethod - safe since we only use valid HTTP methods\n                    method_upper = method_lower.upper()\n\n                    try:\n                        parameters = self._extract_parameters(\n                            getattr(operation, \"parameters\", None), path_level_params\n                        )\n\n                        request_body_info = self._extract_request_body(\n                            getattr(operation, \"requestBody\", None)\n                        )\n\n                        responses = self._extract_responses(\n                            getattr(operation, \"responses\", None)\n                        )\n\n                        extensions = {}\n                        if hasattr(operation, \"model_extra\") and operation.model_extra:\n                            extensions = {\n                                k: v\n                                for k, v in operation.model_extra.items()\n                                if k.startswith(\"x-\")\n                            }\n\n                        # Extract schemas separately for input and output\n                        input_schemas = self._extract_input_schema_dependencies(\n                            parameters,\n                            request_body_info,\n                            schema_definitions,\n                        )\n                        output_schemas = self._extract_output_schema_dependencies(\n                            responses,\n                            schema_definitions,\n                        )\n\n                        # Create initial route without pre-calculated fields\n                        route = HTTPRoute(\n                            path=path_str,\n                            method=method_upper,  # type: ignore[arg-type]  # Known valid HTTP method\n                            operation_id=getattr(operation, \"operationId\", None),\n                            summary=getattr(operation, \"summary\", None),\n                            description=getattr(operation, \"description\", None),\n                            tags=getattr(operation, \"tags\", []) or [],\n                            parameters=parameters,\n                            request_body=request_body_info,\n                            responses=responses,\n                            request_schemas=input_schemas,\n                            response_schemas=output_schemas,\n                            extensions=extensions,\n                            openapi_version=self.openapi_version,\n                        )\n\n                        # Pre-calculate schema and parameter mapping for performance\n                        try:\n                            flat_schema, param_map = _combine_schemas_and_map_params(\n                                route,\n                                convert_refs=False,  # Parser already converted refs\n                            )\n                            route.flat_param_schema = flat_schema\n                            route.parameter_map = param_map\n                        except Exception as schema_error:\n                            logger.warning(\n                                f\"Failed to pre-calculate schema for route {method_upper} {path_str}: {schema_error}\"\n                            )\n                            # Continue with empty pre-calculated fields\n                            route.flat_param_schema = {\n                                \"type\": \"object\",\n                                \"properties\": {},\n                            }\n                            route.parameter_map = {}\n                        routes.append(route)\n                    except ValueError as op_error:\n                        # Re-raise ValueError for external reference errors\n                        if \"External or non-local reference not supported\" in str(\n                            op_error\n                        ):\n                            raise\n                        op_id = getattr(operation, \"operationId\", \"unknown\")\n                        logger.error(\n                            f\"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}\",\n                            exc_info=True,\n                        )\n                    except Exception as op_error:\n                        op_id = getattr(operation, \"operationId\", \"unknown\")\n                        logger.error(\n                            f\"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}\",\n                            exc_info=True,\n                        )\n\n        logger.debug(f\"Finished parsing. Extracted {len(routes)} HTTP routes.\")\n        return routes\n\n\n# Export public symbols\n__all__ = [\n    \"OpenAPIParser\",\n    \"parse_openapi_to_http_routes\",\n]\n"
  },
  {
    "path": "src/fastmcp/utilities/openapi/schemas.py",
    "content": "\"\"\"Schema manipulation utilities for OpenAPI operations.\"\"\"\n\nfrom typing import Any\n\nfrom fastmcp.utilities.logging import get_logger\n\nfrom .models import HTTPRoute, JsonSchema, ResponseInfo\n\nlogger = get_logger(__name__)\n\n\ndef clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None:\n    \"\"\"\n    Clean up a schema dictionary for display by removing internal/complex fields.\n    \"\"\"\n    if not schema or not isinstance(schema, dict):\n        return schema\n\n    # Make a copy to avoid modifying the input schema\n    cleaned = schema.copy()\n\n    # Fields commonly removed for simpler display to LLMs or users\n    fields_to_remove = [\n        \"allOf\",\n        \"anyOf\",\n        \"oneOf\",\n        \"not\",  # Composition keywords\n        \"nullable\",  # Handled by type unions usually\n        \"discriminator\",\n        \"readOnly\",\n        \"writeOnly\",\n        \"deprecated\",\n        \"xml\",\n        \"externalDocs\",\n        # Can be verbose, maybe remove based on flag?\n        # \"pattern\", \"minLength\", \"maxLength\",\n        # \"minimum\", \"maximum\", \"exclusiveMinimum\", \"exclusiveMaximum\",\n        # \"multipleOf\", \"minItems\", \"maxItems\", \"uniqueItems\",\n        # \"minProperties\", \"maxProperties\"\n    ]\n\n    for field in fields_to_remove:\n        if field in cleaned:\n            cleaned.pop(field)\n\n    # Recursively clean properties and items\n    if \"properties\" in cleaned:\n        cleaned[\"properties\"] = {\n            k: clean_schema_for_display(v) for k, v in cleaned[\"properties\"].items()\n        }\n        # Remove properties section if empty after cleaning\n        if not cleaned[\"properties\"]:\n            cleaned.pop(\"properties\")\n\n    if \"items\" in cleaned:\n        cleaned[\"items\"] = clean_schema_for_display(cleaned[\"items\"])\n        # Remove items section if empty after cleaning\n        if not cleaned[\"items\"]:\n            cleaned.pop(\"items\")\n\n    if \"additionalProperties\" in cleaned:\n        # Often verbose, can be simplified\n        if isinstance(cleaned[\"additionalProperties\"], dict):\n            cleaned[\"additionalProperties\"] = clean_schema_for_display(\n                cleaned[\"additionalProperties\"]\n            )\n        elif cleaned[\"additionalProperties\"] is True:\n            # Maybe keep 'true' or represent as 'Allows additional properties' text?\n            pass  # Keep simple boolean for now\n\n    return cleaned\n\n\ndef _replace_ref_with_defs(\n    info: dict[str, Any], description: str | None = None\n) -> dict[str, Any]:\n    \"\"\"\n    Replace openapi $ref with jsonschema $defs recursively.\n\n    Examples:\n    - {\"type\": \"object\", \"properties\": {\"$ref\": \"#/components/schemas/...\"}}\n    - {\"type\": \"object\", \"additionalProperties\": {\"$ref\": \"#/components/schemas/...\"}, \"properties\": {...}}\n    - {\"$ref\": \"#/components/schemas/...\"}\n    - {\"items\": {\"$ref\": \"#/components/schemas/...\"}}\n    - {\"anyOf\": [{\"$ref\": \"#/components/schemas/...\"}]}\n    - {\"allOf\": [{\"$ref\": \"#/components/schemas/...\"}]}\n    - {\"oneOf\": [{\"$ref\": \"#/components/schemas/...\"}]}\n\n    Args:\n        info: dict[str, Any]\n        description: str | None\n\n    Returns:\n        dict[str, Any]\n    \"\"\"\n    schema = info.copy()\n    if ref_path := schema.get(\"$ref\"):\n        if isinstance(ref_path, str):\n            if ref_path.startswith(\"#/components/schemas/\"):\n                schema_name = ref_path.split(\"/\")[-1]\n                schema[\"$ref\"] = f\"#/$defs/{schema_name}\"\n            elif not ref_path.startswith(\"#/\"):\n                raise ValueError(\n                    f\"External or non-local reference not supported: {ref_path}. \"\n                    f\"FastMCP only supports local schema references starting with '#/'. \"\n                    f\"Please include all schema definitions within the OpenAPI document.\"\n                )\n    elif properties := schema.get(\"properties\"):\n        if \"$ref\" in properties:\n            schema[\"properties\"] = _replace_ref_with_defs(properties)\n        else:\n            schema[\"properties\"] = {\n                prop_name: _replace_ref_with_defs(prop_schema)\n                for prop_name, prop_schema in properties.items()\n            }\n    elif item_schema := schema.get(\"items\"):\n        schema[\"items\"] = _replace_ref_with_defs(item_schema)\n    for section in [\"anyOf\", \"allOf\", \"oneOf\"]:\n        if section in schema:\n            schema[section] = [_replace_ref_with_defs(item) for item in schema[section]]\n    if additionalProperties := schema.get(\"additionalProperties\"):\n        if not isinstance(additionalProperties, bool):\n            schema[\"additionalProperties\"] = _replace_ref_with_defs(\n                additionalProperties\n            )\n    # Handle propertyNames\n    if property_names := schema.get(\"propertyNames\"):\n        if isinstance(property_names, dict):\n            schema[\"propertyNames\"] = _replace_ref_with_defs(property_names)\n    # Handle patternProperties\n    if pattern_properties := schema.get(\"patternProperties\"):\n        if isinstance(pattern_properties, dict):\n            schema[\"patternProperties\"] = {\n                pattern: _replace_ref_with_defs(subschema)\n                if isinstance(subschema, dict)\n                else subschema\n                for pattern, subschema in pattern_properties.items()\n            }\n    if info.get(\"description\", description) and not schema.get(\"description\"):\n        schema[\"description\"] = description\n    return schema\n\n\ndef _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"\n    Make an optional parameter schema nullable to allow None values.\n\n    For optional parameters, we need to allow null values in addition to the\n    specified type to handle cases where None is passed for optional parameters.\n    \"\"\"\n    # If schema already has multiple types or is already nullable, don't modify\n    if \"anyOf\" in schema or \"oneOf\" in schema or \"allOf\" in schema:\n        return schema\n\n    # If it's already nullable (type includes null), don't modify\n    if isinstance(schema.get(\"type\"), list) and \"null\" in schema[\"type\"]:\n        return schema\n\n    # Create a new schema that allows null in addition to the original type\n    if \"type\" in schema:\n        original_type = schema[\"type\"]\n        if isinstance(original_type, str):\n            # Handle different types appropriately\n            if original_type in (\"array\", \"object\"):\n                # For complex types (array/object), preserve the full structure\n                # and allow null as an alternative\n                if original_type == \"array\" and \"items\" in schema:\n                    # Array with items - preserve items in anyOf branch\n                    array_schema = schema.copy()\n                    top_level_fields = [\"default\", \"description\", \"title\", \"example\"]\n                    nullable_schema = {}\n\n                    # Move top-level fields to the root\n                    for field in top_level_fields:\n                        if field in array_schema:\n                            nullable_schema[field] = array_schema.pop(field)\n\n                    nullable_schema[\"anyOf\"] = [array_schema, {\"type\": \"null\"}]\n                    return nullable_schema\n\n                elif original_type == \"object\" and \"properties\" in schema:\n                    # Object with properties - preserve properties in anyOf branch\n                    object_schema = schema.copy()\n                    top_level_fields = [\"default\", \"description\", \"title\", \"example\"]\n                    nullable_schema = {}\n\n                    # Move top-level fields to the root\n                    for field in top_level_fields:\n                        if field in object_schema:\n                            nullable_schema[field] = object_schema.pop(field)\n\n                    nullable_schema[\"anyOf\"] = [object_schema, {\"type\": \"null\"}]\n                    return nullable_schema\n                else:\n                    # Simple object/array without items/properties\n                    nullable_schema = {}\n                    original_schema = schema.copy()\n                    top_level_fields = [\"default\", \"description\", \"title\", \"example\"]\n\n                    for field in top_level_fields:\n                        if field in original_schema:\n                            nullable_schema[field] = original_schema.pop(field)\n\n                    nullable_schema[\"anyOf\"] = [original_schema, {\"type\": \"null\"}]\n                    return nullable_schema\n            else:\n                # Simple types (string, integer, number, boolean)\n                top_level_fields = [\"default\", \"description\", \"title\", \"example\"]\n                nullable_schema = {}\n                original_schema = schema.copy()\n\n                for field in top_level_fields:\n                    if field in original_schema:\n                        nullable_schema[field] = original_schema.pop(field)\n\n                nullable_schema[\"anyOf\"] = [original_schema, {\"type\": \"null\"}]\n                return nullable_schema\n\n    return schema\n\n\ndef _combine_schemas_and_map_params(\n    route: HTTPRoute,\n    convert_refs: bool = True,\n) -> tuple[dict[str, Any], dict[str, dict[str, str]]]:\n    \"\"\"\n    Combines parameter and request body schemas into a single schema.\n    Handles parameter name collisions by adding location suffixes.\n    Also returns parameter mapping for request director.\n\n    Args:\n        route: HTTPRoute object\n\n    Returns:\n        Tuple of (combined schema dictionary, parameter mapping)\n        Parameter mapping format: {'flat_arg_name': {'location': 'path', 'openapi_name': 'id'}}\n    \"\"\"\n    properties = {}\n    required = []\n    parameter_map = {}  # Track mapping from flat arg names to OpenAPI locations\n\n    # First pass: collect parameter names by location and body properties\n    param_names_by_location = {\n        \"path\": set(),\n        \"query\": set(),\n        \"header\": set(),\n        \"cookie\": set(),\n    }\n    body_props = {}\n\n    for param in route.parameters:\n        param_names_by_location[param.location].add(param.name)\n\n    if route.request_body and route.request_body.content_schema:\n        content_type = next(iter(route.request_body.content_schema))\n\n        # Convert refs if needed\n        if convert_refs:\n            body_schema = _replace_ref_with_defs(\n                route.request_body.content_schema[content_type]\n            )\n        else:\n            body_schema = route.request_body.content_schema[content_type]\n\n        if route.request_body.description and not body_schema.get(\"description\"):\n            body_schema[\"description\"] = route.request_body.description\n\n        # Handle allOf at the top level by merging all schemas\n        if \"allOf\" in body_schema and isinstance(body_schema[\"allOf\"], list):\n            merged_props = {}\n            merged_required = []\n\n            for sub_schema in body_schema[\"allOf\"]:\n                if isinstance(sub_schema, dict):\n                    # Merge properties\n                    if \"properties\" in sub_schema:\n                        merged_props.update(sub_schema[\"properties\"])\n                    # Merge required fields\n                    if \"required\" in sub_schema:\n                        merged_required.extend(sub_schema[\"required\"])\n\n            # Update body_schema with merged properties\n            body_schema[\"properties\"] = merged_props\n            if merged_required:\n                # Remove duplicates while preserving order\n                seen = set()\n                body_schema[\"required\"] = [\n                    x for x in merged_required if not (x in seen or seen.add(x))\n                ]\n            # Remove the allOf since we've merged it\n            body_schema.pop(\"allOf\", None)\n\n        body_props = body_schema.get(\"properties\", {})\n\n    # Detect collisions: parameters that exist in both body and path/query/header\n    all_non_body_params = set()\n    for location_params in param_names_by_location.values():\n        all_non_body_params.update(location_params)\n\n    body_param_names = set(body_props.keys())\n    colliding_params = all_non_body_params & body_param_names\n\n    # Add parameters with suffixes for collisions\n    for param in route.parameters:\n        if param.name in colliding_params:\n            # Add suffix for non-body parameters when collision detected\n            suffixed_name = f\"{param.name}__{param.location}\"\n            if param.required:\n                required.append(suffixed_name)\n\n            # Track parameter mapping\n            parameter_map[suffixed_name] = {\n                \"location\": param.location,\n                \"openapi_name\": param.name,\n            }\n\n            # Convert refs if needed\n            if convert_refs:\n                param_schema = _replace_ref_with_defs(param.schema_, param.description)\n            else:\n                param_schema = param.schema_.copy()\n                if param.description and not param_schema.get(\"description\"):\n                    param_schema[\"description\"] = param.description\n            original_desc = param_schema.get(\"description\", \"\")\n            location_desc = f\"({param.location.capitalize()} parameter)\"\n            if original_desc:\n                param_schema[\"description\"] = f\"{original_desc} {location_desc}\"\n            else:\n                param_schema[\"description\"] = location_desc\n\n            # Don't make optional parameters nullable - they can simply be omitted\n            # The OpenAPI specification doesn't require optional parameters to accept null values\n\n            properties[suffixed_name] = param_schema\n        else:\n            # No collision, use original name\n            if param.required:\n                required.append(param.name)\n\n            # Track parameter mapping\n            parameter_map[param.name] = {\n                \"location\": param.location,\n                \"openapi_name\": param.name,\n            }\n\n            # Convert refs if needed\n            if convert_refs:\n                param_schema = _replace_ref_with_defs(param.schema_, param.description)\n            else:\n                param_schema = param.schema_.copy()\n                if param.description and not param_schema.get(\"description\"):\n                    param_schema[\"description\"] = param.description\n\n            # Don't make optional parameters nullable - they can simply be omitted\n            # The OpenAPI specification doesn't require optional parameters to accept null values\n\n            properties[param.name] = param_schema\n\n    # Add request body properties (no suffixes for body parameters)\n    if route.request_body and route.request_body.content_schema:\n        # If body is just a $ref, we need to handle it differently\n        if \"$ref\" in body_schema and not body_props:\n            # The entire body is a reference to a schema\n            # We need to expand this inline or keep the ref\n            # For simplicity, we'll keep it as a single property\n            properties[\"body\"] = body_schema\n            if route.request_body.required:\n                required.append(\"body\")\n            parameter_map[\"body\"] = {\"location\": \"body\", \"openapi_name\": \"body\"}\n        elif body_props:\n            # Normal case: body has properties\n            for prop_name, prop_schema in body_props.items():\n                properties[prop_name] = prop_schema\n\n                # Track parameter mapping for body properties\n                parameter_map[prop_name] = {\n                    \"location\": \"body\",\n                    \"openapi_name\": prop_name,\n                }\n\n            if route.request_body.required:\n                required.extend(body_schema.get(\"required\", []))\n        else:\n            # Handle direct array/primitive schemas (like list[str] parameters from FastAPI)\n            # Use the schema title as parameter name, fall back to generic name\n            param_name = body_schema.get(\"title\", \"body\").lower()\n\n            # Clean the parameter name to be valid\n            import re\n\n            param_name = re.sub(r\"[^a-zA-Z0-9_]\", \"_\", param_name)\n            if not param_name or param_name[0].isdigit():\n                param_name = \"body_data\"\n\n            properties[param_name] = body_schema\n            if route.request_body.required:\n                required.append(param_name)\n            parameter_map[param_name] = {\"location\": \"body\", \"openapi_name\": param_name}\n\n    result = {\n        \"type\": \"object\",\n        \"properties\": properties,\n        \"required\": required,\n    }\n    # Add schema definitions if available\n    schema_defs = route.request_schemas\n    if schema_defs:\n        if convert_refs:\n            # Need to convert refs and prune\n            all_defs = schema_defs.copy()\n            # Convert each schema definition recursively\n            for name, schema in all_defs.items():\n                if isinstance(schema, dict):\n                    all_defs[name] = _replace_ref_with_defs(schema)\n\n            # Prune to only needed schemas\n            used_refs = set()\n\n            def find_refs_in_value(value):\n                \"\"\"Recursively find all $ref references.\"\"\"\n                if isinstance(value, dict):\n                    if \"$ref\" in value and isinstance(value[\"$ref\"], str):\n                        ref = value[\"$ref\"]\n                        if ref.startswith(\"#/$defs/\"):\n                            used_refs.add(ref.split(\"/\")[-1])\n                    for v in value.values():\n                        find_refs_in_value(v)\n                elif isinstance(value, list):\n                    for item in value:\n                        find_refs_in_value(item)\n\n            # Find refs in properties\n            find_refs_in_value(properties)\n\n            # Collect transitive dependencies\n            if used_refs:\n                collected_all = False\n                while not collected_all:\n                    initial_count = len(used_refs)\n                    for name in list(used_refs):\n                        if name in all_defs:\n                            find_refs_in_value(all_defs[name])\n                    collected_all = len(used_refs) == initial_count\n\n                result[\"$defs\"] = {\n                    name: def_schema\n                    for name, def_schema in all_defs.items()\n                    if name in used_refs\n                }\n        else:\n            # From parser - already converted and pruned\n            result[\"$defs\"] = schema_defs\n\n    return result, parameter_map\n\n\ndef _combine_schemas(route: HTTPRoute) -> dict[str, Any]:\n    \"\"\"\n    Combines parameter and request body schemas into a single schema.\n    Handles parameter name collisions by adding location suffixes.\n\n    This is a backward compatibility wrapper around _combine_schemas_and_map_params.\n\n    Args:\n        route: HTTPRoute object\n\n    Returns:\n        Combined schema dictionary\n    \"\"\"\n    schema, _ = _combine_schemas_and_map_params(route)\n    return schema\n\n\ndef extract_output_schema_from_responses(\n    responses: dict[str, ResponseInfo],\n    schema_definitions: dict[str, Any] | None = None,\n    openapi_version: str | None = None,\n) -> dict[str, Any] | None:\n    \"\"\"\n    Extract output schema from OpenAPI responses for use as MCP tool output schema.\n\n    This function finds the first successful response (200, 201, 202, 204) with a\n    JSON-compatible content type and extracts its schema. If the schema is not an\n    object type, it wraps it to comply with MCP requirements.\n\n    Args:\n        responses: Dictionary of ResponseInfo objects keyed by status code\n        schema_definitions: Optional schema definitions to include in the output schema\n        openapi_version: OpenAPI version string, used to optimize nullable field handling\n\n    Returns:\n        dict: MCP-compliant output schema with potential wrapping, or None if no suitable schema found\n    \"\"\"\n    if not responses:\n        return None\n\n    # Priority order for success status codes\n    success_codes = [\"200\", \"201\", \"202\", \"204\"]\n\n    # Find the first successful response\n    response_info = None\n    for status_code in success_codes:\n        if status_code in responses:\n            response_info = responses[status_code]\n            break\n\n    # If no explicit success codes, try any 2xx response\n    if response_info is None:\n        for status_code, resp_info in responses.items():\n            if status_code.startswith(\"2\"):\n                response_info = resp_info\n                break\n\n    if response_info is None or not response_info.content_schema:\n        return None\n\n    # Prefer application/json, then fall back to other JSON-compatible types\n    json_compatible_types = [\n        \"application/json\",\n        \"application/vnd.api+json\",\n        \"application/hal+json\",\n        \"application/ld+json\",\n        \"text/json\",\n    ]\n\n    schema = None\n    for content_type in json_compatible_types:\n        if content_type in response_info.content_schema:\n            schema = response_info.content_schema[content_type]\n            break\n\n    # If no JSON-compatible type found, try the first available content type\n    if schema is None and response_info.content_schema:\n        first_content_type = next(iter(response_info.content_schema))\n        schema = response_info.content_schema[first_content_type]\n        logger.debug(\n            f\"Using non-JSON content type for output schema: {first_content_type}\"\n        )\n\n    if not schema or not isinstance(schema, dict):\n        return None\n\n    # Convert refs if needed\n    output_schema = _replace_ref_with_defs(schema)\n\n    # If schema has a $ref, resolve it first before processing nullable fields\n    if \"$ref\" in output_schema and schema_definitions:\n        ref_path = output_schema[\"$ref\"]\n        if ref_path.startswith(\"#/$defs/\"):\n            schema_name = ref_path.split(\"/\")[-1]\n            if schema_name in schema_definitions:\n                # Replace $ref with the actual schema definition\n                output_schema = _replace_ref_with_defs(schema_definitions[schema_name])\n\n    if openapi_version and openapi_version.startswith(\"3\"):\n        # Convert OpenAPI 3.x schema to JSON Schema format for proper handling\n        # of constructs like oneOf, anyOf, and nullable fields\n        from .json_schema_converter import convert_openapi_schema_to_json_schema\n\n        output_schema = convert_openapi_schema_to_json_schema(\n            output_schema, openapi_version\n        )\n\n    # MCP requires output schemas to be objects. If this schema is not an object,\n    # we need to wrap it similar to how ParsedFunction.from_function() does it\n    if output_schema.get(\"type\") != \"object\":\n        # Create a wrapped schema that contains the original schema under a \"result\" key\n        wrapped_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"result\": output_schema},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n        output_schema = wrapped_schema\n\n    # Add schema definitions if available\n    if schema_definitions:\n        # Convert refs if needed\n        processed_defs = schema_definitions.copy()\n        # Convert each schema definition recursively\n        for name, schema in processed_defs.items():\n            if isinstance(schema, dict):\n                processed_defs[name] = _replace_ref_with_defs(schema)\n\n        # Convert OpenAPI schema definitions to JSON Schema format if needed\n        if openapi_version and openapi_version.startswith(\"3\"):\n            from .json_schema_converter import convert_openapi_schema_to_json_schema\n\n            for def_name in list(processed_defs.keys()):\n                processed_defs[def_name] = convert_openapi_schema_to_json_schema(\n                    processed_defs[def_name], openapi_version\n                )\n\n        output_schema[\"$defs\"] = processed_defs\n\n    return output_schema\n\n\n# Export public symbols\n__all__ = [\n    \"_combine_schemas\",\n    \"_combine_schemas_and_map_params\",\n    \"_make_optional_parameter_nullable\",\n    \"clean_schema_for_display\",\n    \"extract_output_schema_from_responses\",\n]\n"
  },
  {
    "path": "src/fastmcp/utilities/pagination.py",
    "content": "\"\"\"Pagination utilities for MCP list operations.\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport binascii\nimport json\nfrom collections.abc import Sequence\nfrom dataclasses import dataclass\nfrom typing import TypeVar\n\nT = TypeVar(\"T\")\n\n\n@dataclass\nclass CursorState:\n    \"\"\"Internal representation of pagination cursor state.\n\n    The cursor encodes the offset into the result set. This is opaque to clients\n    per the MCP spec - they should not parse or modify cursors.\n    \"\"\"\n\n    offset: int\n\n    def encode(self) -> str:\n        \"\"\"Encode cursor state to an opaque string.\"\"\"\n        data = json.dumps({\"o\": self.offset})\n        return base64.urlsafe_b64encode(data.encode()).decode()\n\n    @classmethod\n    def decode(cls, cursor: str) -> CursorState:\n        \"\"\"Decode cursor from an opaque string.\n\n        Raises:\n            ValueError: If the cursor is invalid or malformed.\n        \"\"\"\n        try:\n            data = json.loads(base64.urlsafe_b64decode(cursor.encode()).decode())\n            return cls(offset=data[\"o\"])\n        except (\n            json.JSONDecodeError,\n            KeyError,\n            ValueError,\n            TypeError,\n            binascii.Error,\n        ) as e:\n            raise ValueError(f\"Invalid cursor: {cursor}\") from e\n\n\ndef paginate_sequence(\n    items: Sequence[T],\n    cursor: str | None,\n    page_size: int,\n) -> tuple[list[T], str | None]:\n    \"\"\"Paginate a sequence of items.\n\n    Args:\n        items: The full sequence to paginate.\n        cursor: Optional cursor from a previous request. None for first page.\n        page_size: Maximum number of items per page.\n\n    Returns:\n        Tuple of (page_items, next_cursor). next_cursor is None if no more pages.\n\n    Raises:\n        ValueError: If the cursor is invalid.\n    \"\"\"\n    offset = 0\n    if cursor:\n        state = CursorState.decode(cursor)\n        offset = state.offset\n\n    end = offset + page_size\n    page = list(items[offset:end])\n\n    next_cursor = None\n    if end < len(items):\n        next_cursor = CursorState(offset=end).encode()\n\n    return page, next_cursor\n"
  },
  {
    "path": "src/fastmcp/utilities/skills.py",
    "content": "\"\"\"Client utilities for discovering and downloading skills from MCP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport mcp.types\n\nif TYPE_CHECKING:\n    from fastmcp.client import Client\n\n\n@dataclass\nclass SkillSummary:\n    \"\"\"Summary information about a skill available on a server.\"\"\"\n\n    name: str\n    description: str\n    uri: str\n\n\n@dataclass\nclass SkillFile:\n    \"\"\"Information about a file within a skill.\"\"\"\n\n    path: str\n    size: int\n    hash: str\n\n\n@dataclass\nclass SkillManifest:\n    \"\"\"Full manifest of a skill including all files.\"\"\"\n\n    name: str\n    files: list[SkillFile]\n\n\nasync def list_skills(client: Client) -> list[SkillSummary]:\n    \"\"\"List all available skills from an MCP server.\n\n    Discovers skills by finding resources with URIs matching the\n    `skill://{name}/SKILL.md` pattern.\n\n    Args:\n        client: Connected FastMCP client\n\n    Returns:\n        List of SkillSummary objects with name, description, and URI\n\n    Example:\n        ```python\n        from fastmcp import Client\n        from fastmcp.utilities.skills import list_skills\n\n        async with Client(\"http://skills-server/mcp\") as client:\n            skills = await list_skills(client)\n            for skill in skills:\n                print(f\"{skill.name}: {skill.description}\")\n        ```\n    \"\"\"\n    resources = await client.list_resources()\n    skills = []\n\n    for resource in resources:\n        uri = str(resource.uri)\n        # Match skill://{name}/SKILL.md pattern\n        if uri.startswith(\"skill://\") and uri.endswith(\"/SKILL.md\"):\n            # Extract skill name from URI\n            path_part = uri[len(\"skill://\") :]\n            name = path_part.rsplit(\"/\", 1)[0]\n            skills.append(\n                SkillSummary(\n                    name=name,\n                    description=resource.description or \"\",\n                    uri=uri,\n                )\n            )\n\n    return skills\n\n\nasync def get_skill_manifest(client: Client, skill_name: str) -> SkillManifest:\n    \"\"\"Get the manifest for a specific skill.\n\n    Args:\n        client: Connected FastMCP client\n        skill_name: Name of the skill\n\n    Returns:\n        SkillManifest with file listing\n\n    Raises:\n        ValueError: If manifest cannot be read or parsed\n    \"\"\"\n    manifest_uri = f\"skill://{skill_name}/_manifest\"\n    result = await client.read_resource(manifest_uri)\n\n    if not result:\n        raise ValueError(f\"Could not read manifest for skill: {skill_name}\")\n\n    content = result[0]\n    if isinstance(content, mcp.types.TextResourceContents):\n        try:\n            manifest_data = json.loads(content.text)\n        except json.JSONDecodeError as e:\n            raise ValueError(f\"Invalid manifest JSON for skill: {skill_name}\") from e\n    else:\n        raise ValueError(f\"Unexpected manifest format for skill: {skill_name}\")\n\n    try:\n        return SkillManifest(\n            name=manifest_data[\"skill\"],\n            files=[\n                SkillFile(path=f[\"path\"], size=f[\"size\"], hash=f[\"hash\"])\n                for f in manifest_data[\"files\"]\n            ],\n        )\n    except (KeyError, TypeError) as e:\n        raise ValueError(f\"Invalid manifest format for skill: {skill_name}\") from e\n\n\nasync def download_skill(\n    client: Client,\n    skill_name: str,\n    target_dir: str | Path,\n    *,\n    overwrite: bool = False,\n) -> Path:\n    \"\"\"Download a skill and all its files to a local directory.\n\n    Creates a subdirectory named after the skill containing all files.\n\n    Args:\n        client: Connected FastMCP client\n        skill_name: Name of the skill to download\n        target_dir: Directory where skill folder will be created\n        overwrite: If True, overwrite existing skill directory. If False\n            (default), raise FileExistsError if directory exists.\n\n    Returns:\n        Path to the downloaded skill directory\n\n    Raises:\n        ValueError: If skill cannot be found or downloaded\n        FileExistsError: If skill directory exists and overwrite=False\n\n    Example:\n        ```python\n        from fastmcp import Client\n        from fastmcp.utilities.skills import download_skill\n\n        async with Client(\"http://skills-server/mcp\") as client:\n            skill_path = await download_skill(\n                client,\n                \"pdf-processing\",\n                \"~/.claude/skills\"\n            )\n            print(f\"Downloaded to: {skill_path}\")\n        ```\n    \"\"\"\n    target_dir = Path(target_dir).expanduser().resolve()\n    skill_dir = (target_dir / skill_name).resolve()\n\n    # Security: ensure skill_dir stays within target_dir\n    if not skill_dir.is_relative_to(target_dir):\n        raise ValueError(f\"Skill name {skill_name!r} would escape the target directory\")\n\n    # Check if directory exists\n    if skill_dir.exists() and not overwrite:\n        raise FileExistsError(\n            f\"Skill directory already exists: {skill_dir}. \"\n            \"Use overwrite=True to replace.\"\n        )\n\n    # Get manifest to know what files to download\n    manifest = await get_skill_manifest(client, skill_name)\n\n    # Create skill directory\n    skill_dir.mkdir(parents=True, exist_ok=True)\n\n    # Download each file\n    for file_info in manifest.files:\n        # Security: reject absolute paths and paths that escape skill_dir\n        if Path(file_info.path).is_absolute():\n            continue\n        file_path = (skill_dir / file_info.path).resolve()\n        if not file_path.is_relative_to(skill_dir):\n            continue\n\n        file_uri = f\"skill://{skill_name}/{file_info.path}\"\n        result = await client.read_resource(file_uri)\n\n        if not result:\n            continue\n\n        content = result[0]\n\n        # Create parent directories if needed\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n\n        # Write content\n        if isinstance(content, mcp.types.TextResourceContents):\n            file_path.write_text(content.text)\n        elif isinstance(content, mcp.types.BlobResourceContents):\n            file_path.write_bytes(base64.b64decode(content.blob))\n        else:\n            # Skip unknown content types\n            continue\n\n    return skill_dir\n\n\nasync def sync_skills(\n    client: Client,\n    target_dir: str | Path,\n    *,\n    overwrite: bool = False,\n) -> list[Path]:\n    \"\"\"Download all available skills from a server.\n\n    Args:\n        client: Connected FastMCP client\n        target_dir: Directory where skill folders will be created\n        overwrite: If True, overwrite existing files\n\n    Returns:\n        List of paths to downloaded skill directories\n\n    Example:\n        ```python\n        from fastmcp import Client\n        from fastmcp.utilities.skills import sync_skills\n\n        async with Client(\"http://skills-server/mcp\") as client:\n            paths = await sync_skills(client, \"~/.claude/skills\")\n            print(f\"Downloaded {len(paths)} skills\")\n        ```\n    \"\"\"\n    skills = await list_skills(client)\n    downloaded = []\n\n    for skill in skills:\n        try:\n            path = await download_skill(\n                client, skill.name, target_dir, overwrite=overwrite\n            )\n            downloaded.append(path)\n        except FileExistsError:\n            # Skip existing skills when not overwriting\n            continue\n\n    return downloaded\n"
  },
  {
    "path": "src/fastmcp/utilities/tests.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport multiprocessing\nimport socket\nimport time\nfrom collections.abc import AsyncGenerator, Callable, Generator\nfrom contextlib import asynccontextmanager, contextmanager, suppress\nfrom typing import TYPE_CHECKING, Any, Literal\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nimport uvicorn\n\nfrom fastmcp import settings\nfrom fastmcp.client.auth.oauth import OAuth\nfrom fastmcp.utilities.http import find_available_port\n\nif TYPE_CHECKING:\n    from fastmcp.server.server import FastMCP\n\n\n@contextmanager\ndef temporary_settings(**kwargs: Any):\n    \"\"\"\n    Temporarily override FastMCP setting values.\n\n    Args:\n        **kwargs: The settings to override, including nested settings.\n\n    Example:\n        Temporarily override a setting:\n        ```python\n        import fastmcp\n        from fastmcp.utilities.tests import temporary_settings\n\n        with temporary_settings(log_level='DEBUG'):\n            assert fastmcp.settings.log_level == 'DEBUG'\n        assert fastmcp.settings.log_level == 'INFO'\n        ```\n    \"\"\"\n    old_settings = copy.deepcopy(settings)\n\n    try:\n        # apply the new settings\n        for attr, value in kwargs.items():\n            settings.set_setting(attr, value)\n        yield\n\n    finally:\n        # restore the old settings\n        for attr in kwargs:\n            settings.set_setting(attr, old_settings.get_setting(attr))\n\n\ndef _run_server(mcp_server: FastMCP, transport: Literal[\"sse\"], port: int) -> None:\n    # Some Starlette apps are not pickleable, so we need to create them here based on the indicated transport\n    if transport == \"sse\":\n        app = mcp_server.http_app(transport=\"sse\")\n    else:\n        raise ValueError(f\"Invalid transport: {transport}\")\n    uvicorn_server = uvicorn.Server(\n        config=uvicorn.Config(\n            app=app,\n            host=\"127.0.0.1\",\n            port=port,\n            log_level=\"error\",\n            ws=\"websockets-sansio\",\n        )\n    )\n    uvicorn_server.run()\n\n\n@contextmanager\ndef run_server_in_process(\n    server_fn: Callable[..., None],\n    *args: Any,\n    provide_host_and_port: bool = True,\n    host: str = \"127.0.0.1\",\n    port: int | None = None,\n    **kwargs: Any,\n) -> Generator[str, None, None]:\n    \"\"\"\n    Context manager that runs a FastMCP server in a separate process and\n    returns the server URL. When the context manager is exited, the server process is killed.\n\n    Args:\n        server_fn: The function that runs a FastMCP server. FastMCP servers are\n            not pickleable, so we need a function that creates and runs one.\n        *args: Arguments to pass to the server function.\n        provide_host_and_port: Whether to provide the host and port to the server function as kwargs.\n        host: Host to bind the server to (default: \"127.0.0.1\").\n        port: Port to bind the server to (default: find available port).\n        **kwargs: Keyword arguments to pass to the server function.\n\n    Returns:\n        The server URL.\n    \"\"\"\n    # Use provided port or find an available one\n    if port is None:\n        port = find_available_port()\n\n    if provide_host_and_port:\n        kwargs |= {\"host\": host, \"port\": port}\n\n    proc = multiprocessing.Process(\n        target=server_fn, args=args, kwargs=kwargs, daemon=True\n    )\n    proc.start()\n\n    # Wait for server to be running\n    max_attempts = 30\n    attempt = 0\n    while attempt < max_attempts and proc.is_alive():\n        try:\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                s.connect((host, port))\n                break\n        except ConnectionRefusedError:\n            if attempt < 5:\n                time.sleep(0.05)\n            elif attempt < 15:\n                time.sleep(0.1)\n            else:\n                time.sleep(0.2)\n            attempt += 1\n    else:\n        raise RuntimeError(f\"Server failed to start after {max_attempts} attempts\")\n\n    yield f\"http://{host}:{port}\"\n\n    proc.terminate()\n    proc.join(timeout=5)\n    if proc.is_alive():\n        # If it's still alive, then force kill it\n        proc.kill()\n        proc.join(timeout=2)\n        if proc.is_alive():\n            raise RuntimeError(\"Server process failed to terminate even after kill\")\n\n\n@asynccontextmanager\nasync def run_server_async(\n    server: FastMCP,\n    port: int | None = None,\n    transport: Literal[\"http\", \"streamable-http\", \"sse\"] = \"http\",\n    path: str = \"/mcp\",\n    host: str = \"127.0.0.1\",\n) -> AsyncGenerator[str, None]:\n    \"\"\"\n    Start a FastMCP server as an asyncio task for in-process async testing.\n\n    This is the recommended way to test FastMCP servers. It runs the server\n    as an async task in the same process, eliminating subprocess coordination,\n    sleeps, and cleanup issues.\n\n    Args:\n        server: FastMCP server instance\n        port: Port to bind to (default: find available port)\n        transport: Transport type (\"http\", \"streamable-http\", or \"sse\")\n        path: URL path for the server (default: \"/mcp\")\n        host: Host to bind to (default: \"127.0.0.1\")\n\n    Yields:\n        Server URL string\n\n    Example:\n        ```python\n        import pytest\n        from fastmcp import FastMCP, Client\n        from fastmcp.client.transports import StreamableHttpTransport\n        from fastmcp.utilities.tests import run_server_async\n\n        @pytest.fixture\n        async def server():\n            mcp = FastMCP(\"test\")\n\n            @mcp.tool()\n            def greet(name: str) -> str:\n                return f\"Hello, {name}!\"\n\n            async with run_server_async(mcp) as url:\n                yield url\n\n        async def test_greet(server: str):\n            async with Client(StreamableHttpTransport(server)) as client:\n                result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n                assert result.content[0].text == \"Hello, World!\"\n        ```\n    \"\"\"\n    import asyncio\n\n    if port is None:\n        port = find_available_port()\n\n    # Wait a tiny bit for the port to be released if it was just used\n    await asyncio.sleep(0.01)\n\n    # Start server as a background task\n    server_task = asyncio.create_task(\n        server.run_http_async(\n            host=host,\n            port=port,\n            transport=transport,\n            path=path,\n            show_banner=False,\n        )\n    )\n\n    # Wait for server lifespan to be ready\n    await server._started.wait()\n\n    # Give uvicorn a moment to bind the port after lifespan is ready\n    await asyncio.sleep(0.1)\n\n    try:\n        yield f\"http://{host}:{port}{path}\"\n    finally:\n        # Cleanup: cancel the task with timeout to avoid hanging on Windows\n        server_task.cancel()\n        with suppress(asyncio.CancelledError, asyncio.TimeoutError):\n            await asyncio.wait_for(server_task, timeout=2.0)\n\n\nclass HeadlessOAuth(OAuth):\n    \"\"\"\n    OAuth provider that bypasses browser interaction for testing.\n\n    This simulates the complete OAuth flow programmatically by making HTTP requests\n    instead of opening a browser and running a callback server. Useful for automated testing.\n    \"\"\"\n\n    def __init__(self, mcp_url: str, **kwargs):\n        \"\"\"Initialize HeadlessOAuth with stored response tracking.\"\"\"\n        self._stored_response = None\n        super().__init__(mcp_url, **kwargs)\n\n    async def redirect_handler(self, authorization_url: str) -> None:\n        \"\"\"Make HTTP request to authorization URL and store response for callback handler.\"\"\"\n        async with httpx.AsyncClient() as client:\n            response = await client.get(authorization_url, follow_redirects=False)\n            self._stored_response = response\n\n    async def callback_handler(self) -> tuple[str, str | None]:\n        \"\"\"Parse stored response and return (auth_code, state).\"\"\"\n        if not self._stored_response:\n            raise RuntimeError(\n                \"No authorization response stored. redirect_handler must be called first.\"\n            )\n\n        response = self._stored_response\n\n        # Extract auth code from redirect location\n        if response.status_code == 302:\n            redirect_url = response.headers[\"location\"]\n            parsed = urlparse(redirect_url)\n            query_params = parse_qs(parsed.query)\n\n            if \"error\" in query_params:\n                error = query_params[\"error\"][0]\n                error_desc = query_params.get(\"error_description\", [\"Unknown error\"])[0]\n                raise RuntimeError(\n                    f\"OAuth authorization failed: {error} - {error_desc}\"\n                )\n\n            auth_code = query_params[\"code\"][0]\n            state = query_params.get(\"state\", [None])[0]\n            return auth_code, state\n        else:\n            raise RuntimeError(f\"Authorization failed: {response.status_code}\")\n"
  },
  {
    "path": "src/fastmcp/utilities/timeout.py",
    "content": "\"\"\"Timeout normalization utilities.\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime\n\n\ndef normalize_timeout_to_timedelta(\n    value: int | float | datetime.timedelta | None,\n) -> datetime.timedelta | None:\n    \"\"\"Normalize a timeout value to a timedelta.\n\n    Args:\n        value: Timeout value as int/float (seconds), timedelta, or None\n\n    Returns:\n        timedelta if value provided, None otherwise\n    \"\"\"\n    if value is None:\n        return None\n    if isinstance(value, datetime.timedelta):\n        return value\n    if isinstance(value, int | float):\n        return datetime.timedelta(seconds=float(value))\n    raise TypeError(f\"Invalid timeout type: {type(value)}\")\n\n\ndef normalize_timeout_to_seconds(\n    value: int | float | datetime.timedelta | None,\n) -> float | None:\n    \"\"\"Normalize a timeout value to seconds (float).\n\n    Args:\n        value: Timeout value as int/float (seconds), timedelta, or None.\n            Zero values are treated as \"disabled\" and return None.\n\n    Returns:\n        float seconds if value provided and non-zero, None otherwise\n    \"\"\"\n    if value is None:\n        return None\n    if isinstance(value, datetime.timedelta):\n        seconds = value.total_seconds()\n        return None if seconds == 0 else seconds\n    if isinstance(value, int | float):\n        return None if value == 0 else float(value)\n    raise TypeError(f\"Invalid timeout type: {type(value)}\")\n"
  },
  {
    "path": "src/fastmcp/utilities/token_cache.py",
    "content": "\"\"\"In-memory cache for token verification results.\n\nProvides a generic TTL-based cache for ``AccessToken`` objects, designed to\nreduce repeated network calls during opaque-token verification.  Only\n*successful* verifications should be cached; errors and failures must be\nretried on every request.\n\nExample:\n    ```python\n    from fastmcp.utilities.token_cache import TokenCache\n\n    cache = TokenCache(ttl_seconds=300, max_size=10000)\n\n    # On cache miss, call the upstream verifier and store the result.\n    hit, token = cache.get(raw_token)\n    if not hit:\n        token = await _call_upstream(raw_token)\n        if token is not None:\n            cache.set(raw_token, token)\n    ```\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport time\nfrom dataclasses import dataclass\n\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nDEFAULT_MAX_CACHE_SIZE = 10_000\n_CLEANUP_INTERVAL = 60  # seconds between periodic sweeps\n\n\n@dataclass\nclass _CacheEntry:\n    \"\"\"A cached token result with its absolute expiration timestamp.\"\"\"\n\n    result: AccessToken\n    expires_at: float\n\n\nclass TokenCache:\n    \"\"\"TTL-based in-memory cache for ``AccessToken`` objects.\n\n    Features:\n    - SHA-256 hashed cache keys (fixed size, regardless of token length).\n    - Per-entry TTL that respects both the configured ``ttl_seconds`` and the\n      token's own ``expires_at`` claim (whichever is sooner).\n    - Bounded size with FIFO eviction when the cache is full.\n    - Periodic cleanup of expired entries to prevent unbounded growth.\n    - Defensive deep copies on both store and retrieve to prevent\n      callers from mutating cached values.\n\n    Caching is disabled when ``ttl_seconds`` is ``None`` or ``0``, or\n    when ``max_size`` is ``0``.  Negative values raise ``ValueError``.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        ttl_seconds: int | None = None,\n        max_size: int | None = None,\n    ) -> None:\n        \"\"\"Initialise the cache.\n\n        Args:\n            ttl_seconds: How long cached entries remain valid, in seconds.\n                ``None`` or ``0`` disables caching entirely.\n            max_size: Upper bound on the number of entries.  When the limit is\n                reached, expired entries are swept first; if still full the\n                oldest entry is evicted.  Defaults to 10 000.\n        \"\"\"\n        if ttl_seconds is not None and ttl_seconds < 0:\n            raise ValueError(\n                f\"cache_ttl_seconds must be non-negative, got {ttl_seconds}\"\n            )\n        if max_size is not None and max_size < 0:\n            raise ValueError(f\"max_cache_size must be non-negative, got {max_size}\")\n        self._ttl = ttl_seconds or 0\n        self._max_size = max_size if max_size is not None else DEFAULT_MAX_CACHE_SIZE\n        self._entries: dict[str, _CacheEntry] = {}\n        self._last_cleanup = time.monotonic()\n\n    @property\n    def enabled(self) -> bool:\n        \"\"\"Return whether caching is active.\"\"\"\n        return self._ttl > 0 and self._max_size > 0\n\n    # -- public API ----------------------------------------------------------\n\n    def get(self, token: str) -> tuple[bool, AccessToken | None]:\n        \"\"\"Look up a cached verification result.\n\n        Returns:\n            ``(True, AccessToken)`` on a cache hit, ``(False, None)`` on a miss\n            or when caching is disabled.  The returned ``AccessToken`` is a deep\n            copy that is safe to mutate.\n        \"\"\"\n        if not self.enabled:\n            return (False, None)\n\n        cache_key = self._hash_token(token)\n        entry = self._entries.get(cache_key)\n\n        if entry is None:\n            return (False, None)\n\n        if entry.expires_at < time.time():\n            del self._entries[cache_key]\n            return (False, None)\n\n        return (True, entry.result.model_copy(deep=True))\n\n    def set(self, token: str, result: AccessToken) -> None:\n        \"\"\"Store a *successful* verification result.\n\n        Only successful verifications should be cached.  Failures (inactive\n        tokens, missing scopes, HTTP errors, timeouts) must **not** be cached\n        so that transient problems do not produce sticky false negatives.\n        \"\"\"\n        if not self.enabled:\n            return\n\n        cache_key = self._hash_token(token)\n\n        self._maybe_cleanup()\n        if cache_key not in self._entries:\n            self._enforce_size_limit()\n\n        expires_at = time.time() + self._ttl\n        if result.expires_at:\n            expires_at = min(expires_at, float(result.expires_at))\n\n        self._entries[cache_key] = _CacheEntry(\n            result=result.model_copy(deep=True),\n            expires_at=expires_at,\n        )\n\n    # -- internals -----------------------------------------------------------\n\n    @staticmethod\n    def _hash_token(token: str) -> str:\n        \"\"\"Return the SHA-256 hex digest of *token*.\"\"\"\n        return hashlib.sha256(token.encode(\"utf-8\")).hexdigest()\n\n    def _cleanup_expired(self) -> None:\n        \"\"\"Remove all entries whose TTL has elapsed.\"\"\"\n        now = time.time()\n        expired = [k for k, v in self._entries.items() if v.expires_at < now]\n        for key in expired:\n            del self._entries[key]\n        if expired:\n            logger.debug(\"Cleaned up %d expired cache entries\", len(expired))\n\n    def _maybe_cleanup(self) -> None:\n        \"\"\"Run ``_cleanup_expired`` at most once per cleanup interval.\"\"\"\n        now = time.monotonic()\n        if now - self._last_cleanup > _CLEANUP_INTERVAL:\n            self._cleanup_expired()\n            self._last_cleanup = now\n\n    def _enforce_size_limit(self) -> None:\n        \"\"\"Ensure there is room for at least one new entry.\"\"\"\n        if len(self._entries) < self._max_size:\n            return\n        self._cleanup_expired()\n        if len(self._entries) >= self._max_size:\n            oldest_key = next(iter(self._entries))\n            del self._entries[oldest_key]\n"
  },
  {
    "path": "src/fastmcp/utilities/types.py",
    "content": "\"\"\"Common types used across FastMCP.\"\"\"\n\nimport base64\nimport inspect\nimport mimetypes\nimport os\nfrom collections.abc import Callable\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom types import EllipsisType, UnionType\nfrom typing import (\n    Annotated,\n    Any,\n    Protocol,\n    TypeAlias,\n    Union,\n    get_args,\n    get_origin,\n    get_type_hints,\n)\n\nimport mcp.types\nfrom mcp.types import Annotations, ContentBlock, ModelPreferences, SamplingMessage\nfrom pydantic import AnyUrl, BaseModel, ConfigDict, Field, TypeAdapter, UrlConstraints\nfrom typing_extensions import TypeVar\n\nT = TypeVar(\"T\", default=Any)\n\n# sentinel values for optional arguments\nNotSet = ...\nNotSetT: TypeAlias = EllipsisType\n\n\ndef get_fn_name(fn: Callable[..., Any]) -> str:\n    return fn.__name__  # ty: ignore[unresolved-attribute]\n\n\nclass FastMCPBaseModel(BaseModel):\n    \"\"\"Base model for FastMCP models.\"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n\n@lru_cache(maxsize=5000)\ndef get_cached_typeadapter(cls: T) -> TypeAdapter[T]:\n    \"\"\"\n    TypeAdapters are heavy objects, and in an application context we'd typically\n    create them once in a global scope and reuse them as often as possible.\n    However, this isn't feasible for user-generated functions. Instead, we use a\n    cache to minimize the cost of creating them as much as possible.\n    \"\"\"\n    # For functions, process annotations to handle forward references and convert\n    # Annotated[Type, \"string\"] to Annotated[Type, Field(description=\"string\")]\n    if inspect.isfunction(cls) or inspect.ismethod(cls):\n        if hasattr(cls, \"__annotations__\") and cls.__annotations__:\n            try:\n                # Resolve forward references first\n                resolved_hints = get_type_hints(cls, include_extras=True)\n            except Exception:\n                # If forward reference resolution fails, use original annotations\n                resolved_hints = cls.__annotations__\n\n            # Process annotations to convert string descriptions to Fields\n            processed_hints = {}\n\n            for name, annotation in resolved_hints.items():\n                # Check if this is Annotated[Type, \"string\"] and convert to Annotated[Type, Field(description=\"string\")]\n                if (\n                    get_origin(annotation) is Annotated\n                    and len(get_args(annotation)) == 2\n                    and isinstance(get_args(annotation)[1], str)\n                ):\n                    base_type, description = get_args(annotation)\n                    processed_hints[name] = Annotated[\n                        base_type, Field(description=description)\n                    ]\n                else:\n                    processed_hints[name] = annotation\n\n            # Create new function if annotations changed\n            if processed_hints != cls.__annotations__:\n                import types\n\n                # Handle both functions and methods\n                if inspect.ismethod(cls):\n                    actual_func = cls.__func__\n                    code = actual_func.__code__  # ty: ignore[unresolved-attribute]\n                    globals_dict = actual_func.__globals__  # ty: ignore[unresolved-attribute]\n                    name = actual_func.__name__  # ty: ignore[unresolved-attribute]\n                    defaults = actual_func.__defaults__  # ty: ignore[unresolved-attribute]\n                    kwdefaults = actual_func.__kwdefaults__  # ty: ignore[unresolved-attribute]\n                    closure = actual_func.__closure__  # ty: ignore[unresolved-attribute]\n                else:\n                    code = cls.__code__\n                    globals_dict = cls.__globals__\n                    name = cls.__name__\n                    defaults = cls.__defaults__\n                    kwdefaults = cls.__kwdefaults__\n                    closure = cls.__closure__\n\n                new_func = types.FunctionType(\n                    code,\n                    globals_dict,\n                    name,\n                    defaults,\n                    closure,\n                )\n                new_func.__dict__.update(cls.__dict__)\n                new_func.__module__ = cls.__module__\n                new_func.__qualname__ = getattr(cls, \"__qualname__\", cls.__name__)\n                new_func.__annotations__ = processed_hints\n                new_func.__kwdefaults__ = kwdefaults\n\n                if inspect.ismethod(cls):\n                    new_method = types.MethodType(new_func, cls.__self__)\n                    return TypeAdapter(new_method)\n                else:\n                    return TypeAdapter(new_func)\n\n    return TypeAdapter(cls)\n\n\ndef issubclass_safe(cls: type, base: type) -> bool:\n    \"\"\"Check if cls is a subclass of base, even if cls is a type variable.\"\"\"\n    try:\n        if origin := get_origin(cls):\n            return issubclass_safe(origin, base)\n        return issubclass(cls, base)\n    except TypeError:\n        return False\n\n\ndef is_class_member_of_type(cls: Any, base: type) -> bool:\n    \"\"\"\n    Check if cls is a member of base, even if cls is a type variable.\n\n    Base can be a type, a UnionType, or an Annotated type. Generic types are not\n    considered members (e.g. T is not a member of list[T]).\n    \"\"\"\n    origin = get_origin(cls)\n    # Handle both types of unions: UnionType (from types module, used with | syntax)\n    # and typing.Union (used with Union[] syntax)\n    if origin is UnionType or origin == Union:\n        return any(is_class_member_of_type(arg, base) for arg in get_args(cls))\n    elif origin is Annotated:\n        # For Annotated[T, ...], check if T is a member of base\n        args = get_args(cls)\n        if args:\n            return is_class_member_of_type(args[0], base)\n        return False\n    else:\n        return issubclass_safe(cls, base)\n\n\ndef find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:\n    \"\"\"\n    Find the name of the kwarg that is of type kwarg_type.\n\n    Includes union types that contain the kwarg_type, as well as Annotated types.\n    \"\"\"\n    if inspect.ismethod(fn) and hasattr(fn, \"__func__\"):\n        fn = fn.__func__\n\n    # Try to get resolved type hints\n    try:\n        # Use include_extras=True to preserve Annotated metadata\n        type_hints = get_type_hints(fn, include_extras=True)\n    except Exception:\n        # If resolution fails, use raw annotations if they exist\n        type_hints = getattr(fn, \"__annotations__\", {})\n\n    sig = inspect.signature(fn)\n    for name, param in sig.parameters.items():\n        # Use resolved hint if available, otherwise raw annotation\n        annotation = type_hints.get(name, param.annotation)\n        if is_class_member_of_type(annotation, kwarg_type):\n            return name\n    return None\n\n\ndef create_function_without_params(\n    fn: Callable[..., Any], exclude_params: list[str]\n) -> Callable[..., Any]:\n    \"\"\"\n    Create a new function with the same code but without the specified parameters in annotations.\n\n    This is used to exclude parameters from type adapter processing when they can't be serialized.\n    The excluded parameters are removed from the function's __annotations__ dictionary.\n    \"\"\"\n    import types\n\n    if inspect.ismethod(fn):\n        actual_func = fn.__func__\n        code = actual_func.__code__  # ty: ignore[unresolved-attribute]\n        globals_dict = actual_func.__globals__  # ty: ignore[unresolved-attribute]\n        name = actual_func.__name__  # ty: ignore[unresolved-attribute]\n        defaults = actual_func.__defaults__  # ty: ignore[unresolved-attribute]\n        closure = actual_func.__closure__  # ty: ignore[unresolved-attribute]\n    else:\n        code = fn.__code__  # ty: ignore[unresolved-attribute]\n        globals_dict = fn.__globals__  # ty: ignore[unresolved-attribute]\n        name = fn.__name__  # ty: ignore[unresolved-attribute]\n        defaults = fn.__defaults__  # ty: ignore[unresolved-attribute]\n        closure = fn.__closure__  # ty: ignore[unresolved-attribute]\n\n    # Create a copy of annotations without the excluded parameters\n    original_annotations = getattr(fn, \"__annotations__\", {})\n    new_annotations = {\n        k: v for k, v in original_annotations.items() if k not in exclude_params\n    }\n\n    # Create new signature without the excluded parameters\n    sig = inspect.signature(fn)\n    new_params = [\n        param for name, param in sig.parameters.items() if name not in exclude_params\n    ]\n    new_sig = inspect.Signature(new_params, return_annotation=sig.return_annotation)\n\n    new_func = types.FunctionType(\n        code,\n        globals_dict,\n        name,\n        defaults,\n        closure,\n    )\n    new_func.__dict__.update(fn.__dict__)\n    new_func.__module__ = fn.__module__\n    new_func.__qualname__ = getattr(fn, \"__qualname__\", fn.__name__)  # ty: ignore[unresolved-attribute]\n    new_func.__annotations__ = new_annotations\n    new_func.__signature__ = new_sig  # type: ignore[attr-defined]\n\n    if inspect.ismethod(fn):\n        return types.MethodType(new_func, fn.__self__)\n    else:\n        return new_func\n\n\nclass Image:\n    \"\"\"Helper class for returning images from tools.\"\"\"\n\n    def __init__(\n        self,\n        path: str | Path | None = None,\n        data: bytes | None = None,\n        format: str | None = None,\n        annotations: Annotations | None = None,\n    ):\n        if path is None and data is None:\n            raise ValueError(\"Either path or data must be provided\")\n        if path is not None and data is not None:\n            raise ValueError(\"Only one of path or data can be provided\")\n\n        self.path = self._get_expanded_path(path)\n        self.data = data\n        self._format = format\n        self._mime_type = self._get_mime_type()\n        self.annotations = annotations\n\n    @staticmethod\n    def _get_expanded_path(path: str | Path | None) -> Path | None:\n        \"\"\"Expand environment variables and user home in path.\"\"\"\n        return Path(os.path.expandvars(str(path))).expanduser() if path else None\n\n    def _get_mime_type(self) -> str:\n        \"\"\"Get MIME type from format or guess from file extension.\"\"\"\n        if self._format:\n            return f\"image/{self._format.lower()}\"\n\n        if self.path:\n            # Workaround for WEBP in Py3.10\n            mimetypes.add_type(\"image/webp\", \".webp\")\n            resp = mimetypes.guess_type(self.path, strict=False)\n            if resp and resp[0] is not None:\n                return resp[0]\n            return \"application/octet-stream\"\n        return \"image/png\"  # default for raw binary data\n\n    def _get_data(self) -> str:\n        \"\"\"Get raw image data as base64-encoded string.\"\"\"\n        if self.path:\n            with open(self.path, \"rb\") as f:\n                data = base64.b64encode(f.read()).decode()\n        elif self.data is not None:\n            data = base64.b64encode(self.data).decode()\n        else:\n            raise ValueError(\"No image data available\")\n        return data\n\n    def to_image_content(\n        self,\n        mime_type: str | None = None,\n        annotations: Annotations | None = None,\n    ) -> mcp.types.ImageContent:\n        \"\"\"Convert to MCP ImageContent.\"\"\"\n        data = self._get_data()\n\n        return mcp.types.ImageContent(\n            type=\"image\",\n            data=data,\n            mimeType=mime_type or self._mime_type,\n            annotations=annotations or self.annotations,\n        )\n\n    def to_data_uri(self, mime_type: str | None = None) -> str:\n        \"\"\"Get image as a data URI.\"\"\"\n        data = self._get_data()\n        return f\"data:{mime_type or self._mime_type};base64,{data}\"\n\n\nclass Audio:\n    \"\"\"Helper class for returning audio from tools.\"\"\"\n\n    def __init__(\n        self,\n        path: str | Path | None = None,\n        data: bytes | None = None,\n        format: str | None = None,\n        annotations: Annotations | None = None,\n    ):\n        if path is None and data is None:\n            raise ValueError(\"Either path or data must be provided\")\n        if path is not None and data is not None:\n            raise ValueError(\"Only one of path or data can be provided\")\n\n        self.path = Path(os.path.expandvars(str(path))).expanduser() if path else None\n        self.data = data\n        self._format = format\n        self._mime_type = self._get_mime_type()\n        self.annotations = annotations\n\n    def _get_mime_type(self) -> str:\n        \"\"\"Get MIME type from format or guess from file extension.\"\"\"\n        if self._format:\n            return f\"audio/{self._format.lower()}\"\n\n        if self.path:\n            suffix = self.path.suffix.lower()\n            return {\n                \".wav\": \"audio/wav\",\n                \".mp3\": \"audio/mpeg\",\n                \".ogg\": \"audio/ogg\",\n                \".m4a\": \"audio/mp4\",\n                \".flac\": \"audio/flac\",\n            }.get(suffix, \"application/octet-stream\")\n        return \"audio/wav\"  # default for raw binary data\n\n    def to_audio_content(\n        self,\n        mime_type: str | None = None,\n        annotations: Annotations | None = None,\n    ) -> mcp.types.AudioContent:\n        if self.path:\n            with open(self.path, \"rb\") as f:\n                data = base64.b64encode(f.read()).decode()\n        elif self.data is not None:\n            data = base64.b64encode(self.data).decode()\n        else:\n            raise ValueError(\"No audio data available\")\n\n        return mcp.types.AudioContent(\n            type=\"audio\",\n            data=data,\n            mimeType=mime_type or self._mime_type,\n            annotations=annotations or self.annotations,\n        )\n\n\nclass File:\n    \"\"\"Helper class for returning file data from tools.\"\"\"\n\n    def __init__(\n        self,\n        path: str | Path | None = None,\n        data: bytes | None = None,\n        format: str | None = None,\n        name: str | None = None,\n        annotations: Annotations | None = None,\n    ):\n        if path is None and data is None:\n            raise ValueError(\"Either path or data must be provided\")\n        if path is not None and data is not None:\n            raise ValueError(\"Only one of path or data can be provided\")\n\n        self.path = Path(os.path.expandvars(str(path))).expanduser() if path else None\n        self.data = data\n        self._format = format\n        self._mime_type = self._get_mime_type()\n        self._name = name\n        self.annotations = annotations\n\n    def _get_mime_type(self) -> str:\n        \"\"\"Get MIME type from format or guess from file extension.\"\"\"\n        if self._format:\n            fmt = self._format.lower()\n            # Map common text formats to text/plain\n            if fmt in {\"plain\", \"txt\", \"text\"}:\n                return \"text/plain\"\n            return f\"application/{fmt}\"\n\n        if self.path:\n            mime_type, _ = mimetypes.guess_type(self.path)\n            if mime_type:\n                return mime_type\n\n        return \"application/octet-stream\"\n\n    def to_resource_content(\n        self,\n        mime_type: str | None = None,\n        annotations: Annotations | None = None,\n    ) -> mcp.types.EmbeddedResource:\n        if self.path:\n            with open(self.path, \"rb\") as f:\n                raw_data = f.read()\n                uri_str = self.path.resolve().as_uri()\n        elif self.data is not None:\n            raw_data = self.data\n            if self._name:\n                uri_str = f\"file:///{self._name}.{self._mime_type.split('/')[1]}\"\n            else:\n                uri_str = f\"file:///resource.{self._mime_type.split('/')[1]}\"\n        else:\n            raise ValueError(\"No resource data available\")\n\n        mime = mime_type or self._mime_type\n        UriType = Annotated[AnyUrl, UrlConstraints(host_required=False)]\n        uri = TypeAdapter(UriType).validate_python(uri_str)\n\n        if mime.startswith(\"text/\"):\n            try:\n                text = raw_data.decode(\"utf-8\")\n            except UnicodeDecodeError:\n                text = raw_data.decode(\"latin-1\")\n            resource = mcp.types.TextResourceContents(\n                text=text,\n                mimeType=mime,\n                uri=uri,\n            )\n        else:\n            data = base64.b64encode(raw_data).decode()\n            resource = mcp.types.BlobResourceContents(\n                blob=data,\n                mimeType=mime,\n                uri=uri,\n            )\n\n        return mcp.types.EmbeddedResource(\n            type=\"resource\",\n            resource=resource,\n            annotations=annotations or self.annotations,\n        )\n\n\ndef replace_type(type_, type_map: dict[type, type]):\n    \"\"\"\n    Given a (possibly generic, nested, or otherwise complex) type, replaces all\n    instances of old_type with new_type.\n\n    This is useful for transforming types when creating tools.\n\n    Args:\n        type_: The type to replace instances of old_type with new_type.\n        old_type: The type to replace.\n        new_type: The type to replace old_type with.\n\n    Examples:\n    ```python\n    >>> replace_type(list[int | bool], {int: str})\n    list[str | bool]\n\n    >>> replace_type(list[list[int]], {int: str})\n    list[list[str]]\n    ```\n    \"\"\"\n    if type_ in type_map:\n        return type_map[type_]\n\n    origin = get_origin(type_)\n    if not origin:\n        return type_\n\n    args = get_args(type_)\n    new_args = tuple(replace_type(arg, type_map) for arg in args)\n\n    if origin is UnionType:\n        return Union[new_args]  # noqa: UP007\n    else:\n        return origin[new_args]\n\n\nclass ContextSamplingFallbackProtocol(Protocol):\n    async def __call__(\n        self,\n        messages: str | list[str | SamplingMessage],\n        system_prompt: str | None = None,\n        temperature: float | None = None,\n        max_tokens: int | None = None,\n        model_preferences: ModelPreferences | str | list[str] | None = None,\n    ) -> ContentBlock: ...\n"
  },
  {
    "path": "src/fastmcp/utilities/ui.py",
    "content": "\"\"\"\nShared UI utilities for FastMCP HTML pages.\n\nThis module provides reusable HTML/CSS components for OAuth callbacks,\nconsent pages, and other user-facing interfaces.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport html\n\nfrom starlette.responses import HTMLResponse\n\n# FastMCP branding\nFASTMCP_LOGO_URL = \"https://gofastmcp.com/assets/brand/blue-logo.png\"\n\n# Base CSS styles shared across all FastMCP pages\nBASE_STYLES = \"\"\"\n    * {\n        margin: 0;\n        padding: 0;\n        box-sizing: border-box;\n    }\n\n    body {\n        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n        margin: 0;\n        padding: 0;\n        min-height: 100vh;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        background: #f9fafb;\n        color: #0a0a0a;\n    }\n\n    .container {\n        background: #ffffff;\n        border: 1px solid #e5e7eb;\n        padding: 3rem 2.5rem;\n        border-radius: 1rem;\n        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n        text-align: center;\n        max-width: 36rem;\n        margin: 1rem;\n        width: 100%;\n    }\n\n    @media (max-width: 640px) {\n        .container {\n            padding: 2rem 1.5rem;\n            margin: 0.5rem;\n        }\n    }\n\n    .logo {\n        width: 64px;\n        height: auto;\n        margin-bottom: 1.5rem;\n        display: block;\n        margin-left: auto;\n        margin-right: auto;\n    }\n\n    h1 {\n        font-size: 1.5rem;\n        font-weight: 600;\n        margin-bottom: 1.5rem;\n        color: #111827;\n    }\n\"\"\"\n\n# Button styles\nBUTTON_STYLES = \"\"\"\n    .button-group {\n        display: flex;\n        gap: 0.75rem;\n        margin-top: 1.5rem;\n        justify-content: center;\n    }\n\n    button {\n        padding: 0.75rem 2rem;\n        font-size: 0.9375rem;\n        font-weight: 500;\n        border-radius: 0.5rem;\n        border: none;\n        cursor: pointer;\n        transition: all 0.15s;\n        font-family: inherit;\n    }\n\n    button:hover {\n        transform: translateY(-1px);\n        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n    }\n\n    .btn-approve, .btn-primary {\n        background: #10b981;\n        color: #ffffff;\n        min-width: 120px;\n    }\n\n    .btn-deny, .btn-secondary {\n        background: #6b7280;\n        color: #ffffff;\n        min-width: 120px;\n    }\n\"\"\"\n\n# Info box / message box styles\nINFO_BOX_STYLES = \"\"\"\n    .info-box {\n        background: #f0f9ff;\n        border: 1px solid #bae6fd;\n        border-radius: 0.5rem;\n        padding: 1rem;\n        margin-bottom: 1.5rem;\n        text-align: left;\n        font-size: 0.9375rem;\n        line-height: 1.5;\n        color: #374151;\n    }\n\n    .info-box p {\n        margin-bottom: 0.5rem;\n    }\n\n    .info-box p:last-child {\n        margin-bottom: 0;\n    }\n\n    .info-box.centered {\n        text-align: center;\n    }\n\n    .info-box.error {\n        background: #fef2f2;\n        border-color: #fecaca;\n        color: #991b1b;\n    }\n\n    .info-box strong {\n        color: #0ea5e9;\n        font-weight: 600;\n    }\n\n    .info-box .server-name-link {\n        color: #0ea5e9;\n        text-decoration: underline;\n        font-weight: 600;\n        cursor: pointer;\n        transition: opacity 0.15s;\n    }\n\n    .info-box .server-name-link:hover {\n        opacity: 0.8;\n    }\n\n    /* Monospace info box - gray styling with code font */\n    .info-box-mono {\n        background: #f9fafb;\n        border: 1px solid #e5e7eb;\n        border-radius: 0.5rem;\n        padding: 0.875rem;\n        margin: 1.25rem 0;\n        font-size: 0.875rem;\n        color: #6b7280;\n        font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;\n        text-align: left;\n    }\n\n    .info-box-mono.centered {\n        text-align: center;\n    }\n\n    .info-box-mono.error {\n        background: #fef2f2;\n        border-color: #fecaca;\n        color: #991b1b;\n    }\n\n    .info-box-mono strong {\n        color: #111827;\n        font-weight: 600;\n    }\n\n    .warning-box {\n        background: #f0f9ff;\n        border: 1px solid #bae6fd;\n        border-radius: 0.5rem;\n        padding: 1rem;\n        margin-bottom: 1.5rem;\n        text-align: center;\n    }\n\n    .warning-box p {\n        margin-bottom: 0.5rem;\n        line-height: 1.5;\n        color: #6b7280;\n        font-size: 0.9375rem;\n    }\n\n    .warning-box p:last-child {\n        margin-bottom: 0;\n    }\n\n    .warning-box strong {\n        color: #0ea5e9;\n        font-weight: 600;\n    }\n\n    .warning-box a {\n        color: #0ea5e9;\n        text-decoration: underline;\n        font-weight: 600;\n    }\n\n    .warning-box a:hover {\n        color: #0284c7;\n        text-decoration: underline;\n    }\n\"\"\"\n\n# Status message styles (for success/error indicators)\nSTATUS_MESSAGE_STYLES = \"\"\"\n    .status-message {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        gap: 0.75rem;\n        margin-bottom: 1.5rem;\n    }\n\n    .status-icon {\n        font-size: 1.5rem;\n        line-height: 1;\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        width: 2rem;\n        height: 2rem;\n        border-radius: 0.5rem;\n        flex-shrink: 0;\n    }\n\n    .status-icon.success {\n        background: #10b98120;\n    }\n\n    .status-icon.error {\n        background: #ef444420;\n    }\n\n    .message {\n        font-size: 1.125rem;\n        line-height: 1.75;\n        color: #111827;\n        font-weight: 600;\n        text-align: left;\n    }\n\"\"\"\n\n# Detail box styles (for key-value pairs)\nDETAIL_BOX_STYLES = \"\"\"\n    .detail-box {\n        background: #f9fafb;\n        border: 1px solid #e5e7eb;\n        border-radius: 0.5rem;\n        padding: 1rem;\n        margin-bottom: 1.5rem;\n        text-align: left;\n    }\n\n    .detail-row {\n        display: flex;\n        padding: 0.5rem 0;\n        border-bottom: 1px solid #e5e7eb;\n    }\n\n    .detail-row:last-child {\n        border-bottom: none;\n    }\n\n    .detail-label {\n        font-weight: 600;\n        min-width: 160px;\n        color: #6b7280;\n        font-size: 0.875rem;\n        flex-shrink: 0;\n        padding-right: 1rem;\n    }\n\n    .detail-value {\n        flex: 1;\n        font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;\n        font-size: 0.75rem;\n        color: #111827;\n        word-break: break-all;\n        overflow-wrap: break-word;\n    }\n\"\"\"\n\n# Redirect section styles (for OAuth redirect URI box)\nREDIRECT_SECTION_STYLES = \"\"\"\n    .redirect-section {\n        background: #fffbeb;\n        border: 1px solid #fcd34d;\n        border-radius: 0.5rem;\n        padding: 1rem;\n        margin-bottom: 1.5rem;\n        text-align: left;\n    }\n\n    .redirect-section .label {\n        font-size: 0.875rem;\n        color: #6b7280;\n        font-weight: 600;\n        margin-bottom: 0.5rem;\n        display: block;\n    }\n\n    .redirect-section .value {\n        font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;\n        font-size: 0.875rem;\n        color: #111827;\n        word-break: break-all;\n        margin-top: 0.25rem;\n    }\n\"\"\"\n\n# Collapsible details styles\nDETAILS_STYLES = \"\"\"\n    details {\n        margin-bottom: 1.5rem;\n        text-align: left;\n    }\n\n    summary {\n        cursor: pointer;\n        font-size: 0.875rem;\n        color: #6b7280;\n        font-weight: 600;\n        list-style: none;\n        padding: 0.5rem;\n        border-radius: 0.25rem;\n    }\n\n    summary:hover {\n        background: #f9fafb;\n    }\n\n    summary::marker {\n        display: none;\n    }\n\n    summary::before {\n        content: \"▶\";\n        display: inline-block;\n        margin-right: 0.5rem;\n        transition: transform 0.2s;\n        font-size: 0.75rem;\n    }\n\n    details[open] summary::before {\n        transform: rotate(90deg);\n    }\n\"\"\"\n\n# Helper text styles\nHELPER_TEXT_STYLES = \"\"\"\n    .close-instruction, .help-text {\n        font-size: 0.875rem;\n        color: #6b7280;\n        margin-top: 1.5rem;\n    }\n\"\"\"\n\n# Tooltip styles for hover help\nTOOLTIP_STYLES = \"\"\"\n    .help-link-container {\n        position: fixed;\n        bottom: 1.5rem;\n        right: 1.5rem;\n        font-size: 0.875rem;\n    }\n\n    .help-link {\n        color: #6b7280;\n        text-decoration: none;\n        cursor: help;\n        position: relative;\n        display: inline-block;\n        border-bottom: 1px dotted #9ca3af;\n    }\n\n    @media (max-width: 640px) {\n        .help-link {\n            background: #ffffff;\n            padding: 0.25rem 0.5rem;\n            border-radius: 0.25rem;\n            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n        }\n    }\n\n    .help-link:hover {\n        color: #111827;\n        border-bottom-color: #111827;\n    }\n\n    .help-link:hover .tooltip {\n        opacity: 1;\n        visibility: visible;\n    }\n\n    .tooltip {\n        position: absolute;\n        bottom: 100%;\n        right: 0;\n        left: auto;\n        margin-bottom: 0.5rem;\n        background: #1f2937;\n        color: #ffffff;\n        padding: 0.75rem 1rem;\n        border-radius: 0.5rem;\n        font-size: 0.8125rem;\n        line-height: 1.5;\n        width: 280px;\n        max-width: calc(100vw - 3rem);\n        opacity: 0;\n        visibility: hidden;\n        transition: opacity 0.2s, visibility 0.2s;\n        box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);\n        text-align: left;\n    }\n\n    .tooltip::after {\n        content: '';\n        position: absolute;\n        top: 100%;\n        right: 1rem;\n        border: 6px solid transparent;\n        border-top-color: #1f2937;\n    }\n\n    .tooltip-link {\n        color: #60a5fa;\n        text-decoration: underline;\n    }\n\"\"\"\n\n\ndef create_page(\n    content: str,\n    title: str = \"FastMCP\",\n    additional_styles: str = \"\",\n    csp_policy: str = \"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'\",\n) -> str:\n    \"\"\"\n    Create a complete HTML page with FastMCP styling.\n\n    Args:\n        content: HTML content to place inside the page\n        title: Page title\n        additional_styles: Extra CSS to include\n        csp_policy: Content Security Policy header value.\n            If empty string \"\", the CSP meta tag is omitted entirely.\n\n    Returns:\n        Complete HTML page as string\n    \"\"\"\n    title = html.escape(title)\n\n    # Only include CSP meta tag if policy is non-empty\n    csp_meta = (\n        f'<meta http-equiv=\"Content-Security-Policy\" content=\"{html.escape(csp_policy, quote=True)}\" />'\n        if csp_policy\n        else \"\"\n    )\n\n    return f\"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n        <title>{title}</title>\n        <style>\n            {BASE_STYLES}\n            {additional_styles}\n        </style>\n        {csp_meta}\n    </head>\n    <body>\n        {content}\n    </body>\n    </html>\n    \"\"\"\n\n\ndef create_logo(icon_url: str | None = None, alt_text: str = \"FastMCP\") -> str:\n    \"\"\"Create logo HTML.\n\n    Args:\n        icon_url: Optional custom icon URL. If not provided, uses the FastMCP logo.\n        alt_text: Alt text for the logo image.\n\n    Returns:\n        HTML for logo image tag.\n    \"\"\"\n    url = icon_url or FASTMCP_LOGO_URL\n    alt = html.escape(alt_text)\n    return f'<img src=\"{html.escape(url)}\" alt=\"{alt}\" class=\"logo\" />'\n\n\ndef create_status_message(message: str, is_success: bool = True) -> str:\n    \"\"\"\n    Create a status message with icon.\n\n    Args:\n        message: Status message text\n        is_success: True for success (✓), False for error (✕)\n\n    Returns:\n        HTML for status message\n    \"\"\"\n    message = html.escape(message)\n    icon = \"✓\" if is_success else \"✕\"\n    icon_class = \"success\" if is_success else \"error\"\n\n    return f\"\"\"\n        <div class=\"status-message\">\n            <span class=\"status-icon {icon_class}\">{icon}</span>\n            <div class=\"message\">{message}</div>\n        </div>\n    \"\"\"\n\n\ndef create_info_box(\n    content: str,\n    is_error: bool = False,\n    centered: bool = False,\n    monospace: bool = False,\n) -> str:\n    \"\"\"\n    Create an info box.\n\n    Args:\n        content: HTML content for the info box\n        is_error: True for error styling, False for normal\n        centered: True to center the text, False for left-aligned\n        monospace: True to use gray monospace font styling instead of blue\n\n    Returns:\n        HTML for info box\n    \"\"\"\n    content = html.escape(content)\n    base_class = \"info-box-mono\" if monospace else \"info-box\"\n    classes = [base_class]\n    if is_error:\n        classes.append(\"error\")\n    if centered:\n        classes.append(\"centered\")\n    class_str = \" \".join(classes)\n    return f'<div class=\"{class_str}\">{content}</div>'\n\n\ndef create_detail_box(rows: list[tuple[str, str]]) -> str:\n    \"\"\"\n    Create a detail box with key-value pairs.\n\n    Args:\n        rows: List of (label, value) tuples\n\n    Returns:\n        HTML for detail box\n    \"\"\"\n    rows_html = \"\\n\".join(\n        f\"\"\"\n        <div class=\"detail-row\">\n            <div class=\"detail-label\">{html.escape(label)}:</div>\n            <div class=\"detail-value\">{html.escape(value)}</div>\n        </div>\n        \"\"\"\n        for label, value in rows\n    )\n\n    return f'<div class=\"detail-box\">{rows_html}</div>'\n\n\ndef create_button_group(buttons: list[tuple[str, str, str]]) -> str:\n    \"\"\"\n    Create a group of buttons.\n\n    Args:\n        buttons: List of (text, value, css_class) tuples\n\n    Returns:\n        HTML for button group\n    \"\"\"\n    buttons_html = \"\\n\".join(\n        f'<button type=\"submit\" name=\"action\" value=\"{value}\" class=\"{css_class}\">{text}</button>'\n        for text, value, css_class in buttons\n    )\n\n    return f'<div class=\"button-group\">{buttons_html}</div>'\n\n\ndef create_secure_html_response(html: str, status_code: int = 200) -> HTMLResponse:\n    \"\"\"\n    Create an HTMLResponse with security headers.\n\n    Adds X-Frame-Options: DENY to prevent clickjacking attacks per MCP security best practices.\n\n    Args:\n        html: HTML content to return\n        status_code: HTTP status code\n\n    Returns:\n        HTMLResponse with security headers\n    \"\"\"\n    return HTMLResponse(\n        content=html,\n        status_code=status_code,\n        headers={\"X-Frame-Options\": \"DENY\"},\n    )\n"
  },
  {
    "path": "src/fastmcp/utilities/version_check.py",
    "content": "\"\"\"Version checking utilities for FastMCP.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport time\nfrom pathlib import Path\n\nimport httpx\nfrom packaging.version import Version\n\nfrom fastmcp.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nPYPI_URL = \"https://pypi.org/pypi/fastmcp/json\"\nCACHE_TTL_SECONDS = 60 * 60 * 12  # 12 hours\nREQUEST_TIMEOUT_SECONDS = 2.0\n\n\ndef _get_cache_path(include_prereleases: bool = False) -> Path:\n    \"\"\"Get the path to the version cache file.\"\"\"\n    import fastmcp\n\n    suffix = \"_prerelease\" if include_prereleases else \"\"\n    return fastmcp.settings.home / f\"version_cache{suffix}.json\"\n\n\ndef _read_cache(include_prereleases: bool = False) -> tuple[str | None, float]:\n    \"\"\"Read cached version info.\n\n    Returns:\n        Tuple of (cached_version, cache_timestamp) or (None, 0) if no cache.\n    \"\"\"\n    cache_path = _get_cache_path(include_prereleases)\n    if not cache_path.exists():\n        return None, 0\n\n    try:\n        data = json.loads(cache_path.read_text())\n        return data.get(\"latest_version\"), data.get(\"timestamp\", 0)\n    except (json.JSONDecodeError, OSError):\n        return None, 0\n\n\ndef _write_cache(latest_version: str, include_prereleases: bool = False) -> None:\n    \"\"\"Write version info to cache.\"\"\"\n    cache_path = _get_cache_path(include_prereleases)\n    try:\n        cache_path.parent.mkdir(parents=True, exist_ok=True)\n        cache_path.write_text(\n            json.dumps({\"latest_version\": latest_version, \"timestamp\": time.time()})\n        )\n    except OSError:\n        # Silently ignore cache write failures\n        pass\n\n\ndef _fetch_latest_version(include_prereleases: bool = False) -> str | None:\n    \"\"\"Fetch the latest version from PyPI.\n\n    Args:\n        include_prereleases: If True, include pre-release versions (alpha, beta, rc).\n\n    Returns:\n        The latest version string, or None if the fetch failed.\n    \"\"\"\n    try:\n        response = httpx.get(PYPI_URL, timeout=REQUEST_TIMEOUT_SECONDS)\n        response.raise_for_status()\n        data = response.json()\n\n        releases = data.get(\"releases\", {})\n        if not releases:\n            return None\n\n        versions = []\n        for version_str in releases:\n            try:\n                v = Version(version_str)\n                # Skip prereleases if not requested\n                if not include_prereleases and v.is_prerelease:\n                    continue\n                versions.append(v)\n            except ValueError:\n                logger.debug(f\"Skipping invalid version string: {version_str}\")\n                continue\n\n        if not versions:\n            return None\n\n        return str(max(versions))\n\n    except (httpx.HTTPError, json.JSONDecodeError, KeyError):\n        return None\n\n\ndef get_latest_version(include_prereleases: bool = False) -> str | None:\n    \"\"\"Get the latest version of FastMCP from PyPI, using cache when available.\n\n    Args:\n        include_prereleases: If True, include pre-release versions.\n\n    Returns:\n        The latest version string, or None if unavailable.\n    \"\"\"\n    # Check cache first\n    cached_version, cache_timestamp = _read_cache(include_prereleases)\n    if cached_version and (time.time() - cache_timestamp) < CACHE_TTL_SECONDS:\n        return cached_version\n\n    # Fetch from PyPI\n    latest_version = _fetch_latest_version(include_prereleases)\n\n    # Update cache if we got a valid version\n    if latest_version:\n        _write_cache(latest_version, include_prereleases)\n        return latest_version\n\n    # Return stale cache if available\n    return cached_version\n\n\ndef check_for_newer_version() -> str | None:\n    \"\"\"Check if a newer version of FastMCP is available.\n\n    Returns:\n        The latest version string if newer than current, None otherwise.\n    \"\"\"\n    import fastmcp\n\n    setting = fastmcp.settings.check_for_updates\n    if setting == \"off\":\n        return None\n\n    include_prereleases = setting == \"prerelease\"\n    latest_version = get_latest_version(include_prereleases)\n    if not latest_version:\n        return None\n\n    try:\n        current = Version(fastmcp.__version__)\n        latest = Version(latest_version)\n\n        if latest > current:\n            return latest_version\n    except ValueError:\n        logger.debug(\n            f\"Could not compare versions: current={fastmcp.__version__!r}, \"\n            f\"latest={latest_version!r}\"\n        )\n\n    return None\n"
  },
  {
    "path": "src/fastmcp/utilities/versions.py",
    "content": "\"\"\"Version comparison utilities for component versioning.\n\nThis module provides utilities for comparing component versions. Versions are\nstrings that are first attempted to be parsed as PEP 440 versions (using the\n`packaging` library), falling back to lexicographic string comparison.\n\nExamples:\n    - \"1\", \"2\", \"10\" → parsed as PEP 440, compared semantically (1 < 2 < 10)\n    - \"1.0\", \"2.0\" → parsed as PEP 440\n    - \"v1.0\" → 'v' prefix stripped, parsed as \"1.0\"\n    - \"2025-01-15\" → not valid PEP 440, compared as strings\n    - None → sorts lowest (unversioned components)\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable, Sequence\nfrom dataclasses import dataclass\nfrom functools import total_ordering\nfrom typing import TYPE_CHECKING, Any, TypeVar, cast\n\nfrom packaging.version import InvalidVersion, Version\n\nif TYPE_CHECKING:\n    from fastmcp.utilities.components import FastMCPComponent\n\nC = TypeVar(\"C\", bound=Any)\n\n\n@dataclass\nclass VersionSpec:\n    \"\"\"Specification for filtering components by version.\n\n    Used by transforms and providers to filter components to a specific\n    version or version range. Unversioned components (version=None) always\n    match any spec.\n\n    Args:\n        gte: If set, only versions >= this value match.\n        lt: If set, only versions < this value match.\n        eq: If set, only this exact version matches (gte/lt ignored).\n    \"\"\"\n\n    gte: str | None = None\n    lt: str | None = None\n    eq: str | None = None\n\n    def matches(self, version: str | None, *, match_none: bool = True) -> bool:\n        \"\"\"Check if a version matches this spec.\n\n        Args:\n            version: The version to check, or None for unversioned.\n            match_none: Whether unversioned (None) components match. Defaults to True\n                for backward compatibility with retrieval operations. Set to False\n                when filtering (e.g., enable/disable) to exclude unversioned components\n                from version-specific rules.\n\n        Returns:\n            True if the version matches the spec.\n        \"\"\"\n        if version is None:\n            return match_none\n\n        if self.eq is not None:\n            return version == self.eq\n\n        key = parse_version_key(version)\n\n        if self.gte is not None:\n            gte_key = parse_version_key(self.gte)\n            if key < gte_key:\n                return False\n\n        if self.lt is not None:\n            lt_key = parse_version_key(self.lt)\n            if not key < lt_key:\n                return False\n\n        return True\n\n    def intersect(self, other: VersionSpec | None) -> VersionSpec:\n        \"\"\"Return a spec that satisfies both this spec and other.\n\n        Used by transforms to combine caller constraints with filter constraints.\n        For example, if a VersionFilter has lt=\"3.0\" and caller requests eq=\"1.0\",\n        the intersection validates \"1.0\" is in range and returns the exact spec.\n\n        Args:\n            other: Another spec to intersect with, or None.\n\n        Returns:\n            A VersionSpec that matches only versions satisfying both specs.\n        \"\"\"\n        if other is None:\n            return self\n\n        if self.eq is not None:\n            # This spec wants exact - validate against other's range\n            if other.matches(self.eq):\n                return self\n            return VersionSpec(eq=\"__impossible__\")\n\n        if other.eq is not None:\n            # Other wants exact - validate against our range\n            if self.matches(other.eq):\n                return other\n            return VersionSpec(eq=\"__impossible__\")\n\n        # Both are ranges - take tighter bounds\n        return VersionSpec(\n            gte=max_version(self.gte, other.gte),\n            lt=min_version(self.lt, other.lt),\n        )\n\n\n@total_ordering\nclass VersionKey:\n    \"\"\"A comparable version key that handles None, PEP 440 versions, and strings.\n\n    Comparison order:\n    1. None (unversioned) sorts lowest\n    2. PEP 440 versions sort by semantic version order\n    3. Invalid versions (strings) sort lexicographically\n    4. When comparing PEP 440 vs string, PEP 440 comes first\n    \"\"\"\n\n    __slots__ = (\"_is_none\", \"_is_pep440\", \"_parsed\", \"_raw\")\n\n    def __init__(self, version: str | None) -> None:\n        self._raw = version\n        self._is_none = version is None\n        self._is_pep440 = False\n        self._parsed: Version | str | None = None\n\n        if version is not None:\n            # Strip leading 'v' if present (common convention like \"v1.0\")\n            normalized = version.lstrip(\"v\") if version.startswith(\"v\") else version\n            try:\n                self._parsed = Version(normalized)\n                self._is_pep440 = True\n            except InvalidVersion:\n                # Fall back to string comparison for non-PEP 440 versions\n                self._parsed = version\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, VersionKey):\n            return NotImplemented\n        if self._is_none and other._is_none:\n            return True\n        if self._is_none != other._is_none:\n            return False\n        # Both are not None\n        if self._is_pep440 and other._is_pep440:\n            return self._parsed == other._parsed\n        if not self._is_pep440 and not other._is_pep440:\n            return self._parsed == other._parsed\n        # One is PEP 440, other is string - never equal\n        return False\n\n    def __lt__(self, other: object) -> bool:\n        if not isinstance(other, VersionKey):\n            return NotImplemented\n        # None sorts lowest\n        if self._is_none and other._is_none:\n            return False  # Equal\n        if self._is_none:\n            return True  # None < anything\n        if other._is_none:\n            return False  # anything > None\n\n        # Both are not None\n        if self._is_pep440 and other._is_pep440:\n            # Both PEP 440 - compare normally\n            assert isinstance(self._parsed, Version)\n            assert isinstance(other._parsed, Version)\n            return self._parsed < other._parsed\n        if not self._is_pep440 and not other._is_pep440:\n            # Both strings - lexicographic\n            assert isinstance(self._parsed, str)\n            assert isinstance(other._parsed, str)\n            return self._parsed < other._parsed\n        # Mixed: PEP 440 sorts before strings\n        # (arbitrary but consistent choice)\n        return self._is_pep440\n\n    def __repr__(self) -> str:\n        return f\"VersionKey({self._raw!r})\"\n\n\ndef parse_version_key(version: str | None) -> VersionKey:\n    \"\"\"Parse a version string into a sortable key.\n\n    Args:\n        version: The version string, or None for unversioned.\n\n    Returns:\n        A VersionKey suitable for sorting.\n    \"\"\"\n    return VersionKey(version)\n\n\ndef version_sort_key(component: FastMCPComponent) -> VersionKey:\n    \"\"\"Get a sort key for a component based on its version.\n\n    Use with sorted() or max() to order components by version.\n\n    Args:\n        component: The component to get a sort key for.\n\n    Returns:\n        A sortable VersionKey.\n\n    Example:\n        ```python\n        tools = [tool_v1, tool_v2, tool_unversioned]\n        highest = max(tools, key=version_sort_key)  # Returns tool_v2\n        ```\n    \"\"\"\n    return parse_version_key(component.version)\n\n\ndef compare_versions(a: str | None, b: str | None) -> int:\n    \"\"\"Compare two version strings.\n\n    Args:\n        a: First version string (or None).\n        b: Second version string (or None).\n\n    Returns:\n        -1 if a < b, 0 if a == b, 1 if a > b.\n\n    Example:\n        ```python\n        compare_versions(\"1.0\", \"2.0\")  # Returns -1\n        compare_versions(\"2.0\", \"1.0\")  # Returns 1\n        compare_versions(None, \"1.0\")   # Returns -1 (None < any version)\n        ```\n    \"\"\"\n    key_a = parse_version_key(a)\n    key_b = parse_version_key(b)\n    return (key_a > key_b) - (key_a < key_b)\n\n\ndef is_version_greater(a: str | None, b: str | None) -> bool:\n    \"\"\"Check if version a is greater than version b.\n\n    Args:\n        a: First version string (or None).\n        b: Second version string (or None).\n\n    Returns:\n        True if a > b, False otherwise.\n    \"\"\"\n    return compare_versions(a, b) > 0\n\n\ndef max_version(a: str | None, b: str | None) -> str | None:\n    \"\"\"Return the greater of two versions.\n\n    Args:\n        a: First version string (or None).\n        b: Second version string (or None).\n\n    Returns:\n        The greater version, or None if both are None.\n    \"\"\"\n    if a is None:\n        return b\n    if b is None:\n        return a\n    return a if compare_versions(a, b) >= 0 else b\n\n\ndef min_version(a: str | None, b: str | None) -> str | None:\n    \"\"\"Return the lesser of two versions.\n\n    Args:\n        a: First version string (or None).\n        b: Second version string (or None).\n\n    Returns:\n        The lesser version, or None if both are None.\n    \"\"\"\n    if a is None:\n        return b\n    if b is None:\n        return a\n    return a if compare_versions(a, b) <= 0 else b\n\n\ndef dedupe_with_versions(\n    components: Sequence[C],\n    key_fn: Callable[[C], str],\n) -> list[C]:\n    \"\"\"Deduplicate components by key, keeping highest version.\n\n    Groups components by key, selects the highest version from each group,\n    and injects available versions into meta if any component is versioned.\n\n    Args:\n        components: Sequence of components to deduplicate.\n        key_fn: Function to extract the grouping key from a component.\n\n    Returns:\n        Deduplicated list with versions injected into meta.\n    \"\"\"\n    by_key: dict[str, list[C]] = {}\n    for c in components:\n        by_key.setdefault(key_fn(c), []).append(c)\n\n    result: list[C] = []\n    for versions in by_key.values():\n        highest: C = cast(C, max(versions, key=version_sort_key))\n        if any(c.version is not None for c in versions):\n            all_versions = sorted(\n                [c.version for c in versions if c.version is not None],\n                key=parse_version_key,\n                reverse=True,\n            )\n            meta = highest.meta or {}\n            highest = highest.model_copy(\n                update={\n                    \"meta\": {\n                        **meta,\n                        \"fastmcp\": {\n                            **meta.get(\"fastmcp\", {}),\n                            \"versions\": all_versions,\n                        },\n                    }\n                }\n            )\n        result.append(highest)\n    return result\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/cli/__init__.py",
    "content": "\"\"\"CLI test package.\"\"\"\n"
  },
  {
    "path": "tests/cli/test_cimd_cli.py",
    "content": "\"\"\"Tests for the CIMD CLI commands (create and validate).\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom unittest.mock import AsyncMock, patch\n\nimport pytest\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.cli.cimd import create_command, validate_command\nfrom fastmcp.server.auth.cimd import CIMDDocument, CIMDFetchError, CIMDValidationError\n\n\nclass TestCIMDCreateCommand:\n    \"\"\"Tests for `fastmcp auth cimd create`.\"\"\"\n\n    def test_minimal_output(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert doc[\"client_name\"] == \"Test App\"\n        assert doc[\"redirect_uris\"] == [\"http://localhost:*/callback\"]\n        assert doc[\"token_endpoint_auth_method\"] == \"none\"\n        assert doc[\"grant_types\"] == [\"authorization_code\"]\n        assert doc[\"response_types\"] == [\"code\"]\n        # Placeholder client_id\n        assert \"YOUR-DOMAIN\" in doc[\"client_id\"]\n\n    def test_with_client_id(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            client_id=\"https://myapp.example.com/client.json\",\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert doc[\"client_id\"] == \"https://myapp.example.com/client.json\"\n\n    def test_with_output_file(self, tmp_path):\n        output_file = tmp_path / \"client.json\"\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            client_id=\"https://example.com/client.json\",\n            output=str(output_file),\n        )\n        doc = json.loads(output_file.read_text())\n        assert doc[\"client_id\"] == \"https://example.com/client.json\"\n        assert doc[\"client_name\"] == \"Test App\"\n\n    def test_relative_path_resolved(self, tmp_path, monkeypatch):\n        \"\"\"Relative paths should be resolved against cwd.\"\"\"\n        monkeypatch.chdir(tmp_path)\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            output=\"./subdir/client.json\",\n        )\n        resolved = tmp_path / \"subdir\" / \"client.json\"\n        assert resolved.exists()\n        doc = json.loads(resolved.read_text())\n        assert doc[\"client_name\"] == \"Test App\"\n\n    def test_with_scope(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            scope=\"read write\",\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert doc[\"scope\"] == \"read write\"\n\n    def test_with_client_uri(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            client_uri=\"https://example.com\",\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert doc[\"client_uri\"] == \"https://example.com\"\n\n    def test_with_logo_uri(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            logo_uri=\"https://example.com/logo.png\",\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert doc[\"logo_uri\"] == \"https://example.com/logo.png\"\n\n    def test_multiple_redirect_uris(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\n                \"http://localhost:*/callback\",\n                \"https://myapp.example.com/callback\",\n            ],\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert len(doc[\"redirect_uris\"]) == 2\n\n    def test_no_pretty(self, capsys: pytest.CaptureFixture[str]):\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            pretty=False,\n        )\n        output = capsys.readouterr().out.strip()\n        # Compact JSON has no newlines within the object\n        assert \"\\n\" not in output\n        doc = json.loads(output)\n        assert doc[\"client_name\"] == \"Test App\"\n\n    def test_placeholder_warning_on_stderr(self, capsys: pytest.CaptureFixture[str]):\n        \"\"\"When outputting to stdout with no --client-id, warning goes to stderr.\"\"\"\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n        )\n        captured = capsys.readouterr()\n        # stdout has valid JSON\n        json.loads(captured.out)\n        # stderr has the warning (Rich Console writes to stderr)\n        assert \"placeholder\" in captured.err\n\n    def test_no_warning_with_client_id(self, capsys: pytest.CaptureFixture[str]):\n        \"\"\"No placeholder warning when --client-id is provided.\"\"\"\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n            client_id=\"https://example.com/client.json\",\n        )\n        captured = capsys.readouterr()\n        assert \"placeholder\" not in captured.err\n\n    def test_optional_fields_omitted_when_none(\n        self, capsys: pytest.CaptureFixture[str]\n    ):\n        \"\"\"Optional fields like scope, client_uri, logo_uri are omitted if not given.\"\"\"\n        create_command(\n            name=\"Test App\",\n            redirect_uri=[\"http://localhost:*/callback\"],\n        )\n        doc = json.loads(capsys.readouterr().out)\n        assert \"scope\" not in doc\n        assert \"client_uri\" not in doc\n        assert \"logo_uri\" not in doc\n\n\nclass TestCIMDValidateCommand:\n    \"\"\"Tests for `fastmcp auth cimd validate`.\"\"\"\n\n    def test_invalid_url_format(self, capsys: pytest.CaptureFixture[str]):\n        with pytest.raises(SystemExit, match=\"1\"):\n            validate_command(\"http://insecure.com/client.json\")\n        captured = capsys.readouterr()\n        assert \"Invalid CIMD URL\" in captured.out\n\n    def test_root_path_rejected(self, capsys: pytest.CaptureFixture[str]):\n        with pytest.raises(SystemExit, match=\"1\"):\n            validate_command(\"https://example.com/\")\n        captured = capsys.readouterr()\n        assert \"Invalid CIMD URL\" in captured.out\n\n    def test_success(self, capsys: pytest.CaptureFixture[str]):\n        mock_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://myapp.example.com/client.json\"),\n            client_name=\"Test App\",\n            redirect_uris=[\"http://localhost:*/callback\"],\n            token_endpoint_auth_method=\"none\",\n            grant_types=[\"authorization_code\"],\n            response_types=[\"code\"],\n        )\n        with patch.object(CIMDDocument, \"__init__\", return_value=None):\n            pass\n        mock_fetch = AsyncMock(return_value=mock_doc)\n        with patch(\n            \"fastmcp.cli.cimd.CIMDFetcher.fetch\",\n            mock_fetch,\n        ):\n            validate_command(\"https://myapp.example.com/client.json\")\n        captured = capsys.readouterr()\n        assert \"Valid CIMD document\" in captured.out\n        assert \"Test App\" in captured.out\n\n    def test_fetch_error(self, capsys: pytest.CaptureFixture[str]):\n        mock_fetch = AsyncMock(side_effect=CIMDFetchError(\"Connection refused\"))\n        with patch(\n            \"fastmcp.cli.cimd.CIMDFetcher.fetch\",\n            mock_fetch,\n        ):\n            with pytest.raises(SystemExit, match=\"1\"):\n                validate_command(\"https://myapp.example.com/client.json\")\n        captured = capsys.readouterr()\n        assert \"Failed to fetch\" in captured.out\n\n    def test_validation_error(self, capsys: pytest.CaptureFixture[str]):\n        mock_fetch = AsyncMock(side_effect=CIMDValidationError(\"client_id mismatch\"))\n        with patch(\n            \"fastmcp.cli.cimd.CIMDFetcher.fetch\",\n            mock_fetch,\n        ):\n            with pytest.raises(SystemExit, match=\"1\"):\n                validate_command(\"https://myapp.example.com/client.json\")\n        captured = capsys.readouterr()\n        assert \"Validation error\" in captured.out\n"
  },
  {
    "path": "tests/cli/test_cli.py",
    "content": "import subprocess\nfrom pathlib import Path\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom fastmcp.cli.cli import _parse_env_var, app\n\n\nclass TestMainCLI:\n    \"\"\"Test the main CLI application.\"\"\"\n\n    def test_app_exists(self):\n        \"\"\"Test that the main app is properly configured.\"\"\"\n        # app.name is a tuple in cyclopts\n        assert \"fastmcp\" in app.name\n        assert \"FastMCP\" in app.help\n        # Just check that version exists, not the specific value\n        assert hasattr(app, \"version\")\n\n    def test_parse_env_var_valid(self):\n        \"\"\"Test parsing valid environment variables.\"\"\"\n        key, value = _parse_env_var(\"KEY=value\")\n        assert key == \"KEY\"\n        assert value == \"value\"\n\n        key, value = _parse_env_var(\"COMPLEX_KEY=complex=value=with=equals\")\n        assert key == \"COMPLEX_KEY\"\n        assert value == \"complex=value=with=equals\"\n\n    def test_parse_env_var_invalid(self):\n        \"\"\"Test parsing invalid environment variables exits.\"\"\"\n        with pytest.raises(SystemExit) as exc_info:\n            _parse_env_var(\"INVALID_FORMAT\")\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n\n\nclass TestVersionCommand:\n    \"\"\"Test the version command.\"\"\"\n\n    @patch(\"fastmcp.cli.cli.check_for_newer_version\", return_value=None)\n    def test_version_command_execution(self, mock_check):\n        \"\"\"Test that version command executes properly.\"\"\"\n        # The version command should execute without raising SystemExit\n        command, bound, _ = app.parse_args([\"version\"])\n        command()  # Should not raise\n\n    def test_version_command_parsing(self):\n        \"\"\"Test that the version command parses arguments correctly.\"\"\"\n        command, bound, _ = app.parse_args([\"version\"])\n        assert callable(command)\n        assert command.__name__ == \"version\"  # type: ignore[attr-defined]\n        # Default arguments aren't included in bound.arguments\n        assert bound.arguments == {}\n\n    def test_version_command_with_copy_flag(self):\n        \"\"\"Test that the version command parses --copy flag correctly.\"\"\"\n        command, bound, _ = app.parse_args([\"version\", \"--copy\"])\n        assert callable(command)\n        assert command.__name__ == \"version\"  # type: ignore[attr-defined]\n        assert bound.arguments == {\"copy\": True}\n\n    @patch(\"fastmcp.cli.cli.pyperclip.copy\")\n    @patch(\"fastmcp.cli.cli.console\")\n    def test_version_command_copy_functionality(\n        self, mock_console, mock_pyperclip_copy\n    ):\n        \"\"\"Test that the version command copies to clipboard when --copy is used.\"\"\"\n        command, bound, _ = app.parse_args([\"version\", \"--copy\"])\n        command(**bound.arguments)\n\n        # Verify pyperclip.copy was called with plain text format\n        mock_pyperclip_copy.assert_called_once()\n        copied_text = mock_pyperclip_copy.call_args[0][0]\n\n        # Verify the copied text contains expected version info keys in plain text\n        assert \"FastMCP version:\" in copied_text\n        assert \"MCP version:\" in copied_text\n        assert \"Python version:\" in copied_text\n        assert \"Platform:\" in copied_text\n        assert \"FastMCP root path:\" in copied_text\n\n        # Verify no ANSI escape codes (terminal control characters)\n        assert \"\\x1b[\" not in copied_text\n        mock_console.print.assert_called_with(\n            \"[green]✓[/green] Version information copied to clipboard\"\n        )\n\n\nclass TestDevCommand:\n    \"\"\"Test the dev command.\"\"\"\n\n    def test_dev_inspector_command_parsing(self):\n        \"\"\"Test that dev inspector command can be parsed with various options.\"\"\"\n        # Test basic parsing\n        command, bound, _ = app.parse_args([\"dev\", \"inspector\", \"server.py\"])\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n\n        # Test with options\n        command, bound, _ = app.parse_args(\n            [\n                \"dev\",\n                \"inspector\",\n                \"server.py\",\n                \"--with\",\n                \"package1\",\n                \"--inspector-version\",\n                \"1.0.0\",\n                \"--ui-port\",\n                \"3000\",\n            ]\n        )\n        assert bound.arguments[\"with_packages\"] == [\"package1\"]\n        assert bound.arguments[\"inspector_version\"] == \"1.0.0\"\n        assert bound.arguments[\"ui_port\"] == 3000\n\n    def test_dev_inspector_command_parsing_with_new_options(self):\n        \"\"\"Test dev inspector command parsing with new uv options.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"dev\",\n                \"inspector\",\n                \"server.py\",\n                \"--python\",\n                \"3.10\",\n                \"--project\",\n                \"/workspace\",\n                \"--with-requirements\",\n                \"dev-requirements.txt\",\n                \"--with\",\n                \"pytest\",\n            ]\n        )\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"python\"] == \"3.10\"\n        assert bound.arguments[\"project\"] == Path(\"/workspace\")\n        assert bound.arguments[\"with_requirements\"] == Path(\"dev-requirements.txt\")\n        assert bound.arguments[\"with_packages\"] == [\"pytest\"]\n\n\nclass TestRunCommand:\n    \"\"\"Test the run command.\"\"\"\n\n    def test_run_command_parsing_basic(self):\n        \"\"\"Test basic run command parsing.\"\"\"\n        command, bound, _ = app.parse_args([\"run\", \"server.py\"])\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        # Cyclopts only includes non-default values\n        assert \"transport\" not in bound.arguments\n        assert \"host\" not in bound.arguments\n        assert \"port\" not in bound.arguments\n        assert \"path\" not in bound.arguments\n        assert \"log_level\" not in bound.arguments\n        assert \"no_banner\" not in bound.arguments\n\n    def test_run_command_parsing_with_options(self):\n        \"\"\"Test run command parsing with various options.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--transport\",\n                \"http\",\n                \"--host\",\n                \"localhost\",\n                \"--port\",\n                \"8080\",\n                \"--path\",\n                \"/v1/mcp\",\n                \"--log-level\",\n                \"DEBUG\",\n                \"--no-banner\",\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"transport\"] == \"http\"\n        assert bound.arguments[\"host\"] == \"localhost\"\n        assert bound.arguments[\"port\"] == 8080\n        assert bound.arguments[\"path\"] == \"/v1/mcp\"\n        assert bound.arguments[\"log_level\"] == \"DEBUG\"\n        assert bound.arguments[\"no_banner\"] is True\n\n    def test_run_command_parsing_partial_options(self):\n        \"\"\"Test run command parsing with only some options.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--transport\",\n                \"http\",\n                \"--no-banner\",\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"transport\"] == \"http\"\n        assert bound.arguments[\"no_banner\"] is True\n        # Other options should not be present\n        assert \"host\" not in bound.arguments\n        assert \"port\" not in bound.arguments\n        assert \"log_level\" not in bound.arguments\n        assert \"path\" not in bound.arguments\n\n    def test_run_command_parsing_with_new_options(self):\n        \"\"\"Test run command parsing with new uv options.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--python\",\n                \"3.11\",\n                \"--with\",\n                \"pandas\",\n                \"--with\",\n                \"numpy\",\n                \"--project\",\n                \"/path/to/project\",\n                \"--with-requirements\",\n                \"requirements.txt\",\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"python\"] == \"3.11\"\n        assert bound.arguments[\"with_packages\"] == [\"pandas\", \"numpy\"]\n        assert bound.arguments[\"project\"] == Path(\"/path/to/project\")\n        assert bound.arguments[\"with_requirements\"] == Path(\"requirements.txt\")\n\n    def test_run_command_transport_aliases(self):\n        \"\"\"Test that both 'http' and 'streamable-http' are accepted as valid transport options.\"\"\"\n        # Test with 'http' transport\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--transport\",\n                \"http\",\n            ]\n        )\n        assert command is not None\n        assert bound.arguments[\"transport\"] == \"http\"\n\n        # Test with 'streamable-http' transport\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--transport\",\n                \"streamable-http\",\n            ]\n        )\n        assert command is not None\n        assert bound.arguments[\"transport\"] == \"streamable-http\"\n\n    def test_run_command_parsing_with_server_args(self):\n        \"\"\"Test run command parsing with server arguments after --.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--\",\n                \"--config\",\n                \"test.json\",\n                \"--debug\",\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        # Server args after -- are captured as positional arguments in bound.args\n        assert bound.args == (\"server.py\", \"--config\", \"test.json\", \"--debug\")\n\n    def test_run_command_parsing_with_mixed_args(self):\n        \"\"\"Test run command parsing with both FastMCP options and server args.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--transport\",\n                \"http\",\n                \"--port\",\n                \"8080\",\n                \"--\",\n                \"--server-port\",\n                \"9090\",\n                \"--debug\",\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"transport\"] == \"http\"\n        assert bound.arguments[\"port\"] == 8080\n        # Server args after -- are captured separately from FastMCP options\n        assert bound.args == (\"server.py\", \"--server-port\", \"9090\", \"--debug\")\n\n    def test_run_command_parsing_with_positional_server_args(self):\n        \"\"\"Test run command parsing with positional server arguments.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--\",\n                \"arg1\",\n                \"arg2\",\n                \"--flag\",\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        # Positional args and flags after -- are all captured\n        assert bound.args == (\"server.py\", \"arg1\", \"arg2\", \"--flag\")\n\n    def test_run_command_parsing_server_args_require_delimiter(self):\n        \"\"\"Test that server args without -- delimiter are rejected.\"\"\"\n        # Should fail because --config is not a recognized FastMCP option\n        with pytest.raises(SystemExit):\n            app.parse_args(\n                [\n                    \"run\",\n                    \"server.py\",\n                    \"--config\",\n                    \"test.json\",\n                ]\n            )\n\n    def test_run_command_parsing_project_flag(self):\n        \"\"\"Test run command parsing with --project flag.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--project\",\n                \"./test-env\",\n            ]\n        )\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"project\"] == Path(\"./test-env\")\n\n    def test_run_command_parsing_skip_source_flag(self):\n        \"\"\"Test run command parsing with --skip-source flag.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--skip-source\",\n            ]\n        )\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"skip_source\"] is True\n\n    def test_run_command_parsing_project_and_skip_source(self):\n        \"\"\"Test run command parsing with --project and --skip-source flags.\"\"\"\n        command, bound, _ = app.parse_args(\n            [\n                \"run\",\n                \"server.py\",\n                \"--project\",\n                \"./test-env\",\n                \"--skip-source\",\n            ]\n        )\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"project\"] == Path(\"./test-env\")\n        assert bound.arguments[\"skip_source\"] is True\n\n    def test_show_server_banner_setting(self):\n        \"\"\"Test that show_server_banner setting works with environment variable.\"\"\"\n        import os\n        from unittest import mock\n\n        from fastmcp.settings import Settings\n\n        # Test default (banner shown)\n        settings = Settings()\n        assert settings.show_server_banner is True\n\n        # Test with env var set to false (banner hidden)\n        with mock.patch.dict(os.environ, {\"FASTMCP_SHOW_SERVER_BANNER\": \"false\"}):\n            settings = Settings()\n            assert settings.show_server_banner is False\n\n        # Test CLI precedence logic (simulated)\n        with mock.patch.dict(os.environ, {\"FASTMCP_SHOW_SERVER_BANNER\": \"true\"}):\n            settings = Settings()\n            # CLI --no-banner flag would override\n            cli_no_banner = True\n            final = cli_no_banner if cli_no_banner else not settings.show_server_banner\n            assert final is True  # Banner suppressed by CLI flag\n\n\nclass TestWindowsSpecific:\n    \"\"\"Test Windows-specific functionality.\"\"\"\n\n    @patch(\"subprocess.run\")\n    def test_get_npx_command_windows_cmd(self, mock_run):\n        \"\"\"Test npx command detection on Windows with npx.cmd.\"\"\"\n        from fastmcp.cli.cli import _get_npx_command\n\n        with patch(\"sys.platform\", \"win32\"):\n            # First call succeeds with npx.cmd\n            mock_run.return_value = Mock(returncode=0)\n\n            result = _get_npx_command()\n\n            assert result == \"npx.cmd\"\n            mock_run.assert_called_once_with(\n                [\"npx.cmd\", \"--version\"],\n                check=True,\n                capture_output=True,\n            )\n\n    @patch(\"subprocess.run\")\n    def test_get_npx_command_windows_exe(self, mock_run):\n        \"\"\"Test npx command detection on Windows with npx.exe.\"\"\"\n        from fastmcp.cli.cli import _get_npx_command\n\n        with patch(\"sys.platform\", \"win32\"):\n            # First call fails, second succeeds\n            mock_run.side_effect = [\n                subprocess.CalledProcessError(1, \"npx.cmd\"),\n                Mock(returncode=0),\n            ]\n\n            result = _get_npx_command()\n\n            assert result == \"npx.exe\"\n            assert mock_run.call_count == 2\n\n    @patch(\"subprocess.run\")\n    def test_get_npx_command_windows_cmd_missing(self, mock_run):\n        \"\"\"Test npx command detection continues when npx.cmd is missing.\"\"\"\n        from fastmcp.cli.cli import _get_npx_command\n\n        with patch(\"sys.platform\", \"win32\"):\n            # Missing npx.cmd should not abort detection\n            mock_run.side_effect = [\n                FileNotFoundError(\"npx.cmd not found\"),\n                Mock(returncode=0),\n            ]\n\n            result = _get_npx_command()\n\n            assert result == \"npx.exe\"\n            assert mock_run.call_count == 2\n\n    @patch(\"subprocess.run\")\n    def test_get_npx_command_windows_fallback(self, mock_run):\n        \"\"\"Test npx command detection on Windows with plain npx.\"\"\"\n        from fastmcp.cli.cli import _get_npx_command\n\n        with patch(\"sys.platform\", \"win32\"):\n            # First two calls fail, third succeeds\n            mock_run.side_effect = [\n                subprocess.CalledProcessError(1, \"npx.cmd\"),\n                subprocess.CalledProcessError(1, \"npx.exe\"),\n                Mock(returncode=0),\n            ]\n\n            result = _get_npx_command()\n\n            assert result == \"npx\"\n            assert mock_run.call_count == 3\n\n    @patch(\"subprocess.run\")\n    def test_get_npx_command_windows_not_found(self, mock_run):\n        \"\"\"Test npx command detection on Windows when npx is not found.\"\"\"\n        from fastmcp.cli.cli import _get_npx_command\n\n        with patch(\"sys.platform\", \"win32\"):\n            # All calls fail\n            mock_run.side_effect = subprocess.CalledProcessError(1, \"npx\")\n\n            result = _get_npx_command()\n\n            assert result is None\n            assert mock_run.call_count == 3\n\n    @patch(\"subprocess.run\")\n    def test_get_npx_command_unix(self, mock_run):\n        \"\"\"Test npx command detection on Unix systems.\"\"\"\n        from fastmcp.cli.cli import _get_npx_command\n\n        with patch(\"sys.platform\", \"darwin\"):\n            result = _get_npx_command()\n\n            assert result == \"npx\"\n            mock_run.assert_not_called()\n\n    def test_windows_path_parsing_with_colon(self, tmp_path):\n        \"\"\"Test parsing Windows paths with drive letters and colons.\"\"\"\n        from pathlib import Path\n\n        from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import (\n            FileSystemSource,\n        )\n\n        # Create a real test file to test the logic\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"# test server\")\n\n        # Test normal file parsing (works on all platforms)\n        source = FileSystemSource(path=str(test_file))\n        assert source.entrypoint is None\n        assert Path(source.path).resolve() == test_file.resolve()\n\n        # Test file:object parsing\n        source = FileSystemSource(path=f\"{test_file}:myapp\")\n        assert source.entrypoint == \"myapp\"\n\n        # Test that the file portion resolves correctly when object is specified\n        assert Path(source.path).resolve() == test_file.resolve()\n\n\nclass TestInspectCommand:\n    \"\"\"Test the inspect command.\"\"\"\n\n    def test_inspect_command_parsing_basic(self):\n        \"\"\"Test basic inspect command parsing.\"\"\"\n        command, bound, _ = app.parse_args([\"inspect\", \"server.py\"])\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        # Only explicitly set parameters are in bound.arguments\n        assert \"output\" not in bound.arguments\n\n    def test_inspect_command_parsing_with_output(self, tmp_path):\n        \"\"\"Test inspect command parsing with output file.\"\"\"\n        output_file = tmp_path / \"output.json\"\n\n        command, bound, _ = app.parse_args(\n            [\n                \"inspect\",\n                \"server.py\",\n                \"--output\",\n                str(output_file),\n            ]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        # Output is parsed as a Path object\n        assert bound.arguments[\"output\"] == output_file\n\n    async def test_inspect_command_text_summary(self, tmp_path, capsys):\n        \"\"\"Test inspect command with no format shows text summary.\"\"\"\n        # Create a real server file\n        server_file = tmp_path / \"test_server.py\"\n        server_file.write_text(\"\"\"\nimport fastmcp\n\nmcp = fastmcp.FastMCP(\"InspectTestServer\", instructions=\"Test instructions\", version=\"1.0.0\")\n\n@mcp.tool\ndef test_tool(x: int) -> int:\n    return x * 2\n\"\"\")\n\n        # Parse and execute the command without format or output\n        command, bound, _ = app.parse_args(\n            [\n                \"inspect\",\n                str(server_file),\n            ]\n        )\n\n        await command(**bound.arguments)\n\n        # Check the console output\n        captured = capsys.readouterr()\n        # Check for the table format output\n        assert \"InspectTestServer\" in captured.out\n        assert \"Test instructions\" in captured.out\n        assert \"1.0.0\" in captured.out\n        assert \"Tools\" in captured.out\n        assert \"1\" in captured.out  # number of tools\n        assert \"FastMCP\" in captured.out\n        assert \"MCP\" in captured.out\n        assert \"Use --format [fastmcp|mcp] for complete JSON output\" in captured.out\n\n    async def test_inspect_command_with_real_server(self, tmp_path):\n        \"\"\"Test inspect command with a real server file.\"\"\"\n        # Create a real server file\n        server_file = tmp_path / \"test_server.py\"\n        server_file.write_text(\"\"\"\nimport fastmcp\n\nmcp = fastmcp.FastMCP(\"InspectTestServer\")\n\n@mcp.tool\ndef test_tool(x: int) -> int:\n    return x * 2\n\n@mcp.prompt\ndef test_prompt(name: str) -> str:\n    return f\"Hello, {name}!\"\n\"\"\")\n\n        output_file = tmp_path / \"inspect_output.json\"\n\n        # Parse and execute the command with format and output file\n        command, bound, _ = app.parse_args(\n            [\n                \"inspect\",\n                str(server_file),\n                \"--format\",\n                \"fastmcp\",\n                \"--output\",\n                str(output_file),\n            ]\n        )\n\n        await command(**bound.arguments)\n\n        # Verify the output file was created and contains expected content\n        assert output_file.exists()\n        content = output_file.read_text()\n\n        # Basic checks that the fastmcp format worked\n        import json\n\n        data = json.loads(content)\n        assert data[\"server\"][\"name\"] == \"InspectTestServer\"\n        assert len(data[\"tools\"]) == 1\n        assert len(data[\"prompts\"]) == 1\n"
  },
  {
    "path": "tests/cli/test_client_commands.py",
    "content": "\"\"\"Tests for fastmcp list and fastmcp call CLI commands.\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import patch\n\nimport mcp.types\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.cli import client as client_module\nfrom fastmcp.cli.client import (\n    Client,\n    _build_client,\n    _build_stdio_from_command,\n    _format_call_result_text,\n    _is_http_target,\n    _sanitize_untrusted_text,\n    call_command,\n    coerce_value,\n    format_tool_signature,\n    list_command,\n    parse_tool_arguments,\n    resolve_server_spec,\n)\nfrom fastmcp.client.client import CallToolResult\nfrom fastmcp.client.transports.stdio import StdioTransport\n\n# ---------------------------------------------------------------------------\n# coerce_value\n# ---------------------------------------------------------------------------\n\n\nclass TestCoerceValue:\n    def test_integer(self):\n        assert coerce_value(\"42\", {\"type\": \"integer\"}) == 42\n\n    def test_integer_negative(self):\n        assert coerce_value(\"-7\", {\"type\": \"integer\"}) == -7\n\n    def test_integer_invalid(self):\n        with pytest.raises(ValueError, match=\"Expected integer\"):\n            coerce_value(\"abc\", {\"type\": \"integer\"})\n\n    def test_number(self):\n        assert coerce_value(\"3.14\", {\"type\": \"number\"}) == 3.14\n\n    def test_number_integer_value(self):\n        assert coerce_value(\"5\", {\"type\": \"number\"}) == 5.0\n\n    def test_number_invalid(self):\n        with pytest.raises(ValueError, match=\"Expected number\"):\n            coerce_value(\"xyz\", {\"type\": \"number\"})\n\n    def test_boolean_true_variants(self):\n        for val in (\"true\", \"True\", \"TRUE\", \"1\", \"yes\"):\n            assert coerce_value(val, {\"type\": \"boolean\"}) is True\n\n    def test_boolean_false_variants(self):\n        for val in (\"false\", \"False\", \"FALSE\", \"0\", \"no\"):\n            assert coerce_value(val, {\"type\": \"boolean\"}) is False\n\n    def test_boolean_invalid(self):\n        with pytest.raises(ValueError, match=\"Expected boolean\"):\n            coerce_value(\"maybe\", {\"type\": \"boolean\"})\n\n    def test_array(self):\n        assert coerce_value(\"[1, 2, 3]\", {\"type\": \"array\"}) == [1, 2, 3]\n\n    def test_array_invalid(self):\n        with pytest.raises(ValueError, match=\"Expected JSON array\"):\n            coerce_value(\"not-json\", {\"type\": \"array\"})\n\n    def test_object(self):\n        assert coerce_value('{\"a\": 1}', {\"type\": \"object\"}) == {\"a\": 1}\n\n    def test_string(self):\n        assert coerce_value(\"hello\", {\"type\": \"string\"}) == \"hello\"\n\n    def test_string_default(self):\n        \"\"\"Unknown or missing type treats value as string.\"\"\"\n        assert coerce_value(\"hello\", {}) == \"hello\"\n\n    def test_string_preserves_numeric_looking_values(self):\n        assert coerce_value(\"42\", {\"type\": \"string\"}) == \"42\"\n\n\n# ---------------------------------------------------------------------------\n# parse_tool_arguments\n# ---------------------------------------------------------------------------\n\n\nclass TestParseToolArguments:\n    SCHEMA: dict[str, Any] = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"query\": {\"type\": \"string\"},\n            \"limit\": {\"type\": \"integer\"},\n            \"verbose\": {\"type\": \"boolean\"},\n        },\n        \"required\": [\"query\"],\n    }\n\n    def test_basic_key_value(self):\n        result = parse_tool_arguments((\"query=hello\", \"limit=10\"), None, self.SCHEMA)\n        assert result == {\"query\": \"hello\", \"limit\": 10}\n\n    def test_input_json_only(self):\n        result = parse_tool_arguments((), '{\"query\": \"hello\", \"limit\": 5}', self.SCHEMA)\n        assert result == {\"query\": \"hello\", \"limit\": 5}\n\n    def test_key_value_overrides_input_json(self):\n        result = parse_tool_arguments(\n            (\"limit=20\",), '{\"query\": \"hello\", \"limit\": 5}', self.SCHEMA\n        )\n        assert result == {\"query\": \"hello\", \"limit\": 20}\n\n    def test_value_containing_equals(self):\n        result = parse_tool_arguments((\"query=a=b=c\",), None, self.SCHEMA)\n        assert result == {\"query\": \"a=b=c\"}\n\n    def test_invalid_arg_format_exits(self):\n        with pytest.raises(SystemExit):\n            parse_tool_arguments((\"noequalssign\",), None, self.SCHEMA)\n\n    def test_invalid_input_json_exits(self):\n        with pytest.raises(SystemExit):\n            parse_tool_arguments((), \"not-valid-json\", self.SCHEMA)\n\n    def test_input_json_non_object_exits(self):\n        with pytest.raises(SystemExit):\n            parse_tool_arguments((), \"[1,2,3]\", self.SCHEMA)\n\n    def test_single_json_object_as_positional(self):\n        result = parse_tool_arguments(\n            ('{\"query\": \"hello\", \"limit\": 5}',), None, self.SCHEMA\n        )\n        assert result == {\"query\": \"hello\", \"limit\": 5}\n\n    def test_json_positional_ignored_when_input_json_set(self):\n        \"\"\"When --input-json is already provided, a JSON positional arg is not special.\"\"\"\n        with pytest.raises(SystemExit):\n            parse_tool_arguments(('{\"limit\": 99}',), '{\"query\": \"hello\"}', self.SCHEMA)\n\n    def test_coercion_error_exits(self):\n        with pytest.raises(SystemExit):\n            parse_tool_arguments((\"limit=abc\",), None, self.SCHEMA)\n\n\n# ---------------------------------------------------------------------------\n# format_tool_signature\n# ---------------------------------------------------------------------------\n\n\nclass TestFormatToolSignature:\n    def _make_tool(\n        self,\n        name: str = \"my_tool\",\n        properties: dict[str, Any] | None = None,\n        required: list[str] | None = None,\n        output_schema: dict[str, Any] | None = None,\n        description: str | None = None,\n    ) -> mcp.types.Tool:\n        input_schema: dict[str, Any] = {\"type\": \"object\"}\n        if properties is not None:\n            input_schema[\"properties\"] = properties\n        if required is not None:\n            input_schema[\"required\"] = required\n        return mcp.types.Tool(\n            name=name,\n            description=description,\n            inputSchema=input_schema,\n            outputSchema=output_schema,\n        )\n\n    def test_no_params(self):\n        tool = self._make_tool()\n        assert format_tool_signature(tool) == \"my_tool()\"\n\n    def test_required_param(self):\n        tool = self._make_tool(\n            properties={\"query\": {\"type\": \"string\"}},\n            required=[\"query\"],\n        )\n        assert format_tool_signature(tool) == \"my_tool(query: str)\"\n\n    def test_optional_param_with_default(self):\n        tool = self._make_tool(\n            properties={\"limit\": {\"type\": \"integer\", \"default\": 10}},\n        )\n        assert format_tool_signature(tool) == \"my_tool(limit: int = 10)\"\n\n    def test_optional_param_without_default(self):\n        tool = self._make_tool(\n            properties={\"limit\": {\"type\": \"integer\"}},\n        )\n        assert format_tool_signature(tool) == \"my_tool(limit: int = ...)\"\n\n    def test_mixed_required_and_optional(self):\n        tool = self._make_tool(\n            properties={\n                \"query\": {\"type\": \"string\"},\n                \"limit\": {\"type\": \"integer\", \"default\": 10},\n            },\n            required=[\"query\"],\n        )\n        sig = format_tool_signature(tool)\n        assert sig == \"my_tool(query: str, limit: int = 10)\"\n\n    def test_with_output_schema(self):\n        tool = self._make_tool(\n            properties={\"q\": {\"type\": \"string\"}},\n            required=[\"q\"],\n            output_schema={\"type\": \"object\"},\n        )\n        assert format_tool_signature(tool) == \"my_tool(q: str) -> dict\"\n\n    def test_anyof_type(self):\n        tool = self._make_tool(\n            properties={\"value\": {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}]}},\n            required=[\"value\"],\n        )\n        assert format_tool_signature(tool) == \"my_tool(value: str | int)\"\n\n\n# ---------------------------------------------------------------------------\n# resolve_server_spec\n# ---------------------------------------------------------------------------\n\n\nclass TestResolveServerSpec:\n    def test_http_url(self):\n        assert (\n            resolve_server_spec(\"http://localhost:8000/mcp\")\n            == \"http://localhost:8000/mcp\"\n        )\n\n    def test_https_url(self):\n        assert (\n            resolve_server_spec(\"https://example.com/mcp\") == \"https://example.com/mcp\"\n        )\n\n    def test_python_file_existing(self, tmp_path: Path):\n        py_file = tmp_path / \"server.py\"\n        py_file.write_text(\"# empty\")\n        result = resolve_server_spec(str(py_file))\n        assert isinstance(result, StdioTransport)\n        assert result.command == \"fastmcp\"\n        assert result.args == [\"run\", str(py_file.resolve()), \"--no-banner\"]\n\n    def test_json_mcp_config(self, tmp_path: Path):\n        config_file = tmp_path / \"mcp.json\"\n        config = {\"mcpServers\": {\"test\": {\"url\": \"http://localhost:8000\"}}}\n        config_file.write_text(json.dumps(config))\n        result = resolve_server_spec(str(config_file))\n        assert isinstance(result, dict)\n        assert \"mcpServers\" in result\n\n    def test_json_fastmcp_config_exits(self, tmp_path: Path):\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"type\": \"file\"}}))\n        with pytest.raises(SystemExit):\n            resolve_server_spec(str(config_file))\n\n    def test_json_not_found_exits(self, tmp_path: Path):\n        with pytest.raises(SystemExit):\n            resolve_server_spec(str(tmp_path / \"nonexistent.json\"))\n\n    def test_directory_exits(self, tmp_path: Path):\n        \"\"\"Directories should not be treated as file paths.\"\"\"\n        with pytest.raises(SystemExit):\n            resolve_server_spec(str(tmp_path))\n\n    def test_unrecognised_exits(self):\n        with pytest.raises(SystemExit):\n            resolve_server_spec(\"some_random_thing\")\n\n    def test_command_returns_stdio_transport(self):\n        result = resolve_server_spec(None, command=\"npx -y @mcp/server\")\n        assert isinstance(result, StdioTransport)\n        assert result.command == \"npx\"\n        assert result.args == [\"-y\", \"@mcp/server\"]\n\n    def test_command_single_word(self):\n        result = resolve_server_spec(None, command=\"myserver\")\n        assert isinstance(result, StdioTransport)\n        assert result.command == \"myserver\"\n        assert result.args == []\n\n    def test_server_spec_and_command_exits(self):\n        with pytest.raises(SystemExit):\n            resolve_server_spec(\"http://localhost:8000\", command=\"npx server\")\n\n    def test_neither_server_spec_nor_command_exits(self):\n        with pytest.raises(SystemExit):\n            resolve_server_spec(None)\n\n    def test_transport_sse_rewrites_url(self):\n        result = resolve_server_spec(\"http://localhost:8000/mcp\", transport=\"sse\")\n        assert result == \"http://localhost:8000/mcp/sse\"\n\n    def test_transport_sse_no_duplicate_suffix(self):\n        result = resolve_server_spec(\"http://localhost:8000/sse\", transport=\"sse\")\n        assert result == \"http://localhost:8000/sse\"\n\n    def test_transport_sse_trailing_slash(self):\n        result = resolve_server_spec(\"http://localhost:8000/mcp/\", transport=\"sse\")\n        assert result == \"http://localhost:8000/mcp/sse\"\n\n    def test_transport_http_leaves_url_unchanged(self):\n        result = resolve_server_spec(\"http://localhost:8000/mcp\", transport=\"http\")\n        assert result == \"http://localhost:8000/mcp\"\n\n\n# ---------------------------------------------------------------------------\n# _build_stdio_from_command\n# ---------------------------------------------------------------------------\n\n\nclass TestBuildStdioFromCommand:\n    def test_simple_command(self):\n        transport = _build_stdio_from_command(\"uvx my-server\")\n        assert transport.command == \"uvx\"\n        assert transport.args == [\"my-server\"]\n\n    def test_quoted_args(self):\n        transport = _build_stdio_from_command(\"npx -y '@scope/server'\")\n        assert transport.command == \"npx\"\n        assert transport.args == [\"-y\", \"@scope/server\"]\n\n    def test_empty_command_exits(self):\n        with pytest.raises(SystemExit):\n            _build_stdio_from_command(\"\")\n\n    def test_invalid_shell_syntax_exits(self):\n        with pytest.raises(SystemExit):\n            _build_stdio_from_command(\"npx 'unterminated\")\n\n\n# ---------------------------------------------------------------------------\n# _is_http_target\n# ---------------------------------------------------------------------------\n\n\nclass TestIsHttpTarget:\n    def test_http_url(self):\n        assert _is_http_target(\"http://localhost:8000\") is True\n\n    def test_https_url(self):\n        assert _is_http_target(\"https://example.com/mcp\") is True\n\n    def test_file_path(self):\n        assert _is_http_target(\"/path/to/server.py\") is False\n\n    def test_stdio_transport(self):\n        assert _is_http_target(StdioTransport(command=\"npx\", args=[])) is False\n\n    def test_mcp_config_dict(self):\n        \"\"\"MCPConfig dicts are not HTTP targets — auth is per-server internally.\"\"\"\n        assert _is_http_target({\"mcpServers\": {}}) is False\n\n\n# ---------------------------------------------------------------------------\n# _build_client\n# ---------------------------------------------------------------------------\n\n\nclass TestBuildClient:\n    def test_http_target_gets_oauth_by_default(self):\n        client = _build_client(\"http://localhost:8000/mcp\")\n        # OAuth is applied during Client init via _set_auth\n        assert client.transport.auth is not None\n\n    def test_stdio_target_no_auth(self):\n        transport = StdioTransport(command=\"npx\", args=[\"-y\", \"@mcp/server\"])\n        client = _build_client(transport)\n        # Stdio transports don't support auth — no auth should be set\n        assert not hasattr(client.transport, \"auth\") or client.transport.auth is None\n\n    def test_explicit_auth_none_disables_oauth(self):\n        client = _build_client(\"http://localhost:8000/mcp\", auth=\"none\")\n        # \"none\" explicitly disables auth, even for HTTP targets\n        assert client.transport.auth is None\n\n    def test_mcp_config_no_auth(self):\n        \"\"\"MCPConfig dicts handle auth per-server; no top-level auth applied.\"\"\"\n        client = _build_client({\"mcpServers\": {\"test\": {\"url\": \"http://localhost\"}}})\n        # MCPConfigTransport doesn't support _set_auth — no crash means success\n        assert client.transport is not None\n\n\n# ---------------------------------------------------------------------------\n# Integration tests — invoke actual CLI commands via monkeypatched _build_client\n# ---------------------------------------------------------------------------\n\n\ndef _build_test_server() -> FastMCP:\n    \"\"\"Create a minimal FastMCP server for integration tests.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    def greet(name: str) -> str:\n        \"\"\"Say hello to someone.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return a + b\n\n    @server.resource(\"test://greeting\")\n    def greeting_resource() -> str:\n        \"\"\"A static greeting resource.\"\"\"\n        return \"Hello from resource!\"\n\n    @server.prompt\n    def ask(topic: str) -> str:\n        \"\"\"Ask about a topic.\"\"\"\n        return f\"Tell me about {topic}\"\n\n    return server\n\n\n@pytest.fixture()\ndef _patch_client():\n    \"\"\"Patch resolve_server_spec and _build_client so CLI commands use the\n    in-process test server without needing a real transport.\"\"\"\n    server = _build_test_server()\n\n    def fake_resolve(server_spec: Any, **kwargs: Any) -> str:\n        return \"fake\"\n\n    def fake_build_client(resolved: Any, **kwargs: Any) -> Client:\n        return Client(server)\n\n    with (\n        patch.object(client_module, \"resolve_server_spec\", side_effect=fake_resolve),\n        patch.object(client_module, \"_build_client\", side_effect=fake_build_client),\n    ):\n        yield\n\n\nclass TestListCommandCLI:\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_list_tools(self, capsys: pytest.CaptureFixture[str]):\n        await list_command(\"fake://server\")\n        captured = capsys.readouterr()\n        assert \"greet\" in captured.out\n        assert \"add\" in captured.out\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_list_json(self, capsys: pytest.CaptureFixture[str]):\n        await list_command(\"fake://server\", json_output=True)\n        captured = capsys.readouterr()\n        data = json.loads(captured.out)\n        names = {t[\"name\"] for t in data[\"tools\"]}\n        assert \"greet\" in names\n        assert \"add\" in names\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_list_resources(self, capsys: pytest.CaptureFixture[str]):\n        await list_command(\"fake://server\", resources=True)\n        captured = capsys.readouterr()\n        assert \"test://greeting\" in captured.out\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_list_prompts(self, capsys: pytest.CaptureFixture[str]):\n        await list_command(\"fake://server\", prompts=True)\n        captured = capsys.readouterr()\n        assert \"ask\" in captured.out\n\n\nclass TestCallCommandCLI:\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_tool(self, capsys: pytest.CaptureFixture[str]):\n        await call_command(\"fake://server\", \"greet\", \"name=World\")\n        captured = capsys.readouterr()\n        assert \"Hello, World!\" in captured.out\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_tool_json(self, capsys: pytest.CaptureFixture[str]):\n        await call_command(\"fake://server\", \"greet\", \"name=World\", json_output=True)\n        captured = capsys.readouterr()\n        data = json.loads(captured.out)\n        assert data[\"is_error\"] is False\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_tool_not_found(self):\n        with pytest.raises(SystemExit):\n            await call_command(\"fake://server\", \"nonexistent\")\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_tool_missing_args(self):\n        with pytest.raises(SystemExit):\n            await call_command(\"fake://server\", \"greet\")\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_resource_by_uri(self, capsys: pytest.CaptureFixture[str]):\n        await call_command(\"fake://server\", \"test://greeting\")\n        captured = capsys.readouterr()\n        assert \"Hello from resource!\" in captured.out\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_resource_json(self, capsys: pytest.CaptureFixture[str]):\n        await call_command(\"fake://server\", \"test://greeting\", json_output=True)\n        captured = capsys.readouterr()\n        data = json.loads(captured.out)\n        assert isinstance(data, list)\n        assert data[0][\"text\"] == \"Hello from resource!\"\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_prompt(self, capsys: pytest.CaptureFixture[str]):\n        await call_command(\"fake://server\", \"ask\", \"topic=Python\", prompt=True)\n        captured = capsys.readouterr()\n        assert \"Python\" in captured.out\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_prompt_json(self, capsys: pytest.CaptureFixture[str]):\n        await call_command(\n            \"fake://server\", \"ask\", \"topic=Python\", prompt=True, json_output=True\n        )\n        captured = capsys.readouterr()\n        data = json.loads(captured.out)\n        assert \"messages\" in data\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_call_prompt_not_found(self):\n        with pytest.raises(SystemExit):\n            await call_command(\"fake://server\", \"nonexistent\", prompt=True)\n\n    async def test_call_missing_target(self):\n        with pytest.raises(SystemExit):\n            await call_command(\"fake://server\", \"\")\n\n\n# ---------------------------------------------------------------------------\n# Structured content serialization\n# ---------------------------------------------------------------------------\n\n\nclass TestFormatCallResult:\n    def test_structured_content_uses_dict_not_data(\n        self, capsys: pytest.CaptureFixture[str]\n    ):\n        \"\"\"structured_content (raw dict) is used for display, not data (which may\n        be a non-serializable dataclass).\"\"\"\n        result = CallToolResult(\n            content=[mcp.types.TextContent(type=\"text\", text=\"ok\")],\n            structured_content={\"key\": \"value\"},\n            meta=None,\n            data=object(),  # non-serializable on purpose\n            is_error=False,\n        )\n        # Should not raise — uses structured_content, not data\n        _format_call_result_text(result)\n        captured = capsys.readouterr()\n        assert \"value\" in captured.out\n\n    def test_escapes_rich_markup_and_control_chars(\n        self, capsys: pytest.CaptureFixture[str]\n    ):\n        result = CallToolResult(\n            content=[mcp.types.TextContent(type=\"text\", text=\"[red]x[/red]\\x1b[2J\")],\n            structured_content=None,\n            meta=None,\n            data=None,\n            is_error=False,\n        )\n\n        _format_call_result_text(result)\n        captured = capsys.readouterr()\n        assert \"[red]x[/red]\" in captured.out\n        assert \"\\\\x1b\" in captured.out\n        assert \"\\x1b\" not in captured.out\n\n\nclass TestSanitizeUntrustedText:\n    def test_sanitize_untrusted_text(self):\n        value = \"[bold]hello[/bold]\\x07\"\n        sanitized = _sanitize_untrusted_text(value)\n        assert sanitized == \"\\\\[bold]hello\\\\[/bold]\\\\x07\"\n"
  },
  {
    "path": "tests/cli/test_config.py",
    "content": "\"\"\"Tests for FastMCP configuration file support with nested structure.\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\n\nimport pytest\nfrom pydantic import ValidationError\n\nfrom fastmcp.utilities.mcp_server_config import (\n    Deployment,\n    MCPServerConfig,\n)\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\n\nclass TestFileSystemSource:\n    \"\"\"Test FileSystemSource class.\"\"\"\n\n    def test_dict_source_minimal(self):\n        \"\"\"Test that dict source is converted to FileSystemSource.\"\"\"\n        config = MCPServerConfig(source={\"path\": \"server.py\"})\n        # Dict is converted to FileSystemSource\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"server.py\"\n        assert config.source.entrypoint is None\n        assert config.source.type == \"filesystem\"\n\n    def test_dict_source_with_entrypoint(self):\n        \"\"\"Test dict source with entrypoint field.\"\"\"\n        config = MCPServerConfig(source={\"path\": \"server.py\", \"entrypoint\": \"app\"})\n        # Dict with entrypoint is converted to FileSystemSource\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"server.py\"\n        assert config.source.entrypoint == \"app\"\n        assert config.source.type == \"filesystem\"\n\n    def test_filesystem_source_entrypoint(self):\n        \"\"\"Test FileSystemSource entrypoint format.\"\"\"\n        config = MCPServerConfig(\n            source=FileSystemSource(path=\"src/server.py\", entrypoint=\"mcp\")\n        )\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"src/server.py\"\n        assert config.source.entrypoint == \"mcp\"\n        assert config.source.type == \"filesystem\"\n\n\nclass TestEnvironment:\n    \"\"\"Test Environment class.\"\"\"\n\n    def test_environment_config_fields(self):\n        \"\"\"Test all Environment fields.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={\n                \"python\": \"3.12\",\n                \"dependencies\": [\"requests\", \"numpy>=2.0\"],\n                \"requirements\": \"requirements.txt\",\n                \"project\": \".\",\n                \"editable\": [\"../my-package\"],\n            },\n        )\n\n        env = config.environment\n        assert env.python == \"3.12\"\n        assert env.dependencies == [\"requests\", \"numpy>=2.0\"]\n        # Paths are stored as Path objects\n        assert env.requirements == Path(\"requirements.txt\")\n        assert env.project == Path(\".\")\n        assert env.editable == [Path(\"../my-package\")]\n\n    def test_needs_uv(self):\n        \"\"\"Test needs_uv() method.\"\"\"\n        # No environment config - doesn't need UV\n        config = MCPServerConfig(source={\"path\": \"server.py\"})\n        assert not config.environment._must_run_with_uv()\n\n        # Empty environment - doesn't need UV\n        config = MCPServerConfig(source={\"path\": \"server.py\"}, environment={})\n        assert not config.environment._must_run_with_uv()\n\n        # With dependencies - needs UV\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"}, environment={\"dependencies\": [\"requests\"]}\n        )\n        assert config.environment._must_run_with_uv()\n\n        # With Python version - needs UV\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"}, environment={\"python\": \"3.12\"}\n        )\n        assert config.environment._must_run_with_uv()\n\n    def test_build_uv_run_command(self):\n        \"\"\"Test build_uv_run_command() method.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={\n                \"python\": \"3.12\",\n                \"dependencies\": [\"requests\", \"numpy\"],\n                \"requirements\": \"requirements.txt\",\n                \"project\": \".\",\n            },\n        )\n\n        cmd = config.environment.build_command([\"fastmcp\", \"run\", \"server.py\"])\n\n        assert cmd[0] == \"uv\"\n        assert cmd[1] == \"run\"\n        # Python version not added when project is specified (project defines its own Python)\n        assert \"--python\" not in cmd\n        assert \"3.12\" not in cmd\n        assert \"--project\" in cmd\n        # Project path should be resolved to absolute path\n        project_idx = cmd.index(\"--project\")\n        assert Path(cmd[project_idx + 1]).is_absolute()\n        assert \"--with\" in cmd\n        assert \"requests\" in cmd\n        assert \"numpy\" in cmd\n        assert \"--with-requirements\" in cmd\n        # Requirements path should be resolved to absolute path\n        req_idx = cmd.index(\"--with-requirements\")\n        assert Path(cmd[req_idx + 1]).is_absolute()\n        # Command args should be at the end\n        assert \"fastmcp\" in cmd[-3:]\n        assert \"run\" in cmd[-2:]\n        assert \"server.py\" in cmd[-1:]\n\n\nclass TestDeployment:\n    \"\"\"Test Deployment class.\"\"\"\n\n    def test_deployment_config_fields(self):\n        \"\"\"Test all Deployment fields.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            deployment={\n                \"transport\": \"http\",\n                \"host\": \"0.0.0.0\",\n                \"port\": 8000,\n                \"path\": \"/api/\",\n                \"log_level\": \"DEBUG\",\n                \"env\": {\"API_KEY\": \"secret\"},\n                \"cwd\": \"./work\",\n                \"args\": [\"--debug\"],\n            },\n        )\n\n        deploy = config.deployment\n        assert deploy.transport == \"http\"\n        assert deploy.host == \"0.0.0.0\"\n        assert deploy.port == 8000\n        assert deploy.path == \"/api/\"\n        assert deploy.log_level == \"DEBUG\"\n        assert deploy.env == {\"API_KEY\": \"secret\"}\n        assert deploy.cwd == \"./work\"\n        assert deploy.args == [\"--debug\"]\n\n    def test_apply_runtime_settings(self, tmp_path):\n        \"\"\"Test apply_runtime_settings() method.\"\"\"\n        import os\n\n        # Create config with env vars and cwd\n        work_dir = tmp_path / \"work\"\n        work_dir.mkdir()\n\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            deployment={\n                \"env\": {\"TEST_VAR\": \"test_value\"},\n                \"cwd\": \"work\",\n            },\n        )\n\n        original_cwd = os.getcwd()\n        original_env = os.environ.get(\"TEST_VAR\")\n\n        try:\n            config.deployment.apply_runtime_settings(tmp_path / \"fastmcp.json\")\n\n            # Check environment variable was set\n            assert os.environ[\"TEST_VAR\"] == \"test_value\"\n\n            # Check working directory was changed\n            assert Path.cwd() == work_dir.resolve()\n\n        finally:\n            # Restore original state\n            os.chdir(original_cwd)\n            if original_env is None:\n                os.environ.pop(\"TEST_VAR\", None)\n            else:\n                os.environ[\"TEST_VAR\"] = original_env\n\n    def test_env_var_interpolation(self, tmp_path):\n        \"\"\"Test environment variable interpolation in deployment env.\"\"\"\n        import os\n\n        # Set up test environment variables\n        os.environ[\"BASE_URL\"] = \"example.com\"\n        os.environ[\"ENV_NAME\"] = \"production\"\n\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            deployment={\n                \"env\": {\n                    \"API_URL\": \"https://api.${BASE_URL}/v1\",\n                    \"DATABASE\": \"postgres://${ENV_NAME}.db\",\n                    \"PREFIXED\": \"MY_${ENV_NAME}_SERVER\",\n                    \"MISSING\": \"value_${NONEXISTENT}_here\",\n                    \"STATIC\": \"no_interpolation\",\n                }\n            },\n        )\n\n        original_values = {\n            key: os.environ.get(key)\n            for key in [\"API_URL\", \"DATABASE\", \"PREFIXED\", \"MISSING\", \"STATIC\"]\n        }\n\n        try:\n            config.deployment.apply_runtime_settings()\n\n            # Check interpolated values\n            assert os.environ[\"API_URL\"] == \"https://api.example.com/v1\"\n            assert os.environ[\"DATABASE\"] == \"postgres://production.db\"\n            assert os.environ[\"PREFIXED\"] == \"MY_production_SERVER\"\n            # Missing variables should keep the placeholder\n            assert os.environ[\"MISSING\"] == \"value_${NONEXISTENT}_here\"\n            # Static values should remain unchanged\n            assert os.environ[\"STATIC\"] == \"no_interpolation\"\n\n        finally:\n            # Clean up\n            os.environ.pop(\"BASE_URL\", None)\n            os.environ.pop(\"ENV_NAME\", None)\n            for key, value in original_values.items():\n                if value is None:\n                    os.environ.pop(key, None)\n                else:\n                    os.environ[key] = value\n\n\nclass TestMCPServerConfig:\n    \"\"\"Test MCPServerConfig root configuration.\"\"\"\n\n    def test_minimal_config(self):\n        \"\"\"Test creating a config with only required fields.\"\"\"\n        config = MCPServerConfig(source={\"path\": \"server.py\"})\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"server.py\"\n        assert config.source.entrypoint is None\n        # Environment and deployment are now always present but empty\n        assert isinstance(config.environment, UVEnvironment)\n        assert isinstance(config.deployment, Deployment)\n        # Check they have no values set\n        assert not config.environment._must_run_with_uv()\n        assert all(\n            getattr(config.deployment, field, None) is None\n            for field in Deployment.model_fields\n        )\n\n    def test_nested_structure(self):\n        \"\"\"Test the nested configuration structure.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={\n                \"python\": \"3.12\",\n                \"dependencies\": [\"fastmcp\"],\n            },\n            deployment={\n                \"transport\": \"stdio\",\n                \"log_level\": \"INFO\",\n            },\n        )\n\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"server.py\"\n        assert config.source.entrypoint is None\n        assert isinstance(config.environment, UVEnvironment)\n        assert isinstance(config.deployment, Deployment)\n\n    def test_from_file(self, tmp_path):\n        \"\"\"Test loading config from JSON file with nested structure.\"\"\"\n        config_data = {\n            \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n            \"source\": {\"path\": \"src/server.py\", \"entrypoint\": \"app\"},\n            \"environment\": {\"python\": \"3.12\", \"dependencies\": [\"requests\"]},\n            \"deployment\": {\"transport\": \"http\", \"port\": 8000},\n        }\n\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps(config_data))\n\n        config = MCPServerConfig.from_file(config_file)\n\n        # When loaded from JSON with entrypoint format, it becomes EntrypointConfig\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"src/server.py\"\n        assert config.source.entrypoint == \"app\"\n        assert config.environment.python == \"3.12\"\n        assert config.environment.dependencies == [\"requests\"]\n        assert config.deployment.transport == \"http\"\n        assert config.deployment.port == 8000\n\n    def test_from_file_with_string_entrypoint(self, tmp_path):\n        \"\"\"Test loading config with dict source format.\"\"\"\n        config_data = {\n            \"source\": {\"path\": \"server.py\", \"entrypoint\": \"mcp\"},\n            \"environment\": {\"dependencies\": [\"fastmcp\"]},\n        }\n\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps(config_data))\n\n        config = MCPServerConfig.from_file(config_file)\n        # String entrypoint with : should be converted to EntrypointConfig\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"server.py\"\n        assert config.source.entrypoint == \"mcp\"\n\n    def test_string_entrypoint_with_entrypoint_and_environment(self, tmp_path):\n        \"\"\"Test that file.py:entrypoint syntax works with environment config.\"\"\"\n        config_data = {\n            \"source\": {\"path\": \"src/server.py\", \"entrypoint\": \"app\"},\n            \"environment\": {\"python\": \"3.12\", \"dependencies\": [\"fastmcp\", \"requests\"]},\n            \"deployment\": {\"transport\": \"http\", \"port\": 8000},\n        }\n\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps(config_data))\n\n        config = MCPServerConfig.from_file(config_file)\n\n        # Should be parsed into EntrypointConfig\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"src/server.py\"\n        assert config.source.entrypoint == \"app\"\n\n        # Environment config should still work\n        assert config.environment.python == \"3.12\"\n        assert config.environment.dependencies == [\"fastmcp\", \"requests\"]\n\n        # Deployment config should still work\n        assert config.deployment.transport == \"http\"\n        assert config.deployment.port == 8000\n\n    def test_find_config_in_current_dir(self, tmp_path):\n        \"\"\"Test finding config in current directory.\"\"\"\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"path\": \"server.py\"}}))\n\n        original_cwd = os.getcwd()\n        try:\n            os.chdir(tmp_path)\n            found = MCPServerConfig.find_config()\n            assert found == config_file\n        finally:\n            os.chdir(original_cwd)\n\n    def test_find_config_not_in_parent_dir(self, tmp_path):\n        \"\"\"Test that config is NOT found in parent directory.\"\"\"\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"path\": \"server.py\"}}))\n\n        subdir = tmp_path / \"subdir\"\n        subdir.mkdir()\n\n        # Should NOT find config in parent directory\n        found = MCPServerConfig.find_config(subdir)\n        assert found is None\n\n    def test_find_config_in_specified_dir(self, tmp_path):\n        \"\"\"Test finding config in the specified directory.\"\"\"\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"path\": \"server.py\"}}))\n\n        # Should find config when looking in the directory that contains it\n        found = MCPServerConfig.find_config(tmp_path)\n        assert found == config_file\n\n    def test_find_config_not_found(self, tmp_path):\n        \"\"\"Test when config is not found.\"\"\"\n        found = MCPServerConfig.find_config(tmp_path)\n        assert found is None\n\n    def test_invalid_transport(self, tmp_path):\n        \"\"\"Test loading config with invalid transport value.\"\"\"\n        config_data = {\n            \"source\": {\"path\": \"server.py\"},\n            \"deployment\": {\"transport\": \"invalid_transport\"},\n        }\n\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps(config_data))\n\n        with pytest.raises(ValidationError):\n            MCPServerConfig.from_file(config_file)\n\n    def test_optional_sections(self):\n        \"\"\"Test that all config sections are optional except source.\"\"\"\n        # Only source is required\n        config = MCPServerConfig(source={\"path\": \"server.py\"})\n        assert isinstance(config.source, FileSystemSource)\n        assert config.source.path == \"server.py\"\n        # Environment and deployment are now always present but may be empty\n        assert isinstance(config.environment, UVEnvironment)\n        assert isinstance(config.deployment, Deployment)\n\n        # Only environment with values\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"}, environment={\"python\": \"3.12\"}\n        )\n        assert config.environment.python == \"3.12\"\n        assert isinstance(config.deployment, Deployment)\n        assert all(\n            getattr(config.deployment, field, None) is None\n            for field in Deployment.model_fields\n        )\n\n        # Only deployment with values\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"}, deployment={\"transport\": \"http\"}\n        )\n        assert isinstance(config.environment, UVEnvironment)\n        # Check all fields except 'type' which has a default value\n        assert all(\n            getattr(config.environment, field, None) is None\n            for field in UVEnvironment.model_fields\n            if field != \"type\"\n        )\n        assert config.deployment.transport == \"http\"\n\n\nclass TestMCPServerConfigRoundtrip:\n    \"\"\"Test that MCPServerConfig survives model_dump() -> reconstruct pattern.\n\n    This is used by the CLI to apply overrides immutably.\n    \"\"\"\n\n    def test_roundtrip_preserves_schema(self):\n        \"\"\"Ensure schema_ field survives dump/reconstruct cycle.\"\"\"\n        config = MCPServerConfig(source=FileSystemSource(path=\"server.py\"))\n        config_dict = config.model_dump()\n        reconstructed = MCPServerConfig(**config_dict)\n        assert reconstructed.schema_ == config.schema_\n\n    def test_roundtrip_with_all_fields(self):\n        \"\"\"Full config survives dump/reconstruct.\"\"\"\n        config = MCPServerConfig(\n            source=FileSystemSource(path=\"server.py\", entrypoint=\"app\"),\n            environment=UVEnvironment(python=\"3.11\"),\n            deployment=Deployment(transport=\"http\", port=8080),\n        )\n        config_dict = config.model_dump()\n        reconstructed = MCPServerConfig(**config_dict)\n        assert reconstructed.source.path == \"server.py\"\n        assert reconstructed.environment.python == \"3.11\"\n        assert reconstructed.deployment.port == 8080\n"
  },
  {
    "path": "tests/cli/test_cursor.py",
    "content": "import base64\nimport json\nfrom pathlib import Path\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom fastmcp.cli.install.cursor import (\n    cursor_command,\n    generate_cursor_deeplink,\n    install_cursor,\n    install_cursor_workspace,\n    open_deeplink,\n)\nfrom fastmcp.mcp_config import StdioMCPServer\n\n\nclass TestCursorDeeplinkGeneration:\n    \"\"\"Test cursor deeplink generation functionality.\"\"\"\n\n    def test_generate_deeplink_basic(self):\n        \"\"\"Test basic deeplink generation.\"\"\"\n        server_config = StdioMCPServer(\n            command=\"uv\",\n            args=[\"run\", \"--with\", \"fastmcp\", \"fastmcp\", \"run\", \"server.py\"],\n        )\n\n        deeplink = generate_cursor_deeplink(\"test-server\", server_config)\n\n        assert deeplink.startswith(\"cursor://anysphere.cursor-deeplink/mcp/install?\")\n        assert \"name=test-server\" in deeplink\n        assert \"config=\" in deeplink\n\n        # Verify base64 encoding\n        config_part = deeplink.split(\"config=\")[1]\n        decoded = base64.urlsafe_b64decode(config_part).decode()\n        config_data = json.loads(decoded)\n\n        assert config_data[\"command\"] == \"uv\"\n        assert config_data[\"args\"] == [\n            \"run\",\n            \"--with\",\n            \"fastmcp\",\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n\n    def test_generate_deeplink_with_env_vars(self):\n        \"\"\"Test deeplink generation with environment variables.\"\"\"\n        server_config = StdioMCPServer(\n            command=\"uv\",\n            args=[\"run\", \"--with\", \"fastmcp\", \"fastmcp\", \"run\", \"server.py\"],\n            env={\"API_KEY\": \"secret123\", \"DEBUG\": \"true\"},\n        )\n\n        deeplink = generate_cursor_deeplink(\"my-server\", server_config)\n\n        # Decode and verify\n        config_part = deeplink.split(\"config=\")[1]\n        decoded = base64.urlsafe_b64decode(config_part).decode()\n        config_data = json.loads(decoded)\n\n        assert config_data[\"env\"] == {\"API_KEY\": \"secret123\", \"DEBUG\": \"true\"}\n\n    def test_generate_deeplink_special_characters(self):\n        \"\"\"Test deeplink generation with special characters in server name.\"\"\"\n        server_config = StdioMCPServer(\n            command=\"uv\",\n            args=[\"run\", \"--with\", \"fastmcp\", \"fastmcp\", \"run\", \"server.py\"],\n        )\n\n        # Test with spaces and special chars in name - should be URL encoded\n        deeplink = generate_cursor_deeplink(\"my server (test)\", server_config)\n\n        # Spaces and parentheses must be URL-encoded\n        assert \"name=my%20server%20%28test%29\" in deeplink\n        # Ensure no unencoded version appears\n        assert \"name=my server (test)\" not in deeplink\n\n    def test_generate_deeplink_empty_config(self):\n        \"\"\"Test deeplink generation with minimal config.\"\"\"\n        server_config = StdioMCPServer(command=\"python\", args=[\"server.py\"])\n\n        deeplink = generate_cursor_deeplink(\"minimal\", server_config)\n\n        config_part = deeplink.split(\"config=\")[1]\n        decoded = base64.urlsafe_b64decode(config_part).decode()\n        config_data = json.loads(decoded)\n\n        assert config_data[\"command\"] == \"python\"\n        assert config_data[\"args\"] == [\"server.py\"]\n        assert config_data[\"env\"] == {}  # Empty env dict is included\n\n    def test_generate_deeplink_complex_args(self):\n        \"\"\"Test deeplink generation with complex arguments.\"\"\"\n        server_config = StdioMCPServer(\n            command=\"uv\",\n            args=[\n                \"run\",\n                \"--with\",\n                \"fastmcp\",\n                \"--with\",\n                \"numpy>=1.20\",\n                \"--with-editable\",\n                \"/path/to/local/package\",\n                \"fastmcp\",\n                \"run\",\n                \"server.py:CustomServer\",\n            ],\n        )\n\n        deeplink = generate_cursor_deeplink(\"complex-server\", server_config)\n\n        config_part = deeplink.split(\"config=\")[1]\n        decoded = base64.urlsafe_b64decode(config_part).decode()\n        config_data = json.loads(decoded)\n\n        assert \"--with-editable\" in config_data[\"args\"]\n        assert \"server.py:CustomServer\" in config_data[\"args\"]\n\n    def test_generate_deeplink_url_injection_protection(self):\n        \"\"\"Test that special characters in server name are properly URL-encoded to prevent injection.\"\"\"\n        server_config = StdioMCPServer(\n            command=\"python\",\n            args=[\"server.py\"],\n        )\n\n        # Test the PoC case from the security advisory\n        deeplink = generate_cursor_deeplink(\"test&calc\", server_config)\n\n        # The & should be encoded as %26, preventing it from being interpreted as a query parameter separator\n        assert \"name=test%26calc\" in deeplink\n        assert \"name=test&calc\" not in deeplink\n\n        # Verify the URL structure is intact\n        assert deeplink.startswith(\"cursor://anysphere.cursor-deeplink/mcp/install?\")\n        assert deeplink.count(\"&\") == 1  # Only one & between name and config parameters\n\n        # Test other potentially dangerous characters\n        dangerous_names = [\n            (\"test|calc\", \"test%7Ccalc\"),\n            (\"test;calc\", \"test%3Bcalc\"),\n            (\"test<calc\", \"test%3Ccalc\"),\n            (\"test>calc\", \"test%3Ecalc\"),\n            (\"test`calc\", \"test%60calc\"),\n            (\"test$calc\", \"test%24calc\"),\n            (\"test'calc\", \"test%27calc\"),\n            ('test\"calc', \"test%22calc\"),\n            (\"test calc\", \"test%20calc\"),\n            (\"test#anchor\", \"test%23anchor\"),\n            (\"test?query=val\", \"test%3Fquery%3Dval\"),\n        ]\n\n        for dangerous_name, expected_encoded in dangerous_names:\n            deeplink = generate_cursor_deeplink(dangerous_name, server_config)\n            assert f\"name={expected_encoded}\" in deeplink, (\n                f\"Failed to encode {dangerous_name}\"\n            )\n            # Ensure no unencoded special chars that could break URL structure\n            name_part = deeplink.split(\"name=\")[1].split(\"&\")[0]\n            assert name_part == expected_encoded\n\n\nclass TestOpenDeeplink:\n    \"\"\"Test deeplink opening functionality.\"\"\"\n\n    @patch(\"subprocess.run\")\n    def test_open_deeplink_macos(self, mock_run):\n        \"\"\"Test opening deeplink on macOS.\"\"\"\n        with patch(\"sys.platform\", \"darwin\"):\n            mock_run.return_value = Mock(returncode=0)\n\n            result = open_deeplink(\"cursor://test\")\n\n            assert result is True\n            mock_run.assert_called_once_with(\n                [\"open\", \"cursor://test\"], check=True, capture_output=True\n            )\n\n    def test_open_deeplink_windows(self):\n        \"\"\"Test opening deeplink on Windows.\"\"\"\n        with patch(\"sys.platform\", \"win32\"):\n            with patch(\n                \"fastmcp.cli.install.shared.os.startfile\", create=True\n            ) as mock_startfile:\n                result = open_deeplink(\"cursor://test\")\n\n                assert result is True\n                mock_startfile.assert_called_once_with(\"cursor://test\")\n\n    @patch(\"subprocess.run\")\n    def test_open_deeplink_linux(self, mock_run):\n        \"\"\"Test opening deeplink on Linux.\"\"\"\n        with patch(\"sys.platform\", \"linux\"):\n            mock_run.return_value = Mock(returncode=0)\n\n            result = open_deeplink(\"cursor://test\")\n\n            assert result is True\n            mock_run.assert_called_once_with(\n                [\"xdg-open\", \"cursor://test\"], check=True, capture_output=True\n            )\n\n    @patch(\"subprocess.run\")\n    def test_open_deeplink_failure(self, mock_run):\n        \"\"\"Test handling of deeplink opening failure.\"\"\"\n        import subprocess\n\n        with patch(\"sys.platform\", \"darwin\"):\n            mock_run.side_effect = subprocess.CalledProcessError(1, [\"open\"])\n\n            result = open_deeplink(\"cursor://test\")\n\n            assert result is False\n\n    @patch(\"subprocess.run\")\n    def test_open_deeplink_command_not_found(self, mock_run):\n        \"\"\"Test handling when open command is not found.\"\"\"\n        with patch(\"sys.platform\", \"darwin\"):\n            mock_run.side_effect = FileNotFoundError()\n\n            result = open_deeplink(\"cursor://test\")\n\n            assert result is False\n\n    def test_open_deeplink_invalid_scheme(self):\n        \"\"\"Test that non-cursor:// URLs are rejected.\"\"\"\n        result = open_deeplink(\"http://malicious.com\")\n        assert result is False\n\n        result = open_deeplink(\"https://example.com\")\n        assert result is False\n\n        result = open_deeplink(\"file:///etc/passwd\")\n        assert result is False\n\n    def test_open_deeplink_valid_cursor_scheme(self):\n        \"\"\"Test that cursor:// URLs are accepted.\"\"\"\n        with patch(\"sys.platform\", \"darwin\"):\n            with patch(\"subprocess.run\") as mock_run:\n                mock_run.return_value = Mock(returncode=0)\n                result = open_deeplink(\"cursor://anysphere.cursor-deeplink/mcp/install\")\n                assert result is True\n\n    def test_open_deeplink_empty_url(self):\n        \"\"\"Test handling of empty URL.\"\"\"\n        result = open_deeplink(\"\")\n        assert result is False\n\n    def test_open_deeplink_windows_oserror(self):\n        \"\"\"Test handling of OSError on Windows.\"\"\"\n        with patch(\"sys.platform\", \"win32\"):\n            with patch(\n                \"fastmcp.cli.install.shared.os.startfile\", create=True\n            ) as mock_startfile:\n                mock_startfile.side_effect = OSError(\"File not found\")\n                result = open_deeplink(\"cursor://test\")\n                assert result is False\n\n\nclass TestInstallCursor:\n    \"\"\"Test cursor installation functionality.\"\"\"\n\n    @patch(\"fastmcp.cli.install.cursor.open_deeplink\")\n    @patch(\"fastmcp.cli.install.cursor.print\")\n    def test_install_cursor_success(self, mock_print, mock_open_deeplink):\n        \"\"\"Test successful cursor installation.\"\"\"\n        mock_open_deeplink.return_value = True\n\n        result = install_cursor(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n        )\n\n        assert result is True\n        mock_open_deeplink.assert_called_once()\n        # Verify the deeplink was generated correctly\n        call_args = mock_open_deeplink.call_args[0][0]\n        assert call_args.startswith(\"cursor://anysphere.cursor-deeplink/mcp/install?\")\n        assert \"name=test-server\" in call_args\n\n    @patch(\"fastmcp.cli.install.cursor.open_deeplink\")\n    @patch(\"fastmcp.cli.install.cursor.print\")\n    def test_install_cursor_with_packages(self, mock_print, mock_open_deeplink):\n        \"\"\"Test cursor installation with additional packages.\"\"\"\n        mock_open_deeplink.return_value = True\n\n        result = install_cursor(\n            file=Path(\"/path/to/server.py\"),\n            server_object=\"app\",\n            name=\"test-server\",\n            with_packages=[\"numpy\", \"pandas\"],\n            env_vars={\"API_KEY\": \"test\"},\n        )\n\n        assert result is True\n        call_args = mock_open_deeplink.call_args[0][0]\n\n        # Decode the config to verify packages\n        config_part = call_args.split(\"config=\")[1]\n        decoded = base64.urlsafe_b64decode(config_part).decode()\n        config_data = json.loads(decoded)\n\n        # Check that all packages are included\n        assert \"--with\" in config_data[\"args\"]\n        assert \"numpy\" in config_data[\"args\"]\n        assert \"pandas\" in config_data[\"args\"]\n        assert \"fastmcp\" in config_data[\"args\"]\n        assert config_data[\"env\"] == {\"API_KEY\": \"test\"}\n\n    @patch(\"fastmcp.cli.install.cursor.open_deeplink\")\n    @patch(\"fastmcp.cli.install.cursor.print\")\n    def test_install_cursor_with_editable(self, mock_print, mock_open_deeplink):\n        \"\"\"Test cursor installation with editable package.\"\"\"\n        mock_open_deeplink.return_value = True\n\n        # Use an absolute path that works on all platforms\n        editable_path = Path.cwd() / \"local\" / \"package\"\n\n        result = install_cursor(\n            file=Path(\"/path/to/server.py\"),\n            server_object=\"custom_app\",\n            name=\"test-server\",\n            with_editable=[editable_path],\n        )\n\n        assert result is True\n        call_args = mock_open_deeplink.call_args[0][0]\n\n        # Decode and verify editable path\n        config_part = call_args.split(\"config=\")[1]\n        decoded = base64.urlsafe_b64decode(config_part).decode()\n        config_data = json.loads(decoded)\n\n        assert \"--with-editable\" in config_data[\"args\"]\n        # Check that the path was resolved (should be absolute)\n        editable_idx = config_data[\"args\"].index(\"--with-editable\") + 1\n        resolved_path = config_data[\"args\"][editable_idx]\n        assert Path(resolved_path).is_absolute()\n        assert \"server.py:custom_app\" in \" \".join(config_data[\"args\"])\n\n    @patch(\"fastmcp.cli.install.cursor.open_deeplink\")\n    @patch(\"fastmcp.cli.install.cursor.print\")\n    def test_install_cursor_failure(self, mock_print, mock_open_deeplink):\n        \"\"\"Test cursor installation when deeplink fails to open.\"\"\"\n        mock_open_deeplink.return_value = False\n\n        result = install_cursor(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n        )\n\n        assert result is False\n        # Verify failure message was printed\n        mock_print.assert_called()\n\n    def test_install_cursor_workspace_path_is_file(self, tmp_path):\n        \"\"\"Test that passing a file as workspace_path returns False.\"\"\"\n        file_path = tmp_path / \"somefile.txt\"\n        file_path.write_text(\"hello\")\n\n        result = install_cursor_workspace(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n            workspace_path=file_path,\n        )\n\n        assert result is False\n\n    def test_install_cursor_deduplicate_packages(self):\n        \"\"\"Test that duplicate packages are deduplicated.\"\"\"\n        with patch(\"fastmcp.cli.install.cursor.open_deeplink\") as mock_open:\n            mock_open.return_value = True\n\n            install_cursor(\n                file=Path(\"/path/to/server.py\"),\n                server_object=None,\n                name=\"test-server\",\n                with_packages=[\"numpy\", \"fastmcp\", \"numpy\", \"pandas\", \"fastmcp\"],\n            )\n\n            call_args = mock_open.call_args[0][0]\n            config_part = call_args.split(\"config=\")[1]\n            decoded = base64.urlsafe_b64decode(config_part).decode()\n            config_data = json.loads(decoded)\n\n            # Count occurrences of each package\n            args_str = \" \".join(config_data[\"args\"])\n            assert args_str.count(\"--with numpy\") == 1\n            assert args_str.count(\"--with pandas\") == 1\n            assert args_str.count(\"--with fastmcp\") == 1\n\n\nclass TestCursorCommand:\n    \"\"\"Test the cursor CLI command.\"\"\"\n\n    @patch(\"fastmcp.cli.install.cursor.install_cursor\")\n    @patch(\"fastmcp.cli.install.cursor.process_common_args\")\n    async def test_cursor_command_basic(self, mock_process_args, mock_install):\n        \"\"\"Test basic cursor command execution.\"\"\"\n        mock_process_args.return_value = (\n            Path(\"server.py\"),\n            None,\n            \"test-server\",\n            [],\n            {},\n        )\n        mock_install.return_value = True\n\n        with patch(\"sys.exit\") as mock_exit:\n            await cursor_command(\"server.py\")\n\n        mock_install.assert_called_once_with(\n            file=Path(\"server.py\"),\n            server_object=None,\n            name=\"test-server\",\n            with_editable=[],\n            with_packages=[],\n            env_vars={},\n            python_version=None,\n            with_requirements=None,\n            project=None,\n            workspace=None,\n        )\n        mock_exit.assert_not_called()\n\n    @patch(\"fastmcp.cli.install.cursor.install_cursor\")\n    @patch(\"fastmcp.cli.install.cursor.process_common_args\")\n    async def test_cursor_command_failure(self, mock_process_args, mock_install):\n        \"\"\"Test cursor command when installation fails.\"\"\"\n        mock_process_args.return_value = (\n            Path(\"server.py\"),\n            None,\n            \"test-server\",\n            [],\n            {},\n        )\n        mock_install.return_value = False\n\n        with pytest.raises(SystemExit) as exc_info:\n            await cursor_command(\"server.py\")\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n"
  },
  {
    "path": "tests/cli/test_discovery.py",
    "content": "\"\"\"Tests for MCP server discovery and name-based resolution.\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nimport yaml\n\nfrom fastmcp.cli.client import _is_http_target, resolve_server_spec\nfrom fastmcp.cli.discovery import (\n    DiscoveredServer,\n    _normalize_server_entry,\n    _parse_mcp_config,\n    _scan_claude_code,\n    _scan_claude_desktop,\n    _scan_cursor_workspace,\n    _scan_gemini,\n    _scan_goose,\n    _scan_project_mcp_json,\n    discover_servers,\n    resolve_name,\n)\nfrom fastmcp.client.transports.http import StreamableHttpTransport\nfrom fastmcp.client.transports.sse import SSETransport\nfrom fastmcp.client.transports.stdio import StdioTransport\nfrom fastmcp.mcp_config import RemoteMCPServer, StdioMCPServer\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n_STDIO_CONFIG: dict[str, Any] = {\n    \"mcpServers\": {\n        \"weather\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@mcp/weather\"],\n        },\n        \"github\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@mcp/github\"],\n            \"env\": {\"GITHUB_TOKEN\": \"xxx\"},\n        },\n    }\n}\n\n_REMOTE_CONFIG: dict[str, Any] = {\n    \"mcpServers\": {\n        \"api\": {\n            \"url\": \"http://localhost:8000/mcp\",\n        },\n    }\n}\n\n\ndef _write_config(path: Path, data: dict[str, Any]) -> None:\n    path.parent.mkdir(parents=True, exist_ok=True)\n    path.write_text(json.dumps(data))\n\n\n# ---------------------------------------------------------------------------\n# DiscoveredServer properties\n# ---------------------------------------------------------------------------\n\n\nclass TestDiscoveredServer:\n    def test_qualified_name(self):\n        server = DiscoveredServer(\n            name=\"weather\",\n            source=\"claude-desktop\",\n            config=StdioMCPServer(command=\"npx\", args=[\"-y\", \"@mcp/weather\"]),\n            config_path=Path(\"/fake/config.json\"),\n        )\n        assert server.qualified_name == \"claude-desktop:weather\"\n\n    def test_transport_summary_stdio(self):\n        server = DiscoveredServer(\n            name=\"weather\",\n            source=\"cursor\",\n            config=StdioMCPServer(command=\"npx\", args=[\"-y\", \"@mcp/weather\"]),\n            config_path=Path(\"/fake/config.json\"),\n        )\n        assert server.transport_summary == \"stdio: npx -y @mcp/weather\"\n\n    def test_transport_summary_remote(self):\n        server = DiscoveredServer(\n            name=\"api\",\n            source=\"project\",\n            config=RemoteMCPServer(url=\"http://localhost:8000/mcp\"),\n            config_path=Path(\"/fake/config.json\"),\n        )\n        assert server.transport_summary == \"http: http://localhost:8000/mcp\"\n\n    def test_transport_summary_remote_sse(self):\n        server = DiscoveredServer(\n            name=\"api\",\n            source=\"project\",\n            config=RemoteMCPServer(url=\"http://localhost:8000/sse\", transport=\"sse\"),\n            config_path=Path(\"/fake/config.json\"),\n        )\n        assert server.transport_summary == \"sse: http://localhost:8000/sse\"\n\n\n# ---------------------------------------------------------------------------\n# _parse_mcp_config\n# ---------------------------------------------------------------------------\n\n\nclass TestParseMcpConfig:\n    def test_valid_config(self, tmp_path: Path):\n        path = tmp_path / \"config.json\"\n        _write_config(path, _STDIO_CONFIG)\n        servers = _parse_mcp_config(path, \"test-source\")\n        assert len(servers) == 2\n        names = {s.name for s in servers}\n        assert names == {\"weather\", \"github\"}\n        assert all(s.source == \"test-source\" for s in servers)\n        assert all(s.config_path == path for s in servers)\n\n    def test_missing_file(self, tmp_path: Path):\n        path = tmp_path / \"nonexistent.json\"\n        servers = _parse_mcp_config(path, \"test\")\n        assert servers == []\n\n    def test_invalid_json(self, tmp_path: Path):\n        path = tmp_path / \"bad.json\"\n        path.write_text(\"{not json\")\n        servers = _parse_mcp_config(path, \"test\")\n        assert servers == []\n\n    def test_no_mcp_servers_key(self, tmp_path: Path):\n        path = tmp_path / \"config.json\"\n        _write_config(path, {\"something\": \"else\"})\n        servers = _parse_mcp_config(path, \"test\")\n        assert servers == []\n\n    def test_empty_mcp_servers(self, tmp_path: Path):\n        path = tmp_path / \"config.json\"\n        _write_config(path, {\"mcpServers\": {}})\n        servers = _parse_mcp_config(path, \"test\")\n        assert servers == []\n\n    def test_remote_server(self, tmp_path: Path):\n        path = tmp_path / \"config.json\"\n        _write_config(path, _REMOTE_CONFIG)\n        servers = _parse_mcp_config(path, \"test\")\n        assert len(servers) == 1\n        assert isinstance(servers[0].config, RemoteMCPServer)\n        assert servers[0].config.url == \"http://localhost:8000/mcp\"\n\n\n# ---------------------------------------------------------------------------\n# Scanner: Claude Desktop\n# ---------------------------------------------------------------------------\n\n\nclass TestScanClaudeDesktop:\n    def test_finds_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        config_dir = tmp_path / \"Claude\"\n        config_path = config_dir / \"claude_desktop_config.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        # Force darwin for deterministic path\n        monkeypatch.setattr(\"fastmcp.cli.discovery.sys.platform\", \"darwin\")\n\n        # We need to override the path construction. On macOS it's\n        # ~/Library/Application Support/Claude — create that.\n        mac_dir = tmp_path / \"Library\" / \"Application Support\" / \"Claude\"\n        mac_path = mac_dir / \"claude_desktop_config.json\"\n        _write_config(mac_path, _STDIO_CONFIG)\n\n        servers = _scan_claude_desktop()\n        assert len(servers) == 2\n        assert all(s.source == \"claude-desktop\" for s in servers)\n\n    def test_missing_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        monkeypatch.setattr(\"fastmcp.cli.discovery.sys.platform\", \"darwin\")\n        servers = _scan_claude_desktop()\n        assert servers == []\n\n\n# ---------------------------------------------------------------------------\n# Normalize server entry\n# ---------------------------------------------------------------------------\n\n\nclass TestNormalizeServerEntry:\n    def test_remote_type_becomes_transport(self):\n        entry = {\"url\": \"http://localhost:8000/sse\", \"type\": \"sse\"}\n        result = _normalize_server_entry(entry)\n        assert result[\"transport\"] == \"sse\"\n        assert \"type\" not in result\n\n    def test_remote_with_transport_unchanged(self):\n        entry = {\"url\": \"http://localhost:8000/mcp\", \"transport\": \"http\"}\n        result = _normalize_server_entry(entry)\n        assert result[\"transport\"] == \"http\"\n\n    def test_stdio_type_unchanged(self):\n        \"\"\"Stdio entries have ``type`` as a proper field — leave it alone.\"\"\"\n        entry = {\"command\": \"npx\", \"args\": [], \"type\": \"stdio\"}\n        result = _normalize_server_entry(entry)\n        assert result[\"type\"] == \"stdio\"\n\n    def test_gemini_http_url_becomes_url(self):\n        entry = {\"httpUrl\": \"https://api.example.com/mcp/\"}\n        result = _normalize_server_entry(entry)\n        assert result[\"url\"] == \"https://api.example.com/mcp/\"\n        assert \"httpUrl\" not in result\n\n    def test_gemini_http_url_does_not_override_url(self):\n        entry = {\"url\": \"http://real.com\", \"httpUrl\": \"http://other.com\"}\n        result = _normalize_server_entry(entry)\n        assert result[\"url\"] == \"http://real.com\"\n\n\n# ---------------------------------------------------------------------------\n# Scanner: Claude Code\n# ---------------------------------------------------------------------------\n\n\ndef _claude_code_config(\n    *,\n    global_servers: dict[str, Any] | None = None,\n    project_path: str | None = None,\n    project_servers: dict[str, Any] | None = None,\n) -> dict[str, Any]:\n    \"\"\"Build a minimal ~/.claude.json structure.\"\"\"\n    data: dict[str, Any] = {}\n    if global_servers is not None:\n        data[\"mcpServers\"] = global_servers\n    if project_path and project_servers is not None:\n        data[\"projects\"] = {project_path: {\"mcpServers\": project_servers}}\n    return data\n\n\nclass TestScanClaudeCode:\n    def test_global_servers(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        config_path = tmp_path / \".claude.json\"\n        _write_config(\n            config_path,\n            _claude_code_config(global_servers=_STDIO_CONFIG[\"mcpServers\"]),\n        )\n        servers = _scan_claude_code(tmp_path)\n        assert len(servers) == 2\n        assert all(s.source == \"claude-code\" for s in servers)\n\n    def test_project_servers(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        project_dir = tmp_path / \"my-project\"\n        project_dir.mkdir()\n        config_path = tmp_path / \".claude.json\"\n        _write_config(\n            config_path,\n            _claude_code_config(\n                project_path=str(project_dir),\n                project_servers={\"api\": {\"url\": \"http://localhost:8000/mcp\"}},\n            ),\n        )\n        servers = _scan_claude_code(project_dir)\n        assert len(servers) == 1\n        assert servers[0].name == \"api\"\n\n    def test_global_and_project_combined(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        project_dir = tmp_path / \"proj\"\n        project_dir.mkdir()\n        config_path = tmp_path / \".claude.json\"\n        _write_config(\n            config_path,\n            _claude_code_config(\n                global_servers={\"global-tool\": {\"command\": \"echo\", \"args\": [\"hi\"]}},\n                project_path=str(project_dir),\n                project_servers={\"local-tool\": {\"command\": \"cat\", \"args\": []}},\n            ),\n        )\n        servers = _scan_claude_code(project_dir)\n        names = {s.name for s in servers}\n        assert names == {\"global-tool\", \"local-tool\"}\n\n    def test_type_normalized_to_transport(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Claude Code uses ``type: sse`` — verify it becomes ``transport``.\"\"\"\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        config_path = tmp_path / \".claude.json\"\n        _write_config(\n            config_path,\n            _claude_code_config(\n                global_servers={\n                    \"sse-server\": {\n                        \"type\": \"sse\",\n                        \"url\": \"http://localhost:8000/sse\",\n                    }\n                }\n            ),\n        )\n        servers = _scan_claude_code(tmp_path)\n        assert len(servers) == 1\n        assert isinstance(servers[0].config, RemoteMCPServer)\n        assert servers[0].config.transport == \"sse\"\n\n    def test_missing_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        servers = _scan_claude_code(tmp_path)\n        assert servers == []\n\n    def test_no_matching_project(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        config_path = tmp_path / \".claude.json\"\n        _write_config(\n            config_path,\n            _claude_code_config(\n                project_path=\"/some/other/project\",\n                project_servers={\"tool\": {\"command\": \"echo\", \"args\": []}},\n            ),\n        )\n        servers = _scan_claude_code(tmp_path)\n        assert servers == []\n\n\n# ---------------------------------------------------------------------------\n# Scanner: Cursor workspace\n# ---------------------------------------------------------------------------\n\n\nclass TestScanCursorWorkspace:\n    def test_finds_config_in_cwd(self, tmp_path: Path):\n        cursor_path = tmp_path / \".cursor\" / \"mcp.json\"\n        _write_config(cursor_path, _STDIO_CONFIG)\n        servers = _scan_cursor_workspace(tmp_path)\n        assert len(servers) == 2\n        assert all(s.source == \"cursor\" for s in servers)\n\n    def test_finds_config_in_parent(self, tmp_path: Path):\n        cursor_path = tmp_path / \".cursor\" / \"mcp.json\"\n        _write_config(cursor_path, _STDIO_CONFIG)\n        child = tmp_path / \"src\" / \"deep\"\n        child.mkdir(parents=True)\n        servers = _scan_cursor_workspace(child)\n        assert len(servers) == 2\n\n    def test_stops_at_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        # Place config above home — should not be found\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        above_home = tmp_path.parent / \".cursor\" / \"mcp.json\"\n        _write_config(above_home, _STDIO_CONFIG)\n        child = tmp_path / \"project\"\n        child.mkdir()\n        servers = _scan_cursor_workspace(child)\n        assert servers == []\n\n    def test_no_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        # Confine walk to tmp_path so it doesn't find sibling test dirs\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        servers = _scan_cursor_workspace(tmp_path)\n        assert servers == []\n\n\n# ---------------------------------------------------------------------------\n# Scanner: project mcp.json\n# ---------------------------------------------------------------------------\n\n\nclass TestScanProjectMcpJson:\n    def test_finds_config(self, tmp_path: Path):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        servers = _scan_project_mcp_json(tmp_path)\n        assert len(servers) == 2\n        assert all(s.source == \"project\" for s in servers)\n\n    def test_no_config(self, tmp_path: Path):\n        servers = _scan_project_mcp_json(tmp_path)\n        assert servers == []\n\n\n# ---------------------------------------------------------------------------\n# Scanner: Gemini CLI\n# ---------------------------------------------------------------------------\n\n\nclass TestScanGemini:\n    def test_user_level_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        config_path = tmp_path / \".gemini\" / \"settings.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        servers = _scan_gemini(tmp_path)\n        assert len(servers) == 2\n        assert all(s.source == \"gemini\" for s in servers)\n\n    def test_project_level_config(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        project_dir = tmp_path / \"my-project\"\n        project_dir.mkdir()\n        config_path = project_dir / \".gemini\" / \"settings.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        servers = _scan_gemini(project_dir)\n        assert len(servers) == 2\n\n    def test_http_url_normalized(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Gemini uses ``httpUrl`` — verify it becomes ``url``.\"\"\"\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        config_path = tmp_path / \".gemini\" / \"settings.json\"\n        _write_config(\n            config_path,\n            {\n                \"mcpServers\": {\n                    \"api\": {\"httpUrl\": \"https://api.example.com/mcp/\"},\n                }\n            },\n        )\n        servers = _scan_gemini(tmp_path)\n        assert len(servers) == 1\n        assert isinstance(servers[0].config, RemoteMCPServer)\n        assert servers[0].config.url == \"https://api.example.com/mcp/\"\n\n    def test_missing_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        servers = _scan_gemini(tmp_path)\n        assert servers == []\n\n\n# ---------------------------------------------------------------------------\n# Scanner: Goose\n# ---------------------------------------------------------------------------\n\n_GOOSE_CONFIG = {\n    \"extensions\": {\n        \"developer\": {\n            \"enabled\": True,\n            \"name\": \"developer\",\n            \"type\": \"builtin\",\n        },\n        \"tavily\": {\n            \"cmd\": \"npx\",\n            \"args\": [\"-y\", \"mcp-tavily-search\"],\n            \"enabled\": True,\n            \"envs\": {\"TAVILY_API_KEY\": \"xxx\"},\n            \"type\": \"stdio\",\n        },\n        \"disabled-tool\": {\n            \"cmd\": \"echo\",\n            \"args\": [\"hi\"],\n            \"enabled\": False,\n            \"type\": \"stdio\",\n        },\n    }\n}\n\n\nclass TestScanGoose:\n    def test_finds_stdio_extensions(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        monkeypatch.delenv(\"XDG_CONFIG_HOME\", raising=False)\n        config_dir = tmp_path / \".config\" / \"goose\"\n        config_path = config_dir / \"config.yaml\"\n        config_path.parent.mkdir(parents=True)\n        config_path.write_text(yaml.dump(_GOOSE_CONFIG))\n        # Force non-windows platform for path logic\n        monkeypatch.setattr(\"fastmcp.cli.discovery.sys.platform\", \"linux\")\n        servers = _scan_goose()\n        assert len(servers) == 1\n        assert servers[0].name == \"tavily\"\n        assert servers[0].source == \"goose\"\n        assert isinstance(servers[0].config, StdioMCPServer)\n        assert servers[0].config.command == \"npx\"\n\n    def test_skips_builtin_and_disabled(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        monkeypatch.delenv(\"XDG_CONFIG_HOME\", raising=False)\n        config_dir = tmp_path / \".config\" / \"goose\"\n        config_path = config_dir / \"config.yaml\"\n        config_path.parent.mkdir(parents=True)\n        config_path.write_text(yaml.dump(_GOOSE_CONFIG))\n        monkeypatch.setattr(\"fastmcp.cli.discovery.sys.platform\", \"linux\")\n        servers = _scan_goose()\n        names = {s.name for s in servers}\n        assert \"developer\" not in names\n        assert \"disabled-tool\" not in names\n\n    def test_missing_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n        monkeypatch.setattr(\"fastmcp.cli.discovery.sys.platform\", \"linux\")\n        servers = _scan_goose()\n        assert servers == []\n\n\n# ---------------------------------------------------------------------------\n# discover_servers\n# ---------------------------------------------------------------------------\n\n\ndef _suppress_user_scanners(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Suppress all scanners that read real user config files.\"\"\"\n    monkeypatch.setattr(\"fastmcp.cli.discovery._scan_claude_desktop\", lambda: [])\n    monkeypatch.setattr(\"fastmcp.cli.discovery._scan_claude_code\", lambda start_dir: [])\n    monkeypatch.setattr(\"fastmcp.cli.discovery._scan_gemini\", lambda start_dir: [])\n    monkeypatch.setattr(\"fastmcp.cli.discovery._scan_goose\", lambda: [])\n\n\nclass TestDiscoverServers:\n    def test_combines_sources(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        # Set up project mcp.json\n        project_config = tmp_path / \"mcp.json\"\n        _write_config(project_config, _STDIO_CONFIG)\n\n        # Set up cursor config\n        cursor_config = tmp_path / \".cursor\" / \"mcp.json\"\n        _write_config(cursor_config, _REMOTE_CONFIG)\n\n        _suppress_user_scanners(monkeypatch)\n\n        servers = discover_servers(start_dir=tmp_path)\n        sources = {s.source for s in servers}\n        assert \"project\" in sources\n        assert \"cursor\" in sources\n        assert len(servers) == 3  # 2 from project + 1 from cursor\n\n    def test_preserves_duplicates(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Same server name in multiple sources should appear multiple times.\"\"\"\n        project_config = tmp_path / \"mcp.json\"\n        _write_config(project_config, _STDIO_CONFIG)\n\n        cursor_config = tmp_path / \".cursor\" / \"mcp.json\"\n        _write_config(cursor_config, _STDIO_CONFIG)\n\n        _suppress_user_scanners(monkeypatch)\n\n        servers = discover_servers(start_dir=tmp_path)\n        weather_servers = [s for s in servers if s.name == \"weather\"]\n        assert len(weather_servers) == 2\n        assert {s.source for s in weather_servers} == {\"cursor\", \"project\"}\n\n\n# ---------------------------------------------------------------------------\n# resolve_name\n# ---------------------------------------------------------------------------\n\n\nclass TestResolveName:\n    @pytest.fixture(autouse=True)\n    def _isolate_scanners(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Suppress scanners that read real user configs and confine walks to tmp_path.\"\"\"\n        _suppress_user_scanners(monkeypatch)\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n\n    def test_unique_match(self, tmp_path: Path):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        transport = resolve_name(\"weather\", start_dir=tmp_path)\n        assert isinstance(transport, StdioTransport)\n\n    def test_qualified_match(self, tmp_path: Path):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        transport = resolve_name(\"project:weather\", start_dir=tmp_path)\n        assert isinstance(transport, StdioTransport)\n\n    def test_not_found_with_servers(self, tmp_path: Path):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        with pytest.raises(ValueError, match=\"No server named 'nope'.*Available\"):\n            resolve_name(\"nope\", start_dir=tmp_path)\n\n    def test_not_found_no_servers(self, tmp_path: Path):\n        with pytest.raises(ValueError, match=\"No server named 'nope'.*Searched\"):\n            resolve_name(\"nope\", start_dir=tmp_path)\n\n    def test_ambiguous_name(self, tmp_path: Path):\n        project_config = tmp_path / \"mcp.json\"\n        _write_config(project_config, _STDIO_CONFIG)\n        cursor_config = tmp_path / \".cursor\" / \"mcp.json\"\n        _write_config(cursor_config, _STDIO_CONFIG)\n        with pytest.raises(ValueError, match=\"Ambiguous server name 'weather'\"):\n            resolve_name(\"weather\", start_dir=tmp_path)\n\n    def test_ambiguous_resolved_by_qualified(self, tmp_path: Path):\n        project_config = tmp_path / \"mcp.json\"\n        _write_config(project_config, _STDIO_CONFIG)\n        cursor_config = tmp_path / \".cursor\" / \"mcp.json\"\n        _write_config(cursor_config, _STDIO_CONFIG)\n        transport = resolve_name(\"cursor:weather\", start_dir=tmp_path)\n        assert isinstance(transport, StdioTransport)\n\n    def test_qualified_not_found(self, tmp_path: Path):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        with pytest.raises(\n            ValueError, match=\"No server named 'nope' found in source 'project'\"\n        ):\n            resolve_name(\"project:nope\", start_dir=tmp_path)\n\n    def test_remote_server_resolves_to_http_transport(self, tmp_path: Path):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _REMOTE_CONFIG)\n        transport = resolve_name(\"api\", start_dir=tmp_path)\n        assert isinstance(transport, StreamableHttpTransport)\n\n\n# ---------------------------------------------------------------------------\n# Integration: resolve_server_spec falls through to name resolution\n# ---------------------------------------------------------------------------\n\n\nclass TestResolveServerSpecNameFallback:\n    def test_bare_name_resolves(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        config_path = tmp_path / \"mcp.json\"\n        _write_config(config_path, _STDIO_CONFIG)\n        _suppress_user_scanners(monkeypatch)\n        monkeypatch.setattr(\"fastmcp.cli.discovery.Path.home\", lambda: tmp_path)\n\n        # Monkeypatch resolve_name in client module to use our tmp_path\n        original_resolve = resolve_name\n\n        def patched_resolve(name: str, start_dir: Path | None = None) -> Any:\n            return original_resolve(name, start_dir=tmp_path)\n\n        monkeypatch.setattr(\"fastmcp.cli.client.resolve_name\", patched_resolve)\n\n        result = resolve_server_spec(\"weather\")\n        assert isinstance(result, StdioTransport)\n\n    def test_url_takes_priority_over_name(self):\n        \"\"\"URLs should be resolved before name lookup.\"\"\"\n        result = resolve_server_spec(\"http://localhost:8000/mcp\")\n        assert result == \"http://localhost:8000/mcp\"\n\n\n# ---------------------------------------------------------------------------\n# Integration: _is_http_target detects transport objects\n# ---------------------------------------------------------------------------\n\n\nclass TestIsHttpTargetTransports:\n    def test_streamable_http_transport(self):\n        transport = StreamableHttpTransport(\"http://localhost:8000/mcp\")\n        assert _is_http_target(transport) is True\n\n    def test_sse_transport(self):\n        transport = SSETransport(\"http://localhost:8000/sse\")\n        assert _is_http_target(transport) is True\n\n    def test_stdio_transport(self):\n        transport = StdioTransport(command=\"echo\", args=[\"hello\"])\n        assert _is_http_target(transport) is False\n\n    def test_string_url(self):\n        assert _is_http_target(\"http://localhost:8000\") is True\n\n    def test_string_non_url(self):\n        assert _is_http_target(\"server.py\") is False\n\n    def test_dict_config(self):\n        assert _is_http_target({\"mcpServers\": {}}) is False\n"
  },
  {
    "path": "tests/cli/test_generate_cli.py",
    "content": "\"\"\"Tests for fastmcp generate-cli command.\"\"\"\n\nimport sys\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import patch\n\nimport mcp.types\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.cli import generate as generate_module\nfrom fastmcp.cli.client import Client\nfrom fastmcp.cli.generate import (\n    _derive_server_name,\n    _param_to_cli_flag,\n    _schema_to_python_type,\n    _schema_type_label,\n    _to_python_identifier,\n    _tool_function_source,\n    generate_cli_command,\n    generate_cli_script,\n    generate_skill_content,\n    serialize_transport,\n)\nfrom fastmcp.client.transports.stdio import StdioTransport\n\n# ---------------------------------------------------------------------------\n# _schema_to_python_type\n# ---------------------------------------------------------------------------\n\n\nclass TestSchemaToPythonType:\n    def test_simple_string(self):\n        py_type, needs_json = _schema_to_python_type({\"type\": \"string\"})\n        assert py_type == \"str\"\n        assert needs_json is False\n\n    def test_simple_integer(self):\n        py_type, needs_json = _schema_to_python_type({\"type\": \"integer\"})\n        assert py_type == \"int\"\n        assert needs_json is False\n\n    def test_simple_number(self):\n        py_type, needs_json = _schema_to_python_type({\"type\": \"number\"})\n        assert py_type == \"float\"\n        assert needs_json is False\n\n    def test_simple_boolean(self):\n        py_type, needs_json = _schema_to_python_type({\"type\": \"boolean\"})\n        assert py_type == \"bool\"\n        assert needs_json is False\n\n    def test_array_of_strings(self):\n        py_type, needs_json = _schema_to_python_type(\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n        )\n        assert py_type == \"list[str]\"\n        assert needs_json is False\n\n    def test_array_of_integers(self):\n        py_type, needs_json = _schema_to_python_type(\n            {\"type\": \"array\", \"items\": {\"type\": \"integer\"}}\n        )\n        assert py_type == \"list[int]\"\n        assert needs_json is False\n\n    def test_complex_object(self):\n        py_type, needs_json = _schema_to_python_type({\"type\": \"object\"})\n        assert py_type == \"str\"\n        assert needs_json is True\n\n    def test_complex_nested_array(self):\n        py_type, needs_json = _schema_to_python_type(\n            {\"type\": \"array\", \"items\": {\"type\": \"object\"}}\n        )\n        assert py_type == \"str\"\n        assert needs_json is True\n\n    def test_union_of_simple_types(self):\n        py_type, needs_json = _schema_to_python_type({\"type\": [\"string\", \"null\"]})\n        assert py_type == \"str | None\"\n        assert needs_json is False\n\n\n# ---------------------------------------------------------------------------\n# _to_python_identifier\n# ---------------------------------------------------------------------------\n\n\nclass TestToPythonIdentifier:\n    def test_plain_name(self):\n        assert _to_python_identifier(\"hello\") == \"hello\"\n\n    def test_hyphens(self):\n        assert _to_python_identifier(\"get-forecast\") == \"get_forecast\"\n\n    def test_dots_and_slashes(self):\n        assert _to_python_identifier(\"a.b/c\") == \"a_b_c\"\n\n    def test_leading_digit(self):\n        assert _to_python_identifier(\"3d_render\") == \"_3d_render\"\n\n    def test_spaces(self):\n        assert _to_python_identifier(\"my tool\") == \"my_tool\"\n\n    def test_empty_string(self):\n        assert _to_python_identifier(\"\") == \"_unnamed\"\n\n\n# ---------------------------------------------------------------------------\n# serialize_transport\n# ---------------------------------------------------------------------------\n\n\nclass TestSerializeTransport:\n    def test_url_string(self):\n        code, imports = serialize_transport(\"http://localhost:8000/mcp\")\n        assert code == \"'http://localhost:8000/mcp'\"\n        assert imports == set()\n\n    def test_stdio_transport_basic(self):\n        transport = StdioTransport(command=\"fastmcp\", args=[\"run\", \"server.py\"])\n        code, imports = serialize_transport(transport)\n        assert \"StdioTransport\" in code\n        assert \"command='fastmcp'\" in code\n        assert \"args=['run', 'server.py']\" in code\n        assert \"from fastmcp.client.transports import StdioTransport\" in imports\n\n    def test_stdio_transport_with_env(self):\n        transport = StdioTransport(\n            command=\"python\", args=[\"-m\", \"myserver\"], env={\"KEY\": \"val\"}\n        )\n        code, imports = serialize_transport(transport)\n        assert \"env={'KEY': 'val'}\" in code\n\n    def test_dict_passthrough(self):\n        d: dict[str, Any] = {\"mcpServers\": {\"test\": {\"url\": \"http://localhost\"}}}\n        code, imports = serialize_transport(d)\n        assert \"mcpServers\" in code\n        assert imports == set()\n\n\n# ---------------------------------------------------------------------------\n# _tool_function_source\n# ---------------------------------------------------------------------------\n\n\nclass TestToolFunctionSource:\n    def test_required_param(self):\n        tool = mcp.types.Tool(\n            name=\"greet\",\n            inputSchema={\n                \"properties\": {\"name\": {\"type\": \"string\", \"description\": \"Who\"}},\n                \"required\": [\"name\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        assert \"async def greet(\" in source\n        assert \"name: Annotated[str\" in source\n        assert \"= None\" not in source\n        assert \"_call_tool('greet', {'name': name})\" in source\n\n    def test_optional_param(self):\n        tool = mcp.types.Tool(\n            name=\"search\",\n            inputSchema={\n                \"properties\": {\n                    \"query\": {\"type\": \"string\", \"description\": \"Search query\"},\n                    \"limit\": {\"type\": \"integer\", \"description\": \"Max results\"},\n                },\n                \"required\": [\"query\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        assert \"query: Annotated[str\" in source\n        assert \"limit: Annotated[int | None\" in source\n        assert \"= None\" in source\n\n    def test_param_with_default(self):\n        tool = mcp.types.Tool(\n            name=\"fetch\",\n            inputSchema={\n                \"properties\": {\n                    \"url\": {\"type\": \"string\", \"description\": \"URL\"},\n                    \"timeout\": {\n                        \"type\": \"integer\",\n                        \"description\": \"Timeout\",\n                        \"default\": 30,\n                    },\n                },\n                \"required\": [\"url\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        assert \"timeout: Annotated[int\" in source\n        assert \"= 30\" in source\n\n    def test_no_params(self):\n        tool = mcp.types.Tool(\n            name=\"ping\",\n            inputSchema={\"properties\": {}},\n        )\n        source = _tool_function_source(tool)\n        assert \"async def ping(\" in source\n        assert \"_call_tool('ping', {})\" in source\n\n    def test_preserves_underscores(self):\n        tool = mcp.types.Tool(\n            name=\"get_forecast\",\n            inputSchema={\n                \"properties\": {\"city\": {\"type\": \"string\"}},\n                \"required\": [\"city\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        assert \"async def get_forecast(\" in source\n\n    def test_sanitizes_tool_name(self):\n        tool = mcp.types.Tool(\n            name=\"my.tool/v2\",\n            inputSchema={\"properties\": {}},\n        )\n        source = _tool_function_source(tool)\n        assert \"async def my_tool_v2(\" in source\n        assert \"name='my.tool/v2'\" in source\n\n    def test_sanitizes_param_name(self):\n        tool = mcp.types.Tool(\n            name=\"fetch\",\n            inputSchema={\n                \"properties\": {\"content-type\": {\"type\": \"string\", \"description\": \"CT\"}},\n                \"required\": [\"content-type\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        assert \"content_type: Annotated[str\" in source\n        assert \"'content-type': content_type\" in source\n\n    def test_description_in_docstring(self):\n        tool = mcp.types.Tool(\n            name=\"greet\",\n            description=\"Say hello to someone.\",\n            inputSchema={\n                \"properties\": {\"name\": {\"type\": \"string\"}},\n                \"required\": [\"name\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        assert \"'''Say hello to someone.'''\" in source\n\n    def test_description_with_quotes(self):\n        tool = mcp.types.Tool(\n            name=\"fetch\",\n            description=\"Fetch data from 'source' API.\",\n            inputSchema={\n                \"properties\": {\"url\": {\"type\": \"string\"}},\n                \"required\": [\"url\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        # Should escape single quotes in the description\n        assert r\"Fetch data from \\'source\\' API.\" in source\n        # Generated code should compile\n        compile(source, \"<test>\", \"exec\")\n\n    def test_array_of_strings_parameter(self):\n        tool = mcp.types.Tool(\n            name=\"tag_items\",\n            description=\"Tag multiple items.\",\n            inputSchema={\n                \"properties\": {\n                    \"item_id\": {\"type\": \"string\"},\n                    \"tags\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n                },\n                \"required\": [\"item_id\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        # Should use list[str] type with help metadata\n        assert \"tags: Annotated[list[str]\" in source\n        assert \"= []\" in source\n        # Should not have JSON parsing for simple arrays\n        assert \"json.loads\" not in source\n        compile(source, \"<test>\", \"exec\")\n\n    def test_complex_object_parameter(self):\n        tool = mcp.types.Tool(\n            name=\"create_user\",\n            description=\"Create a user.\",\n            inputSchema={\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"metadata\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"role\": {\"type\": \"string\"},\n                            \"dept\": {\"type\": \"string\"},\n                        },\n                    },\n                },\n                \"required\": [\"name\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        # Should use str type for complex object\n        assert \"metadata: Annotated[str | None\" in source\n        # Should include JSON schema in help (with escaped quotes)\n        assert \"JSON Schema:\" in source\n        assert '\\\\\"type\\\\\": \\\\\"object\\\\\"' in source\n        # Should have JSON parsing with isinstance check\n        assert (\n            \"metadata_parsed = json.loads(metadata) if isinstance(metadata, str) else metadata\"\n            in source\n        )\n        # Should use parsed version in call\n        assert \"'metadata': metadata_parsed\" in source\n        compile(source, \"<test>\", \"exec\")\n\n    def test_nested_array_parameter(self):\n        tool = mcp.types.Tool(\n            name=\"batch_process\",\n            description=\"Process batches.\",\n            inputSchema={\n                \"properties\": {\n                    \"batches\": {\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\"id\": {\"type\": \"string\"}},\n                        },\n                    },\n                },\n                \"required\": [\"batches\"],\n            },\n        )\n        source = _tool_function_source(tool)\n        # Nested arrays need JSON parsing\n        assert \"batches: Annotated[str\" in source\n        assert \"JSON Schema:\" in source\n        assert (\n            \"batches_parsed = json.loads(batches) if isinstance(batches, str) else batches\"\n            in source\n        )\n        compile(source, \"<test>\", \"exec\")\n\n    def test_complex_type_with_default(self):\n        \"\"\"Test that complex types with defaults are JSON-serialized.\"\"\"\n        tool = mcp.types.Tool(\n            name=\"configure\",\n            inputSchema={\n                \"properties\": {\n                    \"options\": {\n                        \"type\": \"object\",\n                        \"default\": {\"timeout\": 30, \"retry\": True},\n                    },\n                },\n            },\n        )\n        source = _tool_function_source(tool)\n        # Default should be JSON string, not Python dict\n        # pydantic_core.to_json produces compact JSON\n        assert '= \\'{\"timeout\":30,\"retry\":true}\\'' in source\n        # Should parse safely even with default\n        assert \"isinstance(options, str)\" in source\n        compile(source, \"<test>\", \"exec\")\n\n    def test_name_collision_detection(self):\n        \"\"\"Test that parameter name collisions are detected.\"\"\"\n        tool = mcp.types.Tool(\n            name=\"test\",\n            inputSchema={\n                \"properties\": {\n                    \"content-type\": {\"type\": \"string\"},\n                    \"content_type\": {\"type\": \"string\"},\n                },\n            },\n        )\n        # Should raise ValueError for collision\n        with pytest.raises(ValueError, match=\"both sanitize to 'content_type'\"):\n            _tool_function_source(tool)\n\n\n# ---------------------------------------------------------------------------\n# _derive_server_name\n# ---------------------------------------------------------------------------\n\n\nclass TestDeriveServerName:\n    def test_bare_name(self):\n        assert _derive_server_name(\"weather\") == \"weather\"\n\n    def test_qualified_name(self):\n        assert _derive_server_name(\"cursor:weather\") == \"weather\"\n\n    def test_python_file(self):\n        assert _derive_server_name(\"server.py\") == \"server\"\n\n    def test_url(self):\n        assert _derive_server_name(\"http://localhost:8000/mcp\") == \"localhost\"\n\n    def test_trailing_colon(self):\n        assert _derive_server_name(\"source:\") == \"source\"\n\n\n# ---------------------------------------------------------------------------\n# generate_cli_script — produces compilable Python\n# ---------------------------------------------------------------------------\n\n\nclass TestGenerateCliScript:\n    def _make_tools(self) -> list[mcp.types.Tool]:\n        return [\n            mcp.types.Tool(\n                name=\"greet\",\n                description=\"Say hello\",\n                inputSchema={\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\", \"description\": \"Who to greet\"},\n                    },\n                    \"required\": [\"name\"],\n                },\n            ),\n            mcp.types.Tool(\n                name=\"add_numbers\",\n                description=\"Add two numbers\",\n                inputSchema={\n                    \"properties\": {\n                        \"a\": {\"type\": \"integer\", \"description\": \"First number\"},\n                        \"b\": {\"type\": \"integer\", \"description\": \"Second number\"},\n                    },\n                    \"required\": [\"a\", \"b\"],\n                },\n            ),\n        ]\n\n    def test_compiles(self):\n        script = generate_cli_script(\n            server_name=\"test\",\n            server_spec=\"test\",\n            transport_code='\"http://localhost:8000/mcp\"',\n            extra_imports=set(),\n            tools=self._make_tools(),\n        )\n        compile(script, \"<generated>\", \"exec\")\n\n    def test_contains_tool_functions(self):\n        script = generate_cli_script(\n            server_name=\"test\",\n            server_spec=\"test\",\n            transport_code='\"http://localhost:8000/mcp\"',\n            extra_imports=set(),\n            tools=self._make_tools(),\n        )\n        assert \"async def greet(\" in script\n        assert \"async def add_numbers(\" in script\n\n    def test_contains_generic_commands(self):\n        script = generate_cli_script(\n            server_name=\"test\",\n            server_spec=\"test\",\n            transport_code='\"http://localhost:8000/mcp\"',\n            extra_imports=set(),\n            tools=[],\n        )\n        assert \"async def list_tools(\" in script\n        assert \"async def list_resources(\" in script\n        assert \"async def list_prompts(\" in script\n        assert \"async def read_resource(\" in script\n        assert \"async def get_prompt(\" in script\n\n    def test_embeds_transport(self):\n        script = generate_cli_script(\n            server_name=\"test\",\n            server_spec=\"test\",\n            transport_code=\"StdioTransport(command='fastmcp', args=['run', 'x.py'])\",\n            extra_imports={\"from fastmcp.client.transports import StdioTransport\"},\n            tools=[],\n        )\n        assert \"StdioTransport(command='fastmcp'\" in script\n        assert \"from fastmcp.client.transports import StdioTransport\" in script\n\n    def test_no_tools_still_valid(self):\n        script = generate_cli_script(\n            server_name=\"empty\",\n            server_spec=\"empty\",\n            transport_code='\"http://localhost\"',\n            extra_imports=set(),\n            tools=[],\n        )\n        compile(script, \"<generated>\", \"exec\")\n        assert \"call_tool_app\" in script\n\n    def test_server_name_with_quotes(self):\n        \"\"\"Test that server names with quotes are properly escaped.\"\"\"\n        script = generate_cli_script(\n            server_name='Test \"Server\" Name',\n            server_spec=\"test\",\n            transport_code='\"http://localhost\"',\n            extra_imports=set(),\n            tools=[],\n        )\n        # Should compile without syntax errors\n        compile(script, \"<generated>\", \"exec\")\n        # App name should have escaped quotes\n        assert r'app = cyclopts.App(name=\"test-\\\"server\\\"-name\"' in script\n\n    def test_compiles_with_unusual_names(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"my.tool/v2\",\n                description=\"A tool with dots and slashes\",\n                inputSchema={\n                    \"properties\": {\n                        \"content-type\": {\"type\": \"string\", \"description\": \"CT\"},\n                    },\n                    \"required\": [\"content-type\"],\n                },\n            ),\n        ]\n        script = generate_cli_script(\n            server_name=\"test\",\n            server_spec=\"test\",\n            transport_code='\"http://localhost:8000/mcp\"',\n            extra_imports=set(),\n            tools=tools,\n        )\n        compile(script, \"<generated>\", \"exec\")\n\n    def test_compiles_with_stdio_transport(self):\n        transport = StdioTransport(command=\"fastmcp\", args=[\"run\", \"server.py\"])\n        transport_code, extra_imports = serialize_transport(transport)\n        script = generate_cli_script(\n            server_name=\"test\",\n            server_spec=\"server.py\",\n            transport_code=transport_code,\n            extra_imports=extra_imports,\n            tools=self._make_tools(),\n        )\n        compile(script, \"<generated>\", \"exec\")\n\n\n# ---------------------------------------------------------------------------\n# generate_cli_command — integration tests\n# ---------------------------------------------------------------------------\n\n\ndef _build_test_server() -> FastMCP:\n    \"\"\"Create a minimal FastMCP server for integration tests.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    def greet(name: str) -> str:\n        \"\"\"Say hello to someone.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return a + b\n\n    @server.resource(\"test://greeting\")\n    def greeting_resource() -> str:\n        \"\"\"A static greeting resource.\"\"\"\n        return \"Hello from resource!\"\n\n    @server.prompt\n    def ask(topic: str) -> str:\n        \"\"\"Ask about a topic.\"\"\"\n        return f\"Tell me about {topic}\"\n\n    return server\n\n\n@pytest.fixture()\ndef _patch_client():\n    \"\"\"Patch resolve_server_spec and _build_client to use an in-process server.\"\"\"\n    server = _build_test_server()\n\n    def fake_resolve(server_spec: Any, **kwargs: Any) -> str:\n        return \"fake://server\"\n\n    def fake_build_client(resolved: Any, **kwargs: Any) -> Client:\n        return Client(server)\n\n    with (\n        patch.object(generate_module, \"resolve_server_spec\", side_effect=fake_resolve),\n        patch.object(generate_module, \"_build_client\", side_effect=fake_build_client),\n    ):\n        yield\n\n\nclass TestGenerateCliCommand:\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_writes_file(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        await generate_cli_command(\"test-server\", str(output))\n        assert output.exists()\n        content = output.read_text()\n        compile(content, str(output), \"exec\")\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_contains_tools(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        await generate_cli_command(\"test-server\", str(output))\n        content = output.read_text()\n        assert \"async def greet(\" in content\n        assert \"async def add(\" in content\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_default_output_path(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        monkeypatch.chdir(tmp_path)\n        await generate_cli_command(\"test-server\")\n        assert (tmp_path / \"cli.py\").exists()\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_error_if_exists(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        output.write_text(\"existing\")\n        with pytest.raises(SystemExit):\n            await generate_cli_command(\"test-server\", str(output))\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_force_overwrites(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        output.write_text(\"existing\")\n        await generate_cli_command(\"test-server\", str(output), force=True)\n        content = output.read_text()\n        assert content != \"existing\"\n        assert \"async def greet(\" in content\n\n    @pytest.mark.skipif(\n        sys.platform == \"win32\", reason=\"Unix executable bits N/A on Windows\"\n    )\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_file_is_executable(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        await generate_cli_command(\"test-server\", str(output))\n        assert output.stat().st_mode & 0o111\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_writes_skill_file(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        await generate_cli_command(\"test-server\", str(output))\n        skill_path = tmp_path / \"SKILL.md\"\n        assert skill_path.exists()\n        content = skill_path.read_text()\n        assert \"---\" in content\n        assert \"name:\" in content\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_skill_contains_tools(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        await generate_cli_command(\"test-server\", str(output))\n        content = (tmp_path / \"SKILL.md\").read_text()\n        assert \"### greet\" in content\n        assert \"### add\" in content\n        assert \"--name\" in content\n        assert \"call-tool greet\" in content\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_no_skill_flag(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        await generate_cli_command(\"test-server\", str(output), no_skill=True)\n        assert not (tmp_path / \"SKILL.md\").exists()\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_error_if_skill_exists(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        (tmp_path / \"SKILL.md\").write_text(\"existing\")\n        with pytest.raises(SystemExit):\n            await generate_cli_command(\"test-server\", str(output))\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_force_overwrites_skill(self, tmp_path: Path):\n        output = tmp_path / \"cli.py\"\n        (tmp_path / \"SKILL.md\").write_text(\"existing\")\n        await generate_cli_command(\"test-server\", str(output), force=True)\n        content = (tmp_path / \"SKILL.md\").read_text()\n        assert content != \"existing\"\n        assert \"### greet\" in content\n\n    @pytest.mark.usefixtures(\"_patch_client\")\n    async def test_skill_references_cli_filename(self, tmp_path: Path):\n        output = tmp_path / \"my_weather.py\"\n        await generate_cli_command(\"test-server\", str(output))\n        content = (tmp_path / \"SKILL.md\").read_text()\n        assert \"uv run --with fastmcp python my_weather.py\" in content\n\n\n# ---------------------------------------------------------------------------\n# _param_to_cli_flag\n# ---------------------------------------------------------------------------\n\n\nclass TestParamToCliFlag:\n    def test_simple_name(self):\n        assert _param_to_cli_flag(\"city\") == \"--city\"\n\n    def test_underscore_name(self):\n        assert _param_to_cli_flag(\"max_days\") == \"--max-days\"\n\n    def test_hyphenated_name(self):\n        # content-type → _to_python_identifier → content_type → --content-type\n        assert _param_to_cli_flag(\"content-type\") == \"--content-type\"\n\n    def test_digit_prefix(self):\n        # 3d_mode → _3d_mode → --3d-mode (leading underscore stripped)\n        assert _param_to_cli_flag(\"3d_mode\") == \"--3d-mode\"\n\n    def test_trailing_underscore(self):\n        # from → from_ after identifier sanitization; Cyclopts strips trailing \"-\"\n        assert _param_to_cli_flag(\"from\") == \"--from\"\n\n    def test_camel_case(self):\n        # camelCase → camel-case (cyclopts default_name_transform)\n        assert _param_to_cli_flag(\"myParam\") == \"--my-param\"\n\n    def test_pascal_case(self):\n        assert _param_to_cli_flag(\"MyParam\") == \"--my-param\"\n\n\n# ---------------------------------------------------------------------------\n# _schema_type_label\n# ---------------------------------------------------------------------------\n\n\nclass TestSchemaTypeLabel:\n    def test_simple_string(self):\n        assert _schema_type_label({\"type\": \"string\"}) == \"string\"\n\n    def test_integer(self):\n        assert _schema_type_label({\"type\": \"integer\"}) == \"integer\"\n\n    def test_array_of_strings(self):\n        assert (\n            _schema_type_label({\"type\": \"array\", \"items\": {\"type\": \"string\"}})\n            == \"array[string]\"\n        )\n\n    def test_union_types(self):\n        result = _schema_type_label({\"type\": [\"string\", \"null\"]})\n        assert \"string\" in result\n        assert \"null\" in result\n\n    def test_object(self):\n        assert _schema_type_label({\"type\": \"object\"}) == \"object\"\n\n    def test_missing_type(self):\n        assert _schema_type_label({}) == \"string\"\n\n\n# ---------------------------------------------------------------------------\n# generate_skill_content\n# ---------------------------------------------------------------------------\n\n\nclass TestGenerateSkillContent:\n    def test_frontmatter(self):\n        content = generate_skill_content(\"weather\", \"cli.py\", [])\n        assert content.startswith(\"---\\n\")\n        assert 'name: \"weather-cli\"' in content\n        assert \"description:\" in content\n\n    def test_no_tools(self):\n        content = generate_skill_content(\"weather\", \"cli.py\", [])\n        assert \"## Utility Commands\" in content\n        assert \"## Tool Commands\" not in content\n\n    def test_tool_sections(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"greet\",\n                description=\"Say hello\",\n                inputSchema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\", \"description\": \"Who to greet\"}\n                    },\n                    \"required\": [\"name\"],\n                },\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        assert \"## Tool Commands\" in content\n        assert \"### greet\" in content\n        assert \"Say hello\" in content\n        assert \"call-tool greet\" in content\n        assert \"`--name`\" in content\n        assert \"| string |\" in content\n        assert \"| yes |\" in content\n\n    def test_frontmatter_with_tools_starts_at_column_zero(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"greet\",\n                inputSchema={\"type\": \"object\", \"properties\": {}},\n            ),\n        ]\n        content = generate_skill_content(\"weather\", \"cli.py\", tools)\n        assert content.splitlines()[0] == \"---\"\n\n    def test_optional_param(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"search\",\n                description=\"Search things\",\n                inputSchema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"query\": {\"type\": \"string\"},\n                        \"limit\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"query\"],\n                },\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        # query is required, limit is not\n        assert \"| `--query` | string | yes |\" in content\n        assert \"| `--limit` | integer | no |\" in content\n\n    def test_complex_json_param(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"create\",\n                description=\"Create item\",\n                inputSchema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"data\": {\n                            \"type\": \"object\",\n                            \"properties\": {\"x\": {\"type\": \"integer\"}},\n                        },\n                    },\n                    \"required\": [\"data\"],\n                },\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        assert \"JSON string\" in content\n\n    def test_no_params_tool(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"ping\",\n                description=\"Ping the server\",\n                inputSchema={\"type\": \"object\", \"properties\": {}},\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        assert \"### ping\" in content\n        assert \"call-tool ping\" in content\n        # No parameter table\n        assert \"| Flag |\" not in content\n\n    def test_cli_filename_in_utility_commands(self):\n        content = generate_skill_content(\"test\", \"my_cli.py\", [])\n        assert \"uv run --with fastmcp python my_cli.py list-tools\" in content\n        assert \"uv run --with fastmcp python my_cli.py list-resources\" in content\n\n    def test_pipe_in_description_escaped(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"test\",\n                description=\"Test\",\n                inputSchema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"mode\": {\"type\": \"string\", \"description\": \"a|b|c\"},\n                    },\n                },\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        assert \"a\\\\|b\\\\|c\" in content\n\n    def test_union_type_pipes_escaped(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"test\",\n                description=\"Test\",\n                inputSchema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"val\": {\"type\": [\"string\", \"null\"]},\n                    },\n                },\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        # Pipes in type label must be escaped so markdown table renders correctly\n        assert \"string \\\\| null\" in content\n\n    def test_boolean_param_no_value_placeholder(self):\n        tools = [\n            mcp.types.Tool(\n                name=\"run\",\n                description=\"Run something\",\n                inputSchema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"verbose\": {\"type\": \"boolean\", \"description\": \"Verbose output\"},\n                        \"name\": {\"type\": \"string\"},\n                    },\n                },\n            ),\n        ]\n        content = generate_skill_content(\"test\", \"cli.py\", tools)\n        assert \"--verbose <value>\" not in content\n        assert \"--name <value>\" in content\n\n    def test_server_name_in_header(self):\n        content = generate_skill_content(\"My Weather API\", \"cli.py\", [])\n        assert \"# My Weather API CLI\" in content\n        assert 'name: \"my-weather-api-cli\"' in content\n"
  },
  {
    "path": "tests/cli/test_goose.py",
    "content": "from pathlib import Path\nfrom unittest.mock import patch\nfrom urllib.parse import parse_qs, unquote, urlparse\n\nimport pytest\n\nfrom fastmcp.cli.install.goose import (\n    _build_uvx_command,\n    _slugify,\n    generate_goose_deeplink,\n    goose_command,\n    install_goose,\n)\n\n\nclass TestSlugify:\n    def test_simple_name(self):\n        assert _slugify(\"My Server\") == \"my-server\"\n\n    def test_special_characters(self):\n        assert _slugify(\"my_server (v2.0)\") == \"my-server-v2-0\"\n\n    def test_already_slugified(self):\n        assert _slugify(\"my-server\") == \"my-server\"\n\n    def test_empty_string(self):\n        assert _slugify(\"\") == \"fastmcp-server\"\n\n    def test_only_special_chars(self):\n        assert _slugify(\"!!!\") == \"fastmcp-server\"\n\n    def test_consecutive_hyphens_collapsed(self):\n        assert _slugify(\"a---b\") == \"a-b\"\n\n    def test_leading_trailing_stripped(self):\n        assert _slugify(\"--hello--\") == \"hello\"\n\n\nclass TestBuildUvxCommand:\n    def test_basic(self):\n        cmd = _build_uvx_command(\"server.py\")\n        assert cmd == [\"uvx\", \"fastmcp\", \"run\", \"server.py\"]\n\n    def test_with_python_version(self):\n        cmd = _build_uvx_command(\"server.py\", python_version=\"3.11\")\n        assert cmd == [\n            \"uvx\",\n            \"--python\",\n            \"3.11\",\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n\n    def test_with_packages(self):\n        cmd = _build_uvx_command(\"server.py\", with_packages=[\"numpy\", \"pandas\"])\n        assert \"--with\" in cmd\n        assert \"numpy\" in cmd\n        assert \"pandas\" in cmd\n\n    def test_fastmcp_not_in_with(self):\n        cmd = _build_uvx_command(\"server.py\", with_packages=[\"fastmcp\", \"numpy\"])\n        # fastmcp is the command itself, so it shouldn't appear in --with\n        with_indices = [i for i, v in enumerate(cmd) if v == \"--with\"]\n        with_values = [cmd[i + 1] for i in with_indices]\n        assert \"fastmcp\" not in with_values\n\n    def test_packages_sorted_and_deduplicated(self):\n        cmd = _build_uvx_command(\n            \"server.py\", with_packages=[\"pandas\", \"numpy\", \"pandas\"]\n        )\n        with_indices = [i for i, v in enumerate(cmd) if v == \"--with\"]\n        with_values = [cmd[i + 1] for i in with_indices]\n        assert with_values == [\"numpy\", \"pandas\"]\n\n    def test_server_spec_with_object(self):\n        cmd = _build_uvx_command(\"server.py:app\")\n        assert cmd[-1] == \"server.py:app\"\n\n\nclass TestGooseDeeplinkGeneration:\n    def test_basic_deeplink(self):\n        deeplink = generate_goose_deeplink(\n            name=\"test-server\",\n            command=\"uvx\",\n            args=[\"fastmcp\", \"run\", \"server.py\"],\n        )\n        assert deeplink.startswith(\"goose://extension?\")\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert params[\"cmd\"] == [\"uvx\"]\n        assert params[\"name\"] == [\"test-server\"]\n        assert params[\"id\"] == [\"test-server\"]\n\n    def test_special_characters_in_name(self):\n        deeplink = generate_goose_deeplink(\n            name=\"my server (test)\",\n            command=\"uvx\",\n            args=[\"fastmcp\", \"run\", \"server.py\"],\n        )\n        assert \"name=my%20server%20%28test%29\" in deeplink\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert params[\"id\"] == [\"my-server-test\"]\n\n    def test_url_injection_protection(self):\n        deeplink = generate_goose_deeplink(\n            name=\"test&evil=true\",\n            command=\"uvx\",\n            args=[\"fastmcp\", \"run\", \"server.py\"],\n        )\n        assert \"name=test%26evil%3Dtrue\" in deeplink\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert params[\"name\"] == [\"test&evil=true\"]\n\n    def test_dangerous_characters_encoded(self):\n        dangerous_names = [\n            (\"test|calc\", \"test%7Ccalc\"),\n            (\"test;calc\", \"test%3Bcalc\"),\n            (\"test<calc\", \"test%3Ccalc\"),\n            (\"test>calc\", \"test%3Ecalc\"),\n            (\"test`calc\", \"test%60calc\"),\n            (\"test$calc\", \"test%24calc\"),\n            (\"test'calc\", \"test%27calc\"),\n            ('test\"calc', \"test%22calc\"),\n            (\"test calc\", \"test%20calc\"),\n            (\"test#anchor\", \"test%23anchor\"),\n            (\"test?query=val\", \"test%3Fquery%3Dval\"),\n        ]\n        for dangerous_name, expected_encoded in dangerous_names:\n            deeplink = generate_goose_deeplink(\n                name=dangerous_name, command=\"uvx\", args=[\"fastmcp\", \"run\", \"server.py\"]\n            )\n            assert f\"name={expected_encoded}\" in deeplink, (\n                f\"Failed to encode {dangerous_name}\"\n            )\n\n    def test_custom_description(self):\n        deeplink = generate_goose_deeplink(\n            name=\"my-server\",\n            command=\"uvx\",\n            args=[\"fastmcp\", \"run\", \"server.py\"],\n            description=\"My custom MCP server\",\n        )\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert params[\"description\"] == [\"My custom MCP server\"]\n\n    def test_args_with_special_characters(self):\n        deeplink = generate_goose_deeplink(\n            name=\"test\",\n            command=\"uvx\",\n            args=[\n                \"--with\",\n                \"numpy>=1.20\",\n                \"fastmcp\",\n                \"run\",\n                \"server.py:MyApp\",\n            ],\n        )\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert \"numpy>=1.20\" in params[\"arg\"]\n        assert \"server.py:MyApp\" in params[\"arg\"]\n\n    def test_empty_args(self):\n        deeplink = generate_goose_deeplink(name=\"simple\", command=\"python\", args=[])\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert \"arg\" not in params\n        assert params[\"cmd\"] == [\"python\"]\n\n    def test_command_with_path(self):\n        deeplink = generate_goose_deeplink(\n            name=\"test\",\n            command=\"/usr/local/bin/uvx\",\n            args=[\"fastmcp\", \"run\", \"server.py\"],\n        )\n        parsed = urlparse(deeplink)\n        params = parse_qs(parsed.query)\n        assert params[\"cmd\"] == [\"/usr/local/bin/uvx\"]\n\n\nclass TestInstallGoose:\n    @patch(\"fastmcp.cli.install.goose.open_deeplink\")\n    @patch(\"fastmcp.cli.install.goose.print\")\n    def test_success(self, mock_print, mock_open):\n        mock_open.return_value = True\n        result = install_goose(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n        )\n        assert result is True\n        mock_open.assert_called_once()\n        call_url = mock_open.call_args[0][0]\n        assert call_url.startswith(\"goose://extension?\")\n        assert mock_open.call_args[1] == {\"expected_scheme\": \"goose\"}\n\n    @patch(\"fastmcp.cli.install.goose.open_deeplink\")\n    @patch(\"fastmcp.cli.install.goose.print\")\n    def test_success_uses_uvx(self, mock_print, mock_open):\n        mock_open.return_value = True\n        install_goose(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n        )\n        call_url = mock_open.call_args[0][0]\n        parsed = urlparse(call_url)\n        params = parse_qs(parsed.query)\n        assert params[\"cmd\"] == [\"uvx\"]\n        assert \"fastmcp\" in params[\"arg\"]\n\n    @patch(\"fastmcp.cli.install.goose.open_deeplink\")\n    @patch(\"fastmcp.cli.install.goose.print\")\n    def test_failure(self, mock_print, mock_open):\n        mock_open.return_value = False\n        result = install_goose(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n        )\n        assert result is False\n\n    @patch(\"fastmcp.cli.install.goose.open_deeplink\")\n    @patch(\"fastmcp.cli.install.goose.print\")\n    def test_with_server_object(self, mock_print, mock_open):\n        mock_open.return_value = True\n        install_goose(\n            file=Path(\"/path/to/server.py\"),\n            server_object=\"app\",\n            name=\"test-server\",\n        )\n        call_url = mock_open.call_args[0][0]\n        parsed = urlparse(call_url)\n        params = parse_qs(parsed.query)\n        args = params[\"arg\"]\n        assert any(\"server.py:app\" in unquote(a) for a in args)\n\n    @patch(\"fastmcp.cli.install.goose.open_deeplink\")\n    @patch(\"fastmcp.cli.install.goose.print\")\n    def test_with_packages(self, mock_print, mock_open):\n        mock_open.return_value = True\n        install_goose(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n            with_packages=[\"numpy\", \"pandas\"],\n        )\n        call_url = mock_open.call_args[0][0]\n        parsed = urlparse(call_url)\n        params = parse_qs(parsed.query)\n        args = params[\"arg\"]\n        assert \"numpy\" in args\n        assert \"pandas\" in args\n\n    @patch(\"fastmcp.cli.install.goose.open_deeplink\")\n    @patch(\"fastmcp.cli.install.goose.print\")\n    def test_fallback_message_on_failure(self, mock_print, mock_open):\n        mock_open.return_value = False\n        install_goose(\n            file=Path(\"/path/to/server.py\"),\n            server_object=None,\n            name=\"test-server\",\n        )\n        fallback_calls = [\n            call\n            for call in mock_print.call_args_list\n            if \"copy this link\" in str(call).lower() or \"goose://\" in str(call)\n        ]\n        assert len(fallback_calls) > 0\n\n\nclass TestGooseCommand:\n    @patch(\"fastmcp.cli.install.goose.install_goose\")\n    @patch(\"fastmcp.cli.install.goose.process_common_args\")\n    async def test_basic(self, mock_process, mock_install):\n        mock_process.return_value = (Path(\"server.py\"), None, \"test-server\", [], {})\n        mock_install.return_value = True\n        await goose_command(\"server.py\")\n        mock_install.assert_called_once_with(\n            file=Path(\"server.py\"),\n            server_object=None,\n            name=\"test-server\",\n            with_packages=[],\n            python_version=None,\n        )\n\n    @patch(\"fastmcp.cli.install.goose.install_goose\")\n    @patch(\"fastmcp.cli.install.goose.process_common_args\")\n    async def test_failure_exits(self, mock_process, mock_install):\n        mock_process.return_value = (Path(\"server.py\"), None, \"test-server\", [], {})\n        mock_install.return_value = False\n        with pytest.raises(SystemExit) as exc_info:\n            await goose_command(\"server.py\")\n        assert exc_info.value.code == 1\n"
  },
  {
    "path": "tests/cli/test_install.py",
    "content": "from pathlib import Path\n\nimport pytest\n\nfrom fastmcp.cli.install import install_app\nfrom fastmcp.cli.install.shared import validate_server_name\nfrom fastmcp.cli.install.stdio import install_stdio\n\n\nclass TestInstallApp:\n    \"\"\"Test the install subapp.\"\"\"\n\n    def test_install_app_exists(self):\n        \"\"\"Test that the install app is properly configured.\"\"\"\n        # install_app.name is a tuple in cyclopts\n        assert \"install\" in install_app.name\n        assert \"Install MCP servers\" in install_app.help\n\n    def test_install_commands_registered(self):\n        \"\"\"Test that all install commands are registered.\"\"\"\n        # Check that the app has the expected help text and structure\n        # This is a simpler check that doesn't rely on internal methods\n        assert hasattr(install_app, \"help\")\n        assert \"Install MCP servers\" in install_app.help\n\n        # We can test that the commands parse without errors\n        try:\n            install_app.parse_args([\"claude-code\", \"--help\"])\n            install_app.parse_args([\"claude-desktop\", \"--help\"])\n            install_app.parse_args([\"cursor\", \"--help\"])\n            install_app.parse_args([\"gemini-cli\", \"--help\"])\n            install_app.parse_args([\"goose\", \"--help\"])\n            install_app.parse_args([\"mcp-json\", \"--help\"])\n            install_app.parse_args([\"stdio\", \"--help\"])\n        except SystemExit:\n            # Help commands exit with 0, that's expected\n            pass\n\n\nclass TestClaudeCodeInstall:\n    \"\"\"Test claude-code install command.\"\"\"\n\n    def test_claude_code_basic(self):\n        \"\"\"Test basic claude-code install command parsing.\"\"\"\n        # Parse command with correct parameter names\n        command, bound, _ = install_app.parse_args(\n            [\"claude-code\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        # Verify parsing was successful\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n    def test_claude_code_with_options(self):\n        \"\"\"Test claude-code install with various options.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\n                \"claude-code\",\n                \"server.py\",\n                \"--name\",\n                \"test-server\",\n                \"--with\",\n                \"package1\",\n                \"--with\",\n                \"package2\",\n                \"--env\",\n                \"VAR1=value1\",\n            ]\n        )\n\n        assert bound.arguments[\"with_packages\"] == [\"package1\", \"package2\"]\n        assert bound.arguments[\"env_vars\"] == [\"VAR1=value1\"]\n\n    def test_claude_code_with_new_options(self):\n        \"\"\"Test claude-code install with new uv options.\"\"\"\n        from pathlib import Path\n\n        command, bound, _ = install_app.parse_args(\n            [\n                \"claude-code\",\n                \"server.py\",\n                \"--python\",\n                \"3.11\",\n                \"--project\",\n                \"/workspace\",\n                \"--with-requirements\",\n                \"requirements.txt\",\n            ]\n        )\n\n        assert bound.arguments[\"python\"] == \"3.11\"\n        assert bound.arguments[\"project\"] == Path(\"/workspace\")\n        assert bound.arguments[\"with_requirements\"] == Path(\"requirements.txt\")\n\n\nclass TestClaudeDesktopInstall:\n    \"\"\"Test claude-desktop install command.\"\"\"\n\n    def test_claude_desktop_basic(self):\n        \"\"\"Test basic claude-desktop install command parsing.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"claude-desktop\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n    def test_claude_desktop_with_env_vars(self):\n        \"\"\"Test claude-desktop install with environment variables.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\n                \"claude-desktop\",\n                \"server.py\",\n                \"--name\",\n                \"test-server\",\n                \"--env\",\n                \"VAR1=value1\",\n                \"--env\",\n                \"VAR2=value2\",\n            ]\n        )\n\n        assert bound.arguments[\"env_vars\"] == [\"VAR1=value1\", \"VAR2=value2\"]\n\n    def test_claude_desktop_with_new_options(self):\n        \"\"\"Test claude-desktop install with new uv options.\"\"\"\n        from pathlib import Path\n\n        command, bound, _ = install_app.parse_args(\n            [\n                \"claude-desktop\",\n                \"server.py\",\n                \"--python\",\n                \"3.10\",\n                \"--project\",\n                \"/my/project\",\n                \"--with-requirements\",\n                \"reqs.txt\",\n            ]\n        )\n\n        assert bound.arguments[\"python\"] == \"3.10\"\n        assert bound.arguments[\"project\"] == Path(\"/my/project\")\n        assert bound.arguments[\"with_requirements\"] == Path(\"reqs.txt\")\n\n    def test_claude_desktop_with_config_path(self):\n        \"\"\"Test claude-desktop install with custom config path.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"claude-desktop\", \"server.py\", \"--config-path\", \"/custom/path/Claude\"]\n        )\n\n        assert bound.arguments[\"config_path\"] == Path(\"/custom/path/Claude\")\n\n    def test_claude_desktop_without_config_path(self):\n        \"\"\"Test claude-desktop install without config path defaults to None.\"\"\"\n        command, bound, _ = install_app.parse_args([\"claude-desktop\", \"server.py\"])\n\n        assert bound.arguments.get(\"config_path\") is None\n\n\nclass TestCursorInstall:\n    \"\"\"Test cursor install command.\"\"\"\n\n    def test_cursor_basic(self):\n        \"\"\"Test basic cursor install command parsing.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"cursor\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n    def test_cursor_with_options(self):\n        \"\"\"Test cursor install with options.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"cursor\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n\nclass TestGooseInstall:\n    \"\"\"Test goose install command.\"\"\"\n\n    def test_goose_basic(self):\n        \"\"\"Test basic goose install command parsing.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"goose\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n    def test_goose_with_options(self):\n        \"\"\"Test goose install with various options.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\n                \"goose\",\n                \"server.py\",\n                \"--name\",\n                \"test-server\",\n                \"--with\",\n                \"package1\",\n                \"--with\",\n                \"package2\",\n                \"--env\",\n                \"VAR1=value1\",\n            ]\n        )\n\n        assert bound.arguments[\"with_packages\"] == [\"package1\", \"package2\"]\n        assert bound.arguments[\"env_vars\"] == [\"VAR1=value1\"]\n\n    def test_goose_with_python(self):\n        \"\"\"Test goose install with --python option.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\n                \"goose\",\n                \"server.py\",\n                \"--python\",\n                \"3.11\",\n            ]\n        )\n\n        assert bound.arguments[\"python\"] == \"3.11\"\n\n\nclass TestMcpJsonInstall:\n    \"\"\"Test mcp-json install command.\"\"\"\n\n    def test_mcp_json_basic(self):\n        \"\"\"Test basic mcp-json install command parsing.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"mcp-json\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n    def test_mcp_json_with_copy(self):\n        \"\"\"Test mcp-json install with copy to clipboard option.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"mcp-json\", \"server.py\", \"--name\", \"test-server\", \"--copy\"]\n        )\n\n        assert bound.arguments[\"copy\"] is True\n\n\nclass TestStdioInstall:\n    \"\"\"Test stdio install command.\"\"\"\n\n    def test_stdio_basic(self):\n        \"\"\"Test basic stdio install command parsing.\"\"\"\n        command, bound, _ = install_app.parse_args([\"stdio\", \"server.py\"])\n\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n\n    def test_stdio_with_copy(self):\n        \"\"\"Test stdio install with copy to clipboard option.\"\"\"\n        command, bound, _ = install_app.parse_args([\"stdio\", \"server.py\", \"--copy\"])\n\n        assert bound.arguments[\"copy\"] is True\n\n    def test_stdio_with_packages(self):\n        \"\"\"Test stdio install with additional packages.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\"stdio\", \"server.py\", \"--with\", \"requests\", \"--with\", \"httpx\"]\n        )\n\n        assert bound.arguments[\"with_packages\"] == [\"requests\", \"httpx\"]\n\n    def test_install_stdio_generates_command(self, tmp_path: Path):\n        \"\"\"Test that install_stdio produces a shell command containing fastmcp run.\"\"\"\n        server_file = tmp_path / \"server.py\"\n        server_file.write_text(\"# placeholder\")\n\n        # Capture stdout\n        import io\n        import sys\n\n        captured = io.StringIO()\n        old_stdout = sys.stdout\n        sys.stdout = captured\n        try:\n            result = install_stdio(file=server_file, server_object=None)\n        finally:\n            sys.stdout = old_stdout\n\n        assert result is True\n        output = captured.getvalue()\n        assert \"fastmcp\" in output\n        assert \"run\" in output\n        assert str(server_file.resolve()) in output\n\n    def test_install_stdio_with_object(self, tmp_path: Path):\n        \"\"\"Test that install_stdio includes the :object suffix.\"\"\"\n        server_file = tmp_path / \"server.py\"\n        server_file.write_text(\"# placeholder\")\n\n        import io\n        import sys\n\n        captured = io.StringIO()\n        old_stdout = sys.stdout\n        sys.stdout = captured\n        try:\n            result = install_stdio(file=server_file, server_object=\"app\")\n        finally:\n            sys.stdout = old_stdout\n\n        assert result is True\n        output = captured.getvalue()\n        assert f\"{server_file.resolve()}:app\" in output\n\n\nclass TestGeminiCliInstall:\n    \"\"\"Test gemini-cli install command.\"\"\"\n\n    def test_gemini_cli_basic(self):\n        \"\"\"Test basic gemini-cli install command parsing.\"\"\"\n        # Parse command with correct parameter names\n        command, bound, _ = install_app.parse_args(\n            [\"gemini-cli\", \"server.py\", \"--name\", \"test-server\"]\n        )\n\n        # Verify parsing was successful\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n        assert bound.arguments[\"server_name\"] == \"test-server\"\n\n    def test_gemini_cli_with_options(self):\n        \"\"\"Test gemini-cli install with various options.\"\"\"\n        command, bound, _ = install_app.parse_args(\n            [\n                \"gemini-cli\",\n                \"server.py\",\n                \"--name\",\n                \"test-server\",\n                \"--with\",\n                \"package1\",\n                \"--with\",\n                \"package2\",\n                \"--env\",\n                \"VAR1=value1\",\n            ]\n        )\n\n        assert bound.arguments[\"with_packages\"] == [\"package1\", \"package2\"]\n        assert bound.arguments[\"env_vars\"] == [\"VAR1=value1\"]\n\n    def test_gemini_cli_with_new_options(self):\n        \"\"\"Test gemini-cli install with new uv options.\"\"\"\n        from pathlib import Path\n\n        command, bound, _ = install_app.parse_args(\n            [\n                \"gemini-cli\",\n                \"server.py\",\n                \"--python\",\n                \"3.11\",\n                \"--project\",\n                \"/workspace\",\n                \"--with-requirements\",\n                \"requirements.txt\",\n            ]\n        )\n\n        assert bound.arguments[\"python\"] == \"3.11\"\n        assert bound.arguments[\"project\"] == Path(\"/workspace\")\n        assert bound.arguments[\"with_requirements\"] == Path(\"requirements.txt\")\n\n\nclass TestInstallCommandParsing:\n    \"\"\"Test command parsing and error handling.\"\"\"\n\n    def test_install_minimal_args(self):\n        \"\"\"Test install commands with minimal required arguments.\"\"\"\n        # Each command should work with just a server spec\n        commands_to_test = [\n            [\"claude-code\", \"server.py\"],\n            [\"claude-desktop\", \"server.py\"],\n            [\"cursor\", \"server.py\"],\n            [\"gemini-cli\", \"server.py\"],\n            [\"goose\", \"server.py\"],\n            [\"stdio\", \"server.py\"],\n        ]\n\n        for cmd_args in commands_to_test:\n            command, bound, _ = install_app.parse_args(cmd_args)\n            assert command is not None\n            assert bound.arguments[\"server_spec\"] == \"server.py\"\n\n    def test_mcp_json_minimal(self):\n        \"\"\"Test that mcp-json works with minimal arguments.\"\"\"\n        # Should work with just server spec\n        command, bound, _ = install_app.parse_args([\"mcp-json\", \"server.py\"])\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n\n    def test_stdio_minimal(self):\n        \"\"\"Test that stdio works with minimal arguments.\"\"\"\n        command, bound, _ = install_app.parse_args([\"stdio\", \"server.py\"])\n        assert command is not None\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n\n    def test_python_option(self):\n        \"\"\"Test --python option for all install commands.\"\"\"\n        commands_to_test = [\n            [\"claude-code\", \"server.py\", \"--python\", \"3.11\"],\n            [\"claude-desktop\", \"server.py\", \"--python\", \"3.11\"],\n            [\"cursor\", \"server.py\", \"--python\", \"3.11\"],\n            [\"gemini-cli\", \"server.py\", \"--python\", \"3.11\"],\n            [\"goose\", \"server.py\", \"--python\", \"3.11\"],\n            [\"mcp-json\", \"server.py\", \"--python\", \"3.11\"],\n            [\"stdio\", \"server.py\", \"--python\", \"3.11\"],\n        ]\n\n        for cmd_args in commands_to_test:\n            command, bound, _ = install_app.parse_args(cmd_args)\n            assert command is not None\n            assert bound.arguments[\"python\"] == \"3.11\"\n\n    def test_with_requirements_option(self):\n        \"\"\"Test --with-requirements option for all install commands.\"\"\"\n        commands_to_test = [\n            [\"claude-code\", \"server.py\", \"--with-requirements\", \"requirements.txt\"],\n            [\"claude-desktop\", \"server.py\", \"--with-requirements\", \"requirements.txt\"],\n            [\"cursor\", \"server.py\", \"--with-requirements\", \"requirements.txt\"],\n            [\"gemini-cli\", \"server.py\", \"--with-requirements\", \"requirements.txt\"],\n            [\"mcp-json\", \"server.py\", \"--with-requirements\", \"requirements.txt\"],\n            [\"stdio\", \"server.py\", \"--with-requirements\", \"requirements.txt\"],\n        ]\n\n        for cmd_args in commands_to_test:\n            command, bound, _ = install_app.parse_args(cmd_args)\n            assert command is not None\n            assert str(bound.arguments[\"with_requirements\"]) == \"requirements.txt\"\n\n    def test_project_option(self):\n        \"\"\"Test --project option for all install commands.\"\"\"\n        commands_to_test = [\n            [\"claude-code\", \"server.py\", \"--project\", \"/path/to/project\"],\n            [\"claude-desktop\", \"server.py\", \"--project\", \"/path/to/project\"],\n            [\"cursor\", \"server.py\", \"--project\", \"/path/to/project\"],\n            [\"gemini-cli\", \"server.py\", \"--project\", \"/path/to/project\"],\n            [\"mcp-json\", \"server.py\", \"--project\", \"/path/to/project\"],\n            [\"stdio\", \"server.py\", \"--project\", \"/path/to/project\"],\n        ]\n\n        for cmd_args in commands_to_test:\n            command, bound, _ = install_app.parse_args(cmd_args)\n            assert command is not None\n            assert str(bound.arguments[\"project\"]) == str(Path(\"/path/to/project\"))\n\n\nclass TestServerNameValidation:\n    \"\"\"Test server name validation rejects shell metacharacters.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"name\",\n        [\n            \"my-server\",\n            \"my_server\",\n            \"My Server\",\n            \"server.v2\",\n            \"test123\",\n        ],\n    )\n    def test_valid_names(self, name: str):\n        assert validate_server_name(name) == name\n\n    @pytest.mark.parametrize(\n        \"name\",\n        [\n            \"test&calc\",\n            \"test|whoami\",\n            \"test;ls\",\n            \"test$(id)\",\n            \"test`id`\",\n            'test\"quoted',\n            \"test>file\",\n            \"test<file\",\n        ],\n    )\n    def test_rejects_shell_metacharacters(self, name: str):\n        with pytest.raises(SystemExit):\n            validate_server_name(name)\n"
  },
  {
    "path": "tests/cli/test_mcp_server_config_integration.py",
    "content": "\"\"\"Integration tests for fastmcp.json configuration system.\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\n\nimport pytest\n\nfrom fastmcp.client import Client\nfrom fastmcp.utilities.mcp_server_config import MCPServerConfig\n\n\n@pytest.fixture\ndef server_with_config(tmp_path):\n    \"\"\"Create a complete server setup with fastmcp.json config.\"\"\"\n    # Create server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"\"\"\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Config Test Server\")\n\n@mcp.tool\ndef hello(name: str = \"World\") -> str:\n    '''Say hello to someone'''\n    return f\"Hello, {name}!\"\n\n@mcp.resource(\"resource://greeting\")\ndef get_greeting() -> str:\n    '''Get a greeting message'''\n    return \"Welcome to FastMCP!\"\n\nif __name__ == \"__main__\":\n    import asyncio\n    asyncio.run(mcp.run_async())\n\"\"\")\n\n    # Create config file\n    config_data = {\n        \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n        \"source\": {\"path\": \"server.py\"},\n        \"environment\": {\n            \"python\": sys.version.split()[0],  # Use current Python version\n            \"dependencies\": [\"fastmcp\"],\n        },\n        \"deployment\": {\"transport\": \"stdio\", \"log_level\": \"INFO\"},\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data, indent=2))\n\n    return tmp_path\n\n\nclass TestConfigFileDetection:\n    \"\"\"Test configuration file detection patterns.\"\"\"\n\n    def test_detect_standard_fastmcp_json(self, tmp_path):\n        \"\"\"Test detection of standard fastmcp.json file.\"\"\"\n        config_file = tmp_path / \"fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"path\": \"server.py\"}}))\n\n        # Should be detected as fastmcp config\n        assert \"fastmcp.json\" in config_file.name\n        assert config_file.name.endswith(\"fastmcp.json\")\n\n    def test_detect_prefixed_fastmcp_json(self, tmp_path):\n        \"\"\"Test detection of prefixed fastmcp.json files.\"\"\"\n        config_file = tmp_path / \"my.fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"path\": \"server.py\"}}))\n\n        # Should be detected as fastmcp config\n        assert \"fastmcp.json\" in config_file.name\n\n    def test_detect_test_fastmcp_json(self, tmp_path):\n        \"\"\"Test detection of test_fastmcp.json file.\"\"\"\n        config_file = tmp_path / \"test_fastmcp.json\"\n        config_file.write_text(json.dumps({\"source\": {\"path\": \"server.py\"}}))\n\n        # Should be detected as fastmcp config\n        assert \"fastmcp.json\" in config_file.name\n\n\nclass TestConfigWithClient:\n    \"\"\"Test fastmcp.json configuration with client connections.\"\"\"\n\n    async def test_config_server_with_client(self, server_with_config):\n        \"\"\"Test that a server loaded from config works with a client.\"\"\"\n        # Load the config\n        config_file = server_with_config / \"fastmcp.json\"\n        config = MCPServerConfig.from_file(config_file)\n\n        # Import the server using the source\n        import importlib.util\n        import sys\n\n        # Resolve the path from the source\n        source_path = Path(config.source.path)\n        if not source_path.is_absolute():\n            source_path = (config_file.parent / source_path).resolve()\n\n        spec = importlib.util.spec_from_file_location(\"test_server\", str(source_path))\n        if spec is None or spec.loader is None:\n            raise RuntimeError(f\"Could not load module from {source_path}\")\n\n        module = importlib.util.module_from_spec(spec)\n        sys.modules[\"test_server\"] = module\n        spec.loader.exec_module(module)\n\n        assert hasattr(module, \"mcp\")\n\n        server = module.mcp\n\n        # Connect client to server\n        async with Client(server) as client:\n            # Test tool\n            result = await client.call_tool(\"hello\", {\"name\": \"FastMCP\"})\n            assert result.data == \"Hello, FastMCP!\"  # Use .data for string result\n\n            # Test resource\n            results = await client.read_resource(\"resource://greeting\")\n            assert len(results) == 1\n            # Resource results should have text content\n            assert hasattr(results[0], \"text\") or hasattr(results[0], \"contents\")\n            # Get the text content from the resource\n            text = getattr(results[0], \"text\", None) or getattr(\n                results[0], \"contents\", \"\"\n            )\n            assert \"Welcome to FastMCP!\" in str(text)\n\n\nclass TestEnvironmentExecution:\n    \"\"\"Test environment configuration execution paths.\"\"\"\n\n    def test_needs_uv_with_dependencies(self):\n        \"\"\"Test that environment with dependencies needs UV.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={\"dependencies\": [\"requests\", \"numpy\"]},\n        )\n\n        assert config.environment is not None\n        assert config.environment._must_run_with_uv()\n\n    def test_needs_uv_with_python_version(self):\n        \"\"\"Test that environment with Python version needs UV.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={\"python\": \"3.12\"},\n        )\n\n        assert config.environment is not None\n        assert config.environment._must_run_with_uv()\n\n    def test_no_uv_needed_without_environment(self):\n        \"\"\"Test that no UV is needed without environment config.\"\"\"\n        config = MCPServerConfig(source={\"path\": \"server.py\"})\n\n        # Environment is now always present but may be empty\n        assert config.environment is not None\n        assert not config.environment._must_run_with_uv()\n\n    def test_no_uv_needed_with_empty_environment(self):\n        \"\"\"Test that no UV is needed with empty environment config.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={},\n        )\n\n        assert config.environment is not None\n        assert not config.environment._must_run_with_uv()\n\n\nclass TestPathResolution:\n    \"\"\"Test path resolution in configurations.\"\"\"\n\n    def test_source_path_resolution(self, tmp_path):\n        \"\"\"Test that source paths are resolved relative to config.\"\"\"\n        # Create nested directory structure\n        config_dir = tmp_path / \"config\"\n        config_dir.mkdir()\n        src_dir = tmp_path / \"src\"\n        src_dir.mkdir()\n\n        # Server is in src, config is in config\n        server_file = src_dir / \"server.py\"\n        server_file.write_text(\"# Server\")\n\n        config = MCPServerConfig(source={\"path\": \"../src/server.py\"})\n\n        # The source path is resolved during load_server\n        # For now, just check that the source is created correctly\n        assert config.source.path == \"../src/server.py\"\n\n    def test_cwd_path_resolution(self, tmp_path):\n        \"\"\"Test that working directory is resolved relative to config.\"\"\"\n        import os\n\n        # Create directory structure\n        work_dir = tmp_path / \"work\"\n        work_dir.mkdir()\n\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            deployment={\"cwd\": \"work\"},\n        )\n\n        original_cwd = os.getcwd()\n\n        try:\n            # Apply runtime settings relative to config location\n            assert config.deployment is not None\n            config.deployment.apply_runtime_settings(tmp_path / \"fastmcp.json\")\n\n            # Should change to work directory\n            assert Path.cwd() == work_dir.resolve()\n\n        finally:\n            os.chdir(original_cwd)\n\n    def test_requirements_path_resolution(self, tmp_path):\n        \"\"\"Test that requirements path is resolved correctly.\"\"\"\n        # Create requirements file\n        reqs_file = tmp_path / \"requirements.txt\"\n        reqs_file.write_text(\"fastmcp>=2.0\")\n\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            environment={\"requirements\": \"requirements.txt\"},\n        )\n\n        # Build UV command\n        assert config.environment is not None\n        uv_cmd = config.environment.build_command([\"fastmcp\", \"run\"])\n\n        # Should include requirements file with absolute path\n        assert \"--with-requirements\" in uv_cmd\n        req_idx = uv_cmd.index(\"--with-requirements\") + 1\n        assert Path(uv_cmd[req_idx]).is_absolute()\n        assert Path(uv_cmd[req_idx]).name == \"requirements.txt\"\n\n\nclass TestConfigValidation:\n    \"\"\"Test configuration validation.\"\"\"\n\n    def test_invalid_transport_rejected(self):\n        \"\"\"Test that invalid transport values are rejected.\"\"\"\n        with pytest.raises(ValueError):\n            MCPServerConfig(\n                source={\"path\": \"server.py\"},\n                deployment={\"transport\": \"invalid_transport\"},\n            )\n\n    def test_streamable_http_transport_accepted(self):\n        \"\"\"Test that streamable-http transport is accepted as a valid value.\"\"\"\n        config = MCPServerConfig(\n            source={\"path\": \"server.py\"},\n            deployment={\"transport\": \"streamable-http\"},\n        )\n        assert config.deployment.transport == \"streamable-http\"\n\n    def test_invalid_log_level_rejected(self):\n        \"\"\"Test that invalid log level values are rejected.\"\"\"\n        with pytest.raises(ValueError):\n            MCPServerConfig(\n                source={\"path\": \"server.py\"},\n                deployment={\"log_level\": \"INVALID\"},\n            )\n\n    def test_missing_source_rejected(self):\n        \"\"\"Test that config without source is rejected.\"\"\"\n        with pytest.raises(ValueError):\n            MCPServerConfig()  # type: ignore[call-arg]\n\n    def test_valid_transport_values(self):\n        \"\"\"Test that all valid transport values are accepted.\"\"\"\n        for transport in [\"stdio\", \"http\", \"sse\"]:\n            config = MCPServerConfig(\n                source={\"path\": \"server.py\"},\n                deployment={\"transport\": transport},\n            )\n            assert config.deployment is not None\n            assert config.deployment.transport == transport\n\n    def test_valid_log_levels(self):\n        \"\"\"Test that all valid log levels are accepted.\"\"\"\n        for level in [\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]:\n            config = MCPServerConfig(\n                source={\"path\": \"server.py\"},\n                deployment={\"log_level\": level},\n            )\n            assert config.deployment is not None\n            assert config.deployment.log_level == level\n"
  },
  {
    "path": "tests/cli/test_mcp_server_config_schema.py",
    "content": "\"\"\"Test that the generated JSON schema has the correct structure.\"\"\"\n\nimport pytest\n\nfrom fastmcp.utilities.mcp_server_config.v1.mcp_server_config import (\n    Deployment,\n    generate_schema,\n)\n\n\ndef test_schema_has_correct_id():\n    \"\"\"Test that the schema has the correct $id field.\"\"\"\n    generated_schema = generate_schema()\n\n    assert generated_schema is not None\n    assert \"$id\" in generated_schema\n    assert (\n        generated_schema[\"$id\"]\n        == \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\"\n    )\n\n\ndef test_schema_has_required_fields():\n    \"\"\"Test that the schema specifies the required fields correctly.\"\"\"\n    generated_schema = generate_schema()\n\n    assert generated_schema is not None\n    # Check that source is required\n    assert \"required\" in generated_schema\n    assert \"source\" in generated_schema[\"required\"]\n\n    # Check that source is in properties\n    assert \"properties\" in generated_schema\n    assert \"source\" in generated_schema[\"properties\"]\n\n\ndef test_schema_nested_structure():\n    \"\"\"Test that the schema has the correct nested structure.\"\"\"\n    generated_schema = generate_schema()\n\n    assert generated_schema is not None\n    properties = generated_schema[\"properties\"]\n\n    # Check environment section\n    assert \"environment\" in properties\n    env_schema = properties[\"environment\"]\n    # Environment can be in anyOf or direct properties\n    if \"anyOf\" in env_schema:\n        # Find the UVEnvironment in anyOf\n        for option in env_schema[\"anyOf\"]:\n            if option.get(\"type\") == \"object\" and \"properties\" in option:\n                env_props = option[\"properties\"]\n                assert \"type\" in env_props  # New type field\n                assert \"python\" in env_props\n                assert \"dependencies\" in env_props\n                assert \"requirements\" in env_props\n                assert \"project\" in env_props\n                assert \"editable\" in env_props\n                break\n    elif \"properties\" in env_schema:\n        env_props = env_schema[\"properties\"]\n        assert \"type\" in env_props  # New type field\n        assert \"python\" in env_props\n        assert \"dependencies\" in env_props\n        assert \"requirements\" in env_props\n        assert \"project\" in env_props\n        assert \"editable\" in env_props\n\n    # Check deployment section\n    assert \"deployment\" in properties\n    deploy_schema = properties[\"deployment\"]\n    if \"properties\" in deploy_schema:\n        deploy_props = deploy_schema[\"properties\"]\n        assert \"transport\" in deploy_props\n        assert \"host\" in deploy_props\n        assert \"port\" in deploy_props\n        assert \"log_level\" in deploy_props\n        assert \"env\" in deploy_props\n        assert \"cwd\" in deploy_props\n        assert \"args\" in deploy_props\n\n\ndef test_schema_transport_enum():\n    \"\"\"Test that transport field has correct enum values.\"\"\"\n    generated_schema = generate_schema()\n\n    assert generated_schema is not None\n    # Navigate to transport field\n    deploy_schema = generated_schema[\"properties\"][\"deployment\"]\n\n    # Handle both direct properties and anyOf cases\n    if \"anyOf\" in deploy_schema:\n        # Find the object type in anyOf\n        for option in deploy_schema[\"anyOf\"]:\n            if option.get(\"type\") == \"object\" and \"properties\" in option:\n                transport_schema = option[\"properties\"].get(\"transport\", {})\n                if \"anyOf\" in transport_schema:\n                    # Look for enum in anyOf options\n                    for trans_option in transport_schema[\"anyOf\"]:\n                        if \"enum\" in trans_option:\n                            valid_transports = trans_option[\"enum\"]\n                            assert \"stdio\" in valid_transports\n                            assert \"http\" in valid_transports\n                            assert \"sse\" in valid_transports\n                            assert \"streamable-http\" in valid_transports\n                            break\n    elif \"properties\" in deploy_schema:\n        transport_schema = deploy_schema[\"properties\"].get(\"transport\", {})\n        if \"anyOf\" in transport_schema:\n            for option in transport_schema[\"anyOf\"]:\n                if \"enum\" in option:\n                    valid_transports = option[\"enum\"]\n                    assert \"stdio\" in valid_transports\n                    assert \"http\" in valid_transports\n                    assert \"sse\" in valid_transports\n                    assert \"streamable-http\" in valid_transports\n                    break\n\n\ndef test_schema_log_level_enum():\n    \"\"\"Test that log_level field has correct enum values.\"\"\"\n    generated_schema = generate_schema()\n\n    assert generated_schema is not None\n    # Navigate to log_level field\n    deploy_schema = generated_schema[\"properties\"][\"deployment\"]\n\n    # Handle both direct properties and anyOf cases\n    if \"anyOf\" in deploy_schema:\n        # Find the object type in anyOf\n        for option in deploy_schema[\"anyOf\"]:\n            if option.get(\"type\") == \"object\" and \"properties\" in option:\n                log_level_schema = option[\"properties\"].get(\"log_level\", {})\n                if \"anyOf\" in log_level_schema:\n                    # Look for enum in anyOf options\n                    for level_option in log_level_schema[\"anyOf\"]:\n                        if \"enum\" in level_option:\n                            valid_levels = level_option[\"enum\"]\n                            assert \"DEBUG\" in valid_levels\n                            assert \"INFO\" in valid_levels\n                            assert \"WARNING\" in valid_levels\n                            assert \"ERROR\" in valid_levels\n                            assert \"CRITICAL\" in valid_levels\n                            break\n    elif \"properties\" in deploy_schema:\n        log_level_schema = deploy_schema[\"properties\"].get(\"log_level\", {})\n        if \"anyOf\" in log_level_schema:\n            for option in log_level_schema[\"anyOf\"]:\n                if \"enum\" in option:\n                    valid_levels = option[\"enum\"]\n                    assert \"DEBUG\" in valid_levels\n                    assert \"INFO\" in valid_levels\n                    assert \"WARNING\" in valid_levels\n                    assert \"ERROR\" in valid_levels\n                    assert \"CRITICAL\" in valid_levels\n                    break\n\n\n@pytest.mark.parametrize(\n    \"transport\",\n    [\n        \"streamable-http\",\n        \"http\",\n        \"stdio\",\n        \"sse\",\n        None,\n    ],\n)\ndef test_transport_values_accepted(transport):\n    \"\"\"Test that all valid transport values are accepted.\"\"\"\n    deployment = Deployment(transport=transport)\n    assert deployment.transport == transport\n"
  },
  {
    "path": "tests/cli/test_project_prepare.py",
    "content": "\"\"\"Tests for the fastmcp project prepare command.\"\"\"\n\nimport subprocess\nfrom pathlib import Path\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nfrom fastmcp.utilities.mcp_server_config import MCPServerConfig\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\n\nclass TestMCPServerConfigPrepare:\n    \"\"\"Test the MCPServerConfig.prepare() method.\"\"\"\n\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source\",\n        new_callable=AsyncMock,\n    )\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment\",\n        new_callable=AsyncMock,\n    )\n    async def test_prepare_calls_both_methods(self, mock_env, mock_src):\n        \"\"\"Test that prepare() calls both prepare_environment and prepare_source.\"\"\"\n        config = MCPServerConfig(\n            source=FileSystemSource(path=\"server.py\"),\n            environment=UVEnvironment(python=\"3.10\"),\n        )\n\n        await config.prepare()\n\n        mock_env.assert_called_once()\n        mock_src.assert_called_once()\n\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source\",\n        new_callable=AsyncMock,\n    )\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment\",\n        new_callable=AsyncMock,\n    )\n    async def test_prepare_with_output_dir(self, mock_env, mock_src):\n        \"\"\"Test that prepare() with output_dir calls prepare_environment with it.\"\"\"\n        config = MCPServerConfig(\n            source=FileSystemSource(path=\"server.py\"),\n            environment=UVEnvironment(python=\"3.10\"),\n        )\n\n        output_path = Path(\"/tmp/test-env\")\n        await config.prepare(skip_source=False, output_dir=output_path)\n\n        mock_env.assert_called_once_with(output_dir=output_path)\n        mock_src.assert_called_once()\n\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source\",\n        new_callable=AsyncMock,\n    )\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment\",\n        new_callable=AsyncMock,\n    )\n    async def test_prepare_skip_source(self, mock_env, mock_src):\n        \"\"\"Test that prepare() skips source when skip_source=True.\"\"\"\n        config = MCPServerConfig(\n            source=FileSystemSource(path=\"server.py\"),\n            environment=UVEnvironment(python=\"3.10\"),\n        )\n\n        await config.prepare(skip_source=True)\n\n        mock_env.assert_called_once_with(output_dir=None)\n        mock_src.assert_not_called()\n\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source\",\n        new_callable=AsyncMock,\n    )\n    @patch(\n        \"fastmcp.utilities.mcp_server_config.v1.environments.uv.UVEnvironment.prepare\",\n        new_callable=AsyncMock,\n    )\n    async def test_prepare_no_environment_settings(self, mock_env_prepare, mock_src):\n        \"\"\"Test that prepare() works with default empty environment config.\"\"\"\n        config = MCPServerConfig(\n            source=FileSystemSource(path=\"server.py\"),\n            # environment defaults to empty Environment()\n        )\n\n        await config.prepare(skip_source=False)\n\n        # Environment prepare should be called even with empty config\n        mock_env_prepare.assert_called_once_with(output_dir=None)\n        mock_src.assert_called_once()\n\n\nclass TestEnvironmentPrepare:\n    \"\"\"Test the Environment.prepare() method.\"\"\"\n\n    @patch(\"shutil.which\")\n    async def test_prepare_no_uv_installed(self, mock_which, tmp_path):\n        \"\"\"Test that prepare() raises error when uv is not installed.\"\"\"\n        mock_which.return_value = None\n\n        env = UVEnvironment(python=\"3.10\")\n\n        with pytest.raises(RuntimeError, match=\"uv is not installed\"):\n            await env.prepare(tmp_path / \"test-env\")\n\n    @patch(\"subprocess.run\")\n    @patch(\"shutil.which\")\n    async def test_prepare_no_settings(self, mock_which, mock_run, tmp_path):\n        \"\"\"Test that prepare() does nothing when no settings are configured.\"\"\"\n        mock_which.return_value = \"/usr/bin/uv\"\n\n        env = UVEnvironment()  # No settings\n\n        await env.prepare(tmp_path / \"test-env\")\n\n        # Should not run any commands\n        mock_run.assert_not_called()\n\n    @patch(\"subprocess.run\")\n    @patch(\"shutil.which\")\n    async def test_prepare_with_python(self, mock_which, mock_run, tmp_path):\n        \"\"\"Test that prepare() runs uv with python version.\"\"\"\n        mock_which.return_value = \"/usr/bin/uv\"\n        mock_run.return_value = MagicMock(\n            returncode=0, stdout=\"Environment cached\", stderr=\"\"\n        )\n\n        env = UVEnvironment(python=\"3.10\")\n\n        await env.prepare(tmp_path / \"test-env\")\n\n        # Should run multiple uv commands for initializing the project\n        assert mock_run.call_count > 0\n\n        # Check the first call should be uv init\n        first_call_args = mock_run.call_args_list[0][0][0]\n        assert first_call_args[0] == \"uv\"\n        assert \"init\" in first_call_args\n\n    @patch(\"subprocess.run\")\n    @patch(\"shutil.which\")\n    async def test_prepare_with_dependencies(self, mock_which, mock_run, tmp_path):\n        \"\"\"Test that prepare() includes dependencies.\"\"\"\n        mock_which.return_value = \"/usr/bin/uv\"\n        mock_run.return_value = MagicMock(returncode=0, stdout=\"\", stderr=\"\")\n\n        env = UVEnvironment(dependencies=[\"numpy\", \"pandas\"])\n\n        await env.prepare(tmp_path / \"test-env\")\n\n        # Should run multiple uv commands, one of which should be uv add\n        assert mock_run.call_count > 0\n\n        # Find the add command call\n        add_call = None\n        for call_args, _ in mock_run.call_args_list:\n            args = call_args[0]\n            if \"add\" in args:\n                add_call = args\n                break\n\n        assert add_call is not None, \"Should have called uv add\"\n        assert \"numpy\" in add_call\n        assert \"pandas\" in add_call\n        assert \"fastmcp\" in add_call  # Always added\n\n    @patch(\"subprocess.run\")\n    @patch(\"shutil.which\")\n    async def test_prepare_command_fails(self, mock_which, mock_run, tmp_path):\n        \"\"\"Test that prepare() raises error when uv command fails.\"\"\"\n        mock_which.return_value = \"/usr/bin/uv\"\n        mock_run.side_effect = subprocess.CalledProcessError(\n            1, [\"uv\"], stderr=\"Package not found\"\n        )\n\n        env = UVEnvironment(python=\"3.10\")\n\n        with pytest.raises(RuntimeError, match=\"Failed to initialize project\"):\n            await env.prepare(tmp_path / \"test-env\")\n\n\nclass TestProjectPrepareCommand:\n    \"\"\"Test the CLI project prepare command.\"\"\"\n\n    @patch(\"fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file\")\n    @patch(\"fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config\")\n    async def test_project_prepare_auto_detect(self, mock_find, mock_from_file):\n        \"\"\"Test project prepare with auto-detected config.\"\"\"\n        from fastmcp.cli.cli import prepare\n\n        # Setup mocks\n        mock_find.return_value = Path(\"fastmcp.json\")\n        mock_config = AsyncMock()\n        mock_from_file.return_value = mock_config\n\n        # Run command with output_dir\n        with patch(\"sys.exit\"):\n            with patch(\"fastmcp.cli.cli.console.print\") as mock_print:\n                await prepare(config_path=None, output_dir=\"./test-env\")\n\n        # Should find and load config\n        mock_find.assert_called_once()\n        mock_from_file.assert_called_once_with(Path(\"fastmcp.json\"))\n\n        # Should call prepare with output_dir\n        mock_config.prepare.assert_called_once_with(\n            skip_source=False,\n            output_dir=Path(\"./test-env\"),\n        )\n\n        # Should print success message\n        mock_print.assert_called()\n        success_call = mock_print.call_args_list[-1][0][0]\n        assert \"Project prepared successfully\" in success_call\n\n    @patch(\"pathlib.Path.exists\")\n    @patch(\"fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file\")\n    async def test_project_prepare_explicit_path(self, mock_from_file, mock_exists):\n        \"\"\"Test project prepare with explicit config path.\"\"\"\n        from fastmcp.cli.cli import prepare\n\n        # Setup mocks\n        mock_exists.return_value = True\n        mock_config = AsyncMock()\n        mock_from_file.return_value = mock_config\n\n        # Run command with explicit path\n        with patch(\"fastmcp.cli.cli.console.print\"):\n            await prepare(config_path=\"myconfig.json\", output_dir=\"./test-env\")\n\n        # Should load specified config\n        mock_from_file.assert_called_once_with(Path(\"myconfig.json\"))\n\n        # Should call prepare\n        mock_config.prepare.assert_called_once_with(\n            skip_source=False,\n            output_dir=Path(\"./test-env\"),\n        )\n\n    @patch(\"fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config\")\n    async def test_project_prepare_no_config_found(self, mock_find):\n        \"\"\"Test project prepare when no config is found.\"\"\"\n        from fastmcp.cli.cli import prepare\n\n        # Setup mocks\n        mock_find.return_value = None\n\n        # Run command without output_dir - should exit with error for missing output_dir\n        with pytest.raises(SystemExit) as exc_info:\n            with patch(\"fastmcp.cli.cli.logger.error\") as mock_error:\n                await prepare(config_path=None, output_dir=None)\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        mock_error.assert_called()\n        error_msg = mock_error.call_args[0][0]\n        assert \"--output-dir parameter is required\" in error_msg\n\n    @patch(\"pathlib.Path.exists\")\n    async def test_project_prepare_config_not_exists(self, mock_exists):\n        \"\"\"Test project prepare when specified config doesn't exist.\"\"\"\n        from fastmcp.cli.cli import prepare\n\n        # Setup mocks\n        mock_exists.return_value = False\n\n        # Run command without output_dir - should exit with error for missing output_dir\n        with pytest.raises(SystemExit) as exc_info:\n            with patch(\"fastmcp.cli.cli.logger.error\") as mock_error:\n                await prepare(config_path=\"missing.json\", output_dir=None)\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        mock_error.assert_called()\n        error_msg = mock_error.call_args[0][0]\n        assert \"--output-dir parameter is required\" in error_msg\n\n    @patch(\"pathlib.Path.exists\")\n    @patch(\"fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file\")\n    async def test_project_prepare_failure(self, mock_from_file, mock_exists):\n        \"\"\"Test project prepare when prepare() fails.\"\"\"\n        from fastmcp.cli.cli import prepare\n\n        # Setup mocks\n        mock_exists.return_value = True\n        mock_config = AsyncMock()\n        mock_config.prepare.side_effect = RuntimeError(\"Preparation failed\")\n        mock_from_file.return_value = mock_config\n\n        # Run command - should exit with error\n        with pytest.raises(SystemExit) as exc_info:\n            with patch(\"fastmcp.cli.cli.console.print\") as mock_print:\n                await prepare(config_path=\"config.json\", output_dir=\"./test-env\")\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        # Should print error message\n        error_call = mock_print.call_args_list[-1][0][0]\n        assert \"Failed to prepare project\" in error_call\n"
  },
  {
    "path": "tests/cli/test_run.py",
    "content": "import inspect\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom pydantic import ValidationError\n\nfrom fastmcp.cli.cli import inspector, run\nfrom fastmcp.cli.run import (\n    create_mcp_config_server,\n    is_url,\n    run_module_command,\n)\nfrom fastmcp.client.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.mcp_config import MCPConfig, StdioMCPServer\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\n\nclass TestUrlDetection:\n    \"\"\"Test URL detection functionality.\"\"\"\n\n    def test_is_url_valid_http(self):\n        \"\"\"Test detection of valid HTTP URLs.\"\"\"\n        assert is_url(\"http://example.com\")\n        assert is_url(\"http://localhost:8080\")\n        assert is_url(\"http://127.0.0.1:3000/path\")\n\n    def test_is_url_valid_https(self):\n        \"\"\"Test detection of valid HTTPS URLs.\"\"\"\n        assert is_url(\"https://example.com\")\n        assert is_url(\"https://api.example.com/mcp\")\n        assert is_url(\"https://localhost:8443\")\n\n    def test_is_url_invalid(self):\n        \"\"\"Test detection of non-URLs.\"\"\"\n        assert not is_url(\"server.py\")\n        assert not is_url(\"/path/to/server.py\")\n        assert not is_url(\"server.py:app\")\n        assert not is_url(\"ftp://example.com\")  # Not http/https\n        assert not is_url(\"file:///path/to/file\")\n\n\nclass TestFileSystemSource:\n    \"\"\"Test FileSystemSource path parsing functionality.\"\"\"\n\n    def test_parse_simple_path(self, tmp_path):\n        \"\"\"Test parsing simple file path without object.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"# test server\")\n\n        source = FileSystemSource(path=str(test_file))\n        assert Path(source.path).resolve() == test_file.resolve()\n        assert source.entrypoint is None\n\n    def test_parse_path_with_object(self, tmp_path):\n        \"\"\"Test parsing file path with object specification.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"# test server\")\n\n        source = FileSystemSource(path=f\"{test_file}:app\")\n        assert Path(source.path).resolve() == test_file.resolve()\n        assert source.entrypoint == \"app\"\n\n    def test_parse_complex_object(self, tmp_path):\n        \"\"\"Test parsing file path with complex object specification.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"# test server\")\n\n        # The implementation splits on the last colon, so file:module:app\n        # becomes file_path=\"file:module\" and entrypoint=\"app\"\n        # We need to create a file with a colon in the name for this test\n        complex_file = tmp_path / \"server:module.py\"\n        complex_file.write_text(\"# test server\")\n\n        source = FileSystemSource(path=f\"{complex_file}:app\")\n        assert Path(source.path).resolve() == complex_file.resolve()\n        assert source.entrypoint == \"app\"\n\n    async def test_load_server_nonexistent(self):\n        \"\"\"Test loading nonexistent file path exits.\"\"\"\n        source = FileSystemSource(path=\"nonexistent.py\")\n        with pytest.raises(SystemExit) as exc_info:\n            await source.load_server()\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n\n    async def test_load_server_directory(self, tmp_path):\n        \"\"\"Test loading directory path exits.\"\"\"\n        source = FileSystemSource(path=str(tmp_path))\n        with pytest.raises(SystemExit) as exc_info:\n            await source.load_server()\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n\n\nclass TestMCPConfig:\n    \"\"\"Test MCPConfig functionality.\"\"\"\n\n    async def test_run_mcp_config(self, tmp_path: Path):\n        \"\"\"Test creating a server from an MCPConfig file.\"\"\"\n        server_script = inspect.cleandoc(\"\"\"\n            from fastmcp import FastMCP\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def add(a: int, b: int) -> int:\n                return a + b\n\n            if __name__ == '__main__':\n                mcp.run()\n            \"\"\")\n\n        script_path: Path = tmp_path / \"test.py\"\n        script_path.write_text(server_script)\n\n        mcp_config_path = tmp_path / \"mcp_config.json\"\n\n        mcp_config = MCPConfig(\n            mcpServers={\n                \"test_server\": StdioMCPServer(command=\"python\", args=[str(script_path)])\n            }\n        )\n        mcp_config.write_to_file(mcp_config_path)\n\n        server: FastMCP[None] = create_mcp_config_server(mcp_config_path)\n\n        client = Client[FastMCPTransport](server)\n\n        async with client:\n            tools = await client.list_tools()\n            assert len(tools) == 1\n\n    async def test_validate_mcp_config(self, tmp_path: Path):\n        \"\"\"Test creating a server from an MCPConfig file.\"\"\"\n\n        mcp_config_path = tmp_path / \"mcp_config.json\"\n\n        mcp_config = {\"mcpServers\": {\"test_server\": dict(x=1, y=2)}}\n        with mcp_config_path.open(\"w\") as f:\n            json.dump(mcp_config, f)\n\n        with pytest.raises(ValidationError, match=\"validation errors for MCPConfig\"):\n            create_mcp_config_server(mcp_config_path)\n\n\nclass TestServerImport:\n    \"\"\"Test server import functionality using real files.\"\"\"\n\n    async def test_import_server_basic_mcp(self, tmp_path):\n        \"\"\"Test importing server with basic FastMCP server.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\n\nmcp = fastmcp.FastMCP(\"TestServer\")\n\n@mcp.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\"\"\")\n\n        source = FileSystemSource(path=str(test_file))\n        server = await source.load_server()\n        assert server.name == \"TestServer\"\n        tools = await server.list_tools()\n        assert any(t.name == \"greet\" for t in tools)\n\n    async def test_import_server_with_main_block(self, tmp_path):\n        \"\"\"Test importing server with if __name__ == '__main__' block.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\n\napp = fastmcp.FastMCP(\"MainServer\")\n\n@app.tool\ndef calculate(x: int, y: int) -> int:\n    return x + y\n\nif __name__ == \"__main__\":\n    app.run()\n\"\"\")\n\n        source = FileSystemSource(path=str(test_file))\n        server = await source.load_server()\n        assert server.name == \"MainServer\"\n        tools = await server.list_tools()\n        assert any(t.name == \"calculate\" for t in tools)\n\n    async def test_import_server_standard_names(self, tmp_path):\n        \"\"\"Test automatic detection of standard names (mcp, server, app).\"\"\"\n        # Test with 'mcp' name\n        mcp_file = tmp_path / \"mcp_server.py\"\n        mcp_file.write_text(\"\"\"\nimport fastmcp\nmcp = fastmcp.FastMCP(\"MCPServer\")\n\"\"\")\n\n        source = FileSystemSource(path=str(mcp_file))\n        server = await source.load_server()\n        assert server.name == \"MCPServer\"\n\n        # Test with 'server' name\n        server_file = tmp_path / \"server_server.py\"\n        server_file.write_text(\"\"\"\nimport fastmcp\nserver = fastmcp.FastMCP(\"ServerServer\")\n\"\"\")\n\n        source = FileSystemSource(path=str(server_file))\n        server = await source.load_server()\n        assert server.name == \"ServerServer\"\n\n        # Test with 'app' name\n        app_file = tmp_path / \"app_server.py\"\n        app_file.write_text(\"\"\"\nimport fastmcp\napp = fastmcp.FastMCP(\"AppServer\")\n\"\"\")\n\n        source = FileSystemSource(path=str(app_file))\n        server = await source.load_server()\n        assert server.name == \"AppServer\"\n\n    async def test_import_server_nonstandard_name(self, tmp_path):\n        \"\"\"Test importing server with non-standard object name.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\n\nmy_custom_server = fastmcp.FastMCP(\"CustomServer\")\n\n@my_custom_server.tool\ndef custom_tool() -> str:\n    return \"custom\"\n\"\"\")\n\n        source = FileSystemSource(path=f\"{test_file}:my_custom_server\")\n        server = await source.load_server()\n        assert server.name == \"CustomServer\"\n        tools = await server.list_tools()\n        assert any(t.name == \"custom_tool\" for t in tools)\n\n    async def test_import_server_no_standard_names_fails(self, tmp_path):\n        \"\"\"Test importing server when no standard names exist fails.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\n\nother_name = fastmcp.FastMCP(\"OtherServer\")\n\"\"\")\n\n        source = FileSystemSource(path=str(test_file))\n        with pytest.raises(SystemExit) as exc_info:\n            await source.load_server()\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n\n    async def test_import_server_nonexistent_object_fails(self, tmp_path):\n        \"\"\"Test importing nonexistent server object fails.\"\"\"\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\n\nmcp = fastmcp.FastMCP(\"TestServer\")\n\"\"\")\n\n        source = FileSystemSource(path=f\"{test_file}:nonexistent\")\n        with pytest.raises(SystemExit) as exc_info:\n            await source.load_server()\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n\n\nclass TestV1ServerAsync:\n    \"\"\"Test FastMCP 1.x server async support.\"\"\"\n\n    async def test_run_v1_server_stdio(self, tmp_path):\n        \"\"\"Test that v1 server uses async stdio method.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.cli.run import run_command\n\n        # Create a v1 FastMCP server file with both sync and async tools\n        test_file = tmp_path / \"v1_server.py\"\n        test_file.write_text(\"\"\"\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"V1Server\")\n\n@mcp.tool()\ndef sync_echo(text: str) -> str:\n    '''Sync tool for testing'''\n    return f\"sync: {text}\"\n\n@mcp.tool()\nasync def async_echo(text: str) -> str:\n    '''Async tool for testing'''\n    return f\"async: {text}\"\n\"\"\")\n\n        # Mock the async run method\n        with patch.object(\n            FastMCP1x, \"run_stdio_async\", new_callable=AsyncMock\n        ) as run_mock:\n            await run_command(str(test_file), transport=\"stdio\")\n            run_mock.assert_called_once()\n\n    async def test_run_v1_server_http(self, tmp_path):\n        \"\"\"Test that v1 server uses async http method.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.cli.run import run_command\n\n        # Create a v1 FastMCP server file with both sync and async tools\n        test_file = tmp_path / \"v1_server.py\"\n        test_file.write_text(\"\"\"\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"V1Server\")\n\n@mcp.tool()\ndef sync_echo(text: str) -> str:\n    '''Sync tool for testing'''\n    return f\"sync: {text}\"\n\n@mcp.tool()\nasync def async_echo(text: str) -> str:\n    '''Async tool for testing'''\n    return f\"async: {text}\"\n\"\"\")\n\n        # Mock the async run method\n        with patch.object(\n            FastMCP1x, \"run_streamable_http_async\", new_callable=AsyncMock\n        ) as run_mock:\n            await run_command(str(test_file), transport=\"http\")\n            run_mock.assert_called_once()\n\n    async def test_run_v1_server_streamable_http(self, tmp_path):\n        \"\"\"Test that v1 server uses async streamable-http method.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.cli.run import run_command\n\n        # Create a v1 FastMCP server file with both sync and async tools\n        test_file = tmp_path / \"v1_server.py\"\n        test_file.write_text(\"\"\"\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"V1Server\")\n\n@mcp.tool()\ndef sync_echo(text: str) -> str:\n    '''Sync tool for testing'''\n    return f\"sync: {text}\"\n\n@mcp.tool()\nasync def async_echo(text: str) -> str:\n    '''Async tool for testing'''\n    return f\"async: {text}\"\n\"\"\")\n\n        # Mock the async run method\n        with patch.object(\n            FastMCP1x, \"run_streamable_http_async\", new_callable=AsyncMock\n        ) as run_mock:\n            await run_command(str(test_file), transport=\"streamable-http\")\n            run_mock.assert_called_once()\n\n    async def test_run_v1_server_sse(self, tmp_path):\n        \"\"\"Test that v1 server uses async sse method.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.cli.run import run_command\n\n        # Create a v1 FastMCP server file with both sync and async tools\n        test_file = tmp_path / \"v1_server.py\"\n        test_file.write_text(\"\"\"\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"V1Server\")\n\n@mcp.tool()\ndef sync_echo(text: str) -> str:\n    '''Sync tool for testing'''\n    return f\"sync: {text}\"\n\n@mcp.tool()\nasync def async_echo(text: str) -> str:\n    '''Async tool for testing'''\n    return f\"async: {text}\"\n\"\"\")\n\n        # Mock the async run method\n        with patch.object(\n            FastMCP1x, \"run_sse_async\", new_callable=AsyncMock\n        ) as run_mock:\n            await run_command(str(test_file), transport=\"sse\")\n            run_mock.assert_called_once()\n\n    async def test_run_v1_server_default_transport(self, tmp_path):\n        \"\"\"Test that v1 server uses streamable-http by default.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.cli.run import run_command\n\n        # Create a v1 FastMCP server file with both sync and async tools\n        test_file = tmp_path / \"v1_server.py\"\n        test_file.write_text(\"\"\"\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"V1Server\")\n\n@mcp.tool()\ndef sync_echo(text: str) -> str:\n    '''Sync tool for testing'''\n    return f\"sync: {text}\"\n\n@mcp.tool()\nasync def async_echo(text: str) -> str:\n    '''Async tool for testing'''\n    return f\"async: {text}\"\n\"\"\")\n\n        # Mock the async run method\n        with patch.object(\n            FastMCP1x, \"run_streamable_http_async\", new_callable=AsyncMock\n        ) as run_mock:\n            await run_command(str(test_file))\n            run_mock.assert_called_once()\n\n    async def test_run_v1_server_with_host_port(self, tmp_path):\n        \"\"\"Test that v1 server receives host/port settings.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from mcp.server.fastmcp import FastMCP as FastMCP1x\n\n        from fastmcp.cli.run import run_command\n\n        # Create a v1 FastMCP server file with both sync and async tools\n        test_file = tmp_path / \"v1_server.py\"\n        test_file.write_text(\"\"\"\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"V1Server\")\n\n@mcp.tool()\ndef sync_echo(text: str) -> str:\n    '''Sync tool for testing'''\n    return f\"sync: {text}\"\n\n@mcp.tool()\nasync def async_echo(text: str) -> str:\n    '''Async tool for testing'''\n    return f\"async: {text}\"\n\"\"\")\n\n        # Mock the async run method\n        with patch.object(\n            FastMCP1x, \"run_streamable_http_async\", new_callable=AsyncMock\n        ) as run_mock:\n            await run_command(\n                str(test_file), transport=\"http\", host=\"0.0.0.0\", port=9000\n            )\n            run_mock.assert_called_once()\n\n\nclass TestSkipSource:\n    \"\"\"Test the --skip-source functionality.\"\"\"\n\n    async def test_run_command_calls_prepare_by_default(self, tmp_path):\n        \"\"\"Test that run_command calls source.prepare() by default.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from fastmcp.cli.run import run_command\n\n        # Create a test server file\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\nmcp = fastmcp.FastMCP(\"TestServer\")\n\"\"\")\n\n        # Create a test config file\n        config_file = tmp_path / \"fastmcp.json\"\n        config_data = {\"source\": {\"path\": str(test_file), \"entrypoint\": \"mcp\"}}\n        config_file.write_text(json.dumps(config_data))\n\n        # Mock the prepare method and server run\n        with (\n            patch.object(\n                FileSystemSource, \"prepare\", new_callable=AsyncMock\n            ) as prepare_mock,\n            patch(\"fastmcp.server.server.FastMCP.run_async\", new_callable=AsyncMock),\n        ):\n            # Run the command\n            await run_command(str(config_file))\n\n            # Verify prepare was called\n            prepare_mock.assert_called_once()\n\n    async def test_run_command_skips_prepare_with_flag(self, tmp_path):\n        \"\"\"Test that run_command skips source.prepare() when skip_source=True.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from fastmcp.cli.run import run_command\n\n        # Create a test server file\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\nmcp = fastmcp.FastMCP(\"TestServer\")\n\"\"\")\n\n        # Create a test config file\n        config_file = tmp_path / \"fastmcp.json\"\n        config_data = {\"source\": {\"path\": str(test_file), \"entrypoint\": \"mcp\"}}\n        config_file.write_text(json.dumps(config_data))\n\n        # Mock the prepare method and server run\n        with (\n            patch.object(\n                FileSystemSource, \"prepare\", new_callable=AsyncMock\n            ) as prepare_mock,\n            patch(\"fastmcp.server.server.FastMCP.run_async\", new_callable=AsyncMock),\n        ):\n            # Run the command with skip_source=True\n            await run_command(str(config_file), skip_source=True)\n\n            # Verify prepare was NOT called\n            prepare_mock.assert_not_called()\n\n    async def test_filesystem_source_prepare_by_default(self, tmp_path):\n        \"\"\"Test that FileSystemSource is prepared when using direct file spec.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from fastmcp.cli.run import run_command\n        from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import (\n            FileSystemSource,\n        )\n\n        # Create a test server file\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\nmcp = fastmcp.FastMCP(\"TestServer\")\n\"\"\")\n\n        # Mock the prepare method and server run\n        with (\n            patch.object(\n                FileSystemSource, \"prepare\", new_callable=AsyncMock\n            ) as prepare_mock,\n            patch(\"fastmcp.server.server.FastMCP.run_async\", new_callable=AsyncMock),\n        ):\n            # Run with direct file specification\n            await run_command(str(test_file))\n\n            # Verify prepare was called\n            prepare_mock.assert_called_once()\n\n    async def test_filesystem_source_skip_prepare_with_flag(self, tmp_path):\n        \"\"\"Test that FileSystemSource.prepare() is skipped with skip_source flag.\"\"\"\n        from unittest.mock import AsyncMock, patch\n\n        from fastmcp.cli.run import run_command\n        from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import (\n            FileSystemSource,\n        )\n\n        # Create a test server file\n        test_file = tmp_path / \"server.py\"\n        test_file.write_text(\"\"\"\nimport fastmcp\nmcp = fastmcp.FastMCP(\"TestServer\")\n\"\"\")\n\n        # Mock the prepare method and server run\n        with (\n            patch.object(\n                FileSystemSource, \"prepare\", new_callable=AsyncMock\n            ) as prepare_mock,\n            patch(\"fastmcp.server.server.FastMCP.run_async\", new_callable=AsyncMock),\n        ):\n            # Run with direct file specification and skip_source=True\n            await run_command(str(test_file), skip_source=True)\n\n            # Verify prepare was NOT called\n            prepare_mock.assert_not_called()\n\n\nclass TestReloadFunctionality:\n    \"\"\"Test reload functionality.\"\"\"\n\n    def test_watch_filter_accepts_watched_extensions(self):\n        \"\"\"Test that watch filter accepts common source file extensions.\"\"\"\n        from watchfiles import Change\n\n        from fastmcp.cli.run import _watch_filter\n\n        # Python\n        assert _watch_filter(Change.modified, \"/path/to/file.py\") is True\n        assert _watch_filter(Change.added, \"server.py\") is True\n        # JavaScript/TypeScript\n        assert _watch_filter(Change.modified, \"/path/to/file.js\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.ts\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.jsx\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.tsx\") is True\n        # Markup/Content\n        assert _watch_filter(Change.modified, \"/path/to/file.html\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.md\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.txt\") is True\n        # Styles\n        assert _watch_filter(Change.modified, \"/path/to/file.css\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.scss\") is True\n        # Data/Config\n        assert _watch_filter(Change.modified, \"/path/to/file.json\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.yaml\") is True\n        # Images\n        assert _watch_filter(Change.modified, \"/path/to/file.png\") is True\n        assert _watch_filter(Change.modified, \"/path/to/file.svg\") is True\n\n    def test_watch_filter_rejects_unwatched_extensions(self):\n        \"\"\"Test that watch filter rejects files not in the watched set.\"\"\"\n        from watchfiles import Change\n\n        from fastmcp.cli.run import _watch_filter\n\n        assert _watch_filter(Change.modified, \"/path/to/file.pyc\") is False\n        assert _watch_filter(Change.modified, \"/path/to/file.pyo\") is False\n        assert _watch_filter(Change.modified, \"Dockerfile\") is False\n        assert _watch_filter(Change.modified, \"/path/to/file.lock\") is False\n        assert _watch_filter(Change.modified, \"/path/to/.gitignore\") is False\n\n    def test_all_watched_extensions_are_accepted(self):\n        \"\"\"Test that every extension in WATCHED_EXTENSIONS is accepted.\"\"\"\n        from watchfiles import Change\n\n        from fastmcp.cli.run import WATCHED_EXTENSIONS, _watch_filter\n\n        for ext in WATCHED_EXTENSIONS:\n            path = f\"/path/to/file{ext}\"\n            assert _watch_filter(Change.modified, path) is True, (\n                f\"Expected {ext} to be watched\"\n            )\n\n    def test_watched_extensions_includes_frontend_types(self):\n        \"\"\"Verify WATCHED_EXTENSIONS contains the expected frontend file types.\"\"\"\n        from fastmcp.cli.run import WATCHED_EXTENSIONS\n\n        # Core frontend extensions that must be present\n        expected = {\n            # Python\n            \".py\",\n            # JavaScript/TypeScript\n            \".js\",\n            \".ts\",\n            \".jsx\",\n            \".tsx\",\n            # Markup\n            \".html\",\n            \".md\",\n            \".mdx\",\n            \".xml\",\n            # Styles\n            \".css\",\n            \".scss\",\n            \".sass\",\n            \".less\",\n            # Data/Config\n            \".json\",\n            \".yaml\",\n            \".yml\",\n            \".toml\",\n            # Images\n            \".png\",\n            \".jpg\",\n            \".svg\",\n            # Media\n            \".mp4\",\n            \".mp3\",\n        }\n        for ext in expected:\n            assert ext in WATCHED_EXTENSIONS, f\"Expected {ext} in WATCHED_EXTENSIONS\"\n\n\nclass TestRunModuleCommand:\n    \"\"\"Test run_module_command functionality.\"\"\"\n\n    def test_runs_python_m_module(self):\n        \"\"\"Test that run_module_command invokes python -m <module>.\"\"\"\n        mock_result = MagicMock()\n        mock_result.returncode = 0\n\n        with (\n            patch(\n                \"fastmcp.cli.run.subprocess.run\", return_value=mock_result\n            ) as mock_run,\n            pytest.raises(SystemExit) as exc_info,\n        ):\n            run_module_command(\"my_package\")\n\n        assert exc_info.value.code == 0\n        call_args = mock_run.call_args\n        cmd = call_args[0][0]\n        assert \"-m\" in cmd\n        assert \"my_package\" in cmd\n\n    def test_forwards_extra_args(self):\n        \"\"\"Test that extra arguments are forwarded after the module name.\"\"\"\n        mock_result = MagicMock()\n        mock_result.returncode = 0\n\n        with (\n            patch(\n                \"fastmcp.cli.run.subprocess.run\", return_value=mock_result\n            ) as mock_run,\n            pytest.raises(SystemExit),\n        ):\n            run_module_command(\"my_package\", extra_args=[\"--host\", \"0.0.0.0\"])\n\n        cmd = mock_run.call_args[0][0]\n        assert \"--host\" in cmd\n        assert \"0.0.0.0\" in cmd\n\n    def test_uses_env_command_builder(self):\n        \"\"\"Test that env_command_builder wraps the command.\"\"\"\n        mock_result = MagicMock()\n        mock_result.returncode = 0\n\n        def fake_builder(cmd: list[str]) -> list[str]:\n            return [\"uv\", \"run\", *cmd]\n\n        with (\n            patch(\n                \"fastmcp.cli.run.subprocess.run\", return_value=mock_result\n            ) as mock_run,\n            pytest.raises(SystemExit),\n        ):\n            run_module_command(\"my_package\", env_command_builder=fake_builder)\n\n        cmd = mock_run.call_args[0][0]\n        assert cmd[0] == \"uv\"\n        assert cmd[1] == \"run\"\n        # Should use bare \"python\" (not sys.executable) so uv resolves the interpreter\n        assert cmd[2] == \"python\"\n        assert \"-m\" in cmd\n        assert \"my_package\" in cmd\n\n    def test_exits_with_subprocess_error_code(self):\n        \"\"\"Test that non-zero exit codes from the module are propagated.\"\"\"\n        with (\n            patch(\n                \"fastmcp.cli.run.subprocess.run\",\n                side_effect=subprocess.CalledProcessError(42, [\"python\", \"-m\", \"bad\"]),\n            ),\n            pytest.raises(SystemExit) as exc_info,\n        ):\n            run_module_command(\"bad\")\n\n        assert exc_info.value.code == 42\n\n    def test_no_env_builder_runs_plain_python(self):\n        \"\"\"Test that without env_command_builder, plain python is used.\"\"\"\n        mock_result = MagicMock()\n        mock_result.returncode = 0\n\n        with (\n            patch(\n                \"fastmcp.cli.run.subprocess.run\", return_value=mock_result\n            ) as mock_run,\n            pytest.raises(SystemExit),\n        ):\n            run_module_command(\"my_module\", env_command_builder=None)\n\n        cmd = mock_run.call_args[0][0]\n        assert cmd[0] == sys.executable\n        assert cmd[1] == \"-m\"\n        assert cmd[2] == \"my_module\"\n\n\nclass TestRunModuleMode:\n    \"\"\"Test the run command's module-mode branch.\"\"\"\n\n    async def test_run_module_mode_requires_server_spec(self):\n        \"\"\"Test that module mode exits with error when server_spec is None.\"\"\"\n        with pytest.raises(SystemExit) as exc_info:\n            await run(None, module=True)\n\n        assert exc_info.value.code == 1\n\n    async def test_run_module_mode_warns_ignored_options(self, caplog):\n        \"\"\"Test that ignored options produce a warning in module mode.\"\"\"\n        mock_result = MagicMock()\n        mock_result.returncode = 0\n\n        with (\n            patch(\"fastmcp.cli.run.subprocess.run\", return_value=mock_result),\n            pytest.raises(SystemExit),\n            caplog.at_level(\"WARNING\"),\n        ):\n            await run(\n                \"my_module\",\n                module=True,\n                transport=\"sse\",\n                host=\"0.0.0.0\",\n                port=8080,\n            )\n\n        assert any(\"ignored in module mode\" in r.message for r in caplog.records)\n\n    async def test_run_module_mode_delegates_to_run_module_command(self):\n        \"\"\"Test that module mode calls run_module_command with correct args.\"\"\"\n        mock_result = MagicMock()\n        mock_result.returncode = 0\n\n        with (\n            patch(\n                \"fastmcp.cli.run.subprocess.run\", return_value=mock_result\n            ) as mock_subprocess,\n            pytest.raises(SystemExit),\n        ):\n            await run(\"my_module\", module=True)\n\n        cmd = mock_subprocess.call_args[0][0]\n        assert \"-m\" in cmd\n        assert \"my_module\" in cmd\n\n    async def test_run_module_mode_with_reload(self):\n        \"\"\"Test that --reload in module mode delegates to run_with_reload.\"\"\"\n        with patch(\n            \"fastmcp.cli.run.run_with_reload\", new_callable=AsyncMock\n        ) as mock_reload:\n            await run(\"my_module\", module=True, reload=True, skip_env=True)\n\n        mock_reload.assert_called_once()\n        cmd = mock_reload.call_args[0][0]\n        assert \"fastmcp\" in cmd\n        assert \"--module\" in cmd\n        assert \"--no-reload\" in cmd\n        assert \"my_module\" in cmd\n\n\nclass TestInspectorModuleMode:\n    \"\"\"Test the inspector command's module-mode handling.\"\"\"\n\n    async def test_inspector_module_mode_skips_load_server(self):\n        \"\"\"Test that inspector with module=True skips load_server() and forwards --module.\"\"\"\n        mock_config = MagicMock()\n        mock_config.deployment.port = 8080\n        mock_config.environment.build_command = lambda cmd: cmd\n        mock_config.source.load_server = AsyncMock()\n\n        mock_process = MagicMock()\n        mock_process.returncode = 0\n\n        with (\n            patch(\n                \"fastmcp.cli.cli.load_and_merge_config\",\n                return_value=(mock_config, \"my_module\"),\n            ),\n            patch(\"fastmcp.cli.cli._get_npx_command\", return_value=\"npx\"),\n            patch(\n                \"fastmcp.cli.cli.subprocess.run\", return_value=mock_process\n            ) as mock_subprocess,\n            pytest.raises(SystemExit),\n        ):\n            await inspector(\"my_module\", module=True)\n\n        # load_server should NOT have been called in module mode\n        mock_config.source.load_server.assert_not_called()\n\n        # --module should be in the subprocess command\n        cmd = mock_subprocess.call_args[0][0]\n        assert \"--module\" in cmd\n"
  },
  {
    "path": "tests/cli/test_run_config.py",
    "content": "\"\"\"Integration tests for FastMCP configuration with run command.\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\n\nimport pytest\n\nfrom fastmcp.cli.run import load_mcp_server_config\nfrom fastmcp.utilities.mcp_server_config import (\n    Deployment,\n    MCPServerConfig,\n)\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\n\n@pytest.fixture\ndef sample_config(tmp_path):\n    \"\"\"Create a sample fastmcp.json configuration file with nested structure.\"\"\"\n    config_data = {\n        \"$schema\": \"https://gofastmcp.com/public/schemas/fastmcp.json/v1.json\",\n        \"source\": {\"path\": \"server.py\"},\n        \"environment\": {\"python\": \"3.11\", \"dependencies\": [\"requests\"]},\n        \"deployment\": {\"transport\": \"stdio\", \"env\": {\"TEST_VAR\": \"test_value\"}},\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data, indent=2))\n\n    # Create a simple server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"\"\"\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"Test Server\")\n\n@mcp.tool\ndef test_tool(message: str) -> str:\n    return f\"Echo: {message}\"\n\"\"\")\n\n    return config_file\n\n\ndef test_load_mcp_server_config(sample_config, monkeypatch):\n    \"\"\"Test loading configuration and returning config subsets.\"\"\"\n\n    # Capture environment changes\n    original_env = dict(os.environ)\n\n    try:\n        config = load_mcp_server_config(sample_config)\n\n        # Check that we got the right types\n        assert isinstance(config, MCPServerConfig)\n        assert isinstance(config.source, FileSystemSource)\n        assert isinstance(config.deployment, Deployment)\n        assert isinstance(config.environment, UVEnvironment)\n\n        # Check source - path is not resolved yet, only during load_server\n        assert config.source.path == \"server.py\"\n        assert config.source.entrypoint is None\n\n        # Check environment config\n        assert config.environment.python == \"3.11\"\n        assert config.environment.dependencies == [\"requests\"]\n\n        # Check deployment config\n        assert config.deployment.transport == \"stdio\"\n        assert config.deployment.env == {\"TEST_VAR\": \"test_value\"}\n\n        # Check that environment variables were applied\n        assert os.environ.get(\"TEST_VAR\") == \"test_value\"\n\n    finally:\n        # Restore original environment\n        os.environ.clear()\n        os.environ.update(original_env)\n\n\ndef test_load_config_with_entrypoint_source(tmp_path):\n    \"\"\"Test loading config with entrypoint-format source.\"\"\"\n    config_data = {\n        \"source\": {\"path\": \"src/server.py\", \"entrypoint\": \"app\"},\n        \"deployment\": {\"transport\": \"http\", \"port\": 8000},\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create the server file in subdirectory\n    src_dir = tmp_path / \"src\"\n    src_dir.mkdir()\n    server_file = src_dir / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    config = load_mcp_server_config(config_file)\n\n    # Check source - path is not resolved yet, only during load_server\n    assert config.source.path == \"src/server.py\"\n    assert config.source.entrypoint == \"app\"\n\n    # Check deployment\n    assert config.deployment.transport == \"http\"\n    assert config.deployment.port == 8000\n\n\ndef test_load_config_with_cwd(tmp_path):\n    \"\"\"Test that Deployment applies working directory change.\"\"\"\n\n    # Create a subdirectory\n    subdir = tmp_path / \"subdir\"\n    subdir.mkdir()\n\n    config_data = {\"source\": {\"path\": \"server.py\"}, \"deployment\": {\"cwd\": \"subdir\"}}\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file in subdirectory\n    server_file = subdir / \"server.py\"\n    server_file.write_text(\"# Test server\")\n\n    original_cwd = os.getcwd()\n\n    try:\n        config = load_mcp_server_config(config_file)  # noqa: F841\n\n        # Check that working directory was changed\n        assert Path.cwd() == subdir.resolve()\n\n    finally:\n        # Restore original working directory\n        os.chdir(original_cwd)\n\n\ndef test_load_config_with_relative_cwd(tmp_path):\n    \"\"\"Test configuration with relative working directory.\"\"\"\n\n    # Create nested subdirectories\n    subdir1 = tmp_path / \"dir1\"\n    subdir2 = subdir1 / \"dir2\"\n    subdir2.mkdir(parents=True)\n\n    config_data = {\n        \"source\": {\"path\": \"server.py\"},\n        \"deployment\": {\n            \"cwd\": \"../\"  # Relative to config file location\n        },\n    }\n\n    config_file = subdir2 / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file in parent directory\n    server_file = subdir1 / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    original_cwd = os.getcwd()\n\n    try:\n        config = load_mcp_server_config(config_file)  # noqa: F841\n\n        # Should change to parent directory of config file\n        assert Path.cwd() == subdir1.resolve()\n\n    finally:\n        os.chdir(original_cwd)\n\n\ndef test_load_minimal_config(tmp_path):\n    \"\"\"Test loading minimal configuration with only source.\"\"\"\n    config_data = {\"source\": {\"path\": \"server.py\"}}\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    config = load_mcp_server_config(config_file)\n\n    # Check we got source - path is not resolved yet, only during load_server\n    assert isinstance(config.source, FileSystemSource)\n    assert config.source.path == \"server.py\"\n\n\ndef test_load_config_with_server_args(tmp_path):\n    \"\"\"Test configuration with server arguments.\"\"\"\n    config_data = {\n        \"source\": {\"path\": \"server.py\"},\n        \"deployment\": {\"args\": [\"--debug\", \"--config\", \"custom.json\"]},\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    config = load_mcp_server_config(config_file)\n\n    assert config.deployment.args == [\"--debug\", \"--config\", \"custom.json\"]\n\n\ndef test_load_config_with_log_level(tmp_path):\n    \"\"\"Test configuration with log_level setting.\"\"\"\n    config_data = {\n        \"source\": {\"path\": \"server.py\"},\n        \"deployment\": {\"log_level\": \"DEBUG\"},\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    config = load_mcp_server_config(config_file)\n\n    assert config.deployment.log_level == \"DEBUG\"\n\n\ndef test_load_config_with_various_log_levels(tmp_path):\n    \"\"\"Test that all valid log levels are accepted.\"\"\"\n    valid_levels = [\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n\n    for level in valid_levels:\n        config_data = {\n            \"source\": {\"path\": \"server.py\"},\n            \"deployment\": {\"log_level\": level},\n        }\n\n        config_file = tmp_path / f\"fastmcp_{level}.json\"\n        config_file.write_text(json.dumps(config_data))\n\n        # Create server file\n        server_file = tmp_path / \"server.py\"\n        server_file.write_text(\"# Server\")\n\n        config = load_mcp_server_config(config_file)\n\n        assert config.deployment.log_level == level\n\n\ndef test_config_subset_independence(tmp_path):\n    \"\"\"Test that config subsets can be used independently.\"\"\"\n    config_data = {\n        \"source\": {\"path\": \"server.py\"},\n        \"environment\": {\"python\": \"3.12\", \"dependencies\": [\"pandas\"]},\n        \"deployment\": {\"transport\": \"http\", \"host\": \"0.0.0.0\", \"port\": 3000},\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    config = load_mcp_server_config(config_file)\n\n    # Each subset should be independently usable\n    # Path is not resolved yet, only during load_server\n    assert config.source.path == \"server.py\"\n    assert config.source.entrypoint is None\n\n    assert config.environment.python == \"3.12\"\n    assert config.environment.dependencies == [\"pandas\"]\n    assert config.environment._must_run_with_uv()  # Has dependencies\n\n    assert config.deployment.transport == \"http\"\n    assert config.deployment.host == \"0.0.0.0\"\n    assert config.deployment.port == 3000\n\n\ndef test_environment_config_path_resolution(tmp_path):\n    \"\"\"Test that paths in environment config are resolved correctly.\"\"\"\n    # Create requirements file\n    reqs_file = tmp_path / \"requirements.txt\"\n    reqs_file.write_text(\"fastmcp>=2.0\")\n\n    config_data = {\n        \"source\": {\"path\": \"server.py\"},\n        \"environment\": {\n            \"requirements\": \"requirements.txt\",\n            \"project\": \".\",\n            \"editable\": [\"../other-project\"],\n        },\n    }\n\n    config_file = tmp_path / \"fastmcp.json\"\n    config_file.write_text(json.dumps(config_data))\n\n    # Create server file\n    server_file = tmp_path / \"server.py\"\n    server_file.write_text(\"# Server\")\n\n    config = load_mcp_server_config(config_file)\n\n    # Check that UV command is built with resolved paths\n    uv_cmd = config.environment.build_command([\"fastmcp\", \"run\", \"server.py\"])\n\n    assert \"--with-requirements\" in uv_cmd\n    assert \"--project\" in uv_cmd\n    # Path should be resolved relative to config file\n    req_idx = uv_cmd.index(\"--with-requirements\") + 1\n    assert Path(uv_cmd[req_idx]).is_absolute() or uv_cmd[req_idx] == \"requirements.txt\"\n"
  },
  {
    "path": "tests/cli/test_server_args.py",
    "content": "\"\"\"Test server argument passing functionality.\"\"\"\n\nfrom pathlib import Path\n\nimport pytest\n\nfrom fastmcp.utilities.mcp_server_config import MCPServerConfig\nfrom fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource\n\n\nclass TestServerArguments:\n    \"\"\"Test passing arguments to servers.\"\"\"\n\n    async def test_server_with_argparse(self, tmp_path):\n        \"\"\"Test a server that uses argparse with command line arguments.\"\"\"\n        server_file = tmp_path / \"argparse_server.py\"\n        server_file.write_text(\"\"\"\nimport argparse\nfrom fastmcp import FastMCP\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--name\", default=\"DefaultServer\")\nparser.add_argument(\"--port\", type=int, default=8000)\nparser.add_argument(\"--debug\", action=\"store_true\")\n\nargs = parser.parse_args()\n\nserver_name = f\"{args.name}:{args.port}\"\nif args.debug:\n    server_name += \" (Debug)\"\n\nmcp = FastMCP(server_name)\n\n@mcp.tool\ndef get_config() -> dict:\n    return {\"name\": args.name, \"port\": args.port, \"debug\": args.debug}\n\"\"\")\n\n        # Test with arguments\n        source = FileSystemSource(path=str(server_file))\n        config = MCPServerConfig(source=source)\n\n        from fastmcp.cli.cli import with_argv\n\n        # Simulate passing arguments\n        with with_argv([\"--name\", \"TestServer\", \"--port\", \"9000\", \"--debug\"]):\n            server = await config.source.load_server()\n\n        assert server.name == \"TestServer:9000 (Debug)\"\n\n        # Test the tool works and can access the parsed args\n        tools = await server.list_tools()\n        assert any(t.name == \"get_config\" for t in tools)\n\n    async def test_server_with_no_args(self, tmp_path):\n        \"\"\"Test a server that uses argparse with no arguments (defaults).\"\"\"\n        server_file = tmp_path / \"default_server.py\"\n        server_file.write_text(\"\"\"\nimport argparse\nfrom fastmcp import FastMCP\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--name\", default=\"DefaultName\")\nargs = parser.parse_args()\n\nmcp = FastMCP(args.name)\n\"\"\")\n\n        source = FileSystemSource(path=str(server_file))\n        config = MCPServerConfig(source=source)\n\n        from fastmcp.cli.cli import with_argv\n\n        # Test with empty args list (should use defaults)\n        with with_argv([]):\n            server = await config.source.load_server()\n\n        assert server.name == \"DefaultName\"\n\n    async def test_server_with_sys_argv_access(self, tmp_path):\n        \"\"\"Test a server that directly accesses sys.argv.\"\"\"\n        server_file = tmp_path / \"sysargv_server.py\"\n        server_file.write_text(\"\"\"\nimport sys\nfrom fastmcp import FastMCP\n\n# Direct sys.argv access (less common but should work)\nname = \"DirectServer\"\nif len(sys.argv) > 1 and sys.argv[1] == \"--custom\":\n    name = \"CustomServer\"\n\nmcp = FastMCP(name)\n\"\"\")\n\n        source = FileSystemSource(path=str(server_file))\n        config = MCPServerConfig(source=source)\n\n        from fastmcp.cli.cli import with_argv\n\n        # Test with custom argument\n        with with_argv([\"--custom\"]):\n            server = await config.source.load_server()\n\n        assert server.name == \"CustomServer\"\n\n        # Test without argument\n        with with_argv([]):\n            server = await config.source.load_server()\n\n        assert server.name == \"DirectServer\"\n\n    async def test_config_server_example(self):\n        \"\"\"Test the actual config_server.py example.\"\"\"\n        # Find the examples directory\n        examples_dir = Path(__file__).parent.parent.parent / \"examples\"\n        config_server = examples_dir / \"config_server.py\"\n\n        if not config_server.exists():\n            pytest.skip(\"config_server.py example not found\")\n\n        source = FileSystemSource(path=str(config_server))\n        config = MCPServerConfig(source=source)\n\n        from fastmcp.cli.cli import with_argv\n\n        # Test with debug flag\n        with with_argv([\"--name\", \"TestExample\", \"--debug\"]):\n            server = await config.source.load_server()\n\n        assert server.name == \"TestExample (Debug)\"\n\n        # Verify tools are available\n        tools = await server.list_tools()\n        assert any(t.name == \"get_status\" for t in tools)\n        assert any(t.name == \"echo_message\" for t in tools)\n"
  },
  {
    "path": "tests/cli/test_shared.py",
    "content": "from fastmcp.cli.cli import _parse_env_var\n\n\nclass TestEnvVarParsing:\n    \"\"\"Test environment variable parsing functionality.\"\"\"\n\n    def test_parse_env_var_simple(self):\n        \"\"\"Test parsing simple environment variable.\"\"\"\n        key, value = _parse_env_var(\"API_KEY=secret123\")\n        assert key == \"API_KEY\"\n        assert value == \"secret123\"\n\n    def test_parse_env_var_with_equals_in_value(self):\n        \"\"\"Test parsing env var with equals signs in the value.\"\"\"\n        key, value = _parse_env_var(\"DATABASE_URL=postgresql://user:pass@host:5432/db\")\n        assert key == \"DATABASE_URL\"\n        assert value == \"postgresql://user:pass@host:5432/db\"\n\n    def test_parse_env_var_with_spaces(self):\n        \"\"\"Test parsing env var with spaces (should be stripped).\"\"\"\n        key, value = _parse_env_var(\"  API_KEY  =  secret with spaces  \")\n        assert key == \"API_KEY\"\n        assert value == \"secret with spaces\"\n\n    def test_parse_env_var_empty_value(self):\n        \"\"\"Test parsing env var with empty value.\"\"\"\n        key, value = _parse_env_var(\"EMPTY_VAR=\")\n        assert key == \"EMPTY_VAR\"\n        assert value == \"\"\n"
  },
  {
    "path": "tests/cli/test_tasks.py",
    "content": "\"\"\"Tests for the fastmcp tasks CLI.\"\"\"\n\nimport pytest\n\nfrom fastmcp.cli.tasks import check_distributed_backend, tasks_app\nfrom fastmcp.utilities.tests import temporary_settings\n\n\nclass TestCheckDistributedBackend:\n    \"\"\"Test the distributed backend checker function.\"\"\"\n\n    def test_succeeds_with_redis_url(self):\n        \"\"\"Test that it succeeds with Redis URL.\"\"\"\n        with temporary_settings(docket__url=\"redis://localhost:6379/0\"):\n            check_distributed_backend()\n\n    def test_exits_with_helpful_error_for_memory_url(self):\n        \"\"\"Test that it exits with helpful error for memory:// URLs.\"\"\"\n        with temporary_settings(docket__url=\"memory://test-123\"):\n            with pytest.raises(SystemExit) as exc_info:\n                check_distributed_backend()\n\n            assert isinstance(exc_info.value, SystemExit)\n            assert exc_info.value.code == 1\n\n\nclass TestWorkerCommand:\n    \"\"\"Test the worker command.\"\"\"\n\n    def test_worker_command_parsing(self):\n        \"\"\"Test that worker command parses arguments correctly.\"\"\"\n        command, bound, _ = tasks_app.parse_args([\"worker\", \"server.py\"])\n        assert callable(command)\n        assert command.__name__ == \"worker\"  # type: ignore[attr-defined]\n        assert bound.arguments[\"server_spec\"] == \"server.py\"\n\n\nclass TestTasksAppIntegration:\n    \"\"\"Test the tasks app integration.\"\"\"\n\n    def test_tasks_app_exists(self):\n        \"\"\"Test that the tasks app is properly configured.\"\"\"\n        assert \"tasks\" in tasks_app.name\n        assert \"Docket\" in tasks_app.help\n\n    def test_tasks_app_has_commands(self):\n        \"\"\"Test that all expected commands are registered.\"\"\"\n        # Just verify the app exists and has the right metadata\n        # Detailed command testing is done in individual test classes\n        assert \"tasks\" in tasks_app.name\n        assert tasks_app.help\n"
  },
  {
    "path": "tests/cli/test_with_argv.py",
    "content": "\"\"\"Test the with_argv context manager.\"\"\"\n\nimport sys\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom fastmcp.cli.cli import with_argv\n\n\nclass TestWithArgv:\n    \"\"\"Test the with_argv context manager.\"\"\"\n\n    def test_with_argv_replaces_args(self):\n        \"\"\"Test that with_argv properly replaces sys.argv.\"\"\"\n        original_argv = sys.argv[:]\n        test_args = [\"--name\", \"TestServer\", \"--debug\"]\n\n        with with_argv(test_args):\n            # Should preserve script name and add new args\n            assert sys.argv[0] == original_argv[0]\n            assert sys.argv[1:] == test_args\n\n        # Should restore original argv after context\n        assert sys.argv == original_argv\n\n    def test_with_argv_none_does_nothing(self):\n        \"\"\"Test that with_argv(None) doesn't change sys.argv.\"\"\"\n        original_argv = sys.argv[:]\n\n        with with_argv(None):\n            assert sys.argv == original_argv\n\n        assert sys.argv == original_argv\n\n    def test_with_argv_empty_list(self):\n        \"\"\"Test that with_argv([]) clears arguments but keeps script name.\"\"\"\n        original_argv = sys.argv[:]\n\n        with with_argv([]):\n            # Should have only the script name (no additional args)\n            assert sys.argv == [original_argv[0]]\n            assert len(sys.argv) == 1\n\n        assert sys.argv == original_argv\n\n    def test_with_argv_restores_on_exception(self):\n        \"\"\"Test that sys.argv is restored even if an exception occurs.\"\"\"\n        original_argv = sys.argv[:]\n        test_args = [\"--error\"]\n\n        with pytest.raises(ValueError):\n            with with_argv(test_args):\n                assert sys.argv == [original_argv[0]] + test_args\n                raise ValueError(\"Test error\")\n\n        # Should still restore original argv\n        assert sys.argv == original_argv\n\n    def test_with_argv_nested(self):\n        \"\"\"Test nested with_argv contexts.\"\"\"\n        original_argv = sys.argv[:]\n        args1 = [\"--level1\"]\n        args2 = [\"--level2\", \"--debug\"]\n\n        with with_argv(args1):\n            assert sys.argv == [original_argv[0]] + args1\n\n            with with_argv(args2):\n                assert sys.argv == [original_argv[0]] + args2\n\n            # Should restore to level 1\n            assert sys.argv == [original_argv[0]] + args1\n\n        # Should restore to original\n        assert sys.argv == original_argv\n\n    @patch(\"sys.argv\", [\"test_script.py\", \"existing\", \"args\"])\n    def test_with_argv_with_existing_args(self):\n        \"\"\"Test with_argv when sys.argv already has arguments.\"\"\"\n        original_argv = sys.argv[:]\n        assert original_argv == [\"test_script.py\", \"existing\", \"args\"]\n\n        test_args = [\"--new\", \"args\"]\n\n        with with_argv(test_args):\n            # Should replace existing args but keep script name\n            assert sys.argv == [\"test_script.py\", \"--new\", \"args\"]\n\n        # Should restore original\n        assert sys.argv == original_argv\n"
  },
  {
    "path": "tests/client/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/auth/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/auth/test_oauth_cimd.py",
    "content": "\"\"\"Tests for CIMD (Client ID Metadata Document) support in the OAuth client.\"\"\"\n\nfrom __future__ import annotations\n\nimport warnings\n\nimport httpx\nimport pytest\n\nfrom fastmcp.client.auth import OAuth\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.client.transports.sse import SSETransport\n\nVALID_CIMD_URL = \"https://myapp.example.com/oauth/client.json\"\nMCP_SERVER_URL = \"https://mcp-server.example.com/mcp\"\n\n\nclass TestOAuthClientMetadataURL:\n    \"\"\"Tests for the client_metadata_url parameter on OAuth.\"\"\"\n\n    def test_stored_on_instance(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        assert oauth._client_metadata_url == VALID_CIMD_URL\n\n    def test_none_by_default(self):\n        oauth = OAuth()\n        assert oauth._client_metadata_url is None\n\n    def test_passed_to_parent_on_bind(self):\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n            oauth._bind(MCP_SERVER_URL)\n        assert oauth.context.client_metadata_url == VALID_CIMD_URL\n\n    def test_none_metadata_url_on_parent(self):\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth = OAuth(mcp_url=MCP_SERVER_URL)\n        assert oauth.context.client_metadata_url is None\n\n    def test_unbound_when_no_mcp_url(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        assert oauth._bound is False\n\n    def test_bound_when_mcp_url_provided(self):\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth = OAuth(\n                mcp_url=MCP_SERVER_URL,\n                client_metadata_url=VALID_CIMD_URL,\n            )\n        assert oauth._bound is True\n\n    def test_invalid_cimd_url_rejected(self):\n        \"\"\"CIMD URLs must be HTTPS with a non-root path.\"\"\"\n        with pytest.raises(ValueError, match=\"valid HTTPS URL\"):\n            OAuth(\n                mcp_url=MCP_SERVER_URL,\n                client_metadata_url=\"http://insecure.com/client.json\",\n            )\n\n    def test_root_path_cimd_url_rejected(self):\n        with pytest.raises(ValueError, match=\"valid HTTPS URL\"):\n            OAuth(\n                mcp_url=MCP_SERVER_URL,\n                client_metadata_url=\"https://example.com/\",\n            )\n\n\nclass TestOAuthBind:\n    \"\"\"Tests for the _bind() deferred initialization.\"\"\"\n\n    def test_bind_sets_bound_true(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        assert oauth._bound is False\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth._bind(MCP_SERVER_URL)\n        assert oauth._bound is True\n\n    def test_bind_idempotent(self):\n        \"\"\"Second call to _bind is a no-op.\"\"\"\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth._bind(MCP_SERVER_URL)\n            oauth._bind(\"https://other-server.example.com/mcp\")\n        # First binding wins\n        assert oauth.mcp_url == MCP_SERVER_URL\n\n    def test_bind_sets_mcp_url(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth._bind(MCP_SERVER_URL + \"/\")\n        # Trailing slash stripped\n        assert oauth.mcp_url == MCP_SERVER_URL\n\n    def test_bind_creates_token_storage(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        assert not hasattr(oauth, \"token_storage_adapter\")\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth._bind(MCP_SERVER_URL)\n        assert hasattr(oauth, \"token_storage_adapter\")\n\n    async def test_unbound_raises_runtime_error(self):\n        \"\"\"async_auth_flow should fail clearly when OAuth is not bound.\"\"\"\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        request = httpx.Request(\"GET\", MCP_SERVER_URL)\n        with pytest.raises(RuntimeError, match=\"no server URL\"):\n            async for _ in oauth.async_auth_flow(request):\n                pass\n\n    def test_scopes_forwarded_as_list(self):\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth = OAuth(\n                client_metadata_url=VALID_CIMD_URL,\n                scopes=[\"read\", \"write\"],\n            )\n            oauth._bind(MCP_SERVER_URL)\n        assert oauth.context.client_metadata.scope == \"read write\"\n\n    def test_scopes_forwarded_as_string(self):\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            oauth = OAuth(\n                client_metadata_url=VALID_CIMD_URL,\n                scopes=\"read write\",\n            )\n            oauth._bind(MCP_SERVER_URL)\n        assert oauth.context.client_metadata.scope == \"read write\"\n\n\nclass TestOAuthBindFromTransport:\n    \"\"\"Tests that transports call _bind() on OAuth instances.\"\"\"\n\n    def test_http_transport_binds_oauth(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        assert oauth._bound is False\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            StreamableHttpTransport(MCP_SERVER_URL, auth=oauth)\n        assert oauth._bound is True\n        assert oauth.mcp_url == MCP_SERVER_URL\n\n    def test_sse_transport_binds_oauth(self):\n        oauth = OAuth(client_metadata_url=VALID_CIMD_URL)\n        assert oauth._bound is False\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            SSETransport(MCP_SERVER_URL, auth=oauth)\n        assert oauth._bound is True\n        assert oauth.mcp_url == MCP_SERVER_URL\n\n    def test_http_transport_oauth_string_still_works(self):\n        \"\"\"auth=\"oauth\" should still create a new OAuth instance.\"\"\"\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            transport = StreamableHttpTransport(MCP_SERVER_URL, auth=\"oauth\")\n        assert isinstance(transport.auth, OAuth)\n        assert transport.auth._bound is True\n"
  },
  {
    "path": "tests/client/auth/test_oauth_client.py",
    "content": "from unittest.mock import patch\nfrom urllib.parse import urlparse\n\nimport httpx\nimport pytest\nfrom mcp.types import TextResourceContents\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.auth import OAuth\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.auth.auth import ClientRegistrationOptions\nfrom fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.http import find_available_port\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_async\n\n\ndef fastmcp_server(issuer_url: str):\n    \"\"\"Create a FastMCP server with OAuth authentication.\"\"\"\n    server = FastMCP(\n        \"TestServer\",\n        auth=InMemoryOAuthProvider(\n            base_url=issuer_url,\n            client_registration_options=ClientRegistrationOptions(\n                enabled=True, valid_scopes=[\"read\", \"write\"]\n            ),\n        ),\n    )\n\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @server.resource(\"resource://test\")\n    def get_test_resource() -> str:\n        \"\"\"Get a test resource.\"\"\"\n        return \"Hello from authenticated resource!\"\n\n    return server\n\n\n@pytest.fixture\nasync def streamable_http_server():\n    \"\"\"Start OAuth-enabled server.\"\"\"\n    port = find_available_port()\n    server = fastmcp_server(f\"http://127.0.0.1:{port}\")\n    async with run_server_async(server, port=port, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\ndef client_unauthorized(streamable_http_server: str) -> Client:\n    return Client(transport=StreamableHttpTransport(streamable_http_server))\n\n\n@pytest.fixture\ndef client_with_headless_oauth(streamable_http_server: str) -> Client:\n    \"\"\"Client with headless OAuth that bypasses browser interaction.\"\"\"\n    return Client(\n        transport=StreamableHttpTransport(streamable_http_server),\n        auth=HeadlessOAuth(mcp_url=streamable_http_server, scopes=[\"read\", \"write\"]),\n    )\n\n\nasync def test_unauthorized(client_unauthorized: Client):\n    \"\"\"Test that unauthenticated requests are rejected.\"\"\"\n    with pytest.raises(httpx.HTTPStatusError, match=\"401 Unauthorized\"):\n        async with client_unauthorized:\n            pass\n\n\nasync def test_ping(client_with_headless_oauth: Client):\n    \"\"\"Test that we can ping the server.\"\"\"\n    async with client_with_headless_oauth:\n        assert await client_with_headless_oauth.ping()\n\n\nasync def test_list_tools(client_with_headless_oauth: Client):\n    \"\"\"Test that we can list tools.\"\"\"\n    async with client_with_headless_oauth:\n        tools = await client_with_headless_oauth.list_tools()\n        tool_names = [tool.name for tool in tools]\n        assert \"add\" in tool_names\n\n\nasync def test_call_tool(client_with_headless_oauth: Client):\n    \"\"\"Test that we can call a tool.\"\"\"\n    async with client_with_headless_oauth:\n        result = await client_with_headless_oauth.call_tool(\"add\", {\"a\": 5, \"b\": 3})\n        # The add tool returns int which gets wrapped as structured output\n        # Client unwraps it and puts the actual int in the data field\n        assert result.data == 8\n\n\nasync def test_list_resources(client_with_headless_oauth: Client):\n    \"\"\"Test that we can list resources.\"\"\"\n    async with client_with_headless_oauth:\n        resources = await client_with_headless_oauth.list_resources()\n        resource_uris = [str(resource.uri) for resource in resources]\n        assert \"resource://test\" in resource_uris\n\n\nasync def test_read_resource(client_with_headless_oauth: Client):\n    \"\"\"Test that we can read a resource.\"\"\"\n    async with client_with_headless_oauth:\n        resource = await client_with_headless_oauth.read_resource(\"resource://test\")\n        assert isinstance(resource[0], TextResourceContents)\n        assert resource[0].text == \"Hello from authenticated resource!\"\n\n\nasync def test_oauth_server_metadata_discovery(streamable_http_server: str):\n    \"\"\"Test that we can discover OAuth metadata from the running server.\"\"\"\n    parsed_url = urlparse(streamable_http_server)\n    server_base_url = f\"{parsed_url.scheme}://{parsed_url.netloc}\"\n\n    async with httpx.AsyncClient() as client:\n        # Test OAuth discovery endpoint\n        metadata_url = f\"{server_base_url}/.well-known/oauth-authorization-server\"\n        response = await client.get(metadata_url)\n        assert response.status_code == 200\n\n        metadata = response.json()\n        assert \"authorization_endpoint\" in metadata\n        assert \"token_endpoint\" in metadata\n        assert \"registration_endpoint\" in metadata\n\n        # The endpoints should be properly formed URLs\n        assert metadata[\"authorization_endpoint\"].startswith(server_base_url)\n        assert metadata[\"token_endpoint\"].startswith(server_base_url)\n\n\nclass TestOAuthClientUrlHandling:\n    \"\"\"Tests for OAuth client URL handling (issue #2573).\"\"\"\n\n    def test_oauth_preserves_full_url_with_path(self):\n        \"\"\"OAuth client should preserve the full MCP URL including path components.\n\n        This is critical for servers hosted under path-based endpoints like\n        mcp.example.com/server1/v1.0/mcp where OAuth metadata discovery needs\n        the full path to find the correct .well-known endpoints.\n        \"\"\"\n        mcp_url = \"https://mcp.example.com/server1/v1.0/mcp\"\n        oauth = OAuth(mcp_url=mcp_url)\n\n        # The full URL should be preserved for OAuth discovery\n        assert oauth.context.server_url == mcp_url\n\n        # The stored mcp_url should match\n        assert oauth.mcp_url == mcp_url\n\n    def test_oauth_preserves_root_url(self):\n        \"\"\"OAuth client should work correctly with root-level URLs.\"\"\"\n        mcp_url = \"https://mcp.example.com\"\n        oauth = OAuth(mcp_url=mcp_url)\n\n        assert oauth.context.server_url == mcp_url\n        assert oauth.mcp_url == mcp_url\n\n    def test_oauth_normalizes_trailing_slash(self):\n        \"\"\"OAuth client should normalize trailing slashes for consistency.\"\"\"\n        mcp_url_with_slash = \"https://mcp.example.com/api/mcp/\"\n        oauth = OAuth(mcp_url=mcp_url_with_slash)\n\n        # Trailing slash should be stripped\n        expected = \"https://mcp.example.com/api/mcp\"\n        assert oauth.context.server_url == expected\n        assert oauth.mcp_url == expected\n\n    def test_oauth_token_storage_uses_full_url(self):\n        \"\"\"Token storage should use the full URL to separate tokens per endpoint.\"\"\"\n        mcp_url = \"https://mcp.example.com/server1/v1.0/mcp\"\n        oauth = OAuth(mcp_url=mcp_url)\n\n        # Token storage should key by the full URL, not just the host\n        assert oauth.token_storage_adapter._server_url == mcp_url\n\n\nclass TestOAuthGeneratorCleanup:\n    \"\"\"Tests for OAuth async generator cleanup (issue #2643).\n\n    The MCP SDK's OAuthClientProvider.async_auth_flow() holds a lock via\n    `async with self.context.lock`. If the generator is not explicitly closed,\n    GC may clean it up from a different task, causing:\n    RuntimeError: The current task is not holding this lock\n    \"\"\"\n\n    async def test_generator_closed_on_successful_flow(self):\n        \"\"\"Verify aclose() is called on the parent generator after successful flow.\"\"\"\n        oauth = OAuth(mcp_url=\"https://example.com\")\n\n        # Track generator lifecycle using a wrapper class\n        class TrackedGenerator:\n            def __init__(self):\n                self.aclose_called = False\n                self._exhausted = False\n\n            def __aiter__(self):\n                return self\n\n            async def __anext__(self):\n                if self._exhausted:\n                    raise StopAsyncIteration\n                self._exhausted = True\n                return httpx.Request(\"GET\", \"https://example.com\")\n\n            async def asend(self, value):\n                if self._exhausted:\n                    raise StopAsyncIteration\n                self._exhausted = True\n                return httpx.Request(\"GET\", \"https://example.com\")\n\n            async def athrow(self, exc_type, exc_val=None, exc_tb=None):\n                raise StopAsyncIteration\n\n            async def aclose(self):\n                self.aclose_called = True\n\n        tracked_gen = TrackedGenerator()\n\n        # Patch the parent class to return our tracked generator\n        with patch.object(\n            OAuth.__bases__[0], \"async_auth_flow\", return_value=tracked_gen\n        ):\n            # Drive the OAuth flow\n            flow = oauth.async_auth_flow(httpx.Request(\"GET\", \"https://example.com\"))\n            try:\n                # First asend(None) starts the generator per async generator protocol\n                await flow.asend(None)  # ty: ignore[invalid-argument-type]\n                try:\n                    await flow.asend(httpx.Response(200))\n                except StopAsyncIteration:\n                    pass\n            except StopAsyncIteration:\n                pass\n\n        assert tracked_gen.aclose_called, (\n            \"Generator aclose() was not called after flow completion\"\n        )\n\n    async def test_generator_closed_on_exception(self):\n        \"\"\"Verify aclose() is called even when an exception occurs mid-flow.\"\"\"\n        oauth = OAuth(mcp_url=\"https://example.com\")\n\n        class FailingGenerator:\n            def __init__(self):\n                self.aclose_called = False\n                self._first_call = True\n\n            def __aiter__(self):\n                return self\n\n            async def __anext__(self):\n                return await self.asend(None)\n\n            async def asend(self, value):\n                if self._first_call:\n                    self._first_call = False\n                    return httpx.Request(\"GET\", \"https://example.com\")\n                raise ValueError(\"Simulated failure\")\n\n            async def athrow(self, exc_type, exc_val=None, exc_tb=None):\n                raise StopAsyncIteration\n\n            async def aclose(self):\n                self.aclose_called = True\n\n        tracked_gen = FailingGenerator()\n\n        with patch.object(\n            OAuth.__bases__[0], \"async_auth_flow\", return_value=tracked_gen\n        ):\n            flow = oauth.async_auth_flow(httpx.Request(\"GET\", \"https://example.com\"))\n            with pytest.raises(ValueError, match=\"Simulated failure\"):\n                await flow.asend(None)  # ty: ignore[invalid-argument-type]\n                await flow.asend(httpx.Response(200))\n\n        assert tracked_gen.aclose_called, (\n            \"Generator aclose() was not called after exception\"\n        )\n\n\nclass TestTokenStorageTTL:\n    \"\"\"Tests for client token storage TTL behavior (issue #2670).\n\n    The token storage TTL should NOT be based on access token expiry, because\n    the refresh token may be valid much longer. Using access token expiry would\n    cause both tokens to be deleted when the access token expires, preventing\n    refresh.\n    \"\"\"\n\n    async def test_token_storage_uses_long_ttl(self):\n        \"\"\"Token storage should use a long TTL, not access token expiry.\n\n        This is the ianw case: IdP returns expires_in=300 (5 min access token)\n        but the refresh token is valid for much longer. The entire token entry\n        should NOT be deleted after 5 minutes.\n        \"\"\"\n        from key_value.aio.stores.memory import MemoryStore\n        from mcp.shared.auth import OAuthToken\n\n        from fastmcp.client.auth.oauth import TokenStorageAdapter\n\n        # Create storage adapter\n        storage = MemoryStore()\n        adapter = TokenStorageAdapter(\n            async_key_value=storage, server_url=\"https://test\"\n        )\n\n        # Create a token with short access expiry (5 minutes)\n        token = OAuthToken(\n            access_token=\"test-access-token\",\n            token_type=\"Bearer\",\n            expires_in=300,  # 5 minutes - but we should NOT use this as storage TTL!\n            refresh_token=\"test-refresh-token\",\n            scope=\"read write\",\n        )\n\n        # Store the token\n        await adapter.set_tokens(token)\n\n        # Verify token is stored\n        stored = await adapter.get_tokens()\n        assert stored is not None\n        assert stored.access_token == \"test-access-token\"\n        assert stored.refresh_token == \"test-refresh-token\"\n\n        # The key assertion: the TTL should be 1 year (365 days), not 300 seconds\n        # We verify this by checking the raw storage entry\n        raw = await storage.get(collection=\"mcp-oauth-token\", key=\"https://test/tokens\")\n        assert raw is not None\n\n    async def test_token_storage_preserves_refresh_token(self):\n        \"\"\"Refresh token should not be lost when access token would expire.\"\"\"\n        from key_value.aio.stores.memory import MemoryStore\n        from mcp.shared.auth import OAuthToken\n\n        from fastmcp.client.auth.oauth import TokenStorageAdapter\n\n        storage = MemoryStore()\n        adapter = TokenStorageAdapter(\n            async_key_value=storage, server_url=\"https://test\"\n        )\n\n        # Store token with short access expiry\n        token = OAuthToken(\n            access_token=\"access\",\n            token_type=\"Bearer\",\n            expires_in=300,\n            refresh_token=\"refresh-token-should-survive\",\n            scope=\"read\",\n        )\n        await adapter.set_tokens(token)\n\n        # Retrieve and verify refresh token is present\n        stored = await adapter.get_tokens()\n        assert stored is not None\n        assert stored.refresh_token == \"refresh-token-should-survive\"\n"
  },
  {
    "path": "tests/client/auth/test_oauth_static_client.py",
    "content": "\"\"\"Tests for OAuth static client registration (pre-registered client_id/client_secret).\"\"\"\n\nfrom unittest.mock import patch\n\nimport httpx\nimport pytest\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.auth import OAuth\nfrom fastmcp.client.auth.oauth import ClientNotFoundError\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.auth.auth import ClientRegistrationOptions\nfrom fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.http import find_available_port\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_async\n\n\nclass TestStaticClientInfoConstruction:\n    \"\"\"Static client info should include full metadata from client_metadata.\"\"\"\n\n    def test_static_client_info_includes_metadata(self):\n        \"\"\"Static client info should include redirect_uris, grant_types, etc.\"\"\"\n        oauth = OAuth(\n            mcp_url=\"https://example.com/mcp\",\n            client_id=\"my-client-id\",\n            client_secret=\"my-secret\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        info = oauth._static_client_info\n        assert info is not None\n        assert info.client_id == \"my-client-id\"\n        assert info.client_secret == \"my-secret\"\n        # Metadata fields should be populated from client_metadata\n        assert info.redirect_uris is not None\n        assert len(info.redirect_uris) == 1\n        assert info.grant_types is not None\n        assert \"authorization_code\" in info.grant_types\n        assert \"refresh_token\" in info.grant_types\n        assert info.response_types is not None\n        assert \"code\" in info.response_types\n        assert info.scope == \"read write\"\n        assert info.token_endpoint_auth_method == \"client_secret_post\"\n\n    def test_static_client_info_without_secret(self):\n        \"\"\"Public clients can provide client_id without client_secret.\"\"\"\n        oauth = OAuth(\n            mcp_url=\"https://example.com/mcp\",\n            client_id=\"public-client\",\n        )\n\n        info = oauth._static_client_info\n        assert info is not None\n        assert info.client_id == \"public-client\"\n        assert info.client_secret is None\n        assert info.token_endpoint_auth_method == \"none\"\n        # Metadata should still be present\n        assert info.redirect_uris is not None\n        assert info.grant_types is not None\n\n    def test_no_static_client_info_without_client_id(self):\n        \"\"\"When no client_id is provided, _static_client_info should be None.\"\"\"\n        oauth = OAuth(mcp_url=\"https://example.com/mcp\")\n        assert oauth._static_client_info is None\n\n    def test_static_client_info_includes_additional_metadata(self):\n        \"\"\"Additional client metadata should be included in static client info.\"\"\"\n        oauth = OAuth(\n            mcp_url=\"https://example.com/mcp\",\n            client_id=\"my-client\",\n            additional_client_metadata={\n                \"token_endpoint_auth_method\": \"client_secret_post\"\n            },\n        )\n\n        info = oauth._static_client_info\n        assert info is not None\n        assert info.token_endpoint_auth_method == \"client_secret_post\"\n\n\nclass TestStaticClientInitialize:\n    \"\"\"_initialize should set context.client_info and persist to storage.\"\"\"\n\n    async def test_initialize_sets_context_client_info(self):\n        \"\"\"_initialize should inject static client info into the auth context.\"\"\"\n        oauth = OAuth(\n            mcp_url=\"https://example.com/mcp\",\n            client_id=\"my-client\",\n            client_secret=\"my-secret\",\n        )\n\n        # Mock the parent _initialize since it needs a real server\n        with patch.object(OAuth.__bases__[0], \"_initialize\", return_value=None):\n            await oauth._initialize()\n\n        assert oauth.context.client_info is not None\n        assert oauth.context.client_info.client_id == \"my-client\"\n        assert oauth.context.client_info.client_secret == \"my-secret\"\n\n    async def test_initialize_persists_static_client_to_storage(self):\n        \"\"\"Static client info should be persisted to token storage.\"\"\"\n        oauth = OAuth(\n            mcp_url=\"https://example.com/mcp\",\n            client_id=\"my-client\",\n            client_secret=\"my-secret\",\n        )\n\n        with patch.object(OAuth.__bases__[0], \"_initialize\", return_value=None):\n            await oauth._initialize()\n\n        # Verify it was persisted to storage\n        stored = await oauth.token_storage_adapter.get_client_info()\n        assert stored is not None\n        assert stored.client_id == \"my-client\"\n\n    async def test_initialize_without_static_creds_works(self):\n        \"\"\"_initialize should not error when no static credentials are provided.\"\"\"\n        oauth = OAuth(mcp_url=\"https://example.com/mcp\")\n\n        with patch.object(OAuth.__bases__[0], \"_initialize\", return_value=None):\n            # This should not raise AttributeError\n            await oauth._initialize()\n\n        # context.client_info should be whatever the parent set (None by default)\n\n\nclass TestStaticClientRetryBehavior:\n    \"\"\"Retry-on-stale-credentials should short-circuit for static creds.\"\"\"\n\n    async def test_retry_skipped_with_static_creds(self):\n        \"\"\"When static creds are rejected, should raise immediately, not retry.\"\"\"\n        oauth = OAuth(\n            mcp_url=\"https://example.com/mcp\",\n            client_id=\"bad-client-id\",\n            client_secret=\"bad-secret\",\n        )\n\n        # Make the parent auth flow raise ClientNotFoundError\n        async def failing_auth_flow(request):\n            raise ClientNotFoundError(\"client not found\")\n            yield  # make it a generator  # noqa: E275\n\n        with patch.object(\n            OAuth.__bases__[0], \"async_auth_flow\", side_effect=failing_auth_flow\n        ):\n            flow = oauth.async_auth_flow(httpx.Request(\"GET\", \"https://example.com\"))\n            with pytest.raises(ClientNotFoundError, match=\"static client credentials\"):\n                await flow.__anext__()\n\n    async def test_retry_still_works_without_static_creds(self):\n        \"\"\"Without static creds, the retry behavior should be preserved.\"\"\"\n        oauth = OAuth(mcp_url=\"https://example.com/mcp\")\n\n        call_count = 0\n\n        async def auth_flow_with_retry(request):\n            nonlocal call_count\n            call_count += 1\n            if call_count == 1:\n                raise ClientNotFoundError(\"client not found\")\n            # Second attempt succeeds\n            yield httpx.Request(\"GET\", \"https://example.com\")\n\n        with patch.object(\n            OAuth.__bases__[0], \"async_auth_flow\", side_effect=auth_flow_with_retry\n        ):\n            flow = oauth.async_auth_flow(httpx.Request(\"GET\", \"https://example.com\"))\n            request = await flow.__anext__()\n            assert request is not None\n            assert call_count == 2\n\n\nclass TestStaticClientE2E:\n    \"\"\"End-to-end tests with a real OAuth server using pre-registered clients.\"\"\"\n\n    async def test_static_client_with_dcr_disabled(self):\n        \"\"\"Static client_id should work when the server has DCR disabled.\"\"\"\n        port = find_available_port()\n        callback_port = find_available_port()\n        issuer_url = f\"http://127.0.0.1:{port}\"\n\n        provider = InMemoryOAuthProvider(\n            base_url=issuer_url,\n            client_registration_options=ClientRegistrationOptions(\n                enabled=False,  # DCR disabled\n                valid_scopes=[\"read\", \"write\"],\n            ),\n        )\n\n        server = FastMCP(\"TestServer\", auth=provider)\n\n        @server.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Pre-register a client directly in the provider.\n        # The redirect_uri must match what the OAuth client will use.\n        pre_registered = OAuthClientInformationFull(\n            client_id=\"pre-registered-client\",\n            client_secret=\"pre-registered-secret\",\n            redirect_uris=[AnyUrl(f\"http://localhost:{callback_port}/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            token_endpoint_auth_method=\"client_secret_post\",\n            scope=\"read write\",\n        )\n        await provider.register_client(pre_registered)\n\n        async with run_server_async(server, port=port, transport=\"http\") as url:\n            oauth = HeadlessOAuth(\n                mcp_url=url,\n                client_id=\"pre-registered-client\",\n                client_secret=\"pre-registered-secret\",\n                scopes=[\"read\", \"write\"],\n                callback_port=callback_port,\n            )\n\n            async with Client(\n                transport=StreamableHttpTransport(url),\n                auth=oauth,\n            ) as client:\n                assert await client.ping()\n                tools = await client.list_tools()\n                assert any(t.name == \"greet\" for t in tools)\n\n    async def test_static_client_with_dcr_enabled(self):\n        \"\"\"Static client_id should also work when DCR is enabled (skips DCR).\"\"\"\n        port = find_available_port()\n        callback_port = find_available_port()\n        issuer_url = f\"http://127.0.0.1:{port}\"\n\n        provider = InMemoryOAuthProvider(\n            base_url=issuer_url,\n            client_registration_options=ClientRegistrationOptions(\n                enabled=True,\n                valid_scopes=[\"read\"],\n            ),\n        )\n\n        server = FastMCP(\"TestServer\", auth=provider)\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        pre_registered = OAuthClientInformationFull(\n            client_id=\"my-app\",\n            client_secret=\"my-secret\",\n            redirect_uris=[AnyUrl(f\"http://localhost:{callback_port}/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            token_endpoint_auth_method=\"client_secret_post\",\n            scope=\"read\",\n        )\n        await provider.register_client(pre_registered)\n\n        async with run_server_async(server, port=port, transport=\"http\") as url:\n            oauth = HeadlessOAuth(\n                mcp_url=url,\n                client_id=\"my-app\",\n                client_secret=\"my-secret\",\n                scopes=[\"read\"],\n                callback_port=callback_port,\n            )\n\n            async with Client(\n                transport=StreamableHttpTransport(url),\n                auth=oauth,\n            ) as client:\n                result = await client.call_tool(\"add\", {\"a\": 3, \"b\": 4})\n                assert result.data == 7\n"
  },
  {
    "path": "tests/client/client/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/client/test_auth.py",
    "content": "\"\"\"Client authentication tests.\"\"\"\n\nimport pytest\nfrom mcp.client.auth import OAuthClientProvider\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.auth.bearer import BearerAuth\nfrom fastmcp.client.transports import (\n    SSETransport,\n    StdioTransport,\n    StreamableHttpTransport,\n)\n\n\nclass TestAuth:\n    def test_default_auth_is_none(self):\n        client = Client(transport=StreamableHttpTransport(\"http://localhost:8000\"))\n        assert client.transport.auth is None\n\n    def test_stdio_doesnt_support_auth(self):\n        with pytest.raises(ValueError, match=\"This transport does not support auth\"):\n            Client(transport=StdioTransport(\"echo\", [\"hello\"]), auth=\"oauth\")\n\n    def test_oauth_literal_sets_up_oauth_shttp(self):\n        client = Client(\n            transport=StreamableHttpTransport(\"http://localhost:8000\"), auth=\"oauth\"\n        )\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert isinstance(client.transport.auth, OAuthClientProvider)\n\n    def test_oauth_literal_pass_direct_to_transport(self):\n        client = Client(\n            transport=StreamableHttpTransport(\"http://localhost:8000\", auth=\"oauth\"),\n        )\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert isinstance(client.transport.auth, OAuthClientProvider)\n\n    def test_oauth_literal_sets_up_oauth_sse(self):\n        client = Client(transport=SSETransport(\"http://localhost:8000\"), auth=\"oauth\")\n        assert isinstance(client.transport, SSETransport)\n        assert isinstance(client.transport.auth, OAuthClientProvider)\n\n    def test_oauth_literal_pass_direct_to_transport_sse(self):\n        client = Client(transport=SSETransport(\"http://localhost:8000\", auth=\"oauth\"))\n        assert isinstance(client.transport, SSETransport)\n        assert isinstance(client.transport.auth, OAuthClientProvider)\n\n    def test_auth_string_sets_up_bearer_auth_shttp(self):\n        client = Client(\n            transport=StreamableHttpTransport(\"http://localhost:8000\"),\n            auth=\"test_token\",\n        )\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert isinstance(client.transport.auth, BearerAuth)\n        assert client.transport.auth.token.get_secret_value() == \"test_token\"\n\n    def test_auth_string_pass_direct_to_transport_shttp(self):\n        client = Client(\n            transport=StreamableHttpTransport(\n                \"http://localhost:8000\", auth=\"test_token\"\n            ),\n        )\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert isinstance(client.transport.auth, BearerAuth)\n        assert client.transport.auth.token.get_secret_value() == \"test_token\"\n\n    def test_auth_string_sets_up_bearer_auth_sse(self):\n        client = Client(\n            transport=SSETransport(\"http://localhost:8000\"),\n            auth=\"test_token\",\n        )\n        assert isinstance(client.transport, SSETransport)\n        assert isinstance(client.transport.auth, BearerAuth)\n        assert client.transport.auth.token.get_secret_value() == \"test_token\"\n\n    def test_auth_string_pass_direct_to_transport_sse(self):\n        client = Client(\n            transport=SSETransport(\"http://localhost:8000\", auth=\"test_token\"),\n        )\n        assert isinstance(client.transport, SSETransport)\n        assert isinstance(client.transport.auth, BearerAuth)\n        assert client.transport.auth.token.get_secret_value() == \"test_token\"\n"
  },
  {
    "path": "tests/client/client/test_client.py",
    "content": "\"\"\"Core client functionality: tools, resources, prompts.\"\"\"\n\nimport asyncio\nimport contextlib\nfrom collections.abc import AsyncIterator\nfrom typing import Any, cast\n\nimport anyio\nimport pytest\nfrom mcp import ClientSession, McpError\nfrom mcp.types import TextContent\nfrom pydantic import AnyUrl\n\nimport fastmcp\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import (\n    ClientTransport,\n    FastMCPTransport,\n)\nfrom fastmcp.server.server import FastMCP\n\n\nasync def test_list_tools(fastmcp_server):\n    \"\"\"Test listing tools with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_tools()\n\n        # Check that our tools are available\n        assert len(result) == 3\n        assert set(tool.name for tool in result) == {\"greet\", \"add\", \"sleep\"}\n\n\nasync def test_list_tools_mcp(fastmcp_server):\n    \"\"\"Test the list_tools_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_tools_mcp()\n\n        # Check that we got the raw MCP ListToolsResult object\n        assert hasattr(result, \"tools\")\n        assert len(result.tools) == 3\n        assert set(tool.name for tool in result.tools) == {\"greet\", \"add\", \"sleep\"}\n\n\nasync def test_call_tool(fastmcp_server):\n    \"\"\"Test calling a tool with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Hello, World!\"\n        assert result.structured_content == {\"result\": \"Hello, World!\"}\n        assert result.data == \"Hello, World!\"\n        assert result.is_error is False\n\n\nasync def test_call_tool_mcp(fastmcp_server):\n    \"\"\"Test the call_tool_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.call_tool_mcp(\"greet\", {\"name\": \"World\"})\n\n        # Check that we got the raw MCP CallToolResult object\n        assert hasattr(result, \"content\")\n        assert hasattr(result, \"isError\")\n        assert result.isError is False\n        # The content is a list, so we'll check the first element\n        # by properly accessing it\n        content = result.content\n        assert len(content) > 0\n        first_content = content[0]\n        content_str = str(first_content)\n        assert \"Hello, World!\" in content_str\n\n\nasync def test_call_tool_with_meta():\n    \"\"\"Test that meta parameter is properly passed from client to server.\"\"\"\n    server = FastMCP(\"MetaTestServer\")\n\n    # Create a tool that accesses the meta from the request context\n    @server.tool\n    def check_meta() -> dict[str, Any]:\n        \"\"\"A tool that returns the meta from the request context.\"\"\"\n        from fastmcp.server.dependencies import get_context\n\n        context = get_context()\n        assert context.request_context is not None\n        meta = context.request_context.meta\n\n        # Return the metadata as a dict\n        if meta is not None:\n            return {\n                \"has_meta\": True,\n                \"user_id\": getattr(meta, \"user_id\", None),\n                \"trace_id\": getattr(meta, \"trace_id\", None),\n            }\n        return {\"has_meta\": False}\n\n    client = Client(transport=FastMCPTransport(server))\n\n    async with client:\n        # Test with meta parameter - verify the server receives it\n        test_meta = {\"user_id\": \"test-123\", \"trace_id\": \"abc-def\"}\n        result = await client.call_tool(\"check_meta\", {}, meta=test_meta)\n\n        assert result.data[\"has_meta\"] is True\n        assert result.data[\"user_id\"] == \"test-123\"\n        assert result.data[\"trace_id\"] == \"abc-def\"\n\n        # Test without meta parameter - verify fields are not present\n        result_no_meta = await client.call_tool(\"check_meta\", {})\n        # When meta is not provided, custom fields should not be present\n        assert result_no_meta.data.get(\"user_id\") is None\n        assert result_no_meta.data.get(\"trace_id\") is None\n\n\nasync def test_list_resources(fastmcp_server):\n    \"\"\"Test listing resources with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_resources()\n\n        # Check that our resource is available\n        assert len(result) == 1\n        assert str(result[0].uri) == \"data://users\"\n\n\nasync def test_list_resources_mcp(fastmcp_server):\n    \"\"\"Test the list_resources_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_resources_mcp()\n\n        # Check that we got the raw MCP ListResourcesResult object\n        assert hasattr(result, \"resources\")\n        assert len(result.resources) == 1\n        assert str(result.resources[0].uri) == \"data://users\"\n\n\nasync def test_list_prompts(fastmcp_server):\n    \"\"\"Test listing prompts with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_prompts()\n\n        # Check that our prompt is available\n        assert len(result) == 1\n        assert result[0].name == \"welcome\"\n\n\nasync def test_list_prompts_mcp(fastmcp_server):\n    \"\"\"Test the list_prompts_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_prompts_mcp()\n\n        # Check that we got the raw MCP ListPromptsResult object\n        assert hasattr(result, \"prompts\")\n        assert len(result.prompts) == 1\n        assert result.prompts[0].name == \"welcome\"\n\n\nasync def test_get_prompt(fastmcp_server):\n    \"\"\"Test getting a prompt with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.get_prompt(\"welcome\", {\"name\": \"Developer\"})\n\n        # The result should contain our welcome message\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Welcome to FastMCP, Developer!\"\n        assert result.description == \"Example greeting prompt.\"\n\n\nasync def test_get_prompt_mcp(fastmcp_server):\n    \"\"\"Test the get_prompt_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.get_prompt_mcp(\"welcome\", {\"name\": \"Developer\"})\n\n        # The result should contain our welcome message\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Welcome to FastMCP, Developer!\"\n        assert result.description == \"Example greeting prompt.\"\n\n\nasync def test_client_serializes_all_non_string_arguments():\n    \"\"\"Test that client always serializes non-string arguments to JSON, regardless of server types.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.prompt\n    def echo_args(arg1: str, arg2: str, arg3: str) -> str:\n        \"\"\"Server accepts all string args but client sends mixed types.\"\"\"\n        return f\"arg1: {arg1}, arg2: {arg2}, arg3: {arg3}\"\n\n    client = Client(transport=FastMCPTransport(server))\n\n    async with client:\n        result = await client.get_prompt(\n            \"echo_args\",\n            {\n                \"arg1\": \"hello\",  # string - should pass through\n                \"arg2\": [1, 2, 3],  # list - should be JSON serialized\n                \"arg3\": {\"key\": \"value\"},  # dict - should be JSON serialized\n            },\n        )\n\n        assert isinstance(result.messages[0].content, TextContent)\n        content = result.messages[0].content.text\n        assert \"arg1: hello\" in content\n        assert \"arg2: [1,2,3]\" in content  # JSON serialized list\n        assert 'arg3: {\"key\":\"value\"}' in content  # JSON serialized dict\n\n\nasync def test_client_server_type_conversion_integration():\n    \"\"\"Test that client serialization works with server-side type conversion.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.prompt\n    def typed_prompt(numbers: list[int], config: dict[str, str]) -> str:\n        \"\"\"Server expects typed args - will convert from JSON strings.\"\"\"\n        return f\"Got {len(numbers)} numbers and {len(config)} config items\"\n\n    client = Client(transport=FastMCPTransport(server))\n\n    async with client:\n        result = await client.get_prompt(\n            \"typed_prompt\",\n            {\"numbers\": [1, 2, 3, 4], \"config\": {\"theme\": \"dark\", \"lang\": \"en\"}},\n        )\n\n        assert isinstance(result.messages[0].content, TextContent)\n        content = result.messages[0].content.text\n        assert \"Got 4 numbers and 2 config items\" in content\n\n\nasync def test_client_serialization_error():\n    \"\"\"Test client error when object cannot be serialized.\"\"\"\n    import pydantic_core\n\n    server = FastMCP(\"TestServer\")\n\n    @server.prompt\n    def any_prompt(data: str) -> str:\n        return f\"Got: {data}\"\n\n    # Create an unserializable object\n    class UnserializableClass:\n        def __init__(self):\n            self.func = lambda x: x  # functions can't be JSON serialized\n\n    client = Client(transport=FastMCPTransport(server))\n\n    async with client:\n        with pytest.raises(\n            pydantic_core.PydanticSerializationError, match=\"Unable to serialize\"\n        ):\n            await client.get_prompt(\"any_prompt\", {\"data\": UnserializableClass()})\n\n\nasync def test_server_deserialization_error():\n    \"\"\"Test server error when JSON string cannot be converted to expected type.\"\"\"\n\n    server = FastMCP(\"TestServer\")\n\n    @server.prompt\n    def strict_typed_prompt(numbers: list[int]) -> str:\n        \"\"\"Expects list of integers but will receive invalid JSON.\"\"\"\n        return f\"Got {len(numbers)} numbers\"\n\n    client = Client(transport=FastMCPTransport(server))\n\n    async with client:\n        with pytest.raises(McpError, match=\"Error rendering prompt\"):\n            await client.get_prompt(\n                \"strict_typed_prompt\",\n                {\n                    \"numbers\": \"not valid json\"  # This will fail server-side conversion\n                },\n            )\n\n\nasync def test_read_resource_invalid_uri(fastmcp_server):\n    \"\"\"Test reading a resource with an invalid URI.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n    with pytest.raises(ValueError, match=\"Provided resource URI is invalid\"):\n        await client.read_resource(\"invalid_uri\")\n\n\nasync def test_read_resource(fastmcp_server):\n    \"\"\"Test reading a resource with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        # Use the URI from the resource we know exists in our server\n        uri = cast(\n            AnyUrl, \"data://users\"\n        )  # Use cast for type hint only, the URI is valid\n        result = await client.read_resource(uri)\n\n        # The contents should include our user list\n        contents_str = str(result[0])\n        assert \"Alice\" in contents_str\n        assert \"Bob\" in contents_str\n        assert \"Charlie\" in contents_str\n\n\nasync def test_read_resource_mcp(fastmcp_server):\n    \"\"\"Test the read_resource_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        # Use the URI from the resource we know exists in our server\n        uri = cast(\n            AnyUrl, \"data://users\"\n        )  # Use cast for type hint only, the URI is valid\n        result = await client.read_resource_mcp(uri)\n\n        # Check that we got the raw MCP ReadResourceResult object\n        assert hasattr(result, \"contents\")\n        assert len(result.contents) > 0\n        contents_str = str(result.contents[0])\n        assert \"Alice\" in contents_str\n        assert \"Bob\" in contents_str\n        assert \"Charlie\" in contents_str\n\n\nasync def test_client_connection(fastmcp_server):\n    \"\"\"Test that connect is idempotent.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    # Connect idempotently\n    async with client:\n        assert client.is_connected()\n        # Make a request to ensure connection is working\n        await client.ping()\n    assert not client.is_connected()\n\n\nasync def test_initialize_called_once(fastmcp_server):\n    \"\"\"Test that initialization is called once and sets initialize_result.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n    async with client:\n        # Verify that initialization succeeded by checking initialize_result\n        assert client.initialize_result is not None\n        assert client.initialize_result.serverInfo is not None\n\n\nasync def test_initialize_result_connected(fastmcp_server):\n    \"\"\"Test that initialize_result returns the correct result when connected.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    # Initialize result should be None before connection\n    assert client.initialize_result is None\n\n    async with client:\n        # Once connected, initialize_result should be available\n        result = client.initialize_result\n\n        # Verify the initialize result has expected properties\n        assert hasattr(result, \"serverInfo\")\n        assert result.serverInfo.name == \"TestServer\"\n        assert result.serverInfo.version is not None\n\n\nasync def test_initialize_result_disconnected(fastmcp_server):\n    \"\"\"Test that initialize_result is None when not connected.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    # Initialize result should be None before connection\n    assert client.initialize_result is None\n\n    # Connect and then disconnect\n    async with client:\n        assert client.is_connected()\n\n    # After disconnection, initialize_result should be None again\n    assert not client.is_connected()\n    assert client.initialize_result is None\n\n\nasync def test_server_info_custom_version():\n    \"\"\"Test that custom version is properly set in serverInfo.\"\"\"\n    # Test with custom version\n    server_with_version = FastMCP(\"CustomVersionServer\", version=\"1.2.3\")\n    client = Client(transport=FastMCPTransport(server_with_version))\n\n    async with client:\n        result = client.initialize_result\n        assert result is not None\n        assert result.serverInfo.name == \"CustomVersionServer\"\n        assert result.serverInfo.version == \"1.2.3\"\n\n    # Test without version (backward compatibility)\n    server_without_version = FastMCP(\"DefaultVersionServer\")\n    client = Client(transport=FastMCPTransport(server_without_version))\n\n    async with client:\n        result = client.initialize_result\n        assert result is not None\n        assert result.serverInfo.name == \"DefaultVersionServer\"\n        # Should fall back to FastMCP version\n        assert result.serverInfo.version == fastmcp.__version__\n\n\nclass _DelayedConnectTransport(ClientTransport):\n    def __init__(\n        self,\n        inner: ClientTransport,\n        connect_started: anyio.Event,\n        allow_connect: anyio.Event,\n    ) -> None:\n        self._inner = inner\n        self._connect_started = connect_started\n        self._allow_connect = allow_connect\n\n    @contextlib.asynccontextmanager\n    async def connect_session(\n        self, **session_kwargs: Any\n    ) -> AsyncIterator[ClientSession]:\n        self._connect_started.set()\n        await self._allow_connect.wait()\n        async with self._inner.connect_session(**session_kwargs) as session:\n            yield session\n\n    async def close(self) -> None:\n        await self._inner.close()\n\n\nasync def test_client_nested_context_manager(fastmcp_server):\n    \"\"\"Test that the client connects and disconnects once in nested context manager.\"\"\"\n\n    client = Client(fastmcp_server)\n\n    # Before connection\n    assert not client.is_connected()\n    assert client._session_state.session is None\n\n    # During connection\n    async with client:\n        assert client.is_connected()\n        assert client._session_state.session is not None\n        session = client._session_state.session\n\n        # Reuse the same session\n        async with client:\n            assert client.is_connected()\n            assert client._session_state.session is session\n\n        # Reuse the same session\n        async with client:\n            assert client.is_connected()\n            assert client._session_state.session is session\n\n    # After connection\n    assert not client.is_connected()\n    assert client._session_state.session is None\n\n\nasync def test_client_context_entry_cancelled_starter_cleans_up(fastmcp_server):\n    connect_started = anyio.Event()\n    allow_connect = anyio.Event()\n\n    client = Client(\n        transport=_DelayedConnectTransport(\n            FastMCPTransport(fastmcp_server),\n            connect_started=connect_started,\n            allow_connect=allow_connect,\n        )\n    )\n\n    async def enter_and_never_reach_body() -> None:\n        async with client:\n            pytest.fail(\n                \"Context body should not be reached when __aenter__ is cancelled\"\n            )\n\n    task = asyncio.create_task(enter_and_never_reach_body())\n    await connect_started.wait()\n\n    task.cancel()\n    with pytest.raises(asyncio.CancelledError):\n        await task\n\n    # Connection startup was cancelled; session state should be fully reset.\n    assert client._session_state.session_task is None\n    assert client._session_state.session is None\n    assert client._session_state.nesting_counter == 0\n\n    # A future connection attempt should work normally.\n    allow_connect.set()\n    async with client:\n        tools = await client.list_tools()\n        assert len(tools) == 3\n\n\nasync def test_cancelled_context_entry_waiter_does_not_close_active_session(\n    fastmcp_server,\n):\n    connect_started = anyio.Event()\n    allow_connect = anyio.Event()\n\n    client = Client(\n        transport=_DelayedConnectTransport(\n            FastMCPTransport(fastmcp_server),\n            connect_started=connect_started,\n            allow_connect=allow_connect,\n        )\n    )\n\n    b_done = asyncio.Event()\n    b_started = asyncio.Event()\n\n    async def task_a() -> int:\n        async with client:\n            await b_done.wait()\n            tools = await client.list_tools()\n            return len(tools)\n\n    async def task_b() -> None:\n        b_started.set()\n        async with client:\n            pytest.fail(\"This context should never be entered due to cancellation\")\n\n    a = asyncio.create_task(task_a())\n    await connect_started.wait()\n\n    b = asyncio.create_task(task_b())\n    await b_started.wait()\n    await asyncio.sleep(0)  # let task_b attempt to acquire the client lock\n\n    b.cancel()\n    allow_connect.set()\n\n    with pytest.raises(asyncio.CancelledError):\n        await b\n\n    # task_b is fully cancelled; allow task_a to exercise the connected session.\n    b_done.set()\n    assert await a == 3\n\n\nasync def test_concurrent_client_context_managers():\n    \"\"\"\n    Test that concurrent client usage doesn't cause cross-task cancel scope issues.\n    https://github.com/PrefectHQ/fastmcp/pull/643\n    \"\"\"\n    # Create a simple server\n    server = FastMCP(\"Test Server\")\n\n    @server.tool\n    def echo(text: str) -> str:\n        \"\"\"Echo tool\"\"\"\n        return text\n\n    # Create client\n    client = Client(server)\n\n    # Track results\n    results = {}\n    errors = []\n\n    async def use_client(task_id: str, delay: float = 0):\n        \"\"\"Use the client with a small delay to ensure overlap\"\"\"\n        try:\n            async with client:\n                # Add a small delay to ensure contexts overlap\n                await asyncio.sleep(delay)\n                # Make an actual call to exercise the session\n                tools = await client.list_tools()\n                results[task_id] = len(tools)\n        except Exception as e:\n            errors.append((task_id, str(e)))\n\n    # Run multiple tasks concurrently\n    # The key is having them enter and exit the context at different times\n    await asyncio.gather(\n        use_client(\"task1\", 0.0),\n        use_client(\"task2\", 0.01),  # Slight delay to ensure overlap\n        use_client(\"task3\", 0.02),\n        return_exceptions=False,\n    )\n\n    assert len(errors) == 0, f\"Errors occurred: {errors}\"\n    assert len(results) == 3\n    assert all(count == 1 for count in results.values())  # All should see 1 tool\n\n\nasync def test_resource_template(fastmcp_server):\n    \"\"\"Test using a resource template with InMemoryClient.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        # First, list templates\n        result = await client.list_resource_templates()\n\n        # Check that our template is available\n        assert len(result) == 1\n        assert \"data://user/{user_id}\" in result[0].uriTemplate\n\n        # Now use the template with a specific user_id\n        uri = cast(AnyUrl, \"data://user/123\")\n        result = await client.read_resource(uri)\n\n        # Check the content matches what we expect for the provided user_id\n        content_str = str(result[0])\n        assert '\"id\":\"123\"' in content_str\n        assert '\"name\":\"User 123\"' in content_str\n        assert '\"active\":true' in content_str\n\n\nasync def test_list_resource_templates_mcp(fastmcp_server):\n    \"\"\"Test the list_resource_templates_mcp method that returns raw MCP protocol objects.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        result = await client.list_resource_templates_mcp()\n\n        # Check that we got the raw MCP ListResourceTemplatesResult object\n        assert hasattr(result, \"resourceTemplates\")\n        assert len(result.resourceTemplates) == 1\n        assert \"data://user/{user_id}\" in result.resourceTemplates[0].uriTemplate\n\n\nasync def test_mcp_resource_generation(fastmcp_server):\n    \"\"\"Test that resources are properly generated in MCP format.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        resources = await client.list_resources()\n        assert len(resources) == 1\n        resource = resources[0]\n\n        # Verify resource has correct MCP format\n        assert hasattr(resource, \"uri\")\n        assert hasattr(resource, \"name\")\n        assert hasattr(resource, \"description\")\n        assert str(resource.uri) == \"data://users\"\n\n\nasync def test_mcp_template_generation(fastmcp_server):\n    \"\"\"Test that templates are properly generated in MCP format.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        templates = await client.list_resource_templates()\n        assert len(templates) == 1\n        template = templates[0]\n\n        # Verify template has correct MCP format\n        assert hasattr(template, \"uriTemplate\")\n        assert hasattr(template, \"name\")\n        assert hasattr(template, \"description\")\n        assert \"data://user/{user_id}\" in template.uriTemplate\n\n\nasync def test_template_access_via_client(fastmcp_server):\n    \"\"\"Test that templates can be accessed through a client.\"\"\"\n    client = Client(transport=FastMCPTransport(fastmcp_server))\n\n    async with client:\n        # Verify template works correctly when accessed\n        uri = cast(AnyUrl, \"data://user/456\")\n        result = await client.read_resource(uri)\n        content_str = str(result[0])\n        assert '\"id\":\"456\"' in content_str\n\n\nasync def test_tagged_resource_metadata(tagged_resources_server):\n    \"\"\"Test that resource metadata is preserved in MCP format.\"\"\"\n    client = Client(transport=FastMCPTransport(tagged_resources_server))\n\n    async with client:\n        resources = await client.list_resources()\n        assert len(resources) == 1\n        resource = resources[0]\n\n        # Verify resource metadata is preserved\n        assert str(resource.uri) == \"data://tagged\"\n        assert resource.description == \"A tagged resource\"\n\n\nasync def test_tagged_template_metadata(tagged_resources_server):\n    \"\"\"Test that template metadata is preserved in MCP format.\"\"\"\n    client = Client(transport=FastMCPTransport(tagged_resources_server))\n\n    async with client:\n        templates = await client.list_resource_templates()\n        assert len(templates) == 1\n        template = templates[0]\n\n        # Verify template metadata is preserved\n        assert \"template://{id}\" in template.uriTemplate\n        assert template.description == \"A tagged template\"\n\n\nasync def test_tagged_template_functionality(tagged_resources_server):\n    \"\"\"Test that tagged templates function correctly when accessed.\"\"\"\n    client = Client(transport=FastMCPTransport(tagged_resources_server))\n\n    async with client:\n        # Verify template functionality\n        uri = cast(AnyUrl, \"template://123\")\n        result = await client.read_resource(uri)\n        content_str = str(result[0])\n        assert '\"id\":\"123\"' in content_str\n        assert '\"type\":\"template_data\"' in content_str\n\n\nasync def test_client_unwraps_result_using_meta():\n    \"\"\"Client should unwrap wrapped results using _meta flag.\"\"\"\n    server = FastMCP()\n\n    @server.tool\n    def list_tool() -> list[int]:\n        return [1, 2, 3]\n\n    client = Client(transport=FastMCPTransport(server))\n    async with client:\n        result = await client.call_tool(\"list_tool\", {})\n        assert result.structured_content == {\"result\": [1, 2, 3]}\n        assert result.data == [1, 2, 3]\n        assert result.meta == {\"fastmcp\": {\"wrap_result\": True}}\n\n\nasync def test_client_does_not_unwrap_dict_result():\n    \"\"\"Client should not unwrap dict results that are not wrapped.\"\"\"\n    server = FastMCP()\n\n    @server.tool\n    def dict_tool() -> dict[str, int]:\n        return {\"a\": 1}\n\n    client = Client(transport=FastMCPTransport(server))\n    async with client:\n        result = await client.call_tool(\"dict_tool\", {})\n        assert result.structured_content == {\"a\": 1}\n        assert result.data == {\"a\": 1}\n        assert result.meta is None\n"
  },
  {
    "path": "tests/client/client/test_error_handling.py",
    "content": "\"\"\"Client error handling tests.\"\"\"\n\nimport pytest\nfrom mcp.types import TextContent\nfrom pydantic import AnyUrl\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.exceptions import ResourceError, ToolError\nfrom fastmcp.server.server import FastMCP\n\n\nclass TestErrorHandling:\n    async def test_general_tool_exceptions_are_not_masked_by_default(self):\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def error_tool():\n            raise ValueError(\"This is a test error (abc)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            result = await client.call_tool_mcp(\"error_tool\", {})\n            assert result.isError\n            assert isinstance(result.content[0], TextContent)\n            assert \"test error\" in result.content[0].text\n            assert \"abc\" in result.content[0].text\n\n    async def test_general_tool_exceptions_are_masked_when_enabled(self):\n        mcp = FastMCP(\"TestServer\", mask_error_details=True)\n\n        @mcp.tool\n        def error_tool():\n            raise ValueError(\"This is a test error (abc)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            result = await client.call_tool_mcp(\"error_tool\", {})\n            assert result.isError\n            assert isinstance(result.content[0], TextContent)\n            assert \"test error\" not in result.content[0].text\n            assert \"abc\" not in result.content[0].text\n\n    async def test_validation_errors_are_not_masked_when_enabled(self):\n        mcp = FastMCP(\"TestServer\", mask_error_details=True)\n\n        @mcp.tool\n        def validated_tool(x: int) -> int:\n            return x\n\n        async with Client(transport=FastMCPTransport(mcp)) as client:\n            result = await client.call_tool_mcp(\"validated_tool\", {\"x\": \"abc\"})\n            assert result.isError\n            # Pydantic validation error message should NOT be masked\n            assert isinstance(result.content[0], TextContent)\n            assert \"Input should be a valid integer\" in result.content[0].text\n\n    async def test_specific_tool_errors_are_sent_to_client(self):\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def custom_error_tool():\n            raise ToolError(\"This is a test error (abc)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            result = await client.call_tool_mcp(\"custom_error_tool\", {})\n            assert result.isError\n            assert isinstance(result.content[0], TextContent)\n            assert \"test error\" in result.content[0].text\n            assert \"abc\" in result.content[0].text\n\n    async def test_general_resource_exceptions_are_not_masked_by_default(self):\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.resource(uri=\"exception://resource\")\n        async def exception_resource():\n            raise ValueError(\"This is an internal error (sensitive)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            with pytest.raises(Exception) as excinfo:\n                await client.read_resource(AnyUrl(\"exception://resource\"))\n            assert \"Error reading resource\" in str(excinfo.value)\n            assert \"sensitive\" in str(excinfo.value)\n            assert \"internal error\" in str(excinfo.value)\n\n    async def test_general_resource_exceptions_are_masked_when_enabled(self):\n        mcp = FastMCP(\"TestServer\", mask_error_details=True)\n\n        @mcp.resource(uri=\"exception://resource\")\n        async def exception_resource():\n            raise ValueError(\"This is an internal error (sensitive)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            with pytest.raises(Exception) as excinfo:\n                await client.read_resource(AnyUrl(\"exception://resource\"))\n            assert \"Error reading resource\" in str(excinfo.value)\n            assert \"sensitive\" not in str(excinfo.value)\n            assert \"internal error\" not in str(excinfo.value)\n\n    async def test_resource_errors_are_sent_to_client(self):\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.resource(uri=\"error://resource\")\n        async def error_resource():\n            raise ResourceError(\"This is a resource error (xyz)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            with pytest.raises(Exception) as excinfo:\n                await client.read_resource(AnyUrl(\"error://resource\"))\n            assert \"This is a resource error (xyz)\" in str(excinfo.value)\n\n    async def test_general_template_exceptions_are_not_masked_by_default(self):\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.resource(uri=\"exception://resource/{id}\")\n        async def exception_resource(id: str):\n            raise ValueError(\"This is an internal error (sensitive)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            with pytest.raises(Exception) as excinfo:\n                await client.read_resource(AnyUrl(\"exception://resource/123\"))\n            assert \"Error reading resource\" in str(excinfo.value)\n            assert \"sensitive\" in str(excinfo.value)\n            assert \"internal error\" in str(excinfo.value)\n\n    async def test_general_template_exceptions_are_masked_when_enabled(self):\n        mcp = FastMCP(\"TestServer\", mask_error_details=True)\n\n        @mcp.resource(uri=\"exception://resource/{id}\")\n        async def exception_resource(id: str):\n            raise ValueError(\"This is an internal error (sensitive)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            with pytest.raises(Exception) as excinfo:\n                await client.read_resource(AnyUrl(\"exception://resource/123\"))\n            assert \"Error reading resource\" in str(excinfo.value)\n            assert \"sensitive\" not in str(excinfo.value)\n            assert \"internal error\" not in str(excinfo.value)\n\n    async def test_template_errors_are_sent_to_client(self):\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.resource(uri=\"error://resource/{id}\")\n        async def error_resource(id: str):\n            raise ResourceError(\"This is a resource error (xyz)\")\n\n        client = Client(transport=FastMCPTransport(mcp))\n\n        async with client:\n            with pytest.raises(Exception) as excinfo:\n                await client.read_resource(AnyUrl(\"error://resource/123\"))\n            assert \"This is a resource error (xyz)\" in str(excinfo.value)\n"
  },
  {
    "path": "tests/client/client/test_initialize.py",
    "content": "\"\"\"Client initialization tests.\"\"\"\n\nfrom fastmcp.client import Client\nfrom fastmcp.server.server import FastMCP\n\n\nclass TestInitialize:\n    \"\"\"Tests for client initialization behavior.\"\"\"\n\n    async def test_auto_initialize_default(self, fastmcp_server):\n        \"\"\"Test that auto_initialize=True is the default and works automatically.\"\"\"\n        client = Client(fastmcp_server)\n\n        async with client:\n            # Should be automatically initialized\n            assert client.initialize_result is not None\n            assert client.initialize_result.serverInfo.name == \"TestServer\"\n            assert client.initialize_result.instructions is None\n\n    async def test_auto_initialize_explicit_true(self, fastmcp_server):\n        \"\"\"Test explicit auto_initialize=True.\"\"\"\n        client = Client(fastmcp_server, auto_initialize=True)\n\n        async with client:\n            assert client.initialize_result is not None\n            assert client.initialize_result.serverInfo.name == \"TestServer\"\n\n    async def test_auto_initialize_false(self, fastmcp_server):\n        \"\"\"Test that auto_initialize=False prevents automatic initialization.\"\"\"\n        client = Client(fastmcp_server, auto_initialize=False)\n\n        async with client:\n            # Should not be automatically initialized\n            assert client.initialize_result is None\n\n    async def test_manual_initialize(self, fastmcp_server):\n        \"\"\"Test manual initialization when auto_initialize=False.\"\"\"\n        client = Client(fastmcp_server, auto_initialize=False)\n\n        async with client:\n            # Manually initialize\n            result = await client.initialize()\n\n            assert result is not None\n            assert result.serverInfo.name == \"TestServer\"\n            assert client.initialize_result is result\n\n    async def test_initialize_idempotent(self, fastmcp_server):\n        \"\"\"Test that calling initialize() multiple times returns cached result.\"\"\"\n        client = Client(fastmcp_server, auto_initialize=False)\n\n        async with client:\n            result1 = await client.initialize()\n            result2 = await client.initialize()\n            result3 = await client.initialize()\n\n            # All should return the same cached result\n            assert result1 is result2\n            assert result2 is result3\n\n    async def test_initialize_with_instructions(self):\n        \"\"\"Test that server instructions are available via initialize_result.\"\"\"\n        server = FastMCP(\"InstructionsServer\", instructions=\"Use the greet tool!\")\n\n        @server.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        client = Client(server)\n\n        async with client:\n            result = client.initialize_result\n            assert result is not None\n            assert result.instructions == \"Use the greet tool!\"\n\n    async def test_initialize_timeout_custom(self, fastmcp_server):\n        \"\"\"Test custom timeout for initialize().\"\"\"\n        client = Client(fastmcp_server, auto_initialize=False)\n\n        async with client:\n            # Should succeed with reasonable timeout\n            result = await client.initialize(timeout=5.0)\n            assert result is not None\n\n    async def test_initialize_property_after_auto_init(self, fastmcp_server):\n        \"\"\"Test accessing initialize_result property after auto-initialization.\"\"\"\n        client = Client(fastmcp_server, auto_initialize=True)\n\n        async with client:\n            # Access via property\n            result = client.initialize_result\n            assert result is not None\n            assert result.serverInfo.name == \"TestServer\"\n\n            # Call method - should return cached\n            result2 = await client.initialize()\n            assert result is result2\n\n    async def test_initialize_property_before_connect(self, fastmcp_server):\n        \"\"\"Test that initialize_result property is None before connection.\"\"\"\n        client = Client(fastmcp_server)\n\n        # Not yet connected\n        assert client.initialize_result is None\n\n    async def test_manual_initialize_can_call_tools(self, fastmcp_server):\n        \"\"\"Test that manually initialized client can call tools.\"\"\"\n        client = Client(fastmcp_server, auto_initialize=False)\n\n        async with client:\n            await client.initialize()\n\n            # Should be able to call tools after manual initialization\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert \"Hello, World!\" in str(result.content)\n"
  },
  {
    "path": "tests/client/client/test_session.py",
    "content": "\"\"\"Client session and task error propagation tests.\"\"\"\n\nimport asyncio\n\nimport pytest\n\nfrom fastmcp.client import Client\n\n\nclass TestSessionTaskErrorPropagation:\n    \"\"\"Tests for ensuring session task errors propagate to client calls.\n\n    Regression tests for https://github.com/PrefectHQ/fastmcp/issues/2595\n    where the client would hang indefinitely when the session task failed\n    (e.g., due to HTTP 4xx/5xx errors) instead of raising an exception.\n    \"\"\"\n\n    async def test_session_task_error_propagates_to_call(self, fastmcp_server):\n        \"\"\"Test that errors in session task propagate to pending client calls.\n\n        When the session task fails (e.g., due to HTTP errors), pending\n        client operations should immediately receive the exception rather\n        than hanging indefinitely.\n        \"\"\"\n        client = Client(fastmcp_server)\n\n        async with client:\n            original_task = client._session_state.session_task\n            assert original_task is not None\n\n            async def never_complete():\n                \"\"\"A coroutine that will never complete normally.\"\"\"\n                await asyncio.sleep(1000)\n\n            async def failing_session():\n                \"\"\"Simulates a session task that raises an error.\"\"\"\n                raise ValueError(\"Simulated HTTP error\")\n\n            # Replace session_task with one that will fail\n            client._session_state.session_task = asyncio.create_task(failing_session())\n\n            # The monitoring should detect the session task failure\n            with pytest.raises(ValueError, match=\"Simulated HTTP error\"):\n                await client._await_with_session_monitoring(never_complete())\n\n            # Restore original task for cleanup\n            client._session_state.session_task = original_task\n\n    async def test_session_task_already_done_with_error(self, fastmcp_server):\n        \"\"\"Test that if session task is already done with error, calls fail immediately.\"\"\"\n        client = Client(fastmcp_server)\n\n        async with client:\n            original_task = client._session_state.session_task\n\n            async def raise_error():\n                raise ValueError(\"Session failed\")\n\n            # Replace session_task with one that has already failed\n            failed_task = asyncio.create_task(raise_error())\n            try:\n                await failed_task\n            except ValueError:\n                pass  # Expected\n            client._session_state.session_task = failed_task\n\n            # New calls should fail immediately with the original error\n            async def simple_coro():\n                return \"should not reach\"\n\n            with pytest.raises(ValueError, match=\"Session failed\"):\n                await client._await_with_session_monitoring(simple_coro())\n\n            # Restore original task for cleanup\n            client._session_state.session_task = original_task\n\n    async def test_session_task_already_done_no_error_raises_runtime_error(\n        self, fastmcp_server\n    ):\n        \"\"\"Test that if session task completes without error, raises RuntimeError.\"\"\"\n        client = Client(fastmcp_server)\n\n        async with client:\n            original_task = client._session_state.session_task\n\n            # Create a task that completes normally (unexpected for session task)\n            completed_task = asyncio.create_task(asyncio.sleep(0))\n            await completed_task\n            client._session_state.session_task = completed_task\n\n            async def simple_coro():\n                return \"should not reach\"\n\n            with pytest.raises(\n                RuntimeError, match=\"Session task completed unexpectedly\"\n            ):\n                await client._await_with_session_monitoring(simple_coro())\n\n            # Restore original task for cleanup\n            client._session_state.session_task = original_task\n\n    async def test_normal_operation_unaffected(self, fastmcp_server):\n        \"\"\"Test that normal operation is unaffected by the monitoring.\"\"\"\n        client = Client(fastmcp_server)\n\n        async with client:\n            # These should all work normally\n            tools = await client.list_tools()\n            assert len(tools) > 0\n\n            result = await client.call_tool(\"greet\", {\"name\": \"Test\"})\n            assert \"Hello, Test!\" in str(result.content)\n\n            resources = await client.list_resources()\n            assert len(resources) > 0\n\n            prompts = await client.list_prompts()\n            assert len(prompts) > 0\n\n    async def test_no_session_task_falls_back_to_direct_await(self, fastmcp_server):\n        \"\"\"Test that when no session task exists, it falls back to direct await.\"\"\"\n        client = Client(fastmcp_server)\n\n        async with client:\n            # Temporarily remove session_task to test fallback\n            original_task = client._session_state.session_task\n            client._session_state.session_task = None\n\n            # Should work via direct await\n            async def simple_coro():\n                return \"success\"\n\n            result = await client._await_with_session_monitoring(simple_coro())\n            assert result == \"success\"\n\n            # Restore for cleanup\n            client._session_state.session_task = original_task\n"
  },
  {
    "path": "tests/client/client/test_timeout.py",
    "content": "\"\"\"Client timeout tests.\"\"\"\n\nimport pytest\nfrom mcp import McpError\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.server.server import FastMCP\n\n\nclass TestTimeout:\n    async def test_timeout(self, fastmcp_server: FastMCP):\n        async with Client(\n            transport=FastMCPTransport(fastmcp_server), timeout=0.05\n        ) as client:\n            with pytest.raises(\n                McpError,\n                match=\"Timed out while waiting for response to ClientRequest. Waited 0.05 seconds\",\n            ):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.1})\n\n    async def test_timeout_tool_call(self, fastmcp_server: FastMCP):\n        async with Client(transport=FastMCPTransport(fastmcp_server)) as client:\n            with pytest.raises(McpError):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.1}, timeout=0.01)\n\n    async def test_timeout_tool_call_overrides_client_timeout(\n        self, fastmcp_server: FastMCP\n    ):\n        async with Client(\n            transport=FastMCPTransport(fastmcp_server),\n            timeout=2,\n        ) as client:\n            with pytest.raises(McpError):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.1}, timeout=0.01)\n\n    async def test_timeout_tool_call_overrides_client_timeout_even_if_lower(\n        self, fastmcp_server: FastMCP\n    ):\n        async with Client(\n            transport=FastMCPTransport(fastmcp_server),\n            timeout=0.1,\n        ) as client:\n            await client.call_tool(\"sleep\", {\"seconds\": 0.5}, timeout=2)\n"
  },
  {
    "path": "tests/client/client/test_transport.py",
    "content": "\"\"\"Client transport inference tests.\"\"\"\n\nimport pytest\n\nfrom fastmcp.client.transports import (\n    FastMCPTransport,\n    MCPConfigTransport,\n    SSETransport,\n    StdioTransport,\n    StreamableHttpTransport,\n    infer_transport,\n)\n\n\nclass TestInferTransport:\n    \"\"\"Tests for the infer_transport function.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"url\",\n        [\n            \"http://example.com/api/sse/stream\",\n            \"https://localhost:8080/mcp/sse/endpoint\",\n            \"http://example.com/api/sse\",\n            \"http://example.com/api/sse/\",\n            \"https://localhost:8080/mcp/sse/\",\n            \"http://example.com/api/sse?param=value\",\n            \"https://localhost:8080/mcp/sse/?param=value\",\n            \"https://localhost:8000/mcp/sse?x=1&y=2\",\n        ],\n        ids=[\n            \"path_with_sse_directory\",\n            \"path_with_sse_subdirectory\",\n            \"path_ending_with_sse\",\n            \"path_ending_with_sse_slash\",\n            \"path_ending_with_sse_https\",\n            \"path_with_sse_and_query_params\",\n            \"path_with_sse_slash_and_query_params\",\n            \"path_with_sse_and_ampersand_param\",\n        ],\n    )\n    def test_url_returns_sse_transport(self, url):\n        \"\"\"Test that URLs with /sse/ pattern return SSETransport.\"\"\"\n        assert isinstance(infer_transport(url), SSETransport)\n\n    @pytest.mark.parametrize(\n        \"url\",\n        [\n            \"http://example.com/api\",\n            \"https://localhost:8080/mcp/\",\n            \"http://example.com/asset/image.jpg\",\n            \"https://localhost:8080/sservice/endpoint\",\n            \"https://example.com/assets/file\",\n        ],\n        ids=[\n            \"regular_http_url\",\n            \"regular_https_url\",\n            \"url_with_unrelated_path\",\n            \"url_with_sservice_in_path\",\n            \"url_with_assets_in_path\",\n        ],\n    )\n    def test_url_returns_streamable_http_transport(self, url):\n        \"\"\"Test that URLs without /sse/ pattern return StreamableHttpTransport.\"\"\"\n        assert isinstance(infer_transport(url), StreamableHttpTransport)\n\n    def test_infer_remote_transport_from_config(self):\n        config = {\n            \"mcpServers\": {\n                \"test_server\": {\n                    \"url\": \"http://localhost:8000/sse/\",\n                    \"headers\": {\"Authorization\": \"Bearer 123\"},\n                },\n            }\n        }\n        transport = infer_transport(config)\n        assert isinstance(transport, MCPConfigTransport)\n        assert isinstance(transport.transport, SSETransport)\n        assert transport.transport.url == \"http://localhost:8000/sse/\"\n        assert transport.transport.headers == {\"Authorization\": \"Bearer 123\"}\n\n    def test_infer_local_transport_from_config(self):\n        config = {\n            \"mcpServers\": {\n                \"test_server\": {\n                    \"command\": \"echo\",\n                    \"args\": [\"hello\"],\n                },\n            }\n        }\n        transport = infer_transport(config)\n        assert isinstance(transport, MCPConfigTransport)\n        assert isinstance(transport.transport, StdioTransport)\n        assert transport.transport.command == \"echo\"\n        assert transport.transport.args == [\"hello\"]\n\n    def test_config_with_no_servers(self):\n        \"\"\"Test that an empty MCPConfig raises a ValueError.\"\"\"\n        config = {\"mcpServers\": {}}\n        with pytest.raises(ValueError, match=\"No MCP servers defined in the config\"):\n            infer_transport(config)\n\n    def test_mcpconfigtransport_with_no_servers(self):\n        \"\"\"Test that MCPConfigTransport raises a ValueError when initialized with an empty config.\"\"\"\n        config = {\"mcpServers\": {}}\n        with pytest.raises(ValueError, match=\"No MCP servers defined in the config\"):\n            MCPConfigTransport(config=config)\n\n    def test_infer_composite_client(self):\n        config = {\n            \"mcpServers\": {\n                \"local\": {\n                    \"command\": \"echo\",\n                    \"args\": [\"hello\"],\n                },\n                \"remote\": {\n                    \"url\": \"http://localhost:8000/sse/\",\n                    \"headers\": {\"Authorization\": \"Bearer 123\"},\n                },\n            }\n        }\n        transport = infer_transport(config)\n        assert isinstance(transport, MCPConfigTransport)\n        # Multi-server configs create composite server at connect time\n        assert len(transport.config.mcpServers) == 2\n\n    def test_infer_fastmcp_server(self, fastmcp_server):\n        \"\"\"FastMCP server instances should infer to FastMCPTransport.\"\"\"\n        transport = infer_transport(fastmcp_server)\n        assert isinstance(transport, FastMCPTransport)\n\n    def test_infer_fastmcp_v1_server(self):\n        \"\"\"FastMCP 1.0 server instances should infer to FastMCPTransport.\"\"\"\n        from mcp.server.fastmcp import FastMCP as FastMCP1\n\n        server = FastMCP1()\n        transport = infer_transport(server)\n        assert isinstance(transport, FastMCPTransport)\n"
  },
  {
    "path": "tests/client/sampling/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/sampling/handlers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/sampling/handlers/test_anthropic_handler.py",
    "content": "from typing import Any\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom anthropic import AsyncAnthropic\nfrom anthropic.types import Message, TextBlock, ToolUseBlock, Usage\nfrom mcp.types import (\n    AudioContent,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    ImageContent,\n    ModelHint,\n    ModelPreferences,\n    SamplingMessage,\n    TextContent,\n    ToolResultContent,\n    ToolUseContent,\n)\n\nfrom fastmcp.client.sampling.handlers.anthropic import (\n    AnthropicSamplingHandler,\n    _image_content_to_anthropic_block,\n)\n\n\ndef test_convert_sampling_messages_to_anthropic_messages():\n    msgs = AnthropicSamplingHandler._convert_to_anthropic_messages(\n        messages=[\n            SamplingMessage(\n                role=\"user\", content=TextContent(type=\"text\", text=\"hello\")\n            ),\n            SamplingMessage(\n                role=\"assistant\", content=TextContent(type=\"text\", text=\"ok\")\n            ),\n        ],\n    )\n\n    assert msgs == [\n        {\"role\": \"user\", \"content\": \"hello\"},\n        {\"role\": \"assistant\", \"content\": \"ok\"},\n    ]\n\n\ndef test_image_content_to_anthropic_block():\n    block = _image_content_to_anthropic_block(\n        ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\")\n    )\n\n    assert block == {\n        \"type\": \"image\",\n        \"source\": {\n            \"type\": \"base64\",\n            \"media_type\": \"image/png\",\n            \"data\": \"YWJj\",\n        },\n    }\n\n\ndef test_image_content_unsupported_mime_type_raises():\n    with pytest.raises(ValueError, match=\"Unsupported image MIME type\"):\n        _image_content_to_anthropic_block(\n            ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/bmp\")\n        )\n\n\ndef test_convert_single_image_content_to_anthropic_message():\n    msgs = AnthropicSamplingHandler._convert_to_anthropic_messages(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\"),\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0] == {\n        \"role\": \"user\",\n        \"content\": [\n            {\n                \"type\": \"image\",\n                \"source\": {\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/png\",\n                    \"data\": \"YWJj\",\n                },\n            }\n        ],\n    }\n\n\ndef test_convert_single_audio_content_raises():\n    with pytest.raises(ValueError, match=\"AudioContent is not supported\"):\n        AnthropicSamplingHandler._convert_to_anthropic_messages(\n            messages=[\n                SamplingMessage(\n                    role=\"user\",\n                    content=AudioContent(\n                        type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\"\n                    ),\n                )\n            ],\n        )\n\n\ndef test_convert_list_content_with_image_and_text():\n    msgs = AnthropicSamplingHandler._convert_to_anthropic_messages(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=[\n                    TextContent(type=\"text\", text=\"Describe this image\"),\n                    ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/jpeg\"),\n                ],\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0] == {\n        \"role\": \"user\",\n        \"content\": [\n            {\"type\": \"text\", \"text\": \"Describe this image\"},\n            {\n                \"type\": \"image\",\n                \"source\": {\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/jpeg\",\n                    \"data\": \"YWJj\",\n                },\n            },\n        ],\n    }\n\n\ndef test_convert_list_content_with_audio_raises():\n    with pytest.raises(ValueError, match=\"AudioContent is not supported\"):\n        AnthropicSamplingHandler._convert_to_anthropic_messages(\n            messages=[\n                SamplingMessage(\n                    role=\"user\",\n                    content=[\n                        TextContent(type=\"text\", text=\"Listen to this\"),\n                        AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\"),\n                    ],\n                )\n            ],\n        )\n\n\ndef test_convert_image_in_assistant_message_raises():\n    with pytest.raises(ValueError, match=\"ImageContent is only supported in user\"):\n        AnthropicSamplingHandler._convert_to_anthropic_messages(\n            messages=[\n                SamplingMessage(\n                    role=\"assistant\",\n                    content=ImageContent(\n                        type=\"image\", data=\"YWJj\", mimeType=\"image/png\"\n                    ),\n                )\n            ],\n        )\n\n\ndef test_convert_list_image_in_assistant_message_raises():\n    with pytest.raises(ValueError, match=\"ImageContent is only supported in user\"):\n        AnthropicSamplingHandler._convert_to_anthropic_messages(\n            messages=[\n                SamplingMessage(\n                    role=\"assistant\",\n                    content=[\n                        TextContent(type=\"text\", text=\"Here's the image\"),\n                        ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\"),\n                    ],\n                )\n            ],\n        )\n\n\n@pytest.mark.parametrize(\n    \"prefs,expected\",\n    [\n        (\"claude-3-5-sonnet-20241022\", \"claude-3-5-sonnet-20241022\"),\n        (\n            ModelPreferences(hints=[ModelHint(name=\"claude-3-5-sonnet-20241022\")]),\n            \"claude-3-5-sonnet-20241022\",\n        ),\n        ([\"claude-3-5-sonnet-20241022\", \"other\"], \"claude-3-5-sonnet-20241022\"),\n        (None, \"fallback-model\"),\n        ([\"unknown-model\"], \"fallback-model\"),\n    ],\n)\ndef test_select_model_from_preferences(prefs: Any, expected: str) -> None:\n    mock_client = MagicMock(spec=AsyncAnthropic)\n    handler = AnthropicSamplingHandler(\n        default_model=\"fallback-model\", client=mock_client\n    )\n    assert handler._select_model_from_preferences(prefs) == expected\n\n\ndef test_message_to_create_message_result():\n    mock_client = MagicMock(spec=AsyncAnthropic)\n    handler = AnthropicSamplingHandler(\n        default_model=\"fallback-model\", client=mock_client\n    )\n\n    message = Message(\n        id=\"msg_123\",\n        type=\"message\",\n        role=\"assistant\",\n        content=[TextBlock(type=\"text\", text=\"HELPFUL CONTENT FROM A VERY SMART LLM\")],\n        model=\"claude-3-5-sonnet-20241022\",\n        stop_reason=\"end_turn\",\n        stop_sequence=None,\n        usage=Usage(input_tokens=10, output_tokens=20),\n    )\n\n    result: CreateMessageResult = handler._message_to_create_message_result(message)\n    assert result == CreateMessageResult(\n        content=TextContent(type=\"text\", text=\"HELPFUL CONTENT FROM A VERY SMART LLM\"),\n        role=\"assistant\",\n        model=\"claude-3-5-sonnet-20241022\",\n    )\n\n\ndef test_message_to_result_with_tools():\n    message = Message(\n        id=\"msg_123\",\n        type=\"message\",\n        role=\"assistant\",\n        content=[\n            TextBlock(type=\"text\", text=\"I'll help you with that.\"),\n            ToolUseBlock(\n                type=\"tool_use\",\n                id=\"toolu_123\",\n                name=\"get_weather\",\n                input={\"location\": \"San Francisco\"},\n            ),\n        ],\n        model=\"claude-3-5-sonnet-20241022\",\n        stop_reason=\"tool_use\",\n        stop_sequence=None,\n        usage=Usage(input_tokens=10, output_tokens=20),\n    )\n\n    result: CreateMessageResultWithTools = (\n        AnthropicSamplingHandler._message_to_result_with_tools(message)\n    )\n\n    assert result.role == \"assistant\"\n    assert result.model == \"claude-3-5-sonnet-20241022\"\n    assert result.stopReason == \"toolUse\"\n    content = result.content_as_list\n    assert len(content) == 2\n    assert content[0] == TextContent(type=\"text\", text=\"I'll help you with that.\")\n    assert content[1] == ToolUseContent(\n        type=\"tool_use\",\n        id=\"toolu_123\",\n        name=\"get_weather\",\n        input={\"location\": \"San Francisco\"},\n    )\n\n\ndef test_convert_tool_choice_auto():\n    result = AnthropicSamplingHandler._convert_tool_choice_to_anthropic(\n        MagicMock(mode=\"auto\")\n    )\n    assert result is not None\n    assert result[\"type\"] == \"auto\"\n\n\ndef test_convert_tool_choice_required():\n    result = AnthropicSamplingHandler._convert_tool_choice_to_anthropic(\n        MagicMock(mode=\"required\")\n    )\n    assert result is not None\n    assert result[\"type\"] == \"any\"\n\n\ndef test_convert_tool_choice_none():\n    result = AnthropicSamplingHandler._convert_tool_choice_to_anthropic(\n        MagicMock(mode=\"none\")\n    )\n    # Anthropic doesn't have \"none\", returns None to signal tools should be omitted\n    assert result is None\n\n\ndef test_convert_tool_choice_unknown_raises():\n    with pytest.raises(ValueError, match=\"Unsupported tool_choice mode\"):\n        AnthropicSamplingHandler._convert_tool_choice_to_anthropic(\n            MagicMock(mode=\"unknown\")\n        )\n\n\ndef test_convert_tools_to_anthropic():\n    from mcp.types import Tool\n\n    tools = [\n        Tool(\n            name=\"get_weather\",\n            description=\"Get the current weather\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\"location\": {\"type\": \"string\"}},\n                \"required\": [\"location\"],\n            },\n        )\n    ]\n\n    result = AnthropicSamplingHandler._convert_tools_to_anthropic(tools)\n\n    assert len(result) == 1\n    assert result[0][\"name\"] == \"get_weather\"\n    assert result[0][\"description\"] == \"Get the current weather\"\n    assert result[0][\"input_schema\"] == {\n        \"type\": \"object\",\n        \"properties\": {\"location\": {\"type\": \"string\"}},\n        \"required\": [\"location\"],\n    }\n\n\ndef test_convert_messages_with_tool_use_content():\n    \"\"\"Test converting messages that include tool use content from assistant.\"\"\"\n    msgs = AnthropicSamplingHandler._convert_to_anthropic_messages(\n        messages=[\n            SamplingMessage(\n                role=\"assistant\",\n                content=ToolUseContent(\n                    type=\"tool_use\",\n                    id=\"toolu_123\",\n                    name=\"get_weather\",\n                    input={\"location\": \"NYC\"},\n                ),\n            ),\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0][\"role\"] == \"assistant\"\n    assert msgs[0][\"content\"] == [\n        {\n            \"type\": \"tool_use\",\n            \"id\": \"toolu_123\",\n            \"name\": \"get_weather\",\n            \"input\": {\"location\": \"NYC\"},\n        }\n    ]\n\n\ndef test_convert_messages_with_tool_result_content():\n    \"\"\"Test converting messages that include tool result content from user.\"\"\"\n    msgs = AnthropicSamplingHandler._convert_to_anthropic_messages(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=ToolResultContent(\n                    type=\"tool_result\",\n                    toolUseId=\"toolu_123\",\n                    content=[TextContent(type=\"text\", text=\"72F and sunny\")],\n                ),\n            ),\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0][\"role\"] == \"user\"\n    assert msgs[0][\"content\"] == [\n        {\n            \"type\": \"tool_result\",\n            \"tool_use_id\": \"toolu_123\",\n            \"content\": \"72F and sunny\",\n            \"is_error\": False,\n        }\n    ]\n"
  },
  {
    "path": "tests/client/sampling/handlers/test_google_genai_handler.py",
    "content": "import base64\nfrom unittest.mock import MagicMock\n\nimport pytest\n\ntry:\n    from google.genai import Client as GoogleGenaiClient\n    from google.genai.types import (\n        Candidate,\n        FunctionCall,\n        FunctionCallingConfigMode,\n        GenerateContentResponse,\n        ModelContent,\n        Part,\n        UserContent,\n    )\n    from mcp.types import (\n        AudioContent,\n        CreateMessageResult,\n        ImageContent,\n        ModelHint,\n        ModelPreferences,\n        SamplingMessage,\n        TextContent,\n        ToolChoice,\n        ToolResultContent,\n        ToolUseContent,\n    )\n\n    from fastmcp.client.sampling.handlers.google_genai import (\n        GoogleGenaiSamplingHandler,\n        _convert_messages_to_google_genai_content,\n        _convert_tool_choice_to_google_genai,\n        _response_to_create_message_result,\n        _response_to_result_with_tools,\n        _sampling_content_to_google_genai_part,\n    )\n\n    GOOGLE_GENAI_AVAILABLE = True\nexcept ImportError:\n    GOOGLE_GENAI_AVAILABLE = False\n\npytestmark = pytest.mark.skipif(\n    not GOOGLE_GENAI_AVAILABLE, reason=\"google-genai not installed\"\n)\n\n\ndef test_convert_sampling_messages_to_google_genai_content():\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\", content=TextContent(type=\"text\", text=\"hello\")\n            ),\n            SamplingMessage(\n                role=\"assistant\", content=TextContent(type=\"text\", text=\"ok\")\n            ),\n        ],\n    )\n\n    assert len(msgs) == 2\n    assert isinstance(msgs[0], UserContent)\n    assert isinstance(msgs[1], ModelContent)\n    assert msgs[0].parts[0].text == \"hello\"\n    assert msgs[1].parts[0].text == \"ok\"\n\n\ndef test_convert_single_image_content_to_google_genai():\n    part = _sampling_content_to_google_genai_part(\n        ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\")\n    )\n\n    assert part.inline_data is not None\n    assert part.inline_data.data == base64.b64decode(\"YWJj\")\n    assert part.inline_data.mime_type == \"image/png\"\n\n\ndef test_convert_single_audio_content_to_google_genai():\n    part = _sampling_content_to_google_genai_part(\n        AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\")\n    )\n\n    assert part.inline_data is not None\n    assert part.inline_data.data == base64.b64decode(\"YWJj\")\n    assert part.inline_data.mime_type == \"audio/wav\"\n\n\ndef test_convert_image_message_to_google_genai_content():\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/jpeg\"),\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert isinstance(msgs[0], UserContent)\n    assert msgs[0].parts[0].inline_data is not None\n    assert msgs[0].parts[0].inline_data.mime_type == \"image/jpeg\"\n\n\ndef test_convert_audio_message_to_google_genai_content():\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/mp3\"),\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert isinstance(msgs[0], UserContent)\n    assert msgs[0].parts[0].inline_data is not None\n    assert msgs[0].parts[0].inline_data.mime_type == \"audio/mp3\"\n\n\ndef test_convert_list_content_with_image_and_text():\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=[\n                    TextContent(type=\"text\", text=\"What is in this image?\"),\n                    ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\"),\n                ],\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert isinstance(msgs[0], UserContent)\n    assert len(msgs[0].parts) == 2\n    assert msgs[0].parts[0].text == \"What is in this image?\"\n    assert msgs[0].parts[1].inline_data is not None\n    assert msgs[0].parts[1].inline_data.mime_type == \"image/png\"\n\n\ndef test_convert_list_content_with_audio_and_text():\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=[\n                    TextContent(type=\"text\", text=\"Transcribe this audio\"),\n                    AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\"),\n                ],\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert isinstance(msgs[0], UserContent)\n    assert len(msgs[0].parts) == 2\n    assert msgs[0].parts[0].text == \"Transcribe this audio\"\n    assert msgs[0].parts[1].inline_data is not None\n    assert msgs[0].parts[1].inline_data.mime_type == \"audio/wav\"\n\n\ndef test_get_model():\n    mock_client = MagicMock(spec=GoogleGenaiClient)\n    handler = GoogleGenaiSamplingHandler(\n        default_model=\"fallback-model\", client=mock_client\n    )\n\n    # Test with Gemini model hint\n    prefs = ModelPreferences(hints=[ModelHint(name=\"gemini-2.0-flash-exp\")])\n    assert handler._get_model(prefs) == \"gemini-2.0-flash-exp\"\n\n    # Test with None\n    assert handler._get_model(None) == \"fallback-model\"\n\n    # Test with empty hints\n    prefs_empty = ModelPreferences(hints=[])\n    assert handler._get_model(prefs_empty) == \"fallback-model\"\n\n    # Test with non-Gemini hint falls back to default\n    prefs_other = ModelPreferences(hints=[ModelHint(name=\"gpt-4o\")])\n    assert handler._get_model(prefs_other) == \"fallback-model\"\n\n    # Test with mixed hints selects first Gemini model\n    prefs_mixed = ModelPreferences(\n        hints=[ModelHint(name=\"claude-3.5-sonnet\"), ModelHint(name=\"gemini-2.0-flash\")]\n    )\n    assert handler._get_model(prefs_mixed) == \"gemini-2.0-flash\"\n\n\nasync def test_response_to_create_message_result():\n    # Create a mock response\n    mock_response = MagicMock(spec=GenerateContentResponse)\n    mock_response.text = \"HELPFUL CONTENT FROM GEMINI\"\n\n    result: CreateMessageResult = _response_to_create_message_result(\n        response=mock_response, model=\"gemini-2.0-flash-exp\"\n    )\n    assert result == CreateMessageResult(\n        content=TextContent(type=\"text\", text=\"HELPFUL CONTENT FROM GEMINI\"),\n        role=\"assistant\",\n        model=\"gemini-2.0-flash-exp\",\n    )\n\n\ndef test_convert_tool_choice_to_google_genai():\n    # Test auto mode\n    result = _convert_tool_choice_to_google_genai(ToolChoice(mode=\"auto\"))\n    assert result.function_calling_config is not None\n    assert result.function_calling_config.mode == FunctionCallingConfigMode.AUTO\n\n    # Test required mode\n    result = _convert_tool_choice_to_google_genai(ToolChoice(mode=\"required\"))\n    assert result.function_calling_config is not None\n    assert result.function_calling_config.mode == FunctionCallingConfigMode.ANY\n\n    # Test none mode\n    result = _convert_tool_choice_to_google_genai(ToolChoice(mode=\"none\"))\n    assert result.function_calling_config is not None\n    assert result.function_calling_config.mode == FunctionCallingConfigMode.NONE\n\n    # Test None (defaults to auto)\n    result = _convert_tool_choice_to_google_genai(None)\n    assert result.function_calling_config is not None\n    assert result.function_calling_config.mode == FunctionCallingConfigMode.AUTO\n\n\ndef test_sampling_content_to_google_genai_part_tool_use():\n    \"\"\"Test converting ToolUseContent to Google GenAI Part with FunctionCall.\"\"\"\n    content = ToolUseContent(\n        type=\"tool_use\",\n        id=\"get_weather_abc123\",\n        name=\"get_weather\",\n        input={\"city\": \"London\"},\n    )\n\n    part = _sampling_content_to_google_genai_part(content)\n\n    assert part.function_call is not None\n    assert part.function_call.name == \"get_weather\"\n    assert part.function_call.args == {\"city\": \"London\"}\n\n\ndef test_sampling_content_to_google_genai_part_tool_result():\n    \"\"\"Test converting ToolResultContent to Google GenAI Part with FunctionResponse.\"\"\"\n    content = ToolResultContent(\n        type=\"tool_result\",\n        toolUseId=\"get_weather_abc123\",\n        content=[TextContent(type=\"text\", text=\"Weather is sunny\")],\n    )\n\n    part = _sampling_content_to_google_genai_part(content)\n\n    assert part.function_response is not None\n    # Function name is extracted from toolUseId by removing the UUID suffix\n    assert part.function_response.name == \"get_weather\"\n    assert part.function_response.response == {\"result\": \"Weather is sunny\"}\n\n\ndef test_sampling_content_to_google_genai_part_tool_result_empty():\n    \"\"\"Test converting empty ToolResultContent to Google GenAI Part.\"\"\"\n    content = ToolResultContent(\n        type=\"tool_result\",\n        toolUseId=\"my_tool_xyz789\",\n        content=[],\n    )\n\n    part = _sampling_content_to_google_genai_part(content)\n\n    assert part.function_response is not None\n    assert part.function_response.name == \"my_tool\"\n    assert part.function_response.response == {\"result\": \"\"}\n\n\ndef test_sampling_content_to_google_genai_part_tool_result_no_underscore():\n    \"\"\"Test ToolResultContent when toolUseId has no underscore (fallback).\"\"\"\n    content = ToolResultContent(\n        type=\"tool_result\",\n        toolUseId=\"simplefunction\",\n        content=[TextContent(type=\"text\", text=\"Result\")],\n    )\n\n    part = _sampling_content_to_google_genai_part(content)\n\n    # When no underscore, the full ID is used as the name\n    assert part.function_response is not None\n    assert part.function_response.name == \"simplefunction\"\n\n\ndef test_convert_messages_with_tool_use():\n    \"\"\"Test converting messages containing ToolUseContent.\"\"\"\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=TextContent(type=\"text\", text=\"What's the weather?\"),\n            ),\n            SamplingMessage(\n                role=\"assistant\",\n                content=ToolUseContent(\n                    type=\"tool_use\",\n                    id=\"get_weather_123\",\n                    name=\"get_weather\",\n                    input={\"city\": \"NYC\"},\n                ),\n            ),\n        ],\n    )\n\n    assert len(msgs) == 2\n    assert isinstance(msgs[0], UserContent)\n    assert isinstance(msgs[1], ModelContent)\n    assert msgs[1].parts[0].function_call is not None\n    assert msgs[1].parts[0].function_call.name == \"get_weather\"\n\n\ndef test_convert_messages_with_tool_result():\n    \"\"\"Test converting messages containing ToolResultContent.\"\"\"\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=ToolResultContent(\n                    type=\"tool_result\",\n                    toolUseId=\"get_weather_123\",\n                    content=[TextContent(type=\"text\", text=\"Sunny, 72F\")],\n                ),\n            ),\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert isinstance(msgs[0], UserContent)\n    assert msgs[0].parts[0].function_response is not None\n    assert msgs[0].parts[0].function_response.name == \"get_weather\"\n\n\ndef test_convert_messages_with_multiple_content_blocks():\n    \"\"\"Test converting messages with multiple content blocks (list content).\"\"\"\n    msgs = _convert_messages_to_google_genai_content(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=[\n                    TextContent(type=\"text\", text=\"I need weather info.\"),\n                    ToolResultContent(\n                        type=\"tool_result\",\n                        toolUseId=\"get_weather_xyz\",\n                        content=[TextContent(type=\"text\", text=\"Cloudy\")],\n                    ),\n                ],\n            ),\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert isinstance(msgs[0], UserContent)\n    assert len(msgs[0].parts) == 2\n    assert msgs[0].parts[0].text == \"I need weather info.\"\n    assert msgs[0].parts[1].function_response is not None\n\n\ndef test_response_to_result_with_tools_text_only():\n    \"\"\"Test _response_to_result_with_tools with a text-only response.\"\"\"\n    mock_candidate = MagicMock(spec=Candidate)\n    mock_candidate.content = MagicMock()\n    mock_candidate.content.parts = [Part(text=\"Here's the answer\")]\n    mock_candidate.finish_reason = \"STOP\"\n\n    mock_response = MagicMock(spec=GenerateContentResponse)\n    mock_response.candidates = [mock_candidate]\n\n    result = _response_to_result_with_tools(mock_response, model=\"gemini-2.0-flash\")\n\n    assert result.role == \"assistant\"\n    assert result.model == \"gemini-2.0-flash\"\n    assert result.stopReason == \"endTurn\"\n    assert isinstance(result.content, list)\n    assert len(result.content) == 1\n    assert result.content[0].type == \"text\"\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"Here's the answer\"\n\n\ndef test_response_to_result_with_tools_function_call():\n    \"\"\"Test _response_to_result_with_tools with a function call response.\"\"\"\n    mock_candidate = MagicMock(spec=Candidate)\n    mock_candidate.content = MagicMock()\n    mock_candidate.content.parts = [\n        Part(function_call=FunctionCall(name=\"get_weather\", args={\"city\": \"Paris\"}))\n    ]\n    mock_candidate.finish_reason = \"STOP\"\n\n    mock_response = MagicMock(spec=GenerateContentResponse)\n    mock_response.candidates = [mock_candidate]\n\n    result = _response_to_result_with_tools(mock_response, model=\"gemini-2.0-flash\")\n\n    assert result.stopReason == \"toolUse\"\n    assert isinstance(result.content, list)\n    assert len(result.content) == 1\n    tool_use = result.content[0]\n    assert isinstance(tool_use, ToolUseContent)\n    assert tool_use.type == \"tool_use\"\n    assert tool_use.name == \"get_weather\"\n    assert tool_use.input == {\"city\": \"Paris\"}\n    # ID should be in format \"get_weather_{uuid}\"\n    assert tool_use.id.startswith(\"get_weather_\")\n\n\ndef test_response_to_result_with_tools_mixed_content():\n    \"\"\"Test _response_to_result_with_tools with text and function call.\"\"\"\n    mock_candidate = MagicMock(spec=Candidate)\n    mock_candidate.content = MagicMock()\n    mock_candidate.content.parts = [\n        Part(text=\"Let me check that for you.\"),\n        Part(function_call=FunctionCall(name=\"search\", args={\"query\": \"test\"})),\n    ]\n    mock_candidate.finish_reason = \"STOP\"\n\n    mock_response = MagicMock(spec=GenerateContentResponse)\n    mock_response.candidates = [mock_candidate]\n\n    result = _response_to_result_with_tools(mock_response, model=\"gemini-2.0-flash\")\n\n    assert result.stopReason == \"toolUse\"\n    assert isinstance(result.content, list)\n    assert len(result.content) == 2\n    text_content = result.content[0]\n    assert isinstance(text_content, TextContent)\n    assert text_content.type == \"text\"\n    assert text_content.text == \"Let me check that for you.\"\n    tool_use = result.content[1]\n    assert isinstance(tool_use, ToolUseContent)\n    assert tool_use.type == \"tool_use\"\n    assert tool_use.name == \"search\"\n"
  },
  {
    "path": "tests/client/sampling/handlers/test_openai_handler.py",
    "content": "from typing import Any\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport pytest\nfrom mcp.types import (\n    AudioContent,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    ImageContent,\n    ModelHint,\n    ModelPreferences,\n    SamplingMessage,\n    TextContent,\n    ToolUseContent,\n)\nfrom openai import AsyncOpenAI\nfrom openai.types.chat import (\n    ChatCompletion,\n    ChatCompletionAssistantMessageParam,\n    ChatCompletionContentPartImageParam,\n    ChatCompletionContentPartInputAudioParam,\n    ChatCompletionContentPartTextParam,\n    ChatCompletionMessage,\n    ChatCompletionSystemMessageParam,\n    ChatCompletionUserMessageParam,\n)\nfrom openai.types.chat.chat_completion import Choice\n\nfrom fastmcp.client.sampling.handlers.openai import (\n    OpenAISamplingHandler,\n    _audio_content_to_openai_part,\n    _image_content_to_openai_part,\n)\n\n\ndef test_convert_sampling_messages_to_openai_messages():\n    msgs = OpenAISamplingHandler._convert_to_openai_messages(\n        system_prompt=\"sys\",\n        messages=[\n            SamplingMessage(\n                role=\"user\", content=TextContent(type=\"text\", text=\"hello\")\n            ),\n            SamplingMessage(\n                role=\"assistant\", content=TextContent(type=\"text\", text=\"ok\")\n            ),\n        ],\n    )\n\n    assert msgs == [\n        ChatCompletionSystemMessageParam(content=\"sys\", role=\"system\"),\n        ChatCompletionUserMessageParam(content=\"hello\", role=\"user\"),\n        ChatCompletionAssistantMessageParam(content=\"ok\", role=\"assistant\"),\n    ]\n\n\ndef test_image_content_to_openai_part():\n    part = _image_content_to_openai_part(\n        ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\")\n    )\n\n    assert part == ChatCompletionContentPartImageParam(\n        type=\"image_url\",\n        image_url={\"url\": \"data:image/png;base64,YWJj\"},\n    )\n\n\ndef test_audio_content_to_openai_part_wav():\n    part = _audio_content_to_openai_part(\n        AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\")\n    )\n\n    assert part == ChatCompletionContentPartInputAudioParam(\n        type=\"input_audio\",\n        input_audio={\"data\": \"YWJj\", \"format\": \"wav\"},\n    )\n\n\ndef test_audio_content_to_openai_part_mp3():\n    part = _audio_content_to_openai_part(\n        AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/mpeg\")\n    )\n\n    assert part[\"input_audio\"][\"format\"] == \"mp3\"\n\n\ndef test_audio_content_to_openai_part_unsupported_raises():\n    with pytest.raises(ValueError, match=\"Unsupported audio MIME type\"):\n        _audio_content_to_openai_part(\n            AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/ogg\")\n        )\n\n\ndef test_image_content_to_openai_part_unsupported_raises():\n    with pytest.raises(ValueError, match=\"Unsupported image MIME type\"):\n        _image_content_to_openai_part(\n            ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/bmp\")\n        )\n\n\ndef test_convert_single_image_content_to_openai_message():\n    msgs = OpenAISamplingHandler._convert_to_openai_messages(\n        system_prompt=None,\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\"),\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0] == ChatCompletionUserMessageParam(\n        role=\"user\",\n        content=[\n            ChatCompletionContentPartImageParam(\n                type=\"image_url\",\n                image_url={\"url\": \"data:image/png;base64,YWJj\"},\n            )\n        ],\n    )\n\n\ndef test_convert_single_audio_content_to_openai_message():\n    msgs = OpenAISamplingHandler._convert_to_openai_messages(\n        system_prompt=None,\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=AudioContent(type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\"),\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0] == ChatCompletionUserMessageParam(\n        role=\"user\",\n        content=[\n            ChatCompletionContentPartInputAudioParam(\n                type=\"input_audio\",\n                input_audio={\"data\": \"YWJj\", \"format\": \"wav\"},\n            )\n        ],\n    )\n\n\ndef test_convert_list_content_with_image_and_text():\n    msgs = OpenAISamplingHandler._convert_to_openai_messages(\n        system_prompt=None,\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=[\n                    TextContent(type=\"text\", text=\"What is in this image?\"),\n                    ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/jpeg\"),\n                ],\n            )\n        ],\n    )\n\n    assert len(msgs) == 1\n    assert msgs[0] == ChatCompletionUserMessageParam(\n        role=\"user\",\n        content=[\n            ChatCompletionContentPartTextParam(\n                type=\"text\", text=\"What is in this image?\"\n            ),\n            ChatCompletionContentPartImageParam(\n                type=\"image_url\",\n                image_url={\"url\": \"data:image/jpeg;base64,YWJj\"},\n            ),\n        ],\n    )\n\n\ndef test_convert_image_in_assistant_message_raises():\n    with pytest.raises(ValueError, match=\"ImageContent is only supported in user\"):\n        OpenAISamplingHandler._convert_to_openai_messages(\n            system_prompt=None,\n            messages=[\n                SamplingMessage(\n                    role=\"assistant\",\n                    content=ImageContent(\n                        type=\"image\", data=\"YWJj\", mimeType=\"image/png\"\n                    ),\n                )\n            ],\n        )\n\n\ndef test_convert_audio_in_assistant_message_raises():\n    with pytest.raises(ValueError, match=\"AudioContent is only supported in user\"):\n        OpenAISamplingHandler._convert_to_openai_messages(\n            system_prompt=None,\n            messages=[\n                SamplingMessage(\n                    role=\"assistant\",\n                    content=AudioContent(\n                        type=\"audio\", data=\"YWJj\", mimeType=\"audio/wav\"\n                    ),\n                )\n            ],\n        )\n\n\ndef test_convert_list_image_in_assistant_message_raises():\n    \"\"\"Image/audio in an assistant list-content message should raise, not silently drop.\"\"\"\n    with pytest.raises(ValueError, match=\"only supported in user messages\"):\n        OpenAISamplingHandler._convert_to_openai_messages(\n            system_prompt=None,\n            messages=[\n                SamplingMessage(\n                    role=\"assistant\",\n                    content=[\n                        TextContent(type=\"text\", text=\"Here's the image\"),\n                        ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\"),\n                    ],\n                )\n            ],\n        )\n\n\ndef test_convert_list_tool_calls_with_image_raises():\n    \"\"\"Image/audio alongside tool_calls in assistant list should raise.\"\"\"\n    with pytest.raises(ValueError, match=\"only supported in user messages\"):\n        OpenAISamplingHandler._convert_to_openai_messages(\n            system_prompt=None,\n            messages=[\n                SamplingMessage(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"my_tool\",\n                            input={\"arg\": \"val\"},\n                        ),\n                        ImageContent(type=\"image\", data=\"YWJj\", mimeType=\"image/png\"),\n                    ],\n                )\n            ],\n        )\n\n\n@pytest.mark.parametrize(\n    \"prefs,expected\",\n    [\n        (\"gpt-4o-mini\", \"gpt-4o-mini\"),\n        (ModelPreferences(hints=[ModelHint(name=\"gpt-4o-mini\")]), \"gpt-4o-mini\"),\n        ([\"gpt-4o-mini\", \"other\"], \"gpt-4o-mini\"),\n        (None, \"fallback-model\"),\n        ([\"unknown-model\"], \"fallback-model\"),\n    ],\n)\ndef test_select_model_from_preferences(prefs: Any, expected: str) -> None:\n    mock_client = MagicMock(spec=AsyncOpenAI)\n    handler = OpenAISamplingHandler(default_model=\"fallback-model\", client=mock_client)  # type: ignore[arg-type]\n    assert handler._select_model_from_preferences(prefs) == expected\n\n\nasync def test_handler_passes_max_completion_tokens():\n    \"\"\"Verify the handler uses max_completion_tokens (not max_tokens).\"\"\"\n    mock_client = MagicMock(spec=AsyncOpenAI)\n    mock_client.chat = MagicMock()\n    mock_client.chat.completions = MagicMock()\n    mock_client.chat.completions.create = AsyncMock(\n        return_value=ChatCompletion(\n            id=\"123\",\n            created=123,\n            model=\"gpt-4o-mini\",\n            object=\"chat.completion\",\n            choices=[\n                Choice(\n                    message=ChatCompletionMessage(content=\"hi\", role=\"assistant\"),\n                    finish_reason=\"stop\",\n                    index=0,\n                )\n            ],\n        )\n    )\n    handler = OpenAISamplingHandler(default_model=\"gpt-4o-mini\", client=mock_client)\n    messages = [\n        SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"hello\"))\n    ]\n    params = CreateMessageRequestParams(messages=messages, maxTokens=300)\n    await handler(messages, params, context=None)  # type: ignore[arg-type]\n\n    call_kwargs = mock_client.chat.completions.create.call_args\n    assert \"max_completion_tokens\" in call_kwargs.kwargs\n    assert call_kwargs.kwargs[\"max_completion_tokens\"] == 300\n    assert \"max_tokens\" not in call_kwargs.kwargs\n\n\nasync def test_chat_completion_to_create_message_result():\n    mock_client = MagicMock(spec=AsyncOpenAI)\n    handler = OpenAISamplingHandler(default_model=\"fallback-model\", client=mock_client)  # type: ignore[arg-type]\n    mock_client.chat.completions.create.return_value = ChatCompletion(\n        id=\"123\",\n        created=123,\n        model=\"gpt-4o-mini\",\n        object=\"chat.completion\",\n        choices=[\n            Choice(\n                message=ChatCompletionMessage(\n                    content=\"HELPFUL CONTENT FROM A VERY SMART LLM\", role=\"assistant\"\n                ),\n                finish_reason=\"stop\",\n                index=0,\n            )\n        ],\n    )\n    result: CreateMessageResult = handler._chat_completion_to_create_message_result(\n        chat_completion=mock_client.chat.completions.create.return_value\n    )\n    assert result == CreateMessageResult(\n        content=TextContent(type=\"text\", text=\"HELPFUL CONTENT FROM A VERY SMART LLM\"),\n        role=\"assistant\",\n        model=\"gpt-4o-mini\",\n    )\n"
  },
  {
    "path": "tests/client/tasks/conftest.py",
    "content": "\"\"\"Configuration for client task tests.\"\"\"\n"
  },
  {
    "path": "tests/client/tasks/test_client_prompt_tasks.py",
    "content": "\"\"\"\nTests for client-side prompt task methods.\n\nTests the client's get_prompt_as_task method.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.tasks import PromptTask\n\n\n@pytest.fixture\nasync def prompt_server():\n    \"\"\"Create a test server with background-enabled prompts.\"\"\"\n    mcp = FastMCP(\"prompt-client-test\")\n\n    @mcp.prompt(task=True)\n    async def analysis_prompt(topic: str, style: str = \"formal\") -> str:\n        \"\"\"Generate an analysis prompt.\"\"\"\n        return f\"Analyze {topic} in a {style} style\"\n\n    @mcp.prompt(task=True)\n    async def creative_prompt(theme: str) -> str:\n        \"\"\"Generate a creative writing prompt.\"\"\"\n        return f\"Write a story about {theme}\"\n\n    return mcp\n\n\nasync def test_get_prompt_as_task_returns_prompt_task(prompt_server):\n    \"\"\"get_prompt with task=True returns a PromptTask object.\"\"\"\n    async with Client(prompt_server) as client:\n        task = await client.get_prompt(\"analysis_prompt\", {\"topic\": \"AI\"}, task=True)\n\n        assert isinstance(task, PromptTask)\n        assert isinstance(task.task_id, str)\n\n\nasync def test_prompt_task_server_generated_id(prompt_server):\n    \"\"\"get_prompt with task=True gets server-generated task ID.\"\"\"\n    async with Client(prompt_server) as client:\n        task = await client.get_prompt(\n            \"creative_prompt\",\n            {\"theme\": \"future\"},\n            task=True,\n        )\n\n        # Server should generate a UUID task ID\n        assert task.task_id is not None\n        assert isinstance(task.task_id, str)\n        # UUIDs have hyphens\n        assert \"-\" in task.task_id\n\n\nasync def test_prompt_task_result_returns_get_prompt_result(prompt_server):\n    \"\"\"PromptTask.result() returns GetPromptResult.\"\"\"\n    async with Client(prompt_server) as client:\n        task = await client.get_prompt(\n            \"analysis_prompt\", {\"topic\": \"Robotics\", \"style\": \"casual\"}, task=True\n        )\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        # Get result\n        result = await task.result()\n\n        # Result should be GetPromptResult\n        assert hasattr(result, \"description\")\n        assert hasattr(result, \"messages\")\n        # Check the rendered message content, not the description\n        assert len(result.messages) > 0\n        assert \"Analyze Robotics\" in result.messages[0].content.text\n\n\nasync def test_prompt_task_await_syntax(prompt_server):\n    \"\"\"PromptTask can be awaited directly.\"\"\"\n    async with Client(prompt_server) as client:\n        task = await client.get_prompt(\"creative_prompt\", {\"theme\": \"ocean\"}, task=True)\n\n        # Can await task directly\n        result = await task\n        assert \"Write a story about ocean\" in result.messages[0].content.text\n\n\nasync def test_prompt_task_status_and_wait(prompt_server):\n    \"\"\"PromptTask supports status() and wait() methods.\"\"\"\n    async with Client(prompt_server) as client:\n        task = await client.get_prompt(\"analysis_prompt\", {\"topic\": \"Space\"}, task=True)\n\n        # Check status\n        status = await task.status()\n        assert status.status in [\"working\", \"completed\"]\n\n        # Wait for completion\n        await task.wait(timeout=2.0)\n\n        # Get result\n        result = await task.result()\n        assert \"Analyze Space\" in result.messages[0].content.text\n"
  },
  {
    "path": "tests/client/tasks/test_client_resource_tasks.py",
    "content": "\"\"\"\nTests for client-side resource task methods.\n\nTests the client's read_resource_as_task method.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.tasks import ResourceTask\n\n\n@pytest.fixture\nasync def resource_server():\n    \"\"\"Create a test server with background-enabled resources.\"\"\"\n    mcp = FastMCP(\"resource-client-test\")\n\n    @mcp.resource(\"file://document.txt\", task=True)\n    async def document() -> str:\n        \"\"\"A document resource.\"\"\"\n        return \"Document content here\"\n\n    @mcp.resource(\"file://data/{id}.json\", task=True)\n    async def data_file(id: str) -> str:\n        \"\"\"A parameterized data resource.\"\"\"\n        return f'{{\"id\": \"{id}\", \"value\": 42}}'\n\n    return mcp\n\n\nasync def test_read_resource_as_task_returns_resource_task(resource_server):\n    \"\"\"read_resource with task=True returns a ResourceTask object.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://document.txt\", task=True)\n\n        assert isinstance(task, ResourceTask)\n        assert isinstance(task.task_id, str)\n\n\nasync def test_resource_task_server_generated_id(resource_server):\n    \"\"\"read_resource with task=True gets server-generated task ID.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://document.txt\", task=True)\n\n        # Server should generate a UUID task ID\n        assert task.task_id is not None\n        assert isinstance(task.task_id, str)\n        # UUIDs have hyphens\n        assert \"-\" in task.task_id\n\n\nasync def test_resource_task_result_returns_read_resource_result(resource_server):\n    \"\"\"ResourceTask.result() returns list of ReadResourceContents.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://document.txt\", task=True)\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        # Get result\n        result = await task.result()\n\n        # Result should be list of ReadResourceContents\n        assert isinstance(result, list)\n        assert len(result) > 0\n        assert result[0].text == \"Document content here\"\n\n\nasync def test_resource_task_await_syntax(resource_server):\n    \"\"\"ResourceTask can be awaited directly.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://document.txt\", task=True)\n\n        # Can await task directly\n        result = await task\n        assert result[0].text == \"Document content here\"\n\n\nasync def test_resource_template_task(resource_server):\n    \"\"\"Resource templates work with task support.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://data/999.json\", task=True)\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        # Get result\n        result = await task.result()\n        assert '\"id\": \"999\"' in result[0].text\n\n\nasync def test_resource_task_status_and_wait(resource_server):\n    \"\"\"ResourceTask supports status() and wait() methods.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://document.txt\", task=True)\n\n        # Check status\n        status = await task.status()\n        assert status.status in [\"working\", \"completed\"]\n\n        # Wait for completion\n        await task.wait(timeout=2.0)\n\n        # Get result\n        result = await task.result()\n        assert \"Document content\" in result[0].text\n"
  },
  {
    "path": "tests/client/tasks/test_client_task_notifications.py",
    "content": "\"\"\"\nTests for client-side handling of notifications/tasks/status (SEP-1686 lines 436-444).\n\nVerifies that Task objects receive notifications, update their cache, wake up wait() calls,\nand invoke user callbacks.\n\"\"\"\n\nimport asyncio\nimport time\n\nimport pytest\nfrom mcp.types import GetTaskResult\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def task_notification_server():\n    \"\"\"Server that sends task status notifications.\"\"\"\n    mcp = FastMCP(\"task-notification-test\")\n\n    @mcp.tool(task=True)\n    async def quick_task(value: int) -> int:\n        \"\"\"Quick background task.\"\"\"\n        await asyncio.sleep(0.05)\n        return value * 2\n\n    @mcp.tool(task=True)\n    async def slow_task(duration: float = 0.2) -> str:\n        \"\"\"Slow background task.\"\"\"\n        await asyncio.sleep(duration)\n        return \"done\"\n\n    @mcp.tool(task=True)\n    async def failing_task() -> str:\n        \"\"\"Task that fails.\"\"\"\n        raise ValueError(\"Intentional failure\")\n\n    return mcp\n\n\nasync def test_task_receives_status_notification(task_notification_server):\n    \"\"\"Task object receives and processes status notifications.\"\"\"\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 5}, task=True)\n\n        # Wait for task to complete (notification should arrive)\n        status = await task.wait(timeout=2.0)\n\n        # Verify task completed\n        assert status.status == \"completed\"\n\n\nasync def test_status_cache_updated_by_notification(task_notification_server):\n    \"\"\"Cached status is updated when notification arrives.\"\"\"\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 10}, task=True)\n\n        # Wait for completion (notification should update cache)\n        await task.wait(timeout=2.0)\n\n        # Status should be cached (no server call needed)\n        # Call status() twice - should return same cached object\n        status1 = await task.status()\n        status2 = await task.status()\n\n        # Should be the exact same object (from cache)\n        assert status1 is status2\n        assert status1.status == \"completed\"\n\n\nasync def test_callback_invoked_on_notification(task_notification_server):\n    \"\"\"User callback is invoked when notification arrives.\"\"\"\n    callback_invocations = []\n\n    def status_callback(status: GetTaskResult):\n        \"\"\"Sync callback.\"\"\"\n        callback_invocations.append(status)\n\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 7}, task=True)\n\n        # Register callback\n        task.on_status_change(status_callback)\n\n        # Wait for completion\n        await task.wait(timeout=2.0)\n\n        # Give callbacks a moment to fire\n        await asyncio.sleep(0.1)\n\n    # Callback should have been invoked at least once\n    assert len(callback_invocations) > 0\n\n    # Should have received completed status\n    completed_statuses = [s for s in callback_invocations if s.status == \"completed\"]\n    assert len(completed_statuses) > 0\n\n\nasync def test_async_callback_invoked(task_notification_server):\n    \"\"\"Async callback is invoked when notification arrives.\"\"\"\n    callback_invocations = []\n\n    async def async_status_callback(status: GetTaskResult):\n        \"\"\"Async callback.\"\"\"\n        await asyncio.sleep(0.01)  # Simulate async work\n        callback_invocations.append(status)\n\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 3}, task=True)\n\n        # Register async callback\n        task.on_status_change(async_status_callback)\n\n        # Wait for completion\n        await task.wait(timeout=2.0)\n\n        # Give async callbacks time to complete\n        await asyncio.sleep(0.2)\n\n    # Async callback should have been invoked\n    assert len(callback_invocations) > 0\n\n\nasync def test_multiple_callbacks_all_invoked(task_notification_server):\n    \"\"\"Multiple callbacks are all invoked.\"\"\"\n    callback1_calls = []\n    callback2_calls = []\n\n    def callback1(status: GetTaskResult):\n        callback1_calls.append(status.status)\n\n    def callback2(status: GetTaskResult):\n        callback2_calls.append(status.status)\n\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 8}, task=True)\n\n        task.on_status_change(callback1)\n        task.on_status_change(callback2)\n\n        await task.wait(timeout=2.0)\n        await asyncio.sleep(0.1)\n\n    # Both callbacks should have been invoked\n    assert len(callback1_calls) > 0\n    assert len(callback2_calls) > 0\n\n\nasync def test_callback_error_doesnt_break_notification(task_notification_server):\n    \"\"\"Callback errors don't prevent other callbacks from running.\"\"\"\n    callback1_calls = []\n    callback2_calls = []\n\n    def failing_callback(status: GetTaskResult):\n        callback1_calls.append(\"called\")\n        raise ValueError(\"Callback intentionally fails\")\n\n    def working_callback(status: GetTaskResult):\n        callback2_calls.append(status.status)\n\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 12}, task=True)\n\n        task.on_status_change(failing_callback)\n        task.on_status_change(working_callback)\n\n        await task.wait(timeout=2.0)\n        await asyncio.sleep(0.1)\n\n    # Failing callback was called (and errored)\n    assert len(callback1_calls) > 0\n\n    # Working callback should still have been invoked\n    assert len(callback2_calls) > 0\n\n\nasync def test_wait_wakes_early_on_notification(task_notification_server):\n    \"\"\"wait() wakes up immediately when notification arrives, not after poll interval.\"\"\"\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"quick_task\", {\"value\": 15}, task=True)\n\n        # Record timing\n        start = time.time()\n        status = await task.wait(timeout=5.0)\n        elapsed = time.time() - start\n\n        # Should complete much faster than the fallback poll interval (500ms)\n        # With notifications, should be < 200ms for quick task\n        # Without notifications, would take 500ms+ due to polling\n        assert elapsed < 1.0  # Very generous bound\n        assert status.status == \"completed\"\n\n\nasync def test_notification_with_failed_task(task_notification_server):\n    \"\"\"Notifications work for failed tasks too.\"\"\"\n    async with Client(task_notification_server) as client:\n        task = await client.call_tool(\"failing_task\", {}, task=True)\n\n        with pytest.raises(Exception):\n            await task\n\n        # Should have cached the failed status from notification\n        status = await task.status()\n        assert status.status == \"failed\"\n        assert (\n            status.statusMessage is not None\n        )  # Error details in statusMessage per spec\n"
  },
  {
    "path": "tests/client/tasks/test_client_task_protocol.py",
    "content": "\"\"\"\nTests for client-side task protocol.\n\nGeneric protocol tests that use tools as test fixtures.\n\"\"\"\n\nimport asyncio\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\nasync def test_end_to_end_task_flow():\n    \"\"\"Complete end-to-end flow: submit, poll, retrieve.\"\"\"\n    start_signal = asyncio.Event()\n    complete_signal = asyncio.Event()\n\n    mcp = FastMCP(\"protocol-test\")\n\n    @mcp.tool(task=True)\n    async def controlled_tool(message: str) -> str:\n        \"\"\"Tool with controlled execution.\"\"\"\n        start_signal.set()\n        await complete_signal.wait()\n        return f\"Processed: {message}\"\n\n    async with Client(mcp) as client:\n        # Submit task\n        task = await client.call_tool(\n            \"controlled_tool\", {\"message\": \"integration test\"}, task=True\n        )\n\n        # Wait for execution to start\n        await asyncio.wait_for(start_signal.wait(), timeout=2.0)\n\n        # Check status while running\n        status = await task.status()\n        assert status.status in [\"working\"]\n\n        # Signal completion\n        complete_signal.set()\n\n        # Wait for task to finish and retrieve result\n        result = await task.result()\n        assert result.data == \"Processed: integration test\"\n\n\nasync def test_multiple_concurrent_tasks():\n    \"\"\"Multiple tasks can run concurrently.\"\"\"\n    mcp = FastMCP(\"concurrent-test\")\n\n    @mcp.tool(task=True)\n    async def multiply(a: int, b: int) -> int:\n        return a * b\n\n    async with Client(mcp) as client:\n        # Submit multiple tasks\n        tasks = []\n        for i in range(5):\n            task = await client.call_tool(\"multiply\", {\"a\": i, \"b\": 2}, task=True)\n            tasks.append((task, i * 2))\n\n        # Wait for all to complete and verify results\n        for task, expected in tasks:\n            result = await task.result()\n            assert result.data == expected\n\n\nasync def test_task_id_auto_generation():\n    \"\"\"Task IDs are auto-generated if not provided.\"\"\"\n    mcp = FastMCP(\"id-test\")\n\n    @mcp.tool(task=True)\n    async def echo(message: str) -> str:\n        return f\"Echo: {message}\"\n\n    async with Client(mcp) as client:\n        # Submit without custom task ID\n        task_1 = await client.call_tool(\"echo\", {\"message\": \"first\"}, task=True)\n        task_2 = await client.call_tool(\"echo\", {\"message\": \"second\"}, task=True)\n\n        # Should generate different IDs\n        assert task_1.task_id != task_2.task_id\n        assert len(task_1.task_id) > 0\n        assert len(task_2.task_id) > 0\n"
  },
  {
    "path": "tests/client/tasks/test_client_tool_tasks.py",
    "content": "\"\"\"\nTests for client-side tool task methods.\n\nTests the client's tool-specific task functionality, parallel to\ntest_client_prompt_tasks.py and test_client_resource_tasks.py.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.tasks import ToolTask\n\n\n@pytest.fixture\nasync def tool_task_server():\n    \"\"\"Create a test server with task-enabled tools.\"\"\"\n    mcp = FastMCP(\"tool-task-test\")\n\n    @mcp.tool(task=True)\n    async def echo(message: str) -> str:\n        \"\"\"Echo back the message.\"\"\"\n        return f\"Echo: {message}\"\n\n    @mcp.tool(task=True)\n    async def multiply(a: int, b: int) -> int:\n        \"\"\"Multiply two numbers.\"\"\"\n        return a * b\n\n    return mcp\n\n\nasync def test_call_tool_as_task_returns_tool_task(tool_task_server):\n    \"\"\"call_tool with task=True returns a ToolTask object.\"\"\"\n    async with Client(tool_task_server) as client:\n        task = await client.call_tool(\"echo\", {\"message\": \"hello\"}, task=True)\n\n        assert isinstance(task, ToolTask)\n        assert isinstance(task.task_id, str)\n        assert len(task.task_id) > 0\n\n\nasync def test_tool_task_server_generated_id(tool_task_server):\n    \"\"\"call_tool with task=True gets server-generated task ID.\"\"\"\n    async with Client(tool_task_server) as client:\n        task = await client.call_tool(\"echo\", {\"message\": \"test\"}, task=True)\n\n        # Server should generate a UUID task ID\n        assert task.task_id is not None\n        assert isinstance(task.task_id, str)\n        # UUIDs have hyphens\n        assert \"-\" in task.task_id\n\n\nasync def test_tool_task_result_returns_call_tool_result(tool_task_server):\n    \"\"\"ToolTask.result() returns CallToolResult with tool data.\"\"\"\n    async with Client(tool_task_server) as client:\n        task = await client.call_tool(\"multiply\", {\"a\": 6, \"b\": 7}, task=True)\n        assert not task.returned_immediately\n\n        result = await task.result()\n        assert result.data == 42\n\n\nasync def test_tool_task_await_syntax(tool_task_server):\n    \"\"\"Tool tasks can be awaited directly to get result.\"\"\"\n    async with Client(tool_task_server) as client:\n        task = await client.call_tool(\"multiply\", {\"a\": 7, \"b\": 6}, task=True)\n\n        # Can await task directly (syntactic sugar for task.result())\n        result = await task\n        assert result.data == 42\n\n\nasync def test_tool_task_status_and_wait(tool_task_server):\n    \"\"\"ToolTask.status() returns GetTaskResult.\"\"\"\n    async with Client(tool_task_server) as client:\n        task = await client.call_tool(\"echo\", {\"message\": \"test\"}, task=True)\n\n        status = await task.status()\n        assert status.taskId == task.task_id\n        assert status.status in [\"working\", \"completed\"]\n\n        # Wait for completion\n        await task.wait(timeout=2.0)\n        final_status = await task.status()\n        assert final_status.status == \"completed\"\n"
  },
  {
    "path": "tests/client/tasks/test_task_context_validation.py",
    "content": "\"\"\"\nTests for Task client context validation.\n\nVerifies that Task methods properly validate client context and that\ncached results remain accessible outside context.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def task_server():\n    \"\"\"Create a test server with background tasks.\"\"\"\n    mcp = FastMCP(\"context-test-server\")\n\n    @mcp.tool(task=True)\n    async def background_tool(value: str) -> str:\n        \"\"\"Tool that runs in background.\"\"\"\n        return f\"Result: {value}\"\n\n    @mcp.prompt(task=True)\n    async def background_prompt(topic: str) -> str:\n        \"\"\"Prompt that runs in background.\"\"\"\n        return f\"Prompt about {topic}\"\n\n    @mcp.resource(\"file://background.txt\", task=True)\n    async def background_resource() -> str:\n        \"\"\"Resource that runs in background.\"\"\"\n        return \"Background resource content\"\n\n    return mcp\n\n\nasync def test_task_status_outside_context_raises(task_server):\n    \"\"\"Calling task.status() outside client context raises error.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n    # Now outside context\n\n    with pytest.raises(RuntimeError, match=\"outside client context\"):\n        await task.status()\n\n\nasync def test_task_result_outside_context_raises(task_server):\n    \"\"\"Calling task.result() outside context raises error.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n    # Now outside context\n\n    with pytest.raises(RuntimeError, match=\"outside client context\"):\n        await task.result()\n\n\nasync def test_task_wait_outside_context_raises(task_server):\n    \"\"\"Calling task.wait() outside context raises error.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n    # Now outside context\n\n    with pytest.raises(RuntimeError, match=\"outside client context\"):\n        await task.wait()\n\n\nasync def test_task_cancel_outside_context_raises(task_server):\n    \"\"\"Calling task.cancel() outside context raises error.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n    # Now outside context\n\n    with pytest.raises(RuntimeError, match=\"outside client context\"):\n        await task.cancel()\n\n\nasync def test_cached_tool_task_accessible_outside_context(task_server):\n    \"\"\"Tool tasks with cached results work outside context.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n\n        # Get result once to cache it\n        result1 = await task.result()\n        assert result1.data == \"Result: test\"\n    # Now outside context\n\n    # Should work because result is cached\n    result2 = await task.result()\n    assert result2 is result1  # Same object\n    assert result2.data == \"Result: test\"\n\n\nasync def test_cached_prompt_task_accessible_outside_context(task_server):\n    \"\"\"Prompt tasks with cached results work outside context.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.get_prompt(\n            \"background_prompt\", {\"topic\": \"test\"}, task=True\n        )\n        assert not task.returned_immediately\n\n        # Get result once to cache it\n        result1 = await task.result()\n        assert result1.description == \"Prompt that runs in background.\"\n    # Now outside context\n\n    # Should work because result is cached\n    result2 = await task.result()\n    assert result2 is result1  # Same object\n    assert result2.description == \"Prompt that runs in background.\"\n\n\nasync def test_cached_resource_task_accessible_outside_context(task_server):\n    \"\"\"Resource tasks with cached results work outside context.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.read_resource(\"file://background.txt\", task=True)\n        assert not task.returned_immediately\n\n        # Get result once to cache it\n        result1 = await task.result()\n        assert len(result1) > 0\n    # Now outside context\n\n    # Should work because result is cached\n    result2 = await task.result()\n    assert result2 is result1  # Same object\n\n\nasync def test_uncached_status_outside_context_raises(task_server):\n    \"\"\"Even after caching result, status() still requires client context.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n\n        # Cache the result\n        await task.result()\n    # Now outside context\n\n    # result() works (cached)\n    result = await task.result()\n    assert result.data == \"Result: test\"\n\n    # But status() still needs client connection\n    with pytest.raises(RuntimeError, match=\"outside client context\"):\n        await task.status()\n\n\nasync def test_task_await_syntax_outside_context_raises(task_server):\n    \"\"\"Using await task syntax outside context raises error for background tasks.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        assert not task.returned_immediately\n    # Now outside context\n\n    with pytest.raises(RuntimeError, match=\"outside client context\"):\n        await task  # Same as await task.result()\n\n\nasync def test_task_await_syntax_works_for_cached_results(task_server):\n    \"\"\"Using await task syntax works outside context when result is cached.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        result1 = await task  # Cache it\n    # Now outside context\n\n    result2 = await task  # Should work (cached)\n    assert result2 is result1\n    assert result2.data == \"Result: test\"\n\n\nasync def test_multiple_result_calls_return_same_cached_object(task_server):\n    \"\"\"Multiple result() calls return the same cached object.\"\"\"\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n\n        result1 = await task.result()\n        result2 = await task.result()\n        result3 = await task.result()\n\n        # Should all be the same object (cached)\n        assert result1 is result2\n        assert result2 is result3\n\n\nasync def test_background_task_properties_accessible_outside_context(task_server):\n    \"\"\"Background task properties like task_id accessible outside context.\"\"\"\n    task = None\n    async with Client(task_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"value\": \"test\"}, task=True)\n        task_id_inside = task.task_id\n        assert not task.returned_immediately\n    # Now outside context\n\n    # Properties should still be accessible (they don't need client connection)\n    assert task.task_id == task_id_inside\n    assert task.returned_immediately is False\n"
  },
  {
    "path": "tests/client/tasks/test_task_result_caching.py",
    "content": "\"\"\"\nTests for Task result caching behavior.\n\nVerifies that Task.result() and await task cache results properly to avoid\nredundant server calls and ensure consistent object identity.\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\nasync def test_tool_task_result_cached_on_first_call():\n    \"\"\"First call caches result, subsequent calls return cached value.\"\"\"\n    call_count = 0\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def counting_tool() -> int:\n        nonlocal call_count\n        call_count += 1\n        return call_count\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"counting_tool\", task=True)\n\n        result1 = await task.result()\n        result2 = await task.result()\n        result3 = await task.result()\n\n        # All should return 1 (first execution value)\n        assert result1.data == 1\n        assert result2.data == 1\n        assert result3.data == 1\n\n        # Verify they're the same object (cached)\n        assert result1 is result2 is result3\n\n\nasync def test_prompt_task_result_cached():\n    \"\"\"PromptTask caches results on first call.\"\"\"\n    call_count = 0\n    mcp = FastMCP(\"test\")\n\n    @mcp.prompt(task=True)\n    async def counting_prompt() -> str:\n        nonlocal call_count\n        call_count += 1\n        return f\"Call number: {call_count}\"\n\n    async with Client(mcp) as client:\n        task = await client.get_prompt(\"counting_prompt\", task=True)\n\n        result1 = await task.result()\n        result2 = await task.result()\n        result3 = await task.result()\n\n        # All should return same content\n        assert result1.messages[0].content.text == \"Call number: 1\"\n        assert result2.messages[0].content.text == \"Call number: 1\"\n        assert result3.messages[0].content.text == \"Call number: 1\"\n\n        # Verify they're the same object (cached)\n        assert result1 is result2 is result3\n\n\nasync def test_resource_task_result_cached():\n    \"\"\"ResourceTask caches results on first call.\"\"\"\n    call_count = 0\n    mcp = FastMCP(\"test\")\n\n    @mcp.resource(\"file://counter.txt\", task=True)\n    async def counting_resource() -> str:\n        nonlocal call_count\n        call_count += 1\n        return f\"Count: {call_count}\"\n\n    async with Client(mcp) as client:\n        task = await client.read_resource(\"file://counter.txt\", task=True)\n\n        result1 = await task.result()\n        result2 = await task.result()\n        result3 = await task.result()\n\n        # All should return same content\n        assert result1[0].text == \"Count: 1\"\n        assert result2[0].text == \"Count: 1\"\n        assert result3[0].text == \"Count: 1\"\n\n        # Verify they're the same object (cached)\n        assert result1 is result2 is result3\n\n\nasync def test_multiple_await_returns_same_object():\n    \"\"\"Multiple await task calls return identical object.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def sample_tool() -> str:\n        return \"result\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"sample_tool\", task=True)\n\n        result1 = await task\n        result2 = await task\n        result3 = await task\n\n        # Should be exact same object in memory\n        assert result1 is result2 is result3\n        assert id(result1) == id(result2) == id(result3)\n\n\nasync def test_result_and_await_share_cache():\n    \"\"\"task.result() and await task share the same cache.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def sample_tool() -> str:\n        return \"cached\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"sample_tool\", task=True)\n\n        # Call result() first\n        result_via_method = await task.result()\n\n        # Then await directly\n        result_via_await = await task\n\n        # Should be the same cached object\n        assert result_via_method is result_via_await\n        assert id(result_via_method) == id(result_via_await)\n\n\nasync def test_forbidden_mode_tool_caches_error_result():\n    \"\"\"Tools with task=False (mode=forbidden) cache error results.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=False)\n    async def non_task_tool() -> int:\n        return 1\n\n    async with Client(mcp) as client:\n        # Request as task, but mode=\"forbidden\" will reject with error\n        task = await client.call_tool(\"non_task_tool\", task=True)\n\n        # Should be immediate (error returned immediately)\n        assert task.returned_immediately\n\n        result1 = await task.result()\n        result2 = await task.result()\n        result3 = await task.result()\n\n        # All should return cached error\n        assert result1.is_error\n        assert \"does not support task-augmented execution\" in str(result1)\n\n        # Verify they're the same object (cached)\n        assert result1 is result2 is result3\n\n\nasync def test_forbidden_mode_prompt_raises_error():\n    \"\"\"Prompts with task=False (mode=forbidden) raise error.\"\"\"\n    import pytest\n    from mcp.shared.exceptions import McpError\n\n    mcp = FastMCP(\"test\")\n\n    @mcp.prompt(task=False)\n    async def non_task_prompt() -> str:\n        return \"Immediate\"\n\n    async with Client(mcp) as client:\n        # Prompts with mode=\"forbidden\" raise McpError when called with task=True\n        with pytest.raises(McpError):\n            await client.get_prompt(\"non_task_prompt\", task=True)\n\n\nasync def test_forbidden_mode_resource_raises_error():\n    \"\"\"Resources with task=False (mode=forbidden) raise error.\"\"\"\n    import pytest\n    from mcp.shared.exceptions import McpError\n\n    mcp = FastMCP(\"test\")\n\n    @mcp.resource(\"file://immediate.txt\", task=False)\n    async def non_task_resource() -> str:\n        return \"Immediate\"\n\n    async with Client(mcp) as client:\n        # Resources with mode=\"forbidden\" raise McpError when called with task=True\n        with pytest.raises(McpError):\n            await client.read_resource(\"file://immediate.txt\", task=True)\n\n\nasync def test_immediate_task_caches_result():\n    \"\"\"Immediate tasks (optional mode called without background) cache results.\"\"\"\n    call_count = 0\n    mcp = FastMCP(\"test\", tasks=True)\n\n    # Tool with task=True (optional mode) - but without docket will execute immediately\n    @mcp.tool(task=True)\n    async def task_tool() -> int:\n        nonlocal call_count\n        call_count += 1\n        return call_count\n\n    async with Client(mcp) as client:\n        # Call with task=True\n        task = await client.call_tool(\"task_tool\", task=True)\n\n        # Get result multiple times\n        result1 = await task.result()\n        result2 = await task.result()\n        result3 = await task.result()\n\n        # All should return cached value\n        assert result1.data == 1\n        assert result2.data == 1\n        assert result3.data == 1\n\n        # Verify they're the same object (cached)\n        assert result1 is result2 is result3\n\n\nasync def test_cache_persists_across_mixed_access_patterns():\n    \"\"\"Cache works correctly when mixing result() and await.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def mixed_tool() -> str:\n        return \"mixed\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"mixed_tool\", task=True)\n\n        # Access in various orders\n        result1 = await task\n        result2 = await task.result()\n        result3 = await task\n        result4 = await task.result()\n\n        # All should be the same cached object\n        assert result1 is result2 is result3 is result4\n\n\nasync def test_different_tasks_have_separate_caches():\n    \"\"\"Different task instances maintain separate caches.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def separate_tool(value: str) -> str:\n        return f\"Result: {value}\"\n\n    async with Client(mcp) as client:\n        task1 = await client.call_tool(\"separate_tool\", {\"value\": \"A\"}, task=True)\n        task2 = await client.call_tool(\"separate_tool\", {\"value\": \"B\"}, task=True)\n\n        result1 = await task1.result()\n        result2 = await task2.result()\n\n        # Different results\n        assert result1.data == \"Result: A\"\n        assert result2.data == \"Result: B\"\n\n        # Not the same object\n        assert result1 is not result2\n\n        # But each task's cache works independently\n        result1_again = await task1.result()\n        result2_again = await task2.result()\n\n        assert result1 is result1_again\n        assert result2 is result2_again\n\n\nasync def test_cache_survives_status_checks():\n    \"\"\"Calling status() doesn't affect result caching.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def status_check_tool() -> str:\n        return \"status\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"status_check_tool\", task=True)\n\n        # Check status multiple times\n        await task.status()\n        await task.status()\n\n        result1 = await task.result()\n\n        # Check status again\n        await task.status()\n\n        result2 = await task.result()\n\n        # Cache should still work\n        assert result1 is result2\n\n\nasync def test_cache_survives_wait_calls():\n    \"\"\"Calling wait() doesn't affect result caching.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def wait_test_tool() -> str:\n        return \"waited\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"wait_test_tool\", task=True)\n\n        # Wait for completion\n        await task.wait()\n\n        result1 = await task.result()\n\n        # Wait again (no-op since completed)\n        await task.wait()\n\n        result2 = await task.result()\n\n        # Cache should still work\n        assert result1 is result2\n"
  },
  {
    "path": "tests/client/telemetry/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/telemetry/test_client_tracing.py",
    "content": "\"\"\"Tests for client OpenTelemetry tracing.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncGenerator\n\nimport pytest\nfrom opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter\nfrom opentelemetry.trace import SpanKind, StatusCode\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.exceptions import ToolError\n\n\nclass TestClientToolTracing:\n    \"\"\"Tests for client tool call tracing.\"\"\"\n\n    async def test_call_tool_creates_span(self, trace_exporter: InMemorySpanExporter):\n        server = FastMCP(\"test-server\")\n\n        @server.tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        client = Client(server)\n        async with client:\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert \"Hello, World!\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        span_names = [s.name for s in spans]\n\n        # Client should create \"tools/call greet\" span\n        assert \"tools/call greet\" in span_names\n\n    async def test_call_tool_span_attributes(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        server = FastMCP(\"test-server\")\n\n        @server.tool()\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        client = Client(server)\n        async with client:\n            await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side span (doesn't have fastmcp.server.name)\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call add\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        assert client_span is not None\n        assert client_span.attributes is not None\n        # Standard MCP semantic conventions\n        assert client_span.attributes[\"mcp.method.name\"] == \"tools/call\"\n        # Standard RPC semantic conventions\n        assert client_span.attributes[\"rpc.system\"] == \"mcp\"\n        assert client_span.attributes[\"rpc.method\"] == \"tools/call\"\n        # FastMCP-specific attributes\n        assert client_span.attributes[\"fastmcp.component.key\"] == \"add\"\n\n\nclass TestClientResourceTracing:\n    \"\"\"Tests for client resource read tracing.\"\"\"\n\n    async def test_read_resource_creates_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        server = FastMCP(\"test-server\")\n\n        @server.resource(\"data://config\")\n        def get_config() -> str:\n            return \"config data\"\n\n        client = Client(server)\n        async with client:\n            result = await client.read_resource(\"data://config\")\n            assert \"config data\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        span_names = [s.name for s in spans]\n\n        # Client should create \"resources/read data://config\" span\n        assert \"resources/read data://config\" in span_names\n\n    async def test_read_resource_span_attributes(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        server = FastMCP(\"test-server\")\n\n        @server.resource(\"data://config\")\n        def get_config() -> str:\n            return \"config value\"\n\n        client = Client(server)\n        async with client:\n            await client.read_resource(\"data://config\")\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side resource span (doesn't have fastmcp.server.name)\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name.startswith(\"resources/read data://\")\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        assert client_span is not None\n        assert client_span.attributes is not None\n        # Standard MCP semantic conventions\n        assert client_span.attributes[\"mcp.method.name\"] == \"resources/read\"\n        assert \"data://\" in str(client_span.attributes[\"mcp.resource.uri\"])\n        # Standard RPC semantic conventions\n        assert client_span.attributes[\"rpc.system\"] == \"mcp\"\n        assert client_span.attributes[\"rpc.method\"] == \"resources/read\"\n        # FastMCP-specific attributes\n        # The URI may be normalized with trailing slash\n        assert \"data://\" in str(client_span.attributes[\"fastmcp.component.key\"])\n\n\nclass TestClientPromptTracing:\n    \"\"\"Tests for client prompt get tracing.\"\"\"\n\n    async def test_get_prompt_creates_span(self, trace_exporter: InMemorySpanExporter):\n        server = FastMCP(\"test-server\")\n\n        @server.prompt()\n        def greeting() -> str:\n            return \"Hello from prompt!\"\n\n        client = Client(server)\n        async with client:\n            result = await client.get_prompt(\"greeting\")\n            assert \"Hello from prompt!\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        span_names = [s.name for s in spans]\n\n        # Client should create \"prompts/get greeting\" span\n        assert \"prompts/get greeting\" in span_names\n\n    async def test_get_prompt_span_attributes(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        server = FastMCP(\"test-server\")\n\n        @server.prompt()\n        def welcome(name: str) -> str:\n            return f\"Welcome, {name}!\"\n\n        client = Client(server)\n        async with client:\n            await client.get_prompt(\"welcome\", {\"name\": \"Test\"})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side prompt span (doesn't have fastmcp.server.name)\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"prompts/get welcome\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        assert client_span is not None\n        assert client_span.attributes is not None\n        # Standard MCP semantic conventions\n        assert client_span.attributes[\"mcp.method.name\"] == \"prompts/get\"\n        # Standard RPC semantic conventions\n        assert client_span.attributes[\"rpc.system\"] == \"mcp\"\n        assert client_span.attributes[\"rpc.method\"] == \"prompts/get\"\n        # FastMCP-specific attributes\n        assert client_span.attributes[\"fastmcp.component.key\"] == \"welcome\"\n\n\nclass TestClientServerSpanHierarchy:\n    \"\"\"Tests for span relationships between client and server.\"\"\"\n\n    async def test_client_and_server_spans_created(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        \"\"\"Both client and server should create spans for the same operation.\"\"\"\n        server = FastMCP(\"test-server\")\n\n        @server.tool()\n        def echo(message: str) -> str:\n            return message\n\n        client = Client(server)\n        async with client:\n            await client.call_tool(\"echo\", {\"message\": \"test\"})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client span (no fastmcp.server.name) and server span (has fastmcp.server.name)\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call echo\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call echo\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        # Both spans should exist\n        assert client_span is not None, \"Client should create a span\"\n        assert client_span.attributes is not None\n        assert server_span is not None, \"Server should create a span\"\n        assert server_span.attributes is not None\n\n        # Verify span kinds are correct\n        assert client_span.kind == SpanKind.CLIENT, \"Client span should be CLIENT kind\"\n        assert server_span.kind == SpanKind.SERVER, \"Server span should be SERVER kind\"\n\n        # Verify the spans have different characteristics\n        assert client_span.attributes[\"rpc.method\"] == \"tools/call\"\n        assert server_span.attributes[\"fastmcp.server.name\"] == \"test-server\"\n\n    async def test_trace_context_propagation(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        \"\"\"Server span should be a child of client span via trace context propagation.\"\"\"\n        server = FastMCP(\"test-server\")\n\n        @server.tool()\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        client = Client(server)\n        async with client:\n            await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client span and server span\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call add\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call add\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        assert client_span is not None, \"Client span should exist\"\n        assert server_span is not None, \"Server span should exist\"\n\n        # Verify trace context propagation: server span should be child of client span\n        # Both should share the same trace_id\n        assert server_span.context.trace_id == client_span.context.trace_id, (\n            \"Server and client spans should share the same trace_id\"\n        )\n\n        # Server span's parent should be the client span\n        assert server_span.parent is not None, \"Server span should have a parent\"\n        assert server_span.parent.span_id == client_span.context.span_id, (\n            \"Server span's parent should be the client span\"\n        )\n\n\nclass TestClientErrorTracing:\n    \"\"\"Tests for client span creation during errors.\n\n    Note: MCP protocol errors are returned as successful responses with error content,\n    so client spans may not have ERROR status even when the operation fails. This is\n    different from server-side where exceptions happen inside the span.\n\n    The server-side span WILL have ERROR status because the exception occurs within\n    the server's span context. The client span represents the successful MCP protocol\n    round-trip, while application-level errors are communicated via the response.\n    \"\"\"\n\n    async def test_call_tool_error_creates_spans(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        \"\"\"Both client and server spans should be created when tool fails.\"\"\"\n        server = FastMCP(\"test-server\")\n\n        @server.tool()\n        def failing_tool() -> str:\n            raise ValueError(\"Something went wrong\")\n\n        client = Client(server)\n        async with client:\n            with pytest.raises(ToolError):\n                await client.call_tool(\"failing_tool\", {})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side span\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call failing_tool\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        # Find server-side span\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call failing_tool\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        # Both spans should exist\n        assert client_span is not None, \"Client should create a span\"\n        assert server_span is not None, \"Server should create a span\"\n\n        # Server span should have ERROR status (exception inside span)\n        assert server_span.status.status_code == StatusCode.ERROR\n\n    async def test_read_resource_error_creates_spans(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        \"\"\"Both client and server spans should be created when resource read fails.\"\"\"\n        server = FastMCP(\"test-server\")\n\n        @server.resource(\"data://fail\")\n        def failing_resource() -> str:\n            raise ValueError(\"Resource error\")\n\n        client = Client(server)\n        async with client:\n            with pytest.raises(Exception):\n                await client.read_resource(\"data://fail\")\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side span\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name.startswith(\"resources/read data://fail\")\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        # Find server-side span\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name.startswith(\"resources/read data://fail\")\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        # Both spans should exist\n        assert client_span is not None, \"Client should create a span\"\n        assert server_span is not None, \"Server should create a span\"\n\n        # Server span should have ERROR status\n        assert server_span.status.status_code == StatusCode.ERROR\n\n    async def test_get_prompt_error_creates_spans(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        \"\"\"Both client and server spans should be created when prompt get fails.\"\"\"\n        server = FastMCP(\"test-server\")\n\n        @server.prompt()\n        def failing_prompt() -> str:\n            raise ValueError(\"Prompt error\")\n\n        client = Client(server)\n        async with client:\n            with pytest.raises(Exception):\n                await client.get_prompt(\"failing_prompt\", {})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side span\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"prompts/get failing_prompt\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        # Find server-side span\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"prompts/get failing_prompt\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        # Both spans should exist\n        assert client_span is not None, \"Client should create a span\"\n        assert server_span is not None, \"Server should create a span\"\n\n        # Server span should have ERROR status\n        assert server_span.status.status_code == StatusCode.ERROR\n\n    async def test_call_nonexistent_tool_creates_spans(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        \"\"\"Both client and server spans should be created for nonexistent tool.\"\"\"\n        server = FastMCP(\"test-server\")\n\n        client = Client(server)\n        async with client:\n            with pytest.raises(Exception):\n                await client.call_tool(\"nonexistent\", {})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side span\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call nonexistent\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        # Find server-side span\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call nonexistent\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        # Both spans should exist\n        assert client_span is not None, \"Client should create a span\"\n        assert server_span is not None, \"Server should create a span\"\n\n        # Server span should have ERROR status\n        assert server_span.status.status_code == StatusCode.ERROR\n\n\nclass TestSessionIdOnSpans:\n    \"\"\"Tests for session ID capture on client and server spans.\n\n    Session IDs are only available with HTTP transport (StreamableHttp).\n    \"\"\"\n\n    @pytest.fixture\n    async def http_server_url(self) -> AsyncGenerator[str, None]:\n        \"\"\"Start an HTTP server and return its URL.\"\"\"\n        from fastmcp.utilities.tests import run_server_async\n\n        server = FastMCP(\"session-test-server\")\n\n        @server.tool()\n        def echo(message: str) -> str:\n            return message\n\n        async with run_server_async(server) as url:\n            yield url\n\n    async def test_client_span_includes_session_id(\n        self,\n        trace_exporter: InMemorySpanExporter,\n        http_server_url: str,\n    ):\n        \"\"\"Client span should include session ID when using HTTP transport.\"\"\"\n        from fastmcp.client.transports import StreamableHttpTransport\n\n        transport = StreamableHttpTransport(http_server_url)\n        client = Client(transport=transport)\n        async with client:\n            await client.call_tool(\"echo\", {\"message\": \"test\"})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find client-side span\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call echo\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n\n        assert client_span is not None, \"Client should create a span\"\n        assert client_span.attributes is not None\n        assert \"mcp.session.id\" in client_span.attributes\n        assert client_span.attributes[\"mcp.session.id\"] is not None\n\n    async def test_server_span_includes_session_id(\n        self,\n        trace_exporter: InMemorySpanExporter,\n        http_server_url: str,\n    ):\n        \"\"\"Server span should include session ID when called via HTTP.\"\"\"\n        from fastmcp.client.transports import StreamableHttpTransport\n\n        transport = StreamableHttpTransport(http_server_url)\n        client = Client(transport=transport)\n        async with client:\n            await client.call_tool(\"echo\", {\"message\": \"test\"})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find server-side span\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call echo\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        assert server_span is not None, \"Server should create a span\"\n        assert server_span.attributes is not None\n        assert \"mcp.session.id\" in server_span.attributes\n        assert server_span.attributes[\"mcp.session.id\"] is not None\n\n    async def test_client_and_server_share_same_session_id(\n        self,\n        trace_exporter: InMemorySpanExporter,\n        http_server_url: str,\n    ):\n        \"\"\"Client and server spans should have the same session ID.\"\"\"\n        from fastmcp.client.transports import StreamableHttpTransport\n\n        transport = StreamableHttpTransport(http_server_url)\n        client = Client(transport=transport)\n        async with client:\n            await client.call_tool(\"echo\", {\"message\": \"test\"})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find both spans\n        client_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call echo\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" not in s.attributes\n            ),\n            None,\n        )\n        server_span = next(\n            (\n                s\n                for s in spans\n                if s.name == \"tools/call echo\"\n                and s.attributes is not None\n                and \"fastmcp.server.name\" in s.attributes\n            ),\n            None,\n        )\n\n        assert client_span is not None\n        assert client_span.attributes is not None\n        assert server_span is not None\n        assert server_span.attributes is not None\n\n        # Both should have session IDs and they should match\n        client_session = client_span.attributes.get(\"mcp.session.id\")\n        server_session = server_span.attributes.get(\"mcp.session.id\")\n\n        assert client_session is not None, \"Client span should have session ID\"\n        assert server_session is not None, \"Server span should have session ID\"\n        assert client_session == server_session, (\n            \"Client and server should share the same session ID\"\n        )\n"
  },
  {
    "path": "tests/client/test_elicitation.py",
    "content": "from dataclasses import asdict, dataclass\nfrom enum import Enum\nfrom typing import Any, Literal, cast\n\nimport pytest\nfrom mcp.types import ElicitRequestFormParams, ElicitRequestParams\nfrom pydantic import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.client.client import Client\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.elicitation import (\n    AcceptedElicitation,\n    CancelledElicitation,\n    DeclinedElicitation,\n    validate_elicitation_json_schema,\n)\nfrom fastmcp.utilities.types import TypeAdapter\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP(\"TestServer\")\n\n    @dataclass\n    class Person:\n        name: str\n\n    @mcp.tool\n    async def ask_for_name(context: Context) -> str:\n        result = await context.elicit(\n            message=\"What is your name?\",\n            response_type=Person,\n        )\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, Person)\n            return f\"Hello, {result.data.name}!\"\n        else:\n            return \"No name provided.\"\n\n    @mcp.tool\n    def simple_test() -> str:\n        return \"Hello!\"\n\n    return mcp\n\n\nasync def test_elicitation_with_no_handler(fastmcp_server):\n    \"\"\"Test that elicitation works without a handler.\"\"\"\n\n    async with Client(fastmcp_server) as client:\n        with pytest.raises(ToolError, match=\"Elicitation not supported\"):\n            await client.call_tool(\"ask_for_name\")\n\n\nasync def test_elicitation_accept_content(fastmcp_server):\n    \"\"\"Test basic elicitation functionality.\"\"\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Mock user providing their name\n        return ElicitResult(action=\"accept\", content=response_type(name=\"Alice\"))\n\n    async with Client(\n        fastmcp_server, elicitation_handler=elicitation_handler\n    ) as client:\n        result = await client.call_tool(\"ask_for_name\")\n        assert result.data == \"Hello, Alice!\"\n\n\nasync def test_elicitation_decline(fastmcp_server):\n    \"\"\"Test that elicitation handler receives correct parameters.\"\"\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        return ElicitResult(action=\"decline\")\n\n    async with Client(\n        fastmcp_server, elicitation_handler=elicitation_handler\n    ) as client:\n        result = await client.call_tool(\"ask_for_name\")\n        assert result.data == \"No name provided.\"\n\n\nasync def test_elicitation_handler_parameters():\n    \"\"\"Test that elicitation handler receives correct parameters.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n    captured_params = {}\n\n    @mcp.tool\n    async def test_tool(context: Context) -> str:\n        await context.elicit(\n            message=\"Test message\",\n            response_type=int,\n        )\n        return \"done\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        captured_params[\"message\"] = message\n        captured_params[\"response_type\"] = str(response_type)\n        captured_params[\"params\"] = params\n        captured_params[\"ctx\"] = ctx\n        return ElicitResult(action=\"accept\", content={\"value\": 42})\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        await client.call_tool(\"test_tool\", {})\n\n        assert captured_params[\"message\"] == \"Test message\"\n        assert \"ScalarElicitationType\" in str(captured_params[\"response_type\"])\n        assert captured_params[\"params\"].requestedSchema == {\n            \"properties\": {\"value\": {\"title\": \"Value\", \"type\": \"integer\"}},\n            \"required\": [\"value\"],\n            \"title\": \"ScalarElicitationType\",\n            \"type\": \"object\",\n        }\n        assert captured_params[\"ctx\"] is not None\n\n\nasync def test_elicitation_cancel_action():\n    \"\"\"Test user canceling elicitation request.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def ask_for_optional_info(context: Context) -> str:\n        result = await context.elicit(\n            message=\"Optional: What's your age?\", response_type=int\n        )\n        if result.action == \"cancel\":\n            return \"Request was canceled\"\n        elif result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, int)\n            return f\"Age: {result.data}\"\n        else:\n            return \"No response provided\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        return ElicitResult(action=\"cancel\")\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"ask_for_optional_info\", {})\n        assert result.data == \"Request was canceled\"\n\n\nclass TestScalarResponseTypes:\n    async def test_elicitation_no_response(self):\n        \"\"\"Test elicitation with no response type.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> dict[str, Any]:\n            result = await context.elicit(message=\"\", response_type=None)\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, dict)\n            return cast(dict[str, Any], result.data)\n\n        async def elicitation_handler(\n            message, response_type, params: ElicitRequestParams, ctx\n        ):\n            assert isinstance(params, ElicitRequestFormParams)\n            assert params.requestedSchema == {\"type\": \"object\", \"properties\": {}}\n            assert response_type is None\n            return ElicitResult(action=\"accept\")\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data is None\n\n    async def test_elicitation_empty_response(self):\n        \"\"\"Test elicitation with empty response type.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> dict[str, Any]:\n            result = await context.elicit(message=\"\", response_type=None)\n            assert result.action == \"accept\"\n            assert isinstance(result, AcceptedElicitation)\n            accepted = cast(AcceptedElicitation[dict[str, Any]], result)\n            assert isinstance(accepted.data, dict)\n            return accepted.data\n\n        async def elicitation_handler(\n            message, response_type, params: ElicitRequestParams, ctx\n        ):\n            return ElicitResult(action=\"accept\", content={})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data is None\n\n    async def test_elicitation_response_when_no_response_requested(self):\n        \"\"\"Test elicitation with no response type.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> dict[str, Any]:\n            result = await context.elicit(message=\"\", response_type=None)\n            assert result.action == \"accept\"\n            assert isinstance(result, AcceptedElicitation)\n            accepted = cast(AcceptedElicitation[dict[str, Any]], result)\n            assert isinstance(accepted.data, dict)\n            return accepted.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"hello\"})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            with pytest.raises(\n                ToolError, match=\"Elicitation expected an empty response\"\n            ):\n                await client.call_tool(\"my_tool\", {})\n\n    async def test_elicitation_str_response(self):\n        \"\"\"Test elicitation with string schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> str:\n            result = await context.elicit(message=\"\", response_type=str)\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, str)\n            return result.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"hello\"})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data == \"hello\"\n\n    async def test_elicitation_int_response(self):\n        \"\"\"Test elicitation with number schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> int:\n            result = await context.elicit(message=\"\", response_type=int)\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, int)\n            return result.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": 42})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data == 42\n\n    async def test_elicitation_float_response(self):\n        \"\"\"Test elicitation with number schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> float:\n            result = await context.elicit(message=\"\", response_type=float)\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, float)\n            return result.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": 3.14})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data == 3.14\n\n    async def test_elicitation_bool_response(self):\n        \"\"\"Test elicitation with boolean schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> bool:\n            result = await context.elicit(message=\"\", response_type=bool)\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, bool)\n            return result.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": True})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data is True\n\n    async def test_elicitation_literal_response(self):\n        \"\"\"Test elicitation with literal schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> Literal[\"x\", \"y\"]:\n            # Literal types work at runtime but type checker doesn't recognize them in overloads\n            result = await context.elicit(message=\"\", response_type=Literal[\"x\", \"y\"])  # type: ignore[arg-type]\n            assert isinstance(result, AcceptedElicitation)\n            accepted = cast(AcceptedElicitation[Literal[\"x\", \"y\"]], result)\n            assert isinstance(accepted.data, str)\n            return accepted.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"x\"})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data == \"x\"\n\n    async def test_elicitation_enum_response(self):\n        \"\"\"Test elicitation with enum schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        class ResponseEnum(Enum):\n            X = \"x\"\n            Y = \"y\"\n\n        @mcp.tool\n        async def my_tool(context: Context) -> ResponseEnum:\n            result = await context.elicit(message=\"\", response_type=ResponseEnum)\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, ResponseEnum)\n            return result.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"x\"})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data == \"x\"\n\n    async def test_elicitation_list_of_strings_response(self):\n        \"\"\"Test elicitation with list schema.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def my_tool(context: Context) -> str:\n            result = await context.elicit(message=\"\", response_type=[\"x\", \"y\"])\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, str)\n            return result.data\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"x\"})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"my_tool\", {})\n            assert result.data == \"x\"\n\n\nasync def test_elicitation_handler_error():\n    \"\"\"Test error handling in elicitation handler.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def failing_elicit(context: Context) -> str:\n        try:\n            result = await context.elicit(message=\"This will fail\", response_type=str)\n\n            assert isinstance(result, AcceptedElicitation)\n\n            assert result.action == \"accept\"\n            return f\"Got: {result.data}\"\n        except Exception as e:\n            return f\"Error: {str(e)}\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        raise ValueError(\"Handler failed!\")\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"failing_elicit\", {})\n        assert \"Error:\" in result.data\n\n\nasync def test_elicitation_multiple_calls():\n    \"\"\"Test multiple elicitation calls in sequence.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def multi_step_form(context: Context) -> str:\n        # First question\n        name_result = await context.elicit(\n            message=\"What's your name?\", response_type=str\n        )\n\n        assert isinstance(name_result, AcceptedElicitation)\n\n        if name_result.action != \"accept\":\n            return \"Form abandoned\"\n\n        # Second question\n        age_result = await context.elicit(message=\"What's your age?\", response_type=int)\n\n        assert isinstance(age_result, AcceptedElicitation)\n\n        if age_result.action != \"accept\":\n            return f\"Hello {name_result.data}, form incomplete\"\n\n        return f\"Hello {name_result.data}, you are {age_result.data} years old\"\n\n    call_count = 0\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        nonlocal call_count\n        call_count += 1\n        if call_count == 1:\n            return ElicitResult(action=\"accept\", content={\"value\": \"Bob\"})\n        elif call_count == 2:\n            return ElicitResult(action=\"accept\", content={\"value\": 25})\n        else:\n            raise ValueError(\"Unexpected call\")\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"multi_step_form\", {})\n        assert result.data == \"Hello Bob, you are 25 years old\"\n        assert call_count == 2\n\n\n@dataclass\nclass UserInfo:\n    name: str\n    age: int\n\n\nclass UserInfoTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass UserInfoPydantic(BaseModel):\n    name: str\n    age: int\n\n\n@pytest.mark.parametrize(\n    \"structured_type\", [UserInfo, UserInfoTypedDict, UserInfoPydantic]\n)\nasync def test_structured_response_type(\n    structured_type: type[UserInfo | UserInfoTypedDict | UserInfoPydantic],\n):\n    \"\"\"Test elicitation with dataclass response type.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def get_user_info(context: Context) -> str:\n        result = await context.elicit(\n            message=\"Please provide your information\", response_type=structured_type\n        )\n\n        assert isinstance(result, AcceptedElicitation)\n\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            if isinstance(result.data, dict):\n                data_dict = cast(dict[str, Any], result.data)\n                name = data_dict.get(\"name\")\n                age = data_dict.get(\"age\")\n                assert name is not None\n                assert age is not None\n                return f\"User: {name}, age: {age}\"\n            else:\n                # result.data is a structured type (UserInfo, UserInfoTypedDict, or UserInfoPydantic)\n                assert hasattr(result.data, \"name\")\n                assert hasattr(result.data, \"age\")\n                return f\"User: {result.data.name}, age: {result.data.age}\"\n        return \"No user info provided\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Verify we get the dataclass type\n        assert (\n            TypeAdapter(response_type).json_schema()\n            == TypeAdapter(structured_type).json_schema()\n        )\n\n        # Verify the schema has the dataclass fields (available in params)\n        schema = params.requestedSchema\n        assert schema[\"type\"] == \"object\"\n        assert \"name\" in schema[\"properties\"]\n        assert \"age\" in schema[\"properties\"]\n        assert schema[\"properties\"][\"name\"][\"type\"] == \"string\"\n        assert schema[\"properties\"][\"age\"][\"type\"] == \"integer\"\n\n        return ElicitResult(action=\"accept\", content=UserInfo(name=\"Alice\", age=30))\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"get_user_info\", {})\n        assert result.data == \"User: Alice, age: 30\"\n\n\nasync def test_all_primitive_field_types():\n    class DataEnum(Enum):\n        X = \"x\"\n        Y = \"y\"\n\n    @dataclass\n    class Data:\n        integer: int\n        float_: float\n        number: int | float\n        boolean: bool\n        string: str\n        constant: Literal[\"x\"]\n        union: Literal[\"x\"] | Literal[\"y\"]\n        choice: Literal[\"x\", \"y\"]\n        enum: DataEnum\n\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def get_data(context: Context) -> Data:\n        result = await context.elicit(message=\"Enter data\", response_type=Data)\n        assert isinstance(result, AcceptedElicitation)\n        assert isinstance(result.data, Data)\n        return result.data\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        return ElicitResult(\n            action=\"accept\",\n            content=Data(\n                integer=1,\n                float_=1.0,\n                number=1.0,\n                boolean=True,\n                string=\"hello\",\n                constant=\"x\",\n                union=\"x\",\n                choice=\"x\",\n                enum=DataEnum.X,\n            ),\n        )\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"get_data\", {})\n\n        # Now all literal/enum fields should be preserved as strings\n        result_data = asdict(result.data)\n        result_data_enum = result_data.pop(\"enum\")\n        assert result_data_enum == \"x\"  # Should be a string now, not an enum\n        assert result_data == {\n            \"integer\": 1,\n            \"float_\": 1.0,\n            \"number\": 1.0,\n            \"boolean\": True,\n            \"string\": \"hello\",\n            \"constant\": \"x\",\n            \"union\": \"x\",\n            \"choice\": \"x\",\n        }\n\n\nclass TestValidation:\n    async def test_schema_validation_rejects_non_object(self):\n        \"\"\"Test that non-object schemas are rejected.\"\"\"\n\n        with pytest.raises(TypeError, match=\"must be an object schema\"):\n            validate_elicitation_json_schema({\"type\": \"string\"})\n\n    async def test_schema_validation_rejects_nested_objects(self):\n        \"\"\"Test that nested object schemas are rejected.\"\"\"\n\n        with pytest.raises(\n            TypeError, match=\"is an object, but nested objects are not allowed\"\n        ):\n            validate_elicitation_json_schema(\n                {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"user\": {\n                            \"type\": \"object\",\n                            \"properties\": {\"name\": {\"type\": \"string\"}},\n                        }\n                    },\n                }\n            )\n\n    async def test_schema_validation_rejects_arrays(self):\n        \"\"\"Test that non-enum array schemas are rejected.\"\"\"\n\n        with pytest.raises(TypeError, match=\"is an array, but arrays are only allowed\"):\n            validate_elicitation_json_schema(\n                {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"users\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n                    },\n                }\n            )\n\n\nclass TestPatternMatching:\n    async def test_pattern_matching_accept(self):\n        \"\"\"Test pattern matching with AcceptedElicitation.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def pattern_match_tool(context: Context) -> str:\n            result = await context.elicit(\"Enter your name:\", response_type=str)\n\n            match result:\n                case AcceptedElicitation(data=name):\n                    return f\"Hello {name}!\"\n                case DeclinedElicitation():\n                    return \"You declined\"\n                case CancelledElicitation():\n                    return \"Cancelled\"\n                case _:\n                    return \"Unknown result\"\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"Alice\"})\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"pattern_match_tool\", {})\n            assert result.data == \"Hello Alice!\"\n\n    async def test_pattern_matching_decline(self):\n        \"\"\"Test pattern matching with DeclinedElicitation.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def pattern_match_tool(context: Context) -> str:\n            result = await context.elicit(\"Enter your name:\", response_type=str)\n\n            match result:\n                case AcceptedElicitation(data=name):\n                    return f\"Hello {name}!\"\n                case DeclinedElicitation():\n                    return \"You declined\"\n                case CancelledElicitation():\n                    return \"Cancelled\"\n                case _:\n                    return \"Unknown result\"\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"decline\")\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"pattern_match_tool\", {})\n            assert result.data == \"You declined\"\n\n    async def test_pattern_matching_cancel(self):\n        \"\"\"Test pattern matching with CancelledElicitation.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def pattern_match_tool(context: Context) -> str:\n            result = await context.elicit(\"Enter your name:\", response_type=str)\n\n            match result:\n                case AcceptedElicitation(data=name):\n                    return f\"Hello {name}!\"\n                case DeclinedElicitation():\n                    return \"You declined\"\n                case CancelledElicitation():\n                    return \"Cancelled\"\n                case _:\n                    return \"Unknown result\"\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"cancel\")\n\n        async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n            result = await client.call_tool(\"pattern_match_tool\", {})\n            assert result.data == \"Cancelled\"\n"
  },
  {
    "path": "tests/client/test_elicitation_enums.py",
    "content": "\"\"\"Tests for enum-based elicitation, multi-select, and default values.\"\"\"\n\nfrom dataclasses import dataclass\nfrom enum import Enum\n\nimport pytest\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.client.client import Client\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.elicitation import (\n    AcceptedElicitation,\n    get_elicitation_schema,\n    validate_elicitation_json_schema,\n)\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP(\"TestServer\")\n\n    @dataclass\n    class Person:\n        name: str\n\n    @mcp.tool\n    async def ask_for_name(context: Context) -> str:\n        result = await context.elicit(\n            message=\"What is your name?\",\n            response_type=Person,\n        )\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, Person)\n            return f\"Hello, {result.data.name}!\"\n        else:\n            return \"No name provided.\"\n\n    @mcp.tool\n    def simple_test() -> str:\n        return \"Hello!\"\n\n    return mcp\n\n\nasync def test_elicitation_implicit_acceptance(fastmcp_server):\n    \"\"\"Test that elicitation handler can return data directly without ElicitResult wrapper.\"\"\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Return data directly without wrapping in ElicitResult\n        # This should be treated as implicit acceptance\n        return response_type(name=\"Bob\")\n\n    async with Client(\n        fastmcp_server, elicitation_handler=elicitation_handler\n    ) as client:\n        result = await client.call_tool(\"ask_for_name\")\n        assert result.data == \"Hello, Bob!\"\n\n\nasync def test_elicitation_implicit_acceptance_must_be_dict(fastmcp_server):\n    \"\"\"Test that elicitation handler can return data directly without ElicitResult wrapper.\"\"\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Return data directly without wrapping in ElicitResult\n        # This should be treated as implicit acceptance\n        return \"Bob\"\n\n    async with Client(\n        fastmcp_server, elicitation_handler=elicitation_handler\n    ) as client:\n        with pytest.raises(\n            ToolError,\n            match=\"Elicitation responses must be serializable as a JSON object\",\n        ):\n            await client.call_tool(\"ask_for_name\")\n\n\ndef test_enum_elicitation_schema_inline():\n    \"\"\"Test that enum schemas are generated inline without $ref/$defs for MCP compatibility.\"\"\"\n\n    class Priority(Enum):\n        LOW = \"low\"\n        MEDIUM = \"medium\"\n        HIGH = \"high\"\n\n    @dataclass\n    class TaskRequest:\n        title: str\n        priority: Priority\n\n    # Generate elicitation schema\n    schema = get_elicitation_schema(TaskRequest)\n\n    # Verify no $defs section exists (enums should be inlined)\n    assert \"$defs\" not in schema, (\n        \"Schema should not contain $defs - enums must be inline\"\n    )\n\n    # Verify no $ref in properties\n    for prop_name, prop_schema in schema.get(\"properties\", {}).items():\n        assert \"$ref\" not in prop_schema, (\n            f\"Property {prop_name} contains $ref - should be inline\"\n        )\n\n    # Verify the priority field has inline enum values\n    priority_schema = schema[\"properties\"][\"priority\"]\n    assert \"enum\" in priority_schema, \"Priority should have enum values inline\"\n    assert priority_schema[\"enum\"] == [\"low\", \"medium\", \"high\"]\n    assert priority_schema.get(\"type\") == \"string\"\n\n    # Verify title field is a simple string\n    assert schema[\"properties\"][\"title\"][\"type\"] == \"string\"\n\n\ndef test_enum_elicitation_schema_inline_untitled():\n    \"\"\"Test that enum schemas generate simple enum pattern (no automatic titles).\"\"\"\n\n    class TaskStatus(Enum):\n        NOT_STARTED = \"not_started\"\n        IN_PROGRESS = \"in_progress\"\n        COMPLETED = \"completed\"\n        ON_HOLD = \"on_hold\"\n\n    @dataclass\n    class TaskUpdate:\n        task_id: str\n        status: TaskStatus\n\n    # Generate elicitation schema\n    schema = get_elicitation_schema(TaskUpdate)\n\n    # Verify enum is inline\n    assert \"$defs\" not in schema\n    assert \"$ref\" not in str(schema)\n\n    status_schema = schema[\"properties\"][\"status\"]\n    # Should generate simple enum pattern (no automatic title generation)\n    assert \"enum\" in status_schema\n    assert \"oneOf\" not in status_schema\n    assert \"enumNames\" not in status_schema\n    assert status_schema[\"enum\"] == [\n        \"not_started\",\n        \"in_progress\",\n        \"completed\",\n        \"on_hold\",\n    ]\n\n\nasync def test_dict_based_titled_single_select():\n    \"\"\"Test dict-based titled single-select enum.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def my_tool(ctx: Context) -> str:\n        result = await ctx.elicit(\n            \"Choose priority\",\n            response_type={\n                \"low\": {\"title\": \"Low Priority\"},\n                \"high\": {\"title\": \"High Priority\"},\n            },\n        )\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, str)\n            return result.data\n        return \"declined\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Verify schema follows SEP-1330 pattern with type: \"string\"\n        schema = params.requestedSchema\n        assert schema[\"type\"] == \"object\"\n        assert \"value\" in schema[\"properties\"]\n        value_schema = schema[\"properties\"][\"value\"]\n        assert value_schema[\"type\"] == \"string\"\n        assert \"oneOf\" in value_schema\n        one_of = value_schema[\"oneOf\"]\n        assert {\"const\": \"low\", \"title\": \"Low Priority\"} in one_of\n        assert {\"const\": \"high\", \"title\": \"High Priority\"} in one_of\n\n        return ElicitResult(action=\"accept\", content={\"value\": \"low\"})\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"my_tool\", {})\n        assert result.data == \"low\"\n\n\nasync def test_list_list_multi_select_untitled():\n    \"\"\"Test list[list[str]] for multi-select untitled shorthand.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def my_tool(ctx: Context) -> str:\n        result = await ctx.elicit(\n            \"Choose tags\",\n            response_type=[[\"bug\", \"feature\", \"documentation\"]],\n        )\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, list)\n            return \",\".join(result.data)  # type: ignore[no-matching-overload]\n        return \"declined\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Verify schema has array with enum pattern\n        schema = params.requestedSchema\n        assert schema[\"type\"] == \"object\"\n        assert \"value\" in schema[\"properties\"]\n        value_schema = schema[\"properties\"][\"value\"]\n        assert value_schema[\"type\"] == \"array\"\n        assert \"enum\" in value_schema[\"items\"]\n        assert value_schema[\"items\"][\"enum\"] == [\"bug\", \"feature\", \"documentation\"]\n\n        return ElicitResult(action=\"accept\", content={\"value\": [\"bug\", \"feature\"]})\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"my_tool\", {})\n        assert result.data == \"bug,feature\"\n\n\nasync def test_list_dict_multi_select_titled():\n    \"\"\"Test list[dict] for multi-select titled.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    async def my_tool(ctx: Context) -> str:\n        result = await ctx.elicit(\n            \"Choose priorities\",\n            response_type=[\n                {\n                    \"low\": {\"title\": \"Low Priority\"},\n                    \"high\": {\"title\": \"High Priority\"},\n                }\n            ],\n        )\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, list)\n            return \",\".join(result.data)  # type: ignore[no-matching-overload]\n        return \"declined\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Verify schema has array with SEP-1330 compliant items (anyOf pattern)\n        schema = params.requestedSchema\n        assert schema[\"type\"] == \"object\"\n        assert \"value\" in schema[\"properties\"]\n        value_schema = schema[\"properties\"][\"value\"]\n        assert value_schema[\"type\"] == \"array\"\n        items_schema = value_schema[\"items\"]\n        assert \"anyOf\" in items_schema\n        any_of = items_schema[\"anyOf\"]\n        assert {\"const\": \"low\", \"title\": \"Low Priority\"} in any_of\n        assert {\"const\": \"high\", \"title\": \"High Priority\"} in any_of\n\n        return ElicitResult(action=\"accept\", content={\"value\": [\"low\", \"high\"]})\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"my_tool\", {})\n        assert result.data == \"low,high\"\n\n\nasync def test_list_enum_multi_select():\n    \"\"\"Test list[Enum] for multi-select with enum in dataclass field.\"\"\"\n\n    class Priority(Enum):\n        LOW = \"low\"\n        MEDIUM = \"medium\"\n        HIGH = \"high\"\n\n    @dataclass\n    class TaskRequest:\n        priorities: list[Priority]\n\n    schema = get_elicitation_schema(TaskRequest)\n\n    priorities_schema = schema[\"properties\"][\"priorities\"]\n    assert priorities_schema[\"type\"] == \"array\"\n    assert \"items\" in priorities_schema\n    items_schema = priorities_schema[\"items\"]\n    # Should have enum pattern for untitled enums\n    assert \"enum\" in items_schema\n    assert items_schema[\"enum\"] == [\"low\", \"medium\", \"high\"]\n\n\nasync def test_list_enum_multi_select_direct():\n    \"\"\"Test list[Enum] type annotation passed directly to ctx.elicit().\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    class Priority(Enum):\n        LOW = \"low\"\n        MEDIUM = \"medium\"\n        HIGH = \"high\"\n\n    @mcp.tool\n    async def my_tool(ctx: Context) -> str:\n        result = await ctx.elicit(\n            \"Choose priorities\",\n            response_type=list[Priority],  # Type annotation for multi-select\n        )\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, list)\n            priorities = result.data\n            return \",\".join(\n                [p.value if isinstance(p, Priority) else str(p) for p in priorities]\n            )\n        return \"declined\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        # Verify schema has array with enum pattern\n        schema = params.requestedSchema\n        assert schema[\"type\"] == \"object\"\n        assert \"value\" in schema[\"properties\"]\n        value_schema = schema[\"properties\"][\"value\"]\n        assert value_schema[\"type\"] == \"array\"\n        assert \"enum\" in value_schema[\"items\"]\n        assert value_schema[\"items\"][\"enum\"] == [\"low\", \"medium\", \"high\"]\n\n        return ElicitResult(action=\"accept\", content={\"value\": [\"low\", \"high\"]})\n\n    async with Client(mcp, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"my_tool\", {})\n        assert result.data == \"low,high\"\n\n\nasync def test_validation_allows_enum_arrays():\n    \"\"\"Test validation accepts arrays with enum items.\"\"\"\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"priorities\": {\n                \"type\": \"array\",\n                \"items\": {\"enum\": [\"low\", \"medium\", \"high\"]},\n            }\n        },\n    }\n    validate_elicitation_json_schema(schema)  # Should not raise\n\n\nasync def test_validation_allows_enum_arrays_with_anyof():\n    \"\"\"Test validation accepts arrays with anyOf enum pattern (SEP-1330 compliant).\"\"\"\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"priorities\": {\n                \"type\": \"array\",\n                \"items\": {\n                    \"anyOf\": [\n                        {\"const\": \"low\", \"title\": \"Low Priority\"},\n                        {\"const\": \"high\", \"title\": \"High Priority\"},\n                    ]\n                },\n            }\n        },\n    }\n    validate_elicitation_json_schema(schema)  # Should not raise\n\n\nasync def test_validation_rejects_non_enum_arrays():\n    \"\"\"Test validation still rejects arrays of objects.\"\"\"\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"users\": {\n                \"type\": \"array\",\n                \"items\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}},\n            }\n        },\n    }\n    with pytest.raises(TypeError, match=\"array of objects\"):\n        validate_elicitation_json_schema(schema)\n\n\nasync def test_validation_rejects_primitive_arrays():\n    \"\"\"Test validation rejects arrays of primitives without enum pattern.\"\"\"\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"names\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n        },\n    }\n    with pytest.raises(TypeError, match=\"arrays are only allowed\"):\n        validate_elicitation_json_schema(schema)\n\n\nclass TestElicitationDefaults:\n    \"\"\"Test suite for default values in elicitation schemas.\"\"\"\n\n    def test_string_default_preserved(self):\n        \"\"\"Test that string defaults are preserved in the schema.\"\"\"\n\n        class Model(BaseModel):\n            email: str = Field(default=\"[email protected]\")\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert \"email\" in props\n        assert \"default\" in props[\"email\"]\n        assert props[\"email\"][\"default\"] == \"[email protected]\"\n        assert props[\"email\"][\"type\"] == \"string\"\n\n    def test_integer_default_preserved(self):\n        \"\"\"Test that integer defaults are preserved in the schema.\"\"\"\n\n        class Model(BaseModel):\n            count: int = Field(default=50)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert \"count\" in props\n        assert \"default\" in props[\"count\"]\n        assert props[\"count\"][\"default\"] == 50\n        assert props[\"count\"][\"type\"] == \"integer\"\n\n    def test_number_default_preserved(self):\n        \"\"\"Test that number defaults are preserved in the schema.\"\"\"\n\n        class Model(BaseModel):\n            price: float = Field(default=3.14)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert \"price\" in props\n        assert \"default\" in props[\"price\"]\n        assert props[\"price\"][\"default\"] == 3.14\n        assert props[\"price\"][\"type\"] == \"number\"\n\n    def test_boolean_default_preserved(self):\n        \"\"\"Test that boolean defaults are preserved in the schema.\"\"\"\n\n        class Model(BaseModel):\n            enabled: bool = Field(default=False)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert \"enabled\" in props\n        assert \"default\" in props[\"enabled\"]\n        assert props[\"enabled\"][\"default\"] is False\n        assert props[\"enabled\"][\"type\"] == \"boolean\"\n\n    def test_enum_default_preserved(self):\n        \"\"\"Test that enum defaults are preserved in the schema.\"\"\"\n\n        class Priority(Enum):\n            LOW = \"low\"\n            MEDIUM = \"medium\"\n            HIGH = \"high\"\n\n        class Model(BaseModel):\n            choice: Priority = Field(default=Priority.MEDIUM)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert \"choice\" in props\n        assert \"default\" in props[\"choice\"]\n        assert props[\"choice\"][\"default\"] == \"medium\"\n        assert \"enum\" in props[\"choice\"]\n        assert props[\"choice\"][\"type\"] == \"string\"\n\n    def test_all_defaults_preserved_together(self):\n        \"\"\"Test that all default types are preserved when used together.\"\"\"\n\n        class Priority(Enum):\n            A = \"A\"\n            B = \"B\"\n\n        class Model(BaseModel):\n            string_field: str = Field(default=\"[email protected]\")\n            integer_field: int = Field(default=50)\n            number_field: float = Field(default=3.14)\n            boolean_field: bool = Field(default=False)\n            enum_field: Priority = Field(default=Priority.A)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert props[\"string_field\"][\"default\"] == \"[email protected]\"\n        assert props[\"integer_field\"][\"default\"] == 50\n        assert props[\"number_field\"][\"default\"] == 3.14\n        assert props[\"boolean_field\"][\"default\"] is False\n        assert props[\"enum_field\"][\"default\"] == \"A\"\n\n    def test_mixed_defaults_and_required(self):\n        \"\"\"Test that fields with defaults are not in required list.\"\"\"\n\n        class Model(BaseModel):\n            required_field: str = Field(description=\"Required field\")\n            optional_with_default: int = Field(default=42)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n        required = schema.get(\"required\", [])\n\n        assert \"required_field\" in required\n        assert \"optional_with_default\" not in required\n        assert props[\"optional_with_default\"][\"default\"] == 42\n\n    def test_compress_schema_preserves_defaults(self):\n        \"\"\"Test that compress_schema() doesn't strip default values.\"\"\"\n\n        class Model(BaseModel):\n            string_field: str = Field(default=\"test\")\n            integer_field: int = Field(default=42)\n\n        schema = get_elicitation_schema(Model)\n        props = schema.get(\"properties\", {})\n\n        assert \"default\" in props[\"string_field\"]\n        assert \"default\" in props[\"integer_field\"]\n"
  },
  {
    "path": "tests/client/test_logs.py",
    "content": "import logging\n\nimport pytest\nfrom mcp import LoggingLevel\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.logging import LogMessage\n\n\nclass LogHandler:\n    def __init__(self):\n        self.logs: list[LogMessage] = []\n        self.logger = logging.getLogger(__name__)\n        # Backwards-compatible way to get the log level mapping\n        if hasattr(logging, \"getLevelNamesMapping\"):\n            # For Python 3.11+\n            self.LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()  # pyright: ignore [reportAttributeAccessIssue]\n        else:\n            # For older Python versions\n            self.LOGGING_LEVEL_MAP = logging._nameToLevel\n\n    async def handle_log(self, message: LogMessage) -> None:\n        self.logs.append(message)\n\n        level = self.LOGGING_LEVEL_MAP[message.level.upper()]\n        msg = message.data.get(\"msg\")\n        extra = message.data.get(\"extra\")\n        self.logger.log(level, msg, extra=extra)\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP()\n\n    @mcp.tool\n    async def log(context: Context) -> None:\n        await context.info(message=\"hello?\")\n\n    @mcp.tool\n    async def echo_log(\n        message: str,\n        context: Context,\n        level: LoggingLevel | None = None,\n        logger: str | None = None,\n    ) -> None:\n        await context.log(message=message, level=level)\n\n    return mcp\n\n\nclass TestClientLogs:\n    async def test_log(self, fastmcp_server: FastMCP, caplog):\n        caplog.set_level(logging.INFO, logger=__name__)\n\n        log_handler = LogHandler()\n        async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:\n            await client.call_tool(\"log\", {})\n\n        assert len(log_handler.logs) == 1\n        assert log_handler.logs[0].data[\"msg\"] == \"hello?\"\n        assert log_handler.logs[0].level == \"info\"\n\n        assert len(caplog.records) == 1\n        assert caplog.records[0].msg == \"hello?\"\n        assert caplog.records[0].levelname == \"INFO\"\n\n    async def test_echo_log(self, fastmcp_server: FastMCP, caplog):\n        caplog.set_level(logging.INFO, logger=__name__)\n\n        log_handler = LogHandler()\n        async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:\n            await client.call_tool(\"echo_log\", {\"message\": \"this is a log\"})\n\n            assert len(log_handler.logs) == 1\n            assert len(caplog.records) == 1\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"this is a warning log\", \"level\": \"warning\"}\n            )\n            assert len(log_handler.logs) == 2\n            assert len(caplog.records) == 2\n\n        assert log_handler.logs[0].data[\"msg\"] == \"this is a log\"\n        assert log_handler.logs[0].level == \"info\"\n        assert log_handler.logs[1].data[\"msg\"] == \"this is a warning log\"\n        assert log_handler.logs[1].level == \"warning\"\n\n        assert caplog.records[0].msg == \"this is a log\"\n        assert caplog.records[0].levelname == \"INFO\"\n        assert caplog.records[1].msg == \"this is a warning log\"\n        assert caplog.records[1].levelname == \"WARNING\"\n\n\nclass TestSetLoggingLevel:\n    async def test_set_logging_level(self, fastmcp_server: FastMCP):\n        \"\"\"Client can set the minimum log level and lower-level messages are suppressed.\"\"\"\n        log_handler = LogHandler()\n        async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:\n            await client.set_logging_level(\"warning\")\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"debug msg\", \"level\": \"debug\"}\n            )\n            await client.call_tool(\"echo_log\", {\"message\": \"info msg\", \"level\": \"info\"})\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"warning msg\", \"level\": \"warning\"}\n            )\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"error msg\", \"level\": \"error\"}\n            )\n\n        assert len(log_handler.logs) == 2\n        assert log_handler.logs[0].data[\"msg\"] == \"warning msg\"\n        assert log_handler.logs[1].data[\"msg\"] == \"error msg\"\n\n    async def test_set_logging_level_debug_allows_all(self, fastmcp_server: FastMCP):\n        \"\"\"Setting level to debug allows all messages through.\"\"\"\n        log_handler = LogHandler()\n        async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:\n            await client.set_logging_level(\"debug\")\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"debug msg\", \"level\": \"debug\"}\n            )\n            await client.call_tool(\"echo_log\", {\"message\": \"info msg\", \"level\": \"info\"})\n\n        assert len(log_handler.logs) == 2\n\n    async def test_default_level_allows_all(self, fastmcp_server: FastMCP):\n        \"\"\"Without calling set_logging_level, all messages are sent.\"\"\"\n        log_handler = LogHandler()\n        async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"debug msg\", \"level\": \"debug\"}\n            )\n            await client.call_tool(\"echo_log\", {\"message\": \"info msg\", \"level\": \"info\"})\n\n        assert len(log_handler.logs) == 2\n\n    async def test_server_default_client_log_level(self):\n        \"\"\"Server-wide client_log_level filters messages for all sessions.\"\"\"\n        mcp = FastMCP(client_log_level=\"error\")\n\n        @mcp.tool\n        async def echo_log(\n            message: str, context: Context, level: LoggingLevel | None = None\n        ) -> None:\n            await context.log(message=message, level=level)\n\n        log_handler = LogHandler()\n        async with Client(mcp, log_handler=log_handler.handle_log) as client:\n            await client.call_tool(\"echo_log\", {\"message\": \"info msg\", \"level\": \"info\"})\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"warning msg\", \"level\": \"warning\"}\n            )\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"error msg\", \"level\": \"error\"}\n            )\n\n        assert len(log_handler.logs) == 1\n        assert log_handler.logs[0].data[\"msg\"] == \"error msg\"\n\n    async def test_session_level_overrides_server_default(self):\n        \"\"\"Per-session setLevel overrides the server's client_log_level.\"\"\"\n        mcp = FastMCP(client_log_level=\"error\")\n\n        @mcp.tool\n        async def echo_log(\n            message: str, context: Context, level: LoggingLevel | None = None\n        ) -> None:\n            await context.log(message=message, level=level)\n\n        log_handler = LogHandler()\n        async with Client(mcp, log_handler=log_handler.handle_log) as client:\n            await client.set_logging_level(\"warning\")\n            await client.call_tool(\"echo_log\", {\"message\": \"info msg\", \"level\": \"info\"})\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"warning msg\", \"level\": \"warning\"}\n            )\n            await client.call_tool(\n                \"echo_log\", {\"message\": \"error msg\", \"level\": \"error\"}\n            )\n\n        assert len(log_handler.logs) == 2\n        assert log_handler.logs[0].data[\"msg\"] == \"warning msg\"\n        assert log_handler.logs[1].data[\"msg\"] == \"error msg\"\n\n\nclass TestDefaultLogHandler:\n    \"\"\"Tests for default_log_handler with data as any JSON-serializable type.\"\"\"\n\n    async def test_default_handler_routes_to_correct_levels(self):\n        \"\"\"Test that default_log_handler routes server logs to appropriate Python log levels.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        from mcp.types import LoggingMessageNotificationParams\n\n        from fastmcp.client.logging import default_log_handler\n\n        with patch(\"fastmcp.client.logging.from_server_logger\") as mock_logger:\n            # Set up mock methods\n            mock_logger.debug = MagicMock()\n            mock_logger.info = MagicMock()\n            mock_logger.warning = MagicMock()\n            mock_logger.error = MagicMock()\n            mock_logger.critical = MagicMock()\n\n            # Test each log level\n            test_cases = [\n                (\"debug\", mock_logger.debug, \"Debug message\"),\n                (\"info\", mock_logger.info, \"Info message\"),\n                (\"notice\", mock_logger.info, \"Notice message\"),  # notice -> info\n                (\"warning\", mock_logger.warning, \"Warning message\"),\n                (\"error\", mock_logger.error, \"Error message\"),\n                (\"critical\", mock_logger.critical, \"Critical message\"),\n                (\"alert\", mock_logger.critical, \"Alert message\"),  # alert -> critical\n                (\n                    \"emergency\",\n                    mock_logger.critical,\n                    \"Emergency message\",\n                ),  # emergency -> critical\n            ]\n\n            for level, expected_method, msg in test_cases:\n                # Reset mocks\n                mock_logger.reset_mock()\n\n                # Create log message with data as a string\n                log_msg = LoggingMessageNotificationParams(\n                    level=level,  # type: ignore[arg-type]\n                    logger=\"test.logger\",\n                    data=msg,\n                )\n\n                # Call handler\n                await default_log_handler(log_msg)\n\n                # Verify correct method was called\n                expected_method.assert_called_once_with(\n                    msg=f\"Received {level.upper()} from server (test.logger): {msg}\"\n                )\n\n    async def test_default_handler_without_logger_name(self):\n        \"\"\"Test that default_log_handler works when logger name is None.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        from mcp.types import LoggingMessageNotificationParams\n\n        from fastmcp.client.logging import default_log_handler\n\n        with patch(\"fastmcp.client.logging.from_server_logger\") as mock_logger:\n            mock_logger.info = MagicMock()\n\n            log_msg = LoggingMessageNotificationParams(\n                level=\"info\",\n                logger=None,\n                data=\"Message without logger\",\n            )\n\n            await default_log_handler(log_msg)\n\n            mock_logger.info.assert_called_once_with(\n                msg=\"Received INFO from server: Message without logger\"\n            )\n\n    async def test_default_handler_with_dict_data(self):\n        \"\"\"Test that default_log_handler handles dict data correctly.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        from mcp.types import LoggingMessageNotificationParams\n\n        from fastmcp.client.logging import default_log_handler\n\n        with patch(\"fastmcp.client.logging.from_server_logger\") as mock_logger:\n            mock_logger.info = MagicMock()\n\n            log_msg = LoggingMessageNotificationParams(\n                level=\"info\",\n                logger=\"test.logger\",\n                data={\"key\": \"value\", \"count\": 42},\n            )\n\n            await default_log_handler(log_msg)\n\n            # Should log the entire dict as a string\n            mock_logger.info.assert_called_once()\n            call_args = mock_logger.info.call_args\n            assert \"Received INFO from server (test.logger):\" in call_args[1][\"msg\"]\n            assert \"key\" in call_args[1][\"msg\"]\n            assert \"value\" in call_args[1][\"msg\"]\n\n    async def test_default_handler_with_list_data(self):\n        \"\"\"Test that default_log_handler handles list data correctly.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        from mcp.types import LoggingMessageNotificationParams\n\n        from fastmcp.client.logging import default_log_handler\n\n        with patch(\"fastmcp.client.logging.from_server_logger\") as mock_logger:\n            mock_logger.warning = MagicMock()\n\n            log_msg = LoggingMessageNotificationParams(\n                level=\"warning\",\n                logger=\"test.logger\",\n                data=[\"item1\", \"item2\", \"item3\"],\n            )\n\n            await default_log_handler(log_msg)\n\n            # Should log the entire list as a string\n            mock_logger.warning.assert_called_once()\n            call_args = mock_logger.warning.call_args\n            assert \"Received WARNING from server (test.logger):\" in call_args[1][\"msg\"]\n            assert \"item1\" in call_args[1][\"msg\"]\n\n    async def test_default_handler_with_number_data(self):\n        \"\"\"Test that default_log_handler handles numeric data correctly.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        from mcp.types import LoggingMessageNotificationParams\n\n        from fastmcp.client.logging import default_log_handler\n\n        with patch(\"fastmcp.client.logging.from_server_logger\") as mock_logger:\n            mock_logger.error = MagicMock()\n\n            log_msg = LoggingMessageNotificationParams(\n                level=\"error\",\n                logger=None,\n                data=404,\n            )\n\n            await default_log_handler(log_msg)\n\n            mock_logger.error.assert_called_once_with(\n                msg=\"Received ERROR from server: 404\"\n            )\n"
  },
  {
    "path": "tests/client/test_notifications.py",
    "content": "from dataclasses import dataclass, field\nfrom datetime import datetime\n\nimport mcp.types\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.messages import MessageHandler\nfrom fastmcp.server.context import Context\n\n\n@dataclass\nclass NotificationRecording:\n    \"\"\"Record of a notification that was received.\"\"\"\n\n    method: str\n    notification: mcp.types.ServerNotification\n    timestamp: datetime = field(default_factory=datetime.now)\n\n\nclass RecordingMessageHandler(MessageHandler):\n    \"\"\"A message handler that records all notifications.\"\"\"\n\n    def __init__(self, name: str | None = None):\n        super().__init__()\n        self.notifications: list[NotificationRecording] = []\n        self.name = name\n\n    async def on_notification(self, message: mcp.types.ServerNotification) -> None:\n        \"\"\"Record all notifications with timestamp.\"\"\"\n        self.notifications.append(\n            NotificationRecording(method=message.root.method, notification=message)\n        )\n\n    def get_notifications(\n        self, method: str | None = None\n    ) -> list[NotificationRecording]:\n        \"\"\"Get all recorded notifications, optionally filtered by method.\"\"\"\n        if method is None:\n            return self.notifications\n        return [n for n in self.notifications if n.method == method]\n\n    def assert_notification_sent(self, method: str, times: int = 1) -> bool:\n        \"\"\"Assert that a notification was sent a specific number of times.\"\"\"\n        notifications = self.get_notifications(method)\n        actual_times = len(notifications)\n        assert actual_times == times, (\n            f\"Expected {times} notifications for {method}, \"\n            f\"but received {actual_times} notifications\"\n        )\n        return True\n\n    def assert_notification_not_sent(self, method: str) -> bool:\n        \"\"\"Assert that a notification was not sent.\"\"\"\n        notifications = self.get_notifications(method)\n        assert len(notifications) == 0, (\n            f\"Expected no notifications for {method}, but received {len(notifications)}\"\n        )\n        return True\n\n    def reset(self):\n        \"\"\"Clear all recorded notifications.\"\"\"\n        self.notifications.clear()\n\n\n@pytest.fixture\ndef recording_message_handler():\n    \"\"\"Fixture that provides a recording message handler instance.\"\"\"\n    handler = RecordingMessageHandler(name=\"recording_message_handler\")\n    yield handler\n\n\nclass TestNotificationAPI:\n    \"\"\"Test the notification API.\"\"\"\n\n    async def test_send_notification_async(\n        self,\n        recording_message_handler: RecordingMessageHandler,\n    ):\n        \"\"\"Test that send_notification sends immediately in async context.\"\"\"\n        server = FastMCP(name=\"NotificationAPITestServer\")\n\n        @server.tool\n        async def trigger_notification(ctx: Context) -> str:\n            \"\"\"Send a notification using the async API.\"\"\"\n            await ctx.send_notification(mcp.types.ToolListChangedNotification())\n            return \"Notification sent\"\n\n        async with Client(server, message_handler=recording_message_handler) as client:\n            recording_message_handler.reset()\n            await client.call_tool(\"trigger_notification\", {})\n\n            recording_message_handler.assert_notification_sent(\n                \"notifications/tools/list_changed\", times=1\n            )\n\n    async def test_send_multiple_notifications(\n        self,\n        recording_message_handler: RecordingMessageHandler,\n    ):\n        \"\"\"Test sending multiple different notification types.\"\"\"\n        server = FastMCP(name=\"NotificationAPITestServer\")\n\n        @server.tool\n        async def trigger_all_notifications(ctx: Context) -> str:\n            \"\"\"Send all notification types.\"\"\"\n            await ctx.send_notification(mcp.types.ToolListChangedNotification())\n            await ctx.send_notification(mcp.types.ResourceListChangedNotification())\n            await ctx.send_notification(mcp.types.PromptListChangedNotification())\n            return \"All notifications sent\"\n\n        async with Client(server, message_handler=recording_message_handler) as client:\n            recording_message_handler.reset()\n            await client.call_tool(\"trigger_all_notifications\", {})\n\n            recording_message_handler.assert_notification_sent(\n                \"notifications/tools/list_changed\", times=1\n            )\n            recording_message_handler.assert_notification_sent(\n                \"notifications/resources/list_changed\", times=1\n            )\n            recording_message_handler.assert_notification_sent(\n                \"notifications/prompts/list_changed\", times=1\n            )\n"
  },
  {
    "path": "tests/client/test_oauth_callback_race.py",
    "content": "import anyio\nimport httpx\n\nfrom fastmcp.client.oauth_callback import (\n    OAuthCallbackResult,\n    create_oauth_callback_server,\n)\nfrom fastmcp.utilities.http import find_available_port\n\n\nasync def test_oauth_callback_result_ignores_subsequent_callbacks():\n    \"\"\"Only the first callback should be captured in shared OAuth callback state.\"\"\"\n    port = find_available_port()\n    result = OAuthCallbackResult()\n    result_ready = anyio.Event()\n    server = create_oauth_callback_server(\n        port=port,\n        result_container=result,\n        result_ready=result_ready,\n    )\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(server.serve)\n\n        await anyio.sleep(0.05)\n\n        async with httpx.AsyncClient() as client:\n            first = await client.get(\n                f\"http://127.0.0.1:{port}/callback?code=good&state=s1\"\n            )\n            assert first.status_code == 200\n\n            await result_ready.wait()\n\n            second = await client.get(\n                f\"http://127.0.0.1:{port}/callback?code=evil&state=s2\"\n            )\n            assert second.status_code == 200\n\n        assert result.error is None\n        assert result.code == \"good\"\n        assert result.state == \"s1\"\n\n        tg.cancel_scope.cancel()\n"
  },
  {
    "path": "tests/client/test_oauth_callback_xss.py",
    "content": "\"\"\"Comprehensive XSS protection tests for OAuth callback HTML rendering.\"\"\"\n\nimport pytest\n\nfrom fastmcp.client.oauth_callback import create_callback_html\nfrom fastmcp.utilities.ui import (\n    create_detail_box,\n    create_info_box,\n    create_page,\n    create_status_message,\n)\n\n\ndef test_ui_create_page_escapes_title():\n    \"\"\"Test that page title is properly escaped.\"\"\"\n    xss_title = \"<script>alert(1)</script>\"\n    html = create_page(\"content\", title=xss_title)\n    assert \"&lt;script&gt;alert(1)&lt;/script&gt;\" in html\n    assert \"<script>alert(1)</script>\" not in html\n\n\ndef test_ui_create_status_message_escapes():\n    \"\"\"Test that status messages are properly escaped.\"\"\"\n    xss_message = \"<img src=x onerror=alert(1)>\"\n    html = create_status_message(xss_message)\n    assert \"&lt;img src=x onerror=alert(1)&gt;\" in html\n    assert \"<img src=x onerror=alert(1)>\" not in html\n\n\ndef test_ui_create_info_box_escapes():\n    \"\"\"Test that info box content is properly escaped.\"\"\"\n    xss_content = \"<iframe src=javascript:alert(1)></iframe>\"\n    html = create_info_box(xss_content)\n    assert \"&lt;iframe\" in html\n    assert \"<iframe src=javascript:alert(1)>\" not in html\n\n\ndef test_ui_create_detail_box_escapes():\n    \"\"\"Test that detail box labels and values are properly escaped.\"\"\"\n    xss_label = '<script>alert(\"label\")</script>'\n    xss_value = '<script>alert(\"value\")</script>'\n    html = create_detail_box([(xss_label, xss_value)])\n    assert \"&lt;script&gt;\" in html\n    assert '<script>alert(\"label\")</script>' not in html\n    assert '<script>alert(\"value\")</script>' not in html\n\n\ndef test_callback_html_escapes_error_message():\n    \"\"\"Test that XSS payloads in error messages are properly escaped.\"\"\"\n    xss_payload = \"<img/src/onerror=alert(1)>\"\n    html = create_callback_html(xss_payload, is_success=False)\n\n    assert \"&lt;img/src/onerror=alert(1)&gt;\" in html\n    assert \"<img/src/onerror=alert(1)>\" not in html\n\n\ndef test_callback_html_escapes_server_url():\n    \"\"\"Test that XSS payloads in server_url are properly escaped.\"\"\"\n    xss_payload = \"<script>alert(1)</script>\"\n    html = create_callback_html(\"Success\", is_success=True, server_url=xss_payload)\n\n    assert \"&lt;script&gt;alert(1)&lt;/script&gt;\" in html\n    assert \"<script>alert(1)</script>\" not in html\n\n\ndef test_callback_html_escapes_title():\n    \"\"\"Test that XSS payloads in title are properly escaped.\"\"\"\n    xss_payload = \"<script>alert(document.domain)</script>\"\n    html = create_callback_html(\"Success\", title=xss_payload)\n\n    assert \"&lt;script&gt;alert(document.domain)&lt;/script&gt;\" in html\n    assert \"<script>alert(document.domain)</script>\" not in html\n\n\ndef test_callback_html_mixed_content():\n    \"\"\"Test that legitimate text mixed with XSS attempts is properly escaped.\"\"\"\n    mixed_payload = \"Error: <img src=x onerror=alert(1)> occurred\"\n    html = create_callback_html(mixed_payload, is_success=False)\n\n    assert \"&lt;img src=x onerror=alert(1)&gt;\" in html\n    assert \"Error:\" in html\n    assert \"occurred\" in html\n    assert \"<img src=x onerror=alert(1)>\" not in html\n\n\ndef test_callback_html_event_handlers():\n    \"\"\"Test that event handler attributes are escaped.\"\"\"\n    xss_payload = '\" onload=\"alert(1)'\n    html = create_callback_html(xss_payload, is_success=False)\n\n    assert \"&quot; onload=&quot;alert(1)\" in html\n    assert '\" onload=\"alert(1)' not in html\n\n\ndef test_callback_html_special_characters():\n    \"\"\"Test that special HTML characters are properly escaped.\"\"\"\n    special_chars = \"&<>\\\"'/\"\n    html = create_callback_html(special_chars, is_success=False)\n\n    assert \"&amp;\" in html\n    assert \"&lt;\" in html\n    assert \"&gt;\" in html\n    assert \"&quot;\" in html\n    # Apostrophe gets escaped to &#x27; by html.escape()\n    assert \"&#x27;\" in html\n\n\n@pytest.mark.parametrize(\n    \"xss_vector\",\n    [\n        \"<img src=x onerror=alert(1)>\",\n        \"<script>alert(document.cookie)</script>\",\n        \"<iframe src=javascript:alert(1)>\",\n        \"<svg/onload=alert(1)>\",\n        \"<body onload=alert(1)>\",\n        \"<input onfocus=alert(1) autofocus>\",\n        \"<select onfocus=alert(1) autofocus>\",\n        \"<textarea onfocus=alert(1) autofocus>\",\n        \"<marquee onstart=alert(1)>\",\n        \"<div style=background:url('javascript:alert(1)')>\",\n    ],\n)\ndef test_common_xss_vectors(xss_vector: str):\n    \"\"\"Test that common XSS attack vectors are properly escaped.\"\"\"\n    html = create_callback_html(xss_vector, is_success=False)\n\n    # Should not contain the raw XSS vector\n    assert xss_vector not in html\n\n    # Should contain escaped version (at least the < and > should be escaped)\n    assert \"&lt;\" in html\n    assert \"&gt;\" in html\n\n\ndef test_legitimate_content_still_works():\n    \"\"\"Ensure legitimate content is displayed correctly after escaping.\"\"\"\n    legitimate_message = \"Authentication failed: Invalid credentials\"\n    legitimate_url = \"https://example.com:8080/mcp\"\n\n    # Error case\n    html = create_callback_html(legitimate_message, is_success=False)\n    assert legitimate_message in html\n    assert \"Authentication failed\" in html\n\n    # Success case\n    html = create_callback_html(\"Success\", is_success=True, server_url=legitimate_url)\n    assert legitimate_url in html\n    assert \"Authentication successful\" in html\n\n\ndef test_no_hardcoded_html_tags():\n    \"\"\"Verify that there are no hardcoded HTML tags that bypass escaping.\"\"\"\n    server_url = \"test-server\"\n    html = create_callback_html(\"Success\", is_success=True, server_url=server_url)\n\n    # Should not have <strong> tags around the server URL\n    assert f\"<strong>{server_url}</strong>\" not in html\n    # Should have the server URL displayed normally (escaped)\n    assert server_url in html\n"
  },
  {
    "path": "tests/client/test_openapi.py",
    "content": "import json\n\nimport pytest\nfrom fastapi import FastAPI, Request\nfrom mcp.types import TextResourceContents\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.transports import SSETransport, StreamableHttpTransport\nfrom fastmcp.server.providers.openapi import MCPType, RouteMap\nfrom fastmcp.utilities.tests import run_server_async\n\n\ndef create_fastmcp_server_for_headers() -> FastMCP:\n    \"\"\"Create a FastMCP server from FastAPI app with experimental parser.\"\"\"\n    app = FastAPI()\n\n    @app.get(\"/headers\")\n    def get_headers(request: Request):\n        return request.headers\n\n    @app.get(\"/headers/{header_name}\")\n    def get_header_by_name(header_name: str, request: Request):\n        return request.headers[header_name]\n\n    @app.post(\"/headers\")\n    def post_headers(request: Request):\n        return request.headers\n\n    mcp = FastMCP.from_fastapi(\n        app,\n        httpx_client_kwargs={\"headers\": {\"x-server-header\": \"test-abc\"}},\n        route_maps=[\n            # GET requests with path parameters go to ResourceTemplate\n            RouteMap(\n                methods=[\"GET\"],\n                pattern=r\".*\\{.*\\}.*\",\n                mcp_type=MCPType.RESOURCE_TEMPLATE,\n            ),\n            # GET requests without path parameters go to Resource\n            RouteMap(methods=[\"GET\"], pattern=r\".*\", mcp_type=MCPType.RESOURCE),\n        ],\n    )\n\n    return mcp\n\n\n@pytest.fixture\nasync def shttp_server():\n    \"\"\"Start a test server with StreamableHttp transport.\"\"\"\n    server = create_fastmcp_server_for_headers()\n    async with run_server_async(server, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\nasync def sse_server():\n    \"\"\"Start a test server with SSE transport.\"\"\"\n    server = create_fastmcp_server_for_headers()\n    async with run_server_async(server, transport=\"sse\") as url:\n        yield url\n\n\n@pytest.fixture\nasync def proxy_server(shttp_server: str):\n    \"\"\"Start a proxy server.\"\"\"\n    proxy = FastMCP.as_proxy(StreamableHttpTransport(shttp_server))\n    async with run_server_async(proxy, transport=\"http\") as url:\n        yield url\n\n\nasync def test_fastapi_client_headers_streamable_http_resource(shttp_server: str):\n    async with Client(transport=StreamableHttpTransport(shttp_server)) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"x-server-header\"] == \"test-abc\"\n\n\nasync def test_fastapi_client_headers_sse_resource(sse_server: str):\n    async with Client(transport=SSETransport(sse_server)) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"x-server-header\"] == \"test-abc\"\n\n\nasync def test_fastapi_client_headers_streamable_http_tool(shttp_server: str):\n    async with Client(transport=StreamableHttpTransport(shttp_server)) as client:\n        result = await client.call_tool(\"post_headers_headers_post\")\n        headers: dict[str, str] = result.data\n        assert headers[\"x-server-header\"] == \"test-abc\"\n\n\nasync def test_fastapi_client_headers_sse_tool(sse_server: str):\n    async with Client(transport=SSETransport(sse_server)) as client:\n        result = await client.call_tool(\"post_headers_headers_post\")\n        headers: dict[str, str] = result.data\n        assert headers[\"x-server-header\"] == \"test-abc\"\n\n\nasync def test_client_headers_sse_resource(sse_server: str):\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-TEST\": \"test-123\"})\n    ) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"x-test\"] == \"test-123\"\n\n\nasync def test_client_headers_shttp_resource(shttp_server: str):\n    async with Client(\n        transport=StreamableHttpTransport(shttp_server, headers={\"X-TEST\": \"test-123\"})\n    ) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"x-test\"] == \"test-123\"\n\n\nasync def test_client_headers_sse_resource_template(sse_server: str):\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-TEST\": \"test-123\"})\n    ) as client:\n        result = await client.read_resource(\n            \"resource://get_header_by_name_headers/x-test\"\n        )\n        assert isinstance(result[0], TextResourceContents)\n        header = json.loads(result[0].text)\n        assert header == \"test-123\"\n\n\nasync def test_client_headers_shttp_resource_template(shttp_server: str):\n    async with Client(\n        transport=StreamableHttpTransport(shttp_server, headers={\"X-TEST\": \"test-123\"})\n    ) as client:\n        result = await client.read_resource(\n            \"resource://get_header_by_name_headers/x-test\"\n        )\n        assert isinstance(result[0], TextResourceContents)\n        header = json.loads(result[0].text)\n        assert header == \"test-123\"\n\n\nasync def test_client_headers_sse_tool(sse_server: str):\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-TEST\": \"test-123\"})\n    ) as client:\n        result = await client.call_tool(\"post_headers_headers_post\")\n        headers: dict[str, str] = result.data\n        assert headers[\"x-test\"] == \"test-123\"\n\n\nasync def test_client_headers_shttp_tool(shttp_server: str):\n    async with Client(\n        transport=StreamableHttpTransport(shttp_server, headers={\"X-TEST\": \"test-123\"})\n    ) as client:\n        result = await client.call_tool(\"post_headers_headers_post\")\n        headers: dict[str, str] = result.data\n        assert headers[\"x-test\"] == \"test-123\"\n\n\nasync def test_client_overrides_server_headers(shttp_server: str):\n    async with Client(\n        transport=StreamableHttpTransport(\n            shttp_server, headers={\"x-server-header\": \"test-client\"}\n        )\n    ) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"x-server-header\"] == \"test-client\"\n\n\nasync def test_client_with_excluded_header_is_ignored(sse_server: str):\n    async with Client(\n        transport=SSETransport(\n            sse_server,\n            headers={\n                \"x-server-header\": \"test-client\",\n                \"host\": \"1.2.3.4\",\n                \"not-host\": \"1.2.3.4\",\n            },\n        )\n    ) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"not-host\"] == \"1.2.3.4\"\n        assert headers[\"host\"] == \"fastapi\"\n\n\n@pytest.mark.flaky(retries=2, delay=1)\nasync def test_client_headers_proxy(proxy_server: str):\n    \"\"\"\n    Test that client headers are passed through the proxy to the remove server.\n    \"\"\"\n    async with Client(transport=StreamableHttpTransport(proxy_server)) as client:\n        result = await client.read_resource(\"resource://get_headers_headers_get\")\n        assert isinstance(result[0], TextResourceContents)\n        headers = json.loads(result[0].text)\n        assert headers[\"x-server-header\"] == \"test-abc\"\n"
  },
  {
    "path": "tests/client/test_progress.py",
    "content": "import pytest\n\nfrom fastmcp import Client, Context, FastMCP\n\nPROGRESS_MESSAGES = []\n\n\n@pytest.fixture(autouse=True)\ndef clear_progress_messages():\n    PROGRESS_MESSAGES.clear()\n    yield\n    PROGRESS_MESSAGES.clear()\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP()\n\n    @mcp.tool\n    async def progress_tool(context: Context) -> int:\n        for i in range(3):\n            await context.report_progress(\n                progress=i + 1,\n                total=3,\n                message=f\"{(i + 1) / 3 * 100:.2f}% complete\",\n            )\n        return 100\n\n    return mcp\n\n\nEXPECTED_PROGRESS_MESSAGES = [\n    dict(progress=1, total=3, message=\"33.33% complete\"),\n    dict(progress=2, total=3, message=\"66.67% complete\"),\n    dict(progress=3, total=3, message=\"100.00% complete\"),\n]\n\n\nasync def progress_handler(\n    progress: float, total: float | None, message: str | None\n) -> None:\n    PROGRESS_MESSAGES.append(dict(progress=progress, total=total, message=message))\n\n\nasync def test_progress_handler(fastmcp_server: FastMCP):\n    async with Client(fastmcp_server, progress_handler=progress_handler) as client:\n        await client.call_tool(\"progress_tool\", {})\n\n    assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES\n\n\nasync def test_progress_handler_can_be_supplied_on_tool_call(fastmcp_server: FastMCP):\n    async with Client(fastmcp_server) as client:\n        await client.call_tool(\"progress_tool\", {}, progress_handler=progress_handler)\n\n    assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES\n\n\nasync def test_progress_handler_supplied_on_tool_call_overrides_default(\n    fastmcp_server: FastMCP,\n):\n    async def bad_progress_handler(\n        progress: float, total: float | None, message: str | None\n    ) -> None:\n        raise Exception(\"This should not be called\")\n\n    async with Client(fastmcp_server, progress_handler=bad_progress_handler) as client:\n        await client.call_tool(\"progress_tool\", {}, progress_handler=progress_handler)\n\n    assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES\n\n\nasync def test_default_progress_handler_handles_zero_total() -> None:\n    from fastmcp.client.progress import default_progress_handler\n\n    await default_progress_handler(progress=1, total=0, message=\"starting\")\n"
  },
  {
    "path": "tests/client/test_roots.py",
    "content": "import pytest\n\nfrom fastmcp import Client, Context, FastMCP\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP()\n\n    @mcp.tool\n    async def list_roots(context: Context) -> list[str]:\n        roots = await context.list_roots()\n        return [str(r.uri) for r in roots]\n\n    return mcp\n\n\nclass TestClientRoots:\n    @pytest.mark.parametrize(\"roots\", [[\"x\"], [\"x\", \"y\"]])\n    async def test_invalid_roots(self, fastmcp_server: FastMCP, roots: list[str]):\n        \"\"\"\n        Roots must be URIs\n        \"\"\"\n        with pytest.raises(ValueError, match=\"Input should be a valid URL\"):\n            async with Client(fastmcp_server, roots=roots):\n                pass\n\n    @pytest.mark.parametrize(\"roots\", [[\"https://x.com\"]])\n    async def test_invalid_urls(self, fastmcp_server: FastMCP, roots: list[str]):\n        \"\"\"\n        At this time, root URIs must start with file://\n        \"\"\"\n        with pytest.raises(ValueError, match=\"URL scheme should be 'file'\"):\n            async with Client(fastmcp_server, roots=roots):\n                pass\n\n    @pytest.mark.parametrize(\"roots\", [[\"file://x/y/z\", \"file://x/y/z\"]])\n    async def test_valid_roots(self, fastmcp_server: FastMCP, roots: list[str]):\n        async with Client(fastmcp_server, roots=roots) as client:\n            result = await client.call_tool(\"list_roots\", {})\n            assert result.data == [\n                \"file://x/y/z\",\n                \"file://x/y/z\",\n            ]\n"
  },
  {
    "path": "tests/client/test_sampling.py",
    "content": "import json\nfrom typing import cast\nfrom unittest.mock import AsyncMock\n\nimport pytest\nfrom mcp.types import TextContent\nfrom pydantic_core import to_json\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams\nfrom fastmcp.server.sampling import SamplingResult, SamplingTool\nfrom fastmcp.utilities.types import Image\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP()\n\n    @mcp.tool\n    async def simple_sample(message: str, context: Context) -> str:\n        result = await context.sample(\"Hello, world!\")\n        assert isinstance(result, SamplingResult)\n        assert result.text is not None\n        return result.text\n\n    @mcp.tool\n    async def sample_with_system_prompt(message: str, context: Context) -> str:\n        result = await context.sample(\"Hello, world!\", system_prompt=\"You love FastMCP\")\n        assert isinstance(result, SamplingResult)\n        assert result.text is not None\n        return result.text\n\n    @mcp.tool\n    async def sample_with_messages(message: str, context: Context) -> str:\n        result = await context.sample(\n            [\n                \"Hello!\",\n                SamplingMessage(\n                    content=TextContent(\n                        type=\"text\", text=\"How can I assist you today?\"\n                    ),\n                    role=\"assistant\",\n                ),\n            ]\n        )\n        assert isinstance(result, SamplingResult)\n        assert result.text is not None\n        return result.text\n\n    @mcp.tool\n    async def sample_with_image(image_bytes: bytes, context: Context) -> str:\n        image = Image(data=image_bytes)\n\n        result = await context.sample(\n            [\n                SamplingMessage(\n                    content=TextContent(type=\"text\", text=\"What's in this image?\"),\n                    role=\"user\",\n                ),\n                SamplingMessage(\n                    content=image.to_image_content(),\n                    role=\"user\",\n                ),\n            ]\n        )\n        assert isinstance(result, SamplingResult)\n        assert result.text is not None\n        return result.text\n\n    return mcp\n\n\nasync def test_simple_sampling(fastmcp_server: FastMCP):\n    def sampling_handler(\n        messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n    ) -> str:\n        return \"This is the sample message!\"\n\n    async with Client(fastmcp_server, sampling_handler=sampling_handler) as client:\n        result = await client.call_tool(\"simple_sample\", {\"message\": \"Hello, world!\"})\n        assert result.data == \"This is the sample message!\"\n\n\nasync def test_sampling_with_system_prompt(fastmcp_server: FastMCP):\n    def sampling_handler(\n        messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n    ) -> str:\n        assert params.systemPrompt is not None\n        return params.systemPrompt\n\n    async with Client(fastmcp_server, sampling_handler=sampling_handler) as client:\n        result = await client.call_tool(\n            \"sample_with_system_prompt\", {\"message\": \"Hello, world!\"}\n        )\n        assert result.data == \"You love FastMCP\"\n\n\nasync def test_sampling_with_messages(fastmcp_server: FastMCP):\n    def sampling_handler(\n        messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n    ) -> str:\n        assert len(messages) == 2\n\n        assert isinstance(messages[0].content, TextContent)\n        assert messages[0].content.type == \"text\"\n        assert messages[0].content.text == \"Hello!\"\n\n        assert isinstance(messages[1].content, TextContent)\n        assert messages[1].content.type == \"text\"\n        assert messages[1].content.text == \"How can I assist you today?\"\n        return \"I need to think.\"\n\n    async with Client(fastmcp_server, sampling_handler=sampling_handler) as client:\n        result = await client.call_tool(\n            \"sample_with_messages\", {\"message\": \"Hello, world!\"}\n        )\n        assert result.data == \"I need to think.\"\n\n\nasync def test_sampling_with_fallback(fastmcp_server: FastMCP):\n    openai_sampling_handler = AsyncMock(return_value=\"But I need to think\")\n\n    fastmcp_server = FastMCP(\n        sampling_handler=openai_sampling_handler,\n    )\n\n    @fastmcp_server.tool\n    async def sample_with_fallback(context: Context) -> str:\n        sampling_result = await context.sample(\"Do not think.\")\n        return cast(TextContent, sampling_result).text\n\n    client = Client(fastmcp_server)\n\n    async with client:\n        call_tool_result = await client.call_tool(\"sample_with_fallback\")\n\n    assert call_tool_result.data == \"But I need to think\"\n\n\nasync def test_sampling_with_image(fastmcp_server: FastMCP):\n    def sampling_handler(\n        messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n    ) -> str:\n        assert len(messages) == 2\n        return to_json(messages).decode()\n\n    async with Client(fastmcp_server, sampling_handler=sampling_handler) as client:\n        image_bytes = b\"abc123\"\n        result = await client.call_tool(\n            \"sample_with_image\", {\"image_bytes\": image_bytes}\n        )\n        assert json.loads(result.data) == [\n            {\n                \"role\": \"user\",\n                \"content\": {\n                    \"type\": \"text\",\n                    \"text\": \"What's in this image?\",\n                    \"annotations\": None,\n                    \"_meta\": None,\n                },\n                \"_meta\": None,\n            },\n            {\n                \"role\": \"user\",\n                \"content\": {\n                    \"type\": \"image\",\n                    \"data\": \"YWJjMTIz\",\n                    \"mimeType\": \"image/png\",\n                    \"annotations\": None,\n                    \"_meta\": None,\n                },\n                \"_meta\": None,\n            },\n        ]\n\n\nclass TestSamplingDefaultCapabilities:\n    \"\"\"Tests for default sampling capability advertisement (issue #3329).\"\"\"\n\n    async def test_default_sampling_capabilities_omit_tools(self):\n        \"\"\"Default sampling capabilities should not include tools field.\n\n        When serialized with exclude_none=True (as the MCP session does),\n        the capability should produce {\"sampling\": {}} rather than\n        {\"sampling\": {\"tools\": {}}}, ensuring compatibility with servers\n        that don't recognize the tools sub-field (e.g. older Java MCP SDK).\n        \"\"\"\n        import mcp.types as mcp_types\n\n        server = FastMCP()\n\n        def handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> str:\n            return \"ok\"\n\n        client = Client(server, sampling_handler=handler)\n        caps = client._session_kwargs[\"sampling_capabilities\"]\n        assert isinstance(caps, mcp_types.SamplingCapability)\n        assert caps.tools is None\n\n    async def test_set_sampling_callback_default_capabilities_omit_tools(self):\n        \"\"\"set_sampling_callback should also default to no tools capability.\"\"\"\n        import mcp.types as mcp_types\n\n        server = FastMCP()\n        client = Client(server)\n        client.set_sampling_callback(lambda msgs, params, ctx: \"ok\")\n        caps = client._session_kwargs[\"sampling_capabilities\"]\n        assert isinstance(caps, mcp_types.SamplingCapability)\n        assert caps.tools is None\n\n    async def test_explicit_tools_capability_is_preserved(self):\n        \"\"\"Explicitly passing tools capability should be respected.\"\"\"\n        import mcp.types as mcp_types\n\n        server = FastMCP()\n\n        def handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> str:\n            return \"ok\"\n\n        explicit_caps = mcp_types.SamplingCapability(\n            tools=mcp_types.SamplingToolsCapability()\n        )\n        client = Client(\n            server, sampling_handler=handler, sampling_capabilities=explicit_caps\n        )\n        caps = client._session_kwargs[\"sampling_capabilities\"]\n        assert isinstance(caps, mcp_types.SamplingCapability)\n        assert caps.tools is not None\n\n\nclass TestSamplingWithTools:\n    \"\"\"Tests for sampling with tools functionality.\"\"\"\n\n    async def test_sampling_with_tools_requires_capability(self):\n        \"\"\"Test that sampling with tools raises error when client lacks capability.\"\"\"\n        import mcp.types as mcp_types\n\n        from fastmcp.exceptions import ToolError\n\n        server = FastMCP()\n\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results for: {query}\"\n\n        @server.tool\n        async def sample_with_tool(context: Context) -> str:\n            # This should fail because the client doesn't advertise tools capability\n            result = await context.sample(\n                messages=\"Search for Python tutorials\",\n                tools=[search],\n            )\n            return str(result)\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> str:\n            return \"Response\"\n\n        # Explicitly disable tools capability by passing SamplingCapability without tools\n        async with Client(\n            server,\n            sampling_handler=sampling_handler,\n            sampling_capabilities=mcp_types.SamplingCapability(),  # No tools\n        ) as client:\n            with pytest.raises(ToolError, match=\"sampling.tools capability\"):\n                await client.call_tool(\"sample_with_tool\", {})\n\n    async def test_sampling_with_tools_fallback_handler_can_return_string(self):\n        \"\"\"Test that fallback handler can return a string even when tools are provided.\n\n        The LLM might choose not to use any tools and just return a text response.\n        \"\"\"\n        # This handler returns a string - valid even when tools are provided\n        simple_handler = AsyncMock(return_value=\"Direct response without tools\")\n\n        mcp = FastMCP(sampling_handler=simple_handler)\n\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results for: {query}\"\n\n        @mcp.tool\n        async def sample_with_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Search for Python tutorials\",\n                tools=[search],\n            )\n            return result.text or \"no text\"\n\n        # Client without sampling handler - will use server's fallback\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"sample_with_tool\", {})\n\n        # Handler returned string directly, which is treated as final text response\n        assert result.data == \"Direct response without tools\"\n\n    def test_sampling_tool_schema(self):\n        \"\"\"Test that SamplingTool generates correct schema.\"\"\"\n\n        def search(query: str, limit: int = 10) -> str:\n            \"\"\"Search the web for results.\"\"\"\n            return f\"Results for: {query}\"\n\n        tool = SamplingTool.from_function(search)\n        assert tool.name == \"search\"\n        assert tool.description == \"Search the web for results.\"\n        assert \"query\" in tool.parameters.get(\"properties\", {})\n        assert \"limit\" in tool.parameters.get(\"properties\", {})\n\n    async def test_sampling_tool_run(self):\n        \"\"\"Test that SamplingTool.run() executes correctly.\"\"\"\n\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        tool = SamplingTool.from_function(add)\n        result = await tool.run({\"a\": 5, \"b\": 3})\n        assert result == 8\n\n    async def test_sampling_tool_run_async(self):\n        \"\"\"Test that SamplingTool.run() works with async functions.\"\"\"\n\n        async def async_multiply(a: int, b: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return a * b\n\n        tool = SamplingTool.from_function(async_multiply)\n        result = await tool.run({\"a\": 4, \"b\": 7})\n        assert result == 28\n\n    def test_tool_choice_parameter(self):\n        \"\"\"Test that tool_choice parameter accepts string literals.\"\"\"\n        from fastmcp.server.context import ToolChoiceOption\n\n        # Verify ToolChoiceOption type accepts the valid string values\n        choices: list[ToolChoiceOption] = [\"auto\", \"required\", \"none\"]\n        assert len(choices) == 3\n        assert \"auto\" in choices\n        assert \"required\" in choices\n        assert \"none\" in choices\n"
  },
  {
    "path": "tests/client/test_sampling_result_types.py",
    "content": "from mcp.types import TextContent\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams\n\n\nclass TestSamplingResultType:\n    \"\"\"Tests for result_type parameter (structured output).\"\"\"\n\n    async def test_result_type_creates_final_response_tool(self):\n        \"\"\"Test that result_type creates a synthetic final_response tool.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n        from pydantic import BaseModel\n\n        class MathResult(BaseModel):\n            answer: int\n            explanation: str\n\n        received_tools: list = []\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            received_tools.extend(params.tools or [])\n\n            # Return the final_response tool call\n            return CreateMessageResultWithTools(\n                role=\"assistant\",\n                content=[\n                    ToolUseContent(\n                        type=\"tool_use\",\n                        id=\"call_1\",\n                        name=\"final_response\",\n                        input={\"answer\": 42, \"explanation\": \"The meaning of life\"},\n                    )\n                ],\n                model=\"test-model\",\n                stopReason=\"toolUse\",\n            )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def math_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"What is 6 * 7?\",\n                result_type=MathResult,\n            )\n            # result.result should be a MathResult object\n            assert isinstance(result.result, MathResult)\n            return f\"{result.result.answer}: {result.result.explanation}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"math_tool\", {})\n\n        # Check that final_response tool was added\n        tool_names = [t.name for t in received_tools]\n        assert \"final_response\" in tool_names\n\n        # Check the result\n        assert result.data == \"42: The meaning of life\"\n\n    async def test_result_type_with_user_tools(self):\n        \"\"\"Test result_type works alongside user-provided tools.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n        from pydantic import BaseModel\n\n        class SearchResult(BaseModel):\n            summary: str\n            sources: list[str]\n\n        def search(query: str) -> str:\n            \"\"\"Search for information.\"\"\"\n            return f\"Found info about: {query}\"\n\n        call_count = 0\n        tool_was_called = False\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count, tool_was_called\n            call_count += 1\n\n            if call_count == 1:\n                # First call: use the search tool\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"search\",\n                            input={\"query\": \"Python tutorials\"},\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                # Second call: call final_response\n                tool_was_called = True\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_2\",\n                            name=\"final_response\",\n                            input={\n                                \"summary\": \"Python is great\",\n                                \"sources\": [\"python.org\", \"docs.python.org\"],\n                            },\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def research(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Research Python\",\n                tools=[search],\n                result_type=SearchResult,\n            )\n            assert isinstance(result.result, SearchResult)\n            return f\"{result.result.summary} - {len(result.result.sources)} sources\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"research\", {})\n\n        assert tool_was_called\n        assert result.data == \"Python is great - 2 sources\"\n\n    async def test_result_type_validation_error_retries(self):\n        \"\"\"Test that validation errors are sent back to LLM for retry.\"\"\"\n        from mcp.types import (\n            CreateMessageResultWithTools,\n            ToolResultContent,\n            ToolUseContent,\n        )\n        from pydantic import BaseModel\n\n        class StrictResult(BaseModel):\n            value: int  # Must be an int\n\n        messages_received: list[list[SamplingMessage]] = []\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            messages_received.append(list(messages))\n\n            if len(messages_received) == 1:\n                # First call: invalid type\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"final_response\",\n                            input={\"value\": \"not_an_int\"},  # Wrong type\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                # Second call: valid type after seeing error\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_2\",\n                            name=\"final_response\",\n                            input={\"value\": 42},  # Correct type\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def validate_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Give me a number\",\n                result_type=StrictResult,\n            )\n            assert isinstance(result.result, StrictResult)\n            return str(result.result.value)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"validate_tool\", {})\n\n        # Should have retried after validation error\n        assert len(messages_received) == 2\n\n        # Check that error was passed back\n        last_messages = messages_received[1]\n        # Find the tool result in list content\n        tool_result = None\n        for msg in last_messages:\n            # Tool results are now in a list\n            if isinstance(msg.content, list):\n                for item in msg.content:\n                    if isinstance(item, ToolResultContent):\n                        tool_result = item\n                        break\n            elif isinstance(msg.content, ToolResultContent):\n                tool_result = msg.content\n                break\n        assert tool_result is not None\n        assert tool_result.isError is True\n        assert isinstance(tool_result.content[0], TextContent)\n        error_text = tool_result.content[0].text\n        assert \"Validation error\" in error_text\n\n        # Final result should be correct\n        assert result.data == \"42\"\n\n    async def test_sampling_result_has_text_and_history(self):\n        \"\"\"Test that SamplingResult has text, result, and history attributes.\"\"\"\n        from mcp.types import CreateMessageResultWithTools\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            return CreateMessageResultWithTools(\n                role=\"assistant\",\n                content=[TextContent(type=\"text\", text=\"Hello world\")],\n                model=\"test-model\",\n                stopReason=\"endTurn\",\n            )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def check_result(context: Context) -> str:\n            result = await context.sample(messages=\"Say hello\")\n            # Check all attributes exist\n            assert result.text == \"Hello world\"\n            assert result.result == \"Hello world\"\n            assert len(result.history) >= 1\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"check_result\", {})\n\n        assert result.data == \"ok\"\n\n\nclass TestSampleStep:\n    \"\"\"Tests for ctx.sample_step() - single LLM call with manual control.\"\"\"\n\n    async def test_sample_step_basic(self):\n        \"\"\"Test basic sample_step returns text response.\"\"\"\n        from mcp.types import CreateMessageResultWithTools\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            return CreateMessageResultWithTools(\n                role=\"assistant\",\n                content=[TextContent(type=\"text\", text=\"Hello from step\")],\n                model=\"test-model\",\n                stopReason=\"endTurn\",\n            )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_step(context: Context) -> str:\n            step = await context.sample_step(messages=\"Hi\")\n            assert not step.is_tool_use\n            assert step.text == \"Hello from step\"\n            return step.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_step\", {})\n\n        assert result.data == \"Hello from step\"\n\n    async def test_sample_step_with_tool_execution(self):\n        \"\"\"Test sample_step executes tools by default.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        call_count = 0\n\n        def my_tool(x: int) -> str:\n            \"\"\"A test tool.\"\"\"\n            return f\"result:{x}\"\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"my_tool\",\n                            input={\"x\": 42},\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_step(context: Context) -> str:\n            messages: str | list[SamplingMessage] = \"Run tool\"\n\n            while True:\n                step = await context.sample_step(messages=messages, tools=[my_tool])\n\n                if not step.is_tool_use:\n                    return step.text or \"\"\n\n                # History should include tool results when execute_tools=True\n                messages = step.history\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_step\", {})\n\n        assert result.data == \"Done\"\n        assert call_count == 2\n\n    async def test_sample_step_execute_tools_false(self):\n        \"\"\"Test sample_step with execute_tools=False doesn't execute tools.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        tool_executed = False\n\n        def my_tool() -> str:\n            \"\"\"A test tool.\"\"\"\n            nonlocal tool_executed\n            tool_executed = True\n            return \"executed\"\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            return CreateMessageResultWithTools(\n                role=\"assistant\",\n                content=[\n                    ToolUseContent(\n                        type=\"tool_use\",\n                        id=\"call_1\",\n                        name=\"my_tool\",\n                        input={},\n                    )\n                ],\n                model=\"test-model\",\n                stopReason=\"toolUse\",\n            )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_step(context: Context) -> str:\n            step = await context.sample_step(\n                messages=\"Run tool\",\n                tools=[my_tool],\n                execute_tools=False,\n            )\n            assert step.is_tool_use\n            assert len(step.tool_calls) == 1\n            assert step.tool_calls[0].name == \"my_tool\"\n            # History should include assistant message but no tool results\n            assert len(step.history) == 2  # user + assistant\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_step\", {})\n\n        assert result.data == \"ok\"\n        assert not tool_executed  # Tool should not have been executed\n\n    async def test_sample_step_history_includes_assistant_message(self):\n        \"\"\"Test that history includes assistant message when execute_tools=False.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            return CreateMessageResultWithTools(\n                role=\"assistant\",\n                content=[\n                    ToolUseContent(\n                        type=\"tool_use\",\n                        id=\"call_1\",\n                        name=\"my_tool\",\n                        input={\"query\": \"test\"},\n                    )\n                ],\n                model=\"test-model\",\n                stopReason=\"toolUse\",\n            )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        def my_tool(query: str) -> str:\n            return f\"result for {query}\"\n\n        @mcp.tool\n        async def test_step(context: Context) -> str:\n            step = await context.sample_step(\n                messages=\"Search\",\n                tools=[my_tool],\n                execute_tools=False,\n            )\n            # History should have: user message + assistant message\n            assert len(step.history) == 2\n            assert step.history[0].role == \"user\"\n            assert step.history[1].role == \"assistant\"\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_step\", {})\n\n        assert result.data == \"ok\"\n"
  },
  {
    "path": "tests/client/test_sampling_tool_loop.py",
    "content": "from typing import cast\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams\nfrom fastmcp.server.sampling import SamplingTool\n\n\nclass TestAutomaticToolLoop:\n    \"\"\"Tests for automatic tool execution loop in ctx.sample().\"\"\"\n\n    async def test_automatic_tool_loop_executes_tools(self):\n        \"\"\"Test that ctx.sample() automatically executes tool calls.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        call_count = 0\n        tool_was_called = False\n\n        def get_weather(city: str) -> str:\n            \"\"\"Get weather for a city.\"\"\"\n            nonlocal tool_was_called\n            tool_was_called = True\n            return f\"Weather in {city}: sunny, 72°F\"\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                # First call: return tool use\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"get_weather\",\n                            input={\"city\": \"Seattle\"},\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                # Second call: return final response\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"The weather is sunny!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def weather_assistant(question: str, context: Context) -> str:\n            result = await context.sample(\n                messages=question,\n                tools=[get_weather],\n            )\n            # Get text from SamplingResult\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\n                \"weather_assistant\", {\"question\": \"What's the weather?\"}\n            )\n\n        assert tool_was_called\n        assert call_count == 2\n        assert result.data == \"The weather is sunny!\"\n\n    async def test_automatic_tool_loop_multiple_tools(self):\n        \"\"\"Test that multiple tool calls in one response are all executed.\"\"\"\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        executed_tools: list[str] = []\n\n        def tool_a(x: int) -> int:\n            \"\"\"Tool A.\"\"\"\n            executed_tools.append(f\"tool_a({x})\")\n            return x * 2\n\n        def tool_b(y: int) -> int:\n            \"\"\"Tool B.\"\"\"\n            executed_tools.append(f\"tool_b({y})\")\n            return y + 10\n\n        call_count = 0\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                # Return multiple tool calls\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\", id=\"call_a\", name=\"tool_a\", input={\"x\": 5}\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\", id=\"call_b\", name=\"tool_b\", input={\"y\": 3}\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def multi_tool(context: Context) -> str:\n            result = await context.sample(messages=\"Run tools\", tools=[tool_a, tool_b])\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"multi_tool\", {})\n\n        assert executed_tools == [\"tool_a(5)\", \"tool_b(3)\"]\n        assert result.data == \"Done!\"\n\n    async def test_automatic_tool_loop_handles_unknown_tool(self):\n        \"\"\"Test that unknown tool names result in error being passed to LLM.\"\"\"\n        from mcp.types import (\n            CreateMessageResultWithTools,\n            ToolResultContent,\n            ToolUseContent,\n        )\n\n        def known_tool() -> str:\n            \"\"\"A known tool.\"\"\"\n            return \"known result\"\n\n        messages_received: list[list[SamplingMessage]] = []\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            messages_received.append(list(messages))\n\n            if len(messages_received) == 1:\n                # Request unknown tool\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"unknown_tool\",\n                            input={},\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Handled error\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_unknown(context: Context) -> str:\n            result = await context.sample(messages=\"Test\", tools=[known_tool])\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_unknown\", {})\n\n        # Check that error was passed back in messages\n        assert len(messages_received) == 2\n        last_messages = messages_received[1]\n        # Find the tool result in list content\n        tool_result = None\n        for msg in last_messages:\n            # Tool results are now in a list\n            if isinstance(msg.content, list):\n                for item in msg.content:\n                    if isinstance(item, ToolResultContent):\n                        tool_result = item\n                        break\n            elif isinstance(msg.content, ToolResultContent):\n                tool_result = msg.content\n                break\n        assert tool_result is not None\n        assert tool_result.isError is True\n        # Content is list of TextContent objects\n        assert isinstance(tool_result.content[0], TextContent)\n        error_text = tool_result.content[0].text\n        assert \"Unknown tool\" in error_text\n        assert result.data == \"Handled error\"\n\n    async def test_automatic_tool_loop_handles_tool_exception(self):\n        \"\"\"Test that tool exceptions are caught and passed to LLM as errors.\"\"\"\n        from mcp.types import (\n            CreateMessageResultWithTools,\n            ToolResultContent,\n            ToolUseContent,\n        )\n\n        def failing_tool() -> str:\n            \"\"\"A tool that raises an exception.\"\"\"\n            raise ValueError(\"Tool failed intentionally\")\n\n        messages_received: list[list[SamplingMessage]] = []\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            messages_received.append(list(messages))\n\n            if len(messages_received) == 1:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"failing_tool\",\n                            input={},\n                        )\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Handled error\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_exception(context: Context) -> str:\n            result = await context.sample(messages=\"Test\", tools=[failing_tool])\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_exception\", {})\n\n        # Check that error was passed back\n        assert len(messages_received) == 2\n        last_messages = messages_received[1]\n        # Find the tool result in list content\n        tool_result = None\n        for msg in last_messages:\n            # Tool results are now in a list\n            if isinstance(msg.content, list):\n                for item in msg.content:\n                    if isinstance(item, ToolResultContent):\n                        tool_result = item\n                        break\n            elif isinstance(msg.content, ToolResultContent):\n                tool_result = msg.content\n                break\n        assert tool_result is not None\n        assert tool_result.isError is True\n        # Content is list of TextContent objects\n        assert isinstance(tool_result.content[0], TextContent)\n        error_text = tool_result.content[0].text\n        assert \"Tool failed intentionally\" in error_text\n        assert result.data == \"Handled error\"\n\n    async def test_concurrent_tool_execution_default_sequential(self):\n        \"\"\"Test that tools execute sequentially by default.\"\"\"\n        import asyncio\n        import time\n\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        execution_order: list[tuple[str, float]] = []\n\n        async def slow_tool_a(x: int) -> int:\n            \"\"\"Slow tool A.\"\"\"\n            start = time.time()\n            execution_order.append((\"tool_a_start\", start))\n            await asyncio.sleep(0.1)\n            execution_order.append((\"tool_a_end\", time.time()))\n            return x * 2\n\n        async def slow_tool_b(y: int) -> int:\n            \"\"\"Slow tool B.\"\"\"\n            start = time.time()\n            execution_order.append((\"tool_b_start\", start))\n            await asyncio.sleep(0.1)\n            execution_order.append((\"tool_b_end\", time.time()))\n            return y + 10\n\n        call_count = 0\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_a\",\n                            name=\"slow_tool_a\",\n                            input={\"x\": 5},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_b\",\n                            name=\"slow_tool_b\",\n                            input={\"y\": 3},\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Run tools\",\n                tools=[slow_tool_a, slow_tool_b],\n                # Default: tool_concurrency=None (sequential)\n            )\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_tool\", {})\n\n        assert result.data == \"Done!\"\n        # Verify sequential execution: tool_a must complete before tool_b starts\n        events = [e[0] for e in execution_order]\n        assert events == [\"tool_a_start\", \"tool_a_end\", \"tool_b_start\", \"tool_b_end\"]\n\n    async def test_concurrent_tool_execution_unlimited(self):\n        \"\"\"Test unlimited parallel tool execution with tool_concurrency=0.\"\"\"\n        import asyncio\n        import time\n\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        execution_times: dict[str, dict[str, float]] = {}\n\n        async def slow_tool_a(x: int) -> int:\n            \"\"\"Slow tool A.\"\"\"\n            execution_times[\"tool_a\"] = {\"start\": time.time()}\n            await asyncio.sleep(0.1)\n            execution_times[\"tool_a\"][\"end\"] = time.time()\n            return x * 2\n\n        async def slow_tool_b(y: int) -> int:\n            \"\"\"Slow tool B.\"\"\"\n            execution_times[\"tool_b\"] = {\"start\": time.time()}\n            await asyncio.sleep(0.1)\n            execution_times[\"tool_b\"][\"end\"] = time.time()\n            return y + 10\n\n        call_count = 0\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_a\",\n                            name=\"slow_tool_a\",\n                            input={\"x\": 5},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_b\",\n                            name=\"slow_tool_b\",\n                            input={\"y\": 3},\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Run tools\",\n                tools=[slow_tool_a, slow_tool_b],\n                tool_concurrency=0,  # Unlimited parallel\n            )\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_tool\", {})\n\n        assert result.data == \"Done!\"\n        # Verify parallel execution: both tools should overlap in time\n        assert \"tool_a\" in execution_times\n        assert \"tool_b\" in execution_times\n        # tool_b should start before tool_a finishes (overlap)\n        assert execution_times[\"tool_b\"][\"start\"] < execution_times[\"tool_a\"][\"end\"]\n\n    async def test_concurrent_tool_execution_bounded(self):\n        \"\"\"Test bounded parallel execution with tool_concurrency=2.\"\"\"\n        import asyncio\n        import time\n\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        execution_order: list[tuple[str, float]] = []\n\n        async def slow_tool(name: str, duration: float = 0.1) -> str:\n            \"\"\"Generic slow tool.\"\"\"\n            execution_order.append((f\"{name}_start\", time.time()))\n            await asyncio.sleep(duration)\n            execution_order.append((f\"{name}_end\", time.time()))\n            return f\"{name} done\"\n\n        call_count = 0\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                # Request 3 tools (with concurrency=2, first 2 run parallel, then 3rd)\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"slow_tool\",\n                            input={\"name\": \"tool_1\", \"duration\": 0.1},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_2\",\n                            name=\"slow_tool\",\n                            input={\"name\": \"tool_2\", \"duration\": 0.1},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_3\",\n                            name=\"slow_tool\",\n                            input={\"name\": \"tool_3\", \"duration\": 0.05},\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Run tools\",\n                tools=[slow_tool],\n                tool_concurrency=2,  # Max 2 concurrent\n            )\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_tool\", {})\n\n        assert result.data == \"Done!\"\n        # Verify that at most 2 tools run concurrently\n        events = [e[0] for e in execution_order]\n        # First 2 tools should start before either ends\n        assert events[0] in [\"tool_1_start\", \"tool_2_start\"]\n        assert events[1] in [\"tool_1_start\", \"tool_2_start\"]\n        # Third tool should start after at least one of the first two finishes\n        tool_3_start_idx = events.index(\"tool_3_start\")\n        assert (\n            \"tool_1_end\" in events[:tool_3_start_idx]\n            or \"tool_2_end\" in events[:tool_3_start_idx]\n        )\n\n    async def test_sequential_tool_forces_sequential_execution(self):\n        \"\"\"Test that sequential=True forces all tools to execute sequentially.\"\"\"\n        import asyncio\n        import time\n\n        from mcp.types import CreateMessageResultWithTools, ToolUseContent\n\n        execution_order: list[tuple[str, float]] = []\n\n        async def normal_tool(x: int) -> int:\n            \"\"\"Normal tool.\"\"\"\n            execution_order.append((\"normal_start\", time.time()))\n            await asyncio.sleep(0.05)\n            execution_order.append((\"normal_end\", time.time()))\n            return x * 2\n\n        async def sequential_tool(y: int) -> int:\n            \"\"\"Sequential tool.\"\"\"\n            execution_order.append((\"sequential_start\", time.time()))\n            await asyncio.sleep(0.05)\n            execution_order.append((\"sequential_end\", time.time()))\n            return y + 10\n\n        call_count = 0\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            nonlocal call_count\n            call_count += 1\n\n            if call_count == 1:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"normal_tool\",\n                            input={\"x\": 5},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_2\",\n                            name=\"sequential_tool\",\n                            input={\"y\": 3},\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_tool(context: Context) -> str:\n            # Create tools with sequential=True for one of them\n            normal = SamplingTool.from_function(normal_tool, sequential=False)\n            sequential = SamplingTool.from_function(sequential_tool, sequential=True)\n\n            result = await context.sample(\n                messages=\"Run tools\",\n                tools=[normal, sequential],\n                tool_concurrency=0,  # Request unlimited, but sequential tool forces sequential\n            )\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_tool\", {})\n\n        assert result.data == \"Done!\"\n        # Verify sequential execution: first tool must complete before second starts\n        events = [e[0] for e in execution_order]\n        assert events[0] in [\"normal_start\", \"sequential_start\"]\n        assert events[1] in [\"normal_end\", \"sequential_end\"]\n        # Ensure the second tool starts after the first ends\n        if events[0] == \"normal_start\":\n            assert events[1] == \"normal_end\"\n            assert events[2] == \"sequential_start\"\n        else:\n            assert events[1] == \"sequential_end\"\n            assert events[2] == \"normal_start\"\n\n    async def test_concurrent_tool_execution_error_handling(self):\n        \"\"\"Test that errors are captured per-tool in parallel execution.\"\"\"\n        from mcp.types import (\n            CreateMessageResultWithTools,\n            ToolResultContent,\n            ToolUseContent,\n        )\n\n        def good_tool() -> str:\n            return \"success\"\n\n        def bad_tool() -> str:\n            raise ValueError(\"Tool error\")\n\n        messages_received: list[list[SamplingMessage]] = []\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            messages_received.append(list(messages))\n\n            if len(messages_received) == 1:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\", id=\"call_1\", name=\"good_tool\", input={}\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\", id=\"call_2\", name=\"bad_tool\", input={}\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Handled errors\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Run tools\",\n                tools=[good_tool, bad_tool],\n                tool_concurrency=0,  # Parallel execution\n            )\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_tool\", {})\n\n        assert result.data == \"Handled errors\"\n        # Check that tool results include both success and error\n        tool_result_message = messages_received[1][-1]\n        assert tool_result_message.role == \"user\"\n        tool_results = cast(list[ToolResultContent], tool_result_message.content)\n        assert len(tool_results) == 2\n        # One should be success, one should be error\n        assert any(not r.isError for r in tool_results)\n        assert any(r.isError for r in tool_results)\n\n    async def test_concurrent_tool_result_order_preserved(self):\n        \"\"\"Test that tool results maintain the same order as tool calls.\"\"\"\n        import asyncio\n\n        from mcp.types import (\n            CreateMessageResultWithTools,\n            ToolResultContent,\n            ToolUseContent,\n        )\n\n        async def tool_with_delay(value: int, delay: float) -> int:\n            \"\"\"Tool that takes variable time.\"\"\"\n            await asyncio.sleep(delay)\n            return value\n\n        messages_received: list[list[SamplingMessage]] = []\n\n        def sampling_handler(\n            messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext\n        ) -> CreateMessageResultWithTools:\n            messages_received.append(list(messages))\n\n            if len(messages_received) == 1:\n                # Tools with different delays - later tools finish first\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_1\",\n                            name=\"tool_with_delay\",\n                            input={\"value\": 1, \"delay\": 0.15},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_2\",\n                            name=\"tool_with_delay\",\n                            input={\"value\": 2, \"delay\": 0.05},\n                        ),\n                        ToolUseContent(\n                            type=\"tool_use\",\n                            id=\"call_3\",\n                            name=\"tool_with_delay\",\n                            input={\"value\": 3, \"delay\": 0.1},\n                        ),\n                    ],\n                    model=\"test-model\",\n                    stopReason=\"toolUse\",\n                )\n            else:\n                return CreateMessageResultWithTools(\n                    role=\"assistant\",\n                    content=[TextContent(type=\"text\", text=\"Done!\")],\n                    model=\"test-model\",\n                    stopReason=\"endTurn\",\n                )\n\n        mcp = FastMCP(sampling_handler=sampling_handler)\n\n        @mcp.tool\n        async def test_tool(context: Context) -> str:\n            result = await context.sample(\n                messages=\"Run tools\",\n                tools=[tool_with_delay],\n                tool_concurrency=0,  # Parallel execution\n            )\n            return result.text or \"\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"test_tool\", {})\n\n        assert result.data == \"Done!\"\n        # Check that results are in the correct order (1, 2, 3) despite finishing order (2, 3, 1)\n        tool_result_message = messages_received[1][-1]\n        tool_results = cast(list[ToolResultContent], tool_result_message.content)\n        assert len(tool_results) == 3\n        assert tool_results[0].toolUseId == \"call_1\"\n        assert tool_results[1].toolUseId == \"call_2\"\n        assert tool_results[2].toolUseId == \"call_3\"\n        # Check values are correct\n        result_texts = [cast(TextContent, r.content[0]).text for r in tool_results]\n        assert result_texts == [\"1\", \"2\", \"3\"]\n"
  },
  {
    "path": "tests/client/test_sse.py",
    "content": "import asyncio\nimport json\nimport sys\n\nimport pytest\nfrom mcp import McpError\nfrom mcp.types import TextResourceContents\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import SSETransport\nfrom fastmcp.server.dependencies import get_http_request\nfrom fastmcp.server.http import create_sse_app\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.tests import run_server_async\n\n\ndef create_test_server() -> FastMCP:\n    \"\"\"Create a FastMCP server with tools, resources, and prompts.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    def greet(name: str) -> str:\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @server.tool\n    async def sleep(seconds: float) -> str:\n        \"\"\"Sleep for a given number of seconds.\"\"\"\n        await asyncio.sleep(seconds)\n        return f\"Slept for {seconds} seconds\"\n\n    @server.resource(uri=\"data://users\")\n    async def get_users() -> str:\n        import json\n\n        return json.dumps([\"Alice\", \"Bob\", \"Charlie\"])\n\n    @server.resource(uri=\"data://user/{user_id}\")\n    async def get_user(user_id: str) -> str:\n        import json\n\n        return json.dumps({\"id\": user_id, \"name\": f\"User {user_id}\", \"active\": True})\n\n    @server.resource(uri=\"request://headers\")\n    async def get_headers() -> str:\n        import json\n\n        request = get_http_request()\n        return json.dumps(dict(request.headers))\n\n    @server.prompt\n    def welcome(name: str) -> str:\n        \"\"\"Example greeting prompt.\"\"\"\n        return f\"Welcome to FastMCP, {name}!\"\n\n    return server\n\n\n@pytest.fixture\nasync def sse_server():\n    \"\"\"Start a test server with SSE transport and return its URL.\"\"\"\n    server = create_test_server()\n    async with run_server_async(server, transport=\"sse\") as url:\n        yield url\n\n\nasync def test_ping(sse_server: str):\n    \"\"\"Test pinging the server.\"\"\"\n    async with Client(transport=SSETransport(sse_server)) as client:\n        result = await client.ping()\n        assert result is True\n\n\nasync def test_http_headers(sse_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-DEMO-HEADER\": \"ABC\"})\n    ) as client:\n        raw_result = await client.read_resource(\"request://headers\")\n        assert isinstance(raw_result[0], TextResourceContents)\n        json_result = json.loads(raw_result[0].text)\n        assert \"x-demo-header\" in json_result\n        assert json_result[\"x-demo-header\"] == \"ABC\"\n\n\n@pytest.fixture\nasync def sse_server_custom_path():\n    \"\"\"Start a test server with SSE on a custom path.\"\"\"\n    server = create_test_server()\n    async with run_server_async(server, transport=\"sse\", path=\"/help\") as url:\n        yield url\n\n\n@pytest.fixture\nasync def nested_sse_server():\n    \"\"\"Test nested server mounts with SSE.\"\"\"\n    import uvicorn\n    from starlette.applications import Starlette\n    from starlette.routing import Mount\n\n    from fastmcp.utilities.http import find_available_port\n\n    server = create_test_server()\n    sse_app = create_sse_app(\n        server=server, message_path=\"/mcp/messages\", sse_path=\"/mcp/sse/\"\n    )\n\n    # Nest the app under multiple mounts to test URL resolution\n    inner = Starlette(routes=[Mount(\"/nest-inner\", app=sse_app)])\n    outer = Starlette(routes=[Mount(\"/nest-outer\", app=inner)])\n\n    # Run uvicorn with the nested ASGI app\n    port = find_available_port()\n\n    config = uvicorn.Config(\n        app=outer,\n        host=\"127.0.0.1\",\n        port=port,\n        log_level=\"critical\",\n        ws=\"websockets-sansio\",\n    )\n\n    uvicorn_server = uvicorn.Server(config)\n    server_task = asyncio.create_task(uvicorn_server.serve())\n    await asyncio.sleep(0.1)\n\n    try:\n        yield f\"http://127.0.0.1:{port}/nest-outer/nest-inner/mcp/sse/\"\n    finally:\n        # Graceful shutdown - required for uvicorn 0.39+ due to context isolation\n        uvicorn_server.should_exit = True\n        try:\n            await server_task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_run_server_on_path(sse_server_custom_path: str):\n    \"\"\"Test running server on a custom path.\"\"\"\n    async with Client(transport=SSETransport(sse_server_custom_path)) as client:\n        result = await client.ping()\n        assert result is True\n\n\nasync def test_nested_sse_server_resolves_correctly(nested_sse_server: str):\n    \"\"\"Test patch for https://github.com/modelcontextprotocol/python-sdk/pull/659\"\"\"\n    async with Client(transport=SSETransport(nested_sse_server)) as client:\n        result = await client.ping()\n        assert result is True\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\",\n    reason=\"Timeout tests are flaky on Windows. Timeouts *are* supported but the tests are unreliable.\",\n)\nclass TestTimeout:\n    async def test_timeout(self, sse_server: str):\n        with pytest.raises(\n            McpError,\n            match=\"Timed out while waiting for response to ClientRequest. Waited 0.03 seconds\",\n        ):\n            async with Client(\n                transport=SSETransport(sse_server),\n                timeout=0.03,\n            ) as client:\n                await client.call_tool(\"sleep\", {\"seconds\": 0.1})\n\n    async def test_timeout_tool_call(self, sse_server: str):\n        async with Client(transport=SSETransport(sse_server)) as client:\n            with pytest.raises(McpError, match=\"Timed out\"):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.1}, timeout=0.03)\n\n    async def test_timeout_tool_call_overrides_client_timeout_if_lower(\n        self, sse_server: str\n    ):\n        async with Client(\n            transport=SSETransport(sse_server),\n            timeout=2,\n        ) as client:\n            with pytest.raises(McpError, match=\"Timed out\"):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.1}, timeout=0.03)\n\n    async def test_timeout_client_timeout_does_not_override_tool_call_timeout_if_lower(\n        self, sse_server: str\n    ):\n        \"\"\"\n        With SSE, the tool call timeout always takes precedence over the client.\n\n        Note: on Windows, the behavior appears unpredictable.\n        \"\"\"\n        async with Client(\n            transport=SSETransport(sse_server),\n            timeout=0.5,\n        ) as client:\n            await client.call_tool(\"sleep\", {\"seconds\": 0.8}, timeout=2)\n"
  },
  {
    "path": "tests/client/test_stdio.py",
    "content": "import asyncio\nimport gc\nimport inspect\nimport os\nimport weakref\n\nimport psutil\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.transports import PythonStdioTransport, StdioTransport\n\n\ndef running_under_debugger():\n    return os.environ.get(\"DEBUGPY_RUNNING\") == \"true\"\n\n\ndef gc_collect_harder():\n    gc.collect()\n    gc.collect()\n    gc.collect()\n    gc.collect()\n    gc.collect()\n    gc.collect()\n\n\nclass TestParallelCalls:\n    @pytest.fixture\n    def stdio_script(self, tmp_path):\n        script = inspect.cleandoc('''\n            import os\n            from fastmcp import FastMCP\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def pid() -> int:\n                \"\"\"Gets PID of server\"\"\"\n                return os.getpid()\n\n            if __name__ == \"__main__\":\n                mcp.run()\n            ''')\n        script_file = tmp_path / \"stdio.py\"\n        script_file.write_text(script)\n        return script_file\n\n    async def test_parallel_calls(self, stdio_script):\n        backend_transport = PythonStdioTransport(script_path=stdio_script)\n        backend_client = Client(transport=backend_transport)\n\n        proxy = FastMCP.as_proxy(backend=backend_client, name=\"PROXY\")\n\n        count = 10\n\n        tasks = [proxy.list_tools() for _ in range(count)]\n\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n\n        assert len(results) == count\n        errors = [result for result in results if isinstance(result, Exception)]\n        assert len(errors) == 0\n\n\n@pytest.mark.timeout(15)\nclass TestKeepAlive:\n    # https://github.com/PrefectHQ/fastmcp/issues/581\n\n    @pytest.fixture\n    def stdio_script(self, tmp_path):\n        script = inspect.cleandoc('''\n            import os\n            from fastmcp import FastMCP\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def pid() -> int:\n                \"\"\"Gets PID of server\"\"\"\n                return os.getpid()\n\n            if __name__ == \"__main__\":\n                mcp.run()\n            ''')\n        script_file = tmp_path / \"stdio.py\"\n        script_file.write_text(script)\n        return script_file\n\n    async def test_keep_alive_default_true(self):\n        client = Client(transport=StdioTransport(command=\"python\", args=[\"\"]))\n\n        assert client.transport.keep_alive is True\n\n    async def test_keep_alive_set_false(self):\n        client = Client(\n            transport=StdioTransport(command=\"python\", args=[\"\"], keep_alive=False)\n        )\n        assert client.transport.keep_alive is False\n\n    async def test_keep_alive_maintains_session_across_multiple_calls(\n        self, stdio_script\n    ):\n        client = Client(transport=PythonStdioTransport(script_path=stdio_script))\n        assert client.transport.keep_alive is True\n\n        async with client:\n            result1 = await client.call_tool(\"pid\")\n            pid1: int = result1.data\n\n        async with client:\n            result2 = await client.call_tool(\"pid\")\n            pid2: int = result2.data\n\n        assert pid1 == pid2\n\n    @pytest.mark.skipif(\n        running_under_debugger(), reason=\"Debugger holds a reference to the transport\"\n    )\n    async def test_keep_alive_true_exit_scope_kills_transport(self, stdio_script):\n        transport_weak_ref: weakref.ref[PythonStdioTransport] | None = None\n\n        async def test_server():\n            transport = PythonStdioTransport(script_path=stdio_script, keep_alive=True)\n            nonlocal transport_weak_ref\n            transport_weak_ref = weakref.ref(transport)\n            async with transport.connect_session():\n                pass\n\n        await test_server()\n\n        gc_collect_harder()\n\n        # This test will fail while debugging because the debugger holds a reference to the underlying transport\n        assert transport_weak_ref\n        transport = transport_weak_ref()\n        assert transport is None\n\n    @pytest.mark.skipif(\n        running_under_debugger(), reason=\"Debugger holds a reference to the transport\"\n    )\n    async def test_keep_alive_true_exit_scope_kills_client(self, stdio_script):\n        pid: int | None = None\n\n        async def test_server():\n            transport = PythonStdioTransport(script_path=stdio_script, keep_alive=True)\n            client = Client(transport=transport)\n\n            assert client.transport.keep_alive is True\n\n            async with client:\n                result1 = await client.call_tool(\"pid\")\n                nonlocal pid\n                pid = result1.data\n\n        await test_server()\n\n        gc_collect_harder()\n\n        # This test may fail/hang while debugging because the debugger holds a reference to the underlying transport\n\n        with pytest.raises(psutil.NoSuchProcess):\n            while True:\n                psutil.Process(pid)\n                await asyncio.sleep(0.1)\n\n    async def test_keep_alive_false_exit_scope_kills_server(self, stdio_script):\n        pid: int | None = None\n\n        async def test_server():\n            transport = PythonStdioTransport(script_path=stdio_script, keep_alive=False)\n            client = Client(transport=transport)\n            assert client.transport.keep_alive is False\n            async with client:\n                result1 = await client.call_tool(\"pid\")\n                nonlocal pid\n                pid = result1.data\n\n            del client\n\n        await test_server()\n\n        with pytest.raises(psutil.NoSuchProcess):\n            while True:\n                psutil.Process(pid)\n                await asyncio.sleep(0.1)\n\n    async def test_keep_alive_false_starts_new_session_across_multiple_calls(\n        self, stdio_script\n    ):\n        client = Client(\n            transport=PythonStdioTransport(script_path=stdio_script, keep_alive=False)\n        )\n        assert client.transport.keep_alive is False\n\n        async with client:\n            result1 = await client.call_tool(\"pid\")\n            pid1: int = result1.data\n\n        async with client:\n            result2 = await client.call_tool(\"pid\")\n            pid2: int = result2.data\n\n        assert pid1 != pid2\n\n    async def test_keep_alive_starts_new_session_if_manually_closed(self, stdio_script):\n        client = Client(transport=PythonStdioTransport(script_path=stdio_script))\n        assert client.transport.keep_alive is True\n\n        async with client:\n            result1 = await client.call_tool(\"pid\")\n            pid1: int = result1.data\n\n        await client.close()\n\n        async with client:\n            result2 = await client.call_tool(\"pid\")\n            pid2: int = result2.data\n\n        assert pid1 != pid2\n\n    async def test_keep_alive_maintains_session_if_reentered(self, stdio_script):\n        client = Client(transport=PythonStdioTransport(script_path=stdio_script))\n        assert client.transport.keep_alive is True\n\n        async with client:\n            result1 = await client.call_tool(\"pid\")\n            pid1: int = result1.data\n\n            async with client:\n                result2 = await client.call_tool(\"pid\")\n                pid2: int = result2.data\n\n            result3 = await client.call_tool(\"pid\")\n            pid3: int = result3.data\n\n        assert pid1 == pid2 == pid3\n\n    async def test_close_session_and_try_to_use_client_raises_error(self, stdio_script):\n        client = Client(transport=PythonStdioTransport(script_path=stdio_script))\n        assert client.transport.keep_alive is True\n\n        async with client:\n            await client.close()\n            with pytest.raises(RuntimeError, match=\"Client is not connected\"):\n                await client.call_tool(\"pid\")\n\n    async def test_session_task_failure_raises_immediately_on_enter(self):\n        # Use a command that will fail to start\n        client = Client(\n            transport=StdioTransport(command=\"nonexistent_command\", args=[])\n        )\n\n        # Should raise RuntimeError immediately, not defer until first use\n        with pytest.raises(RuntimeError, match=\"Client failed to connect\"):\n            async with client:\n                pass\n\n\nclass TestLogFile:\n    @pytest.fixture\n    def stdio_script_with_stderr(self, tmp_path):\n        script = inspect.cleandoc('''\n            import sys\n            from fastmcp import FastMCP\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def write_error(message: str) -> str:\n                \"\"\"Writes a message to stderr and returns it\"\"\"\n                print(message, file=sys.stderr, flush=True)\n                return message\n\n            if __name__ == \"__main__\":\n                mcp.run()\n            ''')\n        script_file = tmp_path / \"stderr_script.py\"\n        script_file.write_text(script)\n        return script_file\n\n    async def test_log_file_parameter_accepted_by_stdio_transport(self, tmp_path):\n        \"\"\"Test that log_file parameter can be set on StdioTransport\"\"\"\n        log_file_path = tmp_path / \"errors.log\"\n        transport = StdioTransport(\n            command=\"python\", args=[\"script.py\"], log_file=log_file_path\n        )\n        assert transport.log_file == log_file_path\n\n    async def test_log_file_parameter_accepted_by_python_stdio_transport(\n        self, tmp_path, stdio_script_with_stderr\n    ):\n        \"\"\"Test that log_file parameter can be set on PythonStdioTransport\"\"\"\n        log_file_path = tmp_path / \"errors.log\"\n        transport = PythonStdioTransport(\n            script_path=stdio_script_with_stderr, log_file=log_file_path\n        )\n        assert transport.log_file == log_file_path\n\n    async def test_log_file_parameter_accepts_textio(self, tmp_path):\n        \"\"\"Test that log_file parameter can accept a TextIO object\"\"\"\n        log_file_path = tmp_path / \"errors.log\"\n        with open(log_file_path, \"w\") as log_file:\n            transport = StdioTransport(\n                command=\"python\", args=[\"script.py\"], log_file=log_file\n            )\n            assert transport.log_file == log_file\n\n    async def test_log_file_captures_stderr_output_with_path(\n        self, tmp_path, stdio_script_with_stderr\n    ):\n        \"\"\"Test that stderr output is written to the log_file when using Path\"\"\"\n        log_file_path = tmp_path / \"errors.log\"\n\n        transport = PythonStdioTransport(\n            script_path=stdio_script_with_stderr, log_file=log_file_path\n        )\n        client = Client(transport=transport)\n\n        async with client:\n            await client.call_tool(\"write_error\", {\"message\": \"Test error message\"})\n\n        # Need to wait a bit for stderr to flush\n        await asyncio.sleep(0.1)\n\n        content = log_file_path.read_text()\n        assert \"Test error message\" in content\n\n    async def test_log_file_captures_stderr_output_with_textio(\n        self, tmp_path, stdio_script_with_stderr\n    ):\n        \"\"\"Test that stderr output is written to the log_file when using TextIO\"\"\"\n        log_file_path = tmp_path / \"errors.log\"\n\n        with open(log_file_path, \"w\") as log_file:\n            transport = PythonStdioTransport(\n                script_path=stdio_script_with_stderr, log_file=log_file\n            )\n            client = Client(transport=transport)\n\n            async with client:\n                await client.call_tool(\n                    \"write_error\", {\"message\": \"Test error with TextIO\"}\n                )\n\n            # Need to wait a bit for stderr to flush\n            await asyncio.sleep(0.1)\n\n        content = log_file_path.read_text()\n        assert \"Test error with TextIO\" in content\n\n    async def test_log_file_none_uses_default_behavior(\n        self, tmp_path, stdio_script_with_stderr\n    ):\n        \"\"\"Test that log_file=None uses default stderr handling\"\"\"\n        transport = PythonStdioTransport(\n            script_path=stdio_script_with_stderr, log_file=None\n        )\n        client = Client(transport=transport)\n\n        async with client:\n            # Should work without error even without explicit log_file\n            result = await client.call_tool(\n                \"write_error\", {\"message\": \"Default stderr\"}\n            )\n            assert result.data == \"Default stderr\"\n"
  },
  {
    "path": "tests/client/test_streamable_http.py",
    "content": "import asyncio\nimport json\nimport sys\nfrom contextlib import suppress\nfrom unittest.mock import AsyncMock, call\n\nimport pytest\nfrom mcp import McpError\nfrom mcp.types import TextResourceContents\n\nfrom fastmcp import Context\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.dependencies import get_http_request\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.tests import run_server_async\n\n\ndef create_test_server() -> FastMCP:\n    \"\"\"Create a FastMCP server with tools, resources, and prompts.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    def greet(name: str) -> str:\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.tool\n    async def elicit(ctx: Context) -> str:\n        \"\"\"Elicit a response from the user.\"\"\"\n        result = await ctx.elicit(\"What is your name?\", response_type=str)\n\n        if result.action == \"accept\":\n            return f\"You said your name was: {result.data}!\"  # ty: ignore[unresolved-attribute]\n        else:\n            return \"No name provided\"\n\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @server.tool\n    async def sleep(seconds: float) -> str:\n        \"\"\"Sleep for a given number of seconds.\"\"\"\n        await asyncio.sleep(seconds)\n        return f\"Slept for {seconds} seconds\"\n\n    @server.tool\n    async def greet_with_progress(name: str, ctx: Context) -> str:\n        \"\"\"Report progress for a greeting.\"\"\"\n        await ctx.report_progress(0.5, 1.0, \"Greeting in progress\")\n        await ctx.report_progress(0.75, 1.0, \"Almost there!\")\n        return f\"Hello, {name}!\"\n\n    @server.resource(uri=\"data://users\")\n    async def get_users() -> str:\n        import json\n\n        return json.dumps([\"Alice\", \"Bob\", \"Charlie\"])\n\n    @server.resource(uri=\"data://user/{user_id}\")\n    async def get_user(user_id: str) -> str:\n        import json\n\n        return json.dumps({\"id\": user_id, \"name\": f\"User {user_id}\", \"active\": True})\n\n    @server.resource(uri=\"request://headers\")\n    async def get_headers() -> str:\n        import json\n\n        request = get_http_request()\n        return json.dumps(dict(request.headers))\n\n    @server.prompt\n    def welcome(name: str) -> str:\n        \"\"\"Example greeting prompt.\"\"\"\n        return f\"Welcome to FastMCP, {name}!\"\n\n    return server\n\n\n@pytest.fixture\nasync def streamable_http_server(request):\n    \"\"\"Start a test server and return its URL.\"\"\"\n    import fastmcp\n\n    stateless_http = getattr(request, \"param\", False)\n    if stateless_http:\n        fastmcp.settings.stateless_http = True\n\n    server = create_test_server()\n    async with run_server_async(server) as url:\n        yield url\n\n    if stateless_http:\n        fastmcp.settings.stateless_http = False\n\n\n@pytest.fixture\nasync def streamable_http_server_with_streamable_http_alias():\n    \"\"\"Test that the \"streamable-http\" transport alias works.\"\"\"\n    server = create_test_server()\n    async with run_server_async(server, transport=\"streamable-http\") as url:\n        yield url\n\n\n@pytest.fixture\nasync def nested_server():\n    \"\"\"Test nested server mounts with Starlette.\"\"\"\n    import uvicorn\n    from starlette.applications import Starlette\n    from starlette.routing import Mount\n\n    from fastmcp.utilities.http import find_available_port\n\n    mcp_server = create_test_server()\n    mcp_app = mcp_server.http_app(path=\"/final/mcp\")\n\n    # Nest the app under multiple mounts to test URL resolution\n    inner = Starlette(routes=[Mount(\"/nest-inner\", app=mcp_app)])\n    outer = Starlette(\n        routes=[Mount(\"/nest-outer\", app=inner)], lifespan=mcp_app.lifespan\n    )\n\n    # Run uvicorn with the nested ASGI app\n    port = find_available_port()\n\n    config = uvicorn.Config(\n        app=outer,\n        host=\"127.0.0.1\",\n        port=port,\n        log_level=\"critical\",\n        ws=\"websockets-sansio\",\n        timeout_graceful_shutdown=0,\n    )\n\n    uvicorn_server = uvicorn.Server(config)\n    server_task = asyncio.create_task(uvicorn_server.serve())\n    await asyncio.sleep(0.1)\n\n    yield f\"http://127.0.0.1:{port}/nest-outer/nest-inner/final/mcp\"\n\n    # Graceful shutdown - required for uvicorn 0.39+ due to context isolation\n    uvicorn_server.should_exit = True\n    with suppress(asyncio.CancelledError, asyncio.TimeoutError):\n        await asyncio.wait_for(server_task, timeout=2.0)\n\n\nasync def test_ping(streamable_http_server: str):\n    \"\"\"Test pinging the server.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(streamable_http_server)\n    ) as client:\n        result = await client.ping()\n        assert result is True\n\n\nasync def test_ping_with_streamable_http_alias(\n    streamable_http_server_with_streamable_http_alias: str,\n):\n    \"\"\"Test pinging the server.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(\n            streamable_http_server_with_streamable_http_alias\n        )\n    ) as client:\n        result = await client.ping()\n        assert result is True\n\n\nasync def test_http_headers(streamable_http_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(\n            streamable_http_server, headers={\"X-DEMO-HEADER\": \"ABC\"}\n        )\n    ) as client:\n        raw_result = await client.read_resource(\"request://headers\")\n        assert isinstance(raw_result[0], TextResourceContents)\n        json_result = json.loads(raw_result[0].text)\n        assert \"x-demo-header\" in json_result\n        assert json_result[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_session_id_callback(streamable_http_server: str):\n    \"\"\"Test getting mcp-session-id from the transport.\"\"\"\n    transport = StreamableHttpTransport(streamable_http_server)\n    assert transport.get_session_id() is None\n    async with Client(transport=transport):\n        session_id = transport.get_session_id()\n        assert session_id is not None\n\n\n@pytest.mark.parametrize(\"streamable_http_server\", [True, False], indirect=True)\nasync def test_greet_with_progress_tool(streamable_http_server: str):\n    \"\"\"Test calling the greet tool.\"\"\"\n    progress_handler = AsyncMock(return_value=None)\n\n    async with Client(\n        transport=StreamableHttpTransport(streamable_http_server),\n        progress_handler=progress_handler,\n    ) as client:\n        result = await client.call_tool(\"greet_with_progress\", {\"name\": \"Alice\"})\n        assert result.data == \"Hello, Alice!\"\n\n        progress_handler.assert_has_calls(\n            [\n                call(0.5, 1.0, \"Greeting in progress\"),\n                call(0.75, 1.0, \"Almost there!\"),\n            ]\n        )\n\n\n@pytest.mark.parametrize(\"streamable_http_server\", [True, False], indirect=True)\nasync def test_elicitation_tool(streamable_http_server: str, request):\n    \"\"\"Test calling the elicitation tool in both stateless and stateful modes.\"\"\"\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        return {\"value\": \"Alice\"}\n\n    stateless_http = request.node.callspec.params.get(\"streamable_http_server\", False)\n    if stateless_http:\n        pytest.xfail(\"Elicitation is not supported in stateless HTTP mode\")\n\n    async with Client(\n        transport=StreamableHttpTransport(streamable_http_server),\n        elicitation_handler=elicitation_handler,\n    ) as client:\n        result = await client.call_tool(\"elicit\")\n        assert result.data == \"You said your name was: Alice!\"\n\n\n@pytest.mark.parametrize(\"streamable_http_server\", [True], indirect=True)\nasync def test_stateless_http_rejects_get_sse(streamable_http_server: str):\n    \"\"\"Stateless servers should reject GET SSE requests with 405.\"\"\"\n    import httpx\n\n    async with httpx.AsyncClient() as http_client:\n        response = await http_client.get(streamable_http_server)\n        assert response.status_code == 405\n\n\n@pytest.mark.parametrize(\"streamable_http_server\", [True], indirect=True)\nasync def test_stateless_http_still_accepts_post(streamable_http_server: str):\n    \"\"\"Stateless servers should still handle POST requests normally.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(streamable_http_server)\n    ) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        assert result.data == \"Hello, World!\"\n\n\nasync def test_nested_streamable_http_server_resolves_correctly(nested_server: str):\n    \"\"\"Test patch for https://github.com/modelcontextprotocol/python-sdk/pull/659\"\"\"\n    async with Client(transport=StreamableHttpTransport(nested_server)) as client:\n        result = await client.ping()\n        assert result is True\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\",\n    reason=\"Timeout tests are flaky on Windows. Timeouts *are* supported but the tests are unreliable.\",\n)\nclass TestTimeout:\n    async def test_timeout(self, streamable_http_server: str):\n        # note this transport behaves differently than others and raises\n        # McpError from the *client* context\n        with pytest.raises(McpError, match=\"Timed out\"):\n            async with Client(\n                transport=StreamableHttpTransport(streamable_http_server),\n                timeout=0.02,\n            ) as client:\n                await client.call_tool(\"sleep\", {\"seconds\": 0.05})\n\n    async def test_timeout_tool_call(self, streamable_http_server: str):\n        async with Client(\n            transport=StreamableHttpTransport(streamable_http_server),\n        ) as client:\n            with pytest.raises(McpError):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.2}, timeout=0.1)\n\n    async def test_timeout_tool_call_overrides_client_timeout(\n        self, streamable_http_server: str\n    ):\n        async with Client(\n            transport=StreamableHttpTransport(streamable_http_server),\n            timeout=2,\n        ) as client:\n            with pytest.raises(McpError):\n                await client.call_tool(\"sleep\", {\"seconds\": 0.2}, timeout=0.1)\n"
  },
  {
    "path": "tests/client/transports/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/transports/test_memory_transport.py",
    "content": "\"\"\"Tests for the in-memory FastMCPTransport.\n\nThese tests verify transport-level behavior that affects all tests using\nClient(server) with an in-process FastMCP server.\n\"\"\"\n\nimport time\n\nimport pytest\n\nfrom fastmcp import Client, FastMCP\n\n\n@pytest.mark.timeout(10)\nasync def test_task_teardown_does_not_hang():\n    \"\"\"In-memory transport must tear down in under 2 seconds after a task call.\n\n    This is a regression test for a teardown ordering bug where the Docket\n    Worker shutdown would hang for 5 seconds on every test that used\n    task=True. The root cause was the server lifespan (which owns the Docket\n    Worker) being torn down BEFORE the task group (which owns the server's\n    run() and all its pub/sub subscriptions). Fakeredis blocking operations\n    held by those subscriptions prevented the Worker's internal TaskGroup\n    from cancelling its children, causing a 5-second stall until the\n    Client's move_on_after(5) timeout fired.\n\n    The fix is to nest the task group INSIDE the lifespan context so that\n    all server tasks (and their fakeredis resources) are cancelled and\n    drained before Docket teardown begins.\n\n    If this test takes ~5 seconds, the context manager nesting in\n    FastMCPTransport.connect_session() has been reversed — the lifespan\n    must be the OUTER context and the task group must be the INNER context.\n    \"\"\"\n    mcp = FastMCP(\"teardown-test\")\n\n    @mcp.tool(task=True)\n    async def fast_tool(x: int) -> int:\n        return x * 2\n\n    t0 = time.monotonic()\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"fast_tool\", {\"x\": 21}, task=True)\n        result = await task.result()\n        assert result.data == 42\n\n    elapsed = time.monotonic() - t0\n\n    assert elapsed < 2.0, (\n        f\"Client teardown took {elapsed:.1f}s — expected <2s. \"\n        f\"This usually means the context manager nesting in \"\n        f\"FastMCPTransport.connect_session() is wrong: the lifespan \"\n        f\"must be the OUTER context and the task group the INNER context. \"\n        f\"See the comment in memory.py for details.\"\n    )\n"
  },
  {
    "path": "tests/client/transports/test_no_redirect.py",
    "content": "\"\"\"Tests verifying that client transports do not leak auth credentials on redirects.\n\nhttpx automatically strips Authorization headers on cross-origin redirects via its\n_redirect_headers mechanism. These tests verify that FastMCP's transports rely on\nthis behavior correctly and do not override it.\n\"\"\"\n\nimport httpx\nimport pytest\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, RedirectResponse, Response\nfrom starlette.routing import Route\n\nfrom fastmcp.client.transports.http import StreamableHttpTransport\nfrom fastmcp.client.transports.sse import SSETransport\n\n\nclass TestHttpxBuiltinRedirectProtection:\n    \"\"\"Verify httpx's built-in cross-origin redirect auth stripping.\"\"\"\n\n    async def test_httpx_strips_auth_on_cross_origin_redirect(self):\n        \"\"\"httpx strips Authorization headers when redirecting to a different origin.\"\"\"\n        received_headers: dict[str, str] = {}\n\n        async def target_endpoint(request: Request) -> Response:\n            received_headers.update(dict(request.headers))\n            return JSONResponse({\"status\": \"ok\"})\n\n        async def redirect_cross_origin(request: Request) -> Response:\n            return RedirectResponse(\n                url=\"http://other-host.example.com/target\",\n                status_code=302,\n            )\n\n        app = Starlette(\n            routes=[\n                Route(\"/redirect\", redirect_cross_origin),\n                Route(\"/target\", target_endpoint),\n            ]\n        )\n\n        # Use an httpx client with follow_redirects=True (as MCP does)\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            follow_redirects=True,\n        ) as client:\n            response = await client.get(\n                \"http://origin-host.example.com/redirect\",\n                headers={\"Authorization\": \"Bearer secret-token\"},\n            )\n\n        # httpx followed the redirect but stripped Authorization because\n        # the redirect target is a different origin\n        assert response.status_code == 200\n        assert \"authorization\" not in received_headers\n\n    async def test_httpx_preserves_auth_on_same_origin_redirect(self):\n        \"\"\"httpx preserves Authorization headers when redirecting to the same origin.\"\"\"\n        received_headers: dict[str, str] = {}\n\n        async def target_endpoint(request: Request) -> Response:\n            received_headers.update(dict(request.headers))\n            return JSONResponse({\"status\": \"ok\"})\n\n        async def redirect_same_origin(request: Request) -> Response:\n            return RedirectResponse(\n                url=\"http://same-host.example.com/target\",\n                status_code=302,\n            )\n\n        app = Starlette(\n            routes=[\n                Route(\"/redirect\", redirect_same_origin),\n                Route(\"/target\", target_endpoint),\n            ]\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            follow_redirects=True,\n        ) as client:\n            response = await client.get(\n                \"http://same-host.example.com/redirect\",\n                headers={\"Authorization\": \"Bearer secret-token\"},\n            )\n\n        assert response.status_code == 200\n        assert received_headers.get(\"authorization\") == \"Bearer secret-token\"\n\n    @pytest.mark.parametrize(\n        \"auth_header\",\n        [\n            \"Bearer my-secret-token\",\n            \"Basic dXNlcjpwYXNz\",\n            \"Token ghp_xxxxxxxxxxxx\",\n        ],\n    )\n    async def test_various_auth_headers_stripped_on_cross_origin(\n        self, auth_header: str\n    ):\n        \"\"\"Verify that different auth header formats are all stripped.\"\"\"\n        received_headers: dict[str, str] = {}\n\n        async def target(request: Request) -> Response:\n            received_headers.update(dict(request.headers))\n            return JSONResponse({\"status\": \"ok\"})\n\n        async def redirect(request: Request) -> Response:\n            return RedirectResponse(\n                url=\"http://evil.example.com/steal\",\n                status_code=307,\n            )\n\n        app = Starlette(\n            routes=[\n                Route(\"/api\", redirect),\n                Route(\"/steal\", target),\n            ]\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            follow_redirects=True,\n        ) as client:\n            response = await client.get(\n                \"http://legit.example.com/api\",\n                headers={\"Authorization\": auth_header},\n            )\n\n        assert response.status_code == 200\n        assert \"authorization\" not in received_headers\n\n\nclass TestMcpHttpClientRedirectProtection:\n    \"\"\"Verify that MCP's default httpx client has redirect protection.\"\"\"\n\n    async def test_create_mcp_http_client_strips_auth_on_cross_origin(self):\n        \"\"\"create_mcp_http_client creates clients that strip auth on cross-origin redirects.\"\"\"\n        received_headers: dict[str, str] = {}\n\n        async def target(request: Request) -> Response:\n            received_headers.update(dict(request.headers))\n            return JSONResponse({\"status\": \"ok\"})\n\n        async def redirect(request: Request) -> Response:\n            return RedirectResponse(\n                url=\"http://evil.example.com/steal\",\n                status_code=302,\n            )\n\n        app = Starlette(\n            routes=[\n                Route(\"/api\", redirect),\n                Route(\"/steal\", target),\n            ]\n        )\n\n        # Use AsyncClient directly with ASGI transport rather than\n        # monkey-patching _transport on create_mcp_http_client, which\n        # breaks when proxy env vars are set.\n        client = httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            headers={\"Authorization\": \"Bearer secret\"},\n            follow_redirects=True,\n        )\n\n        async with client:\n            response = await client.get(\"http://legit.example.com/api\")\n\n        assert response.status_code == 200\n        assert \"authorization\" not in received_headers\n\n\nclass TestStreamableHttpTransportFactory:\n    \"\"\"Verify factory and verify-factory redirect behavior.\"\"\"\n\n    def test_verify_factory_still_enables_redirects(self):\n        \"\"\"The verify factory should still create clients with follow_redirects=True.\"\"\"\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=False,\n        )\n        factory = transport._make_verify_factory()\n        assert factory is not None\n        client = factory()\n        assert client.follow_redirects is True\n\n    def test_sse_verify_factory_still_enables_redirects(self):\n        \"\"\"The SSE verify factory should still create clients with follow_redirects=True.\"\"\"\n        transport = SSETransport(\n            \"https://example.com/sse\",\n            verify=False,\n        )\n        factory = transport._make_verify_factory()\n        assert factory is not None\n        client = factory()\n        assert client.follow_redirects is True\n"
  },
  {
    "path": "tests/client/transports/test_transports.py",
    "content": "import ssl\nfrom ssl import VerifyMode\nfrom typing import cast\n\nimport httpx\nimport pytest\nfrom mcp.shared._httpx_utils import McpHttpClientFactory\n\nfrom fastmcp import Client\nfrom fastmcp.client.auth.oauth import OAuth\nfrom fastmcp.client.transports import SSETransport, StreamableHttpTransport\n\n\nasync def test_oauth_uses_same_client_as_transport_streamable_http():\n    transport = StreamableHttpTransport(\n        \"https://some.fake.url/\",\n        httpx_client_factory=lambda *args, **kwargs: httpx.AsyncClient(\n            verify=False, *args, **kwargs\n        ),\n        auth=\"oauth\",\n    )\n\n    assert isinstance(transport.auth, OAuth)\n    async with transport.auth.httpx_client_factory() as httpx_client:\n        assert httpx_client._transport is not None\n        assert (\n            httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n            == VerifyMode.CERT_NONE\n        )\n\n\nasync def test_oauth_uses_same_client_as_transport_sse():\n    transport = SSETransport(\n        \"https://some.fake.url/\",\n        httpx_client_factory=lambda *args, **kwargs: httpx.AsyncClient(\n            verify=False, *args, **kwargs\n        ),\n        auth=\"oauth\",\n    )\n\n    assert isinstance(transport.auth, OAuth)\n    async with transport.auth.httpx_client_factory() as httpx_client:\n        assert httpx_client._transport is not None\n        assert (\n            httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n            == VerifyMode.CERT_NONE\n        )\n\n\nclass TestSSLVerify:\n    def test_streamable_http_transport_stores_verify_false(self):\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=False,\n        )\n        assert transport.verify is False\n\n    def test_streamable_http_transport_stores_verify_ssl_context(self):\n        ctx = ssl.create_default_context()\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=ctx,\n        )\n        assert transport.verify is ctx\n\n    def test_streamable_http_transport_stores_verify_cert_path(self):\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=\"/path/to/cert.pem\",\n        )\n        assert transport.verify == \"/path/to/cert.pem\"\n\n    def test_streamable_http_transport_verify_default_is_none(self):\n        transport = StreamableHttpTransport(\"https://example.com/mcp\")\n        assert transport.verify is None\n\n    def test_sse_transport_stores_verify_false(self):\n        transport = SSETransport(\n            \"https://example.com/sse\",\n            verify=False,\n        )\n        assert transport.verify is False\n\n    def test_sse_transport_stores_verify_ssl_context(self):\n        ctx = ssl.create_default_context()\n        transport = SSETransport(\n            \"https://example.com/sse\",\n            verify=ctx,\n        )\n        assert transport.verify is ctx\n\n    def test_sse_transport_verify_default_is_none(self):\n        transport = SSETransport(\"https://example.com/sse\")\n        assert transport.verify is None\n\n    def test_client_passes_verify_to_streamable_http_transport(self):\n        client = Client(\"https://example.com/mcp\", verify=False)\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert client.transport.verify is False\n\n    def test_client_passes_verify_ssl_context_to_transport(self):\n        ctx = ssl.create_default_context()\n        client = Client(\"https://example.com/mcp\", verify=ctx)\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert client.transport.verify is ctx\n\n    def test_client_passes_verify_cert_path_to_transport(self):\n        client = Client(\n            \"https://example.com/mcp\",\n            verify=\"/path/to/cert.pem\",\n        )\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert client.transport.verify == \"/path/to/cert.pem\"\n\n    def test_client_verify_none_leaves_transport_default(self):\n        client = Client(\"https://example.com/mcp\")\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert client.transport.verify is None\n\n    def test_client_verify_raises_for_non_http_transport(self):\n        from fastmcp import FastMCP\n\n        server = FastMCP(\"test\")\n        with pytest.raises(\n            ValueError,\n            match=\"only supported for HTTP transports\",\n        ):\n            Client(server, verify=False)\n\n    def test_client_passes_verify_to_sse_transport(self):\n        client = Client(\"https://example.com/sse\", verify=False)\n        assert isinstance(client.transport, SSETransport)\n        assert client.transport.verify is False\n\n    async def test_streamable_http_verify_propagates_to_oauth(self):\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=False,\n            auth=\"oauth\",\n        )\n        assert isinstance(transport.auth, OAuth)\n        async with transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n                == VerifyMode.CERT_NONE\n            )\n\n    async def test_sse_verify_propagates_to_oauth(self):\n        transport = SSETransport(\n            \"https://example.com/sse\",\n            verify=False,\n            auth=\"oauth\",\n        )\n        assert isinstance(transport.auth, OAuth)\n        async with transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n                == VerifyMode.CERT_NONE\n            )\n\n    async def test_client_verify_propagates_to_oauth(self):\n        client = Client(\n            \"https://example.com/mcp\",\n            verify=False,\n            auth=\"oauth\",\n        )\n        assert isinstance(client.transport, StreamableHttpTransport)\n        assert isinstance(client.transport.auth, OAuth)\n        async with client.transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n                == VerifyMode.CERT_NONE\n            )\n\n    async def test_verify_propagates_to_preconstructed_oauth_instance(self):\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=False,\n            auth=OAuth(),\n        )\n        assert isinstance(transport.auth, OAuth)\n        async with transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n                == VerifyMode.CERT_NONE\n            )\n\n    async def test_client_verify_resyncs_existing_oauth_on_transport(self):\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            auth=\"oauth\",\n        )\n        assert isinstance(transport.auth, OAuth)\n        # OAuth was created without verify — factory should be default\n        async with transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n                != VerifyMode.CERT_NONE\n            )\n\n        # Now wrap in Client with verify=False — should resync OAuth\n        client = Client(transport, verify=False)\n        assert isinstance(client.transport.auth, OAuth)\n        async with client.transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode\n                == VerifyMode.CERT_NONE\n            )\n\n    async def test_client_verify_overrides_transport_verify_in_oauth(self):\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=False,\n            auth=\"oauth\",\n        )\n        assert isinstance(transport.auth, OAuth)\n        # OAuth should initially have verify=False\n        async with transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode  # type: ignore[attr-defined]\n                == VerifyMode.CERT_NONE\n            )\n\n        # Client overrides verify to True — OAuth should update\n        client = Client(transport, verify=True)\n        assert isinstance(client.transport.auth, OAuth)\n        async with client.transport.auth.httpx_client_factory() as httpx_client:\n            assert (\n                httpx_client._transport._pool._ssl_context.verify_mode\n                != VerifyMode.CERT_NONE\n            )\n\n    async def test_oauth_custom_factory_preserved_with_verify(self):\n        custom_factory = cast(\n            McpHttpClientFactory,\n            lambda **kwargs: httpx.AsyncClient(verify=False, **kwargs),\n        )\n        auth = OAuth(httpx_client_factory=custom_factory)\n        transport = StreamableHttpTransport(\n            \"https://example.com/mcp\",\n            verify=True,\n            auth=auth,\n        )\n        assert isinstance(transport.auth, OAuth)\n        assert transport.auth.httpx_client_factory is custom_factory\n\n    def test_warns_when_both_factory_and_verify_provided_streamable(self):\n        factory = cast(McpHttpClientFactory, httpx.AsyncClient)\n        with pytest.warns(UserWarning, match=\"httpx_client_factory.*takes precedence\"):\n            StreamableHttpTransport(\n                \"https://example.com/mcp\",\n                httpx_client_factory=factory,\n                verify=False,\n            )\n\n    def test_warns_when_both_factory_and_verify_provided_sse(self):\n        factory = cast(McpHttpClientFactory, httpx.AsyncClient)\n        with pytest.warns(UserWarning, match=\"httpx_client_factory.*takes precedence\"):\n            SSETransport(\n                \"https://example.com/sse\",\n                httpx_client_factory=factory,\n                verify=False,\n            )\n"
  },
  {
    "path": "tests/client/transports/test_uv_transport.py",
    "content": "import inspect\nimport sys\nimport tempfile\nfrom pathlib import Path\n\nimport pytest\n\nimport fastmcp\nfrom fastmcp.client import Client\nfrom fastmcp.client.client import CallToolResult\nfrom fastmcp.client.transports import StdioTransport, UvStdioTransport\n\n# Detect if running from dev install to use local source instead of PyPI\n_is_dev_install = \"dev\" in fastmcp.__version__\n_fastmcp_src_dir = (\n    Path(__file__).parent.parent.parent.parent if _is_dev_install else None\n)\n\n\n@pytest.mark.timeout(60)\n@pytest.mark.client_process\n@pytest.mark.skipif(\n    sys.platform == \"win32\",\n    reason=\"Windows file locking issues with uv client process cleanup\",\n)\nasync def test_uv_transport():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        script: str = inspect.cleandoc('''\n            from fastmcp import FastMCP\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def add(x: int, y: int) -> int:\n                \"\"\"Adds two numbers together\"\"\"\n                return x + y\n\n            if __name__ == \"__main__\":\n                mcp.run()\n            ''')\n        script_file: Path = Path(tmpdir) / \"uv.py\"\n        _ = script_file.write_text(script)\n\n        client: Client[UvStdioTransport] = Client(\n            transport=UvStdioTransport(command=str(script_file), keep_alive=False)\n        )\n\n        async with client:\n            result: CallToolResult = await client.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n            sum: int = result.data  # pyright: ignore[reportAny]\n\n        # Explicitly close the transport to ensure subprocess cleanup\n        await client.transport.close()\n        assert sum == 3\n\n\n@pytest.mark.timeout(60)\n@pytest.mark.client_process\n@pytest.mark.skipif(\n    sys.platform == \"win32\",\n    reason=\"Windows file locking issues with uv client process cleanup\",\n)\nasync def test_uv_transport_module():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        module_dir = Path(tmpdir) / \"my_module\"\n        module_dir.mkdir()\n        module_script = inspect.cleandoc('''\n            from fastmcp import FastMCP\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def add(x: int, y: int) -> int:\n                \"\"\"Adds two numbers together\"\"\"\n                return x + y\n            ''')\n        script_file: Path = module_dir / \"module.py\"\n        _ = script_file.write_text(module_script)\n\n        main_script: str = inspect.cleandoc(\"\"\"\n            from .module import mcp\n            mcp.run()\n        \"\"\")\n        main_file = module_dir / \"__main__.py\"\n        _ = main_file.write_text(main_script)\n\n        # In dev installs, use --with-editable to install local source.\n        # In releases, use --with to install from PyPI.\n        if _is_dev_install and _fastmcp_src_dir:\n            transport: StdioTransport = StdioTransport(\n                command=\"uv\",\n                args=[\n                    \"run\",\n                    \"--directory\",\n                    tmpdir,\n                    \"--with-editable\",\n                    str(_fastmcp_src_dir),\n                    \"--module\",\n                    \"my_module\",\n                ],\n                keep_alive=False,\n            )\n        else:\n            transport = UvStdioTransport(\n                with_packages=[\"fastmcp\"],\n                command=\"my_module\",\n                module=True,\n                project_directory=Path(tmpdir),\n                keep_alive=False,\n            )\n\n        client: Client[StdioTransport] = Client(transport=transport)\n\n        async with client:\n            result: CallToolResult = await client.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n            sum: int = result.data  # pyright: ignore[reportAny]\n\n        # Explicitly close the transport to ensure subprocess cleanup\n        await client.transport.close()\n        assert sum == 3\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import asyncio\nimport logging\nimport secrets\nimport socket\nimport sys\nfrom collections.abc import Callable, Generator\nfrom datetime import timedelta\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom opentelemetry import trace\nfrom opentelemetry.sdk.trace import TracerProvider\nfrom opentelemetry.sdk.trace.export import SimpleSpanProcessor\nfrom opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter\n\nfrom fastmcp.utilities.tests import temporary_settings\n\n# Use SelectorEventLoop on Windows to avoid ProactorEventLoop crashes\n# See: https://github.com/python/cpython/issues/116773\nif sys.platform == \"win32\":\n    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())\n\n\ndef pytest_collection_modifyitems(items):\n    \"\"\"Automatically mark tests in integration_tests folder with 'integration' marker.\"\"\"\n    for item in items:\n        # Check if the test is in the integration_tests folder\n        if \"integration_tests\" in str(item.fspath):\n            item.add_marker(pytest.mark.integration)\n\n\n@pytest.fixture(autouse=True)\ndef import_rich_rule():\n    # What a hack\n    import rich.rule  # noqa: F401\n\n    yield\n\n\n@pytest.fixture(autouse=True)\ndef enable_fastmcp_logger_propagation(caplog):\n    \"\"\"Enable propagation on FastMCP root logger so caplog captures FastMCP log messages.\n\n    FastMCP loggers have propagate=False by default, which prevents messages from\n    reaching pytest's caplog handler (attached to root logger). This fixture\n    temporarily enables propagation on the FastMCP root logger so FastMCP logs\n    are captured in tests.\n    \"\"\"\n    root_logger = logging.getLogger(\"fastmcp\")\n    original_propagate = root_logger.propagate\n    root_logger.propagate = True\n\n    yield\n\n    root_logger.propagate = original_propagate\n\n\n@pytest.fixture(autouse=True)\ndef isolate_settings_home(tmp_path: Path):\n    \"\"\"Ensure each test uses an isolated settings.home directory.\n\n    This prevents file locking issues when multiple tests share the same\n    storage directory in settings.home / \"oauth-proxy\".\n\n    Also sets a fast Docket polling interval for tests — the default 50ms\n    is fine for production but still adds ~25ms average pickup latency per\n    task. 10ms makes task tests near-instant.\n    \"\"\"\n    test_home = tmp_path / \"fastmcp-test-home\"\n    test_home.mkdir(exist_ok=True)\n\n    with temporary_settings(\n        home=test_home,\n        docket__minimum_check_interval=timedelta(milliseconds=10),\n        docket__url=f\"memory://{secrets.token_hex(4)}\",\n        client_disconnect_timeout=1,\n    ):\n        yield\n\n\ndef get_fn_name(fn: Callable[..., Any]) -> str:\n    return fn.__name__  # ty: ignore[unresolved-attribute]\n\n\n@pytest.fixture\ndef worker_id(request):\n    \"\"\"Get the xdist worker ID, or 'master' if not using xdist.\"\"\"\n    return getattr(request.config, \"workerinput\", {}).get(\"workerid\", \"master\")\n\n\n@pytest.fixture\ndef free_port():\n    \"\"\"Get a free port for the test to use.\"\"\"\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n        s.bind((\"127.0.0.1\", 0))\n        s.listen(1)\n        port = s.getsockname()[1]\n    return port\n\n\n@pytest.fixture\ndef free_port_factory(worker_id):\n    \"\"\"Factory to get free ports that tracks used ports per test session.\"\"\"\n    used_ports = set()\n\n    def get_port():\n        while True:\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                s.bind((\"127.0.0.1\", 0))\n                s.listen(1)\n                port = s.getsockname()[1]\n                if port not in used_ports:\n                    used_ports.add(port)\n                    return port\n\n    return get_port\n\n\n@pytest.fixture(scope=\"session\")\ndef otel_trace_provider() -> Generator[\n    tuple[TracerProvider, InMemorySpanExporter], None, None\n]:\n    \"\"\"Configure OTEL SDK with in-memory span exporter for testing.\n\n    Session-scoped because TracerProvider can only be set once per process.\n    \"\"\"\n    exporter = InMemorySpanExporter()\n    provider = TracerProvider()\n    provider.add_span_processor(SimpleSpanProcessor(exporter))\n    trace.set_tracer_provider(provider)\n    yield provider, exporter\n\n\n@pytest.fixture\ndef trace_exporter(\n    otel_trace_provider: tuple[TracerProvider, InMemorySpanExporter],\n) -> Generator[InMemorySpanExporter, None, None]:\n    \"\"\"Get the span exporter and clear it between tests.\"\"\"\n    _, exporter = otel_trace_provider\n    exporter.clear()\n    yield exporter\n    exporter.clear()\n\n\n@pytest.fixture\ndef fastmcp_server():\n    \"\"\"Fixture that creates a FastMCP server with tools, resources, and prompts.\"\"\"\n    import asyncio\n    import json\n\n    from fastmcp import FastMCP\n\n    server = FastMCP(\"TestServer\")\n\n    # Add a tool\n    @server.tool\n    def greet(name: str) -> str:\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    # Add a second tool\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @server.tool\n    async def sleep(seconds: float) -> str:\n        \"\"\"Sleep for a given number of seconds.\"\"\"\n        await asyncio.sleep(seconds)\n        return f\"Slept for {seconds} seconds\"\n\n    # Add a resource (return JSON string for proper typing)\n    @server.resource(uri=\"data://users\")\n    async def get_users() -> str:\n        return json.dumps([\"Alice\", \"Bob\", \"Charlie\"], separators=(\",\", \":\"))\n\n    # Add a resource template (return JSON string for proper typing)\n    @server.resource(uri=\"data://user/{user_id}\")\n    async def get_user(user_id: str) -> str:\n        return json.dumps(\n            {\"id\": user_id, \"name\": f\"User {user_id}\", \"active\": True},\n            separators=(\",\", \":\"),\n        )\n\n    # Add a prompt\n    @server.prompt\n    def welcome(name: str) -> str:\n        \"\"\"Example greeting prompt.\"\"\"\n        return f\"Welcome to FastMCP, {name}!\"\n\n    return server\n\n\n@pytest.fixture\ndef tool_server():\n    \"\"\"Fixture that creates a FastMCP server with comprehensive tool set for provider tests.\"\"\"\n    import base64\n\n    from mcp.types import (\n        BlobResourceContents,\n        EmbeddedResource,\n        ImageContent,\n        TextContent,\n    )\n    from pydantic import AnyUrl\n\n    from fastmcp import FastMCP\n    from fastmcp.utilities.types import Audio, File, Image\n\n    mcp = FastMCP()\n\n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        return x + y\n\n    @mcp.tool\n    def list_tool() -> list[str | int]:\n        return [\"x\", 2]\n\n    @mcp.tool\n    def error_tool() -> None:\n        raise ValueError(\"Test error\")\n\n    @mcp.tool\n    def image_tool(path: str) -> Image:\n        return Image(path)\n\n    @mcp.tool\n    def audio_tool(path: str) -> Audio:\n        return Audio(path)\n\n    @mcp.tool\n    def file_tool(path: str) -> File:\n        return File(path)\n\n    @mcp.tool\n    def mixed_content_tool() -> list[TextContent | ImageContent | EmbeddedResource]:\n        return [\n            TextContent(type=\"text\", text=\"Hello\"),\n            ImageContent(type=\"image\", data=\"abc\", mimeType=\"application/octet-stream\"),\n            EmbeddedResource(\n                type=\"resource\",\n                resource=BlobResourceContents(\n                    blob=base64.b64encode(b\"abc\").decode(),\n                    mimeType=\"application/octet-stream\",\n                    uri=AnyUrl(\"file:///test.bin\"),\n                ),\n            ),\n        ]\n\n    @mcp.tool(output_schema=None)\n    def mixed_list_fn(image_path: str) -> list:\n        return [\n            \"text message\",\n            Image(image_path),\n            {\"key\": \"value\"},\n            TextContent(type=\"text\", text=\"direct content\"),\n        ]\n\n    @mcp.tool(output_schema=None)\n    def mixed_audio_list_fn(audio_path: str) -> list:\n        return [\n            \"text message\",\n            Audio(audio_path),\n            {\"key\": \"value\"},\n            TextContent(type=\"text\", text=\"direct content\"),\n        ]\n\n    @mcp.tool(output_schema=None)\n    def mixed_file_list_fn(file_path: str) -> list:\n        return [\n            \"text message\",\n            File(file_path),\n            {\"key\": \"value\"},\n            TextContent(type=\"text\", text=\"direct content\"),\n        ]\n\n    @mcp.tool\n    def file_text_tool() -> File:\n        return File(data=b\"hello world\", format=\"plain\")\n\n    return mcp\n\n\n@pytest.fixture\ndef tagged_resources_server():\n    \"\"\"Fixture that creates a FastMCP server with tagged resources and templates.\"\"\"\n    import json\n\n    from fastmcp import FastMCP\n\n    server = FastMCP(\"TaggedResourcesServer\")\n\n    # Add a resource with tags\n    @server.resource(\n        uri=\"data://tagged\", tags={\"test\", \"metadata\"}, description=\"A tagged resource\"\n    )\n    async def get_tagged_data() -> str:\n        return json.dumps({\"type\": \"tagged_data\"}, separators=(\",\", \":\"))\n\n    # Add a resource template with tags\n    @server.resource(\n        uri=\"template://{id}\",\n        tags={\"template\", \"parameterized\"},\n        description=\"A tagged template\",\n    )\n    async def get_template_data(id: str) -> str:\n        return json.dumps({\"id\": id, \"type\": \"template_data\"}, separators=(\",\", \":\"))\n\n    return server\n"
  },
  {
    "path": "tests/contrib/__init__.py",
    "content": "# This file makes Python treat the directory as a package.\n"
  },
  {
    "path": "tests/contrib/test_bulk_tool_caller.py",
    "content": "from typing import Any\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.bulk_tool_caller.bulk_tool_caller import (\n    BulkToolCaller,\n    CallToolRequest,\n    CallToolRequestResult,\n)\nfrom fastmcp.tools.base import Tool\n\n\nclass ToolException(Exception):\n    \"\"\"Custom exception for tool errors.\"\"\"\n\n    pass\n\n\nasync def error_tool(arg1: str) -> dict[str, Any]:\n    \"\"\"A tool that raises an error for testing purposes.\"\"\"\n    raise ToolException(f\"Error in tool with arg1: {arg1}\")\n\n\ndef error_tool_result_factory(arg1: str) -> CallToolRequestResult:\n    \"\"\"Generates the expected error result for error_tool.\"\"\"\n    # Mimic the error message format generated by BulkToolCaller when catching ToolException\n    formatted_error_text = (\n        \"Error calling tool 'error_tool': Error in tool with arg1: \" + arg1\n    )\n    return CallToolRequestResult(\n        isError=True,\n        content=[TextContent(text=formatted_error_text, type=\"text\")],\n        tool=\"error_tool\",\n        arguments={\"arg1\": arg1},\n    )\n\n\nasync def echo_tool(arg1: str) -> str:\n    \"\"\"A simple tool that echoes arguments or raises an error.\"\"\"\n    return arg1\n\n\ndef echo_tool_result_factory(arg1: str) -> CallToolRequestResult:\n    \"\"\"A tool that returns a result based on the input arguments.\"\"\"\n    return CallToolRequestResult(\n        isError=False,\n        content=[TextContent(text=f\"{arg1}\", type=\"text\")],\n        tool=\"echo_tool\",\n        arguments={\"arg1\": arg1},\n    )\n\n\nasync def no_return_tool(arg1: str) -> None:\n    \"\"\"A simple tool that echoes arguments or raises an error.\"\"\"\n\n\ndef no_return_tool_result_factory(arg1: str) -> CallToolRequestResult:\n    \"\"\"A tool that returns a result based on the input arguments.\"\"\"\n    return CallToolRequestResult(\n        isError=False,\n        content=[],\n        tool=\"no_return_tool\",\n        arguments={\"arg1\": arg1},\n    )\n\n\n@pytest.fixture\ndef live_server_with_tool() -> FastMCP:\n    \"\"\"Fixture to create a FastMCP server instance with the echo_tool registered.\"\"\"\n    server = FastMCP()\n    server.add_tool(Tool.from_function(echo_tool))\n    server.add_tool(Tool.from_function(error_tool))\n    server.add_tool(Tool.from_function(no_return_tool))\n    return server\n\n\n@pytest.fixture\ndef bulk_caller_live(live_server_with_tool: FastMCP) -> BulkToolCaller:\n    \"\"\"Fixture to create a BulkToolCaller instance connected to the live server.\"\"\"\n    bulk_tool_caller = BulkToolCaller()\n    bulk_tool_caller.register_tools(live_server_with_tool)\n    return bulk_tool_caller\n\n\nECHO_TOOL_NAME = \"echo_tool\"\nERROR_TOOL_NAME = \"error_tool\"\nNO_RETURN_TOOL_NAME = \"no_return_tool\"\n\n\nasync def test_call_tool_bulk_single_success(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test single successful call via call_tool_bulk using echo_tool.\"\"\"\n\n    results = await bulk_caller_live.call_tool_bulk(\n        ECHO_TOOL_NAME, [{\"arg1\": \"value1\"}]\n    )\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"value1\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"value1\"},\n            )\n        ]\n    )\n\n\nasync def test_call_tool_bulk_multiple_success(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test multiple successful calls via call_tool_bulk using echo_tool.\"\"\"\n    results = await bulk_caller_live.call_tool_bulk(\n        ECHO_TOOL_NAME, [{\"arg1\": \"value1\"}, {\"arg1\": \"value2\"}]\n    )\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"value1\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"value1\"},\n            ),\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"value2\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"value2\"},\n            ),\n        ]\n    )\n\n\nasync def test_call_tool_bulk_error_stops(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test call_tool_bulk stops on first error using error_tool.\"\"\"\n    results = await bulk_caller_live.call_tool_bulk(\n        ERROR_TOOL_NAME,\n        [{\"arg1\": \"error_value\"}, {\"arg1\": \"value2\"}],\n        continue_on_error=False,\n    )\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=\"Error calling tool 'error_tool': Error in tool with arg1: error_value\",\n                    )\n                ],\n                isError=True,\n                tool=\"error_tool\",\n                arguments={\"arg1\": \"error_value\"},\n            )\n        ]\n    )\n\n\nasync def test_call_tool_bulk_error_continues(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test call_tool_bulk continues on error using error_tool and echo_tool.\"\"\"\n\n    tool_calls = [\n        CallToolRequest(tool=ERROR_TOOL_NAME, arguments={\"arg1\": \"error_value\"}),\n        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={\"arg1\": \"success_value\"}),\n    ]\n\n    results = await bulk_caller_live.call_tools_bulk(tool_calls, continue_on_error=True)\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=\"Error calling tool 'error_tool': Error in tool with arg1: error_value\",\n                    )\n                ],\n                isError=True,\n                tool=\"error_tool\",\n                arguments={\"arg1\": \"error_value\"},\n            ),\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"success_value\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"success_value\"},\n            ),\n        ]\n    )\n\n\nasync def test_call_tools_bulk_single_success(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test single successful call via call_tools_bulk using echo_tool.\"\"\"\n    tool_calls = [CallToolRequest(tool=ECHO_TOOL_NAME, arguments={\"arg1\": \"value1\"})]\n\n    results = await bulk_caller_live.call_tools_bulk(tool_calls)\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"value1\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"value1\"},\n            )\n        ]\n    )\n\n\nasync def test_call_tools_bulk_multiple_success(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test multiple successful calls via call_tools_bulk with different tools.\"\"\"\n    tool_calls = [\n        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={\"arg1\": \"echo_value\"}),\n        CallToolRequest(\n            tool=NO_RETURN_TOOL_NAME, arguments={\"arg1\": \"no_return_value\"}\n        ),\n    ]\n\n    results = await bulk_caller_live.call_tools_bulk(tool_calls)\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"echo_value\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"echo_value\"},\n            ),\n            CallToolRequestResult(\n                content=[], tool=\"no_return_tool\", arguments={\"arg1\": \"no_return_value\"}\n            ),\n        ]\n    )\n\n\nasync def test_call_tools_bulk_error_stops(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test call_tools_bulk stops on first error using error_tool.\"\"\"\n    tool_calls = [\n        CallToolRequest(tool=ERROR_TOOL_NAME, arguments={\"arg1\": \"error_value\"}),\n        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={\"arg1\": \"skipped_value\"}),\n    ]\n\n    results = await bulk_caller_live.call_tools_bulk(\n        tool_calls, continue_on_error=False\n    )\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=\"Error calling tool 'error_tool': Error in tool with arg1: error_value\",\n                    )\n                ],\n                isError=True,\n                tool=\"error_tool\",\n                arguments={\"arg1\": \"error_value\"},\n            )\n        ]\n    )\n\n\nasync def test_call_tools_bulk_error_continues(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test call_tools_bulk continues on error using error_tool and echo_tool.\"\"\"\n    tool_calls = [\n        CallToolRequest(tool=ERROR_TOOL_NAME, arguments={\"arg1\": \"error_value\"}),\n        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={\"arg1\": \"success_value\"}),\n    ]\n\n    results = await bulk_caller_live.call_tools_bulk(tool_calls, continue_on_error=True)\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=\"Error calling tool 'error_tool': Error in tool with arg1: error_value\",\n                    )\n                ],\n                isError=True,\n                tool=\"error_tool\",\n                arguments={\"arg1\": \"error_value\"},\n            ),\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"success_value\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"success_value\"},\n            ),\n        ]\n    )\n\n\nasync def test_call_tools_bulk_blocks_self_invocation(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test call_tools_bulk blocks recursive calls to bulk tools.\"\"\"\n    tool_calls = [\n        CallToolRequest(tool=\"call_tools_bulk\", arguments={\"tool_calls\": []}),\n        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={\"arg1\": \"success_value\"}),\n    ]\n\n    results = await bulk_caller_live.call_tools_bulk(tool_calls, continue_on_error=True)\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=(\n                            \"BulkToolCaller cannot call itself. \"\n                            \"The tools 'call_tools_bulk' and 'call_tool_bulk' are disallowed.\"\n                        ),\n                    )\n                ],\n                isError=True,\n                tool=\"call_tools_bulk\",\n                arguments={\"tool_calls\": []},\n            ),\n            CallToolRequestResult(\n                content=[TextContent(type=\"text\", text=\"success_value\")],\n                tool=\"echo_tool\",\n                arguments={\"arg1\": \"success_value\"},\n            ),\n        ]\n    )\n\n\nasync def test_call_tool_bulk_blocks_self_invocation(bulk_caller_live: BulkToolCaller):\n    \"\"\"Test call_tool_bulk blocks recursive calls to bulk tools.\"\"\"\n\n    results = await bulk_caller_live.call_tool_bulk(\n        \"call_tool_bulk\", [{\"arg1\": \"value1\"}], continue_on_error=False\n    )\n\n    assert results == snapshot(\n        [\n            CallToolRequestResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=(\n                            \"BulkToolCaller cannot call itself. \"\n                            \"The tools 'call_tools_bulk' and 'call_tool_bulk' are disallowed.\"\n                        ),\n                    )\n                ],\n                isError=True,\n                tool=\"call_tool_bulk\",\n                arguments={\"arg1\": \"value1\"},\n            )\n        ]\n    )\n"
  },
  {
    "path": "tests/contrib/test_component_manager.py",
    "content": "import pytest\nfrom starlette import status\nfrom starlette.testclient import TestClient\n\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.component_manager import set_up_component_manager\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\n\nclass TestComponentManagementRoutes:\n    \"\"\"Test the component management routes for tools, resources, and prompts.\"\"\"\n\n    @pytest.fixture\n    def mcp(self):\n        \"\"\"Create a FastMCP server with test tools, resources, and prompts.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n        set_up_component_manager(server=mcp)\n\n        # Add a test tool\n        @mcp.tool\n        def test_tool() -> str:\n            \"\"\"Test tool for tool management routes.\"\"\"\n            return \"test_tool_result\"\n\n        # Add a test resource\n        @mcp.resource(\"data://test_resource\")\n        def test_resource() -> str:\n            \"\"\"Test resource for tool management routes.\"\"\"\n            return \"test_resource_result\"\n\n        # Add a test resource\n        @mcp.resource(\"data://test_resource/{id}\")\n        def test_template(id: str) -> dict:\n            \"\"\"Test template for tool management routes.\"\"\"\n            return {\"id\": id, \"value\": \"data\"}\n\n        # Add a test prompt\n        @mcp.prompt\n        def test_prompt() -> str:\n            \"\"\"Test prompt for tool management routes.\"\"\"\n            return \"test_prompt_result\"\n\n        return mcp\n\n    @pytest.fixture\n    def client(self, mcp):\n        \"\"\"Create a test client for the FastMCP server.\"\"\"\n        return TestClient(mcp.http_app())\n\n    async def test_enable_tool_route(self, client, mcp):\n        \"\"\"Test enabling a tool via the HTTP route.\"\"\"\n        # First disable the tool\n        mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n        # Enable the tool via the HTTP route\n        response = client.post(\"/tools/test_tool/enable\")\n\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Enabled tool: test_tool\"}\n\n        # Verify the tool is enabled\n        tools = await mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n    async def test_disable_tool_route(self, client, mcp):\n        \"\"\"Test disabling a tool via the HTTP route.\"\"\"\n        # First ensure the tool is enabled\n        tools = await mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n        # Disable the tool via the HTTP route\n        response = client.post(\"/tools/test_tool/disable\")\n\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Disabled tool: test_tool\"}\n\n        # Verify the tool is disabled\n        tools = await mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_enable_resource_route(self, client, mcp):\n        \"\"\"Test enabling a resource via the HTTP route.\"\"\"\n        # First disable the resource (can use URI as name for resources)\n        mcp.disable(names={\"data://test_resource\"}, components={\"resource\"})\n        resources = await mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n        # Enable the resource via the HTTP route\n        response = client.post(\"/resources/data://test_resource/enable\")\n\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Enabled resource: data://test_resource\"}\n\n        # Verify the resource is enabled\n        resources = await mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_disable_resource_route(self, client, mcp):\n        \"\"\"Test disabling a resource via the HTTP route.\"\"\"\n        # First ensure the resource is enabled\n        resources = await mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n        # Disable the resource via the HTTP route\n        response = client.post(\"/resources/data://test_resource/disable\")\n\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Disabled resource: data://test_resource\"}\n\n        # Verify the resource is disabled\n        resources = await mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_enable_template_route(self, client, mcp):\n        \"\"\"Test enabling a resource template via the HTTP route.\"\"\"\n        key = \"data://test_resource/{id}\"\n        mcp.disable(names={\"data://test_resource/{id}\"}, components={\"template\"})\n        templates = await mcp.list_resource_templates()\n        assert not any(t.uri_template == key for t in templates)\n        response = client.post(\"/resources/data://test_resource/{id}/enable\")\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\n            \"message\": \"Enabled resource: data://test_resource/{id}\"\n        }\n        templates = await mcp.list_resource_templates()\n        assert any(t.uri_template == key for t in templates)\n\n    async def test_disable_template_route(self, client, mcp):\n        \"\"\"Test disabling a resource template via the HTTP route.\"\"\"\n        key = \"data://test_resource/{id}\"\n        templates = await mcp.list_resource_templates()\n        assert any(t.uri_template == key for t in templates)\n        response = client.post(\"/resources/data://test_resource/{id}/disable\")\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\n            \"message\": \"Disabled resource: data://test_resource/{id}\"\n        }\n        templates = await mcp.list_resource_templates()\n        assert not any(t.uri_template == key for t in templates)\n\n    async def test_enable_prompt_route(self, client, mcp):\n        \"\"\"Test enabling a prompt via the HTTP route.\"\"\"\n        # First disable the prompt\n        mcp.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n        # Enable the prompt via the HTTP route\n        response = client.post(\"/prompts/test_prompt/enable\")\n\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Enabled prompt: test_prompt\"}\n\n        # Verify the prompt is enabled\n        prompts = await mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_disable_prompt_route(self, client, mcp):\n        \"\"\"Test disabling a prompt via the HTTP route.\"\"\"\n        # First ensure the prompt is enabled\n        prompts = await mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n        # Disable the prompt via the HTTP route\n        response = client.post(\"/prompts/test_prompt/disable\")\n\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Disabled prompt: test_prompt\"}\n\n        # Verify the prompt is disabled\n        prompts = await mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n\nclass TestAuthComponentManagementRoutes:\n    \"\"\"Test the component management routes with authentication for tools, resources, and prompts.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        # Generate a key pair and create an auth provider\n        key_pair = RSAKeyPair.generate()\n        self.auth = JWTVerifier(\n            public_key=key_pair.public_key,\n            issuer=\"https://dev.example.com\",\n            audience=\"my-dev-server\",\n        )\n        self.mcp = FastMCP(\"TestServerWithAuth\", auth=self.auth)\n        set_up_component_manager(\n            server=self.mcp, required_scopes=[\"tool:write\", \"tool:read\"]\n        )\n        self.token = key_pair.create_token(\n            subject=\"dev-user\",\n            issuer=\"https://dev.example.com\",\n            audience=\"my-dev-server\",\n            scopes=[\"tool:write\", \"tool:read\"],\n        )\n        self.token_without_scopes = key_pair.create_token(\n            subject=\"dev-user\",\n            issuer=\"https://dev.example.com\",\n            audience=\"my-dev-server\",\n            scopes=[\"tool:read\"],\n        )\n\n        # Add test components\n        @self.mcp.tool\n        def test_tool() -> str:\n            \"\"\"Test tool for auth testing.\"\"\"\n            return \"test_tool_result\"\n\n        @self.mcp.resource(\"data://test_resource\")\n        def test_resource() -> str:\n            \"\"\"Test resource for auth testing.\"\"\"\n            return \"test_resource_result\"\n\n        @self.mcp.prompt\n        def test_prompt() -> str:\n            \"\"\"Test prompt for auth testing.\"\"\"\n            return \"test_prompt_result\"\n\n        # Create test client\n        self.client = TestClient(self.mcp.http_app())\n\n    async def test_unauthorized_enable_tool(self):\n        \"\"\"Test that unauthenticated requests to enable a tool are rejected.\"\"\"\n        self.mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n        response = self.client.post(\"/tools/test_tool/enable\")\n        assert response.status_code == 401\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_authorized_enable_tool(self):\n        \"\"\"Test that authenticated requests to enable a tool are allowed.\"\"\"\n        self.mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n        response = self.client.post(\n            \"/tools/test_tool/enable\", headers={\"Authorization\": \"Bearer \" + self.token}\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Enabled tool: test_tool\"}\n        tools = await self.mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n    async def test_unauthorized_disable_tool(self):\n        \"\"\"Test that unauthenticated requests to disable a tool are rejected.\"\"\"\n        tools = await self.mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n        response = self.client.post(\"/tools/test_tool/disable\")\n        assert response.status_code == 401\n        tools = await self.mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n    async def test_authorized_disable_tool(self):\n        \"\"\"Test that authenticated requests to disable a tool are allowed.\"\"\"\n        tools = await self.mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n        response = self.client.post(\n            \"/tools/test_tool/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Disabled tool: test_tool\"}\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_forbidden_enable_tool(self):\n        \"\"\"Test that requests with insufficient scopes are rejected.\"\"\"\n        self.mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n        response = self.client.post(\n            \"/tools/test_tool/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token_without_scopes},\n        )\n        assert response.status_code == 403\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_authorized_enable_resource(self):\n        \"\"\"Test that authenticated requests to enable a resource are allowed.\"\"\"\n        self.mcp.disable(names={\"data://test_resource\"}, components={\"resource\"})\n        resources = await self.mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n        response = self.client.post(\n            \"/resources/data://test_resource/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Enabled resource: data://test_resource\"}\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_unauthorized_disable_resource(self):\n        \"\"\"Test that unauthenticated requests to disable a resource are rejected.\"\"\"\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n        response = self.client.post(\"/resources/data://test_resource/disable\")\n        assert response.status_code == 401\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_forbidden_enable_resource(self):\n        \"\"\"Test that requests with insufficient scopes are rejected.\"\"\"\n        self.mcp.disable(names={\"data://test_resource\"}, components={\"resource\"})\n        resources = await self.mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n        response = self.client.post(\n            \"/resources/data://test_resource/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token_without_scopes},\n        )\n        assert response.status_code == 403\n        resources = await self.mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_authorized_disable_resource(self):\n        \"\"\"Test that authenticated requests to disable a resource are allowed.\"\"\"\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n        response = self.client.post(\n            \"/resources/data://test_resource/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Disabled resource: data://test_resource\"}\n        resources = await self.mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_unauthorized_enable_prompt(self):\n        \"\"\"Test that unauthenticated requests to enable a prompt are rejected.\"\"\"\n        self.mcp.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n        response = self.client.post(\"/prompts/test_prompt/enable\")\n        assert response.status_code == 401\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_authorized_enable_prompt(self):\n        \"\"\"Test that authenticated requests to enable a prompt are allowed.\"\"\"\n        self.mcp.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n        response = self.client.post(\n            \"/prompts/test_prompt/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Enabled prompt: test_prompt\"}\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_unauthorized_disable_prompt(self):\n        \"\"\"Test that unauthenticated requests to disable a prompt are rejected.\"\"\"\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n        response = self.client.post(\"/prompts/test_prompt/disable\")\n        assert response.status_code == 401\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_forbidden_disable_prompt(self):\n        \"\"\"Test that requests with insufficient scopes are rejected.\"\"\"\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n        response = self.client.post(\n            \"/prompts/test_prompt/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token_without_scopes},\n        )\n        assert response.status_code == 403\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_authorized_disable_prompt(self):\n        \"\"\"Test that authenticated requests to disable a prompt are allowed.\"\"\"\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n        response = self.client.post(\n            \"/prompts/test_prompt/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Disabled prompt: test_prompt\"}\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n\nclass TestComponentManagerWithPath:\n    \"\"\"Test component manager routes when mounted at a custom path.\"\"\"\n\n    @pytest.fixture\n    def mcp_with_path(self):\n        mcp = FastMCP(\"TestServerWithPath\")\n        set_up_component_manager(server=mcp, path=\"/test\")\n\n        @mcp.tool\n        def test_tool() -> str:\n            return \"test_tool_result\"\n\n        @mcp.resource(\"data://test_resource\")\n        def test_resource() -> str:\n            return \"test_resource_result\"\n\n        @mcp.prompt\n        def test_prompt() -> str:\n            return \"test_prompt_result\"\n\n        return mcp\n\n    @pytest.fixture\n    def client_with_path(self, mcp_with_path):\n        return TestClient(mcp_with_path.http_app())\n\n    async def test_enable_tool_route_with_path(self, client_with_path, mcp_with_path):\n        mcp_with_path.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await mcp_with_path.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n        response = client_with_path.post(\"/test/tools/test_tool/enable\")\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Enabled tool: test_tool\"}\n        tools = await mcp_with_path.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n    async def test_disable_resource_route_with_path(\n        self, client_with_path, mcp_with_path\n    ):\n        resources = await mcp_with_path.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n        response = client_with_path.post(\"/test/resources/data://test_resource/disable\")\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Disabled resource: data://test_resource\"}\n        resources = await mcp_with_path.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_enable_prompt_route_with_path(self, client_with_path, mcp_with_path):\n        mcp_with_path.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await mcp_with_path.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n        response = client_with_path.post(\"/test/prompts/test_prompt/enable\")\n        assert response.status_code == status.HTTP_200_OK\n        assert response.json() == {\"message\": \"Enabled prompt: test_prompt\"}\n        prompts = await mcp_with_path.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n\n\nclass TestComponentManagerWithPathAuth:\n    \"\"\"Test component manager routes with auth when mounted at a custom path.\"\"\"\n\n    def setup_method(self):\n        # Generate a key pair and create an auth provider\n        key_pair = RSAKeyPair.generate()\n        self.auth = JWTVerifier(\n            public_key=key_pair.public_key,\n            issuer=\"https://dev.example.com\",\n            audience=\"my-dev-server\",\n        )\n        self.mcp = FastMCP(\"TestServerWithPathAuth\", auth=self.auth)\n        set_up_component_manager(\n            server=self.mcp, path=\"/test\", required_scopes=[\"tool:write\", \"tool:read\"]\n        )\n        self.token = key_pair.create_token(\n            subject=\"dev-user\",\n            issuer=\"https://dev.example.com\",\n            audience=\"my-dev-server\",\n            scopes=[\"tool:read\", \"tool:write\"],\n        )\n        self.token_without_scopes = key_pair.create_token(\n            subject=\"dev-user\",\n            issuer=\"https://dev.example.com\",\n            audience=\"my-dev-server\",\n            scopes=[],\n        )\n\n        @self.mcp.tool\n        def test_tool() -> str:\n            return \"test_tool_result\"\n\n        @self.mcp.resource(\"data://test_resource\")\n        def test_resource() -> str:\n            return \"test_resource_result\"\n\n        @self.mcp.prompt\n        def test_prompt() -> str:\n            return \"test_prompt_result\"\n\n        self.client = TestClient(self.mcp.http_app())\n\n    async def test_unauthorized_enable_tool(self):\n        self.mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n        response = self.client.post(\"/test/tools/test_tool/enable\")\n        assert response.status_code == 401\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_forbidden_enable_tool(self):\n        self.mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n        response = self.client.post(\n            \"/test/tools/test_tool/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token_without_scopes},\n        )\n        assert response.status_code == 403\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_authorized_enable_tool(self):\n        self.mcp.disable(names={\"test_tool\"}, components={\"tool\"})\n        tools = await self.mcp.list_tools()\n        assert not any(t.name == \"test_tool\" for t in tools)\n        response = self.client.post(\n            \"/test/tools/test_tool/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Enabled tool: test_tool\"}\n        tools = await self.mcp.list_tools()\n        assert any(t.name == \"test_tool\" for t in tools)\n\n    async def test_unauthorized_disable_resource(self):\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n        response = self.client.post(\"/test/resources/data://test_resource/disable\")\n        assert response.status_code == 401\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_forbidden_disable_resource(self):\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n        response = self.client.post(\n            \"/test/resources/data://test_resource/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token_without_scopes},\n        )\n        assert response.status_code == 403\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_authorized_disable_resource(self):\n        resources = await self.mcp.list_resources()\n        assert any(str(r.uri) == \"data://test_resource\" for r in resources)\n        response = self.client.post(\n            \"/test/resources/data://test_resource/disable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Disabled resource: data://test_resource\"}\n        resources = await self.mcp.list_resources()\n        assert not any(str(r.uri) == \"data://test_resource\" for r in resources)\n\n    async def test_unauthorized_enable_prompt(self):\n        self.mcp.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n        response = self.client.post(\"/test/prompts/test_prompt/enable\")\n        assert response.status_code == 401\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_forbidden_enable_prompt(self):\n        self.mcp.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n        response = self.client.post(\n            \"/test/prompts/test_prompt/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token_without_scopes},\n        )\n        assert response.status_code == 403\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n\n    async def test_authorized_enable_prompt(self):\n        self.mcp.disable(names={\"test_prompt\"}, components={\"prompt\"})\n        prompts = await self.mcp.list_prompts()\n        assert not any(p.name == \"test_prompt\" for p in prompts)\n        response = self.client.post(\n            \"/test/prompts/test_prompt/enable\",\n            headers={\"Authorization\": \"Bearer \" + self.token},\n        )\n        assert response.status_code == 200\n        assert response.json() == {\"message\": \"Enabled prompt: test_prompt\"}\n        prompts = await self.mcp.list_prompts()\n        assert any(p.name == \"test_prompt\" for p in prompts)\n"
  },
  {
    "path": "tests/contrib/test_mcp_mixin.py",
    "content": "\"\"\"Tests for the MCPMixin class.\"\"\"\n\nimport inspect\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.mcp_mixin import (\n    MCPMixin,\n    mcp_prompt,\n    mcp_resource,\n    mcp_tool,\n)\nfrom fastmcp.contrib.mcp_mixin.mcp_mixin import (\n    _DEFAULT_SEPARATOR_PROMPT,\n    _DEFAULT_SEPARATOR_RESOURCE,\n    _DEFAULT_SEPARATOR_TOOL,\n    _PROMPT_VALID_KWARGS,\n    _RESOURCE_VALID_KWARGS,\n    _TOOL_VALID_KWARGS,\n)\n\n\nclass TestMCPMixin:\n    \"\"\"Test suite for MCPMixin functionality.\"\"\"\n\n    def test_initialization(self):\n        \"\"\"Test that a class inheriting MCPMixin can be initialized.\"\"\"\n\n        class MyMixin(MCPMixin):\n            pass\n\n        instance = MyMixin()\n        assert instance is not None\n\n    # --- Tool Registration Tests ---\n    @pytest.mark.parametrize(\n        \"prefix, separator, expected_key, unexpected_key\",\n        [\n            (\n                None,\n                _DEFAULT_SEPARATOR_TOOL,\n                \"sample_tool\",\n                f\"None{_DEFAULT_SEPARATOR_TOOL}sample_tool\",\n            ),\n            (\n                \"pref\",\n                _DEFAULT_SEPARATOR_TOOL,\n                f\"pref{_DEFAULT_SEPARATOR_TOOL}sample_tool\",\n                \"sample_tool\",\n            ),\n            (\n                \"pref\",\n                \"-\",\n                \"pref-sample_tool\",\n                f\"pref{_DEFAULT_SEPARATOR_TOOL}sample_tool\",\n            ),\n        ],\n        ids=[\"No prefix\", \"Default separator\", \"Custom separator\"],\n    )\n    async def test_tool_registration(\n        self, prefix, separator, expected_key, unexpected_key\n    ):\n        \"\"\"Test tool registration with prefix and separator variations.\"\"\"\n        mcp = FastMCP()\n\n        class MyToolMixin(MCPMixin):\n            @mcp_tool()\n            def sample_tool(self):\n                pass\n\n        instance = MyToolMixin()\n        instance.register_tools(mcp, prefix=prefix, separator=separator)\n\n        registered_tools = await mcp.list_tools()\n        assert any(t.name == expected_key for t in registered_tools)\n        assert not any(t.name == unexpected_key for t in registered_tools)\n\n    @pytest.mark.parametrize(\n        \"prefix, separator, expected_uri_key, expected_name, unexpected_uri_key\",\n        [\n            (\n                None,\n                _DEFAULT_SEPARATOR_RESOURCE,\n                \"test://resource\",\n                \"sample_resource\",\n                f\"None{_DEFAULT_SEPARATOR_RESOURCE}test://resource\",\n            ),\n            (\n                \"pref\",\n                _DEFAULT_SEPARATOR_RESOURCE,\n                f\"pref{_DEFAULT_SEPARATOR_RESOURCE}test://resource\",\n                f\"pref{_DEFAULT_SEPARATOR_RESOURCE}sample_resource\",\n                \"test://resource\",\n            ),\n            (\n                \"pref\",\n                \"fff\",\n                \"prefffftest://resource\",\n                \"preffffsample_resource\",\n                f\"pref{_DEFAULT_SEPARATOR_RESOURCE}test://resource\",\n            ),\n        ],\n        ids=[\"No prefix\", \"Default separator\", \"Custom separator\"],\n    )\n    async def test_resource_registration(\n        self, prefix, separator, expected_uri_key, expected_name, unexpected_uri_key\n    ):\n        \"\"\"Test resource registration with prefix and separator variations.\"\"\"\n        mcp = FastMCP()\n\n        class MyResourceMixin(MCPMixin):\n            @mcp_resource(uri=\"test://resource\")\n            def sample_resource(self):\n                pass\n\n        instance = MyResourceMixin()\n        instance.register_resources(mcp, prefix=prefix, separator=separator)\n\n        registered_resources = await mcp.list_resources()\n        assert any(str(r.uri) == expected_uri_key for r in registered_resources)\n        resource = next(\n            r for r in registered_resources if str(r.uri) == expected_uri_key\n        )\n        assert resource.name == expected_name\n        assert not any(str(r.uri) == unexpected_uri_key for r in registered_resources)\n\n    @pytest.mark.parametrize(\n        \"prefix, separator, expected_name, unexpected_name\",\n        [\n            (\n                None,\n                _DEFAULT_SEPARATOR_PROMPT,\n                \"sample_prompt\",\n                f\"None{_DEFAULT_SEPARATOR_PROMPT}sample_prompt\",\n            ),\n            (\n                \"pref\",\n                _DEFAULT_SEPARATOR_PROMPT,\n                f\"pref{_DEFAULT_SEPARATOR_PROMPT}sample_prompt\",\n                \"sample_prompt\",\n            ),\n            (\n                \"pref\",\n                \":\",\n                \"pref:sample_prompt\",\n                f\"pref{_DEFAULT_SEPARATOR_PROMPT}sample_prompt\",\n            ),\n        ],\n        ids=[\"No prefix\", \"Default separator\", \"Custom separator\"],\n    )\n    async def test_prompt_registration(\n        self, prefix, separator, expected_name, unexpected_name\n    ):\n        \"\"\"Test prompt registration with prefix and separator variations.\"\"\"\n        mcp = FastMCP()\n\n        class MyPromptMixin(MCPMixin):\n            @mcp_prompt()\n            def sample_prompt(self):\n                pass\n\n        instance = MyPromptMixin()\n        instance.register_prompts(mcp, prefix=prefix, separator=separator)\n\n        prompts = await mcp.list_prompts()\n        assert any(p.name == expected_name for p in prompts)\n        assert not any(p.name == unexpected_name for p in prompts)\n\n    async def test_register_all_no_prefix(self):\n        \"\"\"Test register_all method registers all types without a prefix.\"\"\"\n        mcp = FastMCP()\n\n        class MyFullMixin(MCPMixin):\n            @mcp_tool()\n            def tool_all(self):\n                pass\n\n            @mcp_resource(uri=\"res://all\")\n            def resource_all(self):\n                pass\n\n            @mcp_prompt()\n            def prompt_all(self):\n                pass\n\n        instance = MyFullMixin()\n        instance.register_all(mcp)\n\n        tools = await mcp.list_tools()\n        resources = await mcp.list_resources()\n        prompts = await mcp.list_prompts()\n\n        assert any(t.name == \"tool_all\" for t in tools)\n        assert any(str(r.uri) == \"res://all\" for r in resources)\n        assert any(p.name == \"prompt_all\" for p in prompts)\n\n    async def test_register_all_with_prefix_default_separators(self):\n        \"\"\"Test register_all method registers all types with a prefix and default separators.\"\"\"\n        mcp = FastMCP()\n\n        class MyFullMixinPrefixed(MCPMixin):\n            @mcp_tool()\n            def tool_all_p(self):\n                pass\n\n            @mcp_resource(uri=\"res://all_p\")\n            def resource_all_p(self):\n                pass\n\n            @mcp_prompt()\n            def prompt_all_p(self):\n                pass\n\n        instance = MyFullMixinPrefixed()\n        instance.register_all(mcp, prefix=\"all\")\n\n        tools = await mcp.list_tools()\n        resources = await mcp.list_resources()\n        prompts = await mcp.list_prompts()\n\n        assert any(t.name == f\"all{_DEFAULT_SEPARATOR_TOOL}tool_all_p\" for t in tools)\n        assert any(\n            str(r.uri) == f\"all{_DEFAULT_SEPARATOR_RESOURCE}res://all_p\"\n            for r in resources\n        )\n        assert any(\n            p.name == f\"all{_DEFAULT_SEPARATOR_PROMPT}prompt_all_p\" for p in prompts\n        )\n\n    async def test_register_all_with_prefix_custom_separators(self):\n        \"\"\"Test register_all method registers all types with a prefix and custom separators.\"\"\"\n        mcp = FastMCP()\n\n        class MyFullMixinCustomSep(MCPMixin):\n            @mcp_tool()\n            def tool_cust(self):\n                pass\n\n            @mcp_resource(uri=\"res://cust\")\n            def resource_cust(self):\n                pass\n\n            @mcp_prompt()\n            def prompt_cust(self):\n                pass\n\n        instance = MyFullMixinCustomSep()\n        instance.register_all(\n            mcp,\n            prefix=\"cust\",\n            tool_separator=\"-\",\n            resource_separator=\"::\",\n            prompt_separator=\".\",\n        )\n\n        tools = await mcp.list_tools()\n        resources = await mcp.list_resources()\n        prompts = await mcp.list_prompts()\n\n        assert any(t.name == \"cust-tool_cust\" for t in tools)\n        assert any(str(r.uri) == \"cust::res://cust\" for r in resources)\n        assert any(p.name == \"cust.prompt_cust\" for p in prompts)\n\n        # Check default separators weren't used\n        assert not any(\n            t.name == f\"cust{_DEFAULT_SEPARATOR_TOOL}tool_cust\" for t in tools\n        )\n        assert not any(\n            str(r.uri) == f\"cust{_DEFAULT_SEPARATOR_RESOURCE}res://cust\"\n            for r in resources\n        )\n        assert not any(\n            p.name == f\"cust{_DEFAULT_SEPARATOR_PROMPT}prompt_cust\" for p in prompts\n        )\n\n    async def test_tool_with_title_and_meta(self):\n        \"\"\"Test that title (via annotations) and meta arguments are properly passed through.\"\"\"\n        from mcp.types import ToolAnnotations\n\n        mcp = FastMCP()\n\n        class MyToolWithMeta(MCPMixin):\n            @mcp_tool(\n                annotations=ToolAnnotations(title=\"My Tool Title\"),\n                meta={\"version\": \"1.0\", \"author\": \"test\"},\n            )\n            def sample_tool(self):\n                pass\n\n        instance = MyToolWithMeta()\n        instance.register_tools(mcp)\n\n        registered_tools = await mcp.list_tools()\n        tool = next(t for t in registered_tools if t.name == \"sample_tool\")\n\n        assert tool.annotations is not None\n        assert tool.annotations.title == \"My Tool Title\"\n        assert tool.meta == {\"version\": \"1.0\", \"author\": \"test\"}\n\n    async def test_resource_with_meta(self):\n        \"\"\"Test that meta argument is properly passed through for resources.\"\"\"\n        mcp = FastMCP()\n\n        class MyResourceWithMeta(MCPMixin):\n            @mcp_resource(\n                uri=\"test://resource\",\n                title=\"My Resource Title\",\n                meta={\"category\": \"data\", \"internal\": True},\n            )\n            def sample_resource(self):\n                pass\n\n        instance = MyResourceWithMeta()\n        instance.register_resources(mcp)\n\n        registered_resources = await mcp.list_resources()\n        resource = next(\n            r for r in registered_resources if str(r.uri) == \"test://resource\"\n        )\n\n        assert resource.meta == {\"category\": \"data\", \"internal\": True}\n        assert resource.title == \"My Resource Title\"\n\n    async def test_prompt_with_title_and_meta(self):\n        \"\"\"Test that title and meta arguments are properly passed through for prompts.\"\"\"\n        mcp = FastMCP()\n\n        class MyPromptWithMeta(MCPMixin):\n            @mcp_prompt(\n                title=\"My Prompt Title\",\n                meta={\"priority\": \"high\", \"category\": \"analysis\"},\n            )\n            def sample_prompt(self):\n                pass\n\n        instance = MyPromptWithMeta()\n        instance.register_prompts(mcp)\n\n        prompts = await mcp.list_prompts()\n        prompt = next(p for p in prompts if p.name == \"sample_prompt\")\n\n        assert prompt.title == \"My Prompt Title\"\n        assert prompt.meta == {\"priority\": \"high\", \"category\": \"analysis\"}\n\n\nclass TestMCPMixinKwargsSync:\n    \"\"\"Verify that the valid-kwarg sets stay in sync with from_function signatures.\"\"\"\n\n    def test_tool_valid_kwargs_match_from_function(self):\n        from fastmcp.tools.base import Tool\n\n        expected = frozenset(\n            p for p in inspect.signature(Tool.from_function).parameters if p != \"fn\"\n        )\n        assert _TOOL_VALID_KWARGS == expected\n\n    def test_resource_valid_kwargs_match_from_function(self):\n        from fastmcp.resources.base import Resource\n\n        expected = frozenset(\n            p\n            for p in inspect.signature(Resource.from_function).parameters\n            if p not in (\"fn\", \"uri\")\n        )\n        assert _RESOURCE_VALID_KWARGS == expected\n\n    def test_prompt_valid_kwargs_match_from_function(self):\n        from fastmcp.prompts.base import Prompt\n\n        expected = frozenset(\n            p for p in inspect.signature(Prompt.from_function).parameters if p != \"fn\"\n        )\n        assert _PROMPT_VALID_KWARGS == expected\n\n\nclass TestMCPMixinValidation:\n    \"\"\"Unknown kwargs raise TypeError at decoration time, not at registration.\"\"\"\n\n    def test_mcp_tool_rejects_unknown_param(self):\n        with pytest.raises(TypeError, match=\"unexpected keyword argument\"):\n\n            @mcp_tool(definitely_not_a_real_param=\"oops\")\n            def my_tool(self):\n                pass\n\n    def test_mcp_resource_rejects_unknown_param(self):\n        with pytest.raises(TypeError, match=\"unexpected keyword argument\"):\n\n            @mcp_resource(uri=\"test://x\", definitely_not_a_real_param=\"oops\")\n            def my_resource(self):\n                pass\n\n    def test_mcp_prompt_rejects_unknown_param(self):\n        with pytest.raises(TypeError, match=\"unexpected keyword argument\"):\n\n            @mcp_prompt(definitely_not_a_real_param=\"oops\")\n            def my_prompt(self):\n                pass\n\n    def test_error_raised_at_decoration_not_registration(self):\n        \"\"\"The TypeError must surface when the decorator is applied, not later.\"\"\"\n        with pytest.raises(TypeError):\n\n            class MyMixin(MCPMixin):\n                @mcp_tool(bad_kwarg=True)\n                def tool(self):\n                    pass\n\n\nclass TestMCPMixinEnabled:\n    \"\"\"enabled=False suppresses registration; enabled=True (default) registers normally.\"\"\"\n\n    async def test_tool_enabled_false_skips_registration(self):\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_tool(enabled=False)\n            def hidden_tool(self):\n                pass\n\n            @mcp_tool()\n            def visible_tool(self):\n                pass\n\n        MyMixin().register_tools(mcp)\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert \"visible_tool\" in names\n        assert \"hidden_tool\" not in names\n\n    async def test_resource_enabled_false_skips_registration(self):\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_resource(uri=\"test://hidden\", enabled=False)\n            def hidden_resource(self):\n                pass\n\n            @mcp_resource(uri=\"test://visible\")\n            def visible_resource(self):\n                pass\n\n        MyMixin().register_resources(mcp)\n        resources = await mcp.list_resources()\n        uris = {str(r.uri) for r in resources}\n        assert \"test://visible\" in uris\n        assert \"test://hidden\" not in uris\n\n    async def test_prompt_enabled_false_skips_registration(self):\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_prompt(enabled=False)\n            def hidden_prompt(self):\n                pass\n\n            @mcp_prompt()\n            def visible_prompt(self):\n                pass\n\n        MyMixin().register_prompts(mcp)\n        prompts = await mcp.list_prompts()\n        names = {p.name for p in prompts}\n        assert \"visible_prompt\" in names\n        assert \"hidden_prompt\" not in names\n\n    async def test_tool_enabled_true_registers_normally(self):\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_tool(enabled=True)\n            def my_tool(self):\n                pass\n\n        MyMixin().register_tools(mcp)\n        tools = await mcp.list_tools()\n        assert any(t.name == \"my_tool\" for t in tools)\n\n\nclass TestMCPMixinNewParams:\n    \"\"\"Parameters that were previously missing now work end-to-end.\"\"\"\n\n    async def test_tool_auth_param_forwarded(self):\n        from fastmcp.server.auth import require_scopes\n\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_tool(auth=require_scopes(\"write\"))\n            def secure_tool(self):\n                return \"ok\"\n\n        MyMixin().register_tools(mcp)\n        # list_tools() filters by auth context; check internal provider directly\n        tools = await mcp.local_provider.list_tools()\n        assert any(t.name == \"secure_tool\" for t in tools)\n\n    async def test_tool_timeout_param_forwarded(self):\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_tool(timeout=5.0)\n            def timed_tool(self):\n                return \"ok\"\n\n        MyMixin().register_tools(mcp)\n        tools = await mcp.list_tools()\n        assert any(t.name == \"timed_tool\" for t in tools)\n\n    async def test_tool_version_param_forwarded(self):\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_tool(version=\"2.0\")\n            def versioned_tool(self):\n                return \"ok\"\n\n        MyMixin().register_tools(mcp)\n        tools = await mcp.list_tools()\n        assert any(t.name == \"versioned_tool\" for t in tools)\n\n    async def test_resource_auth_param_forwarded(self):\n        from fastmcp.server.auth import require_scopes\n\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_resource(uri=\"test://secure\", auth=require_scopes(\"read\"))\n            def secure_resource(self):\n                return \"data\"\n\n        MyMixin().register_resources(mcp)\n        # list_resources() filters by auth context; check internal provider directly\n        resources = await mcp.local_provider.list_resources()\n        assert any(str(r.uri) == \"test://secure\" for r in resources)\n\n    async def test_prompt_auth_param_forwarded(self):\n        from fastmcp.server.auth import require_scopes\n\n        mcp = FastMCP()\n\n        class MyMixin(MCPMixin):\n            @mcp_prompt(auth=require_scopes(\"read\"))\n            def secure_prompt(self):\n                return \"prompt text\"\n\n        MyMixin().register_prompts(mcp)\n        # list_prompts() filters by auth context; check internal provider directly\n        prompts = await mcp.local_provider.list_prompts()\n        assert any(p.name == \"secure_prompt\" for p in prompts)\n"
  },
  {
    "path": "tests/deprecated/__init__.py",
    "content": "# Tests for deprecated features - deprecation warnings are suppressed via conftest.py\n"
  },
  {
    "path": "tests/deprecated/conftest.py",
    "content": "\"\"\"Conftest for deprecated tests - suppresses deprecation warnings.\"\"\"\n\nimport pytest\n\n\ndef pytest_collection_modifyitems(items):\n    \"\"\"Add filterwarnings marker to all tests in this directory.\"\"\"\n    for item in items:\n        item.add_marker(pytest.mark.filterwarnings(\"ignore::DeprecationWarning\"))\n"
  },
  {
    "path": "tests/deprecated/openapi/test_openapi.py",
    "content": "\"\"\"Tests for deprecated OpenAPI imports.\n\nThese tests verify that the old import paths still work and emit\ndeprecation warnings, ensuring backwards compatibility.\n\"\"\"\n\nimport warnings\n\nimport httpx\n\n\nclass TestDeprecatedServerOpenAPIImports:\n    \"\"\"Test deprecated imports from fastmcp.server.openapi.\"\"\"\n\n    def test_import_fastmcp_openapi_emits_warning(self):\n        \"\"\"Importing from fastmcp.server.openapi should emit deprecation warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            # Force reimport\n            import importlib\n\n            import fastmcp.server.openapi\n\n            importlib.reload(fastmcp.server.openapi)\n\n            deprecation_warnings = [\n                x for x in w if issubclass(x.category, DeprecationWarning)\n            ]\n            assert len(deprecation_warnings) >= 1\n            assert \"providers.openapi\" in str(deprecation_warnings[0].message)\n\n    def test_import_routing_emits_warning(self):\n        \"\"\"Importing from fastmcp.server.openapi.routing should emit deprecation warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            import importlib\n\n            import fastmcp.server.openapi.routing\n\n            importlib.reload(fastmcp.server.openapi.routing)\n\n            deprecation_warnings = [\n                x for x in w if issubclass(x.category, DeprecationWarning)\n            ]\n            assert len(deprecation_warnings) >= 1\n            assert \"providers.openapi\" in str(deprecation_warnings[0].message)\n\n    def test_fastmcp_openapi_class_emits_warning(self):\n        \"\"\"Using FastMCPOpenAPI should emit deprecation warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            from fastmcp.server.openapi.server import FastMCPOpenAPI\n\n            spec = {\n                \"openapi\": \"3.0.0\",\n                \"info\": {\"title\": \"Test\", \"version\": \"1.0.0\"},\n                \"paths\": {},\n            }\n            client = httpx.AsyncClient(base_url=\"https://example.com\")\n            FastMCPOpenAPI(openapi_spec=spec, client=client)\n\n            deprecation_warnings = [\n                x for x in w if issubclass(x.category, DeprecationWarning)\n            ]\n            assert len(deprecation_warnings) >= 1\n            assert \"FastMCPOpenAPI\" in str(deprecation_warnings[-1].message)\n\n    def test_deprecated_imports_still_work(self):\n        \"\"\"All expected symbols should be importable from deprecated locations.\"\"\"\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n\n            from fastmcp.server.openapi import (\n                FastMCPOpenAPI,\n                MCPType,\n                OpenAPIProvider,\n                RouteMap,\n            )\n\n            # Verify they're the right types\n            assert FastMCPOpenAPI is not None\n            assert OpenAPIProvider is not None\n            assert MCPType.TOOL.value == \"TOOL\"\n            assert RouteMap is not None\n\n    def test_deprecated_routing_imports_still_work(self):\n        \"\"\"Routing symbols should be importable from deprecated location.\"\"\"\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n\n            from fastmcp.server.openapi.routing import (\n                DEFAULT_ROUTE_MAPPINGS,\n                MCPType,\n                _determine_route_type,\n            )\n\n            assert DEFAULT_ROUTE_MAPPINGS is not None\n            assert len(DEFAULT_ROUTE_MAPPINGS) > 0\n            assert MCPType.TOOL.value == \"TOOL\"\n            assert _determine_route_type is not None\n\n\nclass TestDeprecatedExperimentalOpenAPIImports:\n    \"\"\"Test deprecated imports from fastmcp.experimental.server.openapi.\"\"\"\n\n    def test_experimental_import_emits_warning(self):\n        \"\"\"Importing from experimental should emit deprecation warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            import importlib\n\n            import fastmcp.experimental.server.openapi\n\n            importlib.reload(fastmcp.experimental.server.openapi)\n\n            deprecation_warnings = [\n                x for x in w if issubclass(x.category, DeprecationWarning)\n            ]\n            assert len(deprecation_warnings) >= 1\n            assert \"providers.openapi\" in str(deprecation_warnings[0].message)\n\n    def test_experimental_imports_still_work(self):\n        \"\"\"All expected symbols should be importable from experimental.\"\"\"\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n\n            from fastmcp.experimental.server.openapi import (\n                DEFAULT_ROUTE_MAPPINGS,\n                FastMCPOpenAPI,\n                MCPType,\n            )\n\n            assert FastMCPOpenAPI is not None\n            assert DEFAULT_ROUTE_MAPPINGS is not None\n            assert MCPType.TOOL.value == \"TOOL\"\n\n\nclass TestDeprecatedComponentsImports:\n    \"\"\"Test deprecated imports from fastmcp.server.openapi.components.\"\"\"\n\n    def test_components_import_emits_warning(self):\n        \"\"\"Importing from components should emit deprecation warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            import importlib\n\n            import fastmcp.server.openapi.components\n\n            importlib.reload(fastmcp.server.openapi.components)\n\n            deprecation_warnings = [\n                x for x in w if issubclass(x.category, DeprecationWarning)\n            ]\n            assert len(deprecation_warnings) >= 1\n            assert \"providers.openapi\" in str(deprecation_warnings[0].message)\n\n    def test_components_imports_still_work(self):\n        \"\"\"Component classes should be importable from deprecated location.\"\"\"\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n\n            from fastmcp.server.openapi.components import (\n                OpenAPIResource,\n                OpenAPIResourceTemplate,\n                OpenAPITool,\n            )\n\n            assert OpenAPITool is not None\n            assert OpenAPIResource is not None\n            assert OpenAPIResourceTemplate is not None\n"
  },
  {
    "path": "tests/deprecated/server/__init__.py",
    "content": "\"\"\"Deprecated server tests.\"\"\"\n"
  },
  {
    "path": "tests/deprecated/server/test_include_exclude_tags.py",
    "content": "\"\"\"Tests for removed include_tags/exclude_tags parameters.\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\n\n\nclass TestIncludeExcludeTagsRemoved:\n    \"\"\"Test that include_tags/exclude_tags raise TypeError with migration hints.\"\"\"\n\n    def test_exclude_tags_raises_type_error(self):\n        with pytest.raises(TypeError, match=\"no longer accepts `exclude_tags`\"):\n            FastMCP(exclude_tags={\"internal\"})\n\n    def test_include_tags_raises_type_error(self):\n        with pytest.raises(TypeError, match=\"no longer accepts `include_tags`\"):\n            FastMCP(include_tags={\"public\"})\n\n    def test_exclude_tags_error_mentions_disable(self):\n        with pytest.raises(TypeError, match=\"server.disable\"):\n            FastMCP(exclude_tags={\"internal\"})\n\n    def test_include_tags_error_mentions_enable(self):\n        with pytest.raises(TypeError, match=\"server.enable\"):\n            FastMCP(include_tags={\"public\"})\n"
  },
  {
    "path": "tests/deprecated/test_add_tool_transformation.py",
    "content": "\"\"\"Tests for deprecated add_tool_transformation API.\"\"\"\n\nimport warnings\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\n\n\nclass TestAddToolTransformationDeprecated:\n    \"\"\"Test that add_tool_transformation still works but emits deprecation warning.\"\"\"\n\n    async def test_add_tool_transformation_emits_warning(self):\n        \"\"\"add_tool_transformation should emit deprecation warning.\"\"\"\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"hello\"\n\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            mcp.add_tool_transformation(\n                \"my_tool\", ToolTransformConfig(name=\"renamed_tool\")\n            )\n\n            assert len(w) == 1\n            assert issubclass(w[0].category, DeprecationWarning)\n            assert \"add_tool_transformation is deprecated\" in str(w[0].message)\n\n    async def test_add_tool_transformation_still_works(self):\n        \"\"\"add_tool_transformation should still apply the transformation.\"\"\"\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def verbose_tool_name() -> str:\n            return \"result\"\n\n        # Suppress warning for this test - we just want to verify it works\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n            mcp.add_tool_transformation(\n                \"verbose_tool_name\", ToolTransformConfig(name=\"short\")\n            )\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = [t.name for t in tools]\n\n            # Original name should be gone, renamed version should exist\n            assert \"verbose_tool_name\" not in tool_names\n            assert \"short\" in tool_names\n\n            # Should be callable by new name\n            result = await client.call_tool(\"short\", {})\n            assert result.content[0].text == \"result\"\n\n    async def test_remove_tool_transformation_emits_warning(self):\n        \"\"\"remove_tool_transformation should emit deprecation warning.\"\"\"\n        mcp = FastMCP(\"test\")\n\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            mcp.remove_tool_transformation(\"any_tool\")\n\n            assert len(w) == 1\n            assert issubclass(w[0].category, DeprecationWarning)\n            assert \"remove_tool_transformation is deprecated\" in str(w[0].message)\n            assert \"no effect\" in str(w[0].message)\n\n    async def test_tool_transformations_constructor_raises_type_error(self):\n        \"\"\"tool_transformations constructor param should raise TypeError.\"\"\"\n        import pytest\n\n        with pytest.raises(TypeError, match=\"no longer accepts `tool_transformations`\"):\n            FastMCP(\n                \"test\",\n                tool_transformations={\"my_tool\": ToolTransformConfig(name=\"renamed\")},\n            )\n"
  },
  {
    "path": "tests/deprecated/test_deprecated.py",
    "content": "import pytest\nfrom starlette.applications import Starlette\n\nfrom fastmcp import FastMCP\n\n\nclass TestRemovedKwargs:\n    def test_host_kwarg_raises_type_error(self):\n        with pytest.raises(TypeError, match=\"no longer accepts `host`\"):\n            FastMCP(host=\"1.2.3.4\")\n\n    def test_settings_property_removed(self):\n        mcp = FastMCP()\n        assert not hasattr(mcp, \"_deprecated_settings\")\n        with pytest.raises(AttributeError):\n            mcp.settings  # noqa: B018  # ty: ignore[unresolved-attribute]\n\n\ndef test_http_app_with_sse_transport():\n    \"\"\"Test that http_app with SSE transport works.\"\"\"\n    server = FastMCP(\"TestServer\")\n    app = server.http_app(transport=\"sse\")\n    assert isinstance(app, Starlette)\n"
  },
  {
    "path": "tests/deprecated/test_exclude_args.py",
    "content": "from typing import Any\n\nimport pytest\nfrom mcp.server.session import ServerSession\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.tools.base import Tool\n\n\nasync def test_tool_exclude_args():\n    \"\"\"Test that tool args are excluded.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(exclude_args=[\"state\"])\n    def echo(message: str, state: dict[str, Any] | None = None) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        if state:\n            # State was read\n            pass\n        return message\n\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert \"state\" not in tools[0].parameters[\"properties\"]\n\n\nasync def test_tool_exclude_args_without_default_value_raises_error():\n    \"\"\"Test that excluding args without default values raises ValueError\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    with pytest.raises(ValueError):\n\n        @mcp.tool(exclude_args=[\"state\"])\n        def echo(message: str, state: dict[str, Any] | None) -> str:\n            \"\"\"Echo back the message provided.\"\"\"\n            if state:\n                # State was read\n                pass\n            return message\n\n\nasync def test_add_tool_method_exclude_args():\n    \"\"\"Test that tool exclude_args work with the add_tool method.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    def create_item(\n        name: str, value: int, state: dict[str, Any] | None = None\n    ) -> dict[str, Any]:\n        \"\"\"Create a new item.\"\"\"\n        if state:\n            # State was read\n            pass\n        return {\"name\": name, \"value\": value}\n\n    tool = Tool.from_function(\n        create_item,\n        name=\"create_item\",\n        exclude_args=[\"state\"],\n    )\n    mcp.add_tool(tool)\n\n    # Check tool via public API\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert \"state\" not in tools[0].parameters[\"properties\"]\n\n\nasync def test_tool_functionality_with_exclude_args():\n    \"\"\"Test that tool functionality is preserved when using exclude_args.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    def create_item(\n        name: str, value: int, state: dict[str, Any] | None = None\n    ) -> dict[str, Any]:\n        \"\"\"Create a new item.\"\"\"\n        if state:\n            # state was read\n            pass\n        return {\"name\": name, \"value\": value}\n\n    tool = Tool.from_function(\n        create_item,\n        name=\"create_item\",\n        exclude_args=[\"state\"],\n    )\n    mcp.add_tool(tool)\n\n    # Use the tool to verify functionality is preserved\n    async with Client(mcp) as client:\n        result = await client.call_tool(\n            \"create_item\", {\"name\": \"test_item\", \"value\": 42}\n        )\n        assert result.data == {\"name\": \"test_item\", \"value\": 42}\n\n\nasync def test_exclude_args_with_non_serializable_type():\n    \"\"\"Test that exclude_args works even when the excluded parameter type can't be serialized.\n\n    This test ensures that exclude_args works correctly when the excluded parameter\n    has a type that Pydantic cannot serialize (like ServerSession). The bug was that\n    get_cached_typeadapter would try to serialize all parameters before compress_schema\n    could exclude them, causing a PydanticSchemaGenerationError.\n    \"\"\"\n\n    def my_tool(message: str, session: ServerSession | None = None) -> str:\n        \"\"\"A tool that takes a non-serializable Session parameter.\"\"\"\n        return message\n\n    # This should not raise an error even though ServerSession can't be serialized\n    tool = Tool.from_function(\n        my_tool,\n        name=\"my_tool\",\n        exclude_args=[\"session\"],\n    )\n\n    # Verify the tool was created successfully\n    assert tool is not None\n    assert tool.name == \"my_tool\"\n\n    # Verify the session parameter is excluded from the schema\n    assert \"session\" not in tool.parameters[\"properties\"]\n    assert \"message\" in tool.parameters[\"properties\"]\n"
  },
  {
    "path": "tests/deprecated/test_function_component_imports.py",
    "content": "\"\"\"Test that deprecated import paths for function components still work.\"\"\"\n\nimport warnings\n\nimport pytest\n\nfrom fastmcp.utilities.tests import temporary_settings\n\n\nclass TestDeprecatedFunctionToolImports:\n    def test_function_tool_from_tool_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning, match=\"Import from fastmcp.tools.function_tool\"\n            ):\n                from fastmcp.tools.base import FunctionTool\n\n            # Verify it's the real class\n            from fastmcp.tools.function_tool import (\n                FunctionTool as CanonicalFunctionTool,\n            )\n\n            assert FunctionTool is CanonicalFunctionTool\n\n    def test_parsed_function_from_tool_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning, match=\"Import from fastmcp.tools.function_tool\"\n            ):\n                from fastmcp.tools.base import ParsedFunction\n\n            from fastmcp.tools.function_tool import (\n                ParsedFunction as CanonicalParsedFunction,\n            )\n\n            assert ParsedFunction is CanonicalParsedFunction\n\n    def test_tool_decorator_from_tool_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning, match=\"Import from fastmcp.tools.function_tool\"\n            ):\n                from fastmcp.tools.base import tool\n\n            from fastmcp.tools.function_tool import tool as canonical_tool\n\n            assert tool is canonical_tool\n\n    def test_no_warning_when_disabled(self):\n        with temporary_settings(deprecation_warnings=False):\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"error\")\n                from fastmcp.tools.base import FunctionTool  # noqa: F401\n\n\nclass TestDeprecatedFunctionResourceImports:\n    def test_function_resource_from_resource_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning,\n                match=\"Import from fastmcp.resources.function_resource\",\n            ):\n                from fastmcp.resources.base import FunctionResource\n\n            from fastmcp.resources.function_resource import (\n                FunctionResource as CanonicalFunctionResource,\n            )\n\n            assert FunctionResource is CanonicalFunctionResource\n\n    def test_resource_decorator_from_resource_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning,\n                match=\"Import from fastmcp.resources.function_resource\",\n            ):\n                from fastmcp.resources.base import resource\n\n            from fastmcp.resources.function_resource import (\n                resource as canonical_resource,\n            )\n\n            assert resource is canonical_resource\n\n    def test_no_warning_when_disabled(self):\n        with temporary_settings(deprecation_warnings=False):\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"error\")\n                from fastmcp.resources.base import FunctionResource  # noqa: F401\n\n\nclass TestDeprecatedFunctionPromptImports:\n    def test_function_prompt_from_prompt_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning, match=\"Import from fastmcp.prompts.function_prompt\"\n            ):\n                from fastmcp.prompts.base import FunctionPrompt\n\n            from fastmcp.prompts.function_prompt import (\n                FunctionPrompt as CanonicalFunctionPrompt,\n            )\n\n            assert FunctionPrompt is CanonicalFunctionPrompt\n\n    def test_prompt_decorator_from_prompt_module(self):\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(\n                DeprecationWarning, match=\"Import from fastmcp.prompts.function_prompt\"\n            ):\n                from fastmcp.prompts.base import prompt\n\n            from fastmcp.prompts.function_prompt import prompt as canonical_prompt\n\n            assert prompt is canonical_prompt\n\n    def test_no_warning_when_disabled(self):\n        with temporary_settings(deprecation_warnings=False):\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"error\")\n                from fastmcp.prompts.base import FunctionPrompt  # noqa: F401\n"
  },
  {
    "path": "tests/deprecated/test_import_server.py",
    "content": "import json\nfrom urllib.parse import quote\n\nfrom mcp.types import TextContent, TextResourceContents\n\nfrom fastmcp.client.client import Client\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom tests.conftest import get_fn_name\n\n\nasync def test_import_basic_functionality():\n    \"\"\"Test that the import method properly imports tools and other resources.\"\"\"\n    # Create main app and sub-app\n    main_app = FastMCP(\"MainApp\")\n    sub_app = FastMCP(\"SubApp\")\n\n    # Add a tool to the sub-app\n    @sub_app.tool\n    def sub_tool() -> str:\n        return \"This is from the sub app\"\n\n    # Import the sub-app to the main app\n    await main_app.import_server(sub_app, \"sub\")\n\n    # Verify the tool was imported with the prefix\n    main_tools = await main_app.list_tools()\n    sub_tools = await sub_app.list_tools()\n    assert any(t.name == \"sub_sub_tool\" for t in main_tools)\n    assert any(t.name == \"sub_tool\" for t in sub_tools)\n\n    # Verify the original tool still exists in the sub-app\n    tool = await main_app.get_tool(\"sub_sub_tool\")\n    assert tool is not None\n    # import_server creates copies with prefixed names (unlike mount which proxies)\n    assert tool.name == \"sub_sub_tool\"\n    assert isinstance(tool, FunctionTool)\n    assert callable(tool.fn)\n\n\nasync def test_import_multiple_apps():\n    \"\"\"Test importing multiple apps to a main app.\"\"\"\n    # Create main app and multiple sub-apps\n    main_app = FastMCP(\"MainApp\")\n    weather_app = FastMCP(\"WeatherApp\")\n    news_app = FastMCP(\"NewsApp\")\n\n    # Add tools to each sub-app\n    @weather_app.tool\n    def get_forecast() -> str:\n        return \"Weather forecast\"\n\n    @news_app.tool\n    def get_headlines() -> str:\n        return \"News headlines\"\n\n    # Import both sub-apps to the main app\n    await main_app.import_server(weather_app, \"weather\")\n    await main_app.import_server(news_app, \"news\")\n\n    # Verify tools were imported with the correct prefixes\n    tools = await main_app.list_tools()\n    assert any(t.name == \"weather_get_forecast\" for t in tools)\n    assert any(t.name == \"news_get_headlines\" for t in tools)\n\n\nasync def test_import_combines_tools():\n    \"\"\"Test that importing preserves existing tools with the same prefix.\"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    first_app = FastMCP(\"FirstApp\")\n    second_app = FastMCP(\"SecondApp\")\n\n    # Add tools to each sub-app\n    @first_app.tool\n    def first_tool() -> str:\n        return \"First app tool\"\n\n    @second_app.tool\n    def second_tool() -> str:\n        return \"Second app tool\"\n\n    # Import first app\n    await main_app.import_server(first_app, \"api\")\n    tools = await main_app.list_tools()\n    assert any(t.name == \"api_first_tool\" for t in tools)\n\n    # Import second app to same prefix\n    await main_app.import_server(second_app, \"api\")\n\n    # Verify second tool is there\n    tools = await main_app.list_tools()\n    assert any(t.name == \"api_second_tool\" for t in tools)\n\n    # Tools from both imports are combined\n    assert any(t.name == \"api_first_tool\" for t in tools)\n\n\nasync def test_import_with_resources():\n    \"\"\"Test importing with resources.\"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    data_app = FastMCP(\"DataApp\")\n\n    # Add a resource to the data app\n    @data_app.resource(uri=\"data://users\")\n    async def get_users() -> str:\n        return \"user1, user2\"\n\n    # Import the data app\n    await main_app.import_server(data_app, \"data\")\n\n    # Verify the resource was imported with the prefix\n    resources = await main_app.list_resources()\n    assert any(str(r.uri) == \"data://data/users\" for r in resources)\n\n\nasync def test_import_with_resource_templates():\n    \"\"\"Test importing with resource templates.\"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    user_app = FastMCP(\"UserApp\")\n\n    # Add a resource template to the user app\n    @user_app.resource(uri=\"users://{user_id}/profile\")\n    def get_user_profile(user_id: str) -> str:\n        import json\n\n        return json.dumps(\n            {\"id\": user_id, \"name\": f\"User {user_id}\"}, separators=(\",\", \":\")\n        )\n\n    # Import the user app\n    await main_app.import_server(user_app, \"api\")\n\n    # Verify the template was imported with the prefix\n    templates = await main_app.list_resource_templates()\n    assert any(t.uri_template == \"users://api/{user_id}/profile\" for t in templates)\n\n\nasync def test_import_with_prompts():\n    \"\"\"Test importing with prompts.\"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    assistant_app = FastMCP(\"AssistantApp\")\n\n    # Add a prompt to the assistant app\n    @assistant_app.prompt\n    def greeting(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    # Import the assistant app\n    await main_app.import_server(assistant_app, \"assistant\")\n\n    # Verify the prompt was imported with the prefix\n    prompts = await main_app.list_prompts()\n    assert any(p.name == \"assistant_greeting\" for p in prompts)\n\n\nasync def test_import_multiple_resource_templates():\n    \"\"\"Test importing multiple apps with resource templates.\"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    weather_app = FastMCP(\"WeatherApp\")\n    news_app = FastMCP(\"NewsApp\")\n\n    # Add templates to each app\n    @weather_app.resource(uri=\"weather://{city}\")\n    def get_weather(city: str) -> str:\n        return f\"Weather for {city}\"\n\n    @news_app.resource(uri=\"news://{category}\")\n    def get_news(category: str) -> str:\n        return f\"News for {category}\"\n\n    # Import both apps\n    await main_app.import_server(weather_app, \"data\")\n    await main_app.import_server(news_app, \"content\")\n\n    # Verify templates were imported with correct prefixes\n    templates = await main_app.list_resource_templates()\n    assert any(t.uri_template == \"weather://data/{city}\" for t in templates)\n    assert any(t.uri_template == \"news://content/{category}\" for t in templates)\n\n\nasync def test_import_multiple_prompts():\n    \"\"\"Test importing multiple apps with prompts.\"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    python_app = FastMCP(\"PythonApp\")\n    sql_app = FastMCP(\"SQLApp\")\n\n    # Add prompts to each app\n    @python_app.prompt\n    def review_python(code: str) -> str:\n        return f\"Reviewing Python code:\\n{code}\"\n\n    @sql_app.prompt\n    def explain_sql(query: str) -> str:\n        return f\"Explaining SQL query:\\n{query}\"\n\n    # Import both apps\n    await main_app.import_server(python_app, \"python\")\n    await main_app.import_server(sql_app, \"sql\")\n\n    # Verify prompts were imported with correct prefixes\n    prompts = await main_app.list_prompts()\n    assert any(p.name == \"python_review_python\" for p in prompts)\n    assert any(p.name == \"sql_explain_sql\" for p in prompts)\n\n\nasync def test_tool_custom_name_preserved_when_imported():\n    \"\"\"Test that a tool's custom name is preserved when imported.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    api_app = FastMCP(\"APIApp\")\n\n    def fetch_data(query: str) -> str:\n        return f\"Data for query: {query}\"\n\n    api_app.add_tool(Tool.from_function(fetch_data, name=\"get_data\"))\n    await main_app.import_server(api_app, \"api\")\n\n    # Check that the tool is accessible by its prefixed name\n    tool = await main_app.get_tool(\"api_get_data\")\n    assert tool is not None\n\n    # Check that the function name is preserved\n    assert isinstance(tool, FunctionTool)\n    assert get_fn_name(tool.fn) == \"fetch_data\"\n\n\nasync def test_call_imported_custom_named_tool():\n    \"\"\"Test calling an imported tool with a custom name.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    api_app = FastMCP(\"APIApp\")\n\n    def fetch_data(query: str) -> str:\n        return f\"Data for query: {query}\"\n\n    api_app.add_tool(Tool.from_function(fetch_data, name=\"get_data\"))\n    await main_app.import_server(api_app, \"api\")\n\n    async with Client(main_app) as client:\n        result = await client.call_tool(\"api_get_data\", {\"query\": \"test\"})\n        assert result.data == \"Data for query: test\"\n\n\nasync def test_first_level_importing_with_custom_name():\n    \"\"\"Test that a tool with a custom name is correctly imported at the first level.\"\"\"\n    service_app = FastMCP(\"ServiceApp\")\n    provider_app = FastMCP(\"ProviderApp\")\n\n    def calculate_value(input: int) -> int:\n        return input * 2\n\n    provider_app.add_tool(Tool.from_function(calculate_value, name=\"compute\"))\n    await service_app.import_server(provider_app, \"provider\")\n\n    # Tool is accessible in the service app with the first prefix\n    tool = await service_app.get_tool(\"provider_compute\")\n    assert tool is not None\n    assert isinstance(tool, FunctionTool)\n    assert get_fn_name(tool.fn) == \"calculate_value\"\n\n\nasync def test_nested_importing_preserves_prefixes():\n    \"\"\"Test that importing a previously imported app preserves prefixes.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    service_app = FastMCP(\"ServiceApp\")\n    provider_app = FastMCP(\"ProviderApp\")\n\n    def calculate_value(input: int) -> int:\n        return input * 2\n\n    provider_app.add_tool(Tool.from_function(calculate_value, name=\"compute\"))\n    await service_app.import_server(provider_app, \"provider\")\n    await main_app.import_server(service_app, \"service\")\n\n    # Tool is accessible in the main app with both prefixes\n    tool = await main_app.get_tool(\"service_provider_compute\")\n    assert tool is not None\n\n\nasync def test_call_nested_imported_tool():\n    \"\"\"Test calling a tool through multiple levels of importing.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    service_app = FastMCP(\"ServiceApp\")\n    provider_app = FastMCP(\"ProviderApp\")\n\n    def calculate_value(input: int) -> int:\n        return input * 2\n\n    provider_app.add_tool(Tool.from_function(calculate_value, name=\"compute\"))\n    await service_app.import_server(provider_app, \"provider\")\n    await main_app.import_server(service_app, \"service\")\n\n    async with Client(main_app) as client:\n        result = await client.call_tool(\"service_provider_compute\", {\"input\": 21})\n        assert result.data == 42\n\n\nasync def test_import_with_proxy_tools():\n    \"\"\"\n    Test importing with tools that have custom names (proxy tools).\n\n    This tests that the tool's name doesn't change even though the registered\n    name does, which is important because we need to forward that name to the\n    proxy server correctly.\n    \"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    api_app = FastMCP(\"APIApp\")\n\n    @api_app.tool\n    def get_data(query: str) -> str:\n        return f\"Data for query: {query}\"\n\n    proxy_app = FastMCP.as_proxy(api_app)\n    await main_app.import_server(proxy_app, \"api\")\n\n    async with Client(main_app) as client:\n        result = await client.call_tool(\"api_get_data\", {\"query\": \"test\"})\n        assert result.data == \"Data for query: test\"\n\n\nasync def test_import_with_proxy_prompts():\n    \"\"\"\n    Test importing with prompts that have custom keys.\n\n    This tests that the prompt's name doesn't change even though the registered\n    key does, which is important for correct rendering.\n    \"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    api_app = FastMCP(\"APIApp\")\n\n    @api_app.prompt\n    def greeting(name: str) -> str:\n        \"\"\"Example greeting prompt.\"\"\"\n        return f\"Hello, {name} from API!\"\n\n    proxy_app = FastMCP.as_proxy(api_app)\n    await main_app.import_server(proxy_app, \"api\")\n\n    async with Client(main_app) as client:\n        result = await client.get_prompt(\"api_greeting\", {\"name\": \"World\"})\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Hello, World from API!\"\n        assert result.description == \"Example greeting prompt.\"\n\n\nasync def test_import_with_proxy_resources():\n    \"\"\"\n    Test importing with resources that have custom keys.\n\n    This tests that the resource's name doesn't change even though the registered\n    key does, which is important for correct access.\n    \"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    api_app = FastMCP(\"APIApp\")\n\n    # Create a resource in the API app\n    @api_app.resource(uri=\"config://settings\")\n    def get_config() -> str:\n        import json\n\n        return json.dumps(\n            {\n                \"api_key\": \"12345\",\n                \"base_url\": \"https://api.example.com\",\n            }\n        )\n\n    proxy_app = FastMCP.as_proxy(api_app)\n    await main_app.import_server(proxy_app, \"api\")\n\n    # Access the resource through the main app with the prefixed key\n    async with Client(main_app) as client:\n        result = await client.read_resource(\"config://api/settings\")\n        assert isinstance(result[0], TextResourceContents)\n        content = json.loads(result[0].text)\n        assert content[\"api_key\"] == \"12345\"\n        assert content[\"base_url\"] == \"https://api.example.com\"\n\n\nasync def test_import_with_proxy_resource_templates():\n    \"\"\"\n    Test importing with resource templates that have custom keys.\n\n    This tests that the template's name doesn't change even though the registered\n    key does, which is important for correct instantiation.\n    \"\"\"\n    # Create apps\n    main_app = FastMCP(\"MainApp\")\n    api_app = FastMCP(\"APIApp\")\n\n    # Create a resource template in the API app\n    @api_app.resource(uri=\"user://{name}/{email}\")\n    def create_user(name: str, email: str) -> str:\n        import json\n\n        return json.dumps({\"name\": name, \"email\": email})\n\n    proxy_app = FastMCP.as_proxy(api_app)\n    await main_app.import_server(proxy_app, \"api\")\n\n    # Instantiate the template through the main app with the prefixed key\n\n    quoted_name = quote(\"John Doe\", safe=\"\")\n    quoted_email = quote(\"john@example.com\", safe=\"\")\n    async with Client(main_app) as client:\n        result = await client.read_resource(f\"user://api/{quoted_name}/{quoted_email}\")\n        assert isinstance(result[0], TextResourceContents)\n        content = json.loads(result[0].text)\n        assert content[\"name\"] == \"John Doe\"\n        assert content[\"email\"] == \"john@example.com\"\n\n\nasync def test_import_with_no_prefix():\n    \"\"\"Test importing a server without providing a prefix.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    sub_app = FastMCP(\"SubApp\")\n\n    @sub_app.tool\n    def sub_tool() -> str:\n        return \"Sub tool result\"\n\n    @sub_app.resource(uri=\"data://config\")\n    def sub_resource():\n        return \"Sub resource data\"\n\n    @sub_app.resource(uri=\"users://{user_id}/info\")\n    def sub_template(user_id: str):\n        return f\"Sub template for user {user_id}\"\n\n    @sub_app.prompt\n    def sub_prompt() -> str:\n        return \"Sub prompt content\"\n\n    # Import without prefix\n    await main_app.import_server(sub_app)\n\n    # Verify all component types are accessible with original names\n    tools = await main_app.list_tools()\n    resources = await main_app.list_resources()\n    templates = await main_app.list_resource_templates()\n    prompts = await main_app.list_prompts()\n    assert any(t.name == \"sub_tool\" for t in tools)\n    assert any(str(r.uri) == \"data://config\" for r in resources)\n    assert any(t.uri_template == \"users://{user_id}/info\" for t in templates)\n    assert any(p.name == \"sub_prompt\" for p in prompts)\n\n    # Test actual functionality through Client\n    async with Client(main_app) as client:\n        # Test tool\n        tool_result = await client.call_tool(\"sub_tool\", {})\n        assert tool_result.data == \"Sub tool result\"\n\n        # Test resource\n        resource_result = await client.read_resource(\"data://config\")\n        assert isinstance(resource_result[0], TextResourceContents)\n        assert resource_result[0].text == \"Sub resource data\"\n\n        # Test template\n        template_result = await client.read_resource(\"users://123/info\")\n        assert isinstance(template_result[0], TextResourceContents)\n        assert template_result[0].text == \"Sub template for user 123\"\n\n        # Test prompt\n        prompt_result = await client.get_prompt(\"sub_prompt\", {})\n        assert prompt_result.messages is not None\n        assert isinstance(prompt_result.messages[0].content, TextContent)\n        assert prompt_result.messages[0].content.text == \"Sub prompt content\"\n\n\nasync def test_import_conflict_resolution_tools():\n    \"\"\"Test that later imported tools overwrite earlier ones when names conflict.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    first_app = FastMCP(\"FirstApp\")\n    second_app = FastMCP(\"SecondApp\")\n\n    @first_app.tool(name=\"shared_tool\")\n    def first_shared_tool() -> str:\n        return \"First app tool\"\n\n    @second_app.tool(name=\"shared_tool\")\n    def second_shared_tool() -> str:\n        return \"Second app tool\"\n\n    # Import both apps without prefix\n    await main_app.import_server(first_app)\n    await main_app.import_server(second_app)\n\n    async with Client(main_app) as client:\n        # The later imported server should win\n        tools = await client.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"shared_tool\" in tool_names\n        assert tool_names.count(\"shared_tool\") == 1  # Should only appear once\n\n        result = await client.call_tool(\"shared_tool\", {})\n        assert result.data == \"Second app tool\"\n\n\nasync def test_import_conflict_resolution_resources():\n    \"\"\"Test that later imported resources overwrite earlier ones when URIs conflict.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    first_app = FastMCP(\"FirstApp\")\n    second_app = FastMCP(\"SecondApp\")\n\n    @first_app.resource(uri=\"shared://data\")\n    def first_resource():\n        return \"First app data\"\n\n    @second_app.resource(uri=\"shared://data\")\n    def second_resource():\n        return \"Second app data\"\n\n    # Import both apps without prefix\n    await main_app.import_server(first_app)\n    await main_app.import_server(second_app)\n\n    async with Client(main_app) as client:\n        # The later imported server should win\n        resources = await client.list_resources()\n        resource_uris = [str(r.uri) for r in resources]\n        assert \"shared://data\" in resource_uris\n        assert resource_uris.count(\"shared://data\") == 1  # Should only appear once\n\n        result = await client.read_resource(\"shared://data\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"Second app data\"\n\n\nasync def test_import_conflict_resolution_templates():\n    \"\"\"Test that later imported templates overwrite earlier ones when URI templates conflict.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    first_app = FastMCP(\"FirstApp\")\n    second_app = FastMCP(\"SecondApp\")\n\n    @first_app.resource(uri=\"users://{user_id}/profile\")\n    def first_template(user_id: str):\n        return f\"First app user {user_id}\"\n\n    @second_app.resource(uri=\"users://{user_id}/profile\")\n    def second_template(user_id: str):\n        return f\"Second app user {user_id}\"\n\n    # Import both apps without prefix\n    await main_app.import_server(first_app)\n    await main_app.import_server(second_app)\n\n    async with Client(main_app) as client:\n        # The later imported server should win\n        templates = await client.list_resource_templates()\n        template_uris = [t.uriTemplate for t in templates]\n        assert \"users://{user_id}/profile\" in template_uris\n        assert (\n            template_uris.count(\"users://{user_id}/profile\") == 1\n        )  # Should only appear once\n\n        result = await client.read_resource(\"users://123/profile\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"Second app user 123\"\n\n\nasync def test_import_conflict_resolution_prompts():\n    \"\"\"Test that later imported prompts overwrite earlier ones when names conflict.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    first_app = FastMCP(\"FirstApp\")\n    second_app = FastMCP(\"SecondApp\")\n\n    @first_app.prompt(name=\"shared_prompt\")\n    def first_shared_prompt() -> str:\n        return \"First app prompt\"\n\n    @second_app.prompt(name=\"shared_prompt\")\n    def second_shared_prompt() -> str:\n        return \"Second app prompt\"\n\n    # Import both apps without prefix\n    await main_app.import_server(first_app)\n    await main_app.import_server(second_app)\n\n    async with Client(main_app) as client:\n        # The later imported server should win\n        prompts = await client.list_prompts()\n        prompt_names = [p.name for p in prompts]\n        assert \"shared_prompt\" in prompt_names\n        assert prompt_names.count(\"shared_prompt\") == 1  # Should only appear once\n\n        result = await client.get_prompt(\"shared_prompt\", {})\n        assert result.messages is not None\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Second app prompt\"\n\n\nasync def test_import_conflict_resolution_with_prefix():\n    \"\"\"Test that later imported components overwrite earlier ones when prefixed names conflict.\"\"\"\n    main_app = FastMCP(\"MainApp\")\n    first_app = FastMCP(\"FirstApp\")\n    second_app = FastMCP(\"SecondApp\")\n\n    @first_app.tool(name=\"shared_tool\")\n    def first_shared_tool() -> str:\n        return \"First app tool\"\n\n    @second_app.tool(name=\"shared_tool\")\n    def second_shared_tool() -> str:\n        return \"Second app tool\"\n\n    # Import both apps with same prefix\n    await main_app.import_server(first_app, \"api\")\n    await main_app.import_server(second_app, \"api\")\n\n    async with Client(main_app) as client:\n        # The later imported server should win\n        tools = await client.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"api_shared_tool\" in tool_names\n        assert tool_names.count(\"api_shared_tool\") == 1  # Should only appear once\n\n        result = await client.call_tool(\"api_shared_tool\", {})\n        assert result.data == \"Second app tool\"\n\n\nasync def test_import_server_resource_uri_prefixing():\n    \"\"\"Test that resource URIs are prefixed when using import_server (names are NOT prefixed).\"\"\"\n    # Create a sub-server with a resource\n    sub_server = FastMCP(\"SubServer\")\n\n    @sub_server.resource(\"resource://test_resource\")\n    def test_resource() -> str:\n        return \"Test content\"\n\n    # Create main server and import sub-server with prefix\n    main_server = FastMCP(\"MainServer\")\n    await main_server.import_server(sub_server, prefix=\"imported\")\n\n    # Get resources and verify URI prefixing (name should NOT be prefixed)\n    resources = await main_server.list_resources()\n    resource = next(\n        r for r in resources if str(r.uri) == \"resource://imported/test_resource\"\n    )\n    assert resource.name == \"test_resource\"\n\n\nasync def test_import_server_resource_template_uri_prefixing():\n    \"\"\"Test that resource template URIs are prefixed when using import_server (names are NOT prefixed).\"\"\"\n    # Create a sub-server with a resource template\n    sub_server = FastMCP(\"SubServer\")\n\n    @sub_server.resource(\"resource://data/{item_id}\")\n    def data_template(item_id: str) -> str:\n        return f\"Data for {item_id}\"\n\n    # Create main server and import sub-server with prefix\n    main_server = FastMCP(\"MainServer\")\n    await main_server.import_server(sub_server, prefix=\"imported\")\n\n    # Get resource templates and verify URI prefixing (name should NOT be prefixed)\n    templates = await main_server.list_resource_templates()\n    template = next(\n        t for t in templates if t.uri_template == \"resource://imported/data/{item_id}\"\n    )\n    assert template.name == \"data_template\"\n\n\nasync def test_import_server_with_new_prefix_format():\n    \"\"\"Test that import_server correctly uses the new prefix format.\"\"\"\n    # Create a server with resources\n    source_server = FastMCP(name=\"SourceServer\")\n\n    @source_server.resource(\"resource://test-resource\")\n    def get_resource():\n        return \"Resource content\"\n\n    @source_server.resource(\"resource:///absolute/path\")\n    def get_absolute_resource():\n        return \"Absolute resource content\"\n\n    @source_server.resource(\"resource://{param}/template\")\n    def get_template_resource(param: str):\n        return f\"Template resource with {param}\"\n\n    # Create target server and import the source server\n    target_server = FastMCP(name=\"TargetServer\")\n    await target_server.import_server(source_server, \"imported\")\n\n    # Check that the resources were imported with the correct prefixes\n    resources = await target_server.list_resources()\n    templates = await target_server.list_resource_templates()\n\n    assert any(str(r.uri) == \"resource://imported/test-resource\" for r in resources)\n    assert any(str(r.uri) == \"resource://imported//absolute/path\" for r in resources)\n    assert any(\n        t.uri_template == \"resource://imported/{param}/template\" for t in templates\n    )\n\n    # Verify we can access the resources\n    async with Client(target_server) as client:\n        result = await client.read_resource(\"resource://imported/test-resource\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"Resource content\"\n\n        result = await client.read_resource(\"resource://imported//absolute/path\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"Absolute resource content\"\n\n        result = await client.read_resource(\"resource://imported/param-value/template\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"Template resource with param-value\"\n"
  },
  {
    "path": "tests/deprecated/test_openapi_deprecations.py",
    "content": "\"\"\"Tests for OpenAPI-related deprecations in 2.14.\"\"\"\n\nimport importlib\nimport warnings\n\nimport pytest\n\n\nclass TestExperimentalOpenAPIImportDeprecation:\n    \"\"\"Test experimental OpenAPI import path deprecations.\"\"\"\n\n    def test_experimental_server_openapi_import_warns(self):\n        \"\"\"Importing from fastmcp.experimental.server.openapi should warn.\"\"\"\n        import fastmcp.experimental.server.openapi\n\n        with pytest.warns(\n            DeprecationWarning,\n            match=r\"Importing from fastmcp\\.experimental\\.server\\.openapi is deprecated\",\n        ):\n            importlib.reload(fastmcp.experimental.server.openapi)\n\n    def test_experimental_utilities_openapi_import_warns(self):\n        \"\"\"Importing from fastmcp.experimental.utilities.openapi should warn.\"\"\"\n        import fastmcp.experimental.utilities.openapi\n\n        with pytest.warns(\n            DeprecationWarning,\n            match=r\"Importing from fastmcp\\.experimental\\.utilities\\.openapi is deprecated\",\n        ):\n            importlib.reload(fastmcp.experimental.utilities.openapi)\n\n    def test_experimental_imports_resolve_to_same_classes(self):\n        \"\"\"Experimental imports should resolve to the same classes as main imports.\"\"\"\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\")\n\n            from fastmcp.experimental.server.openapi import (\n                FastMCPOpenAPI as ExpFastMCPOpenAPI,\n            )\n            from fastmcp.experimental.server.openapi import MCPType as ExpMCPType\n            from fastmcp.experimental.server.openapi import RouteMap as ExpRouteMap\n            from fastmcp.experimental.utilities.openapi import (\n                HTTPRoute as ExpHTTPRoute,\n            )\n            from fastmcp.server.openapi import FastMCPOpenAPI, MCPType, RouteMap\n            from fastmcp.utilities.openapi import HTTPRoute\n\n        assert FastMCPOpenAPI is ExpFastMCPOpenAPI\n        assert RouteMap is ExpRouteMap\n        assert MCPType is ExpMCPType\n        assert HTTPRoute is ExpHTTPRoute\n"
  },
  {
    "path": "tests/deprecated/test_settings.py",
    "content": "import pytest\n\nfrom fastmcp import FastMCP\n\n\nclass TestRemovedServerInitKwargs:\n    \"\"\"Test that removed server initialization keyword arguments raise TypeError.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"kwarg, value, expected_message\",\n        [\n            (\"host\", \"0.0.0.0\", \"run_http_async\"),\n            (\"port\", 8080, \"run_http_async\"),\n            (\"sse_path\", \"/custom-sse\", \"FASTMCP_SSE_PATH\"),\n            (\"message_path\", \"/custom-message\", \"FASTMCP_MESSAGE_PATH\"),\n            (\"streamable_http_path\", \"/custom-http\", \"run_http_async\"),\n            (\"json_response\", True, \"run_http_async\"),\n            (\"stateless_http\", True, \"run_http_async\"),\n            (\"debug\", True, \"FASTMCP_DEBUG\"),\n            (\"log_level\", \"DEBUG\", \"run_http_async\"),\n            (\"on_duplicate_tools\", \"warn\", \"on_duplicate=\"),\n            (\"on_duplicate_resources\", \"error\", \"on_duplicate=\"),\n            (\"on_duplicate_prompts\", \"replace\", \"on_duplicate=\"),\n            (\"tool_serializer\", lambda x: str(x), \"ToolResult\"),\n            (\"include_tags\", {\"public\"}, \"server.enable\"),\n            (\"exclude_tags\", {\"internal\"}, \"server.disable\"),\n            (\n                \"tool_transformations\",\n                {\"my_tool\": {\"name\": \"renamed\"}},\n                \"server.add_transform\",\n            ),\n        ],\n    )\n    def test_removed_kwarg_raises_type_error(self, kwarg, value, expected_message):\n        with pytest.raises(TypeError, match=f\"no longer accepts `{kwarg}`\"):\n            FastMCP(\"TestServer\", **{kwarg: value})\n\n    @pytest.mark.parametrize(\n        \"kwarg, value, expected_message\",\n        [\n            (\"host\", \"0.0.0.0\", \"run_http_async\"),\n            (\"on_duplicate_tools\", \"warn\", \"on_duplicate=\"),\n            (\"include_tags\", {\"public\"}, \"server.enable\"),\n        ],\n    )\n    def test_removed_kwarg_error_includes_migration_hint(\n        self, kwarg, value, expected_message\n    ):\n        with pytest.raises(TypeError, match=expected_message):\n            FastMCP(\"TestServer\", **{kwarg: value})\n\n    def test_unknown_kwarg_raises_standard_type_error(self):\n        with pytest.raises(TypeError, match=\"unexpected keyword argument\"):\n            FastMCP(\"TestServer\", **{\"totally_fake_param\": True})  # ty: ignore[invalid-argument-type]\n\n    def test_valid_kwargs_still_work(self):\n        server = FastMCP(\n            name=\"TestServer\",\n            instructions=\"Test instructions\",\n            on_duplicate=\"warn\",\n            mask_error_details=True,\n        )\n        assert server.name == \"TestServer\"\n        assert server.instructions == \"Test instructions\"\n"
  },
  {
    "path": "tests/deprecated/test_tool_injection_middleware.py",
    "content": "\"\"\"Tests for deprecated PromptToolMiddleware and ResourceToolMiddleware.\"\"\"\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp.types import TextContent\nfrom mcp.types import Tool as SDKTool\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.client import CallToolResult\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.server.middleware.tool_injection import (\n    PromptToolMiddleware,\n    ResourceToolMiddleware,\n)\n\n\nclass TestPromptToolMiddleware:\n    \"\"\"Tests for PromptToolMiddleware.\"\"\"\n\n    @pytest.fixture\n    def server_with_prompts(self):\n        \"\"\"Create a FastMCP server with prompts.\"\"\"\n        mcp = FastMCP(\"PromptServer\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        @mcp.prompt\n        def greeting(name: str) -> str:\n            \"\"\"Generate a greeting message.\"\"\"\n            return f\"Hello, {name}!\"\n\n        @mcp.prompt\n        def farewell(name: str) -> str:\n            \"\"\"Generate a farewell message.\"\"\"\n            return f\"Goodbye, {name}!\"\n\n        return mcp\n\n    async def test_prompt_tools_added_to_list(self, server_with_prompts: FastMCP):\n        \"\"\"Test that prompt tools are added to the tool list.\"\"\"\n        middleware = PromptToolMiddleware()\n        server_with_prompts.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](server_with_prompts) as client:\n            tools: list[SDKTool] = await client.list_tools()\n\n        tool_names: list[str] = [tool.name for tool in tools]\n        # Should have: add, list_prompts, get_prompt\n        assert len(tools) == 3\n        assert \"add\" in tool_names\n        assert \"list_prompts\" in tool_names\n        assert \"get_prompt\" in tool_names\n\n    async def test_list_prompts_tool_works(self, server_with_prompts: FastMCP):\n        \"\"\"Test that the list_prompts tool can be called.\"\"\"\n        middleware = PromptToolMiddleware()\n        server_with_prompts.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](server_with_prompts) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"list_prompts\", arguments={}\n            )\n\n        assert result.content == snapshot(\n            [\n                TextContent(\n                    type=\"text\",\n                    text='[{\"name\":\"greeting\",\"title\":null,\"description\":\"Generate a greeting message.\",\"arguments\":[{\"name\":\"name\",\"description\":null,\"required\":true}],\"icons\":null,\"_meta\":{\"fastmcp\":{\"tags\":[]}}},{\"name\":\"farewell\",\"title\":null,\"description\":\"Generate a farewell message.\",\"arguments\":[{\"name\":\"name\",\"description\":null,\"required\":true}],\"icons\":null,\"_meta\":{\"fastmcp\":{\"tags\":[]}}}]',\n                )\n            ]\n        )\n        assert result.structured_content is not None\n        assert result.structured_content[\"result\"] == snapshot(\n            [\n                {\n                    \"name\": \"greeting\",\n                    \"title\": None,\n                    \"description\": \"Generate a greeting message.\",\n                    \"arguments\": [\n                        {\"name\": \"name\", \"description\": None, \"required\": True}\n                    ],\n                    \"icons\": None,\n                    \"_meta\": {\"fastmcp\": {\"tags\": []}},\n                },\n                {\n                    \"name\": \"farewell\",\n                    \"title\": None,\n                    \"description\": \"Generate a farewell message.\",\n                    \"arguments\": [\n                        {\"name\": \"name\", \"description\": None, \"required\": True}\n                    ],\n                    \"icons\": None,\n                    \"_meta\": {\"fastmcp\": {\"tags\": []}},\n                },\n            ]\n        )\n\n    async def test_get_prompt_tool_works(self, server_with_prompts: FastMCP):\n        \"\"\"Test that the get_prompt tool can be called.\"\"\"\n        middleware = PromptToolMiddleware()\n        server_with_prompts.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](server_with_prompts) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"get_prompt\",\n                arguments={\"name\": \"greeting\", \"arguments\": {\"name\": \"World\"}},\n            )\n\n        # The tool returns the prompt result with structured_content\n        assert result.content == snapshot(\n            [\n                TextContent(\n                    type=\"text\",\n                    text='{\"_meta\":null,\"description\":\"Generate a greeting message.\",\"messages\":[{\"role\":\"user\",\"content\":{\"type\":\"text\",\"text\":\"Hello, World!\",\"annotations\":null,\"_meta\":null}}]}',\n                )\n            ]\n        )\n        assert result.structured_content is not None\n        assert result.structured_content == snapshot(\n            {\n                \"_meta\": None,\n                \"description\": \"Generate a greeting message.\",\n                \"messages\": [\n                    {\n                        \"role\": \"user\",\n                        \"content\": {\n                            \"type\": \"text\",\n                            \"text\": \"Hello, World!\",\n                            \"annotations\": None,\n                            \"_meta\": None,\n                        },\n                    }\n                ],\n            }\n        )\n\n\nclass TestResourceToolMiddleware:\n    \"\"\"Tests for ResourceToolMiddleware.\"\"\"\n\n    @pytest.fixture\n    def server_with_resources(self):\n        \"\"\"Create a FastMCP server with resources.\"\"\"\n        mcp = FastMCP(\"ResourceServer\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        @mcp.resource(\"file://config.txt\")\n        def config_resource() -> str:\n            \"\"\"Get configuration.\"\"\"\n            return \"debug=true\"\n\n        @mcp.resource(\"file://data.json\")\n        def data_resource() -> str:\n            \"\"\"Get data.\"\"\"\n            return '{\"count\": 42}'\n\n        return mcp\n\n    async def test_resource_tools_added_to_list(self, server_with_resources: FastMCP):\n        \"\"\"Test that resource tools are added to the tool list.\"\"\"\n        middleware = ResourceToolMiddleware()\n        server_with_resources.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](server_with_resources) as client:\n            tools: list[SDKTool] = await client.list_tools()\n\n        tool_names: list[str] = [tool.name for tool in tools]\n        # Should have: add, list_resources, read_resource\n        assert len(tools) == 3\n        assert \"add\" in tool_names\n        assert \"list_resources\" in tool_names\n        assert \"read_resource\" in tool_names\n\n    async def test_list_resources_tool_works(self, server_with_resources: FastMCP):\n        \"\"\"Test that the list_resources tool can be called.\"\"\"\n        middleware = ResourceToolMiddleware()\n        server_with_resources.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](server_with_resources) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"list_resources\", arguments={}\n            )\n\n        assert result.structured_content is not None\n        assert result.structured_content[\"result\"] == snapshot(\n            [\n                {\n                    \"name\": \"config_resource\",\n                    \"title\": None,\n                    \"uri\": \"file://config.txt/\",\n                    \"description\": \"Get configuration.\",\n                    \"mimeType\": \"text/plain\",\n                    \"size\": None,\n                    \"icons\": None,\n                    \"annotations\": None,\n                    \"_meta\": {\"fastmcp\": {\"tags\": []}},\n                },\n                {\n                    \"name\": \"data_resource\",\n                    \"title\": None,\n                    \"uri\": \"file://data.json/\",\n                    \"description\": \"Get data.\",\n                    \"mimeType\": \"text/plain\",\n                    \"size\": None,\n                    \"icons\": None,\n                    \"annotations\": None,\n                    \"_meta\": {\"fastmcp\": {\"tags\": []}},\n                },\n            ]\n        )\n\n    async def test_read_resource_tool_works(self, server_with_resources: FastMCP):\n        \"\"\"Test that the read_resource tool can be called.\"\"\"\n        middleware = ResourceToolMiddleware()\n        server_with_resources.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](server_with_resources) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"read_resource\", arguments={\"uri\": \"file://config.txt\"}\n            )\n\n        assert result.content == snapshot(\n            [\n                TextContent(\n                    type=\"text\",\n                    text='{\"contents\":[{\"content\":\"debug=true\",\"mime_type\":\"text/plain\",\"meta\":null}],\"meta\":null}',\n                )\n            ]\n        )\n        assert result.structured_content == snapshot(\n            {\n                \"contents\": [\n                    {\"content\": \"debug=true\", \"mime_type\": \"text/plain\", \"meta\": None}\n                ],\n                \"meta\": None,\n            }\n        )\n"
  },
  {
    "path": "tests/deprecated/test_tool_serializer.py",
    "content": "\"\"\"Tests for deprecated tool serializer functionality.\n\nThese tests verify that serializer parameters still work but are deprecated.\nAll serializer-related tests should be moved here.\n\"\"\"\n\nimport warnings\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.contrib.mcp_mixin import mcp_tool\nfrom fastmcp.server.providers import LocalProvider\nfrom fastmcp.tools.base import Tool, _convert_to_content\nfrom fastmcp.tools.tool_transform import TransformedTool\nfrom fastmcp.utilities.tests import temporary_settings\n\n\nclass TestToolSerializerDeprecated:\n    \"\"\"Tests for deprecated serializer functionality.\"\"\"\n\n    async def test_tool_serializer(self):\n        \"\"\"Test that a tool's serializer is used to serialize the result.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom serializer: {data}\"\n\n        def process_list(items: list[int]) -> int:\n            return sum(items)\n\n        tool = Tool.from_function(process_list, serializer=custom_serializer)\n\n        result = await tool.run(arguments={\"items\": [1, 2, 3, 4, 5]})\n        # Custom serializer affects unstructured content\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Custom serializer: 15\"\n        # Structured output should have the raw value\n        assert result.structured_content == {\"result\": 15}\n\n    def test_custom_serializer(self):\n        \"\"\"Test that a custom serializer is used for non-MCP types.\"\"\"\n\n        def custom_serializer(data):\n            return f\"Serialized: {data}\"\n\n        result = _convert_to_content({\"a\": 1}, serializer=custom_serializer)\n\n        assert result == snapshot(\n            [TextContent(type=\"text\", text=\"Serialized: {'a': 1}\")]\n        )\n\n    def test_custom_serializer_error_fallback(self, caplog):\n        \"\"\"Test that if a custom serializer fails, it falls back to the default.\"\"\"\n\n        def custom_serializer_that_fails(data):\n            raise ValueError(\"Serialization failed\")\n\n        result = _convert_to_content({\"a\": 1}, serializer=custom_serializer_that_fails)\n\n        assert isinstance(result, list)\n        assert result == snapshot([TextContent(type=\"text\", text='{\"a\":1}')])\n\n        assert \"Error serializing tool result\" in caplog.text\n\n\nclass TestSerializerDeprecationWarnings:\n    \"\"\"Tests that deprecation warnings are raised when serializer is used.\"\"\"\n\n    def test_tool_from_function_serializer_warning(self):\n        \"\"\"Test that Tool.from_function warns when serializer is provided.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(DeprecationWarning, match=\"serializer.*deprecated\"):\n                Tool.from_function(my_tool, serializer=custom_serializer)\n\n    def test_tool_from_function_serializer_no_warning_when_disabled(self):\n        \"\"\"Test that no warning is raised when deprecation_warnings is False.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        with temporary_settings(deprecation_warnings=False):\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"error\")\n                # Should not raise\n                Tool.from_function(my_tool, serializer=custom_serializer)\n\n    def test_local_provider_tool_serializer_warning(self):\n        \"\"\"Test that LocalProvider.tool warns when serializer is provided.\"\"\"\n        provider = LocalProvider()\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(DeprecationWarning, match=\"serializer.*deprecated\"):\n                provider.tool(my_tool, serializer=custom_serializer)\n\n    def test_local_provider_tool_decorator_serializer_warning(self):\n        \"\"\"Test that LocalProvider.tool decorator warns when serializer is provided.\"\"\"\n        provider = LocalProvider()\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(DeprecationWarning, match=\"serializer.*deprecated\"):\n\n                @provider.tool(serializer=custom_serializer)\n                def my_tool(x: int) -> int:\n                    return x * 2\n\n    def test_fastmcp_tool_serializer_warning(self):\n        \"\"\"Test that FastMCP.tool warns when serializer is provided via LocalProvider.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        # FastMCP.tool doesn't accept serializer directly, it goes through LocalProvider\n        # So we test LocalProvider.tool which is what FastMCP uses internally\n        provider = LocalProvider()\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(DeprecationWarning, match=\"serializer.*deprecated\"):\n                provider.tool(my_tool, serializer=custom_serializer)\n\n    def test_fastmcp_tool_serializer_parameter_raises_type_error(self):\n        \"\"\"Test that FastMCP tool_serializer parameter raises TypeError.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        with pytest.raises(TypeError, match=\"no longer accepts `tool_serializer`\"):\n            FastMCP(\"TestServer\", tool_serializer=custom_serializer)\n\n    def test_transformed_tool_from_tool_serializer_warning(self):\n        \"\"\"Test that TransformedTool.from_tool warns when serializer is provided.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        parent_tool = Tool.from_function(my_tool)\n\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(DeprecationWarning, match=\"serializer.*deprecated\"):\n                TransformedTool.from_tool(parent_tool, serializer=custom_serializer)\n\n    def test_mcp_mixin_tool_serializer_warning(self):\n        \"\"\"Test that mcp_tool decorator warns when serializer is provided.\"\"\"\n\n        def custom_serializer(data) -> str:\n            return f\"Custom: {data}\"\n\n        with temporary_settings(deprecation_warnings=True):\n            with pytest.warns(DeprecationWarning, match=\"serializer.*deprecated\"):\n\n                @mcp_tool(serializer=custom_serializer)\n                def my_tool(x: int) -> int:\n                    return x * 2\n"
  },
  {
    "path": "tests/experimental/README.md",
    "content": "# Experimental Tests\n\nEach directory in this folder tests a discrete experimental feature. The directory structure within each experiment should approximate the \"normal\" test directory structure to facilitate moving tests into place once/if the experiment is merged into the main codebase."
  },
  {
    "path": "tests/experimental/__init__.py",
    "content": ""
  },
  {
    "path": "tests/experimental/transforms/test_code_mode.py",
    "content": "import importlib\nimport json\nfrom typing import Any\n\nimport pytest\nfrom mcp.types import ImageContent, TextContent\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.experimental.transforms.code_mode import (\n    CodeMode,\n    GetSchemas,\n    GetToolCatalog,\n    MontySandboxProvider,\n    Search,\n    _ensure_async,\n)\nfrom fastmcp.server.context import Context\nfrom fastmcp.tools.base import Tool, ToolResult\n\n\ndef _unwrap_result(result: ToolResult) -> Any:\n    \"\"\"Extract the logical return value from a ToolResult.\"\"\"\n    if result.structured_content is not None:\n        return result.structured_content\n\n    text_blocks = [\n        content.text for content in result.content if isinstance(content, TextContent)\n    ]\n    if not text_blocks:\n        return None\n\n    if len(text_blocks) == 1:\n        try:\n            return json.loads(text_blocks[0])\n        except json.JSONDecodeError:\n            return text_blocks[0]\n\n    values: list[Any] = []\n    for text in text_blocks:\n        try:\n            values.append(json.loads(text))\n        except json.JSONDecodeError:\n            values.append(text)\n    return values\n\n\ndef _unwrap_string_result(result: ToolResult) -> str:\n    \"\"\"Extract a string result from a ToolResult.\n\n    String results are wrapped in ``{\"result\": \"...\"}`` by the\n    structured-output convention.\n    \"\"\"\n    data = _unwrap_result(result)\n    if isinstance(data, dict) and \"result\" in data:\n        return data[\"result\"]\n    assert isinstance(data, str)\n    return data\n\n\nclass _UnsafeTestSandboxProvider:\n    \"\"\"UNSAFE: Uses exec() for testing only. Never use in production.\"\"\"\n\n    async def run(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        external_functions: dict[str, Any] | None = None,\n    ) -> Any:\n        namespace: dict[str, Any] = {}\n        if inputs:\n            namespace.update(inputs)\n        if external_functions:\n            namespace.update(\n                {key: _ensure_async(value) for key, value in external_functions.items()}\n            )\n\n        wrapped = \"async def __test_main__():\\n\"\n        for line in code.splitlines():\n            wrapped += f\"    {line}\\n\"\n        if not code.strip():\n            wrapped += \"    return None\\n\"\n\n        exec(wrapped, namespace, namespace)\n        return await namespace[\"__test_main__\"]()\n\n\nasync def _run_tool(\n    server: FastMCP, name: str, arguments: dict[str, Any]\n) -> ToolResult:\n    return await server.call_tool(name, arguments)\n\n\n# ---------------------------------------------------------------------------\n# CodeMode core tests\n# ---------------------------------------------------------------------------\n\n\nasync def test_code_mode_default_tools() -> None:\n    \"\"\"Default CodeMode exposes search, get_schema, and execute.\"\"\"\n    mcp = FastMCP(\"CodeMode Default\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    listed_tools = await mcp.list_tools(run_middleware=False)\n    assert {tool.name for tool in listed_tools} == {\"search\", \"get_schema\", \"execute\"}\n\n\nasync def test_code_mode_search_returns_lightweight_results() -> None:\n    \"\"\"Default search returns tool names and descriptions, not full schemas.\"\"\"\n    mcp = FastMCP(\"CodeMode Search\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square of a number.\"\"\"\n        return x * x\n\n    @mcp.tool\n    def greet(name: str) -> str:\n        \"\"\"Say hello to someone.\"\"\"\n        return f\"Hello, {name}!\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"square number\"})\n    text = _unwrap_string_result(result)\n    assert \"square\" in text\n    assert \"Compute the square\" in text\n    # Should NOT contain full schema details\n    assert \"inputSchema\" not in text\n\n\nasync def test_code_mode_get_schema_brief() -> None:\n    \"\"\"get_schema with detail=brief returns names and descriptions only.\"\"\"\n    mcp = FastMCP(\"CodeMode Schema Brief\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square of a number.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp, \"get_schema\", {\"tools\": [\"square\"], \"detail\": \"brief\"}\n    )\n    text = _unwrap_string_result(result)\n    assert \"square\" in text\n    assert \"Compute the square\" in text\n    # brief should NOT include parameter details\n    assert \"**Parameters**\" not in text\n\n\nasync def test_code_mode_get_schema_detailed() -> None:\n    \"\"\"get_schema with detail=detailed returns markdown with parameter info.\"\"\"\n    mcp = FastMCP(\"CodeMode Schema Detailed\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square of a number.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp, \"get_schema\", {\"tools\": [\"square\"], \"detail\": \"detailed\"}\n    )\n    text = _unwrap_string_result(result)\n    assert \"### square\" in text\n    assert \"Compute the square\" in text\n    assert \"**Parameters**\" in text\n    assert \"`x` (integer, required)\" in text\n\n\nasync def test_code_mode_get_schema_full() -> None:\n    \"\"\"get_schema with detail=full returns JSON schema.\"\"\"\n    mcp = FastMCP(\"CodeMode Schema Full\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square of a number.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"get_schema\", {\"tools\": [\"square\"], \"detail\": \"full\"})\n    text = _unwrap_string_result(result)\n    parsed = json.loads(text)\n    assert isinstance(parsed, list)\n    assert parsed[0][\"name\"] == \"square\"\n    assert \"inputSchema\" in parsed[0]\n\n\nasync def test_code_mode_get_schema_default_is_detailed() -> None:\n    \"\"\"get_schema defaults to detailed (markdown with parameters).\"\"\"\n    mcp = FastMCP(\"CodeMode Schema Default\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square of a number.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"get_schema\", {\"tools\": [\"square\"]})\n    text = _unwrap_string_result(result)\n    assert \"### square\" in text\n    assert \"**Parameters**\" in text\n\n\nasync def test_code_mode_get_schema_not_found() -> None:\n    \"\"\"get_schema reports tools that don't exist in the catalog.\"\"\"\n    mcp = FastMCP(\"CodeMode Schema NotFound\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"get_schema\", {\"tools\": [\"nonexistent\"]})\n    text = _unwrap_string_result(result)\n    assert \"not found\" in text.lower()\n    assert \"nonexistent\" in text\n\n\nasync def test_code_mode_get_schema_partial_match() -> None:\n    \"\"\"get_schema returns schemas for found tools and reports missing ones.\"\"\"\n    mcp = FastMCP(\"CodeMode Schema Partial\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"get_schema\", {\"tools\": [\"square\", \"nonexistent\"]})\n    text = _unwrap_string_result(result)\n    assert \"### square\" in text\n    assert \"nonexistent\" in text\n\n\nasync def test_code_mode_execute_works() -> None:\n    \"\"\"Execute tool can call backend tools through the sandbox.\"\"\"\n    mcp = FastMCP(\"CodeMode Execute\")\n\n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        return x + y\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp, \"execute\", {\"code\": \"return await call_tool('add', {'x': 2, 'y': 3})\"}\n    )\n    assert _unwrap_result(result) == {\"result\": 5}\n\n\n# ---------------------------------------------------------------------------\n# Tool naming and configuration\n# ---------------------------------------------------------------------------\n\n\nasync def test_code_mode_custom_execute_name() -> None:\n    mcp = FastMCP(\"CodeMode Custom Execute\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(\n        CodeMode(\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n            execute_tool_name=\"run_code\",\n        )\n    )\n\n    listed = await mcp.list_tools(run_middleware=False)\n    names = {t.name for t in listed}\n    assert \"run_code\" in names\n    assert \"execute\" not in names\n\n\nasync def test_code_mode_custom_execute_description() -> None:\n    mcp = FastMCP(\"CodeMode Custom Desc\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(\n        CodeMode(\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n            execute_description=\"Custom execute description\",\n        )\n    )\n\n    listed = await mcp.list_tools(run_middleware=False)\n    by_name = {t.name: t for t in listed}\n    assert by_name[\"execute\"].description == \"Custom execute description\"\n\n\nasync def test_code_mode_default_execute_description() -> None:\n    mcp = FastMCP(\"CodeMode Defaults\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    listed = await mcp.list_tools(run_middleware=False)\n    by_name = {t.name: t for t in listed}\n    desc = by_name[\"execute\"].description or \"\"\n\n    assert \"single block\" in desc\n    assert \"Use `return` to produce output.\" in desc\n    assert (\n        \"Only `call_tool(tool_name: str, params: dict) -> Any` is available in scope.\"\n        in desc\n    )\n\n\n# ---------------------------------------------------------------------------\n# Discovery tool customization\n# ---------------------------------------------------------------------------\n\n\nasync def test_code_mode_no_discovery_tools() -> None:\n    \"\"\"CodeMode with empty discovery_tools exposes only execute.\"\"\"\n    mcp = FastMCP(\"CodeMode No Discovery\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    listed = await mcp.list_tools(run_middleware=False)\n    assert {t.name for t in listed} == {\"execute\"}\n\n\nasync def test_code_mode_custom_discovery_tool_function() -> None:\n    \"\"\"A plain function can serve as a discovery tool factory.\"\"\"\n    mcp = FastMCP(\"CodeMode Custom Discovery\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    def list_all(get_catalog: GetToolCatalog) -> Tool:\n        async def list_tools(\n            ctx: Context = None,  # type: ignore[assignment]\n        ) -> str:\n            \"\"\"List all available tools.\"\"\"\n            tools = await get_catalog(ctx)\n            return \", \".join(t.name for t in tools)\n\n        return Tool.from_function(fn=list_tools, name=\"list_all\")\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[list_all],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    listed = await mcp.list_tools(run_middleware=False)\n    assert {t.name for t in listed} == {\"list_all\", \"execute\"}\n\n    result = await _run_tool(mcp, \"list_all\", {})\n    text = _unwrap_string_result(result)\n    assert \"square\" in text\n\n\nasync def test_code_mode_search_detailed() -> None:\n    \"\"\"Search with detail='detailed' returns markdown with parameter info.\"\"\"\n    mcp = FastMCP(\"CodeMode Search Detailed\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"square\", \"detail\": \"detailed\"})\n    text = _unwrap_string_result(result)\n    assert \"### square\" in text\n    assert \"Compute the square\" in text\n    assert \"**Parameters**\" in text\n    assert \"`x` (integer, required)\" in text\n\n\nasync def test_code_mode_search_tool_full_detail() -> None:\n    \"\"\"Search with detail='full' includes JSON schemas.\"\"\"\n    mcp = FastMCP(\"CodeMode Search Full\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[Search(default_detail=\"full\")],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"square\"})\n    text = _unwrap_string_result(result)\n    parsed = json.loads(text)\n    assert isinstance(parsed, list)\n    assert parsed[0][\"name\"] == \"square\"\n    assert \"inputSchema\" in parsed[0]\n\n\nasync def test_code_mode_custom_search_tool_name() -> None:\n    \"\"\"Search and GetSchemas support custom names.\"\"\"\n    mcp = FastMCP(\"CodeMode Custom Names\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[\n                Search(name=\"find\"),\n                GetSchemas(name=\"describe\"),\n            ],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    listed = await mcp.list_tools(run_middleware=False)\n    assert {t.name for t in listed} == {\"find\", \"describe\", \"execute\"}\n\n\ndef test_code_mode_rejects_discovery_execute_name_collision() -> None:\n    \"\"\"CodeMode raises ValueError when a discovery tool collides with execute.\"\"\"\n    cm = CodeMode(\n        discovery_tools=[Search(name=\"execute\")],\n        sandbox_provider=_UnsafeTestSandboxProvider(),\n    )\n    with pytest.raises(ValueError, match=\"collides\"):\n        cm._build_discovery_tools()\n\n\ndef test_code_mode_rejects_duplicate_discovery_names() -> None:\n    \"\"\"CodeMode raises ValueError when discovery tools have duplicate names.\"\"\"\n    cm = CodeMode(\n        discovery_tools=[Search(name=\"search\"), Search(name=\"search\")],\n        sandbox_provider=_UnsafeTestSandboxProvider(),\n    )\n    with pytest.raises(ValueError, match=\"unique\"):\n        cm._build_discovery_tools()\n\n\n# ---------------------------------------------------------------------------\n# Visibility and auth\n# ---------------------------------------------------------------------------\n\n\nasync def test_code_mode_execute_respects_disabled_tool_visibility() -> None:\n    mcp = FastMCP(\"CodeMode Disabled\")\n\n    @mcp.tool\n    def secret() -> str:\n        return \"nope\"\n\n    mcp.disable(names={\"secret\"}, components={\"tool\"})\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    with pytest.raises(ToolError, match=r\"Unknown tool\"):\n        await _run_tool(\n            mcp, \"execute\", {\"code\": \"return await call_tool('secret', {})\"}\n        )\n\n\nasync def test_code_mode_search_respects_disabled_tool_visibility() -> None:\n    mcp = FastMCP(\"CodeMode Disabled Search\")\n\n    @mcp.tool\n    def secret() -> str:\n        \"\"\"A secret tool.\"\"\"\n        return \"nope\"\n\n    mcp.disable(names={\"secret\"}, components={\"tool\"})\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"secret\"})\n    text = _unwrap_string_result(result)\n    assert \"secret\" not in text or \"No tools\" in text\n\n\nasync def test_code_mode_execute_sees_mid_run_visibility_changes() -> None:\n    \"\"\"Unlocking a tool mid-execution makes it callable in the same run.\"\"\"\n    mcp = FastMCP(\"CodeMode Unlock\")\n\n    @mcp.tool\n    async def unlock(ctx: Context) -> str:\n        await ctx.enable_components(names={\"secret\"}, components={\"tool\"})\n        return \"unlocked\"\n\n    @mcp.tool\n    async def secret() -> str:\n        return \"secret-ok\"\n\n    mcp.disable(names={\"secret\"}, components={\"tool\"})\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\n            \"execute\",\n            {\n                \"code\": \"await call_tool('unlock', {})\\nreturn await call_tool('secret', {})\"\n            },\n        )\n        assert result.data == {\"result\": \"secret-ok\"}\n\n\nasync def test_code_mode_execute_respects_tool_auth() -> None:\n    mcp = FastMCP(\"CodeMode Auth\")\n\n    @mcp.tool(auth=lambda _ctx: False)\n    def protected() -> str:\n        return \"nope\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    with pytest.raises(ToolError, match=r\"Unknown tool\"):\n        await _run_tool(\n            mcp, \"execute\", {\"code\": \"return await call_tool('protected', {})\"}\n        )\n\n\nasync def test_code_mode_search_respects_tool_auth() -> None:\n    mcp = FastMCP(\"CodeMode Auth Search\")\n\n    @mcp.tool(auth=lambda _ctx: False)\n    def protected() -> str:\n        \"\"\"A protected tool.\"\"\"\n        return \"nope\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"protected\"})\n    text = _unwrap_string_result(result)\n    assert \"protected\" not in text or \"No tools\" in text\n\n\nasync def test_code_mode_shadows_colliding_tool_names() -> None:\n    \"\"\"Backend tools with the same name as meta-tools are shadowed.\"\"\"\n    mcp = FastMCP(\"CodeMode Collision\")\n\n    @mcp.tool\n    def search() -> str:\n        return \"real search\"\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    tools = await mcp.list_tools(run_middleware=False)\n    tool_names = {t.name for t in tools}\n    assert \"execute\" in tool_names\n\n    result = await _run_tool(\n        mcp, \"execute\", {\"code\": 'return await call_tool(\"ping\", {})'}\n    )\n    assert _unwrap_result(result) == {\"result\": \"pong\"}\n\n\n# ---------------------------------------------------------------------------\n# get_tool pass-through\n# ---------------------------------------------------------------------------\n\n\nasync def test_code_mode_get_tool_returns_meta_tools_and_passes_through() -> None:\n    \"\"\"get_tool returns meta-tools by name and passes through backend tools.\"\"\"\n    mcp = FastMCP(\"CodeMode GetTool\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    search_tool = await mcp.get_tool(\"search\")\n    assert search_tool is not None\n    assert search_tool.name == \"search\"\n\n    schema_tool = await mcp.get_tool(\"get_schema\")\n    assert schema_tool is not None\n    assert schema_tool.name == \"get_schema\"\n\n    execute_tool = await mcp.get_tool(\"execute\")\n    assert execute_tool is not None\n    assert execute_tool.name == \"execute\"\n\n    ping_tool = await mcp.get_tool(\"ping\")\n    assert ping_tool is not None\n    assert ping_tool.name == \"ping\"\n\n\n# ---------------------------------------------------------------------------\n# Execute edge cases\n# ---------------------------------------------------------------------------\n\n\nasync def test_code_mode_execute_non_text_content_stringified() -> None:\n    mcp = FastMCP(\"CodeMode NonText\")\n\n    @mcp.tool\n    def image_tool() -> ImageContent:\n        return ImageContent(type=\"image\", data=\"base64data\", mimeType=\"image/png\")\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp, \"execute\", {\"code\": \"return await call_tool('image_tool', {})\"}\n    )\n    unwrapped = _unwrap_result(result)\n    assert isinstance(unwrapped, str)\n    assert \"base64data\" in unwrapped\n\n\nasync def test_code_mode_execute_multi_tool_chaining() -> None:\n    \"\"\"Execute block can chain multiple call_tool() calls.\"\"\"\n    mcp = FastMCP(\"CodeMode Chaining\")\n\n    @mcp.tool\n    def double(x: int) -> int:\n        return x * 2\n\n    @mcp.tool\n    def add_one(x: int) -> int:\n        return x + 1\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp,\n        \"execute\",\n        {\n            \"code\": (\n                \"a = await call_tool('double', {'x': 3})\\n\"\n                \"b = await call_tool('add_one', {'x': a['result']})\\n\"\n                \"return b\"\n            )\n        },\n    )\n    assert _unwrap_result(result) == {\"result\": 7}\n\n\nasync def test_code_mode_sandbox_error_surfaces_as_tool_error() -> None:\n    mcp = FastMCP(\"CodeMode Errors\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    with pytest.raises(ToolError):\n        await _run_tool(mcp, \"execute\", {\"code\": \"raise ValueError('boom')\"})\n\n\n# ---------------------------------------------------------------------------\n# Sandbox provider tests\n# ---------------------------------------------------------------------------\n\n\nasync def test_monty_provider_raises_informative_error_when_missing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    provider = MontySandboxProvider()\n    real_import_module = importlib.import_module\n\n    def _fake_import_module(name: str, package: str | None = None):\n        if name == \"pydantic_monty\":\n            raise ModuleNotFoundError(\"No module named 'pydantic_monty'\")\n        return real_import_module(name, package)\n\n    monkeypatch.setattr(importlib, \"import_module\", _fake_import_module)\n\n    with pytest.raises(ImportError, match=r\"fastmcp\\[code-mode\\]\"):\n        await provider.run(\"return 1\")\n\n\nasync def test_monty_provider_forwards_limits() -> None:\n    provider = MontySandboxProvider(limits={\"max_duration_secs\": 0.1})\n\n    with pytest.raises(Exception, match=\"time limit exceeded\"):\n        await provider.run(\"x = 0\\nfor _ in range(10**9):\\n    x += 1\")\n\n\nasync def test_monty_provider_no_limits_by_default() -> None:\n    provider = MontySandboxProvider()\n    result = await provider.run(\"return 1 + 2\")\n    assert result == 3\n"
  },
  {
    "path": "tests/experimental/transforms/test_code_mode_discovery.py",
    "content": "import json\nfrom typing import Any\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.experimental.transforms.code_mode import (\n    CodeMode,\n    GetTags,\n    ListTools,\n    Search,\n    _ensure_async,\n)\nfrom fastmcp.tools.base import ToolResult\n\n\ndef _unwrap_result(result: ToolResult) -> Any:\n    \"\"\"Extract the logical return value from a ToolResult.\"\"\"\n    if result.structured_content is not None:\n        return result.structured_content\n\n    text_blocks = [\n        content.text for content in result.content if isinstance(content, TextContent)\n    ]\n    if not text_blocks:\n        return None\n\n    if len(text_blocks) == 1:\n        try:\n            return json.loads(text_blocks[0])\n        except json.JSONDecodeError:\n            return text_blocks[0]\n\n    values: list[Any] = []\n    for text in text_blocks:\n        try:\n            values.append(json.loads(text))\n        except json.JSONDecodeError:\n            values.append(text)\n    return values\n\n\ndef _unwrap_string_result(result: ToolResult) -> str:\n    \"\"\"Extract a string result from a ToolResult.\"\"\"\n    data = _unwrap_result(result)\n    if isinstance(data, dict) and \"result\" in data:\n        return data[\"result\"]\n    assert isinstance(data, str)\n    return data\n\n\nclass _UnsafeTestSandboxProvider:\n    \"\"\"UNSAFE: Uses exec() for testing only. Never use in production.\"\"\"\n\n    async def run(\n        self,\n        code: str,\n        *,\n        inputs: dict[str, Any] | None = None,\n        external_functions: dict[str, Any] | None = None,\n    ) -> Any:\n        namespace: dict[str, Any] = {}\n        if inputs:\n            namespace.update(inputs)\n        if external_functions:\n            namespace.update(\n                {key: _ensure_async(value) for key, value in external_functions.items()}\n            )\n\n        wrapped = \"async def __test_main__():\\n\"\n        for line in code.splitlines():\n            wrapped += f\"    {line}\\n\"\n        if not code.strip():\n            wrapped += \"    return None\\n\"\n\n        exec(wrapped, namespace, namespace)\n        return await namespace[\"__test_main__\"]()\n\n\nasync def _run_tool(\n    server: FastMCP, name: str, arguments: dict[str, Any]\n) -> ToolResult:\n    return await server.call_tool(name, arguments)\n\n\n# ---------------------------------------------------------------------------\n# Tags discovery tool\n# ---------------------------------------------------------------------------\n\n\nasync def test_categories_brief_shows_tag_counts() -> None:\n    mcp = FastMCP(\"Tags Brief\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        return x + y\n\n    @mcp.tool(tags={\"math\"})\n    def multiply(x: int, y: int) -> int:\n        return x * y\n\n    @mcp.tool(tags={\"text\"})\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[GetTags()],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"tags\", {})\n    text = _unwrap_string_result(result)\n    assert \"math (2 tools)\" in text\n    assert \"text (1 tool)\" in text\n\n\nasync def test_categories_full_lists_tools_per_tag() -> None:\n    mcp = FastMCP(\"Tags Full\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    @mcp.tool(tags={\"text\"})\n    def greet(name: str) -> str:\n        \"\"\"Say hello.\"\"\"\n        return f\"Hello, {name}!\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[GetTags(default_detail=\"full\")],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"tags\", {})\n    text = _unwrap_string_result(result)\n    assert \"### math\" in text\n    assert \"- add: Add two numbers.\" in text\n    assert \"### text\" in text\n    assert \"- greet: Say hello.\" in text\n\n\nasync def test_categories_includes_untagged() -> None:\n    mcp = FastMCP(\"Tags Untagged\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        return x + y\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[GetTags()],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"tags\", {})\n    text = _unwrap_string_result(result)\n    assert \"math\" in text\n    assert \"untagged (1 tool)\" in text\n\n\nasync def test_categories_tool_in_multiple_tags() -> None:\n    mcp = FastMCP(\"Tags Multi-tag\")\n\n    @mcp.tool(tags={\"math\", \"core\"})\n    def add(x: int, y: int) -> int:\n        return x + y\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[GetTags(default_detail=\"full\")],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"tags\", {})\n    text = _unwrap_string_result(result)\n    assert \"### core\" in text\n    assert \"### math\" in text\n    # Tool appears under both tags\n    assert text.count(\"- add\") == 2\n\n\nasync def test_categories_detail_override_per_call() -> None:\n    \"\"\"LLM can override default_detail on a per-call basis.\"\"\"\n    mcp = FastMCP(\"Tags Override\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        \"\"\"Add numbers.\"\"\"\n        return x + y\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[GetTags()],  # default_detail=\"brief\"\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    # Override to full\n    result = await _run_tool(mcp, \"tags\", {\"detail\": \"full\"})\n    text = _unwrap_string_result(result)\n    assert \"### math\" in text\n    assert \"- add: Add numbers.\" in text\n\n\nasync def test_get_tags_empty_catalog() -> None:\n    \"\"\"GetTags with no tools returns 'No tools available.'.\"\"\"\n    mcp = FastMCP(\"CodeMode Empty Tags\")\n\n    mcp.disable(names={\"ping\"}, components={\"tool\"})\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[GetTags()],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"tags\", {})\n    text = _unwrap_string_result(result)\n    assert \"No tools available\" in text\n\n\n# ---------------------------------------------------------------------------\n# Search with tags filtering\n# ---------------------------------------------------------------------------\n\n\nasync def test_search_with_tags_filter() -> None:\n    mcp = FastMCP(\"Search Tags\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    @mcp.tool(tags={\"text\"})\n    def greet(name: str) -> str:\n        \"\"\"Say hello.\"\"\"\n        return f\"Hello, {name}!\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"add hello\", \"tags\": [\"math\"]})\n    text = _unwrap_string_result(result)\n    assert \"add\" in text\n    assert \"greet\" not in text\n\n\nasync def test_search_with_tags_filter_no_matches() -> None:\n    mcp = FastMCP(\"Search Tags Empty\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"add\", \"tags\": [\"nonexistent\"]})\n    text = _unwrap_string_result(result)\n    assert \"add\" not in text or \"No tools\" in text\n\n\nasync def test_search_without_tags_returns_all() -> None:\n    \"\"\"Search without tags parameter searches the full catalog.\"\"\"\n    mcp = FastMCP(\"Search No Tags\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    @mcp.tool(tags={\"text\"})\n    def greet(name: str) -> str:\n        \"\"\"Say hello.\"\"\"\n        return f\"Hello, {name}!\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"add hello\"})\n    text = _unwrap_string_result(result)\n    assert \"add\" in text\n    assert \"greet\" in text\n\n\nasync def test_search_with_untagged_filter() -> None:\n    \"\"\"Search with tags=[\"untagged\"] matches tools that have no tags.\"\"\"\n    mcp = FastMCP(\"Search Untagged\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    @mcp.tool\n    def ping() -> str:\n        \"\"\"Ping.\"\"\"\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"ping add\", \"tags\": [\"untagged\"]})\n    text = _unwrap_string_result(result)\n    assert \"ping\" in text\n    assert \"add\" not in text\n\n\nasync def test_search_default_detail_detailed_skips_get_schema() -> None:\n    \"\"\"Two-stage pattern: Search(default_detail='detailed') returns schemas inline.\"\"\"\n    mcp = FastMCP(\"CodeMode Two-Stage\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[Search(default_detail=\"detailed\")],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"square\"})\n    text = _unwrap_string_result(result)\n    assert \"### square\" in text\n    assert \"**Parameters**\" in text\n    assert \"`x` (integer, required)\" in text\n\n\nasync def test_search_full_detail_empty_results_returns_json() -> None:\n    \"\"\"Search with detail=full and no matches returns valid JSON, not plain text.\"\"\"\n    mcp = FastMCP(\"CodeMode Empty Full\")\n\n    @mcp.tool(tags={\"math\"})\n    def add(x: int, y: int) -> int:\n        return x + y\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp,\n        \"search\",\n        {\"query\": \"nonexistent\", \"tags\": [\"nonexistent\"], \"detail\": \"full\"},\n    )\n    text = _unwrap_string_result(result)\n    parsed = json.loads(text)\n    assert parsed == []\n\n\nasync def test_get_schema_empty_tools_list() -> None:\n    \"\"\"get_schema with an empty tools list returns no-match message.\"\"\"\n    mcp = FastMCP(\"CodeMode Empty Schema\")\n\n    @mcp.tool\n    def ping() -> str:\n        return \"pong\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"get_schema\", {\"tools\": []})\n    text = _unwrap_string_result(result)\n    assert \"No tools matched\" in text\n\n\nasync def test_get_schema_full_partial_match_returns_valid_json() -> None:\n    \"\"\"get_schema with detail=full and missing tools returns valid JSON.\"\"\"\n    mcp = FastMCP(\"Schema Full Partial\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(\n        mcp, \"get_schema\", {\"tools\": [\"square\", \"nonexistent\"], \"detail\": \"full\"}\n    )\n    text = _unwrap_string_result(result)\n    parsed = json.loads(text)\n    assert isinstance(parsed, list)\n    assert parsed[0][\"name\"] == \"square\"\n    assert parsed[-1] == {\"not_found\": [\"nonexistent\"]}\n\n\n# ---------------------------------------------------------------------------\n# Search catalog size annotation\n# ---------------------------------------------------------------------------\n\n\nasync def test_search_shows_catalog_size_when_results_are_subset() -> None:\n    \"\"\"Search annotates results with 'N of M tools' when not all tools match.\"\"\"\n    mcp = FastMCP(\"Search Annotation\")\n\n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    @mcp.tool\n    def multiply(x: int, y: int) -> int:\n        \"\"\"Multiply two numbers.\"\"\"\n        return x * y\n\n    @mcp.tool\n    def greet(name: str) -> str:\n        \"\"\"Say hello.\"\"\"\n        return f\"Hello, {name}!\"\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"add numbers\"})\n    text = _unwrap_string_result(result)\n    # Should show partial result count out of total catalog\n    assert \"of 3 tools:\" in text\n\n\nasync def test_search_omits_annotation_when_all_tools_returned() -> None:\n    \"\"\"Search does not annotate when results include every tool.\"\"\"\n    mcp = FastMCP(\"Search All Match\")\n\n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"add numbers\"})\n    text = _unwrap_string_result(result)\n    assert \"of\" not in text or \"tools:\" not in text\n\n\n# ---------------------------------------------------------------------------\n# Search limit\n# ---------------------------------------------------------------------------\n\n\nasync def test_search_limit_caps_results() -> None:\n    \"\"\"Search with limit returns at most that many results.\"\"\"\n    mcp = FastMCP(\"Search Limit\")\n\n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        \"\"\"Add numbers.\"\"\"\n        return x + y\n\n    @mcp.tool\n    def subtract(x: int, y: int) -> int:\n        \"\"\"Subtract numbers.\"\"\"\n        return x - y\n\n    @mcp.tool\n    def multiply(x: int, y: int) -> int:\n        \"\"\"Multiply numbers.\"\"\"\n        return x * y\n\n    mcp.add_transform(CodeMode(sandbox_provider=_UnsafeTestSandboxProvider()))\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"numbers\", \"limit\": 1})\n    text = _unwrap_string_result(result)\n    assert \"1 of 3 tools:\" in text\n    # Only one tool line (starts with \"- \")\n    tool_lines = [line for line in text.splitlines() if line.startswith(\"- \")]\n    assert len(tool_lines) == 1\n\n\nasync def test_search_default_limit_from_constructor() -> None:\n    \"\"\"Search(default_limit=N) caps results by default.\"\"\"\n    mcp = FastMCP(\"Search Default Limit\")\n\n    @mcp.tool\n    def a() -> str:\n        \"\"\"Tool A.\"\"\"\n        return \"a\"\n\n    @mcp.tool\n    def b() -> str:\n        \"\"\"Tool B.\"\"\"\n        return \"b\"\n\n    @mcp.tool\n    def c() -> str:\n        \"\"\"Tool C.\"\"\"\n        return \"c\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[Search(default_limit=2)],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"search\", {\"query\": \"tool\"})\n    text = _unwrap_string_result(result)\n    assert \"2 of 3 tools:\" in text\n\n\n# ---------------------------------------------------------------------------\n# ListTools discovery tool\n# ---------------------------------------------------------------------------\n\n\nasync def test_list_tools_brief() -> None:\n    \"\"\"ListTools at brief detail lists all tool names and descriptions.\"\"\"\n    mcp = FastMCP(\"ListTools Brief\")\n\n    @mcp.tool\n    def add(x: int, y: int) -> int:\n        \"\"\"Add two numbers.\"\"\"\n        return x + y\n\n    @mcp.tool\n    def multiply(x: int, y: int) -> int:\n        \"\"\"Multiply two numbers.\"\"\"\n        return x * y\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[ListTools()],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"list_tools\", {})\n    text = _unwrap_string_result(result)\n    assert \"add\" in text\n    assert \"multiply\" in text\n    assert \"Add two numbers\" in text\n    # brief should not include parameter details\n    assert \"**Parameters**\" not in text\n\n\nasync def test_list_tools_detailed() -> None:\n    \"\"\"ListTools at detailed shows parameter schemas.\"\"\"\n    mcp = FastMCP(\"ListTools Detailed\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square.\"\"\"\n        return x * x\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[ListTools(default_detail=\"detailed\")],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"list_tools\", {})\n    text = _unwrap_string_result(result)\n    assert \"### square\" in text\n    assert \"**Parameters**\" in text\n    assert \"`x` (integer, required)\" in text\n\n\nasync def test_list_tools_full_returns_json() -> None:\n    \"\"\"ListTools at full returns valid JSON with schemas.\"\"\"\n    mcp = FastMCP(\"ListTools Full\")\n\n    @mcp.tool\n    def ping() -> str:\n        \"\"\"Ping.\"\"\"\n        return \"pong\"\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[ListTools()],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"list_tools\", {\"detail\": \"full\"})\n    text = _unwrap_string_result(result)\n    parsed = json.loads(text)\n    assert isinstance(parsed, list)\n    assert parsed[0][\"name\"] == \"ping\"\n\n\nasync def test_list_tools_empty_catalog() -> None:\n    \"\"\"ListTools with no tools returns no-match message.\"\"\"\n    mcp = FastMCP(\"ListTools Empty\")\n\n    mcp.add_transform(\n        CodeMode(\n            discovery_tools=[ListTools()],\n            sandbox_provider=_UnsafeTestSandboxProvider(),\n        )\n    )\n\n    result = await _run_tool(mcp, \"list_tools\", {})\n    text = _unwrap_string_result(result)\n    assert \"No tools matched\" in text\n"
  },
  {
    "path": "tests/experimental/transforms/test_code_mode_serialization.py",
    "content": "from typing import Any\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms.search.base import (\n    _schema_section,\n    _schema_type,\n    serialize_tools_for_output_markdown,\n)\n\n# ---------------------------------------------------------------------------\n# _schema_type unit tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"schema,expected\",\n    [\n        ({\"type\": \"string\"}, \"string\"),\n        ({\"type\": \"integer\"}, \"integer\"),\n        ({\"type\": \"boolean\"}, \"boolean\"),\n        ({\"type\": \"null\"}, \"null\"),\n        ({\"type\": \"array\", \"items\": {\"type\": \"string\"}}, \"string[]\"),\n        ({\"type\": \"array\", \"items\": {\"type\": \"integer\"}}, \"integer[]\"),\n        ({\"type\": \"array\"}, \"any[]\"),\n        ({\"$ref\": \"#/$defs/Foo\"}, \"object\"),\n        ({\"properties\": {\"x\": {\"type\": \"int\"}}}, \"object\"),\n        ({}, \"any\"),\n        (None, \"any\"),\n        (\"not a dict\", \"any\"),\n    ],\n)\ndef test_schema_type_basic(schema: Any, expected: str) -> None:\n    assert _schema_type(schema) == expected\n\n\n@pytest.mark.parametrize(\n    \"schema,expected\",\n    [\n        ({\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}]}, \"string?\"),\n        ({\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}]}, \"string | integer\"),\n        (\n            {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}, {\"type\": \"null\"}]},\n            \"string | integer?\",\n        ),\n        ({\"anyOf\": [{\"type\": \"null\"}]}, \"null\"),\n        ({\"anyOf\": []}, \"any\"),\n        ({\"oneOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}]}, \"string?\"),\n        ({\"oneOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}]}, \"string | integer\"),\n        ({\"allOf\": [{\"type\": \"object\"}]}, \"object\"),\n        ({\"allOf\": [{\"$ref\": \"#/$defs/Foo\"}, {\"$ref\": \"#/$defs/Bar\"}]}, \"object\"),\n    ],\n)\ndef test_schema_type_unions(schema: Any, expected: str) -> None:\n    assert _schema_type(schema) == expected\n\n\n# ---------------------------------------------------------------------------\n# _schema_section unit tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\n    \"schema,expected_lines\",\n    [\n        (None, [\"**Parameters**\", \"- `value` (any)\"]),\n        (\"string\", [\"**Parameters**\", \"- `value` (any)\"]),\n        ({\"type\": \"string\"}, [\"**Parameters**\", \"- `value` (string)\"]),\n        (\n            {\"type\": \"object\", \"properties\": {}},\n            [\"**Parameters**\", \"*(no parameters)*\"],\n        ),\n    ],\n)\ndef test_schema_section_fallbacks(schema: Any, expected_lines: list[str]) -> None:\n    assert _schema_section(schema, \"Parameters\") == expected_lines\n\n\ndef test_schema_section_lists_fields_with_required_marker() -> None:\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"type\": \"string\"},\n            \"age\": {\"type\": \"integer\"},\n        },\n        \"required\": [\"name\"],\n    }\n    lines = _schema_section(schema, \"Parameters\")\n    assert lines[0] == \"**Parameters**\"\n    assert \"- `name` (string, required)\" in lines\n    assert \"- `age` (integer)\" in lines\n\n\n# ---------------------------------------------------------------------------\n# serialize_tools_for_output_markdown unit tests\n# ---------------------------------------------------------------------------\n\n\ndef test_serialize_tools_for_output_markdown_empty_list() -> None:\n    assert serialize_tools_for_output_markdown([]) == \"No tools matched the query.\"\n\n\nasync def test_serialize_tools_for_output_markdown_basic_tool() -> None:\n    mcp = FastMCP(\"MD Basic\")\n\n    @mcp.tool\n    def square(x: int) -> int:\n        \"\"\"Compute the square of a number.\"\"\"\n        return x * x\n\n    tools = await mcp.list_tools()\n    result = serialize_tools_for_output_markdown(tools)\n\n    assert \"### square\" in result\n    assert \"Compute the square of a number.\" in result\n    assert \"**Parameters**\" in result\n    assert \"`x` (integer, required)\" in result\n\n\nasync def test_serialize_tools_for_output_markdown_omits_output_section_when_no_schema() -> (\n    None\n):\n    mcp = FastMCP(\"MD No Output\")\n\n    @mcp.tool\n    def ping() -> None:\n        pass\n\n    tools = await mcp.list_tools()\n    result = serialize_tools_for_output_markdown(tools)\n\n    assert \"**Returns**\" not in result\n\n\nasync def test_serialize_tools_for_output_markdown_includes_output_section_when_schema_present() -> (\n    None\n):\n    mcp = FastMCP(\"MD With Output\")\n\n    @mcp.tool\n    def double(x: int) -> int:\n        return x * 2\n\n    tools = await mcp.list_tools()\n    result = serialize_tools_for_output_markdown(tools)\n\n    assert \"**Returns**\" in result\n\n\nasync def test_serialize_tools_for_output_markdown_omits_description_when_absent() -> (\n    None\n):\n    mcp = FastMCP(\"MD No Desc\")\n\n    @mcp.tool\n    def ping() -> None:\n        pass\n\n    tools = await mcp.list_tools()\n    result = serialize_tools_for_output_markdown(tools)\n\n    assert \"### ping\" in result\n\n\nasync def test_serialize_tools_for_output_markdown_optional_field_uses_question_mark() -> (\n    None\n):\n    mcp = FastMCP(\"MD Optional\")\n\n    @mcp.tool\n    def greet(name: str, greeting: str | None = None) -> str:\n        return f\"{greeting or 'Hello'}, {name}!\"\n\n    tools = await mcp.list_tools()\n    result = serialize_tools_for_output_markdown(tools)\n\n    assert \"`greeting` (string?)\" in result\n\n\nasync def test_serialize_tools_for_output_markdown_multiple_tools_separated() -> None:\n    mcp = FastMCP(\"MD Multi\")\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    @mcp.tool\n    def subtract(a: int, b: int) -> int:\n        return a - b\n\n    tools = await mcp.list_tools()\n    result = serialize_tools_for_output_markdown(tools)\n\n    assert \"### add\" in result\n    assert \"### subtract\" in result\n    assert \"\\n\\n\" in result\n"
  },
  {
    "path": "tests/fs/test_discovery.py",
    "content": "\"\"\"Tests for filesystem discovery module.\"\"\"\n\nfrom pathlib import Path\n\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate\nfrom fastmcp.server.providers.filesystem_discovery import (\n    discover_and_import,\n    discover_files,\n    extract_components,\n    import_module_from_file,\n)\nfrom fastmcp.tools import FunctionTool\nfrom fastmcp.tools.base import Tool\n\n\nclass TestDiscoverFiles:\n    \"\"\"Tests for discover_files function.\"\"\"\n\n    def test_discover_files_empty_dir(self, tmp_path: Path):\n        \"\"\"Should return empty list for empty directory.\"\"\"\n        files = discover_files(tmp_path)\n        assert files == []\n\n    def test_discover_files_nonexistent_dir(self, tmp_path: Path):\n        \"\"\"Should return empty list for nonexistent directory.\"\"\"\n        nonexistent = tmp_path / \"does_not_exist\"\n        files = discover_files(nonexistent)\n        assert files == []\n\n    def test_discover_files_single_file(self, tmp_path: Path):\n        \"\"\"Should find a single Python file.\"\"\"\n        py_file = tmp_path / \"test.py\"\n        py_file.write_text(\"# test\")\n\n        files = discover_files(tmp_path)\n        assert files == [py_file]\n\n    def test_discover_files_skips_init(self, tmp_path: Path):\n        \"\"\"Should skip __init__.py files.\"\"\"\n        init_file = tmp_path / \"__init__.py\"\n        init_file.write_text(\"# init\")\n        py_file = tmp_path / \"test.py\"\n        py_file.write_text(\"# test\")\n\n        files = discover_files(tmp_path)\n        assert files == [py_file]\n\n    def test_discover_files_recursive(self, tmp_path: Path):\n        \"\"\"Should find files in subdirectories.\"\"\"\n        subdir = tmp_path / \"subdir\"\n        subdir.mkdir()\n        file1 = tmp_path / \"a.py\"\n        file2 = subdir / \"b.py\"\n        file1.write_text(\"# a\")\n        file2.write_text(\"# b\")\n\n        files = discover_files(tmp_path)\n        assert sorted(files) == sorted([file1, file2])\n\n    def test_discover_files_skips_pycache(self, tmp_path: Path):\n        \"\"\"Should skip __pycache__ directories.\"\"\"\n        pycache = tmp_path / \"__pycache__\"\n        pycache.mkdir()\n        cache_file = pycache / \"test.py\"\n        cache_file.write_text(\"# cache\")\n        py_file = tmp_path / \"test.py\"\n        py_file.write_text(\"# test\")\n\n        files = discover_files(tmp_path)\n        assert files == [py_file]\n\n    def test_discover_files_sorted(self, tmp_path: Path):\n        \"\"\"Files should be returned in sorted order.\"\"\"\n        (tmp_path / \"z.py\").write_text(\"# z\")\n        (tmp_path / \"a.py\").write_text(\"# a\")\n        (tmp_path / \"m.py\").write_text(\"# m\")\n\n        files = discover_files(tmp_path)\n        names = [f.name for f in files]\n        assert names == [\"a.py\", \"m.py\", \"z.py\"]\n\n\nclass TestImportModuleFromFile:\n    \"\"\"Tests for import_module_from_file function.\"\"\"\n\n    def test_import_simple_module(self, tmp_path: Path):\n        \"\"\"Should import a simple module.\"\"\"\n        py_file = tmp_path / \"simple.py\"\n        py_file.write_text(\"VALUE = 42\")\n\n        module = import_module_from_file(py_file)\n        assert module.VALUE == 42\n\n    def test_import_module_with_function(self, tmp_path: Path):\n        \"\"\"Should import a module with functions.\"\"\"\n        py_file = tmp_path / \"funcs.py\"\n        py_file.write_text(\n            \"\"\"\\\ndef greet(name):\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        assert module.greet(\"World\") == \"Hello, World!\"\n\n    def test_import_module_with_imports(self, tmp_path: Path):\n        \"\"\"Should handle modules with standard library imports.\"\"\"\n        py_file = tmp_path / \"with_imports.py\"\n        py_file.write_text(\n            \"\"\"\\\nimport os\nimport sys\n\ndef get_cwd():\n    return os.getcwd()\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        assert callable(module.get_cwd)\n\n    def test_import_as_package_with_init(self, tmp_path: Path):\n        \"\"\"Should import as package when __init__.py exists.\"\"\"\n        # Create package structure (use unique name to avoid module caching)\n        pkg = tmp_path / \"testpkg_init\"\n        pkg.mkdir()\n        (pkg / \"__init__.py\").write_text(\"PKG_VAR = 'package'\")\n        module_file = pkg / \"module.py\"\n        module_file.write_text(\"MODULE_VAR = 'module'\")\n\n        module = import_module_from_file(module_file)\n        assert module.MODULE_VAR == \"module\"\n\n    def test_import_with_relative_import(self, tmp_path: Path):\n        \"\"\"Should support relative imports when in a package.\"\"\"\n        # Create package with relative import (use unique name to avoid module caching)\n        pkg = tmp_path / \"testpkg_relative\"\n        pkg.mkdir()\n        (pkg / \"__init__.py\").write_text(\"\")\n        (pkg / \"helper.py\").write_text(\"HELPER_VALUE = 123\")\n        (pkg / \"main.py\").write_text(\n            \"\"\"\\\nfrom .helper import HELPER_VALUE\n\nMAIN_VALUE = HELPER_VALUE * 2\n\"\"\"\n        )\n\n        module = import_module_from_file(pkg / \"main.py\")\n        assert module.MAIN_VALUE == 246\n\n    def test_import_package_module_reload(self, tmp_path: Path):\n        \"\"\"Re-importing a package module should return updated content.\"\"\"\n        # Create package (use unique name to avoid conflicts)\n        pkg = tmp_path / \"testpkg_reload\"\n        pkg.mkdir()\n        (pkg / \"__init__.py\").write_text(\"\")\n        module_file = pkg / \"reloadable.py\"\n        module_file.write_text(\"VALUE = 'original'\")\n\n        # First import\n        module = import_module_from_file(module_file)\n        assert module.VALUE == \"original\"\n\n        # Modify the file\n        module_file.write_text(\"VALUE = 'updated'\")\n\n        # Re-import should see the updated value\n        module = import_module_from_file(module_file)\n        assert module.VALUE == \"updated\"\n\n\nclass TestExtractComponents:\n    \"\"\"Tests for extract_components function.\"\"\"\n\n    def test_extract_no_components(self, tmp_path: Path):\n        \"\"\"Should return empty list for module with no components.\"\"\"\n        py_file = tmp_path / \"plain.py\"\n        py_file.write_text(\n            \"\"\"\\\ndef plain_function():\n    pass\n\nSOME_VAR = 42\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        components = extract_components(module)\n        assert components == []\n\n    def test_extract_tool_component(self, tmp_path: Path):\n        \"\"\"Should extract Tool objects.\"\"\"\n        py_file = tmp_path / \"tools.py\"\n        py_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        components = extract_components(module)\n\n        assert len(components) == 1\n        component = components[0]\n        assert isinstance(component, FunctionTool)\n        assert component.name == \"greet\"\n\n    def test_extract_multiple_components(self, tmp_path: Path):\n        \"\"\"Should extract multiple component types.\"\"\"\n        py_file = tmp_path / \"multi.py\"\n        py_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\nfrom fastmcp.resources import resource\nfrom fastmcp.prompts import prompt\n\n@tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n@resource(\"config://app\")\ndef get_config() -> dict:\n    return {}\n\n@prompt\ndef analyze(topic: str) -> str:\n    return f\"Analyze: {topic}\"\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        components = extract_components(module)\n\n        assert len(components) == 3\n        types = {type(c).__name__ for c in components}\n        assert types == {\"FunctionTool\", \"FunctionResource\", \"FunctionPrompt\"}\n\n    def test_extract_skips_private_components(self, tmp_path: Path):\n        \"\"\"Should skip private components (those starting with _).\"\"\"\n        py_file = tmp_path / \"private.py\"\n        py_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef public_tool() -> str:\n    return \"public\"\n\n# The module attribute starts with _, so it's skipped during discovery\n@tool(\"private_tool_name\")\ndef _private_tool() -> str:\n    return \"private\"\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        components = extract_components(module)\n\n        # Only public_tool should be found (_private_tool starts with _, so skipped)\n        assert len(components) == 1\n        component = components[0]\n        assert component.name == \"public_tool\"\n\n    def test_extract_resource_template(self, tmp_path: Path):\n        \"\"\"Should extract ResourceTemplate objects.\"\"\"\n        py_file = tmp_path / \"templates.py\"\n        py_file.write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"users://{user_id}/profile\")\ndef get_profile(user_id: str) -> dict:\n    return {\"id\": user_id}\n\"\"\"\n        )\n\n        module = import_module_from_file(py_file)\n        components = extract_components(module)\n\n        assert len(components) == 1\n        component = components[0]\n        assert isinstance(component, FunctionResourceTemplate)\n        assert component.uri_template == \"users://{user_id}/profile\"\n\n\nclass TestDiscoverAndImport:\n    \"\"\"Tests for discover_and_import function.\"\"\"\n\n    def test_discover_and_import_empty(self, tmp_path: Path):\n        \"\"\"Should return empty result for empty directory.\"\"\"\n        result = discover_and_import(tmp_path)\n        assert result.components == []\n        assert result.failed_files == {}\n\n    def test_discover_and_import_with_tools(self, tmp_path: Path):\n        \"\"\"Should discover and import tools.\"\"\"\n        tools_dir = tmp_path / \"tools\"\n        tools_dir.mkdir()\n        (tools_dir / \"greet.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        result = discover_and_import(tmp_path)\n\n        assert len(result.components) == 1\n        file_path, component = result.components[0]\n        assert file_path.name == \"greet.py\"\n        assert isinstance(component, FunctionTool)\n        assert component.name == \"greet\"\n\n    def test_discover_and_import_skips_bad_imports(self, tmp_path: Path):\n        \"\"\"Should skip files that fail to import and track them.\"\"\"\n        (tmp_path / \"good.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef good_tool() -> str:\n    return \"good\"\n\"\"\"\n        )\n        (tmp_path / \"bad.py\").write_text(\n            \"\"\"\\\nimport nonexistent_module_xyz123\n\ndef bad_function():\n    pass\n\"\"\"\n        )\n\n        result = discover_and_import(tmp_path)\n\n        # Only good.py should be imported\n        assert len(result.components) == 1\n        _, component = result.components[0]\n        assert component.name == \"good_tool\"\n\n        # bad.py should be in failed_files\n        assert len(result.failed_files) == 1\n        failed_path = tmp_path / \"bad.py\"\n        assert failed_path in result.failed_files\n        assert \"nonexistent_module_xyz123\" in result.failed_files[failed_path]\n\n\nclass TestExtractComponentsVersion:\n    \"\"\"Tests for version propagation in extract_components.\"\"\"\n\n    def test_extract_tool_preserves_version(self, tmp_path: Path):\n        \"\"\"Tools discovered from files should have their version attribute set.\"\"\"\n        tool_file = tmp_path / \"versioned_tool.py\"\n        tool_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool(version=\"1.0\", description=\"v1\")\ndef greet_v1(name: str) -> str:\n    return f\"Hi {name}\"\n\n@tool(version=\"2.0\", description=\"v2\")\ndef greet_v2(name: str) -> str:\n    return f\"Hey {name}\"\n\"\"\"\n        )\n\n        module = import_module_from_file(tool_file)\n        components = extract_components(module)\n\n        tools = [c for c in components if isinstance(c, Tool)]\n        assert len(tools) == 2\n\n        versions = {t.version for t in tools}\n        assert versions == {\"1.0\", \"2.0\"}\n\n    def test_extract_resource_preserves_version(self, tmp_path: Path):\n        \"\"\"Resources discovered from files should have their version attribute set.\"\"\"\n        resource_file = tmp_path / \"versioned_resource.py\"\n        resource_file.write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"data://config\", version=\"1.0\", name=\"config\", description=\"v1 config\")\ndef config_v1() -> str:\n    return '{\"theme\": \"light\"}'\n\"\"\"\n        )\n\n        module = import_module_from_file(resource_file)\n        components = extract_components(module)\n\n        resources = [c for c in components if isinstance(c, Resource)]\n        assert len(resources) == 1\n        assert resources[0].version == \"1.0\"\n\n    def test_extract_resource_template_preserves_version(self, tmp_path: Path):\n        \"\"\"Resource templates discovered from files should have their version set.\"\"\"\n        template_file = tmp_path / \"versioned_template.py\"\n        template_file.write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"users://{user_id}/profile\", version=\"2.0\", description=\"v2 profile\")\ndef get_profile(user_id: str) -> dict:\n    return {\"id\": user_id}\n\"\"\"\n        )\n\n        module = import_module_from_file(template_file)\n        components = extract_components(module)\n\n        templates = [c for c in components if isinstance(c, ResourceTemplate)]\n        assert len(templates) == 1\n        assert templates[0].version == \"2.0\"\n\n    def test_extract_prompt_preserves_version(self, tmp_path: Path):\n        \"\"\"Prompts discovered from files should have their version attribute set.\"\"\"\n        prompt_file = tmp_path / \"versioned_prompt.py\"\n        prompt_file.write_text(\n            \"\"\"\\\nfrom fastmcp.prompts import prompt\n\n@prompt(name=\"summarize\", version=\"1.0\", description=\"v1 prompt\")\ndef summarize_v1(text: str) -> str:\n    return f\"Summarize: {text}\"\n\"\"\"\n        )\n\n        module = import_module_from_file(prompt_file)\n        components = extract_components(module)\n\n        prompts = [c for c in components if isinstance(c, Prompt)]\n        assert len(prompts) == 1\n        assert prompts[0].version == \"1.0\"\n\n    def test_discovered_tool_meta_includes_version(self, tmp_path: Path):\n        \"\"\"get_meta() should include version for tools discovered via filesystem.\"\"\"\n        tool_file = tmp_path / \"meta_tool.py\"\n        tool_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool(name=\"echo\", version=\"3.0\", description=\"Echo tool\")\ndef echo(msg: str) -> str:\n    return msg\n\"\"\"\n        )\n\n        module = import_module_from_file(tool_file)\n        components = extract_components(module)\n\n        tool = components[0]\n        meta = tool.get_meta()\n        assert meta[\"fastmcp\"][\"version\"] == \"3.0\"\n\n    def test_unversioned_components_have_no_version(self, tmp_path: Path):\n        \"\"\"Components without version should have version=None.\"\"\"\n        tool_file = tmp_path / \"no_version_tool.py\"\n        tool_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool(description=\"No version\")\ndef my_tool(x: str) -> str:\n    return x\n\"\"\"\n        )\n\n        module = import_module_from_file(tool_file)\n        components = extract_components(module)\n\n        assert len(components) == 1\n        assert components[0].version is None\n        meta = components[0].get_meta()\n        assert \"version\" not in meta[\"fastmcp\"]\n"
  },
  {
    "path": "tests/fs/test_provider.py",
    "content": "\"\"\"Tests for FileSystemProvider.\"\"\"\n\nimport time\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers import FileSystemProvider\n\n\nclass TestFileSystemProvider:\n    \"\"\"Tests for FileSystemProvider.\"\"\"\n\n    def test_provider_empty_directory(self, tmp_path: Path):\n        \"\"\"Provider should work with empty directory.\"\"\"\n        provider = FileSystemProvider(tmp_path)\n        assert repr(provider).startswith(\"FileSystemProvider\")\n\n    def test_provider_discovers_tools(self, tmp_path: Path):\n        \"\"\"Provider should discover @tool decorated functions.\"\"\"\n        tools_dir = tmp_path / \"tools\"\n        tools_dir.mkdir()\n        (tools_dir / \"greet.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    '''Greet someone by name.'''\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n\n        # Check tool was registered\n        assert len(provider._components) == 1\n\n    def test_provider_discovers_resources(self, tmp_path: Path):\n        \"\"\"Provider should discover @resource decorated functions.\"\"\"\n        (tmp_path / \"config.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"config://app\")\ndef get_config() -> dict:\n    '''Get app config.'''\n    return {\"setting\": \"value\"}\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        assert len(provider._components) == 1\n\n    def test_provider_discovers_resource_templates(self, tmp_path: Path):\n        \"\"\"Provider should discover resource templates.\"\"\"\n        (tmp_path / \"users.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"users://{user_id}/profile\")\ndef get_profile(user_id: str) -> dict:\n    '''Get user profile.'''\n    return {\"id\": user_id}\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        assert len(provider._components) == 1\n\n    def test_provider_discovers_prompts(self, tmp_path: Path):\n        \"\"\"Provider should discover @prompt decorated functions.\"\"\"\n        (tmp_path / \"analyze.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.prompts import prompt\n\n@prompt\ndef analyze(topic: str) -> list:\n    '''Analyze a topic.'''\n    return [{\"role\": \"user\", \"content\": f\"Analyze: {topic}\"}]\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        assert len(provider._components) == 1\n\n    def test_provider_discovers_multiple_in_one_file(self, tmp_path: Path):\n        \"\"\"Provider should discover multiple components in one file.\"\"\"\n        (tmp_path / \"multi.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\nfrom fastmcp.resources import resource\n\n@tool\ndef tool1() -> str:\n    return \"tool1\"\n\n@tool\ndef tool2() -> str:\n    return \"tool2\"\n\n@resource(\"config://app\")\ndef get_config() -> dict:\n    return {}\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        assert len(provider._components) == 3\n\n    def test_provider_skips_undecorated_files(self, tmp_path: Path):\n        \"\"\"Provider should skip files with no decorated functions.\"\"\"\n        (tmp_path / \"utils.py\").write_text(\n            \"\"\"\\\ndef helper_function():\n    return \"helper\"\n\nSOME_CONSTANT = 42\n\"\"\"\n        )\n        (tmp_path / \"tool.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef my_tool() -> str:\n    return \"tool\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        # Only the tool should be registered\n        assert len(provider._components) == 1\n\n\nclass TestFileSystemProviderReloadMode:\n    \"\"\"Tests for FileSystemProvider reload mode.\"\"\"\n\n    def test_reload_false_caches_at_init(self, tmp_path: Path):\n        \"\"\"With reload=False, components are cached at init.\"\"\"\n        (tmp_path / \"tool.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef original() -> str:\n    return \"original\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path, reload=False)\n        assert len(provider._components) == 1\n\n        # Add another file - should NOT be picked up\n        (tmp_path / \"tool2.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef added() -> str:\n    return \"added\"\n\"\"\"\n        )\n\n        # Still only one component\n        assert len(provider._components) == 1\n\n    async def test_reload_true_rescans(self, tmp_path: Path):\n        \"\"\"With reload=True, components are rescanned on each request.\"\"\"\n        (tmp_path / \"tool.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef original() -> str:\n    return \"original\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path, reload=True)\n\n        # Always loaded once at init (to catch errors early)\n        assert provider._loaded\n        assert len(provider._components) == 1\n\n        # Add another file - should be picked up on next _ensure_loaded\n        (tmp_path / \"tool2.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef added() -> str:\n    return \"added\"\n\"\"\"\n        )\n\n        # With reload=True, _ensure_loaded re-scans\n        await provider._ensure_loaded()\n        assert len(provider._components) == 2\n\n    async def test_warning_deduplication_same_file(self, tmp_path: Path, capsys):\n        \"\"\"Warnings for the same broken file should not repeat.\"\"\"\n        bad_file = tmp_path / \"bad.py\"\n        bad_file.write_text(\"1/0  # division by zero\")\n\n        provider = FileSystemProvider(tmp_path, reload=True)\n\n        # First load - should warn\n        captured = capsys.readouterr()\n        # Check for warning indicator (rich may truncate long paths)\n        assert \"WARNING\" in captured.err and \"Failed to import\" in captured.err\n\n        # Second load (same file, unchanged) - should NOT warn again\n        await provider._ensure_loaded()\n        captured = capsys.readouterr()\n        assert \"Failed to import\" not in captured.err\n\n    async def test_warning_on_file_change(self, tmp_path: Path, capsys):\n        \"\"\"Warnings should reappear when a broken file changes.\"\"\"\n        bad_file = tmp_path / \"bad.py\"\n        bad_file.write_text(\"1/0  # division by zero\")\n\n        provider = FileSystemProvider(tmp_path, reload=True)\n\n        # First load - should warn\n        captured = capsys.readouterr()\n        # Check for warning indicator (rich may truncate long paths)\n        assert \"WARNING\" in captured.err and \"Failed to import\" in captured.err\n\n        # Modify the file (different error) - need to ensure mtime changes\n        time.sleep(0.01)  # Ensure mtime differs\n        bad_file.write_text(\"syntax error here !!!\")\n\n        # Next load - should warn again (file changed)\n        await provider._ensure_loaded()\n        captured = capsys.readouterr()\n        # Check for warning indicator (rich may truncate long paths)\n        assert \"WARNING\" in captured.err and \"Failed to import\" in captured.err\n\n    async def test_warning_cleared_when_fixed(self, tmp_path: Path, capsys):\n        \"\"\"Warnings should clear when a file is fixed, and reappear if broken again.\"\"\"\n        bad_file = tmp_path / \"tool.py\"\n        bad_file.write_text(\"1/0  # broken\")\n\n        provider = FileSystemProvider(tmp_path, reload=True)\n\n        # First load - should warn\n        captured = capsys.readouterr()\n        # Check for warning indicator (rich may truncate long paths)\n        assert \"WARNING\" in captured.err and \"Failed to import\" in captured.err\n\n        # Fix the file\n        time.sleep(0.01)\n        bad_file.write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef my_tool() -> str:\n    return \"fixed\"\n\"\"\"\n        )\n\n        # Load again - should NOT warn, file is fixed\n        await provider._ensure_loaded()\n        captured = capsys.readouterr()\n        assert \"Failed to import\" not in captured.err\n        assert len(provider._components) == 1\n\n        # Break it again\n        time.sleep(0.01)\n        bad_file.write_text(\"1/0  # broken again\")\n\n        # Should warn again\n        await provider._ensure_loaded()\n        captured = capsys.readouterr()\n        # Check for warning indicator (rich may truncate long paths)\n        assert \"WARNING\" in captured.err and \"Failed to import\" in captured.err\n\n\nclass TestFileSystemProviderIntegration:\n    \"\"\"Integration tests with FastMCP server.\"\"\"\n\n    async def test_provider_with_fastmcp_server(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should work with FastMCP server.\"\"\"\n        (tmp_path / \"greet.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    '''Greet someone.'''\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            # List tools\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            assert tools[0].name == \"greet\"\n\n            # Call tool\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert \"Hello, World!\" in str(result)\n\n    async def test_provider_with_resources(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should work with resources.\"\"\"\n        (tmp_path / \"config.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"config://app\")\ndef get_config() -> str:\n    '''Get app config.'''\n    return '{\"version\": \"1.0\"}'\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            # List resources\n            resources = await client.list_resources()\n            assert len(resources) == 1\n            assert str(resources[0].uri) == \"config://app\"\n\n            # Read resource\n            result = await client.read_resource(\"config://app\")\n            assert \"1.0\" in str(result)\n\n    async def test_provider_with_resource_templates(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should work with resource templates.\"\"\"\n        (tmp_path / \"users.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"users://{user_id}/profile\")\ndef get_profile(user_id: str) -> str:\n    '''Get user profile.'''\n    return f'{{\"id\": \"{user_id}\", \"name\": \"User {user_id}\"}}'\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            # List templates\n            templates = await client.list_resource_templates()\n            assert len(templates) == 1\n\n            # Read with parameter\n            result = await client.read_resource(\"users://123/profile\")\n            assert \"123\" in str(result)\n\n    async def test_provider_with_prompts(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should work with prompts.\"\"\"\n        (tmp_path / \"analyze.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.prompts import prompt\n\n@prompt\ndef analyze(topic: str) -> str:\n    '''Analyze a topic.'''\n    return f\"Please analyze: {topic}\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            # List prompts\n            prompts = await client.list_prompts()\n            assert len(prompts) == 1\n            assert prompts[0].name == \"analyze\"\n\n            # Get prompt\n            result = await client.get_prompt(\"analyze\", {\"topic\": \"Python\"})\n            assert \"Python\" in str(result)\n\n    async def test_nested_directory_structure(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should work with nested directories.\"\"\"\n        # Create nested structure\n        tools = tmp_path / \"tools\"\n        tools.mkdir()\n        (tools / \"greet.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        payments = tools / \"payments\"\n        payments.mkdir()\n        (payments / \"charge.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool\ndef charge(amount: float) -> str:\n    return f\"Charged ${amount}\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            tools_list = await client.list_tools()\n            assert len(tools_list) == 2\n            names = {t.name for t in tools_list}\n            assert names == {\"greet\", \"charge\"}\n\n\nclass TestFileSystemProviderVersioning:\n    \"\"\"Tests for version propagation through FileSystemProvider.\"\"\"\n\n    async def test_versioned_tool_via_provider(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should preserve tool version in list_tools output.\"\"\"\n        (tmp_path / \"versioned.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool(version=\"1.0\", description=\"v1 greet\")\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            assert tools[0].name == \"greet\"\n            meta = tools[0].meta\n            assert meta is not None\n            assert meta[\"fastmcp\"][\"version\"] == \"1.0\"\n\n    async def test_versioned_resource_via_provider(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should preserve resource version.\"\"\"\n        (tmp_path / \"versioned_resource.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.resources import resource\n\n@resource(\"data://config\", version=\"2.0\", name=\"config\", description=\"v2 config\")\ndef config() -> str:\n    return '{\"theme\": \"dark\"}'\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            resources = await client.list_resources()\n            assert len(resources) == 1\n            assert resources[0].name == \"config\"\n            meta = resources[0].meta\n            assert meta is not None\n            assert meta[\"fastmcp\"][\"version\"] == \"2.0\"\n\n    async def test_versioned_prompt_via_provider(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should preserve prompt version.\"\"\"\n        (tmp_path / \"versioned_prompt.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.prompts import prompt\n\n@prompt(name=\"summarize\", version=\"1.0\", description=\"v1 prompt\")\ndef summarize(text: str) -> str:\n    return f\"Summarize: {text}\"\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            prompts = await client.list_prompts()\n            assert len(prompts) == 1\n            assert prompts[0].name == \"summarize\"\n            meta = prompts[0].meta\n            assert meta is not None\n            assert meta[\"fastmcp\"][\"version\"] == \"1.0\"\n\n    async def test_multiple_tool_versions_via_provider(self, tmp_path: Path):\n        \"\"\"FileSystemProvider should handle multiple versions of the same tool.\"\"\"\n        (tmp_path / \"multi_version.py\").write_text(\n            \"\"\"\\\nfrom fastmcp.tools import tool\n\n@tool(name=\"add\", version=\"1.0\", description=\"v1 add\")\ndef add_v1(x: int, y: int) -> int:\n    return x + y\n\n@tool(name=\"add\", version=\"2.0\", description=\"v2 add with z\")\ndef add_v2(x: int, y: int, z: int = 0) -> int:\n    return x + y + z\n\"\"\"\n        )\n\n        provider = FileSystemProvider(tmp_path)\n        mcp = FastMCP(\"TestServer\", providers=[provider])\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            add_tools = [t for t in tools if t.name == \"add\"]\n            # list_tools deduplicates to the highest version\n            assert len(add_tools) == 1\n            meta = add_tools[0].meta\n            assert meta is not None\n            assert meta[\"fastmcp\"][\"version\"] == \"2.0\"\n            assert meta[\"fastmcp\"][\"versions\"] == [\"2.0\", \"1.0\"]\n"
  },
  {
    "path": "tests/integration_tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration_tests/auth/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration_tests/auth/test_github_provider_integration.py",
    "content": "\"\"\"Integration tests for GitHub OAuth Provider.\n\nTests the complete GitHub OAuth flow using HeadlessOAuth to bypass browser interaction.\n\nThis test requires a GitHub OAuth app to be created at https://github.com/settings/developers\nwith the following configuration:\n- Redirect URL: http://127.0.0.1:9100/auth/callback\n- Client ID and Client Secret should be set as environment variables:\n  - FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID\n  - FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET\n\"\"\"\n\nimport os\nimport re\nimport secrets\nimport time\nfrom collections.abc import AsyncGenerator\nfrom urllib.parse import parse_qs, urlencode, urlparse\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.server.auth.oauth_proxy.models import ClientCode\nfrom fastmcp.server.auth.providers.github import GitHubProvider\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_async\n\nFASTMCP_TEST_AUTH_GITHUB_CLIENT_ID = os.getenv(\"FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID\")\nFASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET = os.getenv(\n    \"FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET\"\n)\n\n# Skip tests if no GitHub OAuth credentials are available\npytestmark = pytest.mark.xfail(\n    not FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID\n    or not FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET,\n    reason=\"FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID and FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET environment variables are not set or empty\",\n)\n\n\ndef create_github_server(base_url: str) -> FastMCP:\n    \"\"\"Create FastMCP server with GitHub OAuth protection.\"\"\"\n    assert FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID is not None\n    assert FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET is not None\n\n    # Create GitHub OAuth provider\n    auth = GitHubProvider(\n        client_id=FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID,\n        client_secret=FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET,\n        base_url=base_url,\n        jwt_signing_key=\"test-secret\",\n    )\n\n    # Create FastMCP server with GitHub authentication\n    server = FastMCP(\"GitHub OAuth Integration Test Server\", auth=auth)\n\n    @server.tool\n    def get_protected_data() -> str:\n        \"\"\"Returns protected data - requires GitHub OAuth.\"\"\"\n        return \"🔐 This data requires GitHub OAuth authentication!\"\n\n    @server.tool\n    def get_user_info() -> str:\n        \"\"\"Returns user info from OAuth context.\"\"\"\n        return \"📝 GitHub OAuth user authenticated successfully\"\n\n    return server\n\n\ndef create_github_server_with_mock_callback(base_url: str) -> FastMCP:\n    \"\"\"Create FastMCP server with GitHub OAuth that mocks the callback for testing.\"\"\"\n    assert FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID is not None\n    assert FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET is not None\n\n    # Create GitHub OAuth provider\n    auth = GitHubProvider(\n        client_id=FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID,\n        client_secret=FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET,\n        base_url=base_url,\n        jwt_signing_key=\"test-secret\",\n    )\n\n    # Mock the authorize method to return a fake code instead of redirecting to GitHub\n    async def mock_authorize(client, params):\n        # Instead of redirecting to GitHub, simulate an immediate callback\n        # Generate a fake authorization code\n        fake_code = secrets.token_urlsafe(32)\n\n        # Create mock token response (simulating what GitHub would return)\n        mock_tokens = {\n            \"access_token\": f\"gho_mock_token_{secrets.token_hex(16)}\",\n            \"token_type\": \"bearer\",\n            \"expires_in\": 3600,\n        }\n\n        # Store the mock tokens in the proxy's code storage\n        await auth._code_store.put(\n            key=fake_code,\n            value=ClientCode(\n                code=fake_code,\n                client_id=client.client_id,\n                redirect_uri=str(params.redirect_uri),\n                code_challenge=params.code_challenge,\n                code_challenge_method=getattr(params, \"code_challenge_method\", \"S256\"),\n                scopes=params.scopes or [],\n                idp_tokens=mock_tokens,\n                expires_at=int(time.time() + 300),  # 5 minutes\n                created_at=time.time(),\n            ),\n        )\n\n        # Return the redirect to the client's callback with the fake code\n        callback_params = {\n            \"code\": fake_code,\n            \"state\": params.state,\n        }\n        separator = \"&\" if \"?\" in str(params.redirect_uri) else \"?\"\n        return f\"{params.redirect_uri}{separator}{urlencode(callback_params)}\"\n\n    auth.authorize = mock_authorize  # type: ignore[assignment]\n\n    # Mock the token verifier to accept our fake tokens\n    original_verify_token = auth._token_validator.verify_token\n\n    async def mock_verify_token(token: str):\n        if token.startswith(\"gho_mock_token_\"):\n            # Return a mock AccessToken for our fake tokens\n            return AccessToken(\n                token=token,\n                client_id=FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID or \"test-client\",\n                scopes=[\"user\"],\n                expires_at=int(time.time() + 3600),\n            )\n        # Fall back to original verification for other tokens\n        return await original_verify_token(token)\n\n    auth._token_validator.verify_token = mock_verify_token  # type: ignore[assignment]\n\n    # Create FastMCP server with mocked GitHub authentication\n    server = FastMCP(\"GitHub OAuth Integration Test Server (Mock)\", auth=auth)\n\n    @server.tool\n    def get_protected_data() -> str:\n        \"\"\"Returns protected data - requires GitHub OAuth.\"\"\"\n        return \"🔐 This data requires GitHub OAuth authentication!\"\n\n    @server.tool\n    def get_user_info() -> str:\n        \"\"\"Returns user info from OAuth context.\"\"\"\n        return \"📝 GitHub OAuth user authenticated successfully\"\n\n    return server\n\n\n@pytest.fixture\nasync def github_server() -> AsyncGenerator[str, None]:\n    \"\"\"Start GitHub OAuth server on a random available port.\"\"\"\n    from fastmcp.utilities.http import find_available_port\n\n    port = find_available_port()\n    base_url = f\"http://127.0.0.1:{port}\"\n    server = create_github_server(base_url)\n    async with run_server_async(server, port=port, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\nasync def github_server_with_mock() -> AsyncGenerator[str, None]:\n    \"\"\"Start GitHub OAuth server with mocked callback on a random available port.\"\"\"\n    from fastmcp.utilities.http import find_available_port\n\n    port = find_available_port()\n    base_url = f\"http://127.0.0.1:{port}\"\n    server = create_github_server_with_mock_callback(base_url)\n    async with run_server_async(server, port=port, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\ndef github_client(github_server: str) -> Client:\n    \"\"\"Create FastMCP client with HeadlessOAuth for GitHub server.\"\"\"\n    return Client(\n        github_server,\n        auth=HeadlessOAuth(mcp_url=github_server),\n    )\n\n\n@pytest.fixture\ndef github_client_with_mock(github_server_with_mock: str) -> Client:\n    \"\"\"Create FastMCP client with HeadlessOAuth for mocked GitHub server.\"\"\"\n    return Client(\n        github_server_with_mock,\n        auth=HeadlessOAuth(mcp_url=github_server_with_mock),\n    )\n\n\nasync def test_github_oauth_credentials_available():\n    \"\"\"Test that GitHub OAuth credentials are available for testing.\"\"\"\n    assert FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID is not None\n    assert FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET is not None\n    assert len(FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID) > 0\n    assert len(FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET) > 0\n\n\nasync def test_github_oauth_authorization_redirect(github_server: str):\n    \"\"\"Test that GitHub OAuth authorization redirects to GitHub correctly through consent flow.\n\n    Since HeadlessOAuth can't handle real GitHub redirects, we test that:\n    1. DCR client registration works\n    2. Authorization endpoint redirects to consent page\n    3. Consent approval redirects to GitHub with correct parameters\n    \"\"\"\n    # Extract base URL\n    parsed = urlparse(github_server)\n    base_url = f\"{parsed.scheme}://{parsed.netloc}\"\n\n    async with httpx.AsyncClient() as http_client:\n        # Step 1: Register OAuth client (DCR)\n        register_response = await http_client.post(\n            f\"{base_url}/register\",\n            json={\n                \"client_name\": \"Integration Test Client\",\n                \"redirect_uris\": [\"http://localhost:12345/callback\"],\n                \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n                \"response_types\": [\"code\"],\n                \"token_endpoint_auth_method\": \"client_secret_post\",\n            },\n        )\n        if register_response.status_code != 201:\n            print(f\"Registration failed: {register_response.status_code}\")\n            print(f\"Response: {register_response.text}\")\n        assert register_response.status_code == 201\n\n        client_info = register_response.json()\n        client_id = client_info[\"client_id\"]\n        assert client_id is not None\n\n        # Step 2: Test authorization endpoint redirects to consent page\n        auth_url = f\"{base_url}/authorize\"\n        auth_params = {\n            \"response_type\": \"code\",\n            \"client_id\": client_id,\n            \"redirect_uri\": \"http://localhost:12345/callback\",\n            \"state\": \"test-state-123\",\n            \"code_challenge\": \"test-challenge\",\n            \"code_challenge_method\": \"S256\",\n        }\n\n        auth_response = await http_client.get(\n            auth_url, params=auth_params, follow_redirects=False\n        )\n\n        # Should redirect to consent page (confused deputy protection)\n        assert auth_response.status_code == 302\n        consent_location = auth_response.headers[\"location\"]\n        assert \"/consent\" in consent_location\n\n        # Step 3: Visit consent page to get CSRF token\n        consent_response = await http_client.get(\n            consent_location, follow_redirects=False\n        )\n        assert consent_response.status_code == 200\n\n        # Extract CSRF token from consent page HTML\n        csrf_match = re.search(\n            r'name=\"csrf_token\"\\s+value=\"([^\"]+)\"', consent_response.text\n        )\n        assert csrf_match, \"CSRF token not found in consent page\"\n        csrf_token = csrf_match.group(1)\n\n        # Extract txn_id from consent URL\n        txn_id_match = re.search(r\"txn_id=([^&]+)\", consent_location)\n        assert txn_id_match, \"txn_id not found in consent URL\"\n        txn_id = txn_id_match.group(1)\n\n        # Step 4: Approve consent\n        approve_response = await http_client.post(\n            f\"{base_url}/consent\",\n            data={\n                \"action\": \"approve\",\n                \"txn_id\": txn_id,\n                \"csrf_token\": csrf_token,\n            },\n            cookies=consent_response.cookies,\n            follow_redirects=False,\n        )\n\n        # Should redirect to GitHub\n        assert approve_response.status_code in (302, 303)\n        redirect_location = approve_response.headers[\"location\"]\n\n        # Parse redirect URL - should be GitHub\n        redirect_parsed = urlparse(redirect_location)\n        assert redirect_parsed.hostname == \"github.com\"\n        assert redirect_parsed.path == \"/login/oauth/authorize\"\n\n        # Check that GitHub gets the right parameters\n        github_params = parse_qs(redirect_parsed.query)\n        assert \"client_id\" in github_params\n        assert github_params[\"client_id\"][0] == FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID\n        assert \"redirect_uri\" in github_params\n        # The redirect_uri should be our proxy's callback, not the client's\n        proxy_callback = github_params[\"redirect_uri\"][0]\n        assert proxy_callback.startswith(base_url)\n        assert proxy_callback.endswith(\"/auth/callback\")\n\n\nasync def test_github_oauth_server_metadata(github_server: str):\n    \"\"\"Test OAuth server metadata discovery.\"\"\"\n    from urllib.parse import urlparse\n\n    import httpx\n\n    # Extract base URL from server URL\n    parsed = urlparse(github_server)\n    base_url = f\"{parsed.scheme}://{parsed.netloc}\"\n\n    async with httpx.AsyncClient() as http_client:\n        # Test OAuth authorization server metadata\n        metadata_response = await http_client.get(\n            f\"{base_url}/.well-known/oauth-authorization-server\"\n        )\n        assert metadata_response.status_code == 200\n\n        metadata = metadata_response.json()\n        assert \"authorization_endpoint\" in metadata\n        assert \"token_endpoint\" in metadata\n        assert \"registration_endpoint\" in metadata\n        assert \"issuer\" in metadata\n\n        # Verify endpoints are properly formed\n        assert metadata[\"authorization_endpoint\"].startswith(base_url)\n        assert metadata[\"token_endpoint\"].startswith(base_url)\n        assert metadata[\"registration_endpoint\"].startswith(base_url)\n\n\nasync def test_github_oauth_unauthorized_access(github_server: str):\n    \"\"\"Test that unauthenticated requests are rejected.\"\"\"\n    import httpx\n\n    from fastmcp.client.transports import StreamableHttpTransport\n\n    # Create client without OAuth authentication\n    unauthorized_client = Client(transport=StreamableHttpTransport(github_server))\n\n    # Attempt to connect without authentication should fail\n    with pytest.raises(httpx.HTTPStatusError, match=\"401 Unauthorized\"):\n        async with unauthorized_client:\n            pass\n\n\nasync def test_github_oauth_with_mock(github_client_with_mock: Client):\n    \"\"\"Test complete GitHub OAuth flow with mocked callback.\"\"\"\n\n    async with github_client_with_mock:\n        # Test that we can ping the server (requires successful OAuth)\n        assert await github_client_with_mock.ping()\n\n        # Test that we can call protected tools\n        result = await github_client_with_mock.call_tool(\"get_protected_data\", {})\n        assert \"🔐 This data requires GitHub OAuth authentication!\" in str(result.data)\n\n        # Test that we can call user info tool\n        result = await github_client_with_mock.call_tool(\"get_user_info\", {})\n        assert \"📝 GitHub OAuth user authenticated successfully\" in str(result.data)\n\n\nasync def test_github_oauth_mock_only_accepts_mock_tokens(github_server_with_mock: str):\n    \"\"\"Test that the mock token verifier only accepts mock tokens, not real ones.\"\"\"\n    from urllib.parse import urlparse\n\n    import httpx\n\n    # Extract base URL\n    parsed = urlparse(github_server_with_mock)\n    base_url = f\"{parsed.scheme}://{parsed.netloc}\"\n\n    async with httpx.AsyncClient() as http_client:\n        # Test that a fake \"real\" GitHub token is rejected\n        fake_real_token = \"gho_real_token_should_be_rejected\"\n\n        auth_response = await http_client.post(\n            f\"{base_url}/mcp\",\n            headers={\n                \"Authorization\": f\"Bearer {fake_real_token}\",\n                \"Content-Type\": \"application/json\",\n            },\n            json={\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"ping\"},\n        )\n\n        # Should be unauthorized because it's not a mock token\n        assert auth_response.status_code == 401\n"
  },
  {
    "path": "tests/integration_tests/conftest.py",
    "content": "import os\n\nimport pytest\n\n\ndef _is_rate_limit_error(excinfo, report=None) -> bool:\n    \"\"\"Check if an exception indicates a rate limit error from GitHub API.\n\n    Args:\n        excinfo: The exception info from pytest\n        report: Optional test report for additional context (captured output, longrepr)\n    \"\"\"\n    if excinfo is None:\n        return False\n\n    exc = excinfo.value\n    exc_type = excinfo.typename\n    exc_str = str(exc).lower()\n\n    # BrokenResourceError typically indicates connection closed due to rate limit\n    if exc_type == \"BrokenResourceError\":\n        return True\n\n    # httpx.HTTPStatusError with 429 status\n    if exc_type == \"HTTPStatusError\":\n        try:\n            if hasattr(exc, \"response\") and exc.response.status_code == 429:\n                return True\n        except Exception:\n            pass\n\n    # Check for rate limit indicators in exception message\n    if \"429\" in exc_str or \"rate limit\" in exc_str or \"too many requests\" in exc_str:\n        return True\n\n    # Timeout exceptions may indicate rate limiting when the 429 causes asyncio\n    # shutdown issues. Check if it's a timeout and look for 429 in the captured output.\n    if \"timeout\" in exc_type.lower() or \"timeout\" in exc_str:\n        # Check captured output for 429 indicators\n        if report is not None:\n            longrepr_str = str(report.longrepr).lower() if report.longrepr else \"\"\n            if \"429\" in longrepr_str or \"too many requests\" in longrepr_str:\n                return True\n\n            # Check captured stdout/stderr\n            for section_name, content in getattr(report, \"sections\", []):\n                if \"429\" in content or \"too many requests\" in content.lower():\n                    return True\n\n    return False\n\n\n@pytest.hookimpl(hookwrapper=True)\ndef pytest_runtest_makereport(item, call):\n    \"\"\"Convert rate limit failures to skips for GitHub integration tests.\"\"\"\n    outcome = yield\n    report = outcome.get_result()\n\n    # Only process actual failures during the call or teardown phase, not xfails\n    if (\n        report.when in (\"call\", \"teardown\")\n        and report.failed\n        and not hasattr(report, \"wasxfail\")\n        and item.module.__name__ == \"tests.integration_tests.test_github_mcp_remote\"\n        and _is_rate_limit_error(call.excinfo, report)\n    ):\n        report.outcome = \"skipped\"\n        report.longrepr = (\n            os.path.abspath(__file__),\n            None,\n            \"Skipped: Skipping due to GitHub API rate limit (429)\",\n        )\n"
  },
  {
    "path": "tests/integration_tests/test_github_mcp_remote.py",
    "content": "import json\nimport os\n\nimport pytest\nfrom mcp import McpError\nfrom mcp.types import TextContent, Tool\n\nfrom fastmcp import Client\nfrom fastmcp.client import StreamableHttpTransport\nfrom fastmcp.client.auth.bearer import BearerAuth\n\nGITHUB_REMOTE_MCP_URL = \"https://api.githubcopilot.com/mcp/\"\n\nHEADER_AUTHORIZATION = \"Authorization\"\nFASTMCP_GITHUB_TOKEN = os.getenv(\"FASTMCP_GITHUB_TOKEN\")\n\n\n# Skip tests if no GitHub token is available\npytestmark = pytest.mark.xfail(\n    not FASTMCP_GITHUB_TOKEN,\n    reason=\"The FASTMCP_GITHUB_TOKEN environment variable is not set or empty\",\n)\n\n\n@pytest.fixture(name=\"streamable_http_client\")\ndef fixture_streamable_http_client() -> Client[StreamableHttpTransport]:\n    assert FASTMCP_GITHUB_TOKEN is not None\n\n    return Client(\n        StreamableHttpTransport(\n            url=GITHUB_REMOTE_MCP_URL,\n            auth=BearerAuth(FASTMCP_GITHUB_TOKEN),\n        )\n    )\n\n\nclass TestGithubMCPRemote:\n    async def test_connect_disconnect(\n        self,\n        streamable_http_client: Client[StreamableHttpTransport],\n    ):\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected() is True\n            await streamable_http_client._disconnect()  # pylint: disable=W0212 (protected-access)\n            assert streamable_http_client.is_connected() is False\n\n    async def test_ping(self, streamable_http_client: Client[StreamableHttpTransport]):\n        \"\"\"Test pinging the server.\"\"\"\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected() is True\n            result = await streamable_http_client.ping()\n            assert result is True\n\n    async def test_list_tools(\n        self, streamable_http_client: Client[StreamableHttpTransport]\n    ):\n        \"\"\"Test listing the MCP tools\"\"\"\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected()\n            tools = await streamable_http_client.list_tools()\n            assert isinstance(tools, list)\n            assert len(tools) > 0  # Ensure the tools list is non-empty\n            for tool in tools:\n                assert isinstance(tool, Tool)\n                assert len(tool.name) > 0\n                assert tool.description is not None and len(tool.description) > 0\n                assert isinstance(tool.inputSchema, dict)\n                assert len(tool.inputSchema) > 0\n\n    async def test_list_resources(\n        self, streamable_http_client: Client[StreamableHttpTransport]\n    ):\n        \"\"\"Test listing the MCP resources\"\"\"\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected()\n            resources = await streamable_http_client.list_resources()\n            assert isinstance(resources, list)\n            assert len(resources) == 0\n\n    async def test_list_prompts(\n        self, streamable_http_client: Client[StreamableHttpTransport]\n    ):\n        \"\"\"Test listing the MCP prompts\"\"\"\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected()\n            prompts = await streamable_http_client.list_prompts()\n            # there is at least one prompt (as of July 2025)\n            assert len(prompts) >= 1\n\n    async def test_call_tool_ko(\n        self, streamable_http_client: Client[StreamableHttpTransport]\n    ):\n        \"\"\"Test calling a non-existing tool\"\"\"\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected()\n            with pytest.raises(McpError, match=r\"unknown tool|tool not found\"):\n                await streamable_http_client.call_tool(\"foo\")\n\n    async def test_call_tool_list_commits(\n        self,\n        streamable_http_client: Client[StreamableHttpTransport],\n    ):\n        \"\"\"Test calling a list_commit tool\"\"\"\n        async with streamable_http_client:\n            assert streamable_http_client.is_connected()\n            result = await streamable_http_client.call_tool(\n                \"list_commits\", {\"owner\": \"prefecthq\", \"repo\": \"fastmcp\"}\n            )\n\n            # at this time, the github server does not support structured content\n            assert result.structured_content is None\n            assert isinstance(result.content, list)\n            assert len(result.content) == 1\n            assert isinstance(result.content[0], TextContent)\n            commits = json.loads(result.content[0].text)\n            for commit in commits:\n                assert isinstance(commit, dict)\n                assert \"sha\" in commit\n                assert \"commit\" in commit\n                assert \"author\" in commit[\"commit\"]\n                assert len(commit[\"commit\"][\"author\"][\"date\"]) > 0\n                assert len(commit[\"commit\"][\"author\"][\"name\"]) > 0\n                assert len(commit[\"commit\"][\"author\"][\"email\"]) > 0\n"
  },
  {
    "path": "tests/integration_tests/test_timeout_fix.py",
    "content": "\"\"\"Test that verifies the timeout fix for issue #2842 and #2845.\"\"\"\n\nimport asyncio\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.utilities.tests import run_server_async\n\n\ndef create_test_server() -> FastMCP:\n    \"\"\"Create a FastMCP server with a slow tool.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    async def slow_tool(duration: int = 6) -> str:\n        \"\"\"A tool that takes some time to complete.\"\"\"\n        await asyncio.sleep(duration)\n        return f\"Completed in {duration} seconds\"\n\n    return server\n\n\n@pytest.fixture\nasync def streamable_http_server():\n    \"\"\"Start a test server and return its URL.\"\"\"\n    server = create_test_server()\n    async with run_server_async(server) as url:\n        yield url\n\n\n@pytest.mark.integration\n@pytest.mark.timeout(15)\nasync def test_slow_tool_with_http_transport(streamable_http_server: str):\n    \"\"\"Test that tools taking >5 seconds work correctly with HTTP transport.\n\n    This test verifies the fix for:\n    - Issue #2842: Client can't get result after upgrading to 2.14.2\n    - Issue #2845: Server doesn't return results when tool takes >5 seconds\n\n    The root cause was that the httpx client was created without explicit\n    timeout configuration, defaulting to httpx's 5-second timeout.\n    \"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(streamable_http_server)\n    ) as client:\n        # This should NOT timeout since we fixed the default timeout\n        result = await client.call_tool(\"slow_tool\", {\"duration\": 6})\n        assert result.data == \"Completed in 6 seconds\"\n"
  },
  {
    "path": "tests/prompts/__init__.py",
    "content": ""
  },
  {
    "path": "tests/prompts/test_prompt.py",
    "content": "import pytest\nfrom mcp.types import EmbeddedResource, TextResourceContents\nfrom pydantic import FileUrl\n\nfrom fastmcp.prompts.base import (\n    Message,\n    Prompt,\n    PromptResult,\n)\n\n\nclass TestRenderPrompt:\n    async def test_basic_fn(self):\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [Message(\"Hello, world!\")]\n\n    async def test_async_fn(self):\n        async def fn() -> str:\n            return \"Hello, world!\"\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [Message(\"Hello, world!\")]\n\n    async def test_fn_with_args(self):\n        async def fn(name: str, age: int = 30) -> str:\n            return f\"Hello, {name}! You're {age} years old.\"\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render(arguments=dict(name=\"World\"))\n        assert result.messages == [Message(\"Hello, World! You're 30 years old.\")]\n\n    async def test_callable_object(self):\n        class MyPrompt:\n            def __call__(self, name: str) -> str:\n                return f\"Hello, {name}!\"\n\n        prompt = Prompt.from_function(MyPrompt())\n        result = await prompt.render(arguments=dict(name=\"World\"))\n        assert result.messages == [Message(\"Hello, World!\")]\n\n    async def test_async_callable_object(self):\n        class MyPrompt:\n            async def __call__(self, name: str) -> str:\n                return f\"Hello, {name}!\"\n\n        prompt = Prompt.from_function(MyPrompt())\n        result = await prompt.render(arguments=dict(name=\"World\"))\n        assert result.messages == [Message(\"Hello, World!\")]\n\n    async def test_fn_with_invalid_kwargs(self):\n        async def fn(name: str, age: int = 30) -> str:\n            return f\"Hello, {name}! You're {age} years old.\"\n\n        prompt = Prompt.from_function(fn)\n        with pytest.raises(ValueError):\n            await prompt.render(arguments=dict(age=40))\n\n    async def test_fn_returns_message_list(self):\n        async def fn() -> list[Message]:\n            return [Message(\"Hello, world!\")]\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [Message(\"Hello, world!\")]\n\n    async def test_fn_returns_assistant_message(self):\n        async def fn() -> list[Message]:\n            return [Message(\"Hello, world!\", role=\"assistant\")]\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [Message(\"Hello, world!\", role=\"assistant\")]\n\n    async def test_fn_returns_multiple_messages(self):\n        expected = [\n            Message(\"Hello, world!\"),\n            Message(\"How can I help you today?\", role=\"assistant\"),\n            Message(\"I'm looking for a restaurant in the center of town.\"),\n        ]\n\n        async def fn() -> list[Message]:\n            return expected\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == expected\n\n    async def test_fn_returns_list_of_strings(self):\n        expected = [\n            \"Hello, world!\",\n            \"I'm looking for a restaurant in the center of town.\",\n        ]\n\n        async def fn() -> list[str]:\n            return expected\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [Message(t) for t in expected]\n\n    async def test_fn_returns_resource_content(self):\n        \"\"\"Test returning a message with resource content.\"\"\"\n\n        async def fn() -> list[Message]:\n            return [\n                Message(\n                    content=EmbeddedResource(\n                        type=\"resource\",\n                        resource=TextResourceContents(\n                            uri=FileUrl(\"file://file.txt\"),\n                            text=\"File contents\",\n                            mimeType=\"text/plain\",\n                        ),\n                    ),\n                    role=\"user\",\n                )\n            ]\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [\n            Message(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=FileUrl(\"file://file.txt\"),\n                        text=\"File contents\",\n                        mimeType=\"text/plain\",\n                    ),\n                ),\n                role=\"user\",\n            )\n        ]\n\n    async def test_fn_returns_mixed_content(self):\n        \"\"\"Test returning messages with mixed content types.\"\"\"\n\n        async def fn() -> list[Message | str]:\n            return [\n                \"Please analyze this file:\",\n                Message(\n                    content=EmbeddedResource(\n                        type=\"resource\",\n                        resource=TextResourceContents(\n                            uri=FileUrl(\"file://file.txt\"),\n                            text=\"File contents\",\n                            mimeType=\"text/plain\",\n                        ),\n                    ),\n                    role=\"user\",\n                ),\n                Message(\"I'll help analyze that file.\", role=\"assistant\"),\n            ]\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [\n            Message(\"Please analyze this file:\"),\n            Message(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=FileUrl(\"file://file.txt\"),\n                        text=\"File contents\",\n                        mimeType=\"text/plain\",\n                    ),\n                ),\n                role=\"user\",\n            ),\n            Message(\"I'll help analyze that file.\", role=\"assistant\"),\n        ]\n\n    async def test_fn_returns_message_with_resource(self):\n        \"\"\"Test returning a message with resource content.\"\"\"\n\n        async def fn() -> list[Message]:\n            return [\n                Message(\n                    content=EmbeddedResource(\n                        type=\"resource\",\n                        resource=TextResourceContents(\n                            uri=FileUrl(\"file://file.txt\"),\n                            text=\"File contents\",\n                            mimeType=\"text/plain\",\n                        ),\n                    ),\n                    role=\"user\",\n                )\n            ]\n\n        prompt = Prompt.from_function(fn)\n        result = await prompt.render()\n        assert result.messages == [\n            Message(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=FileUrl(\"file://file.txt\"),\n                        text=\"File contents\",\n                        mimeType=\"text/plain\",\n                    ),\n                ),\n                role=\"user\",\n            )\n        ]\n\n\nclass TestPromptTypeConversion:\n    async def test_list_of_integers_as_string_args(self):\n        \"\"\"Test that prompts can handle complex types passed as strings from MCP spec.\"\"\"\n\n        def sum_numbers(numbers: list[int]) -> str:\n            \"\"\"Calculate the sum of a list of numbers.\"\"\"\n            total = sum(numbers)\n            return f\"The sum is: {total}\"\n\n        prompt = Prompt.from_function(sum_numbers)\n\n        # MCP spec only allows string arguments, so this should work\n        # after we implement type conversion\n        result_from_string = await prompt.render(\n            arguments={\"numbers\": \"[1, 2, 3, 4, 5]\"}\n        )\n        assert result_from_string.messages == [Message(\"The sum is: 15\")]\n\n        # Both should work now with string conversion\n        result_from_list_string = await prompt.render(\n            arguments={\"numbers\": \"[1, 2, 3, 4, 5]\"}\n        )\n        assert result_from_list_string.messages == result_from_string.messages\n\n    async def test_various_type_conversions(self):\n        \"\"\"Test type conversion for various data types.\"\"\"\n\n        def process_data(\n            name: str,\n            age: int,\n            scores: list[float],\n            metadata: dict[str, str],\n            active: bool,\n        ) -> str:\n            return f\"{name} ({age}): {len(scores)} scores, active={active}, metadata keys={list(metadata.keys())}\"\n\n        prompt = Prompt.from_function(process_data)\n\n        # All arguments as strings (as MCP would send them)\n        result = await prompt.render(\n            arguments={\n                \"name\": \"Alice\",\n                \"age\": \"25\",\n                \"scores\": \"[1.5, 2.0, 3.5]\",\n                \"metadata\": '{\"project\": \"test\", \"version\": \"1.0\"}',\n                \"active\": \"true\",\n            }\n        )\n\n        expected_text = (\n            \"Alice (25): 3 scores, active=True, metadata keys=['project', 'version']\"\n        )\n        assert result.messages == [Message(expected_text)]\n\n    async def test_type_conversion_error_handling(self):\n        \"\"\"Test that informative errors are raised for invalid type conversions.\"\"\"\n        from fastmcp.exceptions import PromptError\n\n        def typed_prompt(numbers: list[int]) -> str:\n            return f\"Got {len(numbers)} numbers\"\n\n        prompt = Prompt.from_function(typed_prompt)\n\n        # Test with invalid JSON - should raise PromptError due to exception handling in render()\n        with pytest.raises(PromptError) as exc_info:\n            await prompt.render(arguments={\"numbers\": \"not valid json\"})\n\n        assert f\"Error rendering prompt {prompt.name}\" in str(exc_info.value)\n\n    async def test_json_parsing_fallback(self):\n        \"\"\"Test that JSON parsing falls back to direct validation when needed.\"\"\"\n\n        def data_prompt(value: int) -> str:\n            return f\"Value: {value}\"\n\n        prompt = Prompt.from_function(data_prompt)\n\n        # This should work with JSON parsing (integer as string)\n        result1 = await prompt.render(arguments={\"value\": \"42\"})\n        assert result1.messages == [Message(\"Value: 42\")]\n\n        # This should work with direct validation (already an integer string)\n        result2 = await prompt.render(arguments={\"value\": \"123\"})\n        assert result2.messages == [Message(\"Value: 123\")]\n\n    async def test_mixed_string_and_typed_args(self):\n        \"\"\"Test mixing string args (no conversion) with typed args (conversion needed).\"\"\"\n\n        def mixed_prompt(message: str, count: int) -> str:\n            return f\"{message} (repeated {count} times)\"\n\n        prompt = Prompt.from_function(mixed_prompt)\n\n        result = await prompt.render(\n            arguments={\n                \"message\": \"Hello world\",  # str - no conversion needed\n                \"count\": \"3\",  # int - conversion needed\n            }\n        )\n\n        assert result.messages == [Message(\"Hello world (repeated 3 times)\")]\n\n\nclass TestPromptArgumentDescriptions:\n    def test_enhanced_descriptions_for_non_string_types(self):\n        \"\"\"Test that non-string argument types get enhanced descriptions with JSON schema.\"\"\"\n\n        def analyze_data(\n            name: str,\n            numbers: list[int],\n            metadata: dict[str, str],\n            threshold: float,\n            active: bool,\n        ) -> str:\n            \"\"\"Analyze numerical data.\"\"\"\n            return f\"Analyzed {name}\"\n\n        prompt = Prompt.from_function(analyze_data)\n\n        assert prompt.arguments is not None\n        # Check that string parameter has no schema enhancement\n        name_arg = next((arg for arg in prompt.arguments if arg.name == \"name\"), None)\n        assert name_arg is not None\n        assert name_arg.description is None  # No enhancement for string types\n\n        # Check that non-string parameters have schema enhancements\n        numbers_arg = next(\n            (arg for arg in prompt.arguments if arg.name == \"numbers\"), None\n        )\n        assert numbers_arg is not None\n        assert numbers_arg.description is not None\n        assert (\n            \"Provide as a JSON string matching the following schema:\"\n            in numbers_arg.description\n        )\n        assert '{\"items\":{\"type\":\"integer\"},\"type\":\"array\"}' in numbers_arg.description\n\n        metadata_arg = next(\n            (arg for arg in prompt.arguments if arg.name == \"metadata\"), None\n        )\n        assert metadata_arg is not None\n        assert metadata_arg.description is not None\n        assert (\n            \"Provide as a JSON string matching the following schema:\"\n            in metadata_arg.description\n        )\n        assert (\n            '{\"additionalProperties\":{\"type\":\"string\"},\"type\":\"object\"}'\n            in metadata_arg.description\n        )\n\n        threshold_arg = next(\n            (arg for arg in prompt.arguments if arg.name == \"threshold\"), None\n        )\n        assert threshold_arg is not None\n        assert threshold_arg.description is not None\n        assert (\n            \"Provide as a JSON string matching the following schema:\"\n            in threshold_arg.description\n        )\n        assert '{\"type\":\"number\"}' in threshold_arg.description\n\n        active_arg = next(\n            (arg for arg in prompt.arguments if arg.name == \"active\"), None\n        )\n        assert active_arg is not None\n        assert active_arg.description is not None\n        assert (\n            \"Provide as a JSON string matching the following schema:\"\n            in active_arg.description\n        )\n        assert '{\"type\":\"boolean\"}' in active_arg.description\n\n    def test_enhanced_descriptions_with_existing_descriptions(self):\n        \"\"\"Test that existing parameter descriptions are preserved with schema appended.\"\"\"\n        from typing import Annotated\n\n        from pydantic import Field\n\n        def documented_prompt(\n            numbers: Annotated[\n                list[int], Field(description=\"A list of integers to process\")\n            ],\n        ) -> str:\n            \"\"\"Process numbers.\"\"\"\n            return \"processed\"\n\n        prompt = Prompt.from_function(documented_prompt)\n\n        assert prompt.arguments is not None\n        numbers_arg = next(\n            (arg for arg in prompt.arguments if arg.name == \"numbers\"), None\n        )\n        assert numbers_arg is not None\n        # Should have both the original description and the schema\n        assert numbers_arg.description is not None\n        assert \"A list of integers to process\" in numbers_arg.description\n        assert \"\\n\\n\" in numbers_arg.description  # Should have newline separator\n        assert (\n            \"Provide as a JSON string matching the following schema:\"\n            in numbers_arg.description\n        )\n\n    def test_string_parameters_no_enhancement(self):\n        \"\"\"Test that string parameters don't get schema enhancement.\"\"\"\n\n        def string_only_prompt(message: str, name: str) -> str:\n            return f\"{message}, {name}\"\n\n        prompt = Prompt.from_function(string_only_prompt)\n\n        assert prompt.arguments is not None\n        for arg in prompt.arguments:\n            # String parameters should not have schema enhancement\n            if arg.description is not None:\n                assert (\n                    \"Provide as a JSON string matching the following schema:\"\n                    not in arg.description\n                )\n\n    def test_prompt_meta_parameter(self):\n        \"\"\"Test that meta parameter is properly handled.\"\"\"\n\n        def test_prompt(message: str) -> str:\n            return f\"Response: {message}\"\n\n        meta_data = {\"version\": \"3.0\", \"type\": \"prompt\"}\n        prompt = Prompt.from_function(test_prompt, meta=meta_data)\n\n        assert prompt.meta == meta_data\n        mcp_prompt = prompt.to_mcp_prompt()\n        # MCP prompt includes fastmcp meta, so check that our meta is included\n        assert mcp_prompt.meta is not None\n        assert meta_data.items() <= mcp_prompt.meta.items()\n\n\nclass TestMessage:\n    def test_message_string_content(self):\n        \"\"\"Test Message with string content.\"\"\"\n        from mcp.types import TextContent\n\n        msg = Message(\"Hello, world!\")\n        assert msg.role == \"user\"\n        assert isinstance(msg.content, TextContent)\n        assert msg.content.text == \"Hello, world!\"\n\n    def test_message_with_role(self):\n        \"\"\"Test Message with explicit role.\"\"\"\n        from mcp.types import TextContent\n\n        msg = Message(\"I can help.\", role=\"assistant\")\n        assert msg.role == \"assistant\"\n        assert isinstance(msg.content, TextContent)\n        assert msg.content.text == \"I can help.\"\n\n    def test_message_auto_serializes_dict(self):\n        \"\"\"Test Message auto-serializes dicts to JSON.\"\"\"\n        from mcp.types import TextContent\n\n        msg = Message({\"key\": \"value\", \"nested\": {\"a\": 1}})\n        assert msg.role == \"user\"\n        assert isinstance(msg.content, TextContent)\n        assert '\"key\"' in msg.content.text\n        assert '\"value\"' in msg.content.text\n\n    def test_message_auto_serializes_list(self):\n        \"\"\"Test Message auto-serializes lists to JSON.\"\"\"\n        from mcp.types import TextContent\n\n        msg = Message([\"item1\", \"item2\", \"item3\"])\n        assert isinstance(msg.content, TextContent)\n        assert '[\"item1\"' in msg.content.text\n\n    def test_message_to_mcp_prompt_message(self):\n        \"\"\"Test conversion to MCP PromptMessage.\"\"\"\n        from mcp.types import TextContent\n\n        msg = Message(\"Hello\", role=\"assistant\")\n        mcp_msg = msg.to_mcp_prompt_message()\n        assert mcp_msg.role == \"assistant\"\n        assert isinstance(mcp_msg.content, TextContent)\n        assert mcp_msg.content.text == \"Hello\"\n\n    def test_message_passthrough_image_content(self):\n        \"\"\"Test Message passes through ImageContent without JSON serialization.\"\"\"\n        from mcp.types import ImageContent\n\n        img = ImageContent(type=\"image\", data=\"base64data\", mimeType=\"image/png\")\n        msg = Message(img, role=\"user\")\n        assert isinstance(msg.content, ImageContent)\n        assert msg.content.data == \"base64data\"\n        assert msg.content.mimeType == \"image/png\"\n\n    def test_message_passthrough_audio_content(self):\n        \"\"\"Test Message passes through AudioContent without JSON serialization.\"\"\"\n        from mcp.types import AudioContent\n\n        audio = AudioContent(type=\"audio\", data=\"base64audio\", mimeType=\"audio/wav\")\n        msg = Message(audio, role=\"user\")\n        assert isinstance(msg.content, AudioContent)\n        assert msg.content.data == \"base64audio\"\n        assert msg.content.mimeType == \"audio/wav\"\n\n    def test_message_image_content_to_mcp_prompt_message(self):\n        \"\"\"Test that ImageContent round-trips through to_mcp_prompt_message.\"\"\"\n        from mcp.types import ImageContent\n\n        img = ImageContent(type=\"image\", data=\"base64data\", mimeType=\"image/png\")\n        msg = Message(img, role=\"user\")\n        mcp_msg = msg.to_mcp_prompt_message()\n        assert isinstance(mcp_msg.content, ImageContent)\n        assert mcp_msg.content.data == \"base64data\"\n\n\nclass TestPromptResult:\n    def test_promptresult_from_string(self):\n        \"\"\"Test PromptResult accepts string and wraps as Message.\"\"\"\n        from mcp.types import TextContent\n\n        result = PromptResult(\"Hello!\")\n        assert len(result.messages) == 1\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Hello!\"\n        assert result.messages[0].role == \"user\"\n\n    def test_promptresult_from_message_list(self):\n        \"\"\"Test PromptResult accepts list of Messages.\"\"\"\n        result = PromptResult(\n            [\n                Message(\"Question?\"),\n                Message(\"Answer.\", role=\"assistant\"),\n            ]\n        )\n        assert len(result.messages) == 2\n        assert result.messages[0].role == \"user\"\n        assert result.messages[1].role == \"assistant\"\n\n    def test_promptresult_rejects_single_message(self):\n        \"\"\"Test PromptResult rejects single Message (must be in list).\"\"\"\n        with pytest.raises(TypeError, match=\"must be str or list\"):\n            PromptResult(Message(\"Hello\"))  # type: ignore[arg-type]\n\n    def test_promptresult_rejects_dict(self):\n        \"\"\"Test PromptResult rejects dict.\"\"\"\n        with pytest.raises(TypeError, match=\"must be str or list\"):\n            PromptResult({\"key\": \"value\"})  # type: ignore[arg-type]\n\n    def test_promptresult_with_meta(self):\n        \"\"\"Test PromptResult with meta field.\"\"\"\n        result = PromptResult(\n            \"Hello!\", meta={\"priority\": \"high\", \"category\": \"greeting\"}\n        )\n        assert result.meta == {\"priority\": \"high\", \"category\": \"greeting\"}\n\n    def test_promptresult_with_description(self):\n        \"\"\"Test PromptResult with description field.\"\"\"\n        result = PromptResult(\"Hello!\", description=\"A greeting prompt\")\n        assert result.description == \"A greeting prompt\"\n\n    def test_promptresult_to_mcp(self):\n        \"\"\"Test conversion to MCP GetPromptResult.\"\"\"\n        result = PromptResult(\n            [Message(\"Hello\"), Message(\"World\", role=\"assistant\")],\n            description=\"Test\",\n            meta={\"key\": \"value\"},\n        )\n        mcp_result = result.to_mcp_prompt_result()\n        assert len(mcp_result.messages) == 2\n        assert mcp_result.description == \"Test\"\n        assert mcp_result.meta == {\"key\": \"value\"}\n\n\nclass TestPromptFieldDefaults:\n    \"\"\"Test prompts with Field() defaults.\"\"\"\n\n    async def test_field_with_default(self):\n        \"\"\"Test that Field(default=...) correctly provides default values.\"\"\"\n\n        from pydantic import Field\n\n        def prompt_with_defaults(\n            required: str = Field(description=\"Required parameter\"),\n            optional: str = Field(\n                default=\"default_value\", description=\"Optional parameter\"\n            ),\n        ) -> str:\n            return f\"required={required}, optional={optional}\"\n\n        prompt = Prompt.from_function(prompt_with_defaults)\n        result = await prompt.render(arguments={\"required\": \"test\"})\n        assert result.messages == [Message(\"required=test, optional=default_value\")]\n\n    async def test_annotated_field_with_default_in_signature(self):\n        \"\"\"Test that Annotated[type, Field(...)] with default in signature works.\"\"\"\n        from typing import Annotated\n\n        from pydantic import Field\n\n        def prompt_with_annotated(\n            required: Annotated[str, Field(description=\"Required parameter\")],\n            optional: Annotated[\n                str, Field(description=\"Optional parameter\")\n            ] = \"default_value\",\n        ) -> str:\n            return f\"required={required}, optional={optional}\"\n\n        prompt = Prompt.from_function(prompt_with_annotated)\n        result = await prompt.render(arguments={\"required\": \"test\"})\n        assert result.messages == [Message(\"required=test, optional=default_value\")]\n\n    async def test_multiple_field_defaults(self):\n        \"\"\"Test multiple parameters with Field() defaults.\"\"\"\n        from pydantic import Field\n\n        def prompt_with_multiple_defaults(\n            name: str = Field(description=\"Name\"),\n            greeting: str = Field(default=\"Hello\", description=\"Greeting\"),\n            punctuation: str = Field(default=\"!\", description=\"Punctuation\"),\n        ) -> str:\n            return f\"{greeting}, {name}{punctuation}\"\n\n        prompt = Prompt.from_function(prompt_with_multiple_defaults)\n\n        # Test with only required parameter\n        result1 = await prompt.render(arguments={\"name\": \"World\"})\n        assert result1.messages == [Message(\"Hello, World!\")]\n\n        # Test overriding one default\n        result2 = await prompt.render(arguments={\"name\": \"World\", \"greeting\": \"Hi\"})\n        assert result2.messages == [Message(\"Hi, World!\")]\n\n        # Test overriding all defaults\n        result3 = await prompt.render(\n            arguments={\"name\": \"World\", \"greeting\": \"Greetings\", \"punctuation\": \".\"}\n        )\n        assert result3.messages == [Message(\"Greetings, World.\")]\n\n    async def test_field_defaults_with_type_conversion(self):\n        \"\"\"Test Field() defaults work with type conversion for non-string types.\"\"\"\n        from pydantic import Field\n\n        def prompt_with_typed_defaults(\n            count: int = Field(description=\"Count\"),\n            multiplier: int = Field(default=2, description=\"Multiplier\"),\n        ) -> str:\n            return f\"result={count * multiplier}\"\n\n        prompt = Prompt.from_function(prompt_with_typed_defaults)\n\n        # Pass count as string (MCP requirement), should use default for multiplier\n        result = await prompt.render(arguments={\"count\": \"5\"})\n        assert result.messages == [Message(\"result=10\")]\n\n\nclass TestPromptCallableAndConcurrency:\n    \"\"\"Test prompts with callable objects and concurrent execution.\"\"\"\n\n    async def test_callable_object_sync(self):\n        \"\"\"Test that callable objects with sync __call__ work.\"\"\"\n\n        class MyPrompt:\n            def __init__(self, greeting: str):\n                self.greeting = greeting\n\n            def __call__(self) -> str:\n                return f\"{self.greeting}, world!\"\n\n        prompt = Prompt.from_function(MyPrompt(\"Hello\"))\n        result = await prompt.render()\n        assert result.messages == [Message(\"Hello, world!\")]\n\n    async def test_callable_object_async(self):\n        \"\"\"Test that callable objects with async __call__ work.\"\"\"\n\n        class AsyncPrompt:\n            def __init__(self, greeting: str):\n                self.greeting = greeting\n\n            async def __call__(self) -> str:\n                return f\"async {self.greeting}!\"\n\n        prompt = Prompt.from_function(AsyncPrompt(\"Hello\"))\n        result = await prompt.render()\n        assert result.messages == [Message(\"async Hello!\")]\n\n    async def test_sync_prompt_runs_concurrently(self):\n        \"\"\"Test that sync prompts run in threadpool and don't block each other.\"\"\"\n        import asyncio\n        import threading\n\n        num_calls = 3\n        barrier = threading.Barrier(num_calls, timeout=0.5)\n\n        def concurrent_prompt() -> str:\n            barrier.wait()\n            return \"done\"\n\n        prompt = Prompt.from_function(concurrent_prompt)\n\n        # Run concurrent renders - will raise BrokenBarrierError if not concurrent\n        results = await asyncio.gather(\n            prompt.render(),\n            prompt.render(),\n            prompt.render(),\n        )\n        assert all(r.messages == [Message(\"done\")] for r in results)\n"
  },
  {
    "path": "tests/prompts/test_standalone_decorator.py",
    "content": "\"\"\"Tests for the standalone @prompt decorator.\n\nThe @prompt decorator attaches metadata to functions without registering them\nto a server. Functions can be added explicitly via server.add_prompt() or\ndiscovered by FileSystemProvider.\n\"\"\"\n\nfrom typing import cast\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.prompts import prompt\nfrom fastmcp.prompts.function_prompt import DecoratedPrompt, PromptMeta\n\n\nclass TestPromptDecorator:\n    \"\"\"Tests for the @prompt decorator.\"\"\"\n\n    def test_prompt_without_parens(self):\n        \"\"\"@prompt without parentheses should attach metadata.\"\"\"\n\n        @prompt\n        def analyze(topic: str) -> str:\n            return f\"Analyze: {topic}\"\n\n        decorated = cast(DecoratedPrompt, analyze)\n        assert callable(analyze)\n        assert hasattr(analyze, \"__fastmcp__\")\n        assert isinstance(decorated.__fastmcp__, PromptMeta)\n        assert decorated.__fastmcp__.name is None  # Uses function name by default\n\n    def test_prompt_with_empty_parens(self):\n        \"\"\"@prompt() with empty parentheses should attach metadata.\"\"\"\n\n        @prompt()\n        def analyze(topic: str) -> str:\n            return f\"Analyze: {topic}\"\n\n        decorated = cast(DecoratedPrompt, analyze)\n        assert callable(analyze)\n        assert hasattr(analyze, \"__fastmcp__\")\n        assert isinstance(decorated.__fastmcp__, PromptMeta)\n\n    def test_prompt_with_name_arg(self):\n        \"\"\"@prompt(\"name\") with name as first arg should work.\"\"\"\n\n        @prompt(\"custom-analyze\")\n        def analyze(topic: str) -> str:\n            return f\"Analyze: {topic}\"\n\n        decorated = cast(DecoratedPrompt, analyze)\n        assert callable(analyze)\n        assert hasattr(analyze, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"custom-analyze\"\n\n    def test_prompt_with_name_kwarg(self):\n        \"\"\"@prompt(name=\"name\") with keyword arg should work.\"\"\"\n\n        @prompt(name=\"custom-analyze\")\n        def analyze(topic: str) -> str:\n            return f\"Analyze: {topic}\"\n\n        decorated = cast(DecoratedPrompt, analyze)\n        assert callable(analyze)\n        assert hasattr(analyze, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"custom-analyze\"\n\n    def test_prompt_with_all_metadata(self):\n        \"\"\"@prompt with all metadata should store it all.\"\"\"\n\n        @prompt(\n            name=\"custom-analyze\",\n            title=\"Analysis Prompt\",\n            description=\"Analyzes topics\",\n            tags={\"analysis\", \"demo\"},\n            meta={\"custom\": \"value\"},\n        )\n        def analyze(topic: str) -> str:\n            return f\"Analyze: {topic}\"\n\n        decorated = cast(DecoratedPrompt, analyze)\n        assert callable(analyze)\n        assert hasattr(analyze, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"custom-analyze\"\n        assert decorated.__fastmcp__.title == \"Analysis Prompt\"\n        assert decorated.__fastmcp__.description == \"Analyzes topics\"\n        assert decorated.__fastmcp__.tags == {\"analysis\", \"demo\"}\n        assert decorated.__fastmcp__.meta == {\"custom\": \"value\"}\n\n    async def test_prompt_function_still_callable(self):\n        \"\"\"Decorated function should still be directly callable.\"\"\"\n\n        @prompt\n        def analyze(topic: str) -> str:\n            \"\"\"Analyze a topic.\"\"\"\n            return f\"Please analyze: {topic}\"\n\n        # The function is still callable even though it has metadata\n        result = cast(DecoratedPrompt, analyze)(\"Python\")\n        assert result == \"Please analyze: Python\"\n\n    def test_prompt_rejects_classmethod_decorator(self):\n        \"\"\"@prompt should reject classmethod-decorated functions.\"\"\"\n        with pytest.raises(TypeError, match=\"classmethod\"):\n\n            class MyClass:\n                @prompt\n                @classmethod\n                def my_prompt(cls, topic: str) -> str:\n                    return f\"Analyze: {topic}\"\n\n    def test_prompt_with_both_name_args_raises(self):\n        \"\"\"@prompt should raise if both positional and keyword name are given.\"\"\"\n        with pytest.raises(TypeError, match=\"Cannot specify.*both.*argument.*keyword\"):\n\n            @prompt(\"name1\", name=\"name2\")  # type: ignore[call-overload]\n            def my_prompt() -> str:\n                return \"hello\"\n\n    async def test_prompt_added_to_server(self):\n        \"\"\"Prompt created by @prompt should work when added to a server.\"\"\"\n\n        @prompt\n        def analyze(topic: str) -> str:\n            \"\"\"Analyze a topic.\"\"\"\n            return f\"Please analyze: {topic}\"\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_prompt(analyze)\n\n        async with Client(mcp) as client:\n            prompts = await client.list_prompts()\n            assert any(p.name == \"analyze\" for p in prompts)\n\n            result = await client.get_prompt(\"analyze\", {\"topic\": \"FastMCP\"})\n            assert \"FastMCP\" in str(result)\n"
  },
  {
    "path": "tests/resources/__init__.py",
    "content": ""
  },
  {
    "path": "tests/resources/test_file_resources.py",
    "content": "import os\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\nfrom pydantic import FileUrl\n\nfrom fastmcp.exceptions import ResourceError\nfrom fastmcp.resources import FileResource\nfrom fastmcp.resources.base import ResourceResult\n\n\n@pytest.fixture\ndef temp_file():\n    \"\"\"Create a temporary file for testing.\n\n    File is automatically cleaned up after the test if it still exists.\n    \"\"\"\n    content = \"test content\"\n    with NamedTemporaryFile(mode=\"w\", delete=False) as f:\n        f.write(content)\n        path = Path(f.name).resolve()\n    yield path\n    try:\n        path.unlink()\n    except FileNotFoundError:\n        pass  # File was already deleted by the test\n\n\nclass TestFileResource:\n    \"\"\"Test FileResource functionality.\"\"\"\n\n    def test_file_resource_creation(self, temp_file: Path):\n        \"\"\"Test creating a FileResource.\"\"\"\n        resource = FileResource(\n            uri=FileUrl(temp_file.as_uri()),\n            name=\"test\",\n            description=\"test file\",\n            path=temp_file,\n        )\n        assert str(resource.uri) == temp_file.as_uri()\n        assert resource.name == \"test\"\n        assert resource.description == \"test file\"\n        assert resource.mime_type == \"text/plain\"  # default\n        assert resource.path == temp_file\n        assert resource.is_binary is False  # default\n\n    def test_file_resource_str_path_conversion(self, temp_file: Path):\n        \"\"\"Test FileResource handles string paths.\"\"\"\n        resource = FileResource(\n            uri=FileUrl(f\"file://{temp_file}\"),\n            name=\"test\",\n            path=Path(str(temp_file)),\n        )\n        assert isinstance(resource.path, Path)\n        assert resource.path.is_absolute()\n\n    async def test_read_text_file(self, temp_file: Path):\n        \"\"\"Test reading a text file.\"\"\"\n        resource = FileResource(\n            uri=FileUrl(f\"file://{temp_file}\"),\n            name=\"test\",\n            path=temp_file,\n        )\n        result = await resource.read()\n        assert isinstance(result, ResourceResult)\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"test content\"\n        assert result.contents[0].mime_type == \"text/plain\"\n\n    async def test_read_binary_file(self, temp_file: Path):\n        \"\"\"Test reading a file as binary.\"\"\"\n        resource = FileResource(\n            uri=FileUrl(f\"file://{temp_file}\"),\n            name=\"test\",\n            path=temp_file,\n            is_binary=True,\n        )\n        result = await resource.read()\n        assert isinstance(result, ResourceResult)\n        assert len(result.contents) == 1\n        assert result.contents[0].content == b\"test content\"\n\n    def test_relative_path_error(self):\n        \"\"\"Test error on relative path.\"\"\"\n        with pytest.raises(ValueError, match=\"Path must be absolute\"):\n            FileResource(\n                uri=FileUrl(\"file:///test.txt\"),\n                name=\"test\",\n                path=Path(\"test.txt\"),\n            )\n\n    async def test_missing_file_error(self, temp_file: Path):\n        \"\"\"Test error when file doesn't exist.\"\"\"\n        # Create path to non-existent file\n        missing = temp_file.parent / \"missing.txt\"\n        resource = FileResource(\n            uri=FileUrl(\"file:///missing.txt\"),\n            name=\"test\",\n            path=missing,\n        )\n        with pytest.raises(ResourceError, match=\"Error reading file\"):\n            await resource.read()\n\n    @pytest.mark.skipif(\n        os.name == \"nt\" or (hasattr(os, \"getuid\") and os.getuid() == 0),\n        reason=\"File permissions behave differently on Windows or when running as root\",\n    )\n    async def test_permission_error(self, temp_file: Path):\n        \"\"\"Test reading a file without permissions.\"\"\"\n        temp_file.chmod(0o000)  # Remove all permissions\n        try:\n            resource = FileResource(\n                uri=FileUrl(temp_file.as_uri()),\n                name=\"test\",\n                path=temp_file,\n            )\n            with pytest.raises(ResourceError, match=\"Error reading file\"):\n                await resource.read()\n        finally:\n            temp_file.chmod(0o644)  # Restore permissions\n"
  },
  {
    "path": "tests/resources/test_function_resources.py",
    "content": "import pytest\nfrom pydantic import AnyUrl, BaseModel\n\nfrom fastmcp.resources.base import ResourceContent\nfrom fastmcp.resources.function_resource import FunctionResource\n\n\nclass TestFunctionResource:\n    \"\"\"Test FunctionResource functionality.\"\"\"\n\n    def test_function_resource_creation(self):\n        \"\"\"Test creating a FunctionResource.\"\"\"\n\n        def my_func() -> str:\n            return \"test content\"\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"fn://test\"),\n            name=\"test\",\n            description=\"test function\",\n            fn=my_func,\n        )\n        assert str(resource.uri) == \"fn://test\"\n        assert resource.name == \"test\"\n        assert resource.description == \"test function\"\n        assert resource.mime_type == \"text/plain\"  # default\n        assert resource.fn == my_func\n\n    async def test_read_text(self):\n        \"\"\"Test reading text from a FunctionResource.\"\"\"\n\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        # read() returns raw value\n        result = await resource.read()\n        assert result == \"Hello, world!\"\n\n        # _read() converts to ResourceResult\n        result = await resource._read()\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"Hello, world!\"\n        assert result.contents[0].mime_type == \"text/plain\"\n\n    async def test_read_binary(self):\n        \"\"\"Test reading binary data from a FunctionResource.\"\"\"\n\n        def get_data() -> bytes:\n            return b\"Hello, world!\"\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        # read() returns raw value\n        result = await resource.read()\n        assert result == b\"Hello, world!\"\n\n        # _read() converts to ResourceResult\n        result = await resource._read()\n        assert result.contents[0].content == b\"Hello, world!\"\n\n    async def test_dict_return_raises_type_error(self):\n        \"\"\"Returning dict from read() raises TypeError - use ResourceResult.\"\"\"\n\n        def get_data() -> dict:\n            return {\"key\": \"value\"}\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        # read() returns raw value (no type checking at runtime)\n        result = await resource.read()\n        assert result == {\"key\": \"value\"}\n\n        # _read() raises TypeError - must return str, bytes, or ResourceResult\n        with pytest.raises(TypeError, match=\"must be str, bytes, or list\"):\n            await resource._read()\n\n    async def test_error_handling(self):\n        \"\"\"Test error handling in FunctionResource.\"\"\"\n\n        def failing_func() -> str:\n            raise ValueError(\"Test error\")\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=failing_func,\n        )\n        with pytest.raises(ValueError, match=\"Test error\"):\n            await resource.read()\n\n    async def test_basemodel_return_raises_type_error(self):\n        \"\"\"Returning BaseModel from read() raises TypeError - use ResourceResult.\"\"\"\n\n        class MyModel(BaseModel):\n            name: str\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=lambda: MyModel(name=\"test\"),\n        )\n        # read() returns raw value (no type checking at runtime)\n        raw_result = await resource.read()\n        assert isinstance(raw_result, MyModel)\n\n        # _read() raises TypeError - must return str, bytes, or ResourceResult\n        with pytest.raises(TypeError, match=\"must be str, bytes, or list\"):\n            await resource._read()\n\n    async def test_custom_type_return_raises_type_error(self):\n        \"\"\"Returning custom type from read() raises TypeError - use ResourceResult.\"\"\"\n\n        class CustomData:\n            def __str__(self) -> str:\n                return \"custom data\"\n\n        def get_data() -> CustomData:\n            return CustomData()\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        # read() returns raw value (no type checking at runtime)\n        raw_result = await resource.read()\n        assert isinstance(raw_result, CustomData)\n\n        # _read() raises TypeError - must return str, bytes, or ResourceResult\n        with pytest.raises(TypeError, match=\"must be str, bytes, or list\"):\n            await resource._read()\n\n    async def test_async_read_text(self):\n        \"\"\"Test reading text from async FunctionResource.\"\"\"\n\n        async def get_data() -> str:\n            return \"Hello, world!\"\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        # read() returns raw value\n        result = await resource.read()\n        assert result == \"Hello, world!\"\n\n        # _read() converts to ResourceResult\n        result = await resource._read()\n        assert result.contents[0].content == \"Hello, world!\"\n        assert result.contents[0].mime_type == \"text/plain\"\n\n    async def test_resource_content_text(self):\n        \"\"\"Test returning ResourceContent with text content.\"\"\"\n\n        def get_data() -> ResourceContent:\n            return ResourceContent(\n                content=\"Hello, world!\",\n                mime_type=\"text/html\",\n                meta={\"csp\": \"script-src 'self'\"},\n            )\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        result = await resource.read()\n        assert isinstance(result, ResourceContent)\n        assert result.content == \"Hello, world!\"\n        assert result.mime_type == \"text/html\"\n        assert result.meta == {\"csp\": \"script-src 'self'\"}\n\n    async def test_resource_content_binary(self):\n        \"\"\"Test returning ResourceContent with binary content.\"\"\"\n\n        def get_data() -> ResourceContent:\n            return ResourceContent(\n                content=b\"\\x00\\x01\\x02\",\n                mime_type=\"application/octet-stream\",\n            )\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        result = await resource.read()\n        assert isinstance(result, ResourceContent)\n        assert result.content == b\"\\x00\\x01\\x02\"\n        assert result.mime_type == \"application/octet-stream\"\n        assert result.meta is None\n\n    async def test_resource_content_without_meta(self):\n        \"\"\"Test that ResourceContent auto-sets mime_type defaults.\"\"\"\n        content = ResourceContent(content=\"plain text\")\n        assert content.content == \"plain text\"\n        assert content.mime_type == \"text/plain\"  # Auto-set for string content\n        assert content.meta is None\n\n    async def test_async_resource_content(self):\n        \"\"\"Test async function returning ResourceContent.\"\"\"\n\n        async def get_data() -> ResourceContent:\n            return ResourceContent(\n                content=\"async content\",\n                meta={\"key\": \"value\"},\n            )\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"function://test\"),\n            name=\"test\",\n            fn=get_data,\n        )\n        result = await resource.read()\n        assert isinstance(result, ResourceContent)\n        assert result.content == \"async content\"\n        assert result.meta == {\"key\": \"value\"}\n\n\nclass TestResourceContentToMcp:\n    \"\"\"Test ResourceContent.to_mcp_resource_contents method.\"\"\"\n\n    def test_text_content_to_mcp(self):\n        \"\"\"Test converting text ResourceContent to MCP type.\"\"\"\n        rc = ResourceContent(\n            content=\"hello world\",\n            mime_type=\"text/html\",\n            meta={\"csp\": \"script-src 'self'\"},\n        )\n        mcp_content = rc.to_mcp_resource_contents(\"resource://test\")\n\n        assert hasattr(mcp_content, \"text\")\n        assert mcp_content.text == \"hello world\"\n        assert mcp_content.mimeType == \"text/html\"\n        assert mcp_content.meta == {\"csp\": \"script-src 'self'\"}\n\n    def test_binary_content_to_mcp(self):\n        \"\"\"Test converting binary ResourceContent to MCP type.\"\"\"\n        rc = ResourceContent(\n            content=b\"\\x00\\x01\\x02\",\n            mime_type=\"application/octet-stream\",\n            meta={\"encoding\": \"raw\"},\n        )\n        mcp_content = rc.to_mcp_resource_contents(\"resource://test\")\n\n        assert hasattr(mcp_content, \"blob\")\n        assert mcp_content.blob == \"AAEC\"  # base64 of \\x00\\x01\\x02\n        assert mcp_content.mimeType == \"application/octet-stream\"\n        assert mcp_content.meta == {\"encoding\": \"raw\"}\n\n    def test_default_mime_types(self):\n        \"\"\"Test default mime types are applied correctly.\"\"\"\n        text_rc = ResourceContent(content=\"text\")\n        text_mcp = text_rc.to_mcp_resource_contents(\"resource://test\")\n        assert text_mcp.mimeType == \"text/plain\"\n\n        binary_rc = ResourceContent(content=b\"binary\")\n        binary_mcp = binary_rc.to_mcp_resource_contents(\"resource://test\")\n        assert binary_mcp.mimeType == \"application/octet-stream\"\n\n    def test_none_meta(self):\n        \"\"\"Test that None meta is handled correctly.\"\"\"\n        rc = ResourceContent(content=\"no meta\")\n        mcp_content = rc.to_mcp_resource_contents(\"resource://test\")\n\n        assert mcp_content.meta is None\n\n\nclass TestFunctionResourceCallable:\n    \"\"\"Test FunctionResource with callable objects.\"\"\"\n\n    async def test_callable_object_sync(self):\n        \"\"\"Test that callable objects with sync __call__ work.\"\"\"\n\n        class MyResource:\n            def __init__(self, value: str):\n                self.value = value\n\n            def __call__(self) -> str:\n                return f\"value: {self.value}\"\n\n        resource = FunctionResource.from_function(MyResource(\"test\"), uri=\"fn://test\")\n        result = await resource.read()\n        assert result == \"value: test\"\n\n    async def test_callable_object_async(self):\n        \"\"\"Test that callable objects with async __call__ work.\"\"\"\n\n        class AsyncResource:\n            def __init__(self, value: str):\n                self.value = value\n\n            async def __call__(self) -> str:\n                return f\"async value: {self.value}\"\n\n        resource = FunctionResource.from_function(\n            AsyncResource(\"test\"), uri=\"fn://test\"\n        )\n        result = await resource.read()\n        assert result == \"async value: test\"\n\n    async def test_sync_resource_runs_concurrently(self):\n        \"\"\"Test that sync resources run in threadpool and don't block each other.\"\"\"\n        import asyncio\n        import threading\n\n        num_calls = 3\n        barrier = threading.Barrier(num_calls, timeout=0.5)\n\n        def concurrent_resource() -> str:\n            barrier.wait()\n            return \"done\"\n\n        resource = FunctionResource.from_function(concurrent_resource, uri=\"fn://test\")\n\n        # Run concurrent reads - will raise BrokenBarrierError if not concurrent\n        results = await asyncio.gather(\n            resource.read(),\n            resource.read(),\n            resource.read(),\n        )\n        assert results == [\"done\", \"done\", \"done\"]\n"
  },
  {
    "path": "tests/resources/test_resource_template.py",
    "content": "import functools\nfrom urllib.parse import quote\n\nimport pytest\nfrom pydantic import BaseModel\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.resources import ResourceTemplate\nfrom fastmcp.resources.function_resource import FunctionResource\nfrom fastmcp.resources.template import build_regex, match_uri_template\n\n\nclass TestResourceTemplate:\n    \"\"\"Test ResourceTemplate functionality.\"\"\"\n\n    def test_template_creation(self):\n        \"\"\"Test creating a template from a function.\"\"\"\n\n        def my_func(key: str, value: int) -> dict:\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{key}/{value}\"\n        assert template.name == \"test\"\n        assert template.mime_type == \"text/plain\"  # default\n\n        assert template.fn(key=\"test\", value=42) == my_func(key=\"test\", value=42)\n\n    def test_template_matches(self):\n        \"\"\"Test matching URIs against a template.\"\"\"\n\n        def my_func(key: str, value: int) -> dict:\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        # Valid match\n        params = template.matches(\"test://foo/123\")\n        assert params == {\"key\": \"foo\", \"value\": \"123\"}\n\n        # No match\n        assert template.matches(\"test://foo\") is None\n        assert template.matches(\"other://foo/123\") is None\n\n    def test_template_matches_with_prefix(self):\n        \"\"\"Test matching URIs against a template with a prefix.\"\"\"\n\n        def my_func(key: str, value: int) -> dict:\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"app+test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        # Valid match\n        params = template.matches(\"app+test://foo/123\")\n        assert params == {\"key\": \"foo\", \"value\": \"123\"}\n\n        # No match\n        assert template.matches(\"test://foo/123\") is None\n        assert template.matches(\"test://foo\") is None\n        assert template.matches(\"other://foo/123\") is None\n\n    def test_template_uri_validation(self):\n        \"\"\"Test validation rule: URI template must have at least one parameter.\"\"\"\n\n        def my_func() -> dict:\n            return {\"data\": \"value\"}\n\n        with pytest.raises(\n            ValueError, match=\"URI template must contain at least one parameter\"\n        ):\n            ResourceTemplate.from_function(\n                fn=my_func,\n                uri_template=\"test://no-params\",\n                name=\"test\",\n            )\n\n    def test_template_uri_params_subset_of_function_params(self):\n        \"\"\"Test validation rule: URI parameters must be a subset of function parameters.\"\"\"\n\n        def my_func(key: str, value: int) -> dict:\n            return {\"key\": key, \"value\": value}\n\n        # This should work - URI params are a subset of function params\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{key}/{value}\"\n\n        # This should fail - 'unknown' is not a function parameter\n        with pytest.raises(\n            ValueError,\n            match=\"Required function arguments .* must be a subset of the URI path parameters\",\n        ):\n            ResourceTemplate.from_function(\n                fn=my_func,\n                uri_template=\"test://{key}/{unknown}\",\n                name=\"test\",\n            )\n\n    def test_required_params_subset_of_uri_params(self):\n        \"\"\"Test validation rule: Required function parameters must be in URI parameters.\"\"\"\n\n        # Function with required parameters\n        def func_with_required(\n            required_param: str, optional_param: str = \"default\"\n        ) -> dict:\n            return {\"required\": required_param, \"optional\": optional_param}\n\n        # This should work - required param is in URI\n        template = ResourceTemplate.from_function(\n            fn=func_with_required,\n            uri_template=\"test://{required_param}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{required_param}\"\n\n        # This should fail - required param is not in URI\n        with pytest.raises(\n            ValueError,\n            match=\"Required function arguments .* must be a subset of the URI path parameters\",\n        ):\n            ResourceTemplate.from_function(\n                fn=func_with_required,\n                uri_template=\"test://{optional_param}\",\n                name=\"test\",\n            )\n\n    def test_multiple_required_params(self):\n        \"\"\"Test validation with multiple required parameters.\"\"\"\n\n        def multi_required(param1: str, param2: int, optional: str = \"default\") -> dict:\n            return {\"p1\": param1, \"p2\": param2, \"opt\": optional}\n\n        # This works - all required params in URI\n        template = ResourceTemplate.from_function(\n            fn=multi_required,\n            uri_template=\"test://{param1}/{param2}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{param1}/{param2}\"\n\n        # This fails - missing one required param\n        with pytest.raises(\n            ValueError,\n            match=\"Required function arguments .* must be a subset of the URI path parameters\",\n        ):\n            ResourceTemplate.from_function(\n                fn=multi_required,\n                uri_template=\"test://{param1}\",\n                name=\"test\",\n            )\n\n    async def test_create_resource(self):\n        \"\"\"Test creating a resource from a template.\"\"\"\n\n        def my_func(key: str, value: int) -> str:\n            return f\"key={key}, value={value}\"\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://foo/123\",\n            {\"key\": \"foo\", \"value\": 123},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        # read() returns raw value from function\n        result = await resource.read()\n        assert result == \"key=foo, value=123\"\n\n        # _read() wraps in ResourceResult\n        resource_result = await resource._read()\n        assert len(resource_result.contents) == 1\n        assert resource_result.contents[0].content == \"key=foo, value=123\"\n\n    async def test_async_text_resource(self):\n        \"\"\"Test creating a text resource from async function.\"\"\"\n\n        async def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        template = ResourceTemplate.from_function(\n            fn=greet,\n            uri_template=\"greet://{name}\",\n            name=\"greeter\",\n        )\n\n        resource = await template.create_resource(\n            \"greet://world\",\n            {\"name\": \"world\"},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        # read() returns raw value\n        result = await resource.read()\n        assert result == \"Hello, world!\"\n\n    async def test_async_binary_resource(self):\n        \"\"\"Test creating a binary resource from async function.\"\"\"\n\n        async def get_bytes(value: str) -> bytes:\n            return value.encode()\n\n        template = ResourceTemplate.from_function(\n            fn=get_bytes,\n            uri_template=\"bytes://{value}\",\n            name=\"bytes\",\n        )\n\n        resource = await template.create_resource(\n            \"bytes://test\",\n            {\"value\": \"test\"},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        # read() returns raw bytes\n        result = await resource.read()\n        assert result == b\"test\"\n\n    async def test_basemodel_conversion(self):\n        \"\"\"Test handling of BaseModel types.\"\"\"\n\n        class MyModel(BaseModel):\n            key: str\n            value: int\n\n        def get_data(key: str, value: int) -> MyModel:\n            return MyModel(key=key, value=value)\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://foo/123\",\n            {\"key\": \"foo\", \"value\": 123},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        # read() returns raw BaseModel\n        result = await resource.read()\n        assert isinstance(result, MyModel)\n        data = result.model_dump()\n        assert data == {\"key\": \"foo\", \"value\": 123}\n\n    async def test_custom_type_conversion(self):\n        \"\"\"Test handling of custom types.\"\"\"\n\n        class CustomData:\n            def __init__(self, value: str):\n                self.value = value\n\n            def __str__(self) -> str:\n                return self.value\n\n        def get_data(value: str) -> CustomData:\n            return CustomData(value)\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"test://{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://hello\",\n            {\"value\": \"hello\"},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        # read() returns raw CustomData object\n        result = await resource.read()\n        assert isinstance(result, CustomData)\n        assert str(result) == \"hello\"\n\n    async def test_wildcard_param_can_create_resource(self):\n        \"\"\"Test that wildcard parameters are valid.\"\"\"\n\n        def identity(path: str) -> str:\n            return path\n\n        template = ResourceTemplate.from_function(\n            fn=identity,\n            uri_template=\"test://{path*}.py\",\n            name=\"test\",\n        )\n\n        assert await template.create_resource(\n            \"test://path/to/test.py\",\n            {\"path\": \"path/to/test.py\"},\n        )\n\n    async def test_wildcard_param_matches(self):\n        def identify(path: str) -> str:\n            return path\n\n        template = ResourceTemplate.from_function(\n            fn=identify,\n            uri_template=\"test://src/{path*}.py\",\n            name=\"test\",\n        )\n        # Valid match\n        params = template.matches(\"test://src/path/to/test.py\")\n        assert params == {\"path\": \"path/to/test\"}\n\n    async def test_multiple_wildcard_params(self):\n        \"\"\"Test that multiple wildcard parameters are valid.\"\"\"\n\n        def identity(path: str, path2: str) -> str:\n            return f\"{path}/{path2}\"\n\n        template = ResourceTemplate.from_function(\n            fn=identity,\n            uri_template=\"test://{path*}/xyz/{path2*}\",\n            name=\"test\",\n        )\n\n        params = template.matches(\"test://path/to/xyz/abc\")\n        assert params == {\"path\": \"path/to\", \"path2\": \"abc\"}\n\n    async def test_wildcard_param_with_regular_param(self):\n        \"\"\"Test that a wildcard parameter can be used with a regular parameter.\"\"\"\n\n        def identity(prefix: str, path: str) -> str:\n            return f\"{prefix}/{path}\"\n\n        template = ResourceTemplate.from_function(\n            fn=identity,\n            uri_template=\"test://{prefix}/{path*}\",\n            name=\"test\",\n        )\n\n        params = template.matches(\"test://src/path/to/test.py\")\n        assert params == {\"prefix\": \"src\", \"path\": \"path/to/test.py\"}\n\n    async def test_function_with_varargs_not_allowed(self):\n        def func(x: int, *args: int) -> int:\n            return x + sum(args)\n\n        with pytest.raises(\n            ValueError,\n            match=r\"Functions with \\*args are not supported as resource templates\",\n        ):\n            ResourceTemplate.from_function(\n                fn=func,\n                uri_template=\"test://{x}/{args*}\",\n                name=\"test\",\n            )\n\n    async def test_function_with_varkwargs_ok(self):\n        def func(x: int, **kwargs: int) -> int:\n            return x + sum(kwargs.values())\n\n        template = ResourceTemplate.from_function(\n            fn=func,\n            uri_template=\"test://{x}/{y}/{z}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{x}/{y}/{z}\"\n\n    async def test_callable_object_as_template(self):\n        \"\"\"Test that a callable object can be used as a template.\"\"\"\n\n        class MyTemplate:\n            \"\"\"This is my template\"\"\"\n\n            def __call__(self, x: str) -> str:\n                \"\"\"ignore this\"\"\"\n                return f\"X was {x}\"\n\n        template = ResourceTemplate.from_function(\n            fn=MyTemplate(),\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://foo\",\n            {\"x\": \"foo\"},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        # read() returns raw string from __call__\n        result = await resource.read()\n        assert result == \"X was foo\"\n\n\nclass TestMatchUriTemplate:\n    \"\"\"Test match_uri_template function.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"test://a/b\", None),\n            (\"test://a/b/c\", None),\n            (\"test://a/x/b\", {\"x\": \"x\"}),\n            (\"test://a/x/y/b\", None),\n            (\"test://a/1-2/b\", {\"x\": \"1-2\"}),\n        ],\n    )\n    def test_match_uri_template_single_param(\n        self, uri: str, expected_params: dict[str, str]\n    ):\n        \"\"\"Test that match_uri_template uses the slash delimiter.\"\"\"\n        uri_template = \"test://a/{x}/b\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"test://foo/123\", {\"x\": \"foo\", \"y\": \"123\"}),\n            (\"test://bar/456\", {\"x\": \"bar\", \"y\": \"456\"}),\n            (\"test://foo/bar\", {\"x\": \"foo\", \"y\": \"bar\"}),\n            (\"test://foo/bar/baz\", None),\n            (\"test://foo/email@domain.com\", {\"x\": \"foo\", \"y\": \"email@domain.com\"}),\n            (\"test://two words/foo\", {\"x\": \"two words\", \"y\": \"foo\"}),\n            (\"test://two.words/foo+bar\", {\"x\": \"two.words\", \"y\": \"foo+bar\"}),\n            (\n                f\"test://escaped{quote('/', safe='')}word/bar\",\n                {\"x\": \"escaped/word\", \"y\": \"bar\"},\n            ),\n            (\n                f\"test://escaped{quote('{', safe='')}x{quote('}', safe='')}word/bar\",\n                {\"x\": \"escaped{x}word\", \"y\": \"bar\"},\n            ),\n            (\"prefix+test://foo/123\", None),\n            (\"test://foo\", None),\n            (\"other://foo/123\", None),\n            (\"t.est://foo/bar\", None),\n        ],\n    )\n    def test_match_uri_template_simple_params(\n        self, uri: str, expected_params: dict[str, str] | None\n    ):\n        \"\"\"Test matching URIs against a template with simple parameters.\"\"\"\n        uri_template = \"test://{x}/{y}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"test://a/b/foo/c/d/123\", {\"x\": \"foo\", \"y\": \"123\"}),\n            (\"test://a/b/bar/c/d/456\", {\"x\": \"bar\", \"y\": \"456\"}),\n            (\"prefix+test://a/b/foo/c/d/123\", None),\n            (\"test://a/b/foo\", None),\n            (\"other://a/b/foo/c/d/123\", None),\n        ],\n    )\n    def test_match_uri_template_params_and_literal_segments(\n        self, uri: str, expected_params: dict[str, str] | None\n    ):\n        \"\"\"Test matching URIs against a template with parameters and literal segments.\"\"\"\n        uri_template = \"test://a/b/{x}/c/d/{y}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"prefix+test://foo/test/123\", {\"x\": \"foo\", \"y\": \"123\"}),\n            (\"prefix+test://bar/test/456\", {\"x\": \"bar\", \"y\": \"456\"}),\n            (\"test://foo/test/123\", None),\n            (\"other.prefix+test://foo/test/123\", None),\n            (\"other+prefix+test://foo/test/123\", None),\n        ],\n    )\n    def test_match_uri_template_with_prefix(\n        self, uri: str, expected_params: dict[str, str] | None\n    ):\n        \"\"\"Test matching URIs against a template with a prefix.\"\"\"\n        uri_template = \"prefix+test://{x}/test/{y}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    def test_match_uri_template_quoted_params(self):\n        uri_template = \"user://{name}/{email}\"\n        quoted_name = quote(\"John Doe\", safe=\"\")\n        quoted_email = quote(\"john@example.com\", safe=\"\")\n        uri = f\"user://{quoted_name}/{quoted_email}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == {\"name\": \"John Doe\", \"email\": \"john@example.com\"}\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"test://a/b\", None),\n            (\"test://a/b/c\", None),\n            (\"test://a/x/b\", {\"x\": \"x\"}),\n            (\"test://a/x/y/b\", {\"x\": \"x/y\"}),\n            (\"bad-prefix://a/x/y/b\", None),\n            (\"test://a/x/y/z\", None),\n        ],\n    )\n    def test_match_uri_template_wildcard_param(\n        self, uri: str, expected_params: dict[str, str]\n    ):\n        \"\"\"Test that match_uri_template uses the slash delimiter.\"\"\"\n        uri_template = \"test://a/{x*}/b\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"test://a/x/y/b/c/d\", {\"x\": \"x/y\", \"y\": \"c/d\"}),\n            (\"bad-prefix://a/x/y/b/c/d\", None),\n            (\"test://a/x/y/c/d\", None),\n            (\"test://a/x/b/y\", {\"x\": \"x\", \"y\": \"y\"}),\n        ],\n    )\n    def test_match_uri_template_multiple_wildcard_params(\n        self, uri: str, expected_params: dict[str, str]\n    ):\n        \"\"\"Test that match_uri_template uses the slash delimiter.\"\"\"\n        uri_template = \"test://a/{x*}/b/{y*}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    def test_match_uri_template_wildcard_and_literal_param(self):\n        \"\"\"Test that match_uri_template uses the slash delimiter.\"\"\"\n        uri = \"test://a/x/y/b\"\n        uri_template = \"test://a/{x*}/{y}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == {\"x\": \"x/y\", \"y\": \"b\"}\n\n    def test_match_consecutive_params(self):\n        \"\"\"Test that consecutive parameters without a / are not matched.\"\"\"\n        uri = \"test://a/x/y\"\n        uri_template = \"test://a/{x}{y}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result is None\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"file://abc/xyz.py\", {\"path\": \"xyz\"}),\n            (\"file://abc/x/y/z.py\", {\"path\": \"x/y/z\"}),\n            (\"file://abc/x/y/z/.py\", {\"path\": \"x/y/z/\"}),\n            (\"file://abc/x/y/z.md\", None),\n            (\"file://x/y/z.txt\", None),\n        ],\n    )\n    def test_match_uri_template_with_non_slash_suffix(\n        self, uri: str, expected_params: dict[str, str]\n    ):\n        uri_template = \"file://abc/{path*}.py\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"resource://test_foo\", {\"x\": \"foo\"}),\n            (\"resource://test_bar\", {\"x\": \"bar\"}),\n            (\"resource://test_hello\", {\"x\": \"hello\"}),\n            (\"resource://test_with_underscores\", {\"x\": \"with_underscores\"}),\n            (\"resource://test_\", None),  # Empty parameter not matched\n            (\"resource://test\", None),  # Missing parameter delimiter\n            (\"resource://other_foo\", None),  # Wrong prefix\n            (\"other://test_foo\", None),  # Wrong scheme\n        ],\n    )\n    def test_match_uri_template_embedded_param(\n        self, uri: str, expected_params: dict[str, str] | None\n    ):\n        \"\"\"Test matching URIs where parameter is embedded within a word segment.\"\"\"\n        uri_template = \"resource://test_{x}\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n    @pytest.mark.parametrize(\n        \"uri, expected_params\",\n        [\n            (\"resource://prefix_foo_suffix\", {\"x\": \"foo\"}),\n            (\"resource://prefix_bar_suffix\", {\"x\": \"bar\"}),\n            (\"resource://prefix_hello_world_suffix\", {\"x\": \"hello_world\"}),\n            (\"resource://prefix__suffix\", None),  # Empty parameter not matched\n            (\"resource://prefix_suffix\", None),  # Missing parameter delimiter\n            (\"resource://other_foo_suffix\", None),  # Wrong prefix\n            (\"resource://prefix_foo_other\", None),  # Wrong suffix\n        ],\n    )\n    def test_match_uri_template_embedded_param_with_prefix_and_suffix(\n        self, uri: str, expected_params: dict[str, str] | None\n    ):\n        \"\"\"Test matching URIs where parameter has both prefix and suffix.\"\"\"\n        uri_template = \"resource://prefix_{x}_suffix\"\n        result = match_uri_template(uri=uri, uri_template=uri_template)\n        assert result == expected_params\n\n\nclass TestContextHandling:\n    \"\"\"Test context handling in resource templates.\"\"\"\n\n    def test_context_parameter_detection(self):\n        \"\"\"Test that context parameters are properly detected in\n        ResourceTemplate.from_function().\"\"\"\n\n        def template_with_context(x: int, ctx: Context) -> str:\n            return str(x)\n\n        ResourceTemplate.from_function(\n            fn=template_with_context,\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n        def template_without_context(x: int) -> str:\n            return str(x)\n\n        ResourceTemplate.from_function(\n            fn=template_without_context,\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n    def test_parameterized_context_parameter_detection(self):\n        \"\"\"Test that parameterized context parameters are properly detected in\n        ResourceTemplate.from_function().\"\"\"\n\n        def template_with_context(x: int, ctx: Context) -> str:\n            return str(x)\n\n        ResourceTemplate.from_function(\n            fn=template_with_context,\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n    def test_parameterized_union_context_parameter_detection(self):\n        \"\"\"Test that context parameters in a union are properly detected in\n        ResourceTemplate.from_function().\"\"\"\n\n        def template_with_context(x: int, ctx: Context | None) -> str:\n            return str(x)\n\n        ResourceTemplate.from_function(\n            fn=template_with_context,\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n    async def test_context_injection(self):\n        \"\"\"Test that context is properly injected during resource creation.\"\"\"\n\n        def resource_with_context(x: int, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return str(x)\n\n        template = ResourceTemplate.from_function(\n            fn=resource_with_context,\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n        context = Context(fastmcp=mcp)\n\n        async with context:\n            resource = await template.create_resource(\n                \"test://42\",\n                {\"x\": 42},\n            )\n\n            assert isinstance(resource, FunctionResource)\n            # read() returns the raw value\n            result = await resource.read()\n            assert result == \"42\"\n\n    async def test_context_optional(self):\n        \"\"\"Test that context is optional when creating resources.\"\"\"\n\n        def resource_with_context(x: int, ctx: Context | None = None) -> str:\n            return str(x)\n\n        template = ResourceTemplate.from_function(\n            fn=resource_with_context,\n            uri_template=\"test://{x}\",\n            name=\"test\",\n        )\n\n        # Even for optional context, we need to provide a context\n        mcp = FastMCP()\n        context = Context(fastmcp=mcp)\n\n        async with context:\n            resource = await template.create_resource(\n                \"test://42\",\n                {\"x\": 42},\n            )\n\n            assert isinstance(resource, FunctionResource)\n            # read() returns the raw value\n            result = await resource.read()\n            assert result == \"42\"\n\n    async def test_context_with_functools_wraps_decorator(self):\n        \"\"\"Regression test for #2524: decorated templates with Context should work.\"\"\"\n\n        def custom_decorator(func):\n            @functools.wraps(func)\n            async def wrapper(*args, **kwargs):\n                return await func(*args, **kwargs)\n\n            return wrapper\n\n        @custom_decorator\n        async def decorated_template(ctx: Context, item_id: int) -> str:\n            assert isinstance(ctx, Context)\n            return f\"item: {item_id}\"\n\n        template = ResourceTemplate.from_function(\n            fn=decorated_template,\n            uri_template=\"test://{item_id}\",\n            name=\"test\",\n        )\n\n        mcp = FastMCP()\n        context = Context(fastmcp=mcp)\n\n        async with context:\n            resource = await template.create_resource(\"test://42\", {\"item_id\": 42})\n            # read() returns the raw value\n            result = await resource.read()\n            assert result == \"item: 42\"\n\n\nclass TestMalformedURITemplates:\n    \"\"\"Test that malformed URI templates from remote servers don't crash.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"template\",\n        [\n            \"test://{bad-name}/path\",\n            \"test://{hyphen-param}/{other-param}/path\",\n            \"test://{1leading}/path\",\n            \"test://{123}/path\",\n        ],\n        ids=[\n            \"hyphen_in_name\",\n            \"multiple_hyphens\",\n            \"leading_digit\",\n            \"all_digits\",\n        ],\n    )\n    def test_build_regex_returns_none_for_invalid_group_names(self, template: str):\n        assert build_regex(template) is None\n\n    def test_build_regex_returns_none_for_duplicate_group_names(self):\n        assert build_regex(\"test://{a}/{a}/path\") is None\n\n    @pytest.mark.parametrize(\n        \"template\",\n        [\n            \"test://{bad-name}/path\",\n            \"test://{a}/{a}/path\",\n            \"test://{1leading}/path\",\n        ],\n        ids=[\n            \"hyphen_in_name\",\n            \"duplicate_groups\",\n            \"leading_digit\",\n        ],\n    )\n    def test_match_uri_template_returns_none_for_malformed_templates(\n        self, template: str\n    ):\n        assert match_uri_template(\"test://anything/path\", template) is None\n\n    def test_resource_template_matches_returns_none_for_malformed_template(self):\n        template = ResourceTemplate(\n            uri_template=\"test://{bad-name}/path\",\n            name=\"test\",\n            parameters={},\n        )\n        assert template.matches(\"test://anything/path\") is None\n\n    def test_build_regex_still_works_for_valid_templates(self):\n        regex = build_regex(\"test://{name}/{id}\")\n        assert regex is not None\n        match = regex.match(\"test://foo/123\")\n        assert match is not None\n        assert match.group(\"name\") == \"foo\"\n        assert match.group(\"id\") == \"123\"\n"
  },
  {
    "path": "tests/resources/test_resource_template_meta.py",
    "content": "from fastmcp.resources import ResourceTemplate\n\n\nclass TestResourceTemplateMeta:\n    \"\"\"Test ResourceTemplate meta functionality.\"\"\"\n\n    def test_template_meta_parameter(self):\n        \"\"\"Test that meta parameter is properly handled.\"\"\"\n\n        def template_func(param: str) -> str:\n            return f\"Result: {param}\"\n\n        meta_data = {\"version\": \"2.0\", \"template\": \"test\"}\n        template = ResourceTemplate.from_function(\n            fn=template_func,\n            uri_template=\"test://{param}\",\n            name=\"test_template\",\n            meta=meta_data,\n        )\n\n        assert template.meta == meta_data\n        mcp_template = template.to_mcp_template()\n        # MCP template includes fastmcp meta, so check that our meta is included\n        assert mcp_template.meta is not None\n        assert meta_data.items() <= mcp_template.meta.items()\n"
  },
  {
    "path": "tests/resources/test_resource_template_query_params.py",
    "content": "import pytest\n\nfrom fastmcp.resources import ResourceTemplate\n\n\nclass TestQueryParameterExtraction:\n    \"\"\"Test basic query parameter extraction from URIs.\"\"\"\n\n    async def test_single_query_param(self):\n        \"\"\"Test resource template with single query parameter.\"\"\"\n\n        def get_data(id: str, format: str = \"json\") -> str:\n            return f\"Data {id} in {format}\"\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"data://{id}{?format}\",\n            name=\"test\",\n        )\n\n        # Match without query param (uses default)\n        params = template.matches(\"data://123\")\n        assert params == {\"id\": \"123\"}\n\n        # Match with query param\n        params = template.matches(\"data://123?format=xml\")\n        assert params == {\"id\": \"123\", \"format\": \"xml\"}\n\n    async def test_multiple_query_params(self):\n        \"\"\"Test resource template with multiple query parameters.\"\"\"\n\n        def get_items(category: str, page: int = 1, limit: int = 10) -> str:\n            return f\"Category {category}, page {page}, limit {limit}\"\n\n        template = ResourceTemplate.from_function(\n            fn=get_items,\n            uri_template=\"items://{category}{?page,limit}\",\n            name=\"test\",\n        )\n\n        # No query params\n        params = template.matches(\"items://books\")\n        assert params == {\"category\": \"books\"}\n\n        # One query param\n        params = template.matches(\"items://books?page=2\")\n        assert params == {\"category\": \"books\", \"page\": \"2\"}\n\n        # Both query params\n        params = template.matches(\"items://books?page=2&limit=20\")\n        assert params == {\"category\": \"books\", \"page\": \"2\", \"limit\": \"20\"}\n\n\nclass TestQueryParameterTypeCoercion:\n    \"\"\"Test type coercion for query parameters.\"\"\"\n\n    async def test_int_coercion(self):\n        \"\"\"Test integer type coercion for query parameters.\"\"\"\n\n        def get_page(resource: str, page: int = 1) -> dict:\n            return {\"resource\": resource, \"page\": page, \"type\": type(page).__name__}\n\n        template = ResourceTemplate.from_function(\n            fn=get_page,\n            uri_template=\"resource://{resource}{?page}\",\n            name=\"test\",\n        )\n\n        # Create resource with string query param\n        resource = await template.create_resource(\n            \"resource://docs?page=5\",\n            {\"resource\": \"docs\", \"page\": \"5\"},\n        )\n\n        # read() returns raw dict\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"page\"] == 5\n        assert result[\"type\"] == \"int\"\n\n    async def test_bool_coercion(self):\n        \"\"\"Test boolean type coercion for query parameters.\"\"\"\n\n        def get_config(name: str, enabled: bool = False) -> dict:\n            return {\"name\": name, \"enabled\": enabled, \"type\": type(enabled).__name__}\n\n        template = ResourceTemplate.from_function(\n            fn=get_config,\n            uri_template=\"config://{name}{?enabled}\",\n            name=\"test\",\n        )\n\n        # Test true value\n        resource = await template.create_resource(\n            \"config://feature?enabled=true\",\n            {\"name\": \"feature\", \"enabled\": \"true\"},\n        )\n        # read() returns raw dict\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"enabled\"] is True\n\n        # Test false value\n        resource = await template.create_resource(\n            \"config://feature?enabled=false\",\n            {\"name\": \"feature\", \"enabled\": \"false\"},\n        )\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"enabled\"] is False\n\n    async def test_float_coercion(self):\n        \"\"\"Test float type coercion for query parameters.\"\"\"\n\n        def get_metrics(service: str, threshold: float = 0.5) -> dict:\n            return {\n                \"service\": service,\n                \"threshold\": threshold,\n                \"type\": type(threshold).__name__,\n            }\n\n        template = ResourceTemplate.from_function(\n            fn=get_metrics,\n            uri_template=\"metrics://{service}{?threshold}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"metrics://api?threshold=0.95\",\n            {\"service\": \"api\", \"threshold\": \"0.95\"},\n        )\n\n        # read() returns raw dict\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"threshold\"] == 0.95\n        assert result[\"type\"] == \"float\"\n\n\nclass TestQueryParameterValidation:\n    \"\"\"Test validation rules for query parameters.\"\"\"\n\n    def test_query_params_must_be_optional(self):\n        \"\"\"Test that query parameters must have default values.\"\"\"\n\n        def invalid_func(id: str, format: str) -> str:\n            return f\"Data {id} in {format}\"\n\n        with pytest.raises(\n            ValueError,\n            match=\"Query parameters .* must be optional function parameters with default values\",\n        ):\n            ResourceTemplate.from_function(\n                fn=invalid_func,\n                uri_template=\"data://{id}{?format}\",\n                name=\"test\",\n            )\n\n    def test_required_params_in_path(self):\n        \"\"\"Test that required parameters must be in path.\"\"\"\n\n        def valid_func(id: str, format: str = \"json\") -> str:\n            return f\"Data {id} in {format}\"\n\n        # This should work - required param in path, optional in query\n        template = ResourceTemplate.from_function(\n            fn=valid_func,\n            uri_template=\"data://{id}{?format}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"data://{id}{?format}\"\n\n\nclass TestQueryParameterWithDefaults:\n    \"\"\"Test that missing query parameters use default values.\"\"\"\n\n    async def test_missing_query_param_uses_default(self):\n        \"\"\"Test that missing query parameters fall back to defaults.\"\"\"\n\n        def get_data(id: str, format: str = \"json\", verbose: bool = False) -> dict:\n            return {\"id\": id, \"format\": format, \"verbose\": verbose}\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"data://{id}{?format,verbose}\",\n            name=\"test\",\n        )\n\n        # No query params - should use defaults\n        resource = await template.create_resource(\n            \"data://123\",\n            {\"id\": \"123\"},\n        )\n\n        # read() returns raw dict\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"format\"] == \"json\"\n        assert result[\"verbose\"] is False\n\n    async def test_partial_query_params(self):\n        \"\"\"Test providing only some query parameters.\"\"\"\n\n        def get_data(\n            id: str, format: str = \"json\", limit: int = 10, offset: int = 0\n        ) -> dict:\n            return {\"id\": id, \"format\": format, \"limit\": limit, \"offset\": offset}\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"data://{id}{?format,limit,offset}\",\n            name=\"test\",\n        )\n\n        # Provide only some query params\n        resource = await template.create_resource(\n            \"data://123?limit=20\",\n            {\"id\": \"123\", \"limit\": \"20\"},\n        )\n\n        # read() returns raw dict\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"format\"] == \"json\"  # default\n        assert result[\"limit\"] == 20  # provided\n        assert result[\"offset\"] == 0  # default\n\n\nclass TestQueryParameterWithWildcards:\n    \"\"\"Test query parameters combined with wildcard path parameters.\"\"\"\n\n    async def test_wildcard_with_query_params(self):\n        \"\"\"Test combining wildcard path params with query params.\"\"\"\n\n        def get_file(path: str, encoding: str = \"utf-8\", lines: int = 100) -> dict:\n            return {\"path\": path, \"encoding\": encoding, \"lines\": lines}\n\n        template = ResourceTemplate.from_function(\n            fn=get_file,\n            uri_template=\"files://{path*}{?encoding,lines}\",\n            name=\"test\",\n        )\n\n        # Match path with query params\n        params = template.matches(\"files://src/test/data.txt?encoding=ascii&lines=50\")\n        assert params == {\n            \"path\": \"src/test/data.txt\",\n            \"encoding\": \"ascii\",\n            \"lines\": \"50\",\n        }\n\n        # Create resource\n        resource = await template.create_resource(\n            \"files://src/test/data.txt?lines=50\",\n            {\"path\": \"src/test/data.txt\", \"lines\": \"50\"},\n        )\n\n        # read() returns raw dict\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"path\"] == \"src/test/data.txt\"\n        assert result[\"encoding\"] == \"utf-8\"  # default\n        assert result[\"lines\"] == 50  # provided\n\n\nclass TestBooleanQueryParameterValidation:\n    \"\"\"Test that invalid boolean query parameter values raise errors.\"\"\"\n\n    async def _make_template(self):\n        def get_config(name: str, enabled: bool = False) -> dict:\n            return {\"name\": name, \"enabled\": enabled}\n\n        return ResourceTemplate.from_function(\n            fn=get_config,\n            uri_template=\"config://{name}{?enabled}\",\n            name=\"test\",\n        )\n\n    async def test_invalid_boolean_value_raises_error(self):\n        \"\"\"Test that nonsense boolean values like 'banana' raise ValueError.\"\"\"\n        template = await self._make_template()\n\n        with pytest.raises(ValueError, match=\"Invalid boolean value for enabled\"):\n            resource = await template.create_resource(\n                \"config://feature?enabled=banana\",\n                {\"name\": \"feature\", \"enabled\": \"banana\"},\n            )\n            await resource.read()\n\n    @pytest.mark.parametrize(\n        \"value\", [\"true\", \"True\", \"TRUE\", \"1\", \"yes\", \"Yes\", \"YES\"]\n    )\n    async def test_valid_true_values(self, value: str):\n        \"\"\"Test that all accepted truthy string values coerce to True.\"\"\"\n        template = await self._make_template()\n\n        resource = await template.create_resource(\n            f\"config://feature?enabled={value}\",\n            {\"name\": \"feature\", \"enabled\": value},\n        )\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"enabled\"] is True\n\n    @pytest.mark.parametrize(\n        \"value\", [\"false\", \"False\", \"FALSE\", \"0\", \"no\", \"No\", \"NO\"]\n    )\n    async def test_valid_false_values(self, value: str):\n        \"\"\"Test that all accepted falsy string values coerce to False.\"\"\"\n        template = await self._make_template()\n\n        resource = await template.create_resource(\n            f\"config://feature?enabled={value}\",\n            {\"name\": \"feature\", \"enabled\": value},\n        )\n        result = await resource.read()\n        assert isinstance(result, dict)\n        assert result[\"enabled\"] is False\n\n    @pytest.mark.parametrize(\"value\", [\"banana\", \"nope\", \"2\", \"truee\", \"\"])\n    async def test_various_invalid_boolean_values(self, value: str):\n        \"\"\"Test that various invalid boolean strings raise ValueError.\"\"\"\n        template = await self._make_template()\n\n        with pytest.raises(ValueError, match=\"Invalid boolean value for enabled\"):\n            resource = await template.create_resource(\n                f\"config://feature?enabled={value}\",\n                {\"name\": \"feature\", \"enabled\": value},\n            )\n            await resource.read()\n\n\nclass TestResourceTemplateFieldDefaults:\n    \"\"\"Test resource templates with Field() defaults.\"\"\"\n\n    async def test_field_with_default(self):\n        \"\"\"Test that Field(default=...) correctly provides default values in resource templates.\"\"\"\n        from pydantic import Field\n\n        def get_data(\n            id: str = Field(description=\"Resource ID\"),\n            format: str = Field(default=\"json\", description=\"Output format\"),\n        ) -> str:\n            return f\"id={id}, format={format}\"\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"data://{id}{?format}\",\n            name=\"test\",\n        )\n\n        # Test with only required parameter\n        resource = await template.create_resource(\"data://123\", {\"id\": \"123\"})\n        result = await resource.read()\n        assert result == \"id=123, format=json\"\n\n        # Test with override\n        resource = await template.create_resource(\n            \"data://123?format=xml\", {\"id\": \"123\", \"format\": \"xml\"}\n        )\n        result = await resource.read()\n        assert result == \"id=123, format=xml\"\n\n    async def test_multiple_field_defaults(self):\n        \"\"\"Test multiple query parameters with Field() defaults.\"\"\"\n        from typing import Any\n\n        from pydantic import Field\n\n        def fetch_data(\n            resource_id: str = Field(description=\"Resource ID\"),\n            limit: int = Field(default=10, description=\"Result limit\"),\n            offset: int = Field(default=0, description=\"Result offset\"),\n            format: str = Field(default=\"json\", description=\"Output format\"),\n        ) -> dict[str, Any]:\n            return {\n                \"resource_id\": resource_id,\n                \"limit\": limit,\n                \"offset\": offset,\n                \"format\": format,\n            }\n\n        template = ResourceTemplate.from_function(\n            fn=fetch_data,\n            uri_template=\"api://{resource_id}{?limit,offset,format}\",\n            name=\"test\",\n        )\n\n        # Test with only required parameter - all defaults should apply\n        resource1 = await template.create_resource(\n            \"api://user123\", {\"resource_id\": \"user123\"}\n        )\n        result1 = await resource1.read()\n        assert isinstance(result1, dict)\n        assert result1[\"resource_id\"] == \"user123\"\n        assert result1[\"limit\"] == 10\n        assert result1[\"offset\"] == 0\n        assert result1[\"format\"] == \"json\"\n\n        # Test with some overrides\n        resource2 = await template.create_resource(\n            \"api://user123?limit=50&format=xml\",\n            {\"resource_id\": \"user123\", \"limit\": \"50\", \"format\": \"xml\"},\n        )\n        result2 = await resource2.read()\n        assert isinstance(result2, dict)\n        assert result2[\"resource_id\"] == \"user123\"\n        assert result2[\"limit\"] == 50  # overridden\n        assert result2[\"offset\"] == 0  # default\n        assert result2[\"format\"] == \"xml\"  # overridden\n"
  },
  {
    "path": "tests/resources/test_resources.py",
    "content": "import mcp.types\nimport pytest\nfrom pydantic import AnyUrl, BaseModel\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.resources import Resource, ResourceContent, ResourceResult\nfrom fastmcp.resources.function_resource import FunctionResource\n\n\nclass TestResourceValidation:\n    \"\"\"Test base Resource validation.\"\"\"\n\n    def test_resource_uri_validation(self):\n        \"\"\"Test URI validation.\"\"\"\n\n        def dummy_func() -> str:\n            return \"data\"\n\n        # Valid URI\n        resource = FunctionResource(\n            uri=AnyUrl(\"http://example.com/data\"),\n            name=\"test\",\n            fn=dummy_func,\n        )\n        assert str(resource.uri) == \"http://example.com/data\"\n\n        # Missing protocol\n        with pytest.raises(ValueError, match=\"Input should be a valid URL\"):\n            FunctionResource(\n                uri=AnyUrl(\"invalid\"),\n                name=\"test\",\n                fn=dummy_func,\n            )\n\n        # Missing host\n        with pytest.raises(ValueError, match=\"Input should be a valid URL\"):\n            FunctionResource(\n                uri=AnyUrl(\"http://\"),\n                name=\"test\",\n                fn=dummy_func,\n            )\n\n    def test_resource_name_from_uri(self):\n        \"\"\"Test name is extracted from URI if not provided.\"\"\"\n\n        def dummy_func() -> str:\n            return \"data\"\n\n        resource = FunctionResource(\n            uri=AnyUrl(\"resource://my-resource\"),\n            fn=dummy_func,\n        )\n        assert resource.name == \"resource://my-resource\"\n\n    def test_provided_name_takes_precedence_over_uri(self):\n        \"\"\"Test that provided name takes precedence over URI.\"\"\"\n\n        def dummy_func() -> str:\n            return \"data\"\n\n        # Explicit name takes precedence over URI\n        resource = FunctionResource(\n            uri=AnyUrl(\"resource://uri-name\"),\n            name=\"explicit-name\",\n            fn=dummy_func,\n        )\n        assert resource.name == \"explicit-name\"\n\n    def test_resource_mime_type(self):\n        \"\"\"Test mime type handling.\"\"\"\n\n        def dummy_func() -> str:\n            return \"data\"\n\n        # Default mime type\n        resource = FunctionResource(\n            uri=AnyUrl(\"resource://test\"),\n            fn=dummy_func,\n        )\n        assert resource.mime_type == \"text/plain\"\n\n        # Custom mime type\n        resource = FunctionResource(\n            uri=AnyUrl(\"resource://test\"),\n            fn=dummy_func,\n            mime_type=\"application/json\",\n        )\n        assert resource.mime_type == \"application/json\"\n\n    async def test_resource_read_not_implemented(self):\n        \"\"\"Test that Resource.read() raises NotImplementedError.\"\"\"\n\n        class ConcreteResource(Resource):\n            pass\n\n        resource = ConcreteResource(uri=AnyUrl(\"test://test\"), name=\"test\")\n        with pytest.raises(NotImplementedError, match=\"Subclasses must implement read\"):\n            await resource.read()\n\n    def test_resource_meta_parameter(self):\n        \"\"\"Test that meta parameter is properly handled.\"\"\"\n\n        def resource_func() -> str:\n            return \"test content\"\n\n        meta_data = {\"version\": \"1.0\", \"category\": \"test\"}\n        resource = Resource.from_function(\n            fn=resource_func,\n            uri=\"resource://test\",\n            name=\"test_resource\",\n            meta=meta_data,\n        )\n\n        assert resource.meta == meta_data\n        mcp_resource = resource.to_mcp_resource()\n        # MCP resource includes fastmcp meta, so check that our meta is included\n        assert mcp_resource.meta is not None\n        assert meta_data.items() <= mcp_resource.meta.items()\n\n\nclass TestResourceContent:\n    \"\"\"Test ResourceContent creation and conversion.\"\"\"\n\n    def test_string_content(self):\n        \"\"\"String input creates text content with text/plain mime type.\"\"\"\n        content = ResourceContent(\"hello world\")\n        assert content.content == \"hello world\"\n        assert content.mime_type == \"text/plain\"\n        assert content.meta is None\n\n    def test_bytes_content(self):\n        \"\"\"Bytes input creates binary content with octet-stream mime type.\"\"\"\n        content = ResourceContent(b\"\\x00\\x01\\x02\")\n        assert content.content == b\"\\x00\\x01\\x02\"\n        assert content.mime_type == \"application/octet-stream\"\n        assert content.meta is None\n\n    def test_dict_serialized_to_json(self):\n        \"\"\"Dict input is JSON-serialized with application/json mime type.\"\"\"\n        content = ResourceContent({\"key\": \"value\", \"count\": 42})\n        assert content.content == '{\"key\":\"value\",\"count\":42}'\n        assert content.mime_type == \"application/json\"\n\n    def test_list_serialized_to_json(self):\n        \"\"\"List input is JSON-serialized.\"\"\"\n        content = ResourceContent([1, 2, 3])\n        assert content.content == \"[1,2,3]\"\n        assert content.mime_type == \"application/json\"\n\n    def test_pydantic_model_serialized_to_json(self):\n        \"\"\"Pydantic model is JSON-serialized.\"\"\"\n\n        class Item(BaseModel):\n            name: str\n            price: float\n\n        content = ResourceContent(Item(name=\"Widget\", price=9.99))\n        assert content.content == '{\"name\":\"Widget\",\"price\":9.99}'\n        assert content.mime_type == \"application/json\"\n\n    def test_custom_mime_type(self):\n        \"\"\"Custom mime type overrides default.\"\"\"\n        content = ResourceContent(\"test\", mime_type=\"text/html\")\n        assert content.mime_type == \"text/html\"\n\n    def test_with_meta(self):\n        \"\"\"Meta is passed through to content.\"\"\"\n        content = ResourceContent(\"test\", meta={\"version\": \"1.0\"})\n        assert content.meta == {\"version\": \"1.0\"}\n\n    def test_to_mcp_text_contents(self):\n        \"\"\"Text content converts to TextResourceContents.\"\"\"\n        content = ResourceContent(\n            content=\"hello\", mime_type=\"text/plain\", meta={\"k\": \"v\"}\n        )\n        mcp_content = content.to_mcp_resource_contents(\"resource://test\")\n        assert isinstance(mcp_content, mcp.types.TextResourceContents)\n        assert mcp_content.text == \"hello\"\n        assert mcp_content.mimeType == \"text/plain\"\n        assert str(mcp_content.uri) == \"resource://test\"\n        assert mcp_content.meta == {\"k\": \"v\"}\n\n    def test_to_mcp_blob_contents(self):\n        \"\"\"Binary content converts to BlobResourceContents with base64.\"\"\"\n        content = ResourceContent(\n            content=b\"\\x00\\x01\\x02\", mime_type=\"application/octet-stream\"\n        )\n        mcp_content = content.to_mcp_resource_contents(\"resource://binary\")\n        assert isinstance(mcp_content, mcp.types.BlobResourceContents)\n        assert mcp_content.blob == \"AAEC\"  # base64 of \\x00\\x01\\x02\n        assert mcp_content.mimeType == \"application/octet-stream\"\n\n\nclass TestResourceResult:\n    \"\"\"Test ResourceResult initialization and conversion.\"\"\"\n\n    def test_init_from_string(self):\n        \"\"\"String input is normalized to list[ResourceContent].\"\"\"\n        result = ResourceResult(\"hello world\")\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"hello world\"\n        assert result.contents[0].mime_type == \"text/plain\"\n\n    def test_init_from_bytes(self):\n        \"\"\"Bytes input is normalized to list[ResourceContent].\"\"\"\n        result = ResourceResult(b\"\\xff\\xfe\")\n        assert len(result.contents) == 1\n        assert result.contents[0].content == b\"\\xff\\xfe\"\n        assert result.contents[0].mime_type == \"application/octet-stream\"\n\n    def test_init_from_dict_raises_type_error(self):\n        \"\"\"Dict input raises TypeError - must use ResourceContent for serialization.\"\"\"\n        with pytest.raises(TypeError, match=\"must be str, bytes, or list\"):\n            ResourceResult({\"page\": 1, \"total\": 100})  # type: ignore[arg-type]\n\n    def test_init_from_single_resource_content_raises_type_error(self):\n        \"\"\"Single ResourceContent raises TypeError - must be in a list.\"\"\"\n        content = ResourceContent(content=\"test\", mime_type=\"text/html\")\n        with pytest.raises(TypeError, match=\"must be str, bytes, or list\"):\n            ResourceResult(content)  # type: ignore[arg-type]\n\n    def test_init_from_list_of_resource_content(self):\n        \"\"\"List of ResourceContent is used directly.\"\"\"\n        contents = [\n            ResourceContent(content=\"one\", mime_type=\"text/plain\"),\n            ResourceContent(content=\"two\", mime_type=\"text/plain\"),\n        ]\n        result = ResourceResult(contents)\n        assert len(result.contents) == 2\n        assert result.contents[0].content == \"one\"\n        assert result.contents[1].content == \"two\"\n\n    def test_init_from_mixed_list_raises_type_error(self):\n        \"\"\"Mixed list items raise TypeError - all items must be ResourceContent.\"\"\"\n        with pytest.raises(TypeError, match=r\"contents\\[0\\] must be ResourceContent\"):\n            ResourceResult([\"text\", b\"bytes\", {\"key\": \"value\"}])  # type: ignore[arg-type]\n\n    def test_init_preserves_meta(self):\n        \"\"\"Meta is preserved on ResourceResult.\"\"\"\n        result = ResourceResult(\"test\", meta={\"version\": \"2.0\"})\n        assert result.meta == {\"version\": \"2.0\"}\n\n    def test_to_mcp_result(self):\n        \"\"\"Converts to MCP ReadResourceResult with proper structure.\"\"\"\n        result = ResourceResult(\n            contents=[ResourceContent(content=\"hello\", mime_type=\"text/plain\")],\n            meta={\"source\": \"test\"},\n        )\n        mcp_result = result.to_mcp_result(\"resource://test\")\n        assert isinstance(mcp_result, mcp.types.ReadResourceResult)\n        assert len(mcp_result.contents) == 1\n        assert isinstance(mcp_result.contents[0], mcp.types.TextResourceContents)\n        assert mcp_result.contents[0].text == \"hello\"\n        assert str(mcp_result.contents[0].uri) == \"resource://test\"\n        assert mcp_result.meta == {\"source\": \"test\"}\n\n    def test_to_mcp_result_multiple_contents(self):\n        \"\"\"Multiple contents all get same URI.\"\"\"\n        result = ResourceResult(\n            [\n                ResourceContent(\"one\"),\n                ResourceContent(\"two\"),\n                ResourceContent(\"three\"),\n            ]\n        )\n        mcp_result = result.to_mcp_result(\"resource://multi\")\n        assert len(mcp_result.contents) == 3\n        for item in mcp_result.contents:\n            assert str(item.uri) == \"resource://multi\"\n\n\nclass TestResourceConvertResult:\n    \"\"\"Test Resource.convert_result() method.\"\"\"\n\n    def test_passthrough_resource_result(self):\n        \"\"\"ResourceResult input is returned unchanged.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        resource = FunctionResource(uri=AnyUrl(\"test://test\"), name=\"test\", fn=fn)\n        original = ResourceResult(\"test\", meta={\"original\": True})\n        result = resource.convert_result(original)\n        assert result is original\n\n    def test_converts_raw_value(self):\n        \"\"\"Raw values are converted to ResourceResult.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        resource = FunctionResource(uri=AnyUrl(\"test://test\"), name=\"test\", fn=fn)\n        result = resource.convert_result(\"hello\")\n        assert isinstance(result, ResourceResult)\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"hello\"\n\n    async def test_read_returns_resource_result(self):\n        \"\"\"_read() returns ResourceResult after conversion.\"\"\"\n\n        def fn() -> str:\n            return \"hello world\"\n\n        resource = FunctionResource(uri=AnyUrl(\"test://test\"), name=\"test\", fn=fn)\n        result = await resource._read()\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"hello world\"\n\n\nclass TestResourceMetaPropagation:\n    \"\"\"Test that meta is properly propagated through the full MCP flow.\"\"\"\n\n    async def test_resource_result_meta_received_by_client(self):\n        \"\"\"Meta set on ResourceResult is received by MCP client.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"test://with-meta\")\n        def resource_with_meta() -> ResourceResult:\n            return ResourceResult(\"hello\", meta={\"version\": \"2.0\", \"source\": \"test\"})\n\n        async with Client(mcp) as client:\n            result = await client.read_resource_mcp(\"test://with-meta\")\n            assert result.meta == {\"version\": \"2.0\", \"source\": \"test\"}\n\n    async def test_resource_content_meta_received_by_client(self):\n        \"\"\"Meta set on ResourceContent is received by MCP client.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"test://content-meta\")\n        def resource_with_content_meta() -> ResourceResult:\n            return ResourceResult(\n                [ResourceContent(content=\"data\", meta={\"item_version\": \"1.0\"})]\n            )\n\n        async with Client(mcp) as client:\n            result = await client.read_resource_mcp(\"test://content-meta\")\n            assert len(result.contents) == 1\n            assert result.contents[0].meta == {\"item_version\": \"1.0\"}\n\n    async def test_both_result_and_content_meta(self):\n        \"\"\"Both result-level and content-level meta are propagated.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"test://both-meta\")\n        def resource_both_meta() -> ResourceResult:\n            return ResourceResult(\n                contents=[\n                    ResourceContent(content=\"item\", meta={\"item_key\": \"item_val\"})\n                ],\n                meta={\"result_key\": \"result_val\"},\n            )\n\n        async with Client(mcp) as client:\n            result = await client.read_resource_mcp(\"test://both-meta\")\n            assert result.meta == {\"result_key\": \"result_val\"}\n            assert result.contents[0].meta == {\"item_key\": \"item_val\"}\n"
  },
  {
    "path": "tests/resources/test_standalone_decorator.py",
    "content": "\"\"\"Tests for the standalone @resource decorator.\n\nThe @resource decorator attaches metadata to functions without registering them\nto a server. Functions can be added explicitly via server.add_resource() /\nserver.add_template() or discovered by FileSystemProvider.\n\"\"\"\n\nfrom typing import cast\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.resources import resource\nfrom fastmcp.resources.function_resource import DecoratedResource, ResourceMeta\n\n\nclass TestResourceDecorator:\n    \"\"\"Tests for the @resource decorator.\"\"\"\n\n    def test_resource_requires_uri(self):\n        \"\"\"@resource should require a URI argument.\"\"\"\n        with pytest.raises(TypeError, match=\"requires a URI|was used incorrectly\"):\n\n            @resource  # type: ignore[arg-type]\n            def get_config() -> str:\n                return \"{}\"\n\n    def test_resource_with_uri(self):\n        \"\"\"@resource(\"uri\") should attach metadata.\"\"\"\n\n        @resource(\"config://app\")\n        def get_config() -> dict:\n            return {\"setting\": \"value\"}\n\n        decorated = cast(DecoratedResource, get_config)\n        assert callable(get_config)\n        assert hasattr(get_config, \"__fastmcp__\")\n        assert isinstance(decorated.__fastmcp__, ResourceMeta)\n        assert decorated.__fastmcp__.uri == \"config://app\"\n\n    def test_resource_with_template_uri(self):\n        \"\"\"@resource with template URI should attach metadata.\"\"\"\n\n        @resource(\"users://{user_id}/profile\")\n        def get_profile(user_id: str) -> dict:\n            return {\"id\": user_id}\n\n        decorated = cast(DecoratedResource, get_profile)\n        assert callable(get_profile)\n        assert hasattr(get_profile, \"__fastmcp__\")\n        assert decorated.__fastmcp__.uri == \"users://{user_id}/profile\"\n\n    def test_resource_with_function_params_becomes_template(self):\n        \"\"\"@resource with function params should attach metadata.\"\"\"\n\n        @resource(\"data://items/{category}\")\n        def get_items(category: str, limit: int = 10) -> list:\n            return list(range(limit))\n\n        decorated = cast(DecoratedResource, get_items)\n        assert callable(get_items)\n        assert hasattr(get_items, \"__fastmcp__\")\n        assert decorated.__fastmcp__.uri == \"data://items/{category}\"\n\n    def test_resource_with_all_metadata(self):\n        \"\"\"@resource with all metadata should store it all.\"\"\"\n\n        @resource(\n            \"config://app\",\n            name=\"app-config\",\n            title=\"Application Config\",\n            description=\"Gets app configuration\",\n            mime_type=\"application/json\",\n            tags={\"config\"},\n            meta={\"custom\": \"value\"},\n        )\n        def get_config() -> dict:\n            return {\"setting\": \"value\"}\n\n        decorated = cast(DecoratedResource, get_config)\n        assert callable(get_config)\n        assert hasattr(get_config, \"__fastmcp__\")\n        assert decorated.__fastmcp__.uri == \"config://app\"\n        assert decorated.__fastmcp__.name == \"app-config\"\n        assert decorated.__fastmcp__.title == \"Application Config\"\n        assert decorated.__fastmcp__.description == \"Gets app configuration\"\n        assert decorated.__fastmcp__.mime_type == \"application/json\"\n        assert decorated.__fastmcp__.tags == {\"config\"}\n        assert decorated.__fastmcp__.meta == {\"custom\": \"value\"}\n\n    async def test_resource_function_still_callable(self):\n        \"\"\"Decorated function should still be directly callable.\"\"\"\n\n        @resource(\"config://app\")\n        def get_config() -> dict:\n            \"\"\"Get config.\"\"\"\n            return {\"setting\": \"value\"}\n\n        # The function is still callable even though it has metadata\n        result = cast(DecoratedResource, get_config)()\n        assert result == {\"setting\": \"value\"}\n\n    def test_resource_rejects_classmethod_decorator(self):\n        \"\"\"@resource should reject classmethod-decorated functions.\"\"\"\n\n        # Note: This now happens when added to server, not at decoration time\n        @resource(\"config://app\")\n        def standalone() -> str:\n            return \"{}\"\n\n        # Should not raise at decoration\n        assert callable(standalone)\n\n    async def test_resource_added_to_server(self):\n        \"\"\"Resource created by @resource should work when added to a server.\"\"\"\n\n        @resource(\"config://app\")\n        def get_config() -> str:\n            \"\"\"Get config.\"\"\"\n            return '{\"version\": \"1.0\"}'\n\n        assert callable(get_config)\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_resource(get_config)\n\n        async with Client(mcp) as client:\n            resources = await client.list_resources()\n            assert any(str(r.uri) == \"config://app\" for r in resources)\n\n            result = await client.read_resource(\"config://app\")\n            assert \"1.0\" in str(result)\n\n    async def test_template_added_to_server(self):\n        \"\"\"Template created by @resource should work when added to a server.\"\"\"\n\n        @resource(\"users://{user_id}/profile\")\n        def get_profile(user_id: str) -> str:\n            \"\"\"Get user profile.\"\"\"\n            return f'{{\"id\": \"{user_id}\"}}'\n\n        assert callable(get_profile)\n\n        mcp = FastMCP(\"Test\")\n        # add_resource handles both resources and templates based on metadata\n        mcp.add_resource(get_profile)\n\n        async with Client(mcp) as client:\n            templates = await client.list_resource_templates()\n            assert any(t.uriTemplate == \"users://{user_id}/profile\" for t in templates)\n\n            result = await client.read_resource(\"users://123/profile\")\n            assert \"123\" in str(result)\n"
  },
  {
    "path": "tests/server/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/oauth_proxy/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/oauth_proxy/conftest.py",
    "content": "\"\"\"Shared fixtures and helpers for OAuth proxy tests.\"\"\"\n\nimport asyncio\nimport secrets\nimport time\nfrom unittest.mock import Mock\nfrom urllib.parse import urlencode\n\nimport pytest\nfrom mcp.server.auth.provider import AccessToken\nfrom starlette.applications import Starlette\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp.server.auth.auth import TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n\nclass MockOAuthProvider:\n    \"\"\"Mock OAuth provider for testing OAuth proxy E2E flows.\n\n    This provider simulates a complete OAuth server without requiring:\n    - Real authentication credentials\n    - Browser automation\n    - Network calls to external services\n    \"\"\"\n\n    def __init__(self, port: int = 0):\n        self.port = port\n        self.base_url = f\"http://localhost:{port}\"\n        self.app = None\n        self.server = None\n\n        # Storage for OAuth state\n        self.authorization_codes = {}\n        self.access_tokens = {}\n        self.refresh_tokens = {}\n        self.revoked_tokens = set()\n\n        # Tracking for assertions\n        self.authorize_called = False\n        self.token_called = False\n        self.refresh_called = False\n        self.revoke_called = False\n\n        # Configuration\n        self.require_pkce = False\n        self.token_endpoint_auth_method = \"client_secret_basic\"\n\n    @property\n    def authorize_endpoint(self) -> str:\n        return f\"{self.base_url}/authorize\"\n\n    @property\n    def token_endpoint(self) -> str:\n        return f\"{self.base_url}/token\"\n\n    @property\n    def revocation_endpoint(self) -> str:\n        return f\"{self.base_url}/revoke\"\n\n    def create_app(self) -> Starlette:\n        \"\"\"Create the mock OAuth server application.\"\"\"\n        return Starlette(\n            routes=[\n                Route(\"/authorize\", self.handle_authorize),\n                Route(\"/token\", self.handle_token, methods=[\"POST\"]),\n                Route(\"/revoke\", self.handle_revoke, methods=[\"POST\"]),\n            ]\n        )\n\n    async def handle_authorize(self, request):\n        \"\"\"Handle authorization requests.\"\"\"\n        self.authorize_called = True\n        query = dict(request.query_params)\n\n        # Validate PKCE if required\n        if self.require_pkce and \"code_challenge\" not in query:\n            return JSONResponse(\n                {\"error\": \"invalid_request\", \"error_description\": \"PKCE required\"},\n                status_code=400,\n            )\n\n        # Generate authorization code\n        code = secrets.token_urlsafe(32)\n        self.authorization_codes[code] = {\n            \"client_id\": query.get(\"client_id\"),\n            \"redirect_uri\": query.get(\"redirect_uri\"),\n            \"state\": query.get(\"state\"),\n            \"code_challenge\": query.get(\"code_challenge\"),\n            \"code_challenge_method\": query.get(\"code_challenge_method\", \"S256\"),\n            \"scope\": query.get(\"scope\"),\n            \"created_at\": time.time(),\n        }\n\n        # Redirect back to callback\n        redirect_uri = query[\"redirect_uri\"]\n        params = {\"code\": code}\n        if query.get(\"state\"):\n            params[\"state\"] = query[\"state\"]\n\n        redirect_url = f\"{redirect_uri}?{urlencode(params)}\"\n        return JSONResponse(\n            content={}, status_code=302, headers={\"Location\": redirect_url}\n        )\n\n    async def handle_token(self, request):\n        \"\"\"Handle token requests.\"\"\"\n        self.token_called = True\n        form = await request.form()\n        grant_type = form.get(\"grant_type\")\n\n        if grant_type == \"authorization_code\":\n            code = form.get(\"code\")\n            if code not in self.authorization_codes:\n                return JSONResponse(\n                    {\"error\": \"invalid_grant\", \"error_description\": \"Invalid code\"},\n                    status_code=400,\n                )\n\n            # Validate PKCE if it was used\n            auth_data = self.authorization_codes[code]\n            if auth_data.get(\"code_challenge\"):\n                verifier = form.get(\"code_verifier\")\n                if not verifier:\n                    return JSONResponse(\n                        {\n                            \"error\": \"invalid_request\",\n                            \"error_description\": \"Missing code_verifier\",\n                        },\n                        status_code=400,\n                    )\n                # In a real implementation, we'd validate the verifier\n\n            # Generate tokens\n            access_token = f\"mock_access_{secrets.token_hex(16)}\"\n            refresh_token = f\"mock_refresh_{secrets.token_hex(16)}\"\n\n            self.access_tokens[access_token] = {\n                \"client_id\": auth_data[\"client_id\"],\n                \"scope\": auth_data.get(\"scope\"),\n                \"expires_at\": time.time() + 3600,\n            }\n            self.refresh_tokens[refresh_token] = {\n                \"client_id\": auth_data[\"client_id\"],\n                \"scope\": auth_data.get(\"scope\"),\n            }\n\n            # Clean up used code\n            del self.authorization_codes[code]\n\n            return JSONResponse(\n                {\n                    \"access_token\": access_token,\n                    \"token_type\": \"Bearer\",\n                    \"expires_in\": 3600,\n                    \"refresh_token\": refresh_token,\n                    \"scope\": auth_data.get(\"scope\"),\n                }\n            )\n\n        elif grant_type == \"refresh_token\":\n            self.refresh_called = True\n            refresh_token = form.get(\"refresh_token\")\n\n            if refresh_token not in self.refresh_tokens:\n                return JSONResponse(\n                    {\n                        \"error\": \"invalid_grant\",\n                        \"error_description\": \"Invalid refresh token\",\n                    },\n                    status_code=400,\n                )\n\n            # Generate new access token\n            new_access = f\"mock_access_{secrets.token_hex(16)}\"\n            token_data = self.refresh_tokens[refresh_token]\n\n            self.access_tokens[new_access] = {\n                \"client_id\": token_data[\"client_id\"],\n                \"scope\": token_data.get(\"scope\"),\n                \"expires_at\": time.time() + 3600,\n            }\n\n            return JSONResponse(\n                {\n                    \"access_token\": new_access,\n                    \"token_type\": \"Bearer\",\n                    \"expires_in\": 3600,\n                    \"refresh_token\": refresh_token,  # Same refresh token\n                    \"scope\": token_data.get(\"scope\"),\n                }\n            )\n\n        return JSONResponse({\"error\": \"unsupported_grant_type\"}, status_code=400)\n\n    async def handle_revoke(self, request):\n        \"\"\"Handle token revocation.\"\"\"\n        self.revoke_called = True\n        form = await request.form()\n        token = form.get(\"token\")\n\n        if token:\n            self.revoked_tokens.add(token)\n            # Remove from active tokens\n            self.access_tokens.pop(token, None)\n            self.refresh_tokens.pop(token, None)\n\n        return JSONResponse({})\n\n    async def start(self):\n        \"\"\"Start the mock OAuth server.\"\"\"\n        import socket\n\n        from uvicorn import Config, Server\n\n        self.app = self.create_app()\n\n        # If port is 0, find an available port\n        if self.port == 0:\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                s.bind((\"127.0.0.1\", 0))\n                s.listen(1)\n                self.port = s.getsockname()[1]\n\n        self.base_url = f\"http://localhost:{self.port}\"\n        config = Config(\n            self.app,\n            host=\"localhost\",\n            port=self.port,\n            log_level=\"error\",\n            ws=\"websockets-sansio\",\n        )\n        self.server = Server(config)\n\n        # Start server in background\n        asyncio.create_task(self.server.serve())\n\n        # Wait for server to be ready\n        await asyncio.sleep(0.05)\n\n    async def stop(self):\n        \"\"\"Stop the mock OAuth server.\"\"\"\n        if self.server:\n            self.server.should_exit = True\n            await asyncio.sleep(0.01)\n\n    def reset(self):\n        \"\"\"Reset all state for next test.\"\"\"\n        self.authorization_codes.clear()\n        self.access_tokens.clear()\n        self.refresh_tokens.clear()\n        self.revoked_tokens.clear()\n        self.authorize_called = False\n        self.token_called = False\n        self.refresh_called = False\n        self.revoke_called = False\n\n\nclass MockTokenVerifier(TokenVerifier):\n    \"\"\"Mock token verifier for testing.\"\"\"\n\n    def __init__(self, required_scopes=None):\n        self.required_scopes = required_scopes or [\"read\", \"write\"]\n        self.verify_called = False\n\n    async def verify_token(self, token: str) -> AccessToken | None:  # type: ignore[override]\n        \"\"\"Mock token verification.\"\"\"\n        self.verify_called = True\n        return AccessToken(\n            token=token,\n            client_id=\"mock-client\",\n            scopes=self.required_scopes,\n            expires_at=int(time.time() + 3600),\n        )\n\n\n@pytest.fixture\ndef jwt_verifier():\n    \"\"\"Create a mock JWT verifier for testing.\"\"\"\n    verifier = Mock(spec=JWTVerifier)\n    verifier.required_scopes = [\"read\", \"write\"]\n    verifier.verify_token = Mock(return_value=None)\n    return verifier\n\n\n@pytest.fixture\ndef oauth_proxy(jwt_verifier):\n    \"\"\"Create a standard OAuthProxy instance for testing.\"\"\"\n    from key_value.aio.stores.memory import MemoryStore\n\n    return OAuthProxy(\n        upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n        upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n        upstream_client_id=\"test-client-id\",\n        upstream_client_secret=\"test-client-secret\",\n        token_verifier=jwt_verifier,\n        base_url=\"https://myserver.com\",\n        redirect_path=\"/auth/callback\",\n        jwt_signing_key=\"test-secret\",\n        client_storage=MemoryStore(),\n    )\n\n\n@pytest.fixture\nasync def mock_oauth_provider():\n    \"\"\"Create and start a mock OAuth provider.\"\"\"\n    provider = MockOAuthProvider()\n    await provider.start()\n    yield provider\n    await provider.stop()\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_authorization.py",
    "content": "\"\"\"Tests for OAuth proxy authorization flow.\"\"\"\n\nfrom urllib.parse import parse_qs, urlparse\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.provider import AuthorizationParams\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\n\n\nclass TestOAuthProxyAuthorization:\n    \"\"\"Tests for OAuth proxy authorization flow.\"\"\"\n\n    async def test_authorize_creates_transaction(self, oauth_proxy):\n        \"\"\"Test that authorize creates transaction and redirects to consent.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:54321/callback\")],\n            jwt_signing_key=\"test-secret\",  # type: ignore[call-arg]  # Optional field in MCP SDK\n        )\n\n        # Register client first (required for consent flow)\n        await oauth_proxy.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:54321/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state-123\",\n            code_challenge=\"challenge-abc\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        redirect_url = await oauth_proxy.authorize(client, params)\n\n        # Parse the redirect URL\n        parsed = urlparse(redirect_url)\n        query_params = parse_qs(parsed.query)\n\n        # Should redirect to consent page\n        assert \"/consent\" in redirect_url\n        assert \"txn_id\" in query_params\n\n        # Verify transaction was stored with correct data\n        txn_id = query_params[\"txn_id\"][0]\n        transaction = await oauth_proxy._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert transaction.client_id == \"test-client\"\n        assert transaction.code_challenge == \"challenge-abc\"\n        assert transaction.client_state == \"client-state-123\"\n        assert transaction.scopes == [\"read\", \"write\"]\n\n\nclass TestOAuthProxyPKCE:\n    \"\"\"Tests for OAuth proxy PKCE forwarding.\"\"\"\n\n    @pytest.fixture\n    def proxy_with_pkce(self, jwt_verifier):\n        return OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            forward_pkce=True,\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n    @pytest.fixture\n    def proxy_without_pkce(self, jwt_verifier):\n        from fastmcp.server.auth.oauth_proxy import OAuthProxy\n\n        return OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            forward_pkce=False,\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n    async def test_pkce_forwarding_enabled(self, proxy_with_pkce):\n        \"\"\"Test that proxy generates and forwards its own PKCE.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Register client first\n        await proxy_with_pkce.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"client_challenge\",\n            scopes=[\"read\"],\n        )\n\n        redirect_url = await proxy_with_pkce.authorize(client, params)\n        query_params = parse_qs(urlparse(redirect_url).query)\n\n        # Should redirect to consent page\n        assert \"/consent\" in redirect_url\n        assert \"txn_id\" in query_params\n\n        # Transaction should store both challenges\n        txn_id = query_params[\"txn_id\"][0]\n        transaction = await proxy_with_pkce._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert transaction.code_challenge == \"client_challenge\"  # Client's\n        assert transaction.proxy_code_verifier is not None  # Proxy's verifier\n        # Proxy code challenge is computed from verifier when building upstream URL\n        # Just verify the verifier exists and is different from client's challenge\n        assert len(transaction.proxy_code_verifier) > 0\n\n    async def test_pkce_forwarding_disabled(self, proxy_without_pkce):\n        \"\"\"Test that PKCE is not forwarded when disabled.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Register client first\n        await proxy_without_pkce.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"client_challenge\",\n            scopes=[\"read\"],\n        )\n\n        redirect_url = await proxy_without_pkce.authorize(client, params)\n        query_params = parse_qs(urlparse(redirect_url).query)\n\n        # Should redirect to consent page\n        assert \"/consent\" in redirect_url\n        assert \"txn_id\" in query_params\n\n        # Client's challenge still stored, but no proxy PKCE\n        txn_id = query_params[\"txn_id\"][0]\n        transaction = await proxy_without_pkce._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert transaction.code_challenge == \"client_challenge\"\n        assert transaction.proxy_code_verifier is None  # No proxy PKCE when disabled\n\n\nclass TestParameterForwarding:\n    \"\"\"Tests for parameter forwarding in OAuth proxy.\"\"\"\n\n    async def test_extra_authorize_params_forwarded(self, jwt_verifier):\n        \"\"\"Test that extra authorize parameters are forwarded to upstream.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            extra_authorize_params={\n                \"audience\": \"https://api.example.com\",\n                \"prompt\": \"consent\",\n                \"max_age\": \"3600\",\n            },\n            client_storage=MemoryStore(),\n        )\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            # No resource parameter\n        )\n\n        # Should succeed (no resource check needed)\n        redirect_url = await proxy.authorize(client, params)\n        assert \"/consent\" in redirect_url\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_client_registration.py",
    "content": "\"\"\"Tests for OAuth proxy client registration (DCR).\"\"\"\n\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\n\nclass TestOAuthProxyClientRegistration:\n    \"\"\"Tests for OAuth proxy client registration (DCR).\"\"\"\n\n    async def test_register_client(self, oauth_proxy):\n        \"\"\"Test client registration creates ProxyDCRClient.\"\"\"\n        client_info = OAuthClientInformationFull(\n            client_id=\"original-client\",\n            client_secret=\"original-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await oauth_proxy.register_client(client_info)\n\n        # Client should be retrievable with original credentials\n        stored = await oauth_proxy.get_client(\"original-client\")\n        assert stored is not None\n        assert stored.client_id == \"original-client\"\n        # Proxy uses token_endpoint_auth_method=\"none\", so client_secret is not stored\n        assert stored.client_secret is None\n\n    async def test_get_registered_client(self, oauth_proxy):\n        \"\"\"Test retrieving a registered client.\"\"\"\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:8080/callback\")],\n        )\n        await oauth_proxy.register_client(client_info)\n\n        retrieved = await oauth_proxy.get_client(\"test-client\")\n        assert retrieved is not None\n        assert retrieved.client_id == \"test-client\"\n\n    async def test_get_unregistered_client_returns_none(self, oauth_proxy):\n        \"\"\"Test that unregistered clients return None.\"\"\"\n        client = await oauth_proxy.get_client(\"unknown-client\")\n        assert client is None\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_config.py",
    "content": "\"\"\"Tests for OAuth proxy configuration and validation.\"\"\"\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.provider import AuthorizationParams, AuthorizeError\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyHttpUrl, AnyUrl\n\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.proxy import (\n    _normalize_resource_url,\n    _server_url_has_query,\n)\n\n\nclass TestNormalizeResourceUrl:\n    \"\"\"Unit tests for the _normalize_resource_url helper function.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"url,expected\",\n        [\n            # Basic URL unchanged\n            (\"https://example.com/mcp\", \"https://example.com/mcp\"),\n            # Query parameters stripped\n            (\"https://example.com/mcp?foo=bar\", \"https://example.com/mcp\"),\n            (\"https://example.com/mcp?a=1&b=2\", \"https://example.com/mcp\"),\n            # Fragments stripped\n            (\"https://example.com/mcp#section\", \"https://example.com/mcp\"),\n            # Both query and fragment stripped\n            (\"https://example.com/mcp?foo=bar#section\", \"https://example.com/mcp\"),\n            # Trailing slash stripped\n            (\"https://example.com/mcp/\", \"https://example.com/mcp\"),\n            # Trailing slash with query params\n            (\"https://example.com/mcp/?foo=bar\", \"https://example.com/mcp\"),\n            # Preserves path structure\n            (\n                \"https://example.com/api/v2/mcp?kb_name=test\",\n                \"https://example.com/api/v2/mcp\",\n            ),\n            # Preserves port\n            (\"https://example.com:8080/mcp?foo=bar\", \"https://example.com:8080/mcp\"),\n            # Root path\n            (\"https://example.com/?foo=bar\", \"https://example.com\"),\n            (\"https://example.com/\", \"https://example.com\"),\n        ],\n    )\n    def test_normalizes_urls_correctly(self, url: str, expected: str):\n        \"\"\"Test that URLs are normalized by stripping query params, fragments, and trailing slashes.\"\"\"\n        assert _normalize_resource_url(url) == expected\n\n    @pytest.mark.parametrize(\n        \"url,has_query\",\n        [\n            (\"https://example.com/mcp\", False),\n            (\"https://example.com/mcp?foo=bar\", True),\n            (\"https://example.com/mcp?\", False),  # Empty query string\n            (\"https://example.com/mcp#fragment\", False),\n            (\"https://example.com/mcp?a=1&b=2\", True),\n        ],\n    )\n    def test_server_url_has_query(self, url: str, has_query: bool):\n        \"\"\"Test detection of query parameters in server URLs.\"\"\"\n        assert _server_url_has_query(url) == has_query\n\n\nclass TestResourceURLValidation:\n    \"\"\"Tests for OAuth Proxy resource URL validation (GHSA-5h2m-4q8j-pqpj fix).\"\"\"\n\n    @pytest.fixture\n    def proxy_with_resource_url(self, jwt_verifier):\n        \"\"\"Create an OAuthProxy with set_mcp_path called.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        # Use non-default path to prove fix isn't relying on old hardcoded /mcp\n        proxy.set_mcp_path(\"/api/v2/mcp\")\n        return proxy\n\n    async def test_authorize_rejects_mismatched_resource(self, proxy_with_resource_url):\n        \"\"\"Test that authorization rejects requests with mismatched resource.\"\"\"\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy_with_resource_url.register_client(client)\n\n        # Client requests a different resource than the server's\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://malicious-server.com/mcp\",  # Wrong resource\n        )\n\n        with pytest.raises(AuthorizeError) as exc_info:\n            await proxy_with_resource_url.authorize(client, params)\n\n        assert exc_info.value.error == \"invalid_target\"\n        assert \"Resource does not match\" in exc_info.value.error_description\n\n    async def test_authorize_accepts_matching_resource(self, proxy_with_resource_url):\n        \"\"\"Test that authorization accepts requests with matching resource.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy_with_resource_url.register_client(client)\n\n        # Client requests the correct resource (must match /api/v2/mcp path)\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/api/v2/mcp\",  # Correct resource\n        )\n\n        # Should succeed (redirect to consent page)\n        redirect_url = await proxy_with_resource_url.authorize(client, params)\n        assert \"/consent\" in redirect_url\n\n    async def test_authorize_rejects_old_hardcoded_mcp_path(\n        self, proxy_with_resource_url\n    ):\n        \"\"\"Test that old hardcoded /mcp path is rejected when server uses different path.\"\"\"\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy_with_resource_url.register_client(client)\n\n        # Client requests the old hardcoded /mcp path (would have worked before fix)\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/mcp\",  # Old hardcoded path\n        )\n\n        # Should fail because server is at /api/v2/mcp, not /mcp\n        with pytest.raises(AuthorizeError) as exc_info:\n            await proxy_with_resource_url.authorize(client, params)\n\n        assert exc_info.value.error == \"invalid_target\"\n\n    async def test_authorize_accepts_no_resource(self, proxy_with_resource_url):\n        \"\"\"Test that authorization accepts requests without resource parameter.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy_with_resource_url.register_client(client)\n\n        # Client doesn't specify resource\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            # No resource parameter\n        )\n\n        # Should succeed (no resource check needed)\n        redirect_url = await proxy_with_resource_url.authorize(client, params)\n        assert \"/consent\" in redirect_url\n\n    async def test_authorize_accepts_resource_with_query_params(\n        self, proxy_with_resource_url\n    ):\n        \"\"\"Test that authorization accepts resource URLs with query parameters.\n\n        Per RFC 8707, clients may include query parameters in resource URLs.\n        ChatGPT sends resource URLs like ?kb_name=X, which should match the\n        server's resource URL that doesn't include query params.\n        \"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy_with_resource_url.register_client(client)\n\n        # Client requests resource with query params (like ChatGPT does)\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/api/v2/mcp?kb_name=test\",\n        )\n\n        # Should succeed - base URL matches, query params are normalized away\n        redirect_url = await proxy_with_resource_url.authorize(client, params)\n        assert \"/consent\" in redirect_url\n\n    async def test_authorize_rejects_different_path_with_query_params(\n        self, proxy_with_resource_url\n    ):\n        \"\"\"Test that query param normalization doesn't bypass path validation.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        await proxy_with_resource_url.register_client(client)\n\n        # Client requests wrong path but with query params\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/wrong/path?kb_name=test\",\n        )\n\n        # Should fail - path doesn't match even after normalizing query params\n        with pytest.raises(AuthorizeError) as exc_info:\n            await proxy_with_resource_url.authorize(client, params)\n\n        assert exc_info.value.error == \"invalid_target\"\n\n    async def test_authorize_requires_exact_match_when_server_has_query_params(\n        self, jwt_verifier\n    ):\n        \"\"\"Test that when server URL has query params, exact match is required.\n\n        If a server configures its resource URL with query params (e.g., for\n        multi-tenant or per-KB scoping), clients must provide the exact same\n        query params. This prevents bypassing tenant isolation.\n        \"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        proxy.set_mcp_path(\"/mcp\")\n        # Simulate server configured with query params for tenant scoping\n        proxy._resource_url = AnyHttpUrl(\"https://proxy.example.com/mcp?tenant=acme\")\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client)\n\n        # Client requests with DIFFERENT query params - should fail\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/mcp?tenant=other\",  # Wrong tenant!\n        )\n\n        with pytest.raises(AuthorizeError) as exc_info:\n            await proxy.authorize(client, params)\n\n        assert exc_info.value.error == \"invalid_target\"\n\n    async def test_authorize_accepts_exact_match_when_server_has_query_params(\n        self, jwt_verifier\n    ):\n        \"\"\"Test that exact query param match succeeds when server has query params.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        proxy.set_mcp_path(\"/mcp\")\n        # Simulate server configured with query params for tenant scoping\n        proxy._resource_url = AnyHttpUrl(\"https://proxy.example.com/mcp?tenant=acme\")\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client)\n\n        # Client requests with SAME query params - should succeed\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/mcp?tenant=acme\",  # Exact match\n        )\n\n        redirect_url = await proxy.authorize(client, params)\n        assert \"/consent\" in redirect_url\n\n    async def test_authorize_rejects_no_query_when_server_has_query_params(\n        self, jwt_verifier\n    ):\n        \"\"\"Test that missing query params are rejected when server requires them.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        proxy.set_mcp_path(\"/mcp\")\n        # Simulate server configured with query params for tenant scoping\n        proxy._resource_url = AnyHttpUrl(\"https://proxy.example.com/mcp?tenant=acme\")\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client)\n\n        # Client requests WITHOUT query params - should fail\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"challenge\",\n            scopes=[\"read\"],\n            resource=\"https://proxy.example.com/mcp\",  # Missing tenant param!\n        )\n\n        with pytest.raises(AuthorizeError) as exc_info:\n            await proxy.authorize(client, params)\n\n        assert exc_info.value.error == \"invalid_target\"\n\n    def test_set_mcp_path_creates_jwt_issuer_with_correct_audience(self, jwt_verifier):\n        \"\"\"Test that set_mcp_path creates JWTIssuer with correct audience.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Before set_mcp_path, _jwt_issuer is None\n        assert proxy._jwt_issuer is None\n\n        # Call set_mcp_path with custom path\n        proxy.set_mcp_path(\"/custom/mcp\")\n\n        # After set_mcp_path, _jwt_issuer should be created\n        assert proxy._jwt_issuer is not None\n        assert proxy.jwt_issuer.audience == \"https://proxy.example.com/custom/mcp\"\n        assert proxy.jwt_issuer.issuer == \"https://proxy.example.com/\"\n\n    def test_set_mcp_path_uses_base_url_if_no_path(self, jwt_verifier):\n        \"\"\"Test that set_mcp_path uses base_url as audience if no path provided.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        proxy.set_mcp_path(None)\n\n        assert proxy.jwt_issuer.audience == \"https://proxy.example.com/\"\n\n    def test_jwt_issuer_property_raises_if_not_initialized(self, jwt_verifier):\n        \"\"\"Test that jwt_issuer property raises if set_mcp_path not called.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        with pytest.raises(RuntimeError) as exc_info:\n            _ = proxy.jwt_issuer\n\n        assert \"JWT issuer not initialized\" in str(exc_info.value)\n\n    def test_get_routes_calls_set_mcp_path(self, jwt_verifier):\n        \"\"\"Test that get_routes() calls set_mcp_path() to initialize JWT issuer.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Before get_routes, _jwt_issuer is None\n        assert proxy._jwt_issuer is None\n\n        # get_routes should call set_mcp_path internally\n        proxy.get_routes(\"/api/mcp\")\n\n        # After get_routes, _jwt_issuer should be created with correct audience\n        assert proxy._jwt_issuer is not None\n        assert proxy.jwt_issuer.audience == \"https://proxy.example.com/api/mcp\"\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_e2e.py",
    "content": "\"\"\"End-to-end tests for OAuth proxy using mock provider.\"\"\"\n\nimport time\nfrom unittest.mock import AsyncMock, patch\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.provider import AuthorizationCode, AuthorizationParams\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.auth import RefreshToken\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.models import ClientCode\nfrom tests.server.auth.oauth_proxy.conftest import MockTokenVerifier\n\n\nclass TestOAuthProxyE2E:\n    \"\"\"End-to-end tests using mock OAuth provider.\"\"\"\n\n    async def test_full_oauth_flow_with_mock_provider(self, mock_oauth_provider):\n        \"\"\"Test complete OAuth flow with mock provider.\"\"\"\n        # Create proxy pointing to mock provider\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=mock_oauth_provider.authorize_endpoint,\n            upstream_token_endpoint=mock_oauth_provider.token_endpoint,\n            upstream_client_id=\"mock-client\",\n            upstream_client_secret=\"mock-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Create FastMCP server with proxy\n        server = FastMCP(\"Test Server\", auth=proxy)\n\n        @server.tool\n        def protected_tool() -> str:\n            return \"Protected data\"\n\n        # Start authorization flow\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Register client first\n        await proxy.register_client(client_info)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"\",  # Empty string for no PKCE\n            scopes=[\"read\"],\n        )\n\n        # Get authorization URL (now returns consent redirect)\n        auth_url = await proxy.authorize(client_info, params)\n\n        # Should redirect to consent page\n        assert \"/consent\" in auth_url\n        query_params = parse_qs(urlparse(auth_url).query)\n        assert \"txn_id\" in query_params\n\n        # Verify transaction was created with correct configuration\n        txn_id = query_params[\"txn_id\"][0]\n        transaction = await proxy._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert transaction.client_id == \"test-client\"\n        assert transaction.scopes == [\"read\"]\n        # Transaction ID itself is used as upstream state parameter\n        assert transaction.txn_id == txn_id\n\n    async def test_token_refresh_with_mock_provider(self, mock_oauth_provider):\n        \"\"\"Test token refresh flow with mock provider.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=mock_oauth_provider.authorize_endpoint,\n            upstream_token_endpoint=mock_oauth_provider.token_endpoint,\n            upstream_client_id=\"mock-client\",\n            upstream_client_secret=\"mock-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Initialize JWT issuer before token operations\n        proxy.set_mcp_path(\"/mcp\")\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Register client first\n        await proxy.register_client(client)\n\n        # Set up initial upstream tokens in mock provider\n        upstream_refresh_token = \"mock_refresh_initial\"\n        mock_oauth_provider.refresh_tokens[upstream_refresh_token] = {\n            \"client_id\": \"mock-client\",\n            \"scope\": \"read write\",\n        }\n\n        with patch(\n            \"fastmcp.server.auth.oauth_proxy.proxy.AsyncOAuth2Client\"\n        ) as MockClient:\n            mock_client = AsyncMock()\n\n            # Mock initial token exchange to get FastMCP tokens\n            mock_client.fetch_token = AsyncMock(\n                return_value={\n                    \"access_token\": \"upstream-access-initial\",\n                    \"refresh_token\": upstream_refresh_token,\n                    \"expires_in\": 3600,\n                    \"token_type\": \"Bearer\",\n                }\n            )\n\n            # Configure mock to call real provider for refresh\n            async def mock_refresh(*args, **kwargs):\n                async with httpx.AsyncClient() as http:\n                    response = await http.post(\n                        mock_oauth_provider.token_endpoint,\n                        data={\n                            \"grant_type\": \"refresh_token\",\n                            \"refresh_token\": upstream_refresh_token,\n                        },\n                    )\n                    return response.json()\n\n            mock_client.refresh_token = mock_refresh\n            MockClient.return_value = mock_client\n\n            # Store client code that would be created during OAuth callback\n            client_code = ClientCode(\n                code=\"test-auth-code\",\n                client_id=\"test-client\",\n                redirect_uri=\"http://localhost:12345/callback\",\n                code_challenge=\"\",\n                code_challenge_method=\"S256\",\n                scopes=[\"read\", \"write\"],\n                idp_tokens={\n                    \"access_token\": \"upstream-access-initial\",\n                    \"refresh_token\": upstream_refresh_token,\n                    \"expires_in\": 3600,\n                    \"token_type\": \"Bearer\",\n                },\n                expires_at=time.time() + 300,\n                created_at=time.time(),\n            )\n            await proxy._code_store.put(key=client_code.code, value=client_code)\n\n            # Exchange authorization code to get FastMCP tokens\n            auth_code = AuthorizationCode(\n                code=\"test-auth-code\",\n                scopes=[\"read\", \"write\"],\n                expires_at=time.time() + 300,\n                client_id=\"test-client\",\n                code_challenge=\"\",\n                redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n                redirect_uri_provided_explicitly=True,\n            )\n            initial_result = await proxy.exchange_authorization_code(\n                client=client,\n                authorization_code=auth_code,\n            )\n\n            # Now test refresh with the valid FastMCP refresh token\n            assert initial_result.refresh_token is not None\n            fastmcp_refresh = RefreshToken(\n                token=initial_result.refresh_token,\n                client_id=\"test-client\",\n                scopes=[\"read\"],\n                expires_at=None,\n            )\n\n            result = await proxy.exchange_refresh_token(\n                client, fastmcp_refresh, [\"read\"]\n            )\n\n            # Should return new FastMCP tokens (not upstream tokens)\n            assert result.access_token != \"upstream-access-initial\"\n            # FastMCP tokens are JWTs (have 3 segments)\n            assert len(result.access_token.split(\".\")) == 3\n            assert mock_oauth_provider.refresh_called\n\n    async def test_pkce_validation_with_mock_provider(self, mock_oauth_provider):\n        \"\"\"Test PKCE validation with mock provider.\"\"\"\n        mock_oauth_provider.require_pkce = True\n\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=mock_oauth_provider.authorize_endpoint,\n            upstream_token_endpoint=mock_oauth_provider.token_endpoint,\n            upstream_client_id=\"mock-client\",\n            upstream_client_secret=\"mock-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            forward_pkce=True,  # Enable PKCE forwarding\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Register client first\n        await proxy.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state\",\n            code_challenge=\"client_challenge_value\",\n            scopes=[\"read\"],\n        )\n\n        # Start authorization with PKCE\n        auth_url = await proxy.authorize(client, params)\n        query_params = parse_qs(urlparse(auth_url).query)\n\n        # Should redirect to consent page\n        assert \"/consent\" in auth_url\n        assert \"txn_id\" in query_params\n\n        # Transaction should have proxy's PKCE verifier (different from client's)\n        txn_id = query_params[\"txn_id\"][0]\n        transaction = await proxy._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert (\n            transaction.code_challenge == \"client_challenge_value\"\n        )  # Client's challenge\n        assert transaction.proxy_code_verifier is not None  # Proxy generated its own\n        # Proxy code challenge is computed from verifier when needed\n        assert len(transaction.proxy_code_verifier) > 0\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_oauth_proxy.py",
    "content": "\"\"\"Tests for OAuth proxy initialization and configuration.\"\"\"\n\nimport httpx\nimport pytest\nfrom authlib.integrations.httpx_client import AsyncOAuth2Client\nfrom key_value.aio.stores.memory import MemoryStore\nfrom starlette.applications import Starlette\n\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\n\n\nclass TestOAuthProxyInitialization:\n    \"\"\"Tests for OAuth proxy initialization and configuration.\"\"\"\n\n    def test_basic_initialization(self, jwt_verifier):\n        \"\"\"Test basic proxy initialization with required parameters.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            upstream_client_secret=\"secret-456\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        assert (\n            proxy._upstream_authorization_endpoint\n            == \"https://auth.example.com/authorize\"\n        )\n        assert proxy._upstream_token_endpoint == \"https://auth.example.com/token\"\n        assert proxy._upstream_client_id == \"client-123\"\n        assert proxy._upstream_client_secret is not None\n        assert proxy._upstream_client_secret.get_secret_value() == \"secret-456\"\n        assert str(proxy.base_url) == \"https://api.example.com/\"\n\n    def test_all_optional_parameters(self, jwt_verifier):\n        \"\"\"Test initialization with all optional parameters.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            upstream_client_secret=\"secret-456\",\n            upstream_revocation_endpoint=\"https://auth.example.com/revoke\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            redirect_path=\"/custom/callback\",\n            issuer_url=\"https://issuer.example.com\",\n            service_documentation_url=\"https://docs.example.com\",\n            allowed_client_redirect_uris=[\"http://localhost:*\"],\n            valid_scopes=[\"custom\", \"scopes\"],\n            forward_pkce=False,\n            token_endpoint_auth_method=\"client_secret_post\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        assert proxy._upstream_revocation_endpoint == \"https://auth.example.com/revoke\"\n        assert proxy._redirect_path == \"/custom/callback\"\n        assert proxy._forward_pkce is False\n        assert proxy._token_endpoint_auth_method == \"client_secret_post\"\n        assert proxy.client_registration_options is not None\n        assert proxy.client_registration_options.valid_scopes == [\"custom\", \"scopes\"]\n\n    def test_redirect_path_normalization(self, jwt_verifier):\n        \"\"\"Test that redirect_path is normalized with leading slash.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.com/authorize\",\n            upstream_token_endpoint=\"https://auth.com/token\",\n            upstream_client_id=\"client\",\n            upstream_client_secret=\"secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.com\",\n            redirect_path=\"auth/callback\",  # No leading slash\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        assert proxy._redirect_path == \"/auth/callback\"\n\n    async def test_metadata_advertises_cimd_support(self, jwt_verifier):\n        \"\"\"OAuth metadata should advertise CIMD support when enabled.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            upstream_client_secret=\"secret-456\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n            enable_cimd=True,\n        )\n\n        app = Starlette(routes=proxy.get_routes())\n        transport = httpx.ASGITransport(app=app)\n\n        async with httpx.AsyncClient(\n            transport=transport, base_url=\"https://api.example.com\"\n        ) as client:\n            response = await client.get(\"/.well-known/oauth-authorization-server\")\n\n        assert response.status_code == 200\n        metadata = response.json()\n        assert metadata.get(\"client_id_metadata_document_supported\") is True\n\n\nclass TestOptionalClientSecret:\n    \"\"\"Tests for OAuthProxy without upstream_client_secret.\"\"\"\n\n    def test_no_secret_requires_jwt_signing_key(self, jwt_verifier):\n        \"\"\"OAuthProxy requires jwt_signing_key when client_secret is omitted.\"\"\"\n        with pytest.raises(ValueError, match=\"jwt_signing_key is required\"):\n            OAuthProxy(\n                upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n                upstream_token_endpoint=\"https://auth.example.com/token\",\n                upstream_client_id=\"client-123\",\n                token_verifier=jwt_verifier,\n                base_url=\"https://api.example.com\",\n                client_storage=MemoryStore(),\n            )\n\n    def test_no_secret_with_jwt_key_succeeds(self, jwt_verifier):\n        \"\"\"OAuthProxy initializes successfully without client_secret when jwt_signing_key is given.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            jwt_signing_key=b\"a\" * 32,\n            client_storage=MemoryStore(),\n        )\n        assert proxy._upstream_client_secret is None\n        assert proxy._upstream_client_id == \"client-123\"\n\n    def test_factory_method_without_secret(self, jwt_verifier):\n        \"\"\"_create_upstream_oauth_client works when no secret is configured.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            jwt_signing_key=b\"a\" * 32,\n            client_storage=MemoryStore(),\n        )\n        client = proxy._create_upstream_oauth_client()\n        assert isinstance(client, AsyncOAuth2Client)\n        assert client.client_id == \"client-123\"\n\n    def test_factory_method_with_secret(self, jwt_verifier):\n        \"\"\"_create_upstream_oauth_client includes the secret when configured.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            upstream_client_secret=\"secret-456\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        client = proxy._create_upstream_oauth_client()\n        assert isinstance(client, AsyncOAuth2Client)\n        assert client.client_secret == \"secret-456\"\n\n    def test_consent_cookies_work_without_secret(self, jwt_verifier):\n        \"\"\"Cookie signing/verification works using JWT key when no secret is configured.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"client-123\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://api.example.com\",\n            jwt_signing_key=b\"a\" * 32,\n            client_storage=MemoryStore(),\n        )\n        signed = proxy._sign_cookie(\"test-payload\")\n        assert proxy._verify_cookie(signed) == \"test-payload\"\n        assert proxy._verify_cookie(\"tampered.payload\") is None\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_tokens.py",
    "content": "\"\"\"Tests for OAuth proxy token endpoint and handling.\"\"\"\n\nimport time\nfrom unittest.mock import AsyncMock, Mock, patch\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.handlers.token import TokenErrorResponse\nfrom mcp.server.auth.handlers.token import TokenHandler as SDKTokenHandler\nfrom mcp.server.auth.provider import AuthorizationCode\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\nfrom fastmcp.server.auth.auth import RefreshToken, TokenHandler, TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.models import (\n    DEFAULT_ACCESS_TOKEN_EXPIRY_NO_REFRESH_SECONDS,\n    DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,\n    ClientCode,\n    _hash_token,\n)\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n\nclass TestOAuthProxyTokenEndpointAuth:\n    \"\"\"Tests for token endpoint authentication methods.\"\"\"\n\n    def test_token_auth_method_initialization(self, jwt_verifier):\n        \"\"\"Test different token endpoint auth methods.\"\"\"\n        # client_secret_post\n        proxy_post = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"client\",\n            upstream_client_secret=\"secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            token_endpoint_auth_method=\"client_secret_post\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        assert proxy_post._token_endpoint_auth_method == \"client_secret_post\"\n\n        # client_secret_basic (default)\n        proxy_basic = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"client\",\n            upstream_client_secret=\"secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            token_endpoint_auth_method=\"client_secret_basic\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        assert proxy_basic._token_endpoint_auth_method == \"client_secret_basic\"\n\n        # None (use authlib default)\n        proxy_default = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"client\",\n            upstream_client_secret=\"secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n        assert proxy_default._token_endpoint_auth_method is None\n\n    async def test_token_auth_method_passed_to_client(self, jwt_verifier):\n        \"\"\"Test that auth method is passed to AsyncOAuth2Client.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"client-id\",\n            upstream_client_secret=\"client-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            token_endpoint_auth_method=\"client_secret_post\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Initialize JWT issuer before token operations\n        proxy.set_mcp_path(\"/mcp\")\n\n        # First, create a valid FastMCP token via full OAuth flow\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Mock the upstream OAuth provider response\n        with patch(\n            \"fastmcp.server.auth.oauth_proxy.proxy.AsyncOAuth2Client\"\n        ) as MockClient:\n            mock_client = AsyncMock()\n\n            # Mock initial token exchange (authorization code flow)\n            mock_client.fetch_token = AsyncMock(\n                return_value={\n                    \"access_token\": \"upstream-access-token\",\n                    \"refresh_token\": \"upstream-refresh-token\",\n                    \"expires_in\": 3600,\n                    \"token_type\": \"Bearer\",\n                }\n            )\n\n            # Mock token refresh\n            mock_client.refresh_token = AsyncMock(\n                return_value={\n                    \"access_token\": \"new-upstream-token\",\n                    \"refresh_token\": \"new-upstream-refresh\",\n                    \"expires_in\": 3600,\n                    \"token_type\": \"Bearer\",\n                }\n            )\n            MockClient.return_value = mock_client\n\n            # Register client and do initial OAuth flow to get valid FastMCP tokens\n            await proxy.register_client(client)\n\n            # Store client code that would be created during OAuth callback\n            client_code = ClientCode(\n                code=\"test-auth-code\",\n                client_id=\"test-client\",\n                redirect_uri=\"http://localhost:12345/callback\",\n                code_challenge=\"\",\n                code_challenge_method=\"S256\",\n                scopes=[\"read\"],\n                idp_tokens={\n                    \"access_token\": \"upstream-access-token\",\n                    \"refresh_token\": \"upstream-refresh-token\",\n                    \"expires_in\": 3600,\n                    \"token_type\": \"Bearer\",\n                },\n                expires_at=time.time() + 300,\n                created_at=time.time(),\n            )\n            await proxy._code_store.put(key=client_code.code, value=client_code)\n\n            # Exchange authorization code to get FastMCP tokens\n            auth_code = AuthorizationCode(\n                code=\"test-auth-code\",\n                scopes=[\"read\"],\n                expires_at=time.time() + 300,\n                client_id=\"test-client\",\n                code_challenge=\"\",\n                redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n                redirect_uri_provided_explicitly=True,\n            )\n            result = await proxy.exchange_authorization_code(\n                client=client,\n                authorization_code=auth_code,\n            )\n\n            # Now test refresh with the valid FastMCP refresh token\n            assert result.refresh_token is not None\n            fastmcp_refresh = RefreshToken(\n                token=result.refresh_token,\n                client_id=\"test-client\",\n                scopes=[\"read\"],\n                expires_at=None,\n            )\n\n            # Reset mock to check refresh call\n            MockClient.reset_mock()\n            mock_client.refresh_token = AsyncMock(\n                return_value={\n                    \"access_token\": \"new-upstream-token-2\",\n                    \"refresh_token\": \"new-upstream-refresh-2\",\n                    \"expires_in\": 3600,\n                    \"token_type\": \"Bearer\",\n                }\n            )\n            MockClient.return_value = mock_client\n\n            await proxy.exchange_refresh_token(client, fastmcp_refresh, [\"read\"])\n\n            # Verify auth method was passed to OAuth client\n            MockClient.assert_called_with(\n                client_id=\"client-id\",\n                client_secret=\"client-secret\",\n                token_endpoint_auth_method=\"client_secret_post\",\n                timeout=30.0,\n            )\n\n\nclass TestTokenHandlerErrorTransformation:\n    \"\"\"Tests for TokenHandler's OAuth 2.1 compliant error transformation.\"\"\"\n\n    async def test_transforms_client_auth_failure_to_invalid_client_401(self):\n        \"\"\"Test that client authentication failures return invalid_client with 401.\"\"\"\n        handler = TokenHandler(provider=Mock(), client_authenticator=Mock())\n\n        # Create a mock 401 response like the SDK returns for auth failures\n        mock_response = Mock()\n        mock_response.status_code = 401\n        mock_response.body = (\n            b'{\"error\":\"unauthorized_client\",\"error_description\":\"Invalid client_id\"}'\n        )\n\n        # Patch the parent class's handle() to return our mock response\n        with patch.object(\n            SDKTokenHandler,\n            \"handle\",\n            new_callable=AsyncMock,\n            return_value=mock_response,\n        ):\n            response = await handler.handle(Mock())\n\n        # Should transform to OAuth 2.1 compliant response\n        assert response.status_code == 401\n        assert b'\"error\":\"invalid_client\"' in response.body\n        assert b'\"error_description\":\"Invalid client_id\"' in response.body\n\n    def test_does_not_transform_grant_type_unauthorized_to_invalid_client(self):\n        \"\"\"Test that grant type authorization errors stay as unauthorized_client with 400.\"\"\"\n        handler = TokenHandler(provider=Mock(), client_authenticator=Mock())\n\n        # Simulate error from grant_type not in client_info.grant_types\n        error_response = TokenErrorResponse(\n            error=\"unauthorized_client\",\n            error_description=\"Client not authorized for this grant type\",\n        )\n\n        response = handler.response(error_response)\n\n        # Should NOT transform - keep as 400 unauthorized_client\n        assert response.status_code == 400\n        assert b'\"error\":\"unauthorized_client\"' in response.body\n\n    async def test_transforms_invalid_grant_to_401(self):\n        \"\"\"Test that invalid_grant errors return 401 per MCP spec.\n\n        Per MCP spec: \"Invalid or expired tokens MUST receive a HTTP 401 response.\"\n        The SDK incorrectly returns 400 for all TokenErrorResponse including invalid_grant.\n        \"\"\"\n        handler = TokenHandler(provider=Mock(), client_authenticator=Mock())\n\n        # Create a mock 400 response like the SDK returns for invalid_grant\n        mock_response = Mock()\n        mock_response.status_code = 400\n        mock_response.body = (\n            b'{\"error\":\"invalid_grant\",\"error_description\":\"refresh token has expired\"}'\n        )\n\n        # Patch the parent class's handle() to return our mock response\n        with patch.object(\n            SDKTokenHandler,\n            \"handle\",\n            new_callable=AsyncMock,\n            return_value=mock_response,\n        ):\n            response = await handler.handle(Mock())\n\n        # Should transform to MCP-compliant 401 response\n        assert response.status_code == 401\n        assert b'\"error\":\"invalid_grant\"' in response.body\n        assert b'\"error_description\":\"refresh token has expired\"' in response.body\n\n    def test_does_not_transform_other_400_errors(self):\n        \"\"\"Test that non-invalid_grant 400 errors pass through unchanged.\"\"\"\n        handler = TokenHandler(provider=Mock(), client_authenticator=Mock())\n\n        # Test with invalid_request error (should stay 400)\n        error_response = TokenErrorResponse(\n            error=\"invalid_request\",\n            error_description=\"Missing required parameter\",\n        )\n\n        response = handler.response(error_response)\n\n        # Should pass through unchanged as 400\n        assert response.status_code == 400\n        assert b'\"error\":\"invalid_request\"' in response.body\n\n\nclass TestFallbackAccessTokenExpiry:\n    \"\"\"Test fallback access token expiry constants and configuration.\"\"\"\n\n    def test_default_constants(self):\n        \"\"\"Verify the default expiry constants are set correctly.\"\"\"\n        assert DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS == 60 * 60  # 1 hour\n        assert (\n            DEFAULT_ACCESS_TOKEN_EXPIRY_NO_REFRESH_SECONDS == 60 * 60 * 24 * 365\n        )  # 1 year\n\n    def test_fallback_parameter_stored(self):\n        \"\"\"Verify fallback_access_token_expiry_seconds is stored on provider.\"\"\"\n        provider = OAuthProxy(\n            upstream_authorization_endpoint=\"https://idp.example.com/authorize\",\n            upstream_token_endpoint=\"https://idp.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=JWTVerifier(\n                jwks_uri=\"https://idp.example.com/.well-known/jwks.json\",\n                issuer=\"https://idp.example.com\",\n            ),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-signing-key\",\n            fallback_access_token_expiry_seconds=86400,\n            client_storage=MemoryStore(),\n        )\n\n        assert provider._fallback_access_token_expiry_seconds == 86400\n\n    def test_fallback_parameter_defaults_to_none(self):\n        \"\"\"Verify fallback defaults to None (enabling smart defaults).\"\"\"\n        provider = OAuthProxy(\n            upstream_authorization_endpoint=\"https://idp.example.com/authorize\",\n            upstream_token_endpoint=\"https://idp.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=JWTVerifier(\n                jwks_uri=\"https://idp.example.com/.well-known/jwks.json\",\n                issuer=\"https://idp.example.com\",\n            ),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-signing-key\",\n            client_storage=MemoryStore(),\n        )\n\n        assert provider._fallback_access_token_expiry_seconds is None\n\n\nclass TestUpstreamTokenStorageTTL:\n    \"\"\"Tests for upstream token storage TTL calculation (issue #2670).\n\n    The TTL should use max(refresh_expires_in, expires_in) to handle cases where\n    the refresh token has a shorter lifetime than the access token (e.g., Keycloak\n    with sliding session windows).\n    \"\"\"\n\n    @pytest.fixture\n    def jwt_verifier(self):\n        \"\"\"Create a mock JWT verifier.\"\"\"\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\", \"write\"]\n        verifier.verify_token = AsyncMock(return_value=None)\n        return verifier\n\n    @pytest.fixture\n    def proxy(self, jwt_verifier):\n        \"\"\"Create an OAuth proxy for testing.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://idp.example.com/authorize\",\n            upstream_token_endpoint=\"https://idp.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://proxy.example.com\",\n            jwt_signing_key=\"test-secret-key\",\n            client_storage=MemoryStore(),\n        )\n        proxy.set_mcp_path(\"/mcp\")\n        return proxy\n\n    async def test_ttl_uses_max_when_refresh_shorter_than_access(self, proxy):\n        \"\"\"TTL should use access token expiry when refresh is shorter.\n\n        This is the xsreality case: Keycloak returns refresh_expires_in=120 (2 min)\n        but expires_in=28800 (8 hours). The upstream tokens should persist for\n        8 hours (the access token lifetime), not 2 minutes.\n        \"\"\"\n        # Register client\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client)\n\n        # Simulate xsreality's Keycloak setup: short refresh, long access\n        client_code = ClientCode(\n            code=\"test-auth-code\",\n            client_id=\"test-client\",\n            redirect_uri=\"http://localhost:12345/callback\",\n            code_challenge=\"test-challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\", \"write\"],\n            idp_tokens={\n                \"access_token\": \"upstream-access-token\",\n                \"refresh_token\": \"upstream-refresh-token\",\n                \"expires_in\": 28800,  # 8 hours (access token)\n                \"refresh_expires_in\": 120,  # 2 minutes (refresh token) - SHORTER!\n                \"token_type\": \"Bearer\",\n            },\n            expires_at=time.time() + 300,\n            created_at=time.time(),\n        )\n        await proxy._code_store.put(key=client_code.code, value=client_code)\n\n        # Exchange the code\n        auth_code = AuthorizationCode(\n            code=\"test-auth-code\",\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 300,\n            client_id=\"test-client\",\n            code_challenge=\"test-challenge\",\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n        )\n\n        result = await proxy.exchange_authorization_code(\n            client=client,\n            authorization_code=auth_code,\n        )\n\n        # Verify tokens were issued\n        assert result.access_token is not None\n        assert result.refresh_token is not None\n\n        # The key test: verify upstream tokens are stored with TTL=max(120, 28800)=28800\n        # We can verify this by checking the tokens are still accessible after 2 minutes\n        # would have passed (if TTL was incorrectly set to 120)\n        #\n        # Since we can't easily time-travel in tests, we verify the storage directly\n        # by checking that we can still look up the tokens for refresh purposes.\n        #\n        # Extract the JTI from the refresh token to look up the mapping\n        refresh_payload = proxy.jwt_issuer.verify_token(\n            result.refresh_token, expected_token_use=\"refresh\"\n        )\n        refresh_jti = refresh_payload[\"jti\"]\n\n        # The JTI mapping should exist\n        jti_mapping = await proxy._jti_mapping_store.get(key=refresh_jti)\n        assert jti_mapping is not None\n\n        # The upstream tokens should exist\n        upstream_tokens = await proxy._upstream_token_store.get(\n            key=jti_mapping.upstream_token_id\n        )\n        assert upstream_tokens is not None\n        assert upstream_tokens.access_token == \"upstream-access-token\"\n        assert upstream_tokens.refresh_token == \"upstream-refresh-token\"\n\n    async def test_ttl_uses_refresh_when_refresh_longer_than_access(self, proxy):\n        \"\"\"TTL should use refresh token expiry when refresh is longer.\n\n        This is the ianw case: IdP returns expires_in=300 (5 min) but\n        refresh_expires_in=32318 (9 hours). The upstream tokens should persist\n        for 9 hours (the refresh token lifetime).\n        \"\"\"\n        # Register client\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client)\n\n        # Simulate ianw's setup: short access, long refresh (typical)\n        client_code = ClientCode(\n            code=\"test-auth-code-2\",\n            client_id=\"test-client\",\n            redirect_uri=\"http://localhost:12345/callback\",\n            code_challenge=\"test-challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\", \"write\"],\n            idp_tokens={\n                \"access_token\": \"upstream-access-token-2\",\n                \"refresh_token\": \"upstream-refresh-token-2\",\n                \"expires_in\": 300,  # 5 minutes (access token)\n                \"refresh_expires_in\": 32318,  # 9 hours (refresh token) - LONGER\n                \"token_type\": \"Bearer\",\n            },\n            expires_at=time.time() + 300,\n            created_at=time.time(),\n        )\n        await proxy._code_store.put(key=client_code.code, value=client_code)\n\n        # Exchange the code\n        auth_code = AuthorizationCode(\n            code=\"test-auth-code-2\",\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 300,\n            client_id=\"test-client\",\n            code_challenge=\"test-challenge\",\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n        )\n\n        result = await proxy.exchange_authorization_code(\n            client=client,\n            authorization_code=auth_code,\n        )\n\n        # Verify tokens were issued\n        assert result.access_token is not None\n        assert result.refresh_token is not None\n\n        # Verify upstream tokens are accessible\n        refresh_payload = proxy.jwt_issuer.verify_token(\n            result.refresh_token, expected_token_use=\"refresh\"\n        )\n        refresh_jti = refresh_payload[\"jti\"]\n\n        jti_mapping = await proxy._jti_mapping_store.get(key=refresh_jti)\n        assert jti_mapping is not None\n\n        upstream_tokens = await proxy._upstream_token_store.get(\n            key=jti_mapping.upstream_token_id\n        )\n        assert upstream_tokens is not None\n\n    async def test_refresh_expires_in_zero_issues_refresh_token(self, proxy):\n        \"\"\"refresh_expires_in=0 should fall back to 30-day default.\n\n        Keycloak returns refresh_expires_in=0 for offline tokens (offline_access scope),\n        meaning \"no fixed time-based expiry\". The proxy should still issue a PROXY_RT.\n        \"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client)\n\n        client_code = ClientCode(\n            code=\"test-auth-code-keycloak-offline\",\n            client_id=\"test-client\",\n            redirect_uri=\"http://localhost:12345/callback\",\n            code_challenge=\"test-challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\", \"write\"],\n            idp_tokens={\n                \"access_token\": \"upstream-access-token-kc\",\n                \"refresh_token\": \"upstream-refresh-token-kc\",\n                \"expires_in\": 3600,\n                \"refresh_expires_in\": 0,  # Keycloak offline token convention\n                \"token_type\": \"Bearer\",\n            },\n            expires_at=time.time() + 300,\n            created_at=time.time(),\n        )\n        await proxy._code_store.put(key=client_code.code, value=client_code)\n\n        auth_code = AuthorizationCode(\n            code=\"test-auth-code-keycloak-offline\",\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 300,\n            client_id=\"test-client\",\n            code_challenge=\"test-challenge\",\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n        )\n\n        result = await proxy.exchange_authorization_code(\n            client=client,\n            authorization_code=auth_code,\n        )\n\n        # refresh_expires_in=0 must NOT prevent refresh token issuance\n        assert result.access_token is not None\n        assert result.refresh_token is not None\n\n        # Verify refresh token metadata was stored\n        refresh_meta = await proxy._refresh_token_store.get(\n            key=_hash_token(result.refresh_token)\n        )\n        assert refresh_meta is not None\n"
  },
  {
    "path": "tests/server/auth/oauth_proxy/test_ui.py",
    "content": "\"\"\"Tests for OAuth proxy UI and error page rendering.\"\"\"\n\nfrom unittest.mock import Mock\n\nfrom key_value.aio.stores.memory import MemoryStore\nfrom starlette.requests import Request\nfrom starlette.responses import HTMLResponse\n\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.ui import create_consent_html, create_error_html\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n\nclass TestErrorPageRendering:\n    \"\"\"Test error page rendering for OAuth callback errors.\"\"\"\n\n    def test_create_error_html_basic(self):\n        \"\"\"Test basic error page generation.\"\"\"\n\n        html = create_error_html(\n            error_title=\"Test Error\",\n            error_message=\"This is a test error message\",\n        )\n\n        # Verify it's valid HTML\n        assert \"<!DOCTYPE html>\" in html\n        assert \"<title>Test Error</title>\" in html\n        assert \"This is a test error message\" in html\n        assert 'class=\"info-box error\"' in html\n\n    def test_create_error_html_with_details(self):\n        \"\"\"Test error page with error details.\"\"\"\n\n        html = create_error_html(\n            error_title=\"OAuth Error\",\n            error_message=\"Authentication failed\",\n            error_details={\n                \"Error Code\": \"invalid_scope\",\n                \"Description\": \"Requested scope does not exist\",\n            },\n        )\n\n        # Verify error details are included\n        assert \"Error Details\" in html\n        assert \"Error Code\" in html\n        assert \"invalid_scope\" in html\n        assert \"Description\" in html\n        assert \"Requested scope does not exist\" in html\n\n    def test_create_error_html_escapes_user_input(self):\n        \"\"\"Test that error page properly escapes HTML in user input.\"\"\"\n\n        html = create_error_html(\n            error_title=\"Error <script>alert('xss')</script>\",\n            error_message=\"Message with <b>HTML</b> tags\",\n            error_details={\"Key<script>\": \"Value<img>\"},\n        )\n\n        # Verify HTML is escaped\n        assert \"<script>alert('xss')</script>\" not in html\n        assert \"&lt;script&gt;\" in html\n        assert \"<b>HTML</b>\" not in html\n        assert \"&lt;b&gt;HTML&lt;/b&gt;\" in html\n\n    async def test_callback_error_returns_html_page(self):\n        \"\"\"Test that OAuth callback errors return styled HTML instead of data: URLs.\"\"\"\n        # Create a minimal OAuth proxy\n        provider = OAuthProxy(\n            upstream_authorization_endpoint=\"https://idp.example.com/authorize\",\n            upstream_token_endpoint=\"https://idp.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=JWTVerifier(\n                jwks_uri=\"https://idp.example.com/.well-known/jwks.json\",\n                issuer=\"https://idp.example.com\",\n                audience=\"test-client\",\n            ),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-signing-key\",\n            client_storage=MemoryStore(),\n        )\n\n        # Mock a request with an error from the IdP\n        mock_request = Mock(spec=Request)\n        mock_request.query_params = {\n            \"error\": \"invalid_scope\",\n            \"error_description\": \"The application asked for scope 'read' that doesn't exist\",\n            \"state\": \"test-state\",\n        }\n\n        # Call the callback handler\n        response = await provider._handle_idp_callback(mock_request)\n\n        # Verify we get an HTMLResponse, not a RedirectResponse\n        assert isinstance(response, HTMLResponse)\n        assert response.status_code == 400\n\n        # Verify the response contains the error message\n        assert b\"invalid_scope\" in response.body\n        assert b\"doesn&#x27;t exist\" in response.body  # HTML-escaped apostrophe\n        assert b\"OAuth Error\" in response.body\n\n\nclass TestConsentPageRendering:\n    \"\"\"Test consent page rendering and escaping.\"\"\"\n\n    def test_create_consent_html_escapes_client_id_in_details(self):\n        \"\"\"Test that Application ID is escaped in advanced details.\"\"\"\n\n        html = create_consent_html(\n            client_id='evil<img src=x onerror=alert(\"xss\")>',\n            redirect_uri=\"https://example.com/callback\",\n            scopes=[\"read\"],\n            txn_id=\"txn\",\n            csrf_token=\"csrf\",\n        )\n\n        assert 'evil<img src=x onerror=alert(\"xss\")>' not in html\n        assert \"evil&lt;img src=x onerror=alert(&quot;xss&quot;)&gt;\" in html\n"
  },
  {
    "path": "tests/server/auth/providers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/providers/test_auth0.py",
    "content": "\"\"\"Unit tests for Auth0 OAuth provider.\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom fastmcp.server.auth.oidc_proxy import OIDCConfiguration\nfrom fastmcp.server.auth.providers.auth0 import Auth0Provider\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nTEST_CONFIG_URL = \"https://example.com/.well-known/openid-configuration\"\nTEST_CLIENT_ID = \"test-client-id\"\nTEST_CLIENT_SECRET = \"test-client-secret\"\nTEST_AUDIENCE = \"test-audience\"\nTEST_BASE_URL = \"https://example.com:8000/\"\nTEST_REDIRECT_PATH = \"/test/callback\"\nTEST_REQUIRED_SCOPES = [\"openid\", \"email\"]\n\n\n@pytest.fixture\ndef valid_oidc_configuration_dict():\n    \"\"\"Create a valid OIDC configuration dict for testing.\"\"\"\n    return {\n        \"issuer\": \"https://example.com\",\n        \"authorization_endpoint\": \"https://example.com/authorize\",\n        \"token_endpoint\": \"https://example.com/oauth/token\",\n        \"jwks_uri\": \"https://example.com/.well-known/jwks.json\",\n        \"response_types_supported\": [\"code\"],\n        \"subject_types_supported\": [\"public\"],\n        \"id_token_signing_alg_values_supported\": [\"RS256\"],\n    }\n\n\nclass TestAuth0Provider:\n    \"\"\"Test Auth0Provider initialization.\"\"\"\n\n    def test_init_with_explicit_params(self, valid_oidc_configuration_dict):\n        \"\"\"Test initialization with explicit parameters.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            provider = Auth0Provider(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                audience=TEST_AUDIENCE,\n                base_url=TEST_BASE_URL,\n                redirect_path=TEST_REDIRECT_PATH,\n                required_scopes=TEST_REQUIRED_SCOPES,\n                jwt_signing_key=\"test-secret\",\n            )\n\n            mock_get.assert_called_once()\n\n            call_args = mock_get.call_args\n            assert str(call_args[0][0]) == TEST_CONFIG_URL\n\n            assert provider._upstream_client_id == TEST_CLIENT_ID\n            assert provider._upstream_client_secret is not None\n            assert (\n                provider._upstream_client_secret.get_secret_value()\n                == TEST_CLIENT_SECRET\n            )\n\n            assert isinstance(provider._token_validator, JWTVerifier)\n            assert provider._token_validator.audience == TEST_AUDIENCE\n\n            assert str(provider.base_url) == TEST_BASE_URL\n            assert provider._redirect_path == TEST_REDIRECT_PATH\n            assert provider._token_validator.required_scopes == TEST_REQUIRED_SCOPES\n\n    def test_init_defaults(self, valid_oidc_configuration_dict):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            provider = Auth0Provider(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                audience=TEST_AUDIENCE,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n            )\n\n            # Check defaults\n            assert str(provider.base_url) == TEST_BASE_URL\n            assert provider._redirect_path == \"/auth/callback\"\n            assert provider._token_validator.required_scopes == [\"openid\"]\n"
  },
  {
    "path": "tests/server/auth/providers/test_aws.py",
    "content": "\"\"\"Unit tests for AWS Cognito OAuth provider.\"\"\"\n\nfrom contextlib import contextmanager\nfrom unittest.mock import patch\n\nfrom fastmcp.server.auth.providers.aws import (\n    AWSCognitoProvider,\n)\n\n\n@contextmanager\ndef mock_cognito_oidc_discovery():\n    \"\"\"Context manager to mock AWS Cognito OIDC discovery endpoint.\"\"\"\n    mock_oidc_config = {\n        \"issuer\": \"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX\",\n        \"authorization_endpoint\": \"https://test.auth.us-east-1.amazoncognito.com/oauth2/authorize\",\n        \"token_endpoint\": \"https://test.auth.us-east-1.amazoncognito.com/oauth2/token\",\n        \"jwks_uri\": \"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX/.well-known/jwks.json\",\n        \"userinfo_endpoint\": \"https://test.auth.us-east-1.amazoncognito.com/oauth2/userInfo\",\n        \"response_types_supported\": [\"code\", \"token\"],\n        \"subject_types_supported\": [\"public\"],\n        \"id_token_signing_alg_values_supported\": [\"RS256\"],\n        \"scopes_supported\": [\"openid\", \"email\", \"phone\", \"profile\"],\n        \"token_endpoint_auth_methods_supported\": [\n            \"client_secret_basic\",\n            \"client_secret_post\",\n        ],\n    }\n\n    with patch(\"httpx.get\") as mock_get:\n        mock_response = mock_get.return_value\n        mock_response.raise_for_status.return_value = None\n        mock_response.json.return_value = mock_oidc_config\n        yield\n\n\nclass TestAWSCognitoProvider:\n    \"\"\"Test AWSCognitoProvider initialization.\"\"\"\n\n    def test_init_with_explicit_params(self):\n        \"\"\"Test initialization with explicit parameters.\"\"\"\n        with mock_cognito_oidc_discovery():\n            provider = AWSCognitoProvider(\n                user_pool_id=\"us-east-1_XXXXXXXXX\",\n                aws_region=\"us-east-1\",\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                base_url=\"https://example.com\",\n                redirect_path=\"/custom/callback\",\n                required_scopes=[\"openid\", \"email\"],\n                jwt_signing_key=\"test-secret\",\n            )\n\n            # Check that the provider was initialized correctly\n            assert provider._upstream_client_id == \"test_client\"\n            assert provider._upstream_client_secret is not None\n            assert provider._upstream_client_secret.get_secret_value() == \"test_secret\"\n            assert (\n                str(provider.base_url) == \"https://example.com/\"\n            )  # URLs get normalized with trailing slash\n            assert provider._redirect_path == \"/custom/callback\"\n            # OIDC provider should have discovered the endpoints automatically\n            assert (\n                provider._upstream_authorization_endpoint\n                == \"https://test.auth.us-east-1.amazoncognito.com/oauth2/authorize\"\n            )\n            assert (\n                provider._upstream_token_endpoint\n                == \"https://test.auth.us-east-1.amazoncognito.com/oauth2/token\"\n            )\n\n    def test_init_defaults(self):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        with mock_cognito_oidc_discovery():\n            provider = AWSCognitoProvider(\n                user_pool_id=\"us-east-1_XXXXXXXXX\",\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                base_url=\"https://example.com\",\n                jwt_signing_key=\"test-secret\",\n            )\n\n            # Check defaults\n            assert str(provider.base_url) == \"https://example.com/\"\n            assert provider._redirect_path == \"/auth/callback\"\n            assert provider._token_validator.required_scopes == [\"openid\"]\n            assert provider.aws_region == \"eu-central-1\"\n\n    def test_oidc_discovery_integration(self):\n        \"\"\"Test that OIDC discovery endpoints are used correctly.\"\"\"\n        with mock_cognito_oidc_discovery():\n            provider = AWSCognitoProvider(\n                user_pool_id=\"us-west-2_YYYYYYYY\",\n                aws_region=\"us-west-2\",\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                base_url=\"https://example.com\",\n                jwt_signing_key=\"test-secret\",\n            )\n\n            # OIDC discovery should have configured the endpoints automatically\n            assert provider._upstream_authorization_endpoint is not None\n            assert provider._upstream_token_endpoint is not None\n            assert \"amazoncognito.com\" in provider._upstream_authorization_endpoint\n\n    def test_token_verifier_defaults_audience_to_client_id(self):\n        \"\"\"Test Cognito token verifier enforces the configured client ID by default.\"\"\"\n        with mock_cognito_oidc_discovery():\n            provider = AWSCognitoProvider(\n                user_pool_id=\"us-east-1_XXXXXXXXX\",\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                base_url=\"https://example.com\",\n                jwt_signing_key=\"test-secret\",\n            )\n\n            verifier = provider.get_token_verifier()\n\n            assert verifier.audience == \"test_client\"\n\n    def test_token_verifier_supports_audience_override(self):\n        \"\"\"Test Cognito token verifier still allows explicit audience overrides.\"\"\"\n        with mock_cognito_oidc_discovery():\n            provider = AWSCognitoProvider(\n                user_pool_id=\"us-east-1_XXXXXXXXX\",\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                base_url=\"https://example.com\",\n                jwt_signing_key=\"test-secret\",\n            )\n\n            verifier = provider.get_token_verifier(audience=\"custom-audience\")\n\n            assert verifier.audience == \"custom-audience\"\n\n\n# Token verification functionality is now tested as part of the OIDC provider integration\n# The CognitoTokenVerifier class is an internal implementation detail\n"
  },
  {
    "path": "tests/server/auth/providers/test_azure.py",
    "content": "\"\"\"Tests for Azure (Microsoft Entra) OAuth provider.\"\"\"\n\nfrom urllib.parse import parse_qs, urlparse\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.provider import AuthorizationParams\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\nfrom fastmcp.server.auth.providers.azure import AzureProvider\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\n\n@pytest.fixture\ndef memory_storage() -> MemoryStore:\n    \"\"\"Provide a MemoryStore for tests to avoid SQLite initialization on Windows.\"\"\"\n    return MemoryStore()\n\n\nclass TestAzureProvider:\n    \"\"\"Test Azure OAuth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self, memory_storage: MemoryStore):\n        \"\"\"Test AzureProvider initialization with explicit parameters.\"\"\"\n        provider = AzureProvider(\n            client_id=\"12345678-1234-1234-1234-123456789012\",\n            client_secret=\"azure_secret_123\",\n            tenant_id=\"87654321-4321-4321-4321-210987654321\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\", \"write\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert provider._upstream_client_id == \"12345678-1234-1234-1234-123456789012\"\n        assert provider._upstream_client_secret is not None\n        assert provider._upstream_client_secret.get_secret_value() == \"azure_secret_123\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n        # Check tenant is in the endpoints\n        parsed_auth = urlparse(provider._upstream_authorization_endpoint)\n        assert \"87654321-4321-4321-4321-210987654321\" in parsed_auth.path\n        parsed_token = urlparse(provider._upstream_token_endpoint)\n        assert \"87654321-4321-4321-4321-210987654321\" in parsed_token.path\n\n    def test_init_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check defaults\n        assert provider._redirect_path == \"/auth/callback\"\n        # Azure provider defaults are set but we can't easily verify them without accessing internals\n\n    def test_offline_access_automatically_included(self, memory_storage: MemoryStore):\n        \"\"\"Test that offline_access is automatically added to get refresh tokens.\"\"\"\n        # Without specifying offline_access\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert \"offline_access\" in provider.additional_authorize_scopes\n\n    def test_offline_access_not_duplicated(self, memory_storage: MemoryStore):\n        \"\"\"Test that offline_access is not duplicated if already specified.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"User.Read\", \"offline_access\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Should appear exactly once\n        assert provider.additional_authorize_scopes.count(\"offline_access\") == 1\n        assert \"User.Read\" in provider.additional_authorize_scopes\n\n    def test_oauth_endpoints_configured_correctly(self, memory_storage: MemoryStore):\n        \"\"\"Test that OAuth endpoints are configured correctly.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"my-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test_secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check that endpoints use the correct Azure OAuth2 v2.0 endpoints with tenant\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/authorize\"\n        )\n        assert (\n            provider._upstream_token_endpoint\n            == \"https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token\"\n        )\n        assert (\n            provider._upstream_revocation_endpoint is None\n        )  # Azure doesn't support revocation\n\n    def test_special_tenant_values(self, memory_storage: MemoryStore):\n        \"\"\"Test that special tenant values are accepted.\"\"\"\n        # Test with \"organizations\"\n        provider1 = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"organizations\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        parsed = urlparse(provider1._upstream_authorization_endpoint)\n        assert \"/organizations/\" in parsed.path\n\n        # Test with \"consumers\"\n        provider2 = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"consumers\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        parsed = urlparse(provider2._upstream_authorization_endpoint)\n        assert \"/consumers/\" in parsed.path\n\n    def test_azure_specific_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test handling of custom API scope formats.\"\"\"\n        # Test that the provider accepts custom API scopes without error\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\n                \"read\",\n                \"write\",\n                \"admin\",\n            ],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Provider should initialize successfully with these scopes\n        assert provider is not None\n        # Scopes are stored unprefixed for token validation\n        # (Azure returns unprefixed scopes in JWT tokens)\n        assert provider._token_validator.required_scopes == [\n            \"read\",\n            \"write\",\n            \"admin\",\n        ]\n\n    def test_init_does_not_require_api_client_id_anymore(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"API client ID is no longer required; audience is client_id.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        assert provider is not None\n\n    def test_init_with_custom_audience_uses_jwt_verifier(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"When audience is provided, JWTVerifier is configured with JWKS and issuer.\"\"\"\n        from fastmcp.server.auth.providers.jwt import JWTVerifier\n\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"my-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\".default\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert provider._token_validator is not None\n        assert isinstance(provider._token_validator, JWTVerifier)\n        verifier = provider._token_validator\n        assert verifier.jwks_uri is not None\n        assert verifier.jwks_uri.startswith(\n            \"https://login.microsoftonline.com/my-tenant/discovery/v2.0/keys\"\n        )\n        assert verifier.issuer == \"https://login.microsoftonline.com/my-tenant/v2.0\"\n        assert verifier.audience == \"test_client\"\n        # Scopes are stored unprefixed for token validation\n        # (Azure returns unprefixed scopes like \".default\" in JWT tokens)\n        assert verifier.required_scopes == [\".default\"]\n\n    async def test_authorize_filters_resource_and_stores_unprefixed_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"authorize() should drop resource parameter and store unprefixed scopes for MCP clients.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"common\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\", \"write\"],\n            base_url=\"https://srv.example\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        await provider.register_client(\n            OAuthClientInformationFull(\n                client_id=\"dummy\",\n                client_secret=\"secret\",\n                redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n            )\n        )\n\n        client = OAuthClientInformationFull(\n            client_id=\"dummy\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\n                \"read\",\n                \"write\",\n            ],  # Client sends unprefixed scopes (from PRM which advertises unprefixed)\n            state=\"abc\",\n            code_challenge=\"xyz\",\n            resource=\"https://should.be.ignored\",\n        )\n\n        url = await provider.authorize(client, params)\n\n        # Extract transaction ID from consent redirect\n        parsed = urlparse(url)\n        qs = parse_qs(parsed.query)\n        assert \"txn_id\" in qs, \"Should redirect to consent page with transaction ID\"\n        txn_id = qs[\"txn_id\"][0]\n\n        # Verify transaction stores UNPREFIXED scopes for MCP clients\n        transaction = await provider._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert \"read\" in transaction.scopes\n        assert \"write\" in transaction.scopes\n        # Azure provider filters resource parameter (not stored in transaction)\n        assert transaction.resource is None\n\n        # Verify the upstream Azure URL will have PREFIXED scopes\n        upstream_url = provider._build_upstream_authorize_url(\n            txn_id, transaction.model_dump()\n        )\n        assert (\n            \"api%3A%2F%2Fmy-api%2Fread\" in upstream_url\n            or \"api://my-api/read\" in upstream_url\n        )\n        assert (\n            \"api%3A%2F%2Fmy-api%2Fwrite\" in upstream_url\n            or \"api://my-api/write\" in upstream_url\n        )\n\n    async def test_authorize_appends_additional_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"authorize() should append additional_authorize_scopes to the authorization request.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"common\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            base_url=\"https://srv.example\",\n            additional_authorize_scopes=[\"Mail.Read\", \"User.Read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        await provider.register_client(\n            OAuthClientInformationFull(\n                client_id=\"dummy\",\n                client_secret=\"secret\",\n                redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n            )\n        )\n\n        client = OAuthClientInformationFull(\n            client_id=\"dummy\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\"],  # Client sends unprefixed scopes\n            state=\"abc\",\n            code_challenge=\"xyz\",\n        )\n\n        url = await provider.authorize(client, params)\n\n        # Extract transaction ID from consent redirect\n        parsed = urlparse(url)\n        qs = parse_qs(parsed.query)\n        assert \"txn_id\" in qs, \"Should redirect to consent page with transaction ID\"\n        txn_id = qs[\"txn_id\"][0]\n\n        # Verify transaction stores ONLY MCP scopes (unprefixed)\n        # additional_authorize_scopes are NOT stored in transaction\n        transaction = await provider._transaction_store.get(key=txn_id)\n        assert transaction is not None\n        assert \"read\" in transaction.scopes\n        assert \"Mail.Read\" not in transaction.scopes  # Not in transaction\n        assert \"User.Read\" not in transaction.scopes  # Not in transaction\n\n        # Verify upstream URL includes both MCP scopes (prefixed) AND additional Graph scopes\n        upstream_url = provider._build_upstream_authorize_url(\n            txn_id, transaction.model_dump()\n        )\n        assert (\n            \"api%3A%2F%2Fmy-api%2Fread\" in upstream_url\n            or \"api://my-api/read\" in upstream_url\n        )\n        assert \"Mail.Read\" in upstream_url\n        assert \"User.Read\" in upstream_url\n\n    def test_base_authority_defaults_to_public_cloud(self, memory_storage: MemoryStore):\n        \"\"\"Test that base_authority defaults to login.microsoftonline.com.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://login.microsoftonline.com/test-tenant/oauth2/v2.0/authorize\"\n        )\n        assert (\n            provider._upstream_token_endpoint\n            == \"https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token\"\n        )\n        assert isinstance(provider._token_validator, JWTVerifier)\n        assert (\n            provider._token_validator.issuer\n            == \"https://login.microsoftonline.com/test-tenant/v2.0\"\n        )\n        assert (\n            provider._token_validator.jwks_uri\n            == \"https://login.microsoftonline.com/test-tenant/discovery/v2.0/keys\"\n        )\n\n    def test_base_authority_azure_government(self, memory_storage: MemoryStore):\n        \"\"\"Test Azure Government endpoints with login.microsoftonline.us.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"gov-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            base_authority=\"login.microsoftonline.us\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://login.microsoftonline.us/gov-tenant-id/oauth2/v2.0/authorize\"\n        )\n        assert (\n            provider._upstream_token_endpoint\n            == \"https://login.microsoftonline.us/gov-tenant-id/oauth2/v2.0/token\"\n        )\n        assert isinstance(provider._token_validator, JWTVerifier)\n        assert (\n            provider._token_validator.issuer\n            == \"https://login.microsoftonline.us/gov-tenant-id/v2.0\"\n        )\n        assert (\n            provider._token_validator.jwks_uri\n            == \"https://login.microsoftonline.us/gov-tenant-id/discovery/v2.0/keys\"\n        )\n\n    def test_base_authority_from_parameter(self, memory_storage: MemoryStore):\n        \"\"\"Test that base_authority can be set via parameter.\"\"\"\n        provider = AzureProvider(\n            client_id=\"env-client-id\",\n            client_secret=\"env-secret\",\n            tenant_id=\"env-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            base_authority=\"login.microsoftonline.us\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/authorize\"\n        )\n        assert (\n            provider._upstream_token_endpoint\n            == \"https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/token\"\n        )\n        assert isinstance(provider._token_validator, JWTVerifier)\n        assert (\n            provider._token_validator.issuer\n            == \"https://login.microsoftonline.us/env-tenant-id/v2.0\"\n        )\n        assert (\n            provider._token_validator.jwks_uri\n            == \"https://login.microsoftonline.us/env-tenant-id/discovery/v2.0/keys\"\n        )\n\n    def test_base_authority_with_special_tenant_values(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that base_authority works with special tenant values like 'organizations'.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"organizations\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            base_authority=\"login.microsoftonline.us\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        parsed = urlparse(provider._upstream_authorization_endpoint)\n        assert parsed.netloc == \"login.microsoftonline.us\"\n        assert \"/organizations/\" in parsed.path\n\n    def test_prepare_scopes_for_upstream_refresh_basic_prefixing(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that unprefixed scopes are correctly prefixed for Azure token refresh.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\", \"write\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Unprefixed scopes from storage should be prefixed\n        result = provider._prepare_scopes_for_upstream_refresh([\"read\", \"write\"])\n\n        assert \"api://my-api/read\" in result\n        assert \"api://my-api/write\" in result\n        assert \"offline_access\" in result  # Auto-included for refresh tokens\n        assert len(result) == 3\n\n    def test_prepare_scopes_for_upstream_refresh_already_prefixed(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that already-prefixed scopes remain unchanged.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Already prefixed scopes should pass through unchanged\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"api://my-api/read\", \"api://other-api/admin\"]\n        )\n\n        assert \"api://my-api/read\" in result\n        assert \"api://other-api/admin\" in result\n        assert \"offline_access\" in result  # Auto-included for refresh tokens\n        assert len(result) == 3\n\n    def test_prepare_scopes_for_upstream_refresh_with_additional_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that only OIDC scopes from additional_authorize_scopes are added.\n\n        Azure only allows ONE resource per token request (AADSTS28000), so\n        non-OIDC scopes like User.Read are excluded from refresh requests.\n        \"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\n                \"User.Read\",  # Not OIDC - excluded\n                \"openid\",\n                \"profile\",\n                \"offline_access\",\n            ],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Base scopes should be prefixed, only OIDC scopes appended\n        result = provider._prepare_scopes_for_upstream_refresh([\"read\", \"write\"])\n\n        assert \"api://my-api/read\" in result\n        assert \"api://my-api/write\" in result\n        assert \"User.Read\" not in result  # Not OIDC, excluded\n        assert \"openid\" in result\n        assert \"profile\" in result\n        assert \"offline_access\" in result\n        assert len(result) == 5\n\n    def test_prepare_scopes_for_upstream_refresh_filters_duplicate_additional_scopes(\n        self,\n        memory_storage: MemoryStore,\n    ):\n        \"\"\"Test that accidentally stored additional_authorize_scopes are filtered out.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"User.Read\", \"openid\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # If additional scopes were accidentally stored, they should be filtered\n        # User.Read is not OIDC so won't be added\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"User.Read\", \"openid\"]\n        )\n\n        # Should have: api://my-api/read (prefixed) + openid + offline_access (OIDC scopes)\n        # User.Read is filtered from storage AND not added (not OIDC)\n        assert \"api://my-api/read\" in result\n        assert \"User.Read\" not in result  # Not OIDC\n        assert result.count(\"openid\") == 1\n        assert \"offline_access\" in result  # Auto-included and is OIDC\n        assert len(result) == 3\n\n    def test_prepare_scopes_for_upstream_refresh_mixed_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test mixed scenario with both prefixed and unprefixed scopes.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"openid\"],  # OIDC scope\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Mix of prefixed and unprefixed scopes\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"api://other-api/admin\", \"write\"]\n        )\n\n        assert \"api://my-api/read\" in result\n        assert \"api://other-api/admin\" in result  # Already prefixed, unchanged\n        assert \"api://my-api/write\" in result\n        assert \"openid\" in result\n        assert \"offline_access\" in result  # Auto-included\n        assert len(result) == 5\n\n    def test_prepare_scopes_for_upstream_refresh_scope_with_slash(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that scopes containing '/' are not prefixed.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Scopes with \"/\" should not be prefixed (already fully qualified)\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"https://graph.microsoft.com/.default\"]\n        )\n\n        assert \"api://my-api/read\" in result\n        assert (\n            \"https://graph.microsoft.com/.default\" in result\n        )  # Not prefixed (contains ://)\n\n    def test_prepare_scopes_for_upstream_refresh_empty_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test behavior with empty scopes list.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"User.Read\", \"openid\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Empty scopes should still add OIDC scopes (not User.Read)\n        result = provider._prepare_scopes_for_upstream_refresh([])\n\n        assert \"User.Read\" not in result  # Not OIDC\n        assert \"openid\" in result\n        assert \"offline_access\" in result  # Auto-included\n        assert len(result) == 2  # Only OIDC scopes: openid + offline_access\n\n    def test_prepare_scopes_for_upstream_refresh_no_additional_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test behavior when no additional_authorize_scopes are configured.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Should prefix base scopes, plus auto-added offline_access\n        result = provider._prepare_scopes_for_upstream_refresh([\"read\", \"write\"])\n\n        assert \"api://my-api/read\" in result\n        assert \"api://my-api/write\" in result\n        assert \"offline_access\" in result  # Auto-included\n        assert len(result) == 3\n\n    def test_prepare_scopes_for_upstream_refresh_deduplicates_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that duplicate scopes are deduplicated while preserving order.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"openid\", \"profile\"],  # OIDC scopes only\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Test with duplicate base scopes\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"write\", \"read\", \"openid\"]\n        )\n\n        # Should have deduplicated results in order (OIDC scopes added, offline_access auto-added)\n        assert result == [\n            \"api://my-api/read\",\n            \"api://my-api/write\",\n            \"openid\",\n            \"profile\",\n            \"offline_access\",\n        ]\n        assert len(result) == 5\n\n    def test_prepare_scopes_for_upstream_refresh_deduplicates_prefixed_variants(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that both prefixed and unprefixed variants are deduplicated.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Test with both prefixed and unprefixed variants of same scope\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"api://my-api/read\", \"write\"]\n        )\n\n        # Should deduplicate - first occurrence wins (api://my-api/read from \"read\")\n        assert \"api://my-api/read\" in result\n        assert \"api://my-api/write\" in result\n        assert \"offline_access\" in result  # Auto-included\n        # Should have 3 items (read deduplicated, plus offline_access)\n        assert len(result) == 3\n        assert result.count(\"api://my-api/read\") == 1\n"
  },
  {
    "path": "tests/server/auth/providers/test_azure_scopes.py",
    "content": "\"\"\"Tests for Azure provider scope handling, JWT verifier, and OBO integration.\"\"\"\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\n\nfrom fastmcp.server.auth.providers.azure import (\n    OIDC_SCOPES,\n    AzureJWTVerifier,\n    AzureProvider,\n)\nfrom fastmcp.server.auth.providers.jwt import RSAKeyPair\n\n\n@pytest.fixture\ndef memory_storage() -> MemoryStore:\n    \"\"\"Provide a MemoryStore for tests to avoid SQLite initialization on Windows.\"\"\"\n    return MemoryStore()\n\n\nclass TestOIDCScopeHandling:\n    \"\"\"Tests for OIDC scope handling in Azure provider.\n\n    Azure access tokens do NOT include OIDC scopes (openid, profile, email,\n    offline_access) in the `scp` claim - they're only used during authorization.\n    These tests verify that:\n    1. OIDC scopes are never prefixed with identifier_uri\n    2. OIDC scopes are filtered from token validation\n    3. OIDC scopes are still advertised to clients via valid_scopes\n    \"\"\"\n\n    def test_oidc_scopes_constant(self, memory_storage: MemoryStore):\n        \"\"\"Verify OIDC_SCOPES contains the standard OIDC scopes.\"\"\"\n        assert OIDC_SCOPES == {\"openid\", \"profile\", \"email\", \"offline_access\"}\n\n    def test_prefix_scopes_does_not_prefix_oidc_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that _prefix_scopes_for_azure never prefixes OIDC scopes.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # All OIDC scopes should pass through unchanged\n        result = provider._prefix_scopes_for_azure(\n            [\"openid\", \"profile\", \"email\", \"offline_access\"]\n        )\n\n        assert result == [\"openid\", \"profile\", \"email\", \"offline_access\"]\n\n    def test_prefix_scopes_mixed_oidc_and_custom(self, memory_storage: MemoryStore):\n        \"\"\"Test prefixing with a mix of OIDC and custom scopes.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        result = provider._prefix_scopes_for_azure(\n            [\"read\", \"openid\", \"write\", \"profile\"]\n        )\n\n        # Custom scopes should be prefixed, OIDC scopes should not\n        assert \"api://my-api/read\" in result\n        assert \"api://my-api/write\" in result\n        assert \"openid\" in result\n        assert \"profile\" in result\n        # Verify OIDC scopes are NOT prefixed\n        assert \"api://my-api/openid\" not in result\n        assert \"api://my-api/profile\" not in result\n\n    def test_prefix_scopes_dot_notation_gets_prefixed(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that dot-notation scopes get prefixed (use additional_authorize_scopes for Graph).\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Dot-notation scopes ARE prefixed - use additional_authorize_scopes for Graph\n        # or fully-qualified format like https://graph.microsoft.com/User.Read\n        result = provider._prefix_scopes_for_azure([\"my.scope\", \"admin.read\"])\n\n        assert result == [\"api://my-api/my.scope\", \"api://my-api/admin.read\"]\n\n    def test_prefix_scopes_fully_qualified_graph_not_prefixed(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that fully-qualified Graph scopes are not prefixed.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        result = provider._prefix_scopes_for_azure(\n            [\n                \"https://graph.microsoft.com/User.Read\",\n                \"https://graph.microsoft.com/Mail.Send\",\n            ]\n        )\n\n        # Fully-qualified URIs pass through unchanged\n        assert result == [\n            \"https://graph.microsoft.com/User.Read\",\n            \"https://graph.microsoft.com/Mail.Send\",\n        ]\n\n    def test_required_scopes_with_oidc_filters_validation(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that OIDC scopes in required_scopes are filtered from token validation.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\", \"openid\", \"profile\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Token validator should only require non-OIDC scopes\n        assert provider._token_validator.required_scopes == [\"read\"]\n\n    def test_required_scopes_all_oidc_raises_value_error(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that providing only OIDC scopes raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"at least one non-OIDC scope\"):\n            AzureProvider(\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                tenant_id=\"test-tenant\",\n                base_url=\"https://myserver.com\",\n                identifier_uri=\"api://my-api\",\n                required_scopes=[\"openid\", \"profile\"],\n                jwt_signing_key=\"test-secret\",\n                client_storage=memory_storage,\n            )\n\n    def test_empty_required_scopes_raises_value_error(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that providing empty required_scopes raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"at least one non-OIDC scope\"):\n            AzureProvider(\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                tenant_id=\"test-tenant\",\n                base_url=\"https://myserver.com\",\n                identifier_uri=\"api://my-api\",\n                required_scopes=[],\n                jwt_signing_key=\"test-secret\",\n                client_storage=memory_storage,\n            )\n\n    @pytest.mark.parametrize(\n        \"scopes\",\n        [\n            [\"offline_access\"],\n            [\"openid\", \"email\", \"profile\", \"offline_access\"],\n            [\"email\"],\n        ],\n    )\n    def test_only_oidc_scopes_raises_value_error(\n        self, memory_storage: MemoryStore, scopes: list[str]\n    ):\n        \"\"\"Test that various OIDC-only scope combinations raise ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"at least one non-OIDC scope\"):\n            AzureProvider(\n                client_id=\"test_client\",\n                client_secret=\"test_secret\",\n                tenant_id=\"test-tenant\",\n                base_url=\"https://myserver.com\",\n                required_scopes=scopes,\n                jwt_signing_key=\"test-secret\",\n                client_storage=memory_storage,\n            )\n\n    def test_valid_scopes_includes_oidc_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test that valid_scopes advertises OIDC scopes to clients.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\", \"openid\", \"profile\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # required_scopes (used for validation) excludes OIDC scopes\n        assert provider.required_scopes == [\"read\"]\n        # But valid_scopes (advertised to clients) includes all scopes\n        assert provider.client_registration_options is not None\n        assert provider.client_registration_options.valid_scopes == [\n            \"read\",\n            \"openid\",\n            \"profile\",\n        ]\n\n    def test_prepare_scopes_for_refresh_handles_oidc_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that token refresh correctly handles OIDC scopes.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Simulate stored scopes that include OIDC scopes\n        result = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"openid\", \"profile\"]\n        )\n\n        # Custom scope should be prefixed, OIDC scopes should not\n        assert \"api://my-api/read\" in result\n        assert \"openid\" in result\n        assert \"profile\" in result\n        assert \"api://my-api/openid\" not in result\n        assert \"api://my-api/profile\" not in result\n\n\nclass TestAzureTokenExchangeScopes:\n    \"\"\"Tests for Azure provider's token exchange scope handling.\n\n    Azure requires scopes to be sent during the authorization code exchange.\n    The provider overrides _prepare_scopes_for_token_exchange to return\n    properly prefixed scopes.\n    \"\"\"\n\n    def test_prepare_scopes_returns_prefixed_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test that _prepare_scopes_for_token_exchange returns prefixed scopes.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\", \"write\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        scopes = provider._prepare_scopes_for_token_exchange([\"read\", \"write\"])\n        assert len(scopes) > 0\n        assert \"api://my-api/read\" in scopes\n        assert \"api://my-api/write\" in scopes\n\n    def test_prepare_scopes_includes_additional_oidc_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that _prepare_scopes_for_token_exchange includes OIDC scopes.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"openid\", \"profile\", \"offline_access\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        scopes = provider._prepare_scopes_for_token_exchange([\"read\"])\n        assert len(scopes) > 0\n        assert \"api://my-api/read\" in scopes\n        assert \"openid\" in scopes\n        assert \"profile\" in scopes\n        assert \"offline_access\" in scopes\n\n    def test_prepare_scopes_excludes_other_api_scopes(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test token exchange excludes other API scopes (Azure AADSTS28000).\n\n        Azure only allows ONE resource per token exchange. Other API scopes\n        are requested during authorization but excluded from token exchange.\n        \"\"\"\n        provider = AzureProvider(\n            client_id=\"00000000-1111-2222-3333-444444444444\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"user_impersonation\"],\n            additional_authorize_scopes=[\n                \"openid\",\n                \"profile\",\n                \"offline_access\",\n                \"api://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/user_impersonation\",\n                \"api://11111111-2222-3333-4444-555555555555/user_impersonation\",\n            ],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        scopes = provider._prepare_scopes_for_token_exchange([\"user_impersonation\"])\n        assert len(scopes) > 0\n        # Primary API scope should be prefixed with the provider's identifier_uri\n        assert \"api://00000000-1111-2222-3333-444444444444/user_impersonation\" in scopes\n        # OIDC scopes should be included\n        assert \"openid\" in scopes\n        assert \"profile\" in scopes\n        assert \"offline_access\" in scopes\n        # Other API scopes should NOT be included (Azure multi-resource limitation)\n        assert not any(\"api://aaaaaaaa\" in s for s in scopes)\n        assert not any(\"api://11111111\" in s for s in scopes)\n\n    def test_prepare_scopes_deduplicates_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test that duplicate scopes are deduplicated.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\"],\n            additional_authorize_scopes=[\"api://my-api/read\", \"openid\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Pass a scope that will be prefixed to match one in additional_authorize_scopes\n        scopes = provider._prepare_scopes_for_token_exchange([\"read\"])\n        assert len(scopes) > 0\n        # Should be deduplicated - api://my-api/read appears only once\n        assert scopes.count(\"api://my-api/read\") == 1\n        assert \"openid\" in scopes\n\n    def test_extra_token_params_does_not_contain_scope(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that extra_token_params doesn't contain scope to avoid TypeError.\n\n        Previously, Azure provider set extra_token_params={\"scope\": ...} during init.\n        This caused a TypeError in exchange_refresh_token because it passes both\n        scope=... AND **self._extra_token_params, resulting in:\n        \"got multiple values for keyword argument 'scope'\"\n\n        The fix uses the _prepare_scopes_for_token_exchange hook instead.\n        \"\"\"\n        provider = AzureProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            tenant_id=\"test-tenant\",\n            base_url=\"https://myserver.com\",\n            identifier_uri=\"api://my-api\",\n            required_scopes=[\"read\", \"write\"],\n            additional_authorize_scopes=[\"openid\", \"profile\", \"offline_access\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # extra_token_params should NOT contain \"scope\" to avoid TypeError during refresh\n        assert \"scope\" not in provider._extra_token_params\n\n        # Instead, scopes should be provided via the hook methods\n        exchange_scopes = provider._prepare_scopes_for_token_exchange([\"read\", \"write\"])\n        assert len(exchange_scopes) > 0\n\n        refresh_scopes = provider._prepare_scopes_for_upstream_refresh(\n            [\"read\", \"write\"]\n        )\n        assert len(refresh_scopes) > 0\n\n\nclass TestAzureJWTVerifier:\n    \"\"\"Tests for AzureJWTVerifier pre-configured JWT verifier.\"\"\"\n\n    def test_auto_configures_from_client_and_tenant(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"access_as_user\"],\n        )\n        assert (\n            verifier.jwks_uri\n            == \"https://login.microsoftonline.com/my-tenant-id/discovery/v2.0/keys\"\n        )\n        assert verifier.issuer == \"https://login.microsoftonline.com/my-tenant-id/v2.0\"\n        assert verifier.audience == \"my-client-id\"\n        assert verifier.algorithm == \"RS256\"\n        assert verifier.required_scopes == [\"access_as_user\"]\n\n    async def test_validates_short_form_scopes(self):\n        key_pair = RSAKeyPair.generate()\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"access_as_user\"],\n        )\n        # Override to use our test key instead of JWKS\n        verifier.public_key = key_pair.public_key\n        verifier.jwks_uri = None\n\n        token = key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://login.microsoftonline.com/my-tenant-id/v2.0\",\n            audience=\"my-client-id\",\n            additional_claims={\"scp\": \"access_as_user\"},\n        )\n        result = await verifier.load_access_token(token)\n        assert result is not None\n        assert \"access_as_user\" in result.scopes\n\n    def test_scopes_supported_returns_prefixed_form(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"read\", \"write\"],\n        )\n        assert verifier.scopes_supported == [\n            \"api://my-client-id/read\",\n            \"api://my-client-id/write\",\n        ]\n\n    def test_already_prefixed_scopes_pass_through(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"api://my-client-id/read\"],\n        )\n        assert verifier.scopes_supported == [\"api://my-client-id/read\"]\n\n    def test_oidc_scopes_not_prefixed(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"openid\", \"read\"],\n        )\n        assert verifier.scopes_supported == [\"openid\", \"api://my-client-id/read\"]\n\n    def test_custom_identifier_uri(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"read\"],\n            identifier_uri=\"api://custom-uri\",\n        )\n        assert verifier.scopes_supported == [\"api://custom-uri/read\"]\n\n    def test_custom_base_authority_for_gov_cloud(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"read\"],\n            base_authority=\"login.microsoftonline.us\",\n        )\n        assert (\n            verifier.jwks_uri\n            == \"https://login.microsoftonline.us/my-tenant-id/discovery/v2.0/keys\"\n        )\n        assert verifier.issuer == \"https://login.microsoftonline.us/my-tenant-id/v2.0\"\n\n    def test_scopes_supported_empty_when_no_required_scopes(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"my-tenant-id\",\n        )\n        assert verifier.scopes_supported == []\n\n    def test_default_identifier_uri_uses_client_id(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"abc-123\",\n            tenant_id=\"my-tenant-id\",\n            required_scopes=[\"read\"],\n        )\n        assert verifier.scopes_supported == [\"api://abc-123/read\"]\n\n    def test_multi_tenant_organizations_skips_issuer(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"organizations\",\n        )\n        assert verifier.issuer is None\n\n    def test_multi_tenant_consumers_skips_issuer(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"consumers\",\n        )\n        assert verifier.issuer is None\n\n    def test_multi_tenant_common_skips_issuer(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"common\",\n        )\n        assert verifier.issuer is None\n\n    def test_specific_tenant_sets_issuer(self):\n        verifier = AzureJWTVerifier(\n            client_id=\"my-client-id\",\n            tenant_id=\"12345678-1234-1234-1234-123456789012\",\n        )\n        assert (\n            verifier.issuer\n            == \"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0\"\n        )\n\n\nclass TestAzureOBOIntegration:\n    \"\"\"Tests for azure.identity OBO integration (get_obo_credential, EntraOBOToken).\"\"\"\n\n    async def test_get_obo_credential_returns_configured_credential(self):\n        \"\"\"Test that get_obo_credential returns a properly configured credential.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"test-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n        )\n\n        mock_credential = MagicMock()\n        with patch(\n            \"azure.identity.aio.OnBehalfOfCredential\", return_value=mock_credential\n        ) as mock_class:\n            credential = await provider.get_obo_credential(\n                user_assertion=\"user-token-123\"\n            )\n\n            mock_class.assert_called_once_with(\n                tenant_id=\"test-tenant-id\",\n                client_id=\"test-client-id\",\n                client_secret=\"test-client-secret\",\n                user_assertion=\"user-token-123\",\n                authority=\"https://login.microsoftonline.com\",\n            )\n            assert credential is mock_credential\n\n    async def test_get_obo_credential_caches_by_assertion(self):\n        \"\"\"Test that the same assertion returns the cached credential.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"test-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n        )\n\n        mock_credential = MagicMock()\n        with patch(\n            \"azure.identity.aio.OnBehalfOfCredential\", return_value=mock_credential\n        ) as mock_class:\n            first = await provider.get_obo_credential(user_assertion=\"same-token\")\n            second = await provider.get_obo_credential(user_assertion=\"same-token\")\n\n            assert first is second\n            mock_class.assert_called_once()\n\n    async def test_get_obo_credential_different_assertions_get_different_credentials(\n        self,\n    ):\n        \"\"\"Test that different assertions produce different credentials.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"test-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n        )\n\n        creds = [MagicMock(), MagicMock()]\n        with patch(\"azure.identity.aio.OnBehalfOfCredential\", side_effect=creds):\n            first = await provider.get_obo_credential(user_assertion=\"token-a\")\n            second = await provider.get_obo_credential(user_assertion=\"token-b\")\n\n            assert first is not second\n            assert first is creds[0]\n            assert second is creds[1]\n\n    async def test_get_obo_credential_evicts_oldest_when_over_capacity(self):\n        \"\"\"Test that credentials are evicted LRU-style when cache is full.\"\"\"\n        from unittest.mock import AsyncMock, MagicMock, patch\n\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"test-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n        )\n        provider._obo_max_credentials = 2\n\n        creds = [MagicMock(close=AsyncMock()) for _ in range(3)]\n        with patch(\"azure.identity.aio.OnBehalfOfCredential\", side_effect=creds):\n            await provider.get_obo_credential(user_assertion=\"token-1\")\n            await provider.get_obo_credential(user_assertion=\"token-2\")\n            await provider.get_obo_credential(user_assertion=\"token-3\")\n\n            assert len(provider._obo_credentials) == 2\n            creds[0].close.assert_awaited_once()\n            # token-1's credential was evicted\n            assert (\n                await provider.get_obo_credential(user_assertion=\"token-2\") is creds[1]\n            )\n            assert (\n                await provider.get_obo_credential(user_assertion=\"token-3\") is creds[2]\n            )\n\n    async def test_close_obo_credentials(self):\n        \"\"\"Test that close_obo_credentials closes all cached credentials.\"\"\"\n        from unittest.mock import AsyncMock, MagicMock, patch\n\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"test-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            jwt_signing_key=\"test-secret\",\n        )\n\n        creds = [MagicMock(close=AsyncMock()) for _ in range(2)]\n        with patch(\"azure.identity.aio.OnBehalfOfCredential\", side_effect=creds):\n            await provider.get_obo_credential(user_assertion=\"token-a\")\n            await provider.get_obo_credential(user_assertion=\"token-b\")\n\n        await provider.close_obo_credentials()\n\n        assert len(provider._obo_credentials) == 0\n        for cred in creds:\n            cred.close.assert_awaited_once()\n\n    async def test_get_obo_credential_with_custom_authority(self):\n        \"\"\"Test that get_obo_credential uses custom base_authority.\"\"\"\n        from unittest.mock import MagicMock, patch\n\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"gov-tenant-id\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            base_authority=\"login.microsoftonline.us\",\n            jwt_signing_key=\"test-secret\",\n        )\n\n        mock_credential = MagicMock()\n        with patch(\n            \"azure.identity.aio.OnBehalfOfCredential\", return_value=mock_credential\n        ) as mock_class:\n            await provider.get_obo_credential(user_assertion=\"user-token\")\n\n            call_kwargs = mock_class.call_args[1]\n            assert call_kwargs[\"authority\"] == \"https://login.microsoftonline.us\"\n\n    def test_tenant_and_authority_stored_as_attributes(self):\n        \"\"\"Test that tenant_id and base_authority are stored for OBO credential creation.\"\"\"\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            tenant_id=\"my-tenant\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\"],\n            base_authority=\"login.microsoftonline.us\",\n            jwt_signing_key=\"test-secret\",\n        )\n\n        assert provider._tenant_id == \"my-tenant\"\n        assert provider._base_authority == \"login.microsoftonline.us\"\n\n    def test_entra_obo_token_is_importable(self):\n        \"\"\"Test that EntraOBOToken can be imported.\"\"\"\n        from fastmcp.server.auth.providers.azure import EntraOBOToken\n\n        assert EntraOBOToken is not None\n\n    def test_entra_obo_token_creates_dependency(self):\n        \"\"\"Test that EntraOBOToken creates a dependency with scopes.\"\"\"\n        from fastmcp.server.auth.providers.azure import EntraOBOToken, _EntraOBOToken\n\n        dep = EntraOBOToken([\"https://graph.microsoft.com/User.Read\"])\n        assert isinstance(dep, _EntraOBOToken)\n        assert dep.scopes == [\"https://graph.microsoft.com/User.Read\"]\n\n    def test_entra_obo_token_is_dependency_instance(self):\n        \"\"\"Test that EntraOBOToken is a Dependency instance.\"\"\"\n        from fastmcp.dependencies import Dependency\n        from fastmcp.server.auth.providers.azure import _EntraOBOToken\n\n        dep = _EntraOBOToken([\"scope\"])\n        assert isinstance(dep, Dependency)\n"
  },
  {
    "path": "tests/server/auth/providers/test_descope.py",
    "content": "\"\"\"Tests for Descope OAuth provider.\"\"\"\n\nimport os\nfrom unittest.mock import patch\n\nimport httpx\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.auth.providers.descope import DescopeProvider\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_async\n\n\nclass TestDescopeProvider:\n    \"\"\"Test Descope OAuth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self):\n        \"\"\"Test DescopeProvider initialization with explicit parameters.\"\"\"\n        provider = DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration\",\n            base_url=\"https://myserver.com\",\n        )\n\n        assert provider.project_id == \"P2abc123\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n        assert str(provider.descope_base_url) == \"https://api.descope.com\"\n\n    def test_environment_variable_loading(self):\n        \"\"\"Test that environment variables are loaded correctly.\"\"\"\n        # This test verifies that the provider can be created with environment variables\n        provider = DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration\",\n            base_url=\"http://env-server.com\",\n        )\n\n        # Should have loaded from environment\n        assert provider.project_id == \"P2env123\"\n        assert str(provider.base_url) == \"http://env-server.com/\"\n        assert str(provider.descope_base_url) == \"https://api.descope.com\"\n\n    def test_config_url_parsing(self):\n        \"\"\"Test that config_url is parsed correctly to extract base URL and project ID.\"\"\"\n        # Standard HTTPS URL\n        provider1 = DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration\",\n            base_url=\"https://myserver.com\",\n        )\n        assert str(provider1.descope_base_url) == \"https://api.descope.com\"\n        assert provider1.project_id == \"P2abc123\"\n\n        # HTTP URL (for local testing)\n        provider2 = DescopeProvider(\n            config_url=\"http://localhost:8080/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration\",\n            base_url=\"https://myserver.com\",\n        )\n        assert str(provider2.descope_base_url) == \"http://localhost:8080\"\n        assert provider2.project_id == \"P2abc123\"\n\n        # URL without .well-known/openid-configuration suffix\n        provider3 = DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2abc123/M123\",\n            base_url=\"https://myserver.com\",\n        )\n        assert str(provider3.descope_base_url) == \"https://api.descope.com\"\n        assert provider3.project_id == \"P2abc123\"\n\n    def test_requires_config_url_or_project_id_and_descope_base_url(self):\n        \"\"\"Test that either config_url or both project_id and descope_base_url are required.\"\"\"\n        # Should raise error when neither API is provided\n        with pytest.raises(ValueError, match=\"Either config_url\"):\n            DescopeProvider(\n                base_url=\"https://myserver.com\",\n            )\n\n    def test_backwards_compatibility_with_project_id_and_descope_base_url(self):\n        \"\"\"Test backwards compatibility with old API using project_id and descope_base_url.\"\"\"\n        provider = DescopeProvider(\n            project_id=\"P2abc123\",\n            descope_base_url=\"https://api.descope.com\",\n            base_url=\"https://myserver.com\",\n        )\n\n        assert provider.project_id == \"P2abc123\"\n        assert str(provider.descope_base_url) == \"https://api.descope.com\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n        # Check that JWT verifier uses the old issuer format\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert (\n            provider.token_verifier.issuer == \"https://api.descope.com/v1/apps/P2abc123\"\n        )\n        assert (\n            provider.token_verifier.jwks_uri\n            == \"https://api.descope.com/P2abc123/.well-known/jwks.json\"\n        )\n\n    def test_backwards_compatibility_descope_base_url_without_scheme(self):\n        \"\"\"Test that descope_base_url without scheme gets https:// prefix added.\"\"\"\n        provider = DescopeProvider(\n            project_id=\"P2abc123\",\n            descope_base_url=\"api.descope.com\",\n            base_url=\"https://myserver.com\",\n        )\n\n        assert str(provider.descope_base_url) == \"https://api.descope.com\"\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert (\n            provider.token_verifier.issuer == \"https://api.descope.com/v1/apps/P2abc123\"\n        )\n\n    def test_config_url_takes_precedence_over_old_api(self):\n        \"\"\"Test that config_url takes precedence when both APIs are provided.\"\"\"\n        provider = DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2new123/M123/.well-known/openid-configuration\",\n            project_id=\"P2old123\",  # Should be ignored\n            descope_base_url=\"https://old.descope.com\",  # Should be ignored\n            base_url=\"https://myserver.com\",\n        )\n\n        # Should use values from config_url, not the old API\n        assert provider.project_id == \"P2new123\"\n        assert str(provider.descope_base_url) == \"https://api.descope.com\"\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert (\n            provider.token_verifier.issuer\n            == \"https://api.descope.com/v1/apps/agentic/P2new123/M123\"\n        )\n\n    def test_jwt_verifier_configured_correctly(self):\n        \"\"\"Test that JWT verifier is configured correctly.\"\"\"\n        config_url = \"https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration\"\n        issuer_url = \"https://api.descope.com/v1/apps/agentic/P2abc123/M123\"\n\n        provider = DescopeProvider(\n            config_url=config_url,\n            base_url=\"https://myserver.com\",\n        )\n\n        # Check that JWT verifier uses the correct endpoints\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert (\n            provider.token_verifier.jwks_uri\n            == \"https://api.descope.com/P2abc123/.well-known/jwks.json\"\n        )\n        assert provider.token_verifier.issuer == issuer_url\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.audience == \"P2abc123\"\n\n    def test_required_scopes_support(self):\n        \"\"\"Test that required_scopes are supported and passed to JWT verifier.\"\"\"\n        provider = DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"read\", \"write\"],\n        )\n\n        # Check that required_scopes are set on the token verifier\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.required_scopes == [\"read\", \"write\"]\n\n    def test_required_scopes_with_old_api(self):\n        \"\"\"Test that required_scopes work with the old API (project_id + descope_base_url).\"\"\"\n        provider = DescopeProvider(\n            project_id=\"P2abc123\",\n            descope_base_url=\"https://api.descope.com\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"openid\", \"email\"],\n        )\n\n        # Check that required_scopes are set on the token verifier\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.required_scopes == [\"openid\", \"email\"]\n\n    def test_required_scopes_from_env(self):\n        \"\"\"Test that required_scopes can be set via environment variable.\"\"\"\n        with patch.dict(\n            os.environ,\n            {\n                \"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL\": \"https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration\",\n                \"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL\": \"https://envserver.com\",\n                \"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_REQUIRED_SCOPES\": \"read,write\",\n            },\n        ):\n            provider = DescopeProvider(\n                config_url=\"https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration\",\n                base_url=\"https://envserver.com\",\n                required_scopes=[\"read\", \"write\"],\n            )\n\n            assert isinstance(provider.token_verifier, JWTVerifier)\n            assert provider.token_verifier.required_scopes == [\"read\", \"write\"]\n\n\n@pytest.fixture\nasync def mcp_server_url():\n    \"\"\"Start Descope server.\"\"\"\n    mcp = FastMCP(\n        auth=DescopeProvider(\n            config_url=\"https://api.descope.com/v1/apps/agentic/P2test123/M123/.well-known/openid-configuration\",\n            base_url=\"http://localhost:4321\",\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    async with run_server_async(mcp, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\ndef client_with_headless_oauth(mcp_server_url: str) -> Client:\n    \"\"\"Client with headless OAuth that bypasses browser interaction.\"\"\"\n    return Client(\n        transport=StreamableHttpTransport(mcp_server_url),\n        auth=HeadlessOAuth(mcp_url=mcp_server_url),\n    )\n\n\nclass TestDescopeProviderIntegration:\n    async def test_unauthorized_access(self, mcp_server_url: str):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url) as client:\n                tools = await client.list_tools()  # noqa: F841\n\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    # async def test_authorized_access(self, client_with_headless_oauth: Client):\n    #     async with client_with_headless_oauth:\n    #         tools = await client_with_headless_oauth.list_tools()\n    #     assert tools is not None\n    #     assert len(tools) > 0\n    #     assert \"add\" in tools\n"
  },
  {
    "path": "tests/server/auth/providers/test_discord.py",
    "content": "\"\"\"Tests for Discord OAuth provider.\"\"\"\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\n\nfrom fastmcp.server.auth.providers.discord import DiscordProvider, DiscordTokenVerifier\n\n\n@pytest.fixture\ndef memory_storage() -> MemoryStore:\n    \"\"\"Provide a MemoryStore for tests to avoid SQLite initialization on Windows.\"\"\"\n    return MemoryStore()\n\n\nclass TestDiscordProvider:\n    \"\"\"Test Discord OAuth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self, memory_storage: MemoryStore):\n        \"\"\"Test DiscordProvider initialization with explicit parameters.\"\"\"\n        provider = DiscordProvider(\n            client_id=\"env_client_id\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"email\", \"identify\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert provider._upstream_client_id == \"env_client_id\"\n        assert provider._upstream_client_secret is not None\n        assert provider._upstream_client_secret.get_secret_value() == \"GOCSPX-test123\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n    def test_init_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        provider = DiscordProvider(\n            client_id=\"env_client_id\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check defaults\n        assert provider._redirect_path == \"/auth/callback\"\n\n    def test_oauth_endpoints_configured_correctly(self, memory_storage: MemoryStore):\n        \"\"\"Test that OAuth endpoints are configured correctly.\"\"\"\n        provider = DiscordProvider(\n            client_id=\"env_client_id\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check that endpoints use Discord's OAuth2 endpoints\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://discord.com/oauth2/authorize\"\n        )\n        assert (\n            provider._upstream_token_endpoint == \"https://discord.com/api/oauth2/token\"\n        )\n        # Discord provider doesn't currently set a revocation endpoint\n        assert provider._upstream_revocation_endpoint is None\n\n    def test_discord_specific_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test handling of Discord-specific scope formats.\"\"\"\n        # Just test that the provider accepts Discord-specific scopes without error\n        provider = DiscordProvider(\n            client_id=\"env_client_id\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\n                \"identify\",\n                \"email\",\n            ],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Provider should initialize successfully with these scopes\n        assert provider is not None\n\n    def test_token_verifier_is_bound_to_provider_client_id(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test DiscordProvider binds token verifier to the configured client ID.\"\"\"\n        provider = DiscordProvider(\n            client_id=\"expected-client-id\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        verifier = provider._token_validator\n        assert isinstance(verifier, DiscordTokenVerifier)\n        assert verifier.expected_client_id == \"expected-client-id\"\n\n\nclass TestDiscordTokenVerifier:\n    \"\"\"Test DiscordTokenVerifier behavior.\"\"\"\n\n    async def test_rejects_token_from_different_discord_application(self):\n        \"\"\"Token must be bound to configured Discord client_id.\"\"\"\n        verifier = DiscordTokenVerifier(expected_client_id=\"expected-app-id\")\n\n        mock_client = AsyncMock()\n        token_info_response = MagicMock()\n        token_info_response.status_code = 200\n        token_info_response.json.return_value = {\n            \"application\": {\"id\": \"different-app-id\"},\n            \"user\": {\"id\": \"123\"},\n            \"scopes\": [\"identify\"],\n        }\n        mock_client.get.return_value = token_info_response\n\n        with patch(\n            \"fastmcp.server.auth.providers.discord.httpx.AsyncClient\"\n        ) as mock_client_class:\n            mock_client_class.return_value.__aenter__.return_value = mock_client\n            result = await verifier.verify_token(\"token\")\n\n        assert result is None\n"
  },
  {
    "path": "tests/server/auth/providers/test_github.py",
    "content": "\"\"\"Unit tests for GitHub OAuth provider.\"\"\"\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\n\nfrom fastmcp.server.auth.providers.github import (\n    GitHubProvider,\n    GitHubTokenVerifier,\n)\n\n\n@pytest.fixture\ndef memory_storage() -> MemoryStore:\n    \"\"\"Provide a MemoryStore for tests to avoid SQLite initialization on Windows.\"\"\"\n    return MemoryStore()\n\n\nclass TestGitHubProvider:\n    \"\"\"Test GitHubProvider initialization.\"\"\"\n\n    def test_init_with_explicit_params(self, memory_storage: MemoryStore):\n        \"\"\"Test initialization with explicit parameters.\"\"\"\n        provider = GitHubProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            base_url=\"https://example.com\",\n            redirect_path=\"/custom/callback\",\n            required_scopes=[\"user\", \"repo\"],\n            timeout_seconds=30,\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check that the provider was initialized correctly\n        assert provider._upstream_client_id == \"test_client\"\n        assert provider._upstream_client_secret is not None\n        assert provider._upstream_client_secret.get_secret_value() == \"test_secret\"\n        assert (\n            str(provider.base_url) == \"https://example.com/\"\n        )  # URLs get normalized with trailing slash\n        assert provider._redirect_path == \"/custom/callback\"\n\n    def test_init_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        provider = GitHubProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            base_url=\"https://example.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check defaults\n        assert provider._redirect_path == \"/auth/callback\"\n        # The required_scopes should be passed to the token verifier\n        assert provider._token_validator.required_scopes == [\"user\"]\n\n\nclass TestGitHubTokenVerifier:\n    \"\"\"Test GitHubTokenVerifier.\"\"\"\n\n    def test_init_with_custom_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test initialization with custom required scopes.\"\"\"\n        verifier = GitHubTokenVerifier(\n            required_scopes=[\"user\", \"repo\"],\n            timeout_seconds=30,\n        )\n\n        assert verifier.required_scopes == [\"user\", \"repo\"]\n        assert verifier.timeout_seconds == 30\n\n    def test_init_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test initialization with defaults.\"\"\"\n        verifier = GitHubTokenVerifier()\n\n        assert (\n            verifier.required_scopes == []\n        )  # Parent TokenVerifier sets empty list as default\n        assert verifier.timeout_seconds == 10\n\n    async def test_verify_token_github_api_failure(self):\n        \"\"\"Test token verification when GitHub API returns error.\"\"\"\n        verifier = GitHubTokenVerifier()\n\n        # Mock httpx.AsyncClient to simulate GitHub API failure\n        with patch(\"httpx.AsyncClient\") as mock_client_class:\n            mock_client = MagicMock()\n            mock_client_class.return_value.__aenter__.return_value = mock_client\n\n            # Simulate 401 response from GitHub\n            mock_response = MagicMock()\n            mock_response.status_code = 401\n            mock_response.text = \"Bad credentials\"\n            mock_client.get.return_value = mock_response\n\n            result = await verifier.verify_token(\"invalid_token\")\n            assert result is None\n\n    async def test_verify_token_success(self):\n        \"\"\"Test successful token verification.\"\"\"\n        verifier = GitHubTokenVerifier(required_scopes=[\"user\"])\n\n        # Mock the httpx.AsyncClient directly\n        mock_client = AsyncMock()\n\n        # Mock successful user API response\n        user_response = MagicMock()\n        user_response.status_code = 200\n        user_response.json.return_value = {\n            \"id\": 12345,\n            \"login\": \"testuser\",\n            \"name\": \"Test User\",\n            \"email\": \"test@example.com\",\n            \"avatar_url\": \"https://github.com/testuser.png\",\n        }\n\n        # Mock successful scopes API response\n        scopes_response = MagicMock()\n        scopes_response.headers = {\"x-oauth-scopes\": \"user,repo\"}\n\n        # Set up the mock client to return our responses\n        mock_client.get.side_effect = [user_response, scopes_response]\n\n        # Patch the AsyncClient context manager\n        with patch(\n            \"fastmcp.server.auth.providers.github.httpx.AsyncClient\"\n        ) as mock_client_class:\n            mock_client_class.return_value.__aenter__.return_value = mock_client\n\n            result = await verifier.verify_token(\"valid_token\")\n\n            assert result is not None\n            assert result.token == \"valid_token\"\n            assert result.client_id == \"12345\"\n            assert result.scopes == [\"user\", \"repo\"]\n            assert result.claims[\"login\"] == \"testuser\"\n            assert result.claims[\"name\"] == \"Test User\"\n\n\ndef _mock_github_success(mock_client: AsyncMock) -> None:\n    \"\"\"Configure *mock_client* to return a successful GitHub user + scopes response.\"\"\"\n    user_response = MagicMock()\n    user_response.status_code = 200\n    user_response.json.return_value = {\n        \"id\": 12345,\n        \"login\": \"testuser\",\n        \"name\": \"Test User\",\n        \"email\": \"test@example.com\",\n        \"avatar_url\": \"https://github.com/testuser.png\",\n    }\n\n    scopes_response = MagicMock()\n    scopes_response.status_code = 200\n    scopes_response.headers = {\"x-oauth-scopes\": \"user,repo\"}\n\n    mock_client.get.side_effect = [user_response, scopes_response]\n\n\ndef _mock_github_failure(mock_client: AsyncMock) -> None:\n    \"\"\"Configure *mock_client* to return a 401 GitHub response.\"\"\"\n    fail_response = MagicMock()\n    fail_response.status_code = 401\n    fail_response.text = \"Bad credentials\"\n    mock_client.get.return_value = fail_response\n\n\nclass TestGitHubTokenVerifierCaching:\n    \"\"\"Test caching behaviour on GitHubTokenVerifier.\"\"\"\n\n    def test_cache_disabled_by_default(self):\n        verifier = GitHubTokenVerifier()\n        assert not verifier._cache.enabled\n\n    def test_cache_enabled_with_ttl(self):\n        verifier = GitHubTokenVerifier(cache_ttl_seconds=300)\n        assert verifier._cache.enabled\n\n    async def test_cache_hit_avoids_second_api_call(self):\n        verifier = GitHubTokenVerifier(\n            required_scopes=[\"user\"],\n            cache_ttl_seconds=300,\n        )\n\n        mock_client = AsyncMock()\n\n        with patch(\n            \"fastmcp.server.auth.providers.github.httpx.AsyncClient\"\n        ) as mock_cls:\n            mock_cls.return_value.__aenter__.return_value = mock_client\n\n            _mock_github_success(mock_client)\n            result1 = await verifier.verify_token(\"tok-1\")\n            assert result1 is not None\n            assert mock_client.get.call_count == 2  # /user + /user/repos\n\n            result2 = await verifier.verify_token(\"tok-1\")\n            assert result2 is not None\n            assert result2.client_id == result1.client_id\n            assert mock_client.get.call_count == 2  # no additional calls\n\n    async def test_cache_disabled_makes_every_call(self):\n        verifier = GitHubTokenVerifier(\n            required_scopes=[\"user\"],\n            cache_ttl_seconds=0,\n        )\n\n        mock_client = AsyncMock()\n\n        with patch(\n            \"fastmcp.server.auth.providers.github.httpx.AsyncClient\"\n        ) as mock_cls:\n            mock_cls.return_value.__aenter__.return_value = mock_client\n\n            _mock_github_success(mock_client)\n            await verifier.verify_token(\"tok-1\")\n            assert mock_client.get.call_count == 2\n\n            _mock_github_success(mock_client)\n            await verifier.verify_token(\"tok-1\")\n            assert mock_client.get.call_count == 4\n\n    async def test_failures_are_not_cached(self):\n        verifier = GitHubTokenVerifier(cache_ttl_seconds=300)\n\n        mock_client = AsyncMock()\n\n        with patch(\n            \"fastmcp.server.auth.providers.github.httpx.AsyncClient\"\n        ) as mock_cls:\n            mock_cls.return_value.__aenter__.return_value = mock_client\n\n            _mock_github_failure(mock_client)\n            result1 = await verifier.verify_token(\"bad-tok\")\n            assert result1 is None\n\n            _mock_github_success(mock_client)\n            result2 = await verifier.verify_token(\"bad-tok\")\n            assert result2 is not None\n\n    async def test_cached_result_is_defensive_copy(self):\n        verifier = GitHubTokenVerifier(\n            required_scopes=[\"user\"],\n            cache_ttl_seconds=300,\n        )\n\n        mock_client = AsyncMock()\n\n        with patch(\n            \"fastmcp.server.auth.providers.github.httpx.AsyncClient\"\n        ) as mock_cls:\n            mock_cls.return_value.__aenter__.return_value = mock_client\n\n            _mock_github_success(mock_client)\n            result1 = await verifier.verify_token(\"tok-1\")\n            assert result1 is not None\n            result1.claims[\"login\"] = \"MUTATED\"\n\n            result2 = await verifier.verify_token(\"tok-1\")\n            assert result2 is not None\n            assert result2.claims[\"login\"] == \"testuser\"\n\n    async def test_scope_failure_skips_cache(self):\n        \"\"\"Token verified with fallback scopes (scope API failed) should not be cached.\"\"\"\n        verifier = GitHubTokenVerifier(cache_ttl_seconds=300)\n\n        mock_client = AsyncMock()\n\n        user_response = MagicMock()\n        user_response.status_code = 200\n        user_response.json.return_value = {\n            \"id\": 12345,\n            \"login\": \"testuser\",\n            \"name\": \"Test User\",\n            \"email\": \"test@example.com\",\n            \"avatar_url\": \"https://github.com/testuser.png\",\n        }\n\n        scopes_response = MagicMock()\n        scopes_response.status_code = 500\n        scopes_response.headers = {}\n\n        with patch(\n            \"fastmcp.server.auth.providers.github.httpx.AsyncClient\"\n        ) as mock_cls:\n            mock_cls.return_value.__aenter__.return_value = mock_client\n\n            mock_client.get.side_effect = [user_response, scopes_response]\n            result = await verifier.verify_token(\"tok-1\")\n            assert result is not None\n            # Should NOT be cached because scope response was not 200\n            assert not verifier._cache.enabled or len(verifier._cache._entries) == 0\n\n    def test_provider_passes_cache_params(self, memory_storage: MemoryStore):\n        provider = GitHubProvider(\n            client_id=\"cid\",\n            client_secret=\"csec\",\n            base_url=\"https://example.com\",\n            cache_ttl_seconds=120,\n            max_cache_size=500,\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        verifier = provider._token_validator\n        assert isinstance(verifier, GitHubTokenVerifier)\n        assert verifier._cache.enabled\n        assert verifier._cache._ttl == 120\n        assert verifier._cache._max_size == 500\n"
  },
  {
    "path": "tests/server/auth/providers/test_google.py",
    "content": "\"\"\"Tests for Google OAuth provider.\"\"\"\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\n\nfrom fastmcp.server.auth.providers.google import (\n    GOOGLE_SCOPE_ALIASES,\n    GoogleProvider,\n    GoogleTokenVerifier,\n    _normalize_google_scope,\n)\n\n\n@pytest.fixture\ndef memory_storage() -> MemoryStore:\n    \"\"\"Provide a MemoryStore for tests to avoid SQLite initialization on Windows.\"\"\"\n    return MemoryStore()\n\n\nclass TestGoogleProvider:\n    \"\"\"Test Google OAuth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self, memory_storage: MemoryStore):\n        \"\"\"Test GoogleProvider initialization with explicit parameters.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"openid\", \"email\", \"profile\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert provider._upstream_client_id == \"123456789.apps.googleusercontent.com\"\n        assert provider._upstream_client_secret is not None\n        assert provider._upstream_client_secret.get_secret_value() == \"GOCSPX-test123\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n    def test_init_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check defaults\n        assert provider._redirect_path == \"/auth/callback\"\n        # Google provider has [\"openid\"] as default but we can't easily verify without accessing internals\n\n    def test_oauth_endpoints_configured_correctly(self, memory_storage: MemoryStore):\n        \"\"\"Test that OAuth endpoints are configured correctly.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check that endpoints use Google's OAuth2 endpoints\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://accounts.google.com/o/oauth2/v2/auth\"\n        )\n        assert (\n            provider._upstream_token_endpoint == \"https://oauth2.googleapis.com/token\"\n        )\n        # Google provider doesn't currently set a revocation endpoint\n        assert provider._upstream_revocation_endpoint is None\n\n    def test_google_specific_scopes(self, memory_storage: MemoryStore):\n        \"\"\"Test handling of Google-specific scope formats.\"\"\"\n        # Just test that the provider accepts Google-specific scopes without error\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\n                \"openid\",\n                \"https://www.googleapis.com/auth/userinfo.email\",\n                \"https://www.googleapis.com/auth/userinfo.profile\",\n            ],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Provider should initialize successfully with these scopes\n        assert provider is not None\n\n    def test_extra_authorize_params_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test that Google-specific defaults are set for refresh token support.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Should have Google-specific defaults for refresh token support\n        assert provider._extra_authorize_params == {\n            \"access_type\": \"offline\",\n            \"prompt\": \"consent\",\n        }\n\n    def test_extra_authorize_params_override_defaults(\n        self, memory_storage: MemoryStore\n    ):\n        \"\"\"Test that user can override default extra authorize params.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            extra_authorize_params={\"prompt\": \"select_account\"},\n            client_storage=memory_storage,\n        )\n\n        # User override should replace the default\n        assert provider._extra_authorize_params[\"prompt\"] == \"select_account\"\n        # But other defaults should remain\n        assert provider._extra_authorize_params[\"access_type\"] == \"offline\"\n\n    def test_extra_authorize_params_add_new_params(self, memory_storage: MemoryStore):\n        \"\"\"Test that user can add additional authorize params.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            extra_authorize_params={\"login_hint\": \"user@example.com\"},\n            client_storage=memory_storage,\n        )\n\n        # New param should be added\n        assert provider._extra_authorize_params[\"login_hint\"] == \"user@example.com\"\n        # Defaults should still be present\n        assert provider._extra_authorize_params[\"access_type\"] == \"offline\"\n        assert provider._extra_authorize_params[\"prompt\"] == \"consent\"\n\n    def test_valid_scopes_passed_through(self, memory_storage: MemoryStore):\n        \"\"\"Test that valid_scopes is passed to OAuthProxy.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"openid\"],\n            valid_scopes=[\"openid\", \"email\", \"profile\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        reg_options = provider.client_registration_options\n        assert reg_options is not None\n        assert reg_options.valid_scopes is not None\n        # Shorthands should be normalized to full URIs\n        assert set(reg_options.valid_scopes) == {\n            \"openid\",\n            \"https://www.googleapis.com/auth/userinfo.email\",\n            \"https://www.googleapis.com/auth/userinfo.profile\",\n        }\n\n    def test_valid_scopes_defaults_to_required(self, memory_storage: MemoryStore):\n        \"\"\"Test that valid_scopes defaults to required_scopes when not provided.\"\"\"\n        provider = GoogleProvider(\n            client_id=\"123456789.apps.googleusercontent.com\",\n            client_secret=\"GOCSPX-test123\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"openid\", \"email\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        reg_options = provider.client_registration_options\n        assert reg_options is not None\n        assert reg_options.valid_scopes is not None\n        # Should fall back to the (normalized) required_scopes\n        assert set(reg_options.valid_scopes) == {\n            \"openid\",\n            \"https://www.googleapis.com/auth/userinfo.email\",\n        }\n\n\nclass TestGoogleScopeNormalization:\n    \"\"\"Test Google scope shorthand normalization.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"shorthand, expected\",\n        [\n            (\"email\", \"https://www.googleapis.com/auth/userinfo.email\"),\n            (\"profile\", \"https://www.googleapis.com/auth/userinfo.profile\"),\n            (\"openid\", \"openid\"),\n            (\n                \"https://www.googleapis.com/auth/userinfo.email\",\n                \"https://www.googleapis.com/auth/userinfo.email\",\n            ),\n            (\n                \"https://www.googleapis.com/auth/calendar\",\n                \"https://www.googleapis.com/auth/calendar\",\n            ),\n        ],\n    )\n    def test_normalize_google_scope(self, shorthand: str, expected: str):\n        assert _normalize_google_scope(shorthand) == expected\n\n    def test_verifier_normalizes_required_scopes(self):\n        \"\"\"GoogleTokenVerifier should normalize shorthands in required_scopes.\"\"\"\n        verifier = GoogleTokenVerifier(\n            required_scopes=[\"openid\", \"email\", \"profile\"],\n        )\n\n        assert set(verifier.required_scopes) == {\n            \"openid\",\n            \"https://www.googleapis.com/auth/userinfo.email\",\n            \"https://www.googleapis.com/auth/userinfo.profile\",\n        }\n\n    def test_verifier_full_uris_unchanged(self):\n        \"\"\"Full URIs should pass through normalization unchanged.\"\"\"\n        scopes = [\n            \"openid\",\n            \"https://www.googleapis.com/auth/userinfo.email\",\n        ]\n        verifier = GoogleTokenVerifier(required_scopes=scopes)\n        assert verifier.required_scopes == scopes\n\n    def test_alias_map_is_bidirectional(self):\n        \"\"\"Verify the alias map covers the known Google shorthands.\"\"\"\n        assert \"email\" in GOOGLE_SCOPE_ALIASES\n        assert \"profile\" in GOOGLE_SCOPE_ALIASES\n"
  },
  {
    "path": "tests/server/auth/providers/test_http_client.py",
    "content": "\"\"\"Tests for http_client parameter on token verifiers.\n\nVerifies that all token verifiers accept an optional httpx.AsyncClient for\nconnection pooling (issues #3287 and #3293).\n\"\"\"\n\nimport time\n\nimport httpx\nimport pytest\nfrom pytest_httpx import HTTPXMock\n\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\n\nclass TestIntrospectionHttpClient:\n    \"\"\"Test http_client parameter on IntrospectionTokenVerifier.\"\"\"\n\n    @pytest.fixture\n    def shared_client(self) -> httpx.AsyncClient:\n        return httpx.AsyncClient(timeout=30)\n\n    def test_stores_http_client(self, shared_client: httpx.AsyncClient):\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/introspect\",\n            client_id=\"test\",\n            client_secret=\"secret\",\n            http_client=shared_client,\n        )\n        assert verifier._http_client is shared_client\n\n    def test_default_http_client_is_none(self):\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/introspect\",\n            client_id=\"test\",\n            client_secret=\"secret\",\n        )\n        assert verifier._http_client is None\n\n    async def test_uses_provided_client(\n        self, shared_client: httpx.AsyncClient, httpx_mock: HTTPXMock\n    ):\n        \"\"\"When http_client is provided, it should be used for requests.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-1\",\n                \"scope\": \"read\",\n                \"exp\": int(time.time()) + 3600,\n            },\n        )\n\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/introspect\",\n            client_id=\"test\",\n            client_secret=\"secret\",\n            http_client=shared_client,\n        )\n\n        result = await verifier.verify_token(\"tok\")\n        assert result is not None\n        assert result.client_id == \"user-1\"\n\n    async def test_client_not_closed_after_call(\n        self, shared_client: httpx.AsyncClient, httpx_mock: HTTPXMock\n    ):\n        \"\"\"User-provided client must not be closed by the verifier.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-1\",\n                \"scope\": \"read\",\n                \"exp\": int(time.time()) + 3600,\n            },\n        )\n\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/introspect\",\n            client_id=\"test\",\n            client_secret=\"secret\",\n            http_client=shared_client,\n        )\n\n        await verifier.verify_token(\"tok\")\n        # Client should still be open — not closed by the verifier\n        assert not shared_client.is_closed\n\n    async def test_reuses_client_across_calls(\n        self, shared_client: httpx.AsyncClient, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Same client instance should be reused across multiple verify_token calls.\"\"\"\n        for _ in range(3):\n            httpx_mock.add_response(\n                url=\"https://auth.example.com/introspect\",\n                method=\"POST\",\n                json={\n                    \"active\": True,\n                    \"client_id\": \"user-1\",\n                    \"scope\": \"read\",\n                    \"exp\": int(time.time()) + 3600,\n                },\n            )\n\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/introspect\",\n            client_id=\"test\",\n            client_secret=\"secret\",\n            http_client=shared_client,\n        )\n\n        for _ in range(3):\n            result = await verifier.verify_token(\"tok\")\n            assert result is not None\n\n        assert not shared_client.is_closed\n\n\nclass TestJWTVerifierHttpClient:\n    \"\"\"Test http_client parameter on JWTVerifier.\"\"\"\n\n    @pytest.fixture(scope=\"class\")\n    def rsa_key_pair(self) -> RSAKeyPair:\n        return RSAKeyPair.generate()\n\n    @pytest.fixture\n    def shared_client(self) -> httpx.AsyncClient:\n        return httpx.AsyncClient(timeout=30)\n\n    def test_stores_http_client(self, shared_client: httpx.AsyncClient):\n        verifier = JWTVerifier(\n            jwks_uri=\"https://auth.example.com/.well-known/jwks.json\",\n            http_client=shared_client,\n        )\n        assert verifier._http_client is shared_client\n\n    def test_default_http_client_is_none(self):\n        verifier = JWTVerifier(\n            jwks_uri=\"https://auth.example.com/.well-known/jwks.json\",\n        )\n        assert verifier._http_client is None\n\n    async def test_jwks_fetch_uses_provided_client(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        shared_client: httpx.AsyncClient,\n        httpx_mock: HTTPXMock,\n    ):\n        \"\"\"When http_client is provided, JWKS fetches should use it.\"\"\"\n        from authlib.jose import JsonWebKey\n\n        # Build a JWKS response from the RSA key pair\n        public_key_obj = JsonWebKey.import_key(rsa_key_pair.public_key)\n        jwk_dict = dict(public_key_obj.as_dict())\n        jwk_dict[\"kid\"] = \"test-key-1\"\n        jwk_dict[\"use\"] = \"sig\"\n        jwk_dict[\"alg\"] = \"RS256\"\n\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/.well-known/jwks.json\",\n            json={\"keys\": [jwk_dict]},\n        )\n\n        verifier = JWTVerifier(\n            jwks_uri=\"https://auth.example.com/.well-known/jwks.json\",\n            issuer=\"https://auth.example.com\",\n            http_client=shared_client,\n        )\n\n        token = rsa_key_pair.create_token(\n            issuer=\"https://auth.example.com\",\n            kid=\"test-key-1\",\n        )\n\n        result = await verifier.verify_token(token)\n        assert result is not None\n        assert not shared_client.is_closed\n\n    def test_ssrf_safe_rejects_http_client_with_jwks(\n        self,\n        shared_client: httpx.AsyncClient,\n    ):\n        \"\"\"ssrf_safe=True and http_client cannot be used together with JWKS.\"\"\"\n        with pytest.raises(ValueError, match=\"cannot be used with ssrf_safe=True\"):\n            JWTVerifier(\n                jwks_uri=\"https://auth.example.com/.well-known/jwks.json\",\n                ssrf_safe=True,\n                http_client=shared_client,\n            )\n\n    def test_ssrf_safe_allows_http_client_with_static_key(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        shared_client: httpx.AsyncClient,\n    ):\n        \"\"\"ssrf_safe with http_client is allowed when using static public_key (no HTTP).\"\"\"\n        # This should NOT raise — static key means no JWKS fetching\n        verifier = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            ssrf_safe=True,\n            http_client=shared_client,\n        )\n        assert verifier._http_client is shared_client\n        assert verifier.ssrf_safe is True\n\n\nclass TestGitHubHttpClient:\n    \"\"\"Test http_client parameter on GitHubTokenVerifier.\"\"\"\n\n    def test_stores_http_client(self):\n        from fastmcp.server.auth.providers.github import GitHubTokenVerifier\n\n        client = httpx.AsyncClient()\n        verifier = GitHubTokenVerifier(http_client=client)\n        assert verifier._http_client is client\n\n    async def test_uses_provided_client(self, httpx_mock: HTTPXMock):\n        from fastmcp.server.auth.providers.github import GitHubTokenVerifier\n\n        client = httpx.AsyncClient()\n        httpx_mock.add_response(\n            url=\"https://api.github.com/user\",\n            json={\"id\": 123, \"login\": \"testuser\"},\n        )\n        httpx_mock.add_response(\n            url=\"https://api.github.com/user/repos\",\n            headers={\"x-oauth-scopes\": \"user,repo\"},\n            json=[],\n        )\n\n        verifier = GitHubTokenVerifier(http_client=client)\n        result = await verifier.verify_token(\"ghp_test\")\n        assert result is not None\n        assert not client.is_closed\n\n\nclass TestDiscordHttpClient:\n    \"\"\"Test http_client parameter on DiscordTokenVerifier.\"\"\"\n\n    def test_stores_http_client(self):\n        from fastmcp.server.auth.providers.discord import DiscordTokenVerifier\n\n        client = httpx.AsyncClient()\n        verifier = DiscordTokenVerifier(\n            expected_client_id=\"test-client-id\",\n            http_client=client,\n        )\n        assert verifier._http_client is client\n\n\nclass TestGoogleHttpClient:\n    \"\"\"Test http_client parameter on GoogleTokenVerifier.\"\"\"\n\n    def test_stores_http_client(self):\n        from fastmcp.server.auth.providers.google import GoogleTokenVerifier\n\n        client = httpx.AsyncClient()\n        verifier = GoogleTokenVerifier(http_client=client)\n        assert verifier._http_client is client\n\n\nclass TestWorkOSHttpClient:\n    \"\"\"Test http_client parameter on WorkOSTokenVerifier.\"\"\"\n\n    def test_stores_http_client(self):\n        from fastmcp.server.auth.providers.workos import WorkOSTokenVerifier\n\n        client = httpx.AsyncClient()\n        verifier = WorkOSTokenVerifier(\n            authkit_domain=\"https://test.authkit.app\",\n            http_client=client,\n        )\n        assert verifier._http_client is client\n\n\nclass TestProviderHttpClientPassthrough:\n    \"\"\"Test that convenience providers pass http_client to their verifiers.\"\"\"\n\n    def test_github_provider_threads_http_client(self):\n        from fastmcp.server.auth.providers.github import (\n            GitHubProvider,\n            GitHubTokenVerifier,\n        )\n\n        client = httpx.AsyncClient()\n        provider = GitHubProvider(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            base_url=\"https://example.com\",\n            http_client=client,\n        )\n        # OAuthProxy stores token verifier as _token_validator\n        verifier = provider._token_validator\n        assert isinstance(verifier, GitHubTokenVerifier)\n        assert verifier._http_client is client\n\n    def test_discord_provider_threads_http_client(self):\n        from fastmcp.server.auth.providers.discord import (\n            DiscordProvider,\n            DiscordTokenVerifier,\n        )\n\n        client = httpx.AsyncClient()\n        provider = DiscordProvider(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            base_url=\"https://example.com\",\n            http_client=client,\n        )\n        verifier = provider._token_validator\n        assert isinstance(verifier, DiscordTokenVerifier)\n        assert verifier._http_client is client\n\n    def test_google_provider_threads_http_client(self):\n        from fastmcp.server.auth.providers.google import (\n            GoogleProvider,\n            GoogleTokenVerifier,\n        )\n\n        client = httpx.AsyncClient()\n        provider = GoogleProvider(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            base_url=\"https://example.com\",\n            http_client=client,\n        )\n        verifier = provider._token_validator\n        assert isinstance(verifier, GoogleTokenVerifier)\n        assert verifier._http_client is client\n\n    def test_workos_provider_threads_http_client(self):\n        from fastmcp.server.auth.providers.workos import (\n            WorkOSProvider,\n            WorkOSTokenVerifier,\n        )\n\n        client = httpx.AsyncClient()\n        provider = WorkOSProvider(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            authkit_domain=\"https://test.authkit.app\",\n            base_url=\"https://example.com\",\n            http_client=client,\n        )\n        verifier = provider._token_validator\n        assert isinstance(verifier, WorkOSTokenVerifier)\n        assert verifier._http_client is client\n\n    def test_azure_provider_threads_http_client(self):\n        from fastmcp.server.auth.providers.azure import AzureProvider\n        from fastmcp.server.auth.providers.jwt import JWTVerifier\n\n        client = httpx.AsyncClient()\n        provider = AzureProvider(\n            client_id=\"test-client-id\",\n            client_secret=\"secret\",\n            tenant_id=\"test-tenant-id\",\n            required_scopes=[\"read\"],\n            base_url=\"https://example.com\",\n            http_client=client,\n        )\n        verifier = provider._token_validator\n        assert isinstance(verifier, JWTVerifier)\n        assert verifier._http_client is client\n"
  },
  {
    "path": "tests/server/auth/providers/test_introspection.py",
    "content": "\"\"\"Tests for OAuth 2.0 Token Introspection verifier (RFC 7662).\"\"\"\n\nimport base64\nimport time\nfrom typing import Any\n\nimport pytest\nfrom pytest_httpx import HTTPXMock\n\nfrom fastmcp.server.auth.providers.introspection import (\n    IntrospectionTokenVerifier,\n)\n\n\nclass TestIntrospectionTokenVerifier:\n    \"\"\"Test core token verification logic.\"\"\"\n\n    @pytest.fixture\n    def verifier(self) -> IntrospectionTokenVerifier:\n        \"\"\"Create a basic introspection verifier for testing.\"\"\"\n        return IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            timeout_seconds=5,\n        )\n\n    @pytest.fixture\n    def verifier_with_required_scopes(self) -> IntrospectionTokenVerifier:\n        \"\"\"Create verifier with required scopes.\"\"\"\n        return IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            required_scopes=[\"read\", \"write\"],\n        )\n\n    def test_initialization(self):\n        \"\"\"Test verifier initialization.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n        )\n\n        assert verifier.introspection_url == \"https://auth.example.com/oauth/introspect\"\n        assert verifier.client_id == \"test-client\"\n        assert verifier.client_secret == \"test-secret\"\n        assert verifier.timeout_seconds == 10\n        assert verifier.client_auth_method == \"client_secret_basic\"\n\n    def test_initialization_requires_introspection_url(self):\n        \"\"\"Test that introspection_url is required.\"\"\"\n        with pytest.raises(TypeError):\n            IntrospectionTokenVerifier(  # ty: ignore[missing-argument]\n                client_id=\"test-client\",\n                client_secret=\"test-secret\",\n            )\n\n    def test_initialization_requires_client_id(self):\n        \"\"\"Test that client_id is required.\"\"\"\n        with pytest.raises(TypeError):\n            IntrospectionTokenVerifier(  # ty: ignore[missing-argument]\n                introspection_url=\"https://auth.example.com/oauth/introspect\",\n                client_secret=\"test-secret\",\n            )\n\n    def test_initialization_requires_client_secret(self):\n        \"\"\"Test that client_secret is required.\"\"\"\n        with pytest.raises(TypeError):\n            IntrospectionTokenVerifier(  # ty: ignore[missing-argument]\n                introspection_url=\"https://auth.example.com/oauth/introspect\",\n                client_id=\"test-client\",\n            )\n\n    def test_create_basic_auth_header(self, verifier: IntrospectionTokenVerifier):\n        \"\"\"Test HTTP Basic Auth header creation.\"\"\"\n        auth_header = verifier._create_basic_auth_header()\n\n        # Decode and verify\n        assert auth_header.startswith(\"Basic \")\n        encoded = auth_header[6:]\n        decoded = base64.b64decode(encoded).decode(\"utf-8\")\n        assert decoded == \"test-client:test-secret\"\n\n    def test_extract_scopes_from_string(self, verifier: IntrospectionTokenVerifier):\n        \"\"\"Test scope extraction from space-separated string.\"\"\"\n        response = {\"scope\": \"read write admin\"}\n        scopes = verifier._extract_scopes(response)\n\n        assert scopes == [\"read\", \"write\", \"admin\"]\n\n    def test_extract_scopes_from_array(self, verifier: IntrospectionTokenVerifier):\n        \"\"\"Test scope extraction from array.\"\"\"\n        response = {\"scope\": [\"read\", \"write\", \"admin\"]}\n        scopes = verifier._extract_scopes(response)\n\n        assert scopes == [\"read\", \"write\", \"admin\"]\n\n    def test_extract_scopes_missing(self, verifier: IntrospectionTokenVerifier):\n        \"\"\"Test scope extraction when scope field is missing.\"\"\"\n        response: dict[str, Any] = {}\n        scopes = verifier._extract_scopes(response)\n\n        assert scopes == []\n\n    def test_extract_scopes_with_extra_whitespace(\n        self, verifier: IntrospectionTokenVerifier\n    ):\n        \"\"\"Test scope extraction handles extra whitespace.\"\"\"\n        response = {\"scope\": \"  read   write  admin  \"}\n        scopes = verifier._extract_scopes(response)\n\n        assert scopes == [\"read\", \"write\", \"admin\"]\n\n    async def test_valid_token_verification(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test successful token verification.\"\"\"\n        # Mock introspection endpoint\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read write\",\n                \"exp\": int(time.time()) + 3600,\n                \"iat\": int(time.time()),\n                \"sub\": \"user-123\",\n                \"username\": \"testuser\",\n            },\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert access_token.client_id == \"user-123\"\n        assert access_token.scopes == [\"read\", \"write\"]\n        assert access_token.expires_at is not None\n        assert access_token.claims[\"active\"] is True\n        assert access_token.claims[\"username\"] == \"testuser\"\n\n    async def test_inactive_token_returns_none(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that inactive tokens return None.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": False},\n        )\n\n        access_token = await verifier.verify_token(\"expired-token\")\n\n        assert access_token is None\n\n    async def test_expired_token_returns_none(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that expired tokens return None.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read\",\n                \"exp\": int(time.time()) - 3600,  # Expired 1 hour ago\n            },\n        )\n\n        access_token = await verifier.verify_token(\"expired-token\")\n\n        assert access_token is None\n\n    async def test_token_without_expiration(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test token without expiration field.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read\",\n            },\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert access_token.expires_at is None\n\n    async def test_token_without_scopes(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test token without scope field.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n            },\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert access_token.scopes == []\n\n    async def test_required_scopes_validation(\n        self,\n        verifier_with_required_scopes: IntrospectionTokenVerifier,\n        httpx_mock: HTTPXMock,\n    ):\n        \"\"\"Test that required scopes are validated.\"\"\"\n        # Token with insufficient scopes\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read\",  # Missing 'write'\n            },\n        )\n\n        access_token = await verifier_with_required_scopes.verify_token(\"test-token\")\n\n        assert access_token is None\n\n    async def test_required_scopes_validation_success(\n        self,\n        verifier_with_required_scopes: IntrospectionTokenVerifier,\n        httpx_mock: HTTPXMock,\n    ):\n        \"\"\"Test successful validation with required scopes.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read write admin\",  # Has all required scopes\n            },\n        )\n\n        access_token = await verifier_with_required_scopes.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert set(access_token.scopes) >= {\"read\", \"write\"}\n\n    async def test_http_error_returns_none(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that HTTP errors return None.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            status_code=500,\n            text=\"Internal Server Error\",\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is None\n\n    async def test_authentication_failure_returns_none(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that authentication failures return None.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            status_code=401,\n            text=\"Unauthorized\",\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is None\n\n    async def test_timeout_returns_none(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that timeouts return None.\"\"\"\n        from httpx import TimeoutException\n\n        httpx_mock.add_exception(\n            TimeoutException(\"Request timed out\"),\n            url=\"https://auth.example.com/oauth/introspect\",\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is None\n\n    async def test_malformed_json_returns_none(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that malformed JSON responses return None.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            status_code=200,\n            text=\"not json\",\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is None\n\n    async def test_request_includes_correct_headers(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that the request includes correct headers and auth.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        await verifier.verify_token(\"test-token\")\n\n        # Verify request was made with correct parameters\n        request = httpx_mock.get_request()\n        assert request is not None\n        assert request.method == \"POST\"\n        assert \"Authorization\" in request.headers\n        assert request.headers[\"Authorization\"].startswith(\"Basic \")\n        assert request.headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n        assert request.headers[\"Accept\"] == \"application/json\"\n\n    async def test_request_includes_token_and_hint(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that the request includes token and token_type_hint.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        await verifier.verify_token(\"my-test-token\")\n\n        request = httpx_mock.get_request()\n        assert request is not None\n\n        # Parse form data\n        body = request.content.decode(\"utf-8\")\n        assert \"token=my-test-token\" in body\n        assert \"token_type_hint=access_token\" in body\n\n    async def test_client_id_fallback_to_sub(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that client_id falls back to sub if not present.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"sub\": \"user-456\",\n                \"scope\": \"read\",\n            },\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert access_token.client_id == \"user-456\"\n\n    async def test_client_id_defaults_to_unknown(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that client_id defaults to 'unknown' if neither client_id nor sub present.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"scope\": \"read\",\n            },\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert access_token.client_id == \"unknown\"\n\n    def test_initialization_with_client_secret_post(self):\n        \"\"\"Test verifier initialization with client_secret_post method.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            client_auth_method=\"client_secret_post\",\n        )\n\n        assert verifier.client_auth_method == \"client_secret_post\"\n        assert verifier.introspection_url == \"https://auth.example.com/oauth/introspect\"\n        assert verifier.client_id == \"test-client\"\n        assert verifier.client_secret == \"test-secret\"\n\n    def test_initialization_defaults_to_client_secret_basic(self):\n        \"\"\"Test that client_secret_basic is the default auth method.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n        )\n\n        assert verifier.client_auth_method == \"client_secret_basic\"\n\n    def test_initialization_rejects_invalid_client_auth_method(self):\n        \"\"\"Test that invalid client_auth_method values are rejected.\"\"\"\n        # Test typo with trailing space\n        with pytest.raises(ValueError) as exc_info:\n            IntrospectionTokenVerifier(\n                introspection_url=\"https://auth.example.com/oauth/introspect\",\n                client_id=\"test-client\",\n                client_secret=\"test-secret\",\n                client_auth_method=\"client_secret_basic \",  # ty: ignore[invalid-argument-type]\n            )\n        assert \"Invalid client_auth_method\" in str(exc_info.value)\n        assert \"client_secret_basic \" in str(exc_info.value)\n\n        # Test completely invalid value\n        with pytest.raises(ValueError) as exc_info:\n            IntrospectionTokenVerifier(\n                introspection_url=\"https://auth.example.com/oauth/introspect\",\n                client_id=\"test-client\",\n                client_secret=\"test-secret\",\n                client_auth_method=\"basic\",  # ty: ignore[invalid-argument-type]\n            )\n        assert \"Invalid client_auth_method\" in str(exc_info.value)\n        assert \"basic\" in str(exc_info.value)\n\n    async def test_client_secret_post_includes_credentials_in_body(\n        self, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that client_secret_post includes credentials in POST body.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            client_auth_method=\"client_secret_post\",\n        )\n\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        await verifier.verify_token(\"test-token\")\n\n        # Verify request was made with credentials in body, not header\n        request = httpx_mock.get_request()\n        assert request is not None\n        assert request.method == \"POST\"\n        assert \"Authorization\" not in request.headers\n        assert request.headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n        assert request.headers[\"Accept\"] == \"application/json\"\n\n        # Parse form data\n        body = request.content.decode(\"utf-8\")\n        assert \"token=test-token\" in body\n        assert \"token_type_hint=access_token\" in body\n        assert \"client_id=test-client\" in body\n        assert \"client_secret=test-secret\" in body\n\n    async def test_client_secret_post_verification_success(self, httpx_mock: HTTPXMock):\n        \"\"\"Test successful token verification with client_secret_post.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            client_auth_method=\"client_secret_post\",\n        )\n\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read write\",\n                \"exp\": int(time.time()) + 3600,\n            },\n        )\n\n        access_token = await verifier.verify_token(\"test-token\")\n\n        assert access_token is not None\n        assert access_token.client_id == \"user-123\"\n        assert access_token.scopes == [\"read\", \"write\"]\n\n    async def test_client_secret_basic_still_works(\n        self, verifier: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that client_secret_basic continues to work unchanged.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        await verifier.verify_token(\"test-token\")\n\n        # Verify request was made with Basic Auth header\n        request = httpx_mock.get_request()\n        assert request is not None\n        assert \"Authorization\" in request.headers\n        assert request.headers[\"Authorization\"].startswith(\"Basic \")\n\n        # Verify credentials are NOT in body\n        body = request.content.decode(\"utf-8\")\n        assert \"client_id=\" not in body\n        assert \"client_secret=\" not in body\n\n\nclass TestIntrospectionCaching:\n    \"\"\"Test in-memory caching for token introspection.\"\"\"\n\n    @pytest.fixture\n    def verifier_with_cache(self) -> IntrospectionTokenVerifier:\n        \"\"\"Create verifier with caching enabled.\"\"\"\n        return IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=300,  # 5 minutes\n            max_cache_size=100,\n        )\n\n    @pytest.fixture\n    def verifier_no_cache(self) -> IntrospectionTokenVerifier:\n        \"\"\"Create verifier with caching disabled.\"\"\"\n        return IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=0,  # Disabled\n        )\n\n    def test_default_cache_settings(self):\n        \"\"\"Test that caching is disabled by default.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n        )\n        assert not verifier._cache.enabled\n\n    def test_custom_cache_settings(self):\n        \"\"\"Test that cache settings can be customized.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=60,\n            max_cache_size=500,\n        )\n        assert verifier._cache._ttl == 60\n        assert verifier._cache._max_size == 500\n\n    def test_cache_disabled_with_zero_ttl(self):\n        \"\"\"Test that cache is disabled when TTL is 0 or None.\"\"\"\n        # Explicit 0\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=0,\n        )\n        assert not verifier._cache.enabled\n\n        # Explicit None (same as default)\n        verifier2 = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=None,\n        )\n        assert not verifier2._cache.enabled\n\n    async def test_cache_disabled_with_zero_max_size(self, httpx_mock: HTTPXMock):\n        \"\"\"Test that cache is disabled when max_cache_size is 0.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read\",\n            },\n        )\n\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=300,\n            max_cache_size=0,\n        )\n        result = await verifier.verify_token(\"test-token\")\n        assert result is not None\n        assert result.client_id == \"user-123\"\n\n    def test_negative_max_cache_size_raises(self):\n        \"\"\"Negative max_cache_size is a caller bug and should raise.\"\"\"\n        with pytest.raises(ValueError, match=\"max_cache_size must be non-negative\"):\n            IntrospectionTokenVerifier(\n                introspection_url=\"https://auth.example.com/oauth/introspect\",\n                client_id=\"test-client\",\n                client_secret=\"test-secret\",\n                cache_ttl_seconds=300,\n                max_cache_size=-1,\n            )\n\n    async def test_cache_hit_returns_cached_result(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that cached valid tokens are returned without introspection call.\"\"\"\n        # First call - introspection endpoint called\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read write\",\n                \"exp\": int(time.time()) + 3600,\n            },\n        )\n\n        # First verification\n        result1 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result1 is not None\n        assert result1.client_id == \"user-123\"\n\n        # Verify one request was made\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 1\n\n        # Second verification - should use cache, no new request\n        result2 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result2 is not None\n        assert result2.client_id == \"user-123\"\n\n        # Still only one request\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 1\n\n    async def test_cache_returns_defensive_copy(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that cached tokens are defensive copies (mutations don't leak).\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"scope\": \"read write\",\n                \"exp\": int(time.time()) + 3600,\n                \"custom_claim\": \"original\",\n            },\n        )\n\n        # First verification\n        result1 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result1 is not None\n        assert result1.claims[\"custom_claim\"] == \"original\"\n\n        # Mutate the result (simulating request-path code adding derived claims)\n        result1.claims[\"custom_claim\"] = \"mutated\"\n        result1.claims[\"new_claim\"] = \"injected\"\n        result1.scopes.append(\"admin\")\n\n        # Second verification - should get clean copy, not mutated one\n        result2 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result2 is not None\n        assert result2.claims[\"custom_claim\"] == \"original\"\n        assert \"new_claim\" not in result2.claims\n        assert \"admin\" not in result2.scopes\n\n        # Verify they are different object instances\n        assert result1 is not result2\n\n    async def test_inactive_tokens_not_cached(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that inactive tokens are NOT cached (may become valid later).\"\"\"\n        # Add two responses - inactive tokens should trigger re-introspection\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": False},\n        )\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": False},\n        )\n\n        # First verification\n        result1 = await verifier_with_cache.verify_token(\"inactive-token\")\n        assert result1 is None\n\n        # Verify one request was made\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 1\n\n        # Second verification - should NOT use cache, makes another request\n        result2 = await verifier_with_cache.verify_token(\"inactive-token\")\n        assert result2 is None\n\n        # Two requests made (inactive tokens not cached)\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 2\n\n    async def test_cache_disabled_makes_every_call(\n        self, verifier_no_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that with caching disabled, every call makes a request.\"\"\"\n        # Add multiple responses for the same token\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        # First call\n        await verifier_no_cache.verify_token(\"test-token\")\n\n        # Second call - should also make a request\n        await verifier_no_cache.verify_token(\"test-token\")\n\n        # Two requests were made\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 2\n\n    async def test_different_tokens_are_cached_separately(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that different tokens have separate cache entries.\"\"\"\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-1\"},\n        )\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-2\"},\n        )\n\n        # Verify two different tokens\n        result1 = await verifier_with_cache.verify_token(\"token-1\")\n        result2 = await verifier_with_cache.verify_token(\"token-2\")\n\n        assert result1 is not None\n        assert result1.client_id == \"user-1\"\n        assert result2 is not None\n        assert result2.client_id == \"user-2\"\n\n        # Two requests were made\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 2\n\n        # Verify both again - no new requests\n        await verifier_with_cache.verify_token(\"token-1\")\n        await verifier_with_cache.verify_token(\"token-2\")\n\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 2\n\n    async def test_http_errors_are_not_cached(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that HTTP errors are not cached (transient failures).\"\"\"\n        # First call - HTTP error\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            status_code=500,\n            text=\"Internal Server Error\",\n        )\n        # Second call - success\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        # First verification - fails\n        result1 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result1 is None\n\n        # Second verification - should retry since error wasn't cached\n        result2 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result2 is not None\n        assert result2.client_id == \"user-123\"\n\n        # Two requests were made\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 2\n\n    async def test_timeout_errors_are_not_cached(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that timeout errors are not cached (transient failures).\"\"\"\n        from httpx import TimeoutException\n\n        # First call - timeout\n        httpx_mock.add_exception(\n            TimeoutException(\"Request timed out\"),\n            url=\"https://auth.example.com/oauth/introspect\",\n        )\n        # Second call - success\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        # First verification - times out\n        result1 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result1 is None\n\n        # Second verification - should retry since timeout wasn't cached\n        result2 = await verifier_with_cache.verify_token(\"test-token\")\n        assert result2 is not None\n\n        # Two requests were made\n        requests = httpx_mock.get_requests()\n        assert len(requests) == 2\n\n    def test_token_hashing(self, verifier_with_cache: IntrospectionTokenVerifier):\n        \"\"\"Test that tokens are hashed consistently.\"\"\"\n        hash1 = verifier_with_cache._cache._hash_token(\"test-token\")\n        hash2 = verifier_with_cache._cache._hash_token(\"test-token\")\n        hash3 = verifier_with_cache._cache._hash_token(\"different-token\")\n\n        # Same token produces same hash\n        assert hash1 == hash2\n        # Different tokens produce different hashes\n        assert hash1 != hash3\n        # Hash is a hex string (SHA-256 = 64 chars)\n        assert len(hash1) == 64\n\n    async def test_cache_respects_token_expiration(\n        self, verifier_with_cache: IntrospectionTokenVerifier, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that cache respects token's exp claim for TTL.\"\"\"\n        # Token expiring in 60 seconds (shorter than cache TTL of 300)\n        short_exp = int(time.time()) + 60\n\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\n                \"active\": True,\n                \"client_id\": \"user-123\",\n                \"exp\": short_exp,\n            },\n        )\n\n        await verifier_with_cache.verify_token(\"test-token\")\n\n        # Check that cache entry uses the shorter expiration\n        cache_key = verifier_with_cache._cache._hash_token(\"test-token\")\n        entry = verifier_with_cache._cache._entries[cache_key]\n        # Cache expiration should be at or before token expiration\n        assert entry.expires_at <= short_exp\n\n    async def test_expired_cache_entry_triggers_new_introspection(\n        self, httpx_mock: HTTPXMock\n    ):\n        \"\"\"Test that expired cache entries are evicted and a new call is made.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=1,  # 1 second TTL\n        )\n\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n        httpx_mock.add_response(\n            url=\"https://auth.example.com/oauth/introspect\",\n            method=\"POST\",\n            json={\"active\": True, \"client_id\": \"user-123\"},\n        )\n\n        # First call — caches the result\n        await verifier.verify_token(\"test-token\")\n        assert len(httpx_mock.get_requests()) == 1\n\n        # Expire the cache entry manually\n        cache_key = verifier._cache._hash_token(\"test-token\")\n        verifier._cache._entries[cache_key].expires_at = time.time() - 1\n\n        # Second call — cache miss, new introspection\n        await verifier.verify_token(\"test-token\")\n        assert len(httpx_mock.get_requests()) == 2\n\n    async def test_cache_eviction_at_max_size(self, httpx_mock: HTTPXMock):\n        \"\"\"Test that cache evicts entries when max size is reached.\"\"\"\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            cache_ttl_seconds=300,\n            max_cache_size=2,\n        )\n\n        for i in range(3):\n            httpx_mock.add_response(\n                url=\"https://auth.example.com/oauth/introspect\",\n                method=\"POST\",\n                json={\"active\": True, \"client_id\": f\"user-{i}\"},\n            )\n\n        # Fill cache to capacity\n        await verifier.verify_token(\"token-0\")\n        await verifier.verify_token(\"token-1\")\n        assert len(verifier._cache._entries) == 2\n\n        # Third token should evict the oldest entry\n        await verifier.verify_token(\"token-2\")\n        assert len(verifier._cache._entries) == 2\n\n        # token-0 should have been evicted (FIFO)\n        hash_0 = verifier._cache._hash_token(\"token-0\")\n        assert hash_0 not in verifier._cache._entries\n\n\nclass TestIntrospectionTokenVerifierIntegration:\n    \"\"\"Integration tests with FastMCP server.\"\"\"\n\n    async def test_verifier_used_by_fastmcp(self):\n        \"\"\"Test that IntrospectionTokenVerifier can be used as FastMCP auth.\"\"\"\n        from fastmcp import FastMCP\n\n        # Create verifier\n        verifier = IntrospectionTokenVerifier(\n            introspection_url=\"https://auth.example.com/oauth/introspect\",\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n        )\n\n        # Create protected server - should work without errors\n        mcp = FastMCP(\"Test Server\", auth=verifier)\n\n        @mcp.tool()\n        def greet(name: str) -> str:\n            \"\"\"Greet someone.\"\"\"\n            return f\"Hello, {name}!\"\n\n        # Verify the auth is set correctly\n        assert mcp.auth is verifier\n        tools = await mcp.list_tools()\n        assert len(list(tools)) == 1\n"
  },
  {
    "path": "tests/server/auth/providers/test_propelauth.py",
    "content": "\"\"\"Tests for PropelAuthProvider.\"\"\"\n\nfrom typing import cast\nfrom unittest.mock import AsyncMock\n\nimport httpx\nimport pytest\nfrom pydantic import SecretStr\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.auth import AccessToken\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\nfrom fastmcp.server.auth.providers.propelauth import (\n    PropelAuthProvider,\n    PropelAuthTokenIntrospectionOverrides,\n)\nfrom fastmcp.utilities.tests import run_server_async\n\n\nclass TestPropelAuthProvider:\n    \"\"\"Test PropelAuth's auth provider.\"\"\"\n\n    def test_init_with_only_required_params(self):\n        \"\"\"Test PropelAuthProvider initialization with only required params.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n        )\n\n        # Verify the provider is configured correctly\n        assert len(provider.authorization_servers) == 1\n        assert (\n            str(provider.authorization_servers[0])\n            == \"https://auth.example.com/oauth/2.1\"\n        )\n        assert str(provider.base_url) == \"https://example.com/\"\n\n        # Verify token verifier is configured correctly\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert (\n            provider.token_verifier.introspection_url\n            == \"https://auth.example.com/oauth/2.1/introspect\"\n        )\n        assert provider.token_verifier.client_id == \"client_id_123\"\n        assert provider.token_verifier.client_secret == \"client_secret_123\"\n\n    def test_auth_url_trailing_slash_normalization(self):\n        \"\"\"Test that trailing slash on auth_url is stripped before building URLs.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com/\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert len(provider.authorization_servers) == 1\n        assert (\n            str(provider.authorization_servers[0])\n            == \"https://auth.example.com/oauth/2.1\"\n        )\n        assert (\n            provider.token_verifier.introspection_url\n            == \"https://auth.example.com/oauth/2.1/introspect\"\n        )\n\n    def test_required_scopes_passed_to_verifier(self):\n        \"\"\"Test that required_scopes are passed through to the token verifier.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            required_scopes=[\"read\", \"write\"],\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier.required_scopes == [\"read\", \"write\"]\n\n    def test_introspection_client_secret_as_secret_str(self):\n        \"\"\"Test that SecretStr client_secret is unwrapped correctly.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=SecretStr(\"my_secret\"),\n            base_url=\"https://example.com\",\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier.client_secret == \"my_secret\"\n\n    def test_authorization_servers_configuration(self):\n        \"\"\"Test that authorization_servers contains the correct PropelAuth URL.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.propelauth.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n        )\n\n        assert len(provider.authorization_servers) == 1\n        assert (\n            str(provider.authorization_servers[0])\n            == \"https://auth.propelauth.com/oauth/2.1\"\n        )\n\n    def test_token_introspection_overrides_timeout(self):\n        \"\"\"Test that timeout_seconds override is passed to the verifier.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            token_introspection_overrides={\"timeout_seconds\": 30},\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier.timeout_seconds == 30\n\n    def test_token_introspection_overrides_cache(self):\n        \"\"\"Test that cache overrides are passed to the verifier.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            token_introspection_overrides={\n                \"cache_ttl_seconds\": 300,\n                \"max_cache_size\": 500,\n            },\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier._cache._ttl == 300\n        assert provider.token_verifier._cache._max_size == 500\n\n    def test_token_introspection_overrides_http_client(self):\n        \"\"\"Test that http_client override is passed to the verifier.\"\"\"\n        client = httpx.AsyncClient()\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            token_introspection_overrides={\"http_client\": client},\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier._http_client is client\n\n    def test_token_introspection_overrides_ignores_unknown_keys(self):\n        \"\"\"Test that unknown override keys are silently ignored.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            # This won't typecheck without casting, since it shouldn't be allowed\n            token_introspection_overrides=cast(\n                PropelAuthTokenIntrospectionOverrides, {\"unknown_key\": \"value\"}\n            ),\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier.timeout_seconds == 10\n\n    def test_token_introspection_overrides_ignores_disallowed_known_keys(self):\n        \"\"\"Test that known IntrospectionTokenVerifier keys not in the allow list are ignored.\"\"\"\n        provider = PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            # This won't typecheck without casting, since it shouldn't be allowed\n            token_introspection_overrides=cast(\n                PropelAuthTokenIntrospectionOverrides, {\"client_id\": \"sneaky_override\"}\n            ),\n        )\n\n        assert isinstance(provider.token_verifier, IntrospectionTokenVerifier)\n        assert provider.token_verifier.client_id == \"client_id_123\"\n\n\nclass TestPropelAuthResourceChecking:\n    \"\"\"Test audience (aud) checking when resource is configured.\"\"\"\n\n    def _make_provider(self, resource: str | None = None) -> PropelAuthProvider:\n        return PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"https://example.com\",\n            resource=resource,\n        )\n\n    def _make_access_token(self, aud: str) -> AccessToken:\n        return AccessToken(\n            token=\"test-token\",\n            client_id=\"client_id_123\",\n            scopes=[],\n            claims={\"active\": True, \"sub\": \"user-1\", \"aud\": aud},\n        )\n\n    async def test_no_resource_skips_aud_check(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"When resource is not configured, tokens are accepted without aud checking.\"\"\"\n        provider = self._make_provider(resource=None)\n        token = self._make_access_token(aud=\"https://anything.example.com\")\n        monkeypatch.setattr(\n            provider.token_verifier, \"verify_token\", AsyncMock(return_value=token)\n        )\n\n        result = await provider.verify_token(\"test-token\")\n        assert result is token\n\n    async def test_aud_matches_resource(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Token is accepted when aud matches the configured resource.\"\"\"\n        provider = self._make_provider(resource=\"https://api.example.com/mcp\")\n        token = self._make_access_token(aud=\"https://api.example.com/mcp\")\n        monkeypatch.setattr(\n            provider.token_verifier, \"verify_token\", AsyncMock(return_value=token)\n        )\n\n        result = await provider.verify_token(\"test-token\")\n        assert result is token\n\n    async def test_aud_does_not_match_resource(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Token is rejected when aud doesn't match the configured resource.\"\"\"\n        provider = self._make_provider(resource=\"https://api.example.com/mcp\")\n        token = self._make_access_token(aud=\"https://other-server.example.com/mcp\")\n        monkeypatch.setattr(\n            provider.token_verifier, \"verify_token\", AsyncMock(return_value=token)\n        )\n\n        result = await provider.verify_token(\"test-token\")\n        assert result is None\n\n    async def test_inner_verifier_returns_none(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"When the inner verifier rejects the token, None is returned without aud checking.\"\"\"\n        provider = self._make_provider(resource=\"https://api.example.com/mcp\")\n        monkeypatch.setattr(\n            provider.token_verifier, \"verify_token\", AsyncMock(return_value=None)\n        )\n\n        result = await provider.verify_token(\"test-token\")\n        assert result is None\n\n\n@pytest.fixture\nasync def mcp_server_url():\n    \"\"\"Start MCP server with PropelAuth authentication.\"\"\"\n    mcp = FastMCP(\n        auth=PropelAuthProvider(\n            auth_url=\"https://auth.example.com\",\n            introspection_client_id=\"client_id_123\",\n            introspection_client_secret=\"client_secret_123\",\n            base_url=\"http://localhost:4321\",\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    async with run_server_async(mcp, transport=\"http\") as url:\n        yield url\n\n\nclass TestPropelAuthProviderIntegration:\n    async def test_unauthorized_access(self, mcp_server_url: str):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url) as client:\n                tools = await client.list_tools()  # noqa: F841\n\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    async def test_metadata_route_forwards_propelauth_response(\n        self,\n        monkeypatch: pytest.MonkeyPatch,\n        mcp_server_url: str,\n    ) -> None:\n        \"\"\"Ensure PropelAuth metadata route proxies upstream JSON.\"\"\"\n\n        metadata_payload = {\n            \"issuer\": \"https://auth.example.com\",\n            \"token_endpoint\": \"https://auth.example.com/oauth/2.1/token\",\n            \"authorization_endpoint\": \"https://auth.example.com/oauth/2.1/authorize\",\n        }\n\n        class DummyResponse:\n            status_code = 200\n\n            def __init__(self, data: dict[str, str]):\n                self._data = data\n\n            def json(self):\n                return self._data\n\n            def raise_for_status(self):\n                return None\n\n        class DummyAsyncClient:\n            last_url: str | None = None\n\n            async def __aenter__(self):\n                return self\n\n            async def __aexit__(self, exc_type, exc, tb):\n                return False\n\n            async def get(self, url: str):\n                DummyAsyncClient.last_url = url\n                return DummyResponse(metadata_payload)\n\n        real_httpx_client = httpx.AsyncClient\n\n        monkeypatch.setattr(\n            \"fastmcp.server.auth.providers.propelauth.httpx.AsyncClient\",\n            DummyAsyncClient,\n        )\n\n        base_url = mcp_server_url.rsplit(\"/mcp\", 1)[0]\n        async with real_httpx_client() as client:\n            response = await client.get(\n                f\"{base_url}/.well-known/oauth-authorization-server\"\n            )\n\n        assert response.status_code == 200\n        assert response.json() == metadata_payload\n        assert (\n            DummyAsyncClient.last_url\n            == \"https://auth.example.com/.well-known/oauth-authorization-server/oauth/2.1\"\n        )\n"
  },
  {
    "path": "tests/server/auth/providers/test_scalekit.py",
    "content": "\"\"\"Tests for Scalekit OAuth provider.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.server.auth.providers.scalekit import ScalekitProvider\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_async\n\n\nclass TestScalekitProvider:\n    \"\"\"Test Scalekit OAuth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self):\n        \"\"\"Test ScalekitProvider initialization with explicit parameters.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://myserver.com/\",\n            required_scopes=[\"read\"],\n        )\n\n        assert provider.environment_url == \"https://my-env.scalekit.com\"\n        assert provider.resource_id == \"sk_resource_456\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n        assert provider.required_scopes == [\"read\"]\n\n    def test_init_with_mcp_url_only(self):\n        \"\"\"Allow legacy mcp_url parameter as base_url.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://legacy.scalekit.com\",\n            resource_id=\"sk_resource_legacy\",\n            mcp_url=\"https://legacy-app.com/\",\n        )\n\n        assert str(provider.base_url) == \"https://legacy-app.com/\"\n\n    def test_init_prefers_base_url_over_mcp_url(self):\n        \"\"\"mcp_url should take precedence over base_url when both provided.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://preferred-base.com/\",\n            mcp_url=\"https://unused-base.com/\",\n        )\n\n        assert str(provider.base_url) == \"https://preferred-base.com/\"\n\n    def test_environment_variable_loading(self):\n        \"\"\"Test that environment variables are loaded correctly.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://test-env.scalekit.com\",\n            resource_id=\"sk_resource_test_456\",\n            base_url=\"http://test-server.com\",\n        )\n\n        assert provider.environment_url == \"https://test-env.scalekit.com\"\n        assert provider.resource_id == \"sk_resource_test_456\"\n        assert str(provider.base_url) == \"http://test-server.com/\"\n\n    def test_accepts_client_id_argument(self):\n        \"\"\"client_id parameter should be accepted but ignored.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://myserver.com/\",\n            client_id=\"client_123\",\n        )\n\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n    def test_url_trailing_slash_handling(self):\n        \"\"\"Test that URLs handle trailing slashes correctly.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com/\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://myserver.com/\",\n        )\n\n        assert provider.environment_url == \"https://my-env.scalekit.com\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n    def test_jwt_verifier_configured_correctly(self):\n        \"\"\"Test that JWT verifier is configured correctly.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://myserver.com/\",\n        )\n\n        # Check that JWT verifier uses the correct endpoints\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.jwks_uri == \"https://my-env.scalekit.com/keys\"\n        assert provider.token_verifier.issuer == \"https://my-env.scalekit.com\"\n        assert provider.token_verifier.audience == \"sk_resource_456\"\n\n    def test_required_scopes_hooks_into_verifier(self):\n        \"\"\"Token verifier should enforce required scopes when provided.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://myserver.com/\",\n            required_scopes=[\"read\"],\n        )\n\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.required_scopes == [\"read\"]\n\n    def test_authorization_servers_configuration(self):\n        \"\"\"Test that authorization servers are configured correctly.\"\"\"\n        provider = ScalekitProvider(\n            environment_url=\"https://my-env.scalekit.com\",\n            resource_id=\"sk_resource_456\",\n            base_url=\"https://myserver.com/\",\n        )\n\n        assert len(provider.authorization_servers) == 1\n        assert (\n            str(provider.authorization_servers[0])\n            == \"https://my-env.scalekit.com/resources/sk_resource_456\"\n        )\n\n\n@pytest.fixture\nasync def mcp_server_url():\n    \"\"\"Start Scalekit server.\"\"\"\n    mcp = FastMCP(\n        auth=ScalekitProvider(\n            environment_url=\"https://test-env.scalekit.com\",\n            resource_id=\"sk_resource_test_456\",\n            base_url=\"http://localhost:4321\",\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    async with run_server_async(mcp, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\ndef client_with_headless_oauth(mcp_server_url: str) -> Client:\n    \"\"\"Client with headless OAuth that bypasses browser interaction.\"\"\"\n    return Client(\n        transport=StreamableHttpTransport(mcp_server_url),\n        auth=HeadlessOAuth(mcp_url=mcp_server_url),\n    )\n\n\nclass TestScalekitProviderIntegration:\n    async def test_unauthorized_access(self, mcp_server_url: str):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url) as client:\n                tools = await client.list_tools()  # noqa: F841\n\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    async def test_metadata_route_forwards_scalekit_response(\n        self,\n        monkeypatch: pytest.MonkeyPatch,\n        mcp_server_url: str,\n    ) -> None:\n        \"\"\"Ensure Scalekit metadata route proxies upstream JSON.\"\"\"\n\n        metadata_payload = {\n            \"issuer\": \"https://test-env.scalekit.com\",\n            \"token_endpoint\": \"https://test-env.scalekit.com/token\",\n            \"authorization_endpoint\": \"https://test-env.scalekit.com/authorize\",\n        }\n\n        class DummyResponse:\n            status_code = 200\n\n            def __init__(self, data: dict[str, str]):\n                self._data = data\n\n            def json(self):\n                return self._data\n\n            def raise_for_status(self):\n                return None\n\n        class DummyAsyncClient:\n            last_url: str | None = None\n\n            async def __aenter__(self):\n                return self\n\n            async def __aexit__(self, exc_type, exc, tb):\n                return False\n\n            async def get(self, url: str):\n                DummyAsyncClient.last_url = url\n                return DummyResponse(metadata_payload)\n\n        real_httpx_client = httpx.AsyncClient\n\n        monkeypatch.setattr(\n            \"fastmcp.server.auth.providers.scalekit.httpx.AsyncClient\",\n            DummyAsyncClient,\n        )\n\n        base_url = mcp_server_url.rsplit(\"/mcp\", 1)[0]\n        async with real_httpx_client() as client:\n            response = await client.get(\n                f\"{base_url}/.well-known/oauth-authorization-server\"\n            )\n\n        assert response.status_code == 200\n        assert response.json() == metadata_payload\n        assert (\n            DummyAsyncClient.last_url\n            == \"https://test-env.scalekit.com/.well-known/oauth-authorization-server/resources/sk_resource_test_456\"\n        )\n"
  },
  {
    "path": "tests/server/auth/providers/test_supabase.py",
    "content": "\"\"\"Tests for Supabase Auth provider.\"\"\"\n\nfrom collections.abc import Generator\n\nimport httpx\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\nfrom fastmcp.server.auth.providers.supabase import SupabaseProvider\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_in_process\n\n\nclass TestSupabaseProvider:\n    \"\"\"Test Supabase Auth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self):\n        \"\"\"Test SupabaseProvider initialization with explicit parameters.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n        )\n\n        assert provider.project_url == \"https://abc123.supabase.co\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n    def test_environment_variable_loading(self):\n        \"\"\"Test that environment variables are loaded correctly.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://env123.supabase.co\",\n            base_url=\"http://env-server.com\",\n        )\n\n        assert provider.project_url == \"https://env123.supabase.co\"\n        assert str(provider.base_url) == \"http://env-server.com/\"\n\n    def test_project_url_normalization(self):\n        \"\"\"Test that project_url handles trailing slashes correctly.\"\"\"\n        # Without trailing slash\n        provider1 = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n        )\n        assert provider1.project_url == \"https://abc123.supabase.co\"\n\n        # With trailing slash - should be stripped\n        provider2 = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co/\",\n            base_url=\"https://myserver.com\",\n        )\n        assert provider2.project_url == \"https://abc123.supabase.co\"\n\n    def test_jwt_verifier_configured_correctly(self):\n        \"\"\"Test that JWT verifier is configured correctly.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n        )\n\n        # Check that JWT verifier uses the correct endpoints (default auth_route)\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert (\n            provider.token_verifier.jwks_uri\n            == \"https://abc123.supabase.co/auth/v1/.well-known/jwks.json\"\n        )\n        assert provider.token_verifier.issuer == \"https://abc123.supabase.co/auth/v1\"\n        assert provider.token_verifier.algorithm == \"ES256\"\n\n    def test_jwt_verifier_with_required_scopes(self):\n        \"\"\"Test that JWT verifier respects required_scopes.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"openid\", \"email\"],\n        )\n\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.required_scopes == [\"openid\", \"email\"]\n\n    def test_authorization_servers_configured(self):\n        \"\"\"Test that authorization servers list is configured correctly.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n        )\n\n        assert len(provider.authorization_servers) == 1\n        assert (\n            str(provider.authorization_servers[0])\n            == \"https://abc123.supabase.co/auth/v1\"\n        )\n\n    @pytest.mark.parametrize(\n        \"algorithm\",\n        [\"RS256\", \"ES256\"],\n    )\n    def test_algorithm_configuration(self, algorithm):\n        \"\"\"Test that algorithm can be configured for different JWT signing methods.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n            algorithm=algorithm,\n        )\n\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.algorithm == algorithm\n\n    def test_algorithm_rejects_hs256(self):\n        \"\"\"Test that HS256 is rejected for Supabase's JWKS-based verifier.\"\"\"\n        with pytest.raises(ValueError, match=\"cannot be used with jwks_uri\"):\n            SupabaseProvider(\n                project_url=\"https://abc123.supabase.co\",\n                base_url=\"https://myserver.com\",\n                algorithm=\"HS256\",  # type: ignore[arg-type]\n            )\n\n    def test_algorithm_default_es256(self):\n        \"\"\"Test that algorithm defaults to ES256 when not specified.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n        )\n\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.algorithm == \"ES256\"\n\n    def test_algorithm_from_parameter(self):\n        \"\"\"Test that algorithm can be configured via parameter.\"\"\"\n        provider = SupabaseProvider(\n            project_url=\"https://env123.supabase.co\",\n            base_url=\"https://envserver.com\",\n            algorithm=\"RS256\",\n        )\n\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert provider.token_verifier.algorithm == \"RS256\"\n\n    def test_custom_auth_route(self):\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n            auth_route=\"/custom/auth/route\",\n        )\n\n        assert provider.auth_route == \"custom/auth/route\"\n        assert isinstance(provider.token_verifier, JWTVerifier)\n        assert (\n            provider.token_verifier.jwks_uri\n            == \"https://abc123.supabase.co/custom/auth/route/.well-known/jwks.json\"\n        )\n\n    def test_custom_auth_route_trailing_slash(self):\n        provider = SupabaseProvider(\n            project_url=\"https://abc123.supabase.co\",\n            base_url=\"https://myserver.com\",\n            auth_route=\"/custom/auth/route/\",\n        )\n\n        assert provider.auth_route == \"custom/auth/route\"\n\n\ndef run_mcp_server(host: str, port: int) -> None:\n    mcp = FastMCP(\n        auth=SupabaseProvider(\n            project_url=\"https://test123.supabase.co\",\n            base_url=\"http://localhost:4321\",\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    mcp.run(host=host, port=port, transport=\"http\")\n\n\n@pytest.fixture\ndef mcp_server_url() -> Generator[str]:\n    with run_server_in_process(run_mcp_server) as url:\n        yield f\"{url}/mcp\"\n\n\n@pytest.fixture()\ndef client_with_headless_oauth(\n    mcp_server_url: str,\n) -> Generator[Client, None, None]:\n    \"\"\"Client with headless OAuth that bypasses browser interaction.\"\"\"\n    client = Client(\n        transport=StreamableHttpTransport(mcp_server_url),\n        auth=HeadlessOAuth(mcp_url=mcp_server_url),\n    )\n    yield client\n\n\nclass TestSupabaseProviderIntegration:\n    async def test_unauthorized_access(self, mcp_server_url: str):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url) as client:\n                tools = await client.list_tools()  # noqa: F841\n\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    # async def test_authorized_access(self, client_with_headless_oauth: Client):\n    #     async with client_with_headless_oauth:\n    #         tools = await client_with_headless_oauth.list_tools()\n    #     assert tools is not None\n    #     assert len(tools) > 0\n    #     assert \"add\" in tools\n"
  },
  {
    "path": "tests/server/auth/providers/test_workos.py",
    "content": "\"\"\"Tests for WorkOS OAuth provider.\"\"\"\n\nfrom urllib.parse import urlparse\n\nimport httpx\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom pytest_httpx import HTTPXMock\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.transports import StreamableHttpTransport\nfrom fastmcp.server.auth.providers.workos import (\n    AuthKitProvider,\n    WorkOSProvider,\n    WorkOSTokenVerifier,\n)\nfrom fastmcp.utilities.tests import HeadlessOAuth, run_server_async\n\n\n@pytest.fixture\ndef memory_storage() -> MemoryStore:\n    \"\"\"Provide a MemoryStore for tests to avoid SQLite initialization on Windows.\"\"\"\n    return MemoryStore()\n\n\nclass TestWorkOSProvider:\n    \"\"\"Test WorkOS OAuth provider functionality.\"\"\"\n\n    def test_init_with_explicit_params(self, memory_storage: MemoryStore):\n        \"\"\"Test WorkOSProvider initialization with explicit parameters.\"\"\"\n        provider = WorkOSProvider(\n            client_id=\"client_test123\",\n            client_secret=\"secret_test456\",\n            authkit_domain=\"https://test.authkit.app\",\n            base_url=\"https://myserver.com\",\n            required_scopes=[\"openid\", \"profile\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        assert provider._upstream_client_id == \"client_test123\"\n        assert provider._upstream_client_secret is not None\n        assert provider._upstream_client_secret.get_secret_value() == \"secret_test456\"\n        assert str(provider.base_url) == \"https://myserver.com/\"\n\n    def test_authkit_domain_https_prefix_handling(self, memory_storage: MemoryStore):\n        \"\"\"Test that authkit_domain handles missing https:// prefix.\"\"\"\n        # Without https:// - should add it\n        provider1 = WorkOSProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            authkit_domain=\"test.authkit.app\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        parsed = urlparse(provider1._upstream_authorization_endpoint)\n        assert parsed.scheme == \"https\"\n        assert parsed.netloc == \"test.authkit.app\"\n        assert parsed.path == \"/oauth2/authorize\"\n\n        # With https:// - should keep it\n        provider2 = WorkOSProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            authkit_domain=\"https://test.authkit.app\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        parsed = urlparse(provider2._upstream_authorization_endpoint)\n        assert parsed.scheme == \"https\"\n        assert parsed.netloc == \"test.authkit.app\"\n        assert parsed.path == \"/oauth2/authorize\"\n\n        # With http:// - should be preserved\n        provider3 = WorkOSProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            authkit_domain=\"http://localhost:8080\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n        parsed = urlparse(provider3._upstream_authorization_endpoint)\n        assert parsed.scheme == \"http\"\n        assert parsed.netloc == \"localhost:8080\"\n        assert parsed.path == \"/oauth2/authorize\"\n\n    def test_init_defaults(self, memory_storage: MemoryStore):\n        \"\"\"Test that default values are applied correctly.\"\"\"\n        provider = WorkOSProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            authkit_domain=\"https://test.authkit.app\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check defaults\n        assert provider._redirect_path == \"/auth/callback\"\n        # WorkOS provider has no default scopes but we can't easily verify without accessing internals\n\n    def test_oauth_endpoints_configured_correctly(self, memory_storage: MemoryStore):\n        \"\"\"Test that OAuth endpoints are configured correctly.\"\"\"\n        provider = WorkOSProvider(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            authkit_domain=\"https://test.authkit.app\",\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=memory_storage,\n        )\n\n        # Check that endpoints use the authkit domain\n        assert (\n            provider._upstream_authorization_endpoint\n            == \"https://test.authkit.app/oauth2/authorize\"\n        )\n        assert (\n            provider._upstream_token_endpoint == \"https://test.authkit.app/oauth2/token\"\n        )\n        assert (\n            provider._upstream_revocation_endpoint is None\n        )  # WorkOS doesn't support revocation\n\n\n@pytest.fixture\nasync def mcp_server_url():\n    \"\"\"Start AuthKit server.\"\"\"\n    mcp = FastMCP(\n        auth=AuthKitProvider(\n            authkit_domain=\"https://respectful-lullaby-34-staging.authkit.app\",\n            base_url=\"http://localhost:4321\",\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    async with run_server_async(mcp, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\ndef client_with_headless_oauth(mcp_server_url: str) -> Client:\n    \"\"\"Client with headless OAuth that bypasses browser interaction.\"\"\"\n    return Client(\n        transport=StreamableHttpTransport(mcp_server_url),\n        auth=HeadlessOAuth(mcp_url=mcp_server_url),\n    )\n\n\nclass TestAuthKitProvider:\n    async def test_unauthorized_access(\n        self, memory_storage: MemoryStore, mcp_server_url: str\n    ):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url) as client:\n                tools = await client.list_tools()  # noqa: F841\n\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    # async def test_authorized_access(self, client_with_headless_oauth: Client):\n    #     async with client_with_headless_oauth:\n    #         tools = await client_with_headless_oauth.list_tools()\n    #     assert tools is not None\n    #     assert len(tools) > 0\n    #     assert \"add\" in tools\n\n\nclass TestWorkOSTokenVerifierScopes:\n    async def test_verify_token_rejects_missing_required_scopes(\n        self, httpx_mock: HTTPXMock\n    ):\n        httpx_mock.add_response(\n            url=\"https://test.authkit.app/oauth2/userinfo\",\n            status_code=200,\n            json={\n                \"sub\": \"user_123\",\n                \"email\": \"user@example.com\",\n                \"scope\": \"openid profile\",\n            },\n        )\n\n        verifier = WorkOSTokenVerifier(\n            authkit_domain=\"https://test.authkit.app\",\n            required_scopes=[\"read:secrets\"],\n        )\n\n        result = await verifier.verify_token(\"token\")\n\n        assert result is None\n\n    async def test_verify_token_returns_actual_token_scopes(\n        self, httpx_mock: HTTPXMock\n    ):\n        httpx_mock.add_response(\n            url=\"https://test.authkit.app/oauth2/userinfo\",\n            status_code=200,\n            json={\n                \"sub\": \"user_123\",\n                \"email\": \"user@example.com\",\n                \"scope\": \"openid profile read:secrets\",\n            },\n        )\n\n        verifier = WorkOSTokenVerifier(\n            authkit_domain=\"https://test.authkit.app\",\n            required_scopes=[\"read:secrets\"],\n        )\n\n        result = await verifier.verify_token(\"token\")\n\n        assert result is not None\n        assert result.scopes == [\"openid\", \"profile\", \"read:secrets\"]\n"
  },
  {
    "path": "tests/server/auth/test_auth_provider.py",
    "content": "import re\n\nimport httpx\nimport pytest\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import RemoteAuthProvider\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n\nclass TestAuthProviderBase:\n    \"\"\"Test suite for base AuthProvider behaviors that apply to all auth providers.\"\"\"\n\n    @pytest.fixture\n    def basic_remote_provider(self):\n        \"\"\"Basic RemoteAuthProvider fixture for testing base AuthProvider behaviors.\"\"\"\n        # Create a static token verifier with a test token\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\", \"write\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        return RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://my-server.com\",\n        )\n\n    async def test_www_authenticate_header_points_to_base_url(\n        self, basic_remote_provider\n    ):\n        \"\"\"Test that WWW-Authenticate header points to RFC 9728-compliant metadata URL.\n\n        The WWW-Authenticate header includes the resource path per RFC 9728,\n        so clients can discover where the metadata is actually registered.\n        \"\"\"\n        mcp = FastMCP(\"test-server\", auth=basic_remote_provider)\n        # Mount MCP at a non-root path\n        mcp_http_app = mcp.http_app(path=\"/api/v1/mcp\")\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            # Make unauthorized request to MCP endpoint\n            response = await client.get(\"/api/v1/mcp\")\n            assert response.status_code == 401\n\n            www_auth = response.headers.get(\"www-authenticate\", \"\")\n            assert \"resource_metadata=\" in www_auth\n\n            # Extract the metadata URL from the header\n            match = re.search(r'resource_metadata=\"([^\"]+)\"', www_auth)\n            assert match is not None\n            metadata_url = match.group(1)\n\n            # The metadata URL includes the resource path per RFC 9728\n            assert (\n                metadata_url\n                == \"https://my-server.com/.well-known/oauth-protected-resource/api/v1/mcp\"\n            )\n\n    async def test_automatic_resource_url_capture(self, basic_remote_provider):\n        \"\"\"Test that resource URL is automatically captured from MCP path.\n\n        This test verifies PR #1682 functionality where the resource URL\n        should be automatically set based on the MCP endpoint path.\n        \"\"\"\n        mcp = FastMCP(\"test-server\", auth=basic_remote_provider)\n        # Mount MCP at a specific path\n        mcp_http_app = mcp.http_app(path=\"/mcp\")\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            # The .well-known metadata is at a path-aware location per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n            assert response.status_code == 200\n\n            data = response.json()\n            # The resource URL should be automatically set to the MCP path\n            assert data.get(\"resource\") == \"https://my-server.com/mcp\"\n\n    async def test_automatic_resource_url_with_nested_path(self, basic_remote_provider):\n        \"\"\"Test automatic resource URL capture with deeply nested MCP path.\"\"\"\n        mcp = FastMCP(\"test-server\", auth=basic_remote_provider)\n        mcp_http_app = mcp.http_app(path=\"/api/v2/services/mcp\")\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            # The .well-known metadata includes the resource path per RFC 9728\n            response = await client.get(\n                \"/.well-known/oauth-protected-resource/api/v2/services/mcp\"\n            )\n            assert response.status_code == 200\n\n            data = response.json()\n            # Should automatically capture the nested path\n            assert data.get(\"resource\") == \"https://my-server.com/api/v2/services/mcp\"\n"
  },
  {
    "path": "tests/server/auth/test_authorization.py",
    "content": "\"\"\"Tests for authorization checks and AuthMiddleware.\"\"\"\n\nfrom unittest.mock import Mock\n\nimport mcp.types as mcp_types\nimport pytest\nfrom mcp.server.auth.middleware.auth_context import auth_context_var\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.exceptions import AuthorizationError\nfrom fastmcp.server.auth import (\n    AccessToken,\n    AuthContext,\n    require_scopes,\n    restrict_tag,\n    run_auth_checks,\n)\nfrom fastmcp.server.middleware import AuthMiddleware\nfrom fastmcp.server.transforms import ToolTransform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig, TransformedTool\n\n# =============================================================================\n# Test helpers\n# =============================================================================\n\n\ndef make_token(scopes: list[str] | None = None) -> AccessToken:\n    \"\"\"Create a test access token.\"\"\"\n    return AccessToken(\n        token=\"test-token\",\n        client_id=\"test-client\",\n        scopes=scopes or [],\n        expires_at=None,\n        claims={},\n    )\n\n\ndef make_tool() -> Mock:\n    \"\"\"Create a mock tool for testing.\"\"\"\n    tool = Mock()\n    tool.tags = set()\n    return tool\n\n\n# =============================================================================\n# Tests for require_scopes\n# =============================================================================\n\n\nclass TestRequireScopes:\n    def test_returns_true_with_matching_scope(self):\n        token = make_token(scopes=[\"admin\"])\n        ctx = AuthContext(token=token, component=make_tool())\n        check = require_scopes(\"admin\")\n        assert check(ctx) is True\n\n    def test_returns_true_with_all_required_scopes(self):\n        token = make_token(scopes=[\"read\", \"write\", \"admin\"])\n        ctx = AuthContext(token=token, component=make_tool())\n        check = require_scopes(\"read\", \"write\")\n        assert check(ctx) is True\n\n    def test_returns_false_with_missing_scope(self):\n        token = make_token(scopes=[\"read\"])\n        ctx = AuthContext(token=token, component=make_tool())\n        check = require_scopes(\"admin\")\n        assert check(ctx) is False\n\n    def test_returns_false_with_partial_scopes(self):\n        token = make_token(scopes=[\"read\"])\n        ctx = AuthContext(token=token, component=make_tool())\n        check = require_scopes(\"read\", \"write\")\n        assert check(ctx) is False\n\n    def test_returns_false_without_token(self):\n        ctx = AuthContext(token=None, component=make_tool())\n        check = require_scopes(\"admin\")\n        assert check(ctx) is False\n\n\n# =============================================================================\n# Tests for restrict_tag\n# =============================================================================\n\n\nclass TestRestrictTag:\n    def test_allows_access_when_tag_not_present(self):\n        tool = make_tool()\n        tool.tags = {\"other\"}\n        ctx = AuthContext(token=None, component=tool)\n        check = restrict_tag(\"admin\", scopes=[\"admin\"])\n        assert check(ctx) is True\n\n    def test_blocks_access_when_tag_present_without_token(self):\n        tool = make_tool()\n        tool.tags = {\"admin\"}\n        ctx = AuthContext(token=None, component=tool)\n        check = restrict_tag(\"admin\", scopes=[\"admin\"])\n        assert check(ctx) is False\n\n    def test_blocks_access_when_tag_present_without_scope(self):\n        tool = make_tool()\n        tool.tags = {\"admin\"}\n        token = make_token(scopes=[\"read\"])\n        ctx = AuthContext(token=token, component=tool)\n        check = restrict_tag(\"admin\", scopes=[\"admin\"])\n        assert check(ctx) is False\n\n    def test_allows_access_when_tag_present_with_scope(self):\n        tool = make_tool()\n        tool.tags = {\"admin\"}\n        token = make_token(scopes=[\"admin\"])\n        ctx = AuthContext(token=token, component=tool)\n        check = restrict_tag(\"admin\", scopes=[\"admin\"])\n        assert check(ctx) is True\n\n\n# =============================================================================\n# Tests for run_auth_checks\n# =============================================================================\n\n\nclass TestRunAuthChecks:\n    async def test_single_check_passes(self):\n        ctx = AuthContext(token=make_token(scopes=[\"test\"]), component=make_tool())\n        assert await run_auth_checks(require_scopes(\"test\"), ctx) is True\n\n    async def test_single_check_fails(self):\n        ctx = AuthContext(token=None, component=make_tool())\n        assert await run_auth_checks(require_scopes(\"test\"), ctx) is False\n\n    async def test_multiple_checks_all_pass(self):\n        token = make_token(scopes=[\"test\", \"admin\"])\n        ctx = AuthContext(token=token, component=make_tool())\n        checks = [require_scopes(\"test\"), require_scopes(\"admin\")]\n        assert await run_auth_checks(checks, ctx) is True\n\n    async def test_multiple_checks_one_fails(self):\n        token = make_token(scopes=[\"read\"])\n        ctx = AuthContext(token=token, component=make_tool())\n        checks = [require_scopes(\"read\"), require_scopes(\"admin\")]\n        assert await run_auth_checks(checks, ctx) is False\n\n    async def test_empty_list_passes(self):\n        ctx = AuthContext(token=None, component=make_tool())\n        assert await run_auth_checks([], ctx) is True\n\n    async def test_custom_lambda_check(self):\n        token = make_token()\n        token.claims = {\"level\": 5}\n        ctx = AuthContext(token=token, component=make_tool())\n\n        def check(ctx: AuthContext) -> bool:\n            return ctx.token is not None and ctx.token.claims.get(\"level\", 0) >= 3\n\n        assert await run_auth_checks(check, ctx) is True\n\n    async def test_authorization_error_propagates(self):\n        \"\"\"AuthorizationError from auth check should propagate with custom message.\"\"\"\n\n        def custom_auth_check(ctx: AuthContext) -> bool:\n            raise AuthorizationError(\"Custom denial reason\")\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        with pytest.raises(AuthorizationError, match=\"Custom denial reason\"):\n            await run_auth_checks(custom_auth_check, ctx)\n\n    async def test_generic_exception_is_masked(self):\n        \"\"\"Generic exceptions from auth checks should be masked (return False).\"\"\"\n\n        def buggy_auth_check(ctx: AuthContext) -> bool:\n            raise ValueError(\"Unexpected internal error\")\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        # Should return False, not raise the ValueError\n        assert await run_auth_checks(buggy_auth_check, ctx) is False\n\n    async def test_authorization_error_stops_chain(self):\n        \"\"\"AuthorizationError should stop the check chain and propagate.\"\"\"\n        call_order = []\n\n        def check_1(ctx: AuthContext) -> bool:\n            call_order.append(1)\n            return True\n\n        def check_2(ctx: AuthContext) -> bool:\n            call_order.append(2)\n            raise AuthorizationError(\"Explicit denial\")\n\n        def check_3(ctx: AuthContext) -> bool:\n            call_order.append(3)\n            return True\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        with pytest.raises(AuthorizationError, match=\"Explicit denial\"):\n            await run_auth_checks([check_1, check_2, check_3], ctx)\n\n        # Check 3 should not be called\n        assert call_order == [1, 2]\n\n    async def test_async_check_passes(self):\n        \"\"\"Async auth check functions should be awaited.\"\"\"\n\n        async def async_check(ctx: AuthContext) -> bool:\n            return ctx.token is not None\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        assert await run_auth_checks(async_check, ctx) is True\n\n    async def test_async_check_fails(self):\n        \"\"\"Async auth check that returns False should deny access.\"\"\"\n\n        async def async_check(ctx: AuthContext) -> bool:\n            return False\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        assert await run_auth_checks(async_check, ctx) is False\n\n    async def test_mixed_sync_and_async_checks(self):\n        \"\"\"A mix of sync and async checks should all be evaluated.\"\"\"\n\n        def sync_check(ctx: AuthContext) -> bool:\n            return True\n\n        async def async_check(ctx: AuthContext) -> bool:\n            return ctx.token is not None\n\n        ctx = AuthContext(token=make_token(scopes=[\"test\"]), component=make_tool())\n        checks = [sync_check, async_check, require_scopes(\"test\")]\n        assert await run_auth_checks(checks, ctx) is True\n\n    async def test_async_check_exception_is_masked(self):\n        \"\"\"Async checks that raise non-AuthorizationError should be masked.\"\"\"\n\n        async def buggy_async_check(ctx: AuthContext) -> bool:\n            raise ValueError(\"async error\")\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        assert await run_auth_checks(buggy_async_check, ctx) is False\n\n    async def test_async_check_authorization_error_propagates(self):\n        \"\"\"Async checks that raise AuthorizationError should propagate.\"\"\"\n\n        async def async_denial(ctx: AuthContext) -> bool:\n            raise AuthorizationError(\"Async denial\")\n\n        ctx = AuthContext(token=make_token(), component=make_tool())\n        with pytest.raises(AuthorizationError, match=\"Async denial\"):\n            await run_auth_checks(async_denial, ctx)\n\n\n# =============================================================================\n# Tests for tool-level auth with FastMCP\n# =============================================================================\n\n\ndef set_token(token: AccessToken | None):\n    \"\"\"Set the access token in the auth context var.\"\"\"\n    if token is None:\n        return auth_context_var.set(None)\n    return auth_context_var.set(AuthenticatedUser(token))\n\n\nclass TestToolLevelAuth:\n    async def test_tool_without_auth_is_visible(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"public_tool\"\n\n    async def test_tool_with_auth_hidden_without_token(self):\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool() -> str:\n            return \"protected\"\n\n        # No token set - tool should be hidden\n        tools = await mcp.list_tools()\n        assert len(tools) == 0\n\n    async def test_tool_with_auth_visible_with_token(self):\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool() -> str:\n            return \"protected\"\n\n        # Set token in context\n        token = make_token(scopes=[\"test\"])\n        tok = set_token(token)\n        try:\n            tools = await mcp.list_tools()\n            assert len(tools) == 1\n            assert tools[0].name == \"protected_tool\"\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_tool_with_scope_auth_hidden_without_scope(self):\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"admin\"))\n        def admin_tool() -> str:\n            return \"admin\"\n\n        # Token without admin scope\n        token = make_token(scopes=[\"read\"])\n        tok = set_token(token)\n        try:\n            tools = await mcp.list_tools()\n            assert len(tools) == 0\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_tool_with_scope_auth_visible_with_scope(self):\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"admin\"))\n        def admin_tool() -> str:\n            return \"admin\"\n\n        # Token with admin scope\n        token = make_token(scopes=[\"admin\"])\n        tok = set_token(token)\n        try:\n            tools = await mcp.list_tools()\n            assert len(tools) == 1\n            assert tools[0].name == \"admin_tool\"\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_get_tool_returns_none_without_auth(self):\n        \"\"\"get_tool() returns None for unauthorized tools (consistent with list filtering).\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool() -> str:\n            return \"protected\"\n\n        # get_tool() returns None for unauthorized tools\n        tool = await mcp.get_tool(\"protected_tool\")\n        assert tool is None\n\n    async def test_get_tool_returns_tool_with_auth(self):\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool() -> str:\n            return \"protected\"\n\n        token = make_token(scopes=[\"test\"])\n        tok = set_token(token)\n        try:\n            tool = await mcp.get_tool(\"protected_tool\")\n            assert tool is not None\n            assert tool.name == \"protected_tool\"\n        finally:\n            auth_context_var.reset(tok)\n\n\n# =============================================================================\n# Tests for AuthMiddleware\n# =============================================================================\n\n\nclass TestAuthMiddleware:\n    \"\"\"Tests for middleware filtering via MCP handler layer.\n\n    These tests call _list_tools_mcp() which applies middleware during list,\n    simulating what happens when a client calls list_tools over MCP.\n    \"\"\"\n\n    async def test_middleware_filters_tools_without_token(self):\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"test\"))])\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        # No token - all tools filtered by middleware\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        assert len(result.tools) == 0\n\n    async def test_middleware_allows_tools_with_token(self):\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"test\"))])\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        token = make_token(scopes=[\"test\"])\n        tok = set_token(token)\n        try:\n            result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n            assert len(result.tools) == 1\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_middleware_with_scope_check(self):\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"api\"))])\n\n        @mcp.tool\n        def api_tool() -> str:\n            return \"api\"\n\n        # Token without api scope\n        token = make_token(scopes=[\"read\"])\n        tok = set_token(token)\n        try:\n            result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n            assert len(result.tools) == 0\n        finally:\n            auth_context_var.reset(tok)\n\n        # Token with api scope\n        token = make_token(scopes=[\"api\"])\n        tok = set_token(token)\n        try:\n            result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n            assert len(result.tools) == 1\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_middleware_with_restrict_tag(self):\n        mcp = FastMCP(\n            middleware=[AuthMiddleware(auth=restrict_tag(\"admin\", scopes=[\"admin\"]))]\n        )\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        @mcp.tool(tags={\"admin\"})\n        def admin_tool() -> str:\n            return \"admin\"\n\n        # No token - public tool allowed, admin tool blocked\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        assert len(result.tools) == 1\n        assert result.tools[0].name == \"public_tool\"\n\n        # Token with admin scope - both allowed\n        token = make_token(scopes=[\"admin\"])\n        tok = set_token(token)\n        try:\n            result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n            assert len(result.tools) == 2\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_middleware_skips_tool_on_authorization_error(self):\n        def deny_blocked_tool(ctx: AuthContext) -> bool:\n            if ctx.component.name == \"blocked_tool\":\n                raise AuthorizationError(f\"deny {ctx.component.name}\")\n            return True\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=deny_blocked_tool)])\n\n        @mcp.tool\n        def blocked_tool() -> str:\n            return \"blocked\"\n\n        @mcp.tool\n        def allowed_tool() -> str:\n            return \"allowed\"\n\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        assert [tool.name for tool in result.tools] == [\"allowed_tool\"]\n\n    async def test_middleware_skips_resource_on_authorization_error(self):\n        def deny_blocked_resource(ctx: AuthContext) -> bool:\n            if ctx.component.name == \"blocked_resource\":\n                raise AuthorizationError(f\"deny {ctx.component.name}\")\n            return True\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=deny_blocked_resource)])\n\n        @mcp.resource(\"resource://blocked\")\n        def blocked_resource() -> str:\n            return \"blocked\"\n\n        @mcp.resource(\"resource://allowed\")\n        def allowed_resource() -> str:\n            return \"allowed\"\n\n        result = await mcp._list_resources_mcp(mcp_types.ListResourcesRequest())\n        assert [str(resource.uri) for resource in result.resources] == [\n            \"resource://allowed\"\n        ]\n\n    async def test_middleware_skips_resource_template_on_authorization_error(self):\n        def deny_blocked_resource_template(ctx: AuthContext) -> bool:\n            if ctx.component.name == \"blocked_resource_template\":\n                raise AuthorizationError(f\"deny {ctx.component.name}\")\n            return True\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=deny_blocked_resource_template)])\n\n        @mcp.resource(\"resource://blocked/{item}\")\n        def blocked_resource_template(item: str) -> str:\n            return item\n\n        @mcp.resource(\"resource://allowed/{item}\")\n        def allowed_resource_template(item: str) -> str:\n            return item\n\n        result = await mcp._list_resource_templates_mcp(\n            mcp_types.ListResourceTemplatesRequest()\n        )\n        assert [template.uriTemplate for template in result.resourceTemplates] == [\n            \"resource://allowed/{item}\"\n        ]\n\n    async def test_middleware_skips_prompt_on_authorization_error(self):\n        def deny_blocked_prompt(ctx: AuthContext) -> bool:\n            if ctx.component.name == \"blocked_prompt\":\n                raise AuthorizationError(f\"deny {ctx.component.name}\")\n            return True\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=deny_blocked_prompt)])\n\n        @mcp.prompt\n        def blocked_prompt() -> str:\n            return \"blocked\"\n\n        @mcp.prompt\n        def allowed_prompt() -> str:\n            return \"allowed\"\n\n        result = await mcp._list_prompts_mcp(mcp_types.ListPromptsRequest())\n        assert [prompt.name for prompt in result.prompts] == [\"allowed_prompt\"]\n\n\n# =============================================================================\n# Integration tests with Client\n# =============================================================================\n\n\nclass TestAuthIntegration:\n    async def test_client_only_sees_authorized_tools(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool() -> str:\n            return \"protected\"\n\n        async with Client(mcp) as client:\n            # No token - only public tool visible\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            assert tools[0].name == \"public_tool\"\n\n    async def test_client_with_token_sees_all_authorized_tools(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool() -> str:\n            return \"protected\"\n\n        # Set token before creating client\n        token = make_token(scopes=[\"test\"])\n        tok = set_token(token)\n        try:\n            async with Client(mcp) as client:\n                tools = await client.list_tools()\n                tool_names = [t.name for t in tools]\n                # With token, both tools should be visible\n                assert \"public_tool\" in tool_names\n                assert \"protected_tool\" in tool_names\n        finally:\n            auth_context_var.reset(tok)\n\n\n# =============================================================================\n# Integration tests with async auth checks\n# =============================================================================\n\n\nclass TestAsyncAuthIntegration:\n    async def test_async_auth_check_filters_tool_listing(self):\n        \"\"\"Async auth checks should work for filtering tool lists.\"\"\"\n        mcp = FastMCP()\n\n        async def check_claims(ctx: AuthContext) -> bool:\n            return ctx.token is not None and ctx.token.claims.get(\"role\") == \"admin\"\n\n        @mcp.tool(auth=check_claims)\n        def admin_tool() -> str:\n            return \"admin\"\n\n        @mcp.tool\n        def public_tool() -> str:\n            return \"public\"\n\n        # Without token, only public tool visible\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"public_tool\"\n\n        # With correct claims, both visible\n        token = make_token()\n        token.claims = {\"role\": \"admin\"}\n        tok = set_token(token)\n        try:\n            tools = await mcp.list_tools()\n            assert len(tools) == 2\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_async_auth_check_on_tool_call(self):\n        \"\"\"Async auth checks should work for tool execution via client.\"\"\"\n        mcp = FastMCP()\n\n        async def check_claims(ctx: AuthContext) -> bool:\n            return ctx.token is not None and ctx.token.claims.get(\"role\") == \"admin\"\n\n        @mcp.tool(auth=check_claims)\n        def admin_tool() -> str:\n            return \"secret\"\n\n        token = make_token()\n        token.claims = {\"role\": \"admin\"}\n        tok = set_token(token)\n        try:\n            async with Client(mcp) as client:\n                result = await client.call_tool(\"admin_tool\", {})\n                assert result.content[0].text == \"secret\"\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_async_auth_middleware(self):\n        \"\"\"Async auth checks should work with AuthMiddleware.\"\"\"\n\n        async def async_scope_check(ctx: AuthContext) -> bool:\n            return ctx.token is not None and \"api\" in ctx.token.scopes\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=async_scope_check)])\n\n        @mcp.tool\n        def api_tool() -> str:\n            return \"api\"\n\n        # Without token, tool is hidden\n        result = await mcp._list_tools_mcp(__import__(\"mcp\").types.ListToolsRequest())\n        assert len(result.tools) == 0\n\n        # With token containing \"api\" scope, tool is visible\n        token = make_token(scopes=[\"api\"])\n        tok = set_token(token)\n        try:\n            result = await mcp._list_tools_mcp(\n                __import__(\"mcp\").types.ListToolsRequest()\n            )\n            assert len(result.tools) == 1\n        finally:\n            auth_context_var.reset(tok)\n\n\n# =============================================================================\n# Tests for transformed tools preserving auth\n# =============================================================================\n\n\nclass TestTransformedToolAuth:\n    async def test_transformed_tool_preserves_auth(self):\n        \"\"\"Transformed tools should inherit auth from parent.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool(x: int) -> str:\n            return str(x)\n\n        # Get the tool and transform it\n        tools = await mcp._local_provider.list_tools()\n        original_tool = tools[0]\n        assert original_tool.auth is not None\n\n        # Transform the tool\n        transformed = TransformedTool.from_tool(\n            original_tool,\n            name=\"transformed_protected\",\n        )\n\n        # Auth should be preserved\n        assert transformed.auth is not None\n        assert transformed.auth == original_tool.auth\n\n    async def test_transformed_tool_filtered_without_token(self):\n        \"\"\"Transformed tools with auth should be filtered without token.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool(x: int) -> str:\n            return str(x)\n\n        # Add transformation\n        mcp.add_transform(\n            ToolTransform(\n                {\"protected_tool\": ToolTransformConfig(name=\"renamed_protected\")}\n            )\n        )\n\n        # Without token, transformed tool should not be visible\n        tools = await mcp.list_tools()\n        assert len(tools) == 0\n\n    async def test_transformed_tool_visible_with_token(self):\n        \"\"\"Transformed tools with auth should be visible with token.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(auth=require_scopes(\"test\"))\n        def protected_tool(x: int) -> str:\n            return str(x)\n\n        # Add transformation\n        mcp.add_transform(\n            ToolTransform(\n                {\"protected_tool\": ToolTransformConfig(name=\"renamed_protected\")}\n            )\n        )\n\n        # With token, transformed tool should be visible\n        token = make_token(scopes=[\"test\"])\n        tok = set_token(token)\n        try:\n            tools = await mcp.list_tools()\n            assert len(tools) == 1\n            assert tools[0].name == \"renamed_protected\"\n        finally:\n            auth_context_var.reset(tok)\n\n\n# =============================================================================\n# Tests for AuthMiddleware on_call_tool enforcement\n# =============================================================================\n\n\nclass TestAuthMiddlewareCallTool:\n    async def test_middleware_blocks_call_without_auth(self):\n        \"\"\"AuthMiddleware should raise AuthorizationError on unauthorized call.\"\"\"\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"test\"))])\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        # Without token, calling the tool should raise AuthorizationError\n        async with Client(mcp) as client:\n            with pytest.raises(Exception) as exc_info:\n                await client.call_tool(\"my_tool\", {})\n            # The error message should indicate authorization failure\n            assert (\n                \"authorization\" in str(exc_info.value).lower()\n                or \"insufficient\" in str(exc_info.value).lower()\n            )\n\n    async def test_middleware_allows_call_with_auth(self):\n        \"\"\"AuthMiddleware should allow tool call with valid token.\"\"\"\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"test\"))])\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        # With token, calling the tool should succeed\n        token = make_token(scopes=[\"test\"])\n        tok = set_token(token)\n        try:\n            async with Client(mcp) as client:\n                result = await client.call_tool(\"my_tool\", {})\n                assert result.content[0].text == \"result\"\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_middleware_blocks_call_with_wrong_scope(self):\n        \"\"\"AuthMiddleware should block calls when scope requirements aren't met.\"\"\"\n\n        mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes(\"admin\"))])\n\n        @mcp.tool\n        def admin_tool() -> str:\n            return \"admin result\"\n\n        # With token that lacks admin scope\n        token = make_token(scopes=[\"read\"])\n        tok = set_token(token)\n        try:\n            async with Client(mcp) as client:\n                with pytest.raises(Exception) as exc_info:\n                    await client.call_tool(\"admin_tool\", {})\n                assert (\n                    \"authorization\" in str(exc_info.value).lower()\n                    or \"insufficient\" in str(exc_info.value).lower()\n                )\n        finally:\n            auth_context_var.reset(tok)\n"
  },
  {
    "path": "tests/server/auth/test_cimd.py",
    "content": "\"\"\"Unit tests for CIMD (Client ID Metadata Document) functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport time\nfrom unittest.mock import patch\n\nimport pytest\nfrom pydantic import AnyHttpUrl, ValidationError\n\nfrom fastmcp.server.auth.cimd import (\n    CIMDDocument,\n    CIMDFetcher,\n    CIMDFetchError,\n    CIMDValidationError,\n)\n\n# Standard public IP used for DNS mocking in tests\nTEST_PUBLIC_IP = \"93.184.216.34\"\n\n\nclass TestCIMDDocument:\n    \"\"\"Tests for CIMDDocument model validation.\"\"\"\n\n    def test_valid_minimal_document(self):\n        \"\"\"Test that minimal valid document passes validation.\"\"\"\n        doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n        )\n        assert str(doc.client_id) == \"https://example.com/client.json\"\n        assert doc.token_endpoint_auth_method == \"none\"\n        assert doc.grant_types == [\"authorization_code\"]\n        assert doc.response_types == [\"code\"]\n\n    def test_valid_full_document(self):\n        \"\"\"Test that full document passes validation.\"\"\"\n        doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            client_name=\"My App\",\n            client_uri=AnyHttpUrl(\"https://example.com\"),\n            logo_uri=AnyHttpUrl(\"https://example.com/logo.png\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n            token_endpoint_auth_method=\"none\",\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            scope=\"read write\",\n        )\n        assert doc.client_name == \"My App\"\n        assert doc.scope == \"read write\"\n\n    def test_private_key_jwt_auth_method_allowed(self):\n        \"\"\"Test that private_key_jwt is allowed for CIMD.\"\"\"\n        doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            jwks_uri=AnyHttpUrl(\"https://example.com/.well-known/jwks.json\"),\n        )\n        assert doc.token_endpoint_auth_method == \"private_key_jwt\"\n\n    def test_client_secret_basic_rejected(self):\n        \"\"\"Test that client_secret_basic is rejected for CIMD.\"\"\"\n        with pytest.raises(ValidationError) as exc_info:\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[\"http://localhost:3000/callback\"],\n                token_endpoint_auth_method=\"client_secret_basic\",  # type: ignore[arg-type] - testing invalid value\n            )\n        # Literal type rejects invalid values before custom validator\n        assert \"token_endpoint_auth_method\" in str(exc_info.value)\n\n    def test_client_secret_post_rejected(self):\n        \"\"\"Test that client_secret_post is rejected for CIMD.\"\"\"\n        with pytest.raises(ValidationError) as exc_info:\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[\"http://localhost:3000/callback\"],\n                token_endpoint_auth_method=\"client_secret_post\",  # type: ignore[arg-type] - testing invalid value\n            )\n        assert \"token_endpoint_auth_method\" in str(exc_info.value)\n\n    def test_client_secret_jwt_rejected(self):\n        \"\"\"Test that client_secret_jwt is rejected for CIMD.\"\"\"\n        with pytest.raises(ValidationError) as exc_info:\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[\"http://localhost:3000/callback\"],\n                token_endpoint_auth_method=\"client_secret_jwt\",  # type: ignore[arg-type] - testing invalid value\n            )\n        assert \"token_endpoint_auth_method\" in str(exc_info.value)\n\n    def test_missing_redirect_uris_rejected(self):\n        \"\"\"Test that redirect_uris is required for CIMD.\"\"\"\n        with pytest.raises(ValidationError) as exc_info:\n            CIMDDocument(client_id=AnyHttpUrl(\"https://example.com/client.json\"))\n        assert \"redirect_uris\" in str(exc_info.value)\n\n    def test_empty_redirect_uris_rejected(self):\n        \"\"\"Test that empty redirect_uris is rejected.\"\"\"\n        with pytest.raises(ValidationError) as exc_info:\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[],\n            )\n        assert \"redirect_uris\" in str(exc_info.value)\n\n    def test_redirect_uri_without_scheme_rejected(self):\n        \"\"\"Test that redirect_uris without a scheme are rejected.\"\"\"\n        with pytest.raises(ValidationError, match=\"must have a scheme\"):\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[\"/just/a/path\"],\n            )\n\n    def test_redirect_uri_without_host_rejected(self):\n        \"\"\"Test that redirect_uris without a host are rejected.\"\"\"\n        with pytest.raises(ValidationError, match=\"must have a host\"):\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[\"http://\"],\n            )\n\n    def test_redirect_uri_whitespace_only_rejected(self):\n        \"\"\"Test that whitespace-only redirect_uris are rejected.\"\"\"\n        with pytest.raises(ValidationError, match=\"non-empty\"):\n            CIMDDocument(\n                client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n                redirect_uris=[\"   \"],\n            )\n\n\nclass TestCIMDFetcher:\n    \"\"\"Tests for CIMDFetcher.\"\"\"\n\n    @pytest.fixture\n    def fetcher(self):\n        \"\"\"Create a CIMDFetcher for testing.\"\"\"\n        return CIMDFetcher()\n\n    def test_is_cimd_client_id_valid_urls(self, fetcher: CIMDFetcher):\n        \"\"\"Test is_cimd_client_id accepts valid CIMD URLs.\"\"\"\n        assert fetcher.is_cimd_client_id(\"https://example.com/client.json\")\n        assert fetcher.is_cimd_client_id(\"https://example.com/path/to/client\")\n        assert fetcher.is_cimd_client_id(\"https://sub.example.com/cimd.json\")\n\n    def test_is_cimd_client_id_rejects_http(self, fetcher: CIMDFetcher):\n        \"\"\"Test is_cimd_client_id rejects HTTP URLs.\"\"\"\n        assert not fetcher.is_cimd_client_id(\"http://example.com/client.json\")\n\n    def test_is_cimd_client_id_rejects_root_path(self, fetcher: CIMDFetcher):\n        \"\"\"Test is_cimd_client_id rejects URLs with no path.\"\"\"\n        assert not fetcher.is_cimd_client_id(\"https://example.com/\")\n        assert not fetcher.is_cimd_client_id(\"https://example.com\")\n\n    def test_is_cimd_client_id_rejects_non_url(self, fetcher: CIMDFetcher):\n        \"\"\"Test is_cimd_client_id rejects non-URL strings.\"\"\"\n        assert not fetcher.is_cimd_client_id(\"client-123\")\n        assert not fetcher.is_cimd_client_id(\"my-client\")\n        assert not fetcher.is_cimd_client_id(\"\")\n        assert not fetcher.is_cimd_client_id(\"not a url\")\n\n    def test_validate_redirect_uri_exact_match(self, fetcher: CIMDFetcher):\n        \"\"\"Test redirect_uri validation with exact match.\"\"\"\n        doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n        )\n        assert fetcher.validate_redirect_uri(doc, \"http://localhost:3000/callback\")\n        assert not fetcher.validate_redirect_uri(doc, \"http://localhost:4000/callback\")\n\n    def test_validate_redirect_uri_wildcard_match(self, fetcher: CIMDFetcher):\n        \"\"\"Test redirect_uri validation with wildcard port.\"\"\"\n        doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:*/callback\"],\n        )\n        assert fetcher.validate_redirect_uri(doc, \"http://localhost:3000/callback\")\n        assert fetcher.validate_redirect_uri(doc, \"http://localhost:8080/callback\")\n        assert not fetcher.validate_redirect_uri(doc, \"http://localhost:3000/other\")\n\n\nclass TestCIMDFetcherHTTP:\n    \"\"\"Tests for CIMDFetcher HTTP fetching (using httpx mock).\n\n    Note: With SSRF protection and DNS pinning, HTTP requests go to the resolved IP\n    instead of the hostname. These tests mock DNS resolution to return a public IP\n    and configure httpx_mock to expect the IP-based URL.\n    \"\"\"\n\n    @pytest.fixture\n    def fetcher(self):\n        \"\"\"Create a CIMDFetcher for testing.\"\"\"\n        return CIMDFetcher()\n\n    @pytest.fixture\n    def mock_dns(self):\n        \"\"\"Mock DNS resolution to return test public IP.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            yield\n\n    async def test_fetch_success(self, fetcher: CIMDFetcher, httpx_mock, mock_dns):\n        \"\"\"Test successful CIMD document fetch.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n\n        # With DNS pinning, request goes to IP. Match any URL.\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\n                \"content-type\": \"application/json\",\n                \"content-length\": \"200\",\n            },\n        )\n\n        doc = await fetcher.fetch(url)\n        assert str(doc.client_id) == url\n        assert doc.client_name == \"Test App\"\n\n    async def test_fetch_ttl_cache(self, fetcher: CIMDFetcher, httpx_mock, mock_dns):\n        \"\"\"Test that fetched documents are cached and served from cache within TTL.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n\n        assert first.client_id == second.client_id\n        assert len(httpx_mock.get_requests()) == 1\n\n    async def test_fetch_cache_control_max_age(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Cache-Control max-age should prevent refetch before expiry.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Max-Age App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"cache-control\": \"max-age=60\", \"content-length\": \"200\"},\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n\n        assert first.client_name == second.client_name\n        assert len(httpx_mock.get_requests()) == 1\n\n    async def test_fetch_etag_revalidation_304(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Expired cache should revalidate with ETag and accept 304.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"ETag App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\n                \"cache-control\": \"max-age=0\",\n                \"etag\": '\"v1\"',\n                \"content-length\": \"200\",\n            },\n        )\n        httpx_mock.add_response(\n            status_code=304,\n            headers={\n                \"cache-control\": \"max-age=120\",\n                \"etag\": '\"v1\"',\n                \"content-length\": \"0\",\n            },\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n        requests = httpx_mock.get_requests()\n\n        assert first.client_name == \"ETag App\"\n        assert second.client_name == \"ETag App\"\n        assert len(requests) == 2\n        assert requests[1].headers.get(\"if-none-match\") == '\"v1\"'\n\n    async def test_fetch_last_modified_revalidation_304(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Expired cache should revalidate with Last-Modified and accept 304.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Last-Modified App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        last_modified = \"Wed, 21 Oct 2015 07:28:00 GMT\"\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\n                \"cache-control\": \"max-age=0\",\n                \"last-modified\": last_modified,\n                \"content-length\": \"200\",\n            },\n        )\n        httpx_mock.add_response(\n            status_code=304,\n            headers={\"cache-control\": \"max-age=120\", \"content-length\": \"0\"},\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n        requests = httpx_mock.get_requests()\n\n        assert first.client_name == \"Last-Modified App\"\n        assert second.client_name == \"Last-Modified App\"\n        assert len(requests) == 2\n        assert requests[1].headers.get(\"if-modified-since\") == last_modified\n\n    async def test_fetch_cache_control_no_store(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Cache-Control no-store should prevent storing CIMD documents.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"No-Store App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"cache-control\": \"no-store\", \"content-length\": \"200\"},\n        )\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"cache-control\": \"no-store\", \"content-length\": \"200\"},\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n\n        assert first.client_name == second.client_name\n        assert len(httpx_mock.get_requests()) == 2\n\n    async def test_fetch_cache_control_no_cache(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Cache-Control no-cache should force revalidation on each fetch.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"No-Cache App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\n                \"cache-control\": \"no-cache\",\n                \"etag\": '\"v2\"',\n                \"content-length\": \"200\",\n            },\n        )\n        httpx_mock.add_response(\n            status_code=304,\n            headers={\n                \"cache-control\": \"no-cache\",\n                \"etag\": '\"v2\"',\n                \"content-length\": \"0\",\n            },\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n        requests = httpx_mock.get_requests()\n\n        assert first.client_name == \"No-Cache App\"\n        assert second.client_name == \"No-Cache App\"\n        assert len(requests) == 2\n        assert requests[1].headers.get(\"if-none-match\") == '\"v2\"'\n\n    async def test_fetch_304_without_cache_headers_preserves_policy(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"304 responses without cache headers should not reset cached policy.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"No-Header-304 App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\n                \"cache-control\": \"no-cache\",\n                \"etag\": '\"v3\"',\n                \"content-length\": \"200\",\n            },\n        )\n        # Intentionally omit cache-control/expires on 304.\n        httpx_mock.add_response(\n            status_code=304,\n            headers={\"content-length\": \"0\"},\n        )\n        httpx_mock.add_response(\n            status_code=304,\n            headers={\"content-length\": \"0\"},\n        )\n\n        first = await fetcher.fetch(url)\n        second = await fetcher.fetch(url)\n        third = await fetcher.fetch(url)\n        requests = httpx_mock.get_requests()\n\n        assert first.client_name == \"No-Header-304 App\"\n        assert second.client_name == \"No-Header-304 App\"\n        assert third.client_name == \"No-Header-304 App\"\n        assert len(requests) == 3\n        assert requests[1].headers.get(\"if-none-match\") == '\"v3\"'\n        assert requests[2].headers.get(\"if-none-match\") == '\"v3\"'\n\n    async def test_fetch_304_without_cache_headers_refreshes_cached_freshness(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"A header-less 304 should renew freshness using cached lifetime.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Headerless 304 Freshness App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\n                \"cache-control\": \"max-age=60\",\n                \"etag\": '\"v4\"',\n                \"content-length\": \"200\",\n            },\n        )\n        httpx_mock.add_response(\n            status_code=304,\n            headers={\"content-length\": \"0\"},\n        )\n\n        first = await fetcher.fetch(url)\n\n        # Simulate cache expiry so the next request triggers revalidation.\n        cached_entry = fetcher._cache[url]\n        cached_entry.expires_at = time.time() - 1\n\n        second = await fetcher.fetch(url)\n        third = await fetcher.fetch(url)\n        requests = httpx_mock.get_requests()\n\n        assert first.client_name == \"Headerless 304 Freshness App\"\n        assert second.client_name == \"Headerless 304 Freshness App\"\n        assert third.client_name == \"Headerless 304 Freshness App\"\n        assert len(requests) == 2\n        assert requests[1].headers.get(\"if-none-match\") == '\"v4\"'\n\n    async def test_fetch_client_id_mismatch(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Test that client_id mismatch is rejected.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": \"https://other.com/client.json\",  # Different URL\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"100\"},\n        )\n\n        with pytest.raises(CIMDValidationError) as exc_info:\n            await fetcher.fetch(url)\n        assert \"mismatch\" in str(exc_info.value).lower()\n\n    async def test_fetch_http_error(self, fetcher: CIMDFetcher, httpx_mock, mock_dns):\n        \"\"\"Test handling of HTTP errors.\"\"\"\n        url = \"https://example.com/client.json\"\n        httpx_mock.add_response(status_code=404)\n\n        with pytest.raises(CIMDFetchError) as exc_info:\n            await fetcher.fetch(url)\n        assert \"404\" in str(exc_info.value)\n\n    async def test_fetch_invalid_json(self, fetcher: CIMDFetcher, httpx_mock, mock_dns):\n        \"\"\"Test handling of invalid JSON response.\"\"\"\n        url = \"https://example.com/client.json\"\n        httpx_mock.add_response(\n            content=b\"not json\",\n            headers={\"content-length\": \"10\"},\n        )\n\n        with pytest.raises(CIMDValidationError) as exc_info:\n            await fetcher.fetch(url)\n        assert \"JSON\" in str(exc_info.value)\n\n    async def test_fetch_invalid_document(\n        self, fetcher: CIMDFetcher, httpx_mock, mock_dns\n    ):\n        \"\"\"Test handling of invalid CIMD document.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"client_secret_basic\",  # Not allowed\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"100\"},\n        )\n\n        with pytest.raises(CIMDValidationError) as exc_info:\n            await fetcher.fetch(url)\n        assert \"Invalid CIMD document\" in str(exc_info.value)\n"
  },
  {
    "path": "tests/server/auth/test_cimd_validators.py",
    "content": "\"\"\"Unit tests for CIMD assertion validators, client manager, and redirect URI enforcement.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import AsyncMock, patch\n\nimport pytest\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.cimd import (\n    CIMDAssertionValidator,\n    CIMDClientManager,\n    CIMDDocument,\n)\nfrom fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient\n\n# Standard public IP used for DNS mocking in tests\nTEST_PUBLIC_IP = \"93.184.216.34\"\n\n\nclass TestCIMDAssertionValidator:\n    \"\"\"Tests for CIMDAssertionValidator (private_key_jwt support).\"\"\"\n\n    @pytest.fixture\n    def validator(self):\n        \"\"\"Create a CIMDAssertionValidator for testing.\"\"\"\n        return CIMDAssertionValidator()\n\n    @pytest.fixture\n    def key_pair(self):\n        \"\"\"Generate RSA key pair for testing.\"\"\"\n        from fastmcp.server.auth.providers.jwt import RSAKeyPair\n\n        return RSAKeyPair.generate()\n\n    @pytest.fixture\n    def jwks(self, key_pair):\n        \"\"\"Create JWKS from key pair.\"\"\"\n        import base64\n\n        from cryptography.hazmat.backends import default_backend\n        from cryptography.hazmat.primitives import serialization\n\n        # Load public key\n        public_key = serialization.load_pem_public_key(\n            key_pair.public_key.encode(), backend=default_backend()\n        )\n\n        # Get RSA public numbers\n        from cryptography.hazmat.primitives.asymmetric import rsa\n\n        if isinstance(public_key, rsa.RSAPublicKey):\n            numbers = public_key.public_numbers()\n\n            # Convert to JWK format\n            return {\n                \"keys\": [\n                    {\n                        \"kty\": \"RSA\",\n                        \"kid\": \"test-key-1\",\n                        \"use\": \"sig\",\n                        \"alg\": \"RS256\",\n                        \"n\": base64.urlsafe_b64encode(\n                            numbers.n.to_bytes((numbers.n.bit_length() + 7) // 8, \"big\")\n                        )\n                        .rstrip(b\"=\")\n                        .decode(),\n                        \"e\": base64.urlsafe_b64encode(\n                            numbers.e.to_bytes((numbers.e.bit_length() + 7) // 8, \"big\")\n                        )\n                        .rstrip(b\"=\")\n                        .decode(),\n                    }\n                ]\n            }\n\n    @pytest.fixture\n    def cimd_doc_with_jwks_uri(self):\n        \"\"\"Create CIMD document with jwks_uri.\"\"\"\n        return CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            jwks_uri=AnyHttpUrl(\"https://example.com/.well-known/jwks.json\"),\n        )\n\n    @pytest.fixture\n    def cimd_doc_with_inline_jwks(self, jwks):\n        \"\"\"Create CIMD document with inline JWKS.\"\"\"\n        return CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            jwks=jwks,\n        )\n\n    async def test_valid_assertion_with_jwks_uri(\n        self, validator, key_pair, cimd_doc_with_jwks_uri, httpx_mock\n    ):\n        \"\"\"Test that valid JWT assertion passes validation (jwks_uri).\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Mock JWKS endpoint\n        import base64\n\n        from cryptography.hazmat.backends import default_backend\n        from cryptography.hazmat.primitives import serialization\n\n        public_key = serialization.load_pem_public_key(\n            key_pair.public_key.encode(), backend=default_backend()\n        )\n        from cryptography.hazmat.primitives.asymmetric import rsa\n\n        assert isinstance(public_key, rsa.RSAPublicKey)\n        numbers = public_key.public_numbers()\n\n        jwks = {\n            \"keys\": [\n                {\n                    \"kty\": \"RSA\",\n                    \"kid\": \"test-key-1\",\n                    \"use\": \"sig\",\n                    \"alg\": \"RS256\",\n                    \"n\": base64.urlsafe_b64encode(\n                        numbers.n.to_bytes((numbers.n.bit_length() + 7) // 8, \"big\")\n                    )\n                    .rstrip(b\"=\")\n                    .decode(),\n                    \"e\": base64.urlsafe_b64encode(\n                        numbers.e.to_bytes((numbers.e.bit_length() + 7) // 8, \"big\")\n                    )\n                    .rstrip(b\"=\")\n                    .decode(),\n                }\n            ]\n        }\n\n        # Mock DNS resolution for SSRF-safe fetch\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            httpx_mock.add_response(json=jwks)\n\n            # Create valid assertion (use short lifetime for security compliance)\n            assertion = key_pair.create_token(\n                subject=client_id,\n                issuer=client_id,\n                audience=token_endpoint,\n                additional_claims={\"jti\": \"unique-jti-123\"},\n                expires_in_seconds=60,  # 1 minute (max allowed is 300s)\n                kid=\"test-key-1\",\n            )\n\n            # Should validate successfully\n            assert await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_jwks_uri\n            )\n\n    async def test_valid_assertion_with_inline_jwks(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that valid JWT assertion passes validation (inline JWKS).\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create valid assertion (use short lifetime for security compliance)\n        assertion = key_pair.create_token(\n            subject=client_id,\n            issuer=client_id,\n            audience=token_endpoint,\n            additional_claims={\"jti\": \"unique-jti-456\"},\n            expires_in_seconds=60,  # 1 minute (max allowed is 300s)\n            kid=\"test-key-1\",\n        )\n\n        # Should validate successfully\n        assert await validator.validate_assertion(\n            assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n        )\n\n    async def test_rejects_wrong_issuer(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that wrong issuer is rejected.\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create assertion with wrong issuer\n        assertion = key_pair.create_token(\n            subject=client_id,\n            issuer=\"https://attacker.com\",  # Wrong!\n            audience=token_endpoint,\n            additional_claims={\"jti\": \"unique-jti-789\"},\n            expires_in_seconds=60,\n            kid=\"test-key-1\",\n        )\n\n        with pytest.raises(ValueError) as exc_info:\n            await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n            )\n        assert \"Invalid JWT assertion\" in str(exc_info.value)\n\n    async def test_rejects_wrong_audience(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that wrong audience is rejected.\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create assertion with wrong audience\n        assertion = key_pair.create_token(\n            subject=client_id,\n            issuer=client_id,\n            audience=\"https://wrong-endpoint.com/token\",  # Wrong!\n            additional_claims={\"jti\": \"unique-jti-abc\"},\n            expires_in_seconds=60,\n            kid=\"test-key-1\",\n        )\n\n        with pytest.raises(ValueError) as exc_info:\n            await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n            )\n        assert \"Invalid JWT assertion\" in str(exc_info.value)\n\n    async def test_rejects_wrong_subject(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that wrong subject claim is rejected.\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create assertion with wrong subject\n        assertion = key_pair.create_token(\n            subject=\"https://different-client.com\",  # Wrong!\n            issuer=client_id,\n            audience=token_endpoint,\n            additional_claims={\"jti\": \"unique-jti-def\"},\n            expires_in_seconds=60,\n            kid=\"test-key-1\",\n        )\n\n        with pytest.raises(ValueError) as exc_info:\n            await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n            )\n        assert \"sub claim must be\" in str(exc_info.value)\n\n    async def test_rejects_missing_jti(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that missing jti claim is rejected.\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create assertion without jti\n        assertion = key_pair.create_token(\n            subject=client_id,\n            issuer=client_id,\n            audience=token_endpoint,\n            # No jti!\n            expires_in_seconds=60,\n            kid=\"test-key-1\",\n        )\n\n        with pytest.raises(ValueError) as exc_info:\n            await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n            )\n        assert \"jti claim\" in str(exc_info.value)\n\n    async def test_rejects_replayed_jti(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that replayed JTI is detected and rejected.\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create assertion\n        assertion = key_pair.create_token(\n            subject=client_id,\n            issuer=client_id,\n            audience=token_endpoint,\n            additional_claims={\"jti\": \"replayed-jti\"},\n            expires_in_seconds=60,\n            kid=\"test-key-1\",\n        )\n\n        # First use should succeed\n        assert await validator.validate_assertion(\n            assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n        )\n\n        # Second use with same jti should fail (replay attack)\n        with pytest.raises(ValueError) as exc_info:\n            await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n            )\n        assert \"replay\" in str(exc_info.value).lower()\n\n    async def test_rejects_expired_token(\n        self, validator, key_pair, cimd_doc_with_inline_jwks\n    ):\n        \"\"\"Test that expired tokens are rejected.\"\"\"\n        client_id = \"https://example.com/client.json\"\n        token_endpoint = \"https://oauth.example.com/token\"\n\n        # Create expired assertion (expired 1 hour ago)\n        assertion = key_pair.create_token(\n            subject=client_id,\n            issuer=client_id,\n            audience=token_endpoint,\n            additional_claims={\"jti\": \"expired-jti\"},\n            expires_in_seconds=-3600,  # Negative = expired\n            kid=\"test-key-1\",\n        )\n\n        with pytest.raises(ValueError) as exc_info:\n            await validator.validate_assertion(\n                assertion, client_id, token_endpoint, cimd_doc_with_inline_jwks\n            )\n        assert \"Invalid JWT assertion\" in str(exc_info.value)\n\n\nclass TestCIMDClientManager:\n    \"\"\"Tests for CIMDClientManager.\"\"\"\n\n    @pytest.fixture\n    def manager(self):\n        \"\"\"Create a CIMDClientManager for testing.\"\"\"\n        return CIMDClientManager(enable_cimd=True)\n\n    @pytest.fixture\n    def disabled_manager(self):\n        \"\"\"Create a disabled CIMDClientManager for testing.\"\"\"\n        return CIMDClientManager(enable_cimd=False)\n\n    @pytest.fixture\n    def mock_dns(self):\n        \"\"\"Mock DNS resolution to return test public IP.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            yield\n\n    def test_is_cimd_client_id_enabled(self, manager):\n        \"\"\"Test CIMD URL detection when enabled.\"\"\"\n        assert manager.is_cimd_client_id(\"https://example.com/client.json\")\n        assert not manager.is_cimd_client_id(\"regular-client-id\")\n\n    def test_is_cimd_client_id_disabled(self, disabled_manager):\n        \"\"\"Test CIMD URL detection when disabled.\"\"\"\n        assert not disabled_manager.is_cimd_client_id(\"https://example.com/client.json\")\n        assert not disabled_manager.is_cimd_client_id(\"regular-client-id\")\n\n    async def test_get_client_success(self, manager, httpx_mock, mock_dns):\n        \"\"\"Test successful CIMD client creation.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        client = await manager.get_client(url)\n        assert client is not None\n        assert client.client_id == url\n        assert client.client_name == \"Test App\"\n        # Verify it uses proxy's patterns (None by default), not document's redirect_uris\n        assert client.allowed_redirect_uri_patterns is None\n\n    async def test_get_client_disabled(self, disabled_manager):\n        \"\"\"Test that get_client returns None when disabled.\"\"\"\n        client = await disabled_manager.get_client(\"https://example.com/client.json\")\n        assert client is None\n\n    async def test_get_client_fetch_failure(self, manager, httpx_mock, mock_dns):\n        \"\"\"Test that get_client returns None on fetch failure.\"\"\"\n        url = \"https://example.com/client.json\"\n        httpx_mock.add_response(status_code=404)\n\n        client = await manager.get_client(url)\n        assert client is None\n\n    # Trust policy and consent bypass tests removed - functionality removed from CIMD\n\n\nclass TestCIMDClientManagerGetClientOptions:\n    \"\"\"Tests for CIMDClientManager.get_client with default_scope and allowed patterns.\"\"\"\n\n    @pytest.fixture\n    def mock_dns(self):\n        \"\"\"Mock DNS resolution to return test public IP.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            yield\n\n    async def test_default_scope_applied_when_doc_has_no_scope(\n        self, httpx_mock, mock_dns\n    ):\n        \"\"\"When the CIMD document omits scope, the manager's default_scope is used.\"\"\"\n\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n            # No scope field\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        manager = CIMDClientManager(\n            enable_cimd=True,\n            default_scope=\"read write admin\",\n        )\n        client = await manager.get_client(url)\n        assert client is not None\n        assert client.scope == \"read write admin\"\n\n    async def test_doc_scope_takes_precedence_over_default(self, httpx_mock, mock_dns):\n        \"\"\"When the CIMD document specifies scope, it wins over the default.\"\"\"\n\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n            \"scope\": \"custom-scope\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        manager = CIMDClientManager(\n            enable_cimd=True,\n            default_scope=\"default-scope\",\n        )\n        client = await manager.get_client(url)\n        assert client is not None\n        assert client.scope == \"custom-scope\"\n\n    async def test_allowed_redirect_uri_patterns_stored_on_client(\n        self, httpx_mock, mock_dns\n    ):\n        \"\"\"Proxy's allowed_redirect_uri_patterns are forwarded to the created client.\"\"\"\n\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            \"redirect_uris\": [\"http://localhost:*/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        patterns = [\"http://localhost:*\", \"https://app.example.com/*\"]\n        manager = CIMDClientManager(\n            enable_cimd=True,\n            allowed_redirect_uri_patterns=patterns,\n        )\n        client = await manager.get_client(url)\n        assert client is not None\n        assert client.allowed_redirect_uri_patterns == patterns\n\n    async def test_cimd_document_attached_to_client(self, httpx_mock, mock_dns):\n        \"\"\"The fetched CIMDDocument is attached to the created client.\"\"\"\n\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Attached Doc App\",\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        manager = CIMDClientManager(enable_cimd=True)\n        client = await manager.get_client(url)\n        assert client is not None\n        assert client.cimd_document is not None\n        assert client.cimd_document.client_name == \"Attached Doc App\"\n        assert str(client.cimd_document.client_id) == url\n\n\nclass TestCIMDClientManagerValidatePrivateKeyJwt:\n    \"\"\"Tests for CIMDClientManager.validate_private_key_jwt wrapper.\"\"\"\n\n    @pytest.fixture\n    def manager(self):\n        return CIMDClientManager(enable_cimd=True)\n\n    async def test_missing_cimd_document_raises(self, manager):\n        \"\"\"validate_private_key_jwt raises ValueError if client has no cimd_document.\"\"\"\n\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=None,\n        )\n        with pytest.raises(ValueError, match=\"must have CIMD document\"):\n            await manager.validate_private_key_jwt(\n                \"fake.jwt.token\",\n                client,\n                \"https://oauth.example.com/token\",\n            )\n\n    async def test_wrong_auth_method_raises(self, manager):\n        \"\"\"validate_private_key_jwt raises ValueError if auth method is not private_key_jwt.\"\"\"\n\n        cimd_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n            token_endpoint_auth_method=\"none\",  # Not private_key_jwt\n        )\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=cimd_doc,\n        )\n        with pytest.raises(ValueError, match=\"private_key_jwt\"):\n            await manager.validate_private_key_jwt(\n                \"fake.jwt.token\",\n                client,\n                \"https://oauth.example.com/token\",\n            )\n\n    async def test_success_delegates_to_assertion_validator(self, manager):\n        \"\"\"On success, validate_private_key_jwt delegates to the assertion validator.\"\"\"\n\n        cimd_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            jwks_uri=AnyHttpUrl(\"https://example.com/.well-known/jwks.json\"),\n        )\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=cimd_doc,\n        )\n\n        manager._assertion_validator.validate_assertion = AsyncMock(return_value=True)\n\n        result = await manager.validate_private_key_jwt(\n            \"test.jwt.assertion\",\n            client,\n            \"https://oauth.example.com/token\",\n        )\n        assert result is True\n        manager._assertion_validator.validate_assertion.assert_awaited_once_with(\n            \"test.jwt.assertion\",\n            \"https://example.com/client.json\",\n            \"https://oauth.example.com/token\",\n            cimd_doc,\n        )\n\n\nclass TestCIMDRedirectUriEnforcement:\n    \"\"\"Tests for CIMD redirect_uri validation security.\n\n    Verifies that CIMD clients enforce BOTH:\n    1. CIMD document's redirect_uris\n    2. Proxy's allowed_redirect_uri_patterns\n    \"\"\"\n\n    @pytest.fixture\n    def mock_dns(self):\n        \"\"\"Mock DNS resolution to return test public IP.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            yield\n\n    async def test_cimd_redirect_uris_enforced(self, httpx_mock, mock_dns):\n        \"\"\"Test that CIMD document redirect_uris are enforced.\n\n        Even if proxy patterns allow http://localhost:*, a CIMD client\n        should only accept URIs declared in its document.\n        \"\"\"\n        from mcp.shared.auth import InvalidRedirectUriError\n        from pydantic import AnyUrl\n\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            # CIMD only declares port 3000\n            \"redirect_uris\": [\"http://localhost:3000/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        # Proxy allows any localhost port\n        manager = CIMDClientManager(\n            enable_cimd=True,\n            allowed_redirect_uri_patterns=[\"http://localhost:*\"],\n        )\n        client = await manager.get_client(url)\n        assert client is not None\n\n        # Declared URI should work\n        validated = client.validate_redirect_uri(\n            AnyUrl(\"http://localhost:3000/callback\")\n        )\n        assert str(validated) == \"http://localhost:3000/callback\"\n\n        # Different port should fail (not in CIMD redirect_uris)\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://localhost:4000/callback\"))\n\n    async def test_proxy_patterns_also_checked(self, httpx_mock, mock_dns):\n        \"\"\"Test that proxy patterns are checked even for CIMD clients.\n\n        A CIMD client should not be able to use a redirect_uri that's\n        in its document but not allowed by proxy patterns.\n        \"\"\"\n        from mcp.shared.auth import InvalidRedirectUriError\n        from pydantic import AnyUrl\n\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Test App\",\n            # CIMD declares both localhost and external URI\n            \"redirect_uris\": [\n                \"http://localhost:3000/callback\",\n                \"https://evil.com/callback\",\n            ],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        # Proxy only allows localhost\n        manager = CIMDClientManager(\n            enable_cimd=True,\n            allowed_redirect_uri_patterns=[\"http://localhost:*\"],\n        )\n        client = await manager.get_client(url)\n        assert client is not None\n\n        # Localhost should work (in CIMD and matches pattern)\n        validated = client.validate_redirect_uri(\n            AnyUrl(\"http://localhost:3000/callback\")\n        )\n        assert str(validated) == \"http://localhost:3000/callback\"\n\n        # Evil.com should fail (in CIMD but doesn't match proxy patterns)\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"https://evil.com/callback\"))\n"
  },
  {
    "path": "tests/server/auth/test_debug_verifier.py",
    "content": "\"\"\"Unit tests for DebugTokenVerifier.\"\"\"\n\nimport re\n\nfrom fastmcp.server.auth.providers.debug import DebugTokenVerifier\n\n\nclass TestDebugTokenVerifier:\n    \"\"\"Test DebugTokenVerifier initialization and validation.\"\"\"\n\n    def test_init_defaults(self):\n        \"\"\"Test initialization with default parameters.\"\"\"\n        verifier = DebugTokenVerifier()\n\n        assert verifier.client_id == \"debug-client\"\n        assert verifier.scopes == []\n        assert verifier.required_scopes == []\n        assert callable(verifier.validate)\n\n    def test_init_custom_parameters(self):\n        \"\"\"Test initialization with custom parameters.\"\"\"\n        verifier = DebugTokenVerifier(\n            validate=lambda t: t.startswith(\"valid-\"),\n            client_id=\"custom-client\",\n            scopes=[\"read\", \"write\"],\n            required_scopes=[\"admin\"],\n        )\n\n        assert verifier.client_id == \"custom-client\"\n        assert verifier.scopes == [\"read\", \"write\"]\n        assert verifier.required_scopes == [\"admin\"]\n\n    async def test_verify_token_default_accepts_all(self):\n        \"\"\"Test that default verifier accepts all non-empty tokens.\"\"\"\n        verifier = DebugTokenVerifier()\n\n        result = await verifier.verify_token(\"any-token\")\n\n        assert result is not None\n        assert result.token == \"any-token\"\n        assert result.client_id == \"debug-client\"\n        assert result.scopes == []\n        assert result.expires_at is None\n        assert result.claims == {\"token\": \"any-token\"}\n\n    async def test_verify_token_rejects_empty(self):\n        \"\"\"Test that empty tokens are rejected even with default verifier.\"\"\"\n        verifier = DebugTokenVerifier()\n\n        # Empty string\n        assert await verifier.verify_token(\"\") is None\n\n        # Whitespace only\n        assert await verifier.verify_token(\"   \") is None\n\n    async def test_verify_token_sync_callable_success(self):\n        \"\"\"Test token verification with custom sync callable that passes.\"\"\"\n        verifier = DebugTokenVerifier(\n            validate=lambda t: t.startswith(\"valid-\"),\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n        )\n\n        result = await verifier.verify_token(\"valid-token-123\")\n\n        assert result is not None\n        assert result.token == \"valid-token-123\"\n        assert result.client_id == \"test-client\"\n        assert result.scopes == [\"read\"]\n        assert result.expires_at is None\n        assert result.claims == {\"token\": \"valid-token-123\"}\n\n    async def test_verify_token_sync_callable_failure(self):\n        \"\"\"Test token verification with custom sync callable that fails.\"\"\"\n        verifier = DebugTokenVerifier(validate=lambda t: t.startswith(\"valid-\"))\n\n        result = await verifier.verify_token(\"invalid-token\")\n\n        assert result is None\n\n    async def test_verify_token_async_callable_success(self):\n        \"\"\"Test token verification with custom async callable that passes.\"\"\"\n\n        async def async_validator(token: str) -> bool:\n            # Simulate async operation (e.g., database check)\n            return token in {\"token1\", \"token2\", \"token3\"}\n\n        verifier = DebugTokenVerifier(\n            validate=async_validator,\n            client_id=\"async-client\",\n            scopes=[\"admin\"],\n        )\n\n        result = await verifier.verify_token(\"token2\")\n\n        assert result is not None\n        assert result.token == \"token2\"\n        assert result.client_id == \"async-client\"\n        assert result.scopes == [\"admin\"]\n\n    async def test_verify_token_async_callable_failure(self):\n        \"\"\"Test token verification with custom async callable that fails.\"\"\"\n\n        async def async_validator(token: str) -> bool:\n            return token in {\"token1\", \"token2\", \"token3\"}\n\n        verifier = DebugTokenVerifier(validate=async_validator)\n\n        result = await verifier.verify_token(\"token99\")\n\n        assert result is None\n\n    async def test_verify_token_callable_exception(self):\n        \"\"\"Test that exceptions in validate callable are handled gracefully.\"\"\"\n\n        def failing_validator(token: str) -> bool:\n            raise ValueError(\"Something went wrong\")\n\n        verifier = DebugTokenVerifier(validate=failing_validator)\n\n        result = await verifier.verify_token(\"any-token\")\n\n        assert result is None\n\n    async def test_verify_token_async_callable_exception(self):\n        \"\"\"Test that exceptions in async validate callable are handled gracefully.\"\"\"\n\n        async def failing_async_validator(token: str) -> bool:\n            raise ValueError(\"Async validation failed\")\n\n        verifier = DebugTokenVerifier(validate=failing_async_validator)\n\n        result = await verifier.verify_token(\"any-token\")\n\n        assert result is None\n\n    async def test_verify_token_whitelist_pattern(self):\n        \"\"\"Test using verifier with a whitelist of allowed tokens.\"\"\"\n        allowed_tokens = {\"secret-token-1\", \"secret-token-2\", \"admin-token\"}\n\n        verifier = DebugTokenVerifier(validate=lambda t: t in allowed_tokens)\n\n        # Allowed tokens\n        assert await verifier.verify_token(\"secret-token-1\") is not None\n        assert await verifier.verify_token(\"admin-token\") is not None\n\n        # Disallowed tokens\n        assert await verifier.verify_token(\"unknown-token\") is None\n        assert await verifier.verify_token(\"hacker-token\") is None\n\n    async def test_verify_token_pattern_matching(self):\n        \"\"\"Test using verifier with regex-like pattern matching.\"\"\"\n\n        pattern = re.compile(r\"^[A-Z]{3}-\\d{4}-[a-z]{2}$\")\n\n        verifier = DebugTokenVerifier(\n            validate=lambda t: bool(pattern.match(t)),\n            client_id=\"pattern-client\",\n        )\n\n        # Valid patterns\n        result = await verifier.verify_token(\"ABC-1234-xy\")\n        assert result is not None\n        assert result.client_id == \"pattern-client\"\n\n        # Invalid patterns\n        assert await verifier.verify_token(\"abc-1234-xy\") is None  # Wrong case\n        assert await verifier.verify_token(\"ABC-123-xy\") is None  # Wrong digits\n        assert await verifier.verify_token(\"ABC-1234-xyz\") is None  # Too many chars\n"
  },
  {
    "path": "tests/server/auth/test_enhanced_error_responses.py",
    "content": "\"\"\"Tests for enhanced OAuth error responses.\n\nThis test suite covers:\n1. Enhanced authorization handler (HTML and JSON error pages)\n2. Enhanced middleware (better error messages)\n3. Content negotiation\n4. Server branding in error pages\n\"\"\"\n\nimport pytest\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\nfrom starlette.applications import Starlette\nfrom starlette.testclient import TestClient\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\n\nclass TestEnhancedAuthorizationHandler:\n    \"\"\"Tests for enhanced authorization handler error responses.\"\"\"\n\n    @pytest.fixture\n    def rsa_key_pair(self) -> RSAKeyPair:\n        \"\"\"Generate RSA key pair for testing.\"\"\"\n        return RSAKeyPair.generate()\n\n    @pytest.fixture\n    def oauth_proxy(self, rsa_key_pair):\n        \"\"\"Create OAuth proxy for testing.\"\"\"\n        from key_value.aio.stores.memory import MemoryStore\n\n        return OAuthProxy(\n            upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n            upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=JWTVerifier(\n                public_key=rsa_key_pair.public_key,\n                issuer=\"https://test.com\",\n                audience=\"https://test.com\",\n                base_url=\"https://test.com\",\n            ),\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n    def test_unregistered_client_returns_html_for_browser(self, oauth_proxy):\n        \"\"\"Test that unregistered client returns styled HTML for browser requests.\"\"\"\n        app = Starlette(routes=oauth_proxy.get_routes())\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"unregistered-client-id\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                    \"state\": \"test-state\",\n                },\n                headers={\"Accept\": \"text/html\"},\n            )\n\n            # Should return 400 with HTML content\n            assert response.status_code == 400\n            assert \"text/html\" in response.headers[\"content-type\"]\n\n            # HTML should contain error message\n            html = response.text\n            assert \"Client Not Registered\" in html\n            assert \"unregistered-client-id\" in html\n            assert \"To fix this\" in html\n            assert \"Close this browser window\" in html\n            assert \"Clear authentication tokens\" in html\n\n            # Should have Link header for registration endpoint\n            assert \"Link\" in response.headers\n            assert \"/register\" in response.headers[\"Link\"]\n\n    def test_unregistered_client_returns_json_for_api(self, oauth_proxy):\n        \"\"\"Test that unregistered client returns enhanced JSON for API clients.\"\"\"\n        app = Starlette(routes=oauth_proxy.get_routes())\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"unregistered-client-id\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                    \"state\": \"test-state\",\n                },\n                headers={\"Accept\": \"application/json\"},\n            )\n\n            # Should return 400 with JSON content\n            assert response.status_code == 400\n            assert \"application/json\" in response.headers[\"content-type\"]\n\n            # JSON should have enhanced error response\n            data = response.json()\n            assert data[\"error\"] == \"invalid_request\"\n            assert \"unregistered-client-id\" in data[\"error_description\"]\n            assert data[\"state\"] == \"test-state\"\n\n            # Should include registration endpoint hints\n            assert \"registration_endpoint\" in data\n            assert data[\"registration_endpoint\"] == \"https://myserver.com/register\"\n            assert \"authorization_server_metadata\" in data\n\n            # Should have Link header\n            assert \"Link\" in response.headers\n            assert \"/register\" in response.headers[\"Link\"]\n\n    def test_successful_authorization_not_enhanced(self, oauth_proxy):\n        \"\"\"Test that successful authorizations are not modified by enhancement.\"\"\"\n        app = Starlette(routes=oauth_proxy.get_routes())\n\n        # Register a valid client first\n        client_info = OAuthClientInformationFull(\n            client_id=\"valid-client\",\n            client_secret=\"valid-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n\n        # Need to register synchronously\n        import asyncio\n\n        asyncio.run(oauth_proxy.register_client(client_info))\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"valid-client\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                    \"state\": \"test-state\",\n                },\n                headers={\"Accept\": \"text/html\"},\n                follow_redirects=False,\n            )\n\n            # Should redirect to consent page (302), not return error\n            assert response.status_code == 302\n            assert \"/consent\" in response.headers[\"location\"]\n\n    def test_html_error_includes_server_branding(self, oauth_proxy):\n        \"\"\"Test that HTML error page includes server branding from FastMCP instance.\"\"\"\n        from mcp.types import Icon\n\n        # Create FastMCP server with custom branding\n        mcp = FastMCP(\n            \"My Custom Server\",\n            icons=[Icon(src=\"https://example.com/icon.png\", mimeType=\"image/png\")],\n        )\n\n        # Create app with OAuth routes\n        app = Starlette(routes=oauth_proxy.get_routes())\n        # Attach FastMCP instance to app state (same as done in http.py)\n        app.state.fastmcp_server = mcp\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"unregistered-client-id\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                },\n                headers={\"Accept\": \"text/html\"},\n            )\n\n            assert response.status_code == 400\n            html = response.text\n\n            # Should include custom server icon\n            assert \"https://example.com/icon.png\" in html\n\n\nclass TestEnhancedRequireAuthMiddleware:\n    \"\"\"Tests for enhanced authentication middleware error messages.\"\"\"\n\n    @pytest.fixture\n    def rsa_key_pair(self) -> RSAKeyPair:\n        \"\"\"Generate RSA key pair for testing.\"\"\"\n        return RSAKeyPair.generate()\n\n    @pytest.fixture\n    def jwt_verifier(self, rsa_key_pair):\n        \"\"\"Create JWT verifier for testing.\"\"\"\n        return JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://test.com\",\n            audience=\"https://test.com\",\n            base_url=\"https://test.com\",\n        )\n\n    def test_invalid_token_enhanced_error_message(self, jwt_verifier):\n        \"\"\"Test that invalid_token errors have enhanced error messages.\"\"\"\n        from fastmcp.server.http import create_streamable_http_app\n\n        server = FastMCP(\"Test Server\")\n\n        @server.tool\n        def test_tool() -> str:\n            return \"test\"\n\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=jwt_verifier,\n        )\n\n        with TestClient(app) as client:\n            # Request without Authorization header\n            response = client.post(\"/mcp\")\n\n            assert response.status_code == 401\n            assert \"www-authenticate\" in response.headers\n\n            # Check enhanced error message\n            data = response.json()\n            assert data[\"error\"] == \"invalid_token\"\n            # Should have enhanced description with resolution steps\n            assert \"clear authentication tokens\" in data[\"error_description\"]\n            assert \"automatically re-register\" in data[\"error_description\"]\n\n    def test_invalid_token_www_authenticate_header_format(self, jwt_verifier):\n        \"\"\"Test that WWW-Authenticate header format matches SDK.\"\"\"\n        from fastmcp.server.http import create_streamable_http_app\n\n        server = FastMCP(\"Test Server\")\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=jwt_verifier,\n        )\n\n        with TestClient(app) as client:\n            response = client.post(\"/mcp\")\n\n            assert response.status_code == 401\n            www_auth = response.headers[\"www-authenticate\"]\n\n            # Should follow Bearer challenge format\n            assert www_auth.startswith(\"Bearer \")\n            assert 'error=\"invalid_token\"' in www_auth\n            assert \"error_description=\" in www_auth\n\n    def test_insufficient_scope_not_enhanced(self, rsa_key_pair):\n        \"\"\"Test that insufficient_scope errors are not modified.\"\"\"\n        # Create a valid token with wrong scopes\n        from fastmcp.server.http import create_streamable_http_app\n\n        jwt_verifier = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://test.com\",\n            audience=\"https://test.com\",\n            base_url=\"https://test.com\",\n        )\n\n        server = FastMCP(\"Test Server\")\n\n        @server.tool\n        def test_tool() -> str:\n            return \"test\"\n\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=jwt_verifier,\n        )\n\n        # Note: Testing insufficient_scope would require mocking the verifier\n        # to return a token with wrong scopes. For now, we verify the middleware\n        # is properly in place by checking it rejects unauthenticated requests.\n        with TestClient(app) as client:\n            response = client.post(\"/mcp\")\n            # Without a valid token, we get invalid_token\n            assert response.status_code == 401\n\n\nclass TestContentNegotiation:\n    \"\"\"Tests for content negotiation in error responses.\"\"\"\n\n    @pytest.fixture\n    def oauth_proxy(self):\n        \"\"\"Create OAuth proxy for testing.\"\"\"\n        from key_value.aio.stores.memory import MemoryStore\n\n        return OAuthProxy(\n            upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n            upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=JWTVerifier(\n                public_key=RSAKeyPair.generate().public_key,\n                issuer=\"https://test.com\",\n                audience=\"https://test.com\",\n                base_url=\"https://test.com\",\n            ),\n            base_url=\"https://myserver.com\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n    def test_html_preferred_when_both_accepted(self, oauth_proxy):\n        \"\"\"Test that HTML is preferred when both text/html and application/json are accepted.\"\"\"\n        app = Starlette(routes=oauth_proxy.get_routes())\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"unregistered-client-id\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                },\n                headers={\"Accept\": \"text/html,application/json\"},\n            )\n\n            # Should prefer HTML\n            assert response.status_code == 400\n            assert \"text/html\" in response.headers[\"content-type\"]\n\n    def test_json_when_only_json_accepted(self, oauth_proxy):\n        \"\"\"Test that JSON is returned when only application/json is accepted.\"\"\"\n        app = Starlette(routes=oauth_proxy.get_routes())\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"unregistered-client-id\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                },\n                headers={\"Accept\": \"application/json\"},\n            )\n\n            assert response.status_code == 400\n            assert \"application/json\" in response.headers[\"content-type\"]\n\n    def test_json_when_no_accept_header(self, oauth_proxy):\n        \"\"\"Test that JSON is returned when no Accept header is provided.\"\"\"\n        app = Starlette(routes=oauth_proxy.get_routes())\n\n        with TestClient(app) as client:\n            response = client.get(\n                \"/authorize\",\n                params={\n                    \"client_id\": \"unregistered-client-id\",\n                    \"redirect_uri\": \"http://localhost:12345/callback\",\n                    \"response_type\": \"code\",\n                    \"code_challenge\": \"test-challenge\",\n                },\n            )\n\n            # Without Accept header, should return JSON (API default)\n            assert response.status_code == 400\n            assert \"application/json\" in response.headers[\"content-type\"]\n"
  },
  {
    "path": "tests/server/auth/test_jwt_issuer.py",
    "content": "\"\"\"Unit tests for JWT issuer and token encryption.\"\"\"\n\nimport base64\nimport time\n\nimport pytest\nfrom authlib.jose.errors import JoseError\n\nfrom fastmcp.server.auth.jwt_issuer import (\n    JWTIssuer,\n    derive_jwt_key,\n)\n\n\nclass TestKeyDerivation:\n    \"\"\"Tests for HKDF key derivation functions.\"\"\"\n\n    def test_derive_jwt_key_produces_32_bytes(self):\n        \"\"\"Test that JWT key derivation produces 32-byte key.\"\"\"\n        key = derive_jwt_key(high_entropy_material=\"test-secret\", salt=\"test-salt\")\n        assert len(key) == 44\n        assert isinstance(key, bytes)\n\n        # base64 decode and make sure its 32 bytes\n        key_bytes = base64.urlsafe_b64decode(key)\n        assert len(key_bytes) == 32\n\n        key = derive_jwt_key(low_entropy_material=\"test-secret\", salt=\"test-salt\")\n        assert len(key) == 44\n        assert isinstance(key, bytes)\n\n        # base64 decode and make sure its 32 bytes\n        key_bytes = base64.urlsafe_b64decode(key)\n        assert len(key_bytes) == 32\n\n    def test_derive_jwt_key_with_different_secrets_produces_different_keys(self):\n        \"\"\"Test that different secrets produce different keys.\"\"\"\n        key1 = derive_jwt_key(high_entropy_material=\"secret1\", salt=\"salt\")\n        key2 = derive_jwt_key(high_entropy_material=\"secret2\", salt=\"salt\")\n        assert key1 != key2\n\n        key1 = derive_jwt_key(low_entropy_material=\"secret1\", salt=\"salt\")\n        key2 = derive_jwt_key(low_entropy_material=\"secret2\", salt=\"salt\")\n        assert key1 != key2\n\n    def test_derive_jwt_key_with_different_salts_produces_different_keys(self):\n        \"\"\"Test that different salts produce different keys.\"\"\"\n        key1 = derive_jwt_key(high_entropy_material=\"secret\", salt=\"salt1\")\n        key2 = derive_jwt_key(high_entropy_material=\"secret\", salt=\"salt2\")\n        assert key1 != key2\n\n        key1 = derive_jwt_key(low_entropy_material=\"secret\", salt=\"salt1\")\n        key2 = derive_jwt_key(low_entropy_material=\"secret\", salt=\"salt2\")\n        assert key1 != key2\n\n    def test_derive_jwt_key_is_deterministic(self):\n        \"\"\"Test that same inputs always produce same key.\"\"\"\n        key1 = derive_jwt_key(high_entropy_material=\"secret\", salt=\"salt\")\n        key2 = derive_jwt_key(high_entropy_material=\"secret\", salt=\"salt\")\n        assert key1 == key2\n\n        key1 = derive_jwt_key(low_entropy_material=\"secret\", salt=\"salt\")\n        key2 = derive_jwt_key(low_entropy_material=\"secret\", salt=\"salt\")\n        assert key1 == key2\n\n\nclass TestJWTIssuer:\n    \"\"\"Tests for JWT token issuance and verification.\"\"\"\n\n    @pytest.fixture\n    def issuer(self):\n        \"\"\"Create a JWT issuer for testing.\"\"\"\n        signing_key = derive_jwt_key(\n            low_entropy_material=\"test-secret\", salt=\"test-salt\"\n        )\n        return JWTIssuer(\n            issuer=\"https://test-server.com\",\n            audience=\"https://test-server.com/mcp\",\n            signing_key=signing_key,\n        )\n\n    def test_issue_access_token_creates_valid_jwt(self, issuer):\n        \"\"\"Test that access token is a minimal JWT with correct structure.\"\"\"\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\", \"write\"],\n            jti=\"token-id-123\",\n            expires_in=3600,\n        )\n\n        # Should be a JWT with 3 segments\n        assert len(token.split(\".\")) == 3\n\n        # Should be verifiable\n        payload = issuer.verify_token(token)\n        # Minimal token should only have required claims\n        assert payload[\"client_id\"] == \"client-abc\"\n        assert payload[\"scope\"] == \"read write\"\n        assert payload[\"jti\"] == \"token-id-123\"\n        assert payload[\"iss\"] == \"https://test-server.com\"\n        assert payload[\"aud\"] == \"https://test-server.com/mcp\"\n        # Should NOT have user identity claims\n        assert \"sub\" not in payload\n        assert \"azp\" not in payload\n\n    def test_minimal_token_has_no_user_identity(self, issuer):\n        \"\"\"Test that minimal tokens contain no user identity or custom claims.\"\"\"\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id\",\n            expires_in=3600,\n        )\n\n        payload = issuer.verify_token(token)\n        # Should only have minimal required claims\n        assert \"sub\" not in payload\n        assert \"azp\" not in payload\n        assert \"groups\" not in payload\n        assert \"roles\" not in payload\n        assert \"email\" not in payload\n        # Should have exactly these claims\n        expected_keys = {\"iss\", \"aud\", \"client_id\", \"scope\", \"exp\", \"iat\", \"jti\"}\n        assert set(payload.keys()) == expected_keys\n\n    def test_issue_refresh_token_creates_valid_jwt(self, issuer):\n        \"\"\"Test that refresh token is a minimal JWT with token_use claim.\"\"\"\n        token = issuer.issue_refresh_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"refresh-token-id\",\n            expires_in=60 * 60 * 24 * 30,  # 30 days\n        )\n\n        payload = issuer.verify_token(token, expected_token_use=\"refresh\")\n        assert payload[\"client_id\"] == \"client-abc\"\n        assert payload[\"token_use\"] == \"refresh\"\n        assert payload[\"jti\"] == \"refresh-token-id\"\n        # Should NOT have user identity\n        assert \"sub\" not in payload\n\n    def test_verify_token_validates_signature(self, issuer):\n        \"\"\"Test that token verification fails with wrong signing key.\"\"\"\n        # Create token with one issuer\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id\",\n        )\n\n        # Try to verify with different issuer (different key)\n        other_key = derive_jwt_key(\n            low_entropy_material=\"different-secret\", salt=\"different-salt\"\n        )\n        other_issuer = JWTIssuer(\n            issuer=\"https://test-server.com\",\n            audience=\"https://test-server.com/mcp\",\n            signing_key=other_key,\n        )\n\n        with pytest.raises(JoseError):\n            other_issuer.verify_token(token)\n\n    def test_verify_token_validates_expiration(self, issuer):\n        \"\"\"Test that expired tokens are rejected.\"\"\"\n        # Create token that expires in 1 second\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id\",\n            expires_in=1,\n        )\n\n        # Should be valid immediately\n        payload = issuer.verify_token(token)\n        assert payload[\"client_id\"] == \"client-abc\"\n\n        # Wait for token to expire\n        time.sleep(1.1)\n\n        # Should be rejected\n        with pytest.raises(JoseError, match=\"expired\"):\n            issuer.verify_token(token)\n\n    def test_verify_token_validates_issuer(self, issuer):\n        \"\"\"Test that tokens from different issuers are rejected.\"\"\"\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id\",\n        )\n\n        # Create issuer with different issuer URL but same key\n        other_issuer = JWTIssuer(\n            issuer=\"https://other-server.com\",  # Different issuer\n            audience=\"https://test-server.com/mcp\",\n            signing_key=issuer._signing_key,  # Same key\n        )\n\n        with pytest.raises(JoseError, match=\"issuer\"):\n            other_issuer.verify_token(token)\n\n    def test_verify_token_validates_audience(self, issuer):\n        \"\"\"Test that tokens for different audiences are rejected.\"\"\"\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id\",\n        )\n\n        # Create issuer with different audience but same key\n        other_issuer = JWTIssuer(\n            issuer=\"https://test-server.com\",\n            audience=\"https://other-server.com/mcp\",  # Different audience\n            signing_key=issuer._signing_key,  # Same key\n        )\n\n        with pytest.raises(JoseError, match=\"audience\"):\n            other_issuer.verify_token(token)\n\n    def test_verify_token_rejects_malformed_tokens(self, issuer):\n        \"\"\"Test that malformed tokens are rejected.\"\"\"\n        with pytest.raises(JoseError):\n            issuer.verify_token(\"not-a-jwt\")\n\n        with pytest.raises(JoseError):\n            issuer.verify_token(\"too.few.segments\")\n\n        with pytest.raises(JoseError):\n            issuer.verify_token(\"header.payload\")  # Missing signature\n\n    def test_issue_access_token_with_upstream_claims(self, issuer):\n        \"\"\"Test that upstream claims are included when provided.\"\"\"\n        upstream_claims = {\n            \"sub\": \"user-123\",\n            \"oid\": \"object-id-456\",\n            \"name\": \"Test User\",\n            \"email\": \"test@example.com\",\n            \"roles\": [\"Admin\", \"Reader\"],\n        }\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\", \"write\"],\n            jti=\"token-id-123\",\n            expires_in=3600,\n            upstream_claims=upstream_claims,\n        )\n\n        payload = issuer.verify_token(token)\n        assert \"upstream_claims\" in payload\n        assert payload[\"upstream_claims\"][\"sub\"] == \"user-123\"\n        assert payload[\"upstream_claims\"][\"oid\"] == \"object-id-456\"\n        assert payload[\"upstream_claims\"][\"name\"] == \"Test User\"\n        assert payload[\"upstream_claims\"][\"email\"] == \"test@example.com\"\n        assert payload[\"upstream_claims\"][\"roles\"] == [\"Admin\", \"Reader\"]\n\n    def test_issue_access_token_without_upstream_claims(self, issuer):\n        \"\"\"Test that upstream_claims is not present when not provided.\"\"\"\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id-123\",\n            expires_in=3600,\n        )\n\n        payload = issuer.verify_token(token)\n        assert \"upstream_claims\" not in payload\n\n    def test_issue_refresh_token_with_upstream_claims(self, issuer):\n        \"\"\"Test that refresh tokens also include upstream claims when provided.\"\"\"\n        upstream_claims = {\n            \"sub\": \"user-123\",\n            \"name\": \"Test User\",\n        }\n        token = issuer.issue_refresh_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"refresh-token-id\",\n            expires_in=60 * 60 * 24 * 30,\n            upstream_claims=upstream_claims,\n        )\n\n        payload = issuer.verify_token(token, expected_token_use=\"refresh\")\n        assert \"upstream_claims\" in payload\n        assert payload[\"upstream_claims\"][\"sub\"] == \"user-123\"\n        assert payload[\"upstream_claims\"][\"name\"] == \"Test User\"\n        assert payload[\"token_use\"] == \"refresh\"\n\n    def test_verify_token_rejects_refresh_token_as_access(self, issuer):\n        \"\"\"Refresh tokens must not be accepted when expecting access tokens.\"\"\"\n        token = issuer.issue_refresh_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"refresh-token-id\",\n            expires_in=60 * 60 * 24 * 30,\n        )\n\n        with pytest.raises(JoseError, match=\"Token type mismatch\"):\n            issuer.verify_token(token)\n\n    def test_verify_token_rejects_access_token_as_refresh(self, issuer):\n        \"\"\"Access tokens must not be accepted when expecting refresh tokens.\"\"\"\n        token = issuer.issue_access_token(\n            client_id=\"client-abc\",\n            scopes=[\"read\"],\n            jti=\"token-id\",\n        )\n\n        with pytest.raises(JoseError, match=\"Token type mismatch\"):\n            issuer.verify_token(token, expected_token_use=\"refresh\")\n"
  },
  {
    "path": "tests/server/auth/test_jwt_provider.py",
    "content": "from collections.abc import AsyncGenerator\nfrom typing import Any\nfrom unittest.mock import patch\n\nimport pytest\nfrom pytest_httpx import HTTPXMock\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWKData, JWKSData, JWTVerifier, RSAKeyPair\nfrom fastmcp.utilities.tests import run_server_async\n\n# Standard public IP used for DNS mocking in tests\nTEST_PUBLIC_IP = \"93.184.216.34\"\n\n\nclass SymmetricKeyHelper:\n    \"\"\"Helper class for generating symmetric key JWT tokens for testing.\"\"\"\n\n    def __init__(self, secret: str):\n        \"\"\"Initialize with a secret key.\"\"\"\n        self.secret = secret\n\n    def create_token(\n        self,\n        subject: str = \"fastmcp-user\",\n        issuer: str = \"https://fastmcp.example.com\",\n        audience: str | list[str] | None = None,\n        scopes: list[str] | None = None,\n        expires_in_seconds: int = 3600,\n        additional_claims: dict[str, Any] | None = None,\n        algorithm: str = \"HS256\",\n    ) -> str:\n        \"\"\"\n        Generate a test JWT token using symmetric key for testing purposes.\n\n        Args:\n            subject: Subject claim (usually user ID)\n            issuer: Issuer claim\n            audience: Audience claim - can be a string or list of strings (optional)\n            scopes: List of scopes to include\n            expires_in_seconds: Token expiration time in seconds\n            additional_claims: Any additional claims to include\n            algorithm: JWT signing algorithm (HS256, HS384, or HS512)\n        \"\"\"\n        import time\n\n        from authlib.jose import JsonWebToken\n\n        # Create header\n        header = {\"alg\": algorithm}\n\n        # Create payload\n        payload: dict[str, str | int | list[str]] = {\n            \"sub\": subject,\n            \"iss\": issuer,\n            \"iat\": int(time.time()),\n            \"exp\": int(time.time()) + expires_in_seconds,\n        }\n\n        if audience:\n            payload[\"aud\"] = audience\n\n        if scopes:\n            payload[\"scope\"] = \" \".join(scopes)\n\n        if additional_claims:\n            payload.update(additional_claims)\n\n        # Create JWT\n        jwt_lib = JsonWebToken([algorithm])\n        token_bytes = jwt_lib.encode(header, payload, self.secret)\n\n        return token_bytes.decode(\"utf-8\")\n\n\n@pytest.fixture(scope=\"module\")\ndef rsa_key_pair() -> RSAKeyPair:\n    return RSAKeyPair.generate()\n\n\n@pytest.fixture(scope=\"module\")\ndef symmetric_key_helper() -> SymmetricKeyHelper:\n    \"\"\"Generate a symmetric key helper for testing.\"\"\"\n    return SymmetricKeyHelper(\"test-secret-key-for-hmac-signing\")\n\n\n@pytest.fixture(scope=\"module\")\ndef bearer_token(rsa_key_pair: RSAKeyPair) -> str:\n    return rsa_key_pair.create_token(\n        subject=\"test-user\",\n        issuer=\"https://test.example.com\",\n        audience=\"https://api.example.com\",\n    )\n\n\n@pytest.fixture\ndef bearer_provider(rsa_key_pair: RSAKeyPair) -> JWTVerifier:\n    return JWTVerifier(\n        public_key=rsa_key_pair.public_key,\n        issuer=\"https://test.example.com\",\n        audience=\"https://api.example.com\",\n    )\n\n\n@pytest.fixture\ndef symmetric_provider(symmetric_key_helper: SymmetricKeyHelper) -> JWTVerifier:\n    \"\"\"Create JWTVerifier configured for symmetric key verification.\"\"\"\n    return JWTVerifier(\n        public_key=symmetric_key_helper.secret,\n        issuer=\"https://test.example.com\",\n        audience=\"https://api.example.com\",\n        algorithm=\"HS256\",\n    )\n\n\ndef create_mcp_server(\n    public_key: str,\n    auth_kwargs: dict[str, Any] | None = None,\n) -> FastMCP:\n    mcp = FastMCP(\n        auth=JWTVerifier(\n            public_key=public_key,\n            **auth_kwargs or {},\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    return mcp\n\n\n@pytest.fixture\nasync def mcp_server_url(rsa_key_pair: RSAKeyPair) -> AsyncGenerator[str, None]:\n    server = create_mcp_server(\n        public_key=rsa_key_pair.public_key,\n        auth_kwargs=dict(\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        ),\n    )\n    async with run_server_async(server, transport=\"http\") as url:\n        yield url\n\n\nclass TestRSAKeyPair:\n    def test_generate_key_pair(self):\n        \"\"\"Test RSA key pair generation.\"\"\"\n        key_pair = RSAKeyPair.generate()\n\n        assert key_pair.private_key is not None\n        assert key_pair.public_key is not None\n\n        # Check that keys are in PEM format\n        private_pem = key_pair.private_key.get_secret_value()\n        public_pem = key_pair.public_key\n\n        assert \"-----BEGIN PRIVATE KEY-----\" in private_pem\n        assert \"-----END PRIVATE KEY-----\" in private_pem\n        assert \"-----BEGIN PUBLIC KEY-----\" in public_pem\n        assert \"-----END PUBLIC KEY-----\" in public_pem\n\n    def test_create_basic_token(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test basic token creation.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n        )\n\n        assert isinstance(token, str)\n        assert len(token.split(\".\")) == 3  # JWT has 3 parts\n\n    def test_create_token_with_scopes(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test token creation with scopes.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        assert isinstance(token, str)\n        # We'll validate the scopes in the BearerToken tests\n\n\nclass TestSymmetricKeyJWT:\n    \"\"\"Tests for JWT verification using symmetric keys (HMAC algorithms).\"\"\"\n\n    def test_initialization_with_symmetric_key(\n        self, symmetric_key_helper: SymmetricKeyHelper\n    ):\n        \"\"\"Test JWTVerifier initialization with symmetric key.\"\"\"\n        provider = JWTVerifier(\n            public_key=symmetric_key_helper.secret,\n            issuer=\"https://test.example.com\",\n            algorithm=\"HS256\",\n        )\n\n        assert provider.issuer == \"https://test.example.com\"\n        assert provider.public_key == symmetric_key_helper.secret\n        assert provider.algorithm == \"HS256\"\n        assert provider.jwks_uri is None\n\n    def test_initialization_rejects_hs_algorithm_with_jwks_uri(self):\n        \"\"\"Test that HMAC algorithms cannot be used with JWKS URI.\"\"\"\n        with pytest.raises(ValueError, match=\"cannot be used with jwks_uri\"):\n            JWTVerifier(\n                jwks_uri=\"https://test.example.com/.well-known/jwks.json\",\n                issuer=\"https://test.example.com\",\n                algorithm=\"HS256\",\n            )\n\n    def test_initialization_with_different_symmetric_algorithms(\n        self, symmetric_key_helper: SymmetricKeyHelper\n    ):\n        \"\"\"Test JWTVerifier initialization with different HMAC algorithms.\"\"\"\n        algorithms = [\"HS256\", \"HS384\", \"HS512\"]\n\n        for algorithm in algorithms:\n            provider = JWTVerifier(\n                public_key=symmetric_key_helper.secret,\n                issuer=\"https://test.example.com\",\n                algorithm=algorithm,\n            )\n            assert provider.algorithm == algorithm\n\n    def test_symmetric_algorithm_rejects_jwks_uri(self):\n        \"\"\"HS* algorithms must not be configured with JWKS/public key endpoints.\"\"\"\n        with pytest.raises(ValueError, match=\"cannot be used with jwks_uri\"):\n            JWTVerifier(\n                jwks_uri=\"https://test.example.com/.well-known/jwks.json\",\n                issuer=\"https://test.example.com\",\n                algorithm=\"HS256\",\n            )\n\n    def test_symmetric_algorithm_rejects_pem_public_key(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"HS* algorithms must use a shared secret, not PEM public key material.\"\"\"\n        with pytest.raises(ValueError, match=\"require a shared secret\"):\n            JWTVerifier(\n                public_key=rsa_key_pair.public_key,\n                issuer=\"https://test.example.com\",\n                algorithm=\"HS256\",\n            )\n\n    def test_symmetric_algorithm_accepts_bytes_secret(self):\n        \"\"\"HS* algorithms accept bytes secrets without TypeError.\"\"\"\n        verifier = JWTVerifier(\n            public_key=b\"secret\",\n            algorithm=\"HS256\",\n        )\n        assert verifier.algorithm == \"HS256\"\n\n    async def test_valid_symmetric_token_validation(\n        self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier\n    ):\n        \"\"\"Test validation of a valid token signed with symmetric key.\"\"\"\n        token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\", \"write\"],\n            algorithm=\"HS256\",\n        )\n\n        access_token = await symmetric_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n        assert \"read\" in access_token.scopes\n        assert \"write\" in access_token.scopes\n        assert access_token.expires_at is not None\n\n    async def test_symmetric_token_with_different_algorithms(\n        self, symmetric_key_helper: SymmetricKeyHelper\n    ):\n        \"\"\"Test that different HMAC algorithms work correctly.\"\"\"\n        algorithms = [\"HS256\", \"HS384\", \"HS512\"]\n\n        for algorithm in algorithms:\n            provider = JWTVerifier(\n                public_key=symmetric_key_helper.secret,\n                issuer=\"https://test.example.com\",\n                algorithm=algorithm,\n            )\n\n            token = symmetric_key_helper.create_token(\n                subject=\"test-user\",\n                issuer=\"https://test.example.com\",\n                algorithm=algorithm,\n            )\n\n            access_token = await provider.load_access_token(token)\n            assert access_token is not None\n            assert access_token.client_id == \"test-user\"\n\n    async def test_symmetric_token_issuer_validation(\n        self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier\n    ):\n        \"\"\"Test issuer validation with symmetric key tokens.\"\"\"\n        # Valid issuer\n        valid_token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n        access_token = await symmetric_provider.load_access_token(valid_token)\n        assert access_token is not None\n\n        # Invalid issuer\n        invalid_token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://evil.example.com\",\n            audience=\"https://api.example.com\",\n        )\n        access_token = await symmetric_provider.load_access_token(invalid_token)\n        assert access_token is None\n\n    async def test_symmetric_token_audience_validation(\n        self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier\n    ):\n        \"\"\"Test audience validation with symmetric key tokens.\"\"\"\n        # Valid audience\n        valid_token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n        access_token = await symmetric_provider.load_access_token(valid_token)\n        assert access_token is not None\n\n        # Invalid audience\n        invalid_token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://wrong-api.example.com\",\n        )\n        access_token = await symmetric_provider.load_access_token(invalid_token)\n        assert access_token is None\n\n    async def test_symmetric_token_scope_extraction(\n        self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier\n    ):\n        \"\"\"Test scope extraction from symmetric key tokens.\"\"\"\n        token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\", \"write\", \"admin\"],\n        )\n\n        access_token = await symmetric_provider.load_access_token(token)\n        assert access_token is not None\n        assert set(access_token.scopes) == {\"read\", \"write\", \"admin\"}\n\n    async def test_symmetric_token_expiration(\n        self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier\n    ):\n        \"\"\"Test expiration validation with symmetric key tokens.\"\"\"\n        # Valid token\n        valid_token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            expires_in_seconds=3600,  # 1 hour from now\n        )\n        access_token = await symmetric_provider.load_access_token(valid_token)\n        assert access_token is not None\n\n        # Expired token\n        expired_token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            expires_in_seconds=-3600,  # 1 hour ago\n        )\n        access_token = await symmetric_provider.load_access_token(expired_token)\n        assert access_token is None\n\n    async def test_symmetric_token_invalid_signature(\n        self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier\n    ):\n        \"\"\"Test rejection of tokens with invalid signatures.\"\"\"\n        # Create a token with a different secret\n        other_helper = SymmetricKeyHelper(\"different-secret-key\")\n        token = other_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await symmetric_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_symmetric_token_algorithm_mismatch(\n        self, symmetric_key_helper: SymmetricKeyHelper\n    ):\n        \"\"\"Test that tokens with mismatched algorithms are rejected.\"\"\"\n        # Create provider expecting HS256\n        provider = JWTVerifier(\n            public_key=symmetric_key_helper.secret,\n            issuer=\"https://test.example.com\",\n            algorithm=\"HS256\",\n        )\n\n        # Create token with HS512\n        token = symmetric_key_helper.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            algorithm=\"HS512\",\n        )\n\n        # Should fail because provider expects HS256\n        access_token = await provider.load_access_token(token)\n        assert access_token is None\n\n\nclass TestBearerTokenJWKS:\n    \"\"\"Tests for JWKS URI functionality.\n\n    Note: With SSRF protection, JWKS fetches validate DNS and connect to the\n    resolved IP. Tests mock DNS resolution to return a public IP.\n    \"\"\"\n\n    @pytest.fixture\n    def jwks_provider(self, rsa_key_pair: RSAKeyPair) -> JWTVerifier:\n        \"\"\"Provider configured with JWKS URI.\"\"\"\n        return JWTVerifier(\n            jwks_uri=\"https://test.example.com/.well-known/jwks.json\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n    @pytest.fixture\n    def mock_jwks_data(self, rsa_key_pair: RSAKeyPair) -> JWKSData:\n        \"\"\"Create mock JWKS data from RSA key pair.\"\"\"\n        from authlib.jose import JsonWebKey\n\n        # Create JWK from the RSA public key\n        jwk = JsonWebKey.import_key(rsa_key_pair.public_key)\n        jwk_data: JWKData = jwk.as_dict()\n        jwk_data[\"kid\"] = \"test-key-1\"\n        jwk_data[\"alg\"] = \"RS256\"\n\n        return {\"keys\": [jwk_data]}\n\n    @pytest.fixture\n    def mock_dns(self):\n        \"\"\"Mock DNS resolution to return test public IP.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            yield\n\n    async def test_jwks_token_validation(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        \"\"\"Test token validation using JWKS URI.\"\"\"\n        httpx_mock.add_response(json=mock_jwks_data)\n\n        username = \"test-user\"\n        issuer = \"https://test.example.com\"\n        audience = \"https://api.example.com\"\n\n        token = rsa_key_pair.create_token(\n            subject=username,\n            issuer=issuer,\n            audience=audience,\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == username\n\n        # ensure the raw claims are present - #1398\n        assert access_token.claims.get(\"sub\") == username\n        assert access_token.claims.get(\"iss\") == issuer\n        assert access_token.claims.get(\"aud\") == audience\n\n    async def test_jwks_token_validation_with_invalid_key(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        httpx_mock.add_response(json=mock_jwks_data)\n        token = RSAKeyPair.generate().create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_jwks_token_validation_with_kid(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        mock_jwks_data[\"keys\"][0][\"kid\"] = \"test-key-1\"\n        httpx_mock.add_response(json=mock_jwks_data)\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            kid=\"test-key-1\",\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n\n    async def test_jwks_token_validation_with_kid_and_no_kid_in_token(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        mock_jwks_data[\"keys\"][0][\"kid\"] = \"test-key-1\"\n        httpx_mock.add_response(json=mock_jwks_data)\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n\n    async def test_jwks_token_validation_with_no_kid_and_kid_in_jwks(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        mock_jwks_data[\"keys\"][0][\"kid\"] = \"test-key-1\"\n        httpx_mock.add_response(json=mock_jwks_data)\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n\n    async def test_jwks_token_validation_with_kid_mismatch(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        mock_jwks_data[\"keys\"][0][\"kid\"] = \"test-key-1\"\n        httpx_mock.add_response(json=mock_jwks_data)\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            kid=\"test-key-2\",\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_jwks_token_validation_with_multiple_keys_and_no_kid_in_token(\n        self,\n        rsa_key_pair: RSAKeyPair,\n        jwks_provider: JWTVerifier,\n        mock_jwks_data: JWKSData,\n        httpx_mock: HTTPXMock,\n        mock_dns,\n    ):\n        mock_jwks_data[\"keys\"] = [\n            {\n                \"kid\": \"test-key-1\",\n                \"alg\": \"RS256\",\n            },\n            {\n                \"kid\": \"test-key-2\",\n                \"alg\": \"RS256\",\n            },\n        ]\n\n        httpx_mock.add_response(json=mock_jwks_data)\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await jwks_provider.load_access_token(token)\n        assert access_token is None\n"
  },
  {
    "path": "tests/server/auth/test_jwt_provider_bearer.py",
    "content": "from collections.abc import AsyncGenerator\nfrom typing import Any\n\nimport httpx\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.client.auth.bearer import BearerAuth\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\nfrom fastmcp.utilities.tests import run_server_async\n\n# Standard public IP used for DNS mocking in tests\nTEST_PUBLIC_IP = \"93.184.216.34\"\n\n\n@pytest.fixture(scope=\"module\")\ndef rsa_key_pair() -> RSAKeyPair:\n    return RSAKeyPair.generate()\n\n\n@pytest.fixture(scope=\"module\")\ndef bearer_token(rsa_key_pair: RSAKeyPair) -> str:\n    return rsa_key_pair.create_token(\n        subject=\"test-user\",\n        issuer=\"https://test.example.com\",\n        audience=\"https://api.example.com\",\n    )\n\n\n@pytest.fixture\ndef bearer_provider(rsa_key_pair: RSAKeyPair) -> JWTVerifier:\n    return JWTVerifier(\n        public_key=rsa_key_pair.public_key,\n        issuer=\"https://test.example.com\",\n        audience=\"https://api.example.com\",\n    )\n\n\ndef create_mcp_server(\n    public_key: str,\n    auth_kwargs: dict[str, Any] | None = None,\n) -> FastMCP:\n    mcp = FastMCP(\n        auth=JWTVerifier(\n            public_key=public_key,\n            **auth_kwargs or {},\n        )\n    )\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    return mcp\n\n\n@pytest.fixture\nasync def mcp_server_url(rsa_key_pair: RSAKeyPair) -> AsyncGenerator[str, None]:\n    server = create_mcp_server(\n        public_key=rsa_key_pair.public_key,\n        auth_kwargs=dict(\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        ),\n    )\n    async with run_server_async(server, transport=\"http\") as url:\n        yield url\n\n\nclass TestBearerToken:\n    def test_initialization_with_public_key(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test provider initialization with public key.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key, issuer=\"https://test.example.com\"\n        )\n\n        assert provider.issuer == \"https://test.example.com\"\n        assert provider.public_key is not None\n        assert provider.jwks_uri is None\n\n    def test_initialization_with_jwks_uri(self):\n        \"\"\"Test provider initialization with JWKS URI.\"\"\"\n        provider = JWTVerifier(\n            jwks_uri=\"https://test.example.com/.well-known/jwks.json\",\n            issuer=\"https://test.example.com\",\n        )\n\n        assert provider.issuer == \"https://test.example.com\"\n        assert provider.jwks_uri == \"https://test.example.com/.well-known/jwks.json\"\n        assert provider.public_key is None\n\n    def test_initialization_requires_key_or_uri(self):\n        \"\"\"Test that either public_key or jwks_uri is required.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"Either public_key or jwks_uri must be provided\"\n        ):\n            JWTVerifier(issuer=\"https://test.example.com\")\n\n    def test_initialization_rejects_both_key_and_uri(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test that both public_key and jwks_uri cannot be provided.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"Provide either public_key or jwks_uri, not both\"\n        ):\n            JWTVerifier(\n                public_key=rsa_key_pair.public_key,\n                jwks_uri=\"https://test.example.com/.well-known/jwks.json\",\n                issuer=\"https://test.example.com\",\n            )\n\n    async def test_valid_token_validation(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test validation of a valid token.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n        assert \"read\" in access_token.scopes\n        assert \"write\" in access_token.scopes\n        assert access_token.expires_at is not None\n\n    async def test_expired_token_rejection(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test rejection of expired tokens.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            expires_in_seconds=-3600,  # Expired 1 hour ago\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_invalid_issuer_rejection(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test rejection of tokens with invalid issuer.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://evil.example.com\",  # Wrong issuer\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_invalid_audience_rejection(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test rejection of tokens with invalid audience.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://wrong-api.example.com\",  # Wrong audience\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_no_issuer_validation_when_none(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test that issuer validation is skipped when provider has no issuer configured.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=None,  # No issuer validation\n        )\n\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\", issuer=\"https://any.example.com\"\n        )\n\n        access_token = await provider.load_access_token(token)\n        assert access_token is not None\n\n    async def test_no_audience_validation_when_none(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test that audience validation is skipped when provider has no audience configured.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://test.example.com\",\n            audience=None,  # No audience validation\n        )\n\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://any-api.example.com\",\n        )\n\n        access_token = await provider.load_access_token(token)\n        assert access_token is not None\n\n    async def test_multiple_audiences_validation(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test validation with multiple audiences in token.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            additional_claims={\n                \"aud\": [\"https://api.example.com\", \"https://other-api.example.com\"]\n            },\n        )\n\n        access_token = await provider.load_access_token(token)\n        assert access_token is not None\n\n    async def test_provider_with_multiple_expected_audiences(\n        self, rsa_key_pair: RSAKeyPair\n    ):\n        \"\"\"Test provider configured with multiple expected audiences.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://test.example.com\",\n            audience=[\"https://api.example.com\", \"https://other-api.example.com\"],\n        )\n\n        # Token with single audience that matches one of the expected\n        token1 = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n        access_token1 = await provider.load_access_token(token1)\n        assert access_token1 is not None\n\n        # Token with multiple audiences, one of which matches\n        token2 = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            additional_claims={\n                \"aud\": [\"https://api.example.com\", \"https://third-party.example.com\"]\n            },\n        )\n        access_token2 = await provider.load_access_token(token2)\n        assert access_token2 is not None\n\n        # Token with audience that doesn't match any expected\n        token3 = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://wrong-api.example.com\",\n        )\n        access_token3 = await provider.load_access_token(token3)\n        assert access_token3 is None\n\n    @pytest.mark.parametrize(\n        (\"iss\", \"expected\"),\n        [\n            (\"https://test.example.com\", True),\n            (\"https://other-issuer.example.com\", True),\n            (\"https://wrong-issuer.example.com\", False),\n        ],\n    )\n    async def test_provider_with_multiple_expected_issuers(\n        self, rsa_key_pair: RSAKeyPair, iss: str, expected: bool\n    ):\n        \"\"\"Provider accepts any issuer from the configured list.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=[\"https://test.example.com\", \"https://other-issuer.example.com\"],\n            audience=\"https://api.example.com\",\n        )\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\", issuer=iss, audience=\"https://api.example.com\"\n        )\n        access_token = await provider.load_access_token(token)\n        assert (access_token is not None) is expected\n\n    async def test_scope_extraction_string(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test scope extraction from space-separated string.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\", \"write\", \"admin\"],\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert set(access_token.scopes) == {\"read\", \"write\", \"admin\"}\n\n    async def test_scope_extraction_list(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test scope extraction from list format.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            additional_claims={\"scope\": [\"read\", \"write\"]},  # List format\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert set(access_token.scopes) == {\"read\", \"write\"}\n\n    async def test_no_scopes(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test token with no scopes.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            # No scopes\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert access_token.scopes == []\n\n    async def test_scp_claim_extraction_string(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test scope extraction from 'scp' claim with space-separated string.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            additional_claims={\"scp\": \"read write admin\"},  # 'scp' claim as string\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert set(access_token.scopes) == {\"read\", \"write\", \"admin\"}\n\n    async def test_scp_claim_extraction_list(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test scope extraction from 'scp' claim with list format.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            additional_claims={\n                \"scp\": [\"read\", \"write\", \"admin\"]\n            },  # 'scp' claim as list\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert set(access_token.scopes) == {\"read\", \"write\", \"admin\"}\n\n    async def test_scope_precedence_over_scp(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test that 'scope' claim takes precedence over 'scp' claim when both are present.\"\"\"\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            additional_claims={\n                \"scope\": \"read write\",  # Standard OAuth2 claim\n                \"scp\": \"admin delete\",  # Should be ignored when 'scope' is present\n            },\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n\n        assert access_token is not None\n        assert set(access_token.scopes) == {\"read\", \"write\"}  # Only 'scope' claim used\n\n    async def test_malformed_token_rejection(self, bearer_provider: JWTVerifier):\n        \"\"\"Test rejection of malformed tokens.\"\"\"\n        malformed_tokens = [\n            \"not.a.jwt\",\n            \"too.many.parts.here.invalid\",\n            \"invalid-token\",\n            \"\",\n            \"header.body\",  # Missing signature\n        ]\n\n        for token in malformed_tokens:\n            access_token = await bearer_provider.load_access_token(token)\n            assert access_token is None\n\n    async def test_invalid_signature_rejection(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test rejection of tokens with invalid signatures.\"\"\"\n        # Create a token with a different key pair\n        other_key_pair = RSAKeyPair.generate()\n        token = other_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_client_id_fallback(\n        self, rsa_key_pair: RSAKeyPair, bearer_provider: JWTVerifier\n    ):\n        \"\"\"Test client_id extraction with fallback logic.\"\"\"\n        # Test with explicit client_id claim\n        token = rsa_key_pair.create_token(\n            subject=\"user123\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            additional_claims={\"client_id\": \"app456\"},\n        )\n\n        access_token = await bearer_provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == \"app456\"  # Should prefer client_id over sub\n\n    async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test that string (non-URL) issuers are supported per RFC 7519.\"\"\"\n        # Create provider with string issuer\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"my-service\",  # String issuer, not a URL\n        )\n\n        # Create token with matching string issuer\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"my-service\",  # Same string issuer\n        )\n\n        access_token = await provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n\n    async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test that mismatched string issuers are rejected.\"\"\"\n        # Create provider with one string issuer\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"my-service\",\n        )\n\n        # Create token with different string issuer\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"other-service\",  # Different string issuer\n        )\n\n        access_token = await provider.load_access_token(token)\n        assert access_token is None\n\n    async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair):\n        \"\"\"Test that URL issuers still work after the fix.\"\"\"\n        # Create provider with URL issuer\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://my-auth-server.com\",  # URL issuer\n        )\n\n        # Create token with matching URL issuer\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://my-auth-server.com\",  # Same URL issuer\n        )\n\n        access_token = await provider.load_access_token(token)\n        assert access_token is not None\n        assert access_token.client_id == \"test-user\"\n\n\nclass TestFastMCPBearerAuth:\n    def test_bearer_auth(self):\n        mcp = FastMCP(\n            auth=JWTVerifier(issuer=\"https://test.example.com\", public_key=\"abc\")\n        )\n        assert isinstance(mcp.auth, JWTVerifier)\n\n    async def test_unauthorized_access(self, mcp_server_url: str):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url) as client:\n                tools = await client.list_tools()  # noqa: F841\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    async def test_authorized_access(self, mcp_server_url: str, bearer_token):\n        async with Client(mcp_server_url, auth=BearerAuth(bearer_token)) as client:\n            tools = await client.list_tools()  # noqa: F841\n        assert tools\n\n    async def test_invalid_token_raises_401(self, mcp_server_url: str):\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url, auth=BearerAuth(\"invalid\")) as client:\n                tools = await client.list_tools()  # noqa: F841\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    async def test_expired_token(self, mcp_server_url: str, rsa_key_pair: RSAKeyPair):\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            expires_in_seconds=-3600,\n        )\n\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url, auth=BearerAuth(token)) as client:\n                tools = await client.list_tools()  # noqa: F841\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    async def test_token_with_bad_signature(self, mcp_server_url: str):\n        rsa_key_pair = RSAKeyPair.generate()\n        token = rsa_key_pair.create_token()\n\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            async with Client(mcp_server_url, auth=BearerAuth(token)) as client:\n                tools = await client.list_tools()  # noqa: F841\n        assert isinstance(exc_info.value, httpx.HTTPStatusError)\n        assert exc_info.value.response.status_code == 401\n        assert \"tools\" not in locals()\n\n    async def test_token_with_insufficient_scopes(self, rsa_key_pair: RSAKeyPair):\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\"],\n        )\n\n        server = create_mcp_server(\n            public_key=rsa_key_pair.public_key,\n            auth_kwargs=dict(required_scopes=[\"read\", \"write\"]),\n        )\n\n        async with run_server_async(server, transport=\"http\") as mcp_server_url:\n            with pytest.raises(httpx.HTTPStatusError) as exc_info:\n                async with Client(mcp_server_url, auth=BearerAuth(token)) as client:\n                    tools = await client.list_tools()  # noqa: F841\n            # JWTVerifier returns 401 when verify_token returns None (invalid token)\n            # This is correct behavior - when TokenVerifier.verify_token returns None,\n            # it indicates the token is invalid (not just insufficient permissions)\n            assert isinstance(exc_info.value, httpx.HTTPStatusError)\n            assert exc_info.value.response.status_code == 401\n            assert \"tools\" not in locals()\n\n    async def test_token_with_sufficient_scopes(self, rsa_key_pair: RSAKeyPair):\n        token = rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        server = create_mcp_server(\n            public_key=rsa_key_pair.public_key,\n            auth_kwargs=dict(required_scopes=[\"read\", \"write\"]),\n        )\n\n        async with run_server_async(server, transport=\"http\") as mcp_server_url:\n            async with Client(mcp_server_url, auth=BearerAuth(token)) as client:\n                tools = await client.list_tools()\n            assert tools\n\n\nclass TestJWTVerifierImport:\n    \"\"\"Test JWT token verifier can be imported and created.\"\"\"\n\n    def test_jwt_verifier_requires_pyjwt(self):\n        \"\"\"Test that JWTVerifier raises helpful error without PyJWT.\"\"\"\n        # Since PyJWT is likely installed in test environment, we'll just test construction\n        from fastmcp.server.auth.providers.jwt import JWTVerifier\n\n        # This should work if PyJWT is available\n        try:\n            verifier = JWTVerifier(public_key=\"dummy-key\")\n            assert verifier.public_key == \"dummy-key\"\n            assert verifier.algorithm == \"RS256\"\n        except ImportError as e:\n            # If PyJWT not available, should get helpful error\n            assert \"PyJWT is required\" in str(e)\n\n\nclass TestScopesSupported:\n    \"\"\"Tests for the scopes_supported property on TokenVerifier.\"\"\"\n\n    def test_defaults_to_required_scopes(self, rsa_key_pair: RSAKeyPair):\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            required_scopes=[\"read\", \"write\"],\n        )\n        assert provider.scopes_supported == [\"read\", \"write\"]\n\n    def test_empty_when_no_required_scopes(self, rsa_key_pair: RSAKeyPair):\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n        )\n        assert provider.scopes_supported == []\n"
  },
  {
    "path": "tests/server/auth/test_multi_auth.py",
    "content": "import httpx\nimport pytest\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import MultiAuth, RemoteAuthProvider, TokenVerifier\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n\nclass RaisingVerifier(TokenVerifier):\n    \"\"\"A verifier that always raises, for testing exception resilience.\"\"\"\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        raise RuntimeError(\"simulated failure\")\n\n\nclass TestMultiAuthInit:\n    \"\"\"Test MultiAuth initialization and validation.\"\"\"\n\n    def test_requires_server_or_verifiers(self):\n        \"\"\"MultiAuth with neither server nor verifiers raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"at least a server or one verifier\"):\n            MultiAuth()\n\n    def test_server_only(self):\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        provider = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=provider)\n        assert auth.server is provider\n        assert auth.verifiers == []\n\n    def test_verifiers_only(self):\n        v = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        auth = MultiAuth(verifiers=[v])\n        assert auth.server is None\n        assert auth.verifiers == [v]\n\n    def test_single_verifier_not_in_list(self):\n        \"\"\"A single TokenVerifier (not in a list) is accepted.\"\"\"\n        v = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        auth = MultiAuth(verifiers=v)\n        assert auth.verifiers == [v]\n\n    def test_base_url_from_server(self):\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        provider = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=provider)\n        assert auth.base_url == AnyHttpUrl(\"https://api.example.com/\")\n\n    def test_base_url_override(self):\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        provider = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=provider, base_url=\"https://override.example.com\")\n        assert auth.base_url == AnyHttpUrl(\"https://override.example.com/\")\n\n    def test_required_scopes_from_server(self):\n        verifier = StaticTokenVerifier(\n            tokens={\"t\": {\"client_id\": \"c\", \"scopes\": [\"read\"]}},\n            required_scopes=[\"read\"],\n        )\n        provider = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=provider)\n        assert auth.required_scopes == [\"read\"]\n\n\nclass TestMultiAuthVerifyToken:\n    \"\"\"Test MultiAuth token verification chain.\"\"\"\n\n    async def test_server_verified_first(self):\n        \"\"\"Server's verify_token is tried before verifiers.\"\"\"\n        server_verifier = StaticTokenVerifier(\n            tokens={\"server_token\": {\"client_id\": \"server-client\", \"scopes\": []}}\n        )\n        server = RemoteAuthProvider(\n            token_verifier=server_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        extra = StaticTokenVerifier(\n            tokens={\"extra_token\": {\"client_id\": \"extra-client\", \"scopes\": []}}\n        )\n\n        auth = MultiAuth(server=server, verifiers=[extra])\n\n        result = await auth.verify_token(\"server_token\")\n        assert result is not None\n        assert result.client_id == \"server-client\"\n\n    async def test_falls_back_to_verifiers(self):\n        \"\"\"When server rejects a token, verifiers are tried.\"\"\"\n        server_verifier = StaticTokenVerifier(\n            tokens={\"server_token\": {\"client_id\": \"server-client\", \"scopes\": []}}\n        )\n        server = RemoteAuthProvider(\n            token_verifier=server_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        extra = StaticTokenVerifier(\n            tokens={\"m2m_token\": {\"client_id\": \"m2m-service\", \"scopes\": []}}\n        )\n\n        auth = MultiAuth(server=server, verifiers=[extra])\n\n        result = await auth.verify_token(\"m2m_token\")\n        assert result is not None\n        assert result.client_id == \"m2m-service\"\n\n    async def test_verifier_order_matters(self):\n        \"\"\"Verifiers are tried in order; first match wins.\"\"\"\n        v1 = StaticTokenVerifier(\n            tokens={\"shared_token\": {\"client_id\": \"first\", \"scopes\": []}}\n        )\n        v2 = StaticTokenVerifier(\n            tokens={\"shared_token\": {\"client_id\": \"second\", \"scopes\": []}}\n        )\n\n        auth = MultiAuth(verifiers=[v1, v2])\n        result = await auth.verify_token(\"shared_token\")\n        assert result is not None\n        assert result.client_id == \"first\"\n\n    async def test_no_match_returns_none(self):\n        \"\"\"When no server or verifier accepts the token, returns None.\"\"\"\n        v = StaticTokenVerifier(tokens={\"known\": {\"client_id\": \"c\", \"scopes\": []}})\n        auth = MultiAuth(verifiers=[v])\n        result = await auth.verify_token(\"unknown\")\n        assert result is None\n\n    async def test_verifiers_only_no_server(self):\n        \"\"\"MultiAuth with only verifiers (no server) works.\"\"\"\n        v1 = StaticTokenVerifier(tokens={\"token_a\": {\"client_id\": \"a\", \"scopes\": []}})\n        v2 = StaticTokenVerifier(tokens={\"token_b\": {\"client_id\": \"b\", \"scopes\": []}})\n\n        auth = MultiAuth(verifiers=[v1, v2])\n\n        result_a = await auth.verify_token(\"token_a\")\n        assert result_a is not None\n        assert result_a.client_id == \"a\"\n\n        result_b = await auth.verify_token(\"token_b\")\n        assert result_b is not None\n        assert result_b.client_id == \"b\"\n\n    async def test_raising_verifier_does_not_break_chain(self):\n        \"\"\"If a verifier raises, the chain continues to the next source.\"\"\"\n        good = StaticTokenVerifier(\n            tokens={\"valid\": {\"client_id\": \"good-client\", \"scopes\": []}}\n        )\n        auth = MultiAuth(verifiers=[RaisingVerifier(), good])\n\n        result = await auth.verify_token(\"valid\")\n        assert result is not None\n        assert result.client_id == \"good-client\"\n\n    async def test_raising_server_does_not_break_chain(self):\n        \"\"\"If the server raises, verifiers are still tried.\"\"\"\n        good = StaticTokenVerifier(\n            tokens={\"valid\": {\"client_id\": \"fallback\", \"scopes\": []}}\n        )\n        auth = MultiAuth(server=RaisingVerifier(), verifiers=[good])\n\n        result = await auth.verify_token(\"valid\")\n        assert result is not None\n        assert result.client_id == \"fallback\"\n\n    async def test_all_raising_returns_none(self):\n        \"\"\"If every source raises, verify_token returns None.\"\"\"\n        auth = MultiAuth(verifiers=[RaisingVerifier(), RaisingVerifier()])\n        result = await auth.verify_token(\"anything\")\n        assert result is None\n\n    async def test_server_match_short_circuits(self):\n        \"\"\"When the server matches, verifiers are not consulted.\"\"\"\n        # Both server and verifier know the same token with different client_ids\n        server_verifier = StaticTokenVerifier(\n            tokens={\"token\": {\"client_id\": \"from-server\", \"scopes\": []}}\n        )\n        server = RemoteAuthProvider(\n            token_verifier=server_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        extra = StaticTokenVerifier(\n            tokens={\"token\": {\"client_id\": \"from-verifier\", \"scopes\": []}}\n        )\n\n        auth = MultiAuth(server=server, verifiers=[extra])\n        result = await auth.verify_token(\"token\")\n        assert result is not None\n        assert result.client_id == \"from-server\"\n\n\nclass TestMultiAuthRoutes:\n    \"\"\"Test that routes delegate to the server.\"\"\"\n\n    def test_routes_from_server(self):\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        server = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=server)\n        routes = auth.get_routes(mcp_path=\"/mcp\")\n        # RemoteAuthProvider creates a protected resource metadata route\n        assert len(routes) >= 1\n\n    def test_no_routes_without_server(self):\n        v = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        auth = MultiAuth(verifiers=[v])\n        assert auth.get_routes() == []\n\n    def test_well_known_routes_delegate_to_server(self):\n        \"\"\"get_well_known_routes delegates to the server's implementation.\"\"\"\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        server = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=server)\n        well_known = auth.get_well_known_routes(mcp_path=\"/mcp\")\n        server_well_known = server.get_well_known_routes(mcp_path=\"/mcp\")\n        # MultiAuth should produce the same well-known routes as the server\n        assert len(well_known) == len(server_well_known)\n        assert [r.path for r in well_known] == [r.path for r in server_well_known]\n\n    def test_well_known_routes_empty_without_server(self):\n        v = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        auth = MultiAuth(verifiers=[v])\n        assert auth.get_well_known_routes() == []\n\n    def test_required_scopes_explicit_empty_list(self):\n        \"\"\"Passing required_scopes=[] explicitly clears inherited scopes.\"\"\"\n        verifier = StaticTokenVerifier(\n            tokens={\"t\": {\"client_id\": \"c\", \"scopes\": [\"read\"]}},\n            required_scopes=[\"read\"],\n        )\n        server = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        # Server has required_scopes=[\"read\"], but we explicitly clear them\n        auth = MultiAuth(server=server, required_scopes=[])\n        assert auth.required_scopes == []\n\n\nclass TestMultiAuthIntegration:\n    \"\"\"Integration tests: MultiAuth with a real FastMCP HTTP app.\"\"\"\n\n    async def test_multi_auth_rejects_bad_tokens(self):\n        \"\"\"End-to-end: MultiAuth rejects unknown tokens at the HTTP layer.\"\"\"\n        oauth_tokens = StaticTokenVerifier(\n            tokens={\n                \"oauth_token\": {\n                    \"client_id\": \"interactive-client\",\n                    \"scopes\": [\"read\"],\n                }\n            }\n        )\n        m2m_tokens = StaticTokenVerifier(\n            tokens={\n                \"m2m_token\": {\n                    \"client_id\": \"backend-service\",\n                    \"scopes\": [\"read\"],\n                }\n            }\n        )\n\n        auth = MultiAuth(verifiers=[oauth_tokens, m2m_tokens])\n        mcp = FastMCP(\"test\", auth=auth)\n        app = mcp.http_app(path=\"/mcp\")\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            base_url=\"http://localhost\",\n        ) as client:\n            # No token → 401\n            response = await client.get(\"/mcp\")\n            assert response.status_code == 401\n\n            # Bad token → 401\n            response = await client.get(\n                \"/mcp\", headers={\"Authorization\": \"Bearer bad_token\"}\n            )\n            assert response.status_code == 401\n\n    async def test_multi_auth_with_server_provides_routes(self):\n        \"\"\"MultiAuth with a server exposes the server's metadata routes.\"\"\"\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        server = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        extra = StaticTokenVerifier(tokens={\"m2m\": {\"client_id\": \"svc\", \"scopes\": []}})\n\n        auth = MultiAuth(server=server, verifiers=[extra])\n        mcp = FastMCP(\"test\", auth=auth)\n        app = mcp.http_app(path=\"/mcp\")\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # Protected resource metadata should be available\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n            assert response.status_code == 200\n            data = response.json()\n            assert data[\"resource\"] == \"https://api.example.com/mcp\"\n\n    async def test_multi_auth_accepts_valid_verifier_token(self):\n        \"\"\"MultiAuth accepts tokens from verifiers (not just the server).\n\n        Verifies that both server and verifier tokens pass the HTTP auth\n        middleware. We use GET /mcp to check: 401 means auth rejected,\n        any other status means auth accepted and the request reached the\n        MCP session layer.\n        \"\"\"\n        interactive_tokens = StaticTokenVerifier(\n            tokens={\n                \"interactive_token\": {\n                    \"client_id\": \"interactive-client\",\n                    \"scopes\": [],\n                }\n            }\n        )\n        m2m_tokens = StaticTokenVerifier(\n            tokens={\n                \"m2m_token\": {\n                    \"client_id\": \"backend-service\",\n                    \"scopes\": [],\n                }\n            }\n        )\n\n        auth = MultiAuth(verifiers=[interactive_tokens, m2m_tokens])\n        mcp = FastMCP(\"test\", auth=auth)\n        app = mcp.http_app(path=\"/mcp\")\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app, raise_app_exceptions=False),\n            base_url=\"http://localhost\",\n        ) as client:\n            # No token → 401\n            response = await client.get(\"/mcp\")\n            assert response.status_code == 401\n\n            # Interactive token passes auth (non-401 means auth accepted)\n            response = await client.get(\n                \"/mcp\", headers={\"Authorization\": \"Bearer interactive_token\"}\n            )\n            assert response.status_code != 401\n\n            # M2M token also passes auth\n            response = await client.get(\n                \"/mcp\", headers={\"Authorization\": \"Bearer m2m_token\"}\n            )\n            assert response.status_code != 401\n\n            # Bad token → 401\n            response = await client.get(\n                \"/mcp\", headers={\"Authorization\": \"Bearer bad_token\"}\n            )\n            assert response.status_code == 401\n\n\nclass TestMultiAuthSetMcpPath:\n    \"\"\"Test that set_mcp_path propagates to server and verifiers.\"\"\"\n\n    def test_propagates_to_server(self):\n        verifier = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        server = RemoteAuthProvider(\n            token_verifier=verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n        auth = MultiAuth(server=server)\n        auth.set_mcp_path(\"/mcp\")\n        assert server._mcp_path == \"/mcp\"\n\n    def test_propagates_to_verifiers(self):\n        v1 = StaticTokenVerifier(tokens={\"t\": {\"client_id\": \"c\", \"scopes\": []}})\n        v2 = StaticTokenVerifier(tokens={\"t2\": {\"client_id\": \"c2\", \"scopes\": []}})\n        auth = MultiAuth(verifiers=[v1, v2])\n        auth.set_mcp_path(\"/mcp\")\n        assert v1._mcp_path == \"/mcp\"\n        assert v2._mcp_path == \"/mcp\"\n"
  },
  {
    "path": "tests/server/auth/test_oauth_consent_flow.py",
    "content": "\"\"\"Tests for OAuth Proxy consent flow with server-side storage.\n\nThis test suite verifies:\n1. OAuth transactions are stored in server-side storage (not in-memory)\n2. Authorization codes are stored in server-side storage\n3. Consent flow redirects correctly through /consent endpoint\n4. CSRF protection works with cookies\n5. State persists across storage backends\n6. Security headers (X-Frame-Options) are set correctly\n7. Cookie signing and tampering detection\n8. Auto-approve behavior with valid cookies\n9. Consent binding cookie prevents confused deputy attacks (GHSA-rww4-4w9c-7733)\n\"\"\"\n\nimport re\nimport secrets\nimport time\nfrom urllib.parse import parse_qs, urlparse\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.provider import AuthorizationParams\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\nfrom starlette.applications import Starlette\nfrom starlette.testclient import TestClient\n\nfrom fastmcp.server.auth.auth import AccessToken, TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.models import OAuthTransaction\n\n\nclass MockTokenVerifier(TokenVerifier):\n    \"\"\"Mock token verifier for testing.\"\"\"\n\n    def __init__(self):\n        self.required_scopes = [\"read\", \"write\"]\n\n    async def verify_token(self, token: str):\n        \"\"\"Mock token verification.\"\"\"\n        return AccessToken(\n            token=token,\n            client_id=\"mock-client\",\n            scopes=self.required_scopes,\n            expires_at=int(time.time() + 3600),\n        )\n\n\nclass _Verifier(TokenVerifier):\n    \"\"\"Minimal token verifier for security tests.\"\"\"\n\n    def __init__(self):\n        self.required_scopes = [\"read\"]\n\n    async def verify_token(self, token: str):\n        return AccessToken(\n            token=token, client_id=\"c\", scopes=self.required_scopes, expires_at=None\n        )\n\n\n@pytest.fixture\ndef storage():\n    \"\"\"Create a fresh in-memory storage for each test.\"\"\"\n    return MemoryStore()\n\n\n@pytest.fixture\ndef oauth_proxy_with_storage(storage):\n    \"\"\"Create OAuth proxy with explicit storage backend.\"\"\"\n    return OAuthProxy(\n        upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n        upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n        upstream_client_id=\"test-upstream-client\",\n        upstream_client_secret=\"test-upstream-secret\",\n        token_verifier=MockTokenVerifier(),\n        base_url=\"https://myserver.com\",\n        redirect_path=\"/auth/callback\",\n        client_storage=storage,  # Use our test storage\n        jwt_signing_key=\"test-secret\",\n    )\n\n\n@pytest.fixture\ndef oauth_proxy_https():\n    \"\"\"OAuthProxy configured with HTTPS base_url for __Host- cookies.\"\"\"\n    return OAuthProxy(\n        upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n        upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n        upstream_client_id=\"client-id\",\n        upstream_client_secret=\"client-secret\",\n        token_verifier=_Verifier(),\n        base_url=\"https://myserver.example\",\n        client_storage=MemoryStore(),\n        jwt_signing_key=\"test-secret\",\n    )\n\n\nasync def _start_flow(\n    proxy: OAuthProxy, client_id: str, redirect: str\n) -> tuple[str, str]:\n    \"\"\"Register client and start auth; returns (txn_id, consent_url).\"\"\"\n    await proxy.register_client(\n        OAuthClientInformationFull(\n            client_id=client_id,\n            client_secret=\"s\",\n            redirect_uris=[AnyUrl(redirect)],\n        )\n    )\n    params = AuthorizationParams(\n        redirect_uri=AnyUrl(redirect),\n        redirect_uri_provided_explicitly=True,\n        state=\"client-state-xyz\",\n        code_challenge=\"challenge\",\n        scopes=[\"read\"],\n    )\n    consent_url = await proxy.authorize(\n        OAuthClientInformationFull(\n            client_id=client_id,\n            client_secret=\"s\",\n            redirect_uris=[AnyUrl(redirect)],\n        ),\n        params,\n    )\n    qs = parse_qs(urlparse(consent_url).query)\n    return qs[\"txn_id\"][0], consent_url\n\n\ndef _extract_csrf(html: str) -> str | None:\n    \"\"\"Extract CSRF token from HTML form.\"\"\"\n    m = re.search(r\"name=\\\"csrf_token\\\"\\s+value=\\\"([^\\\"]+)\\\"\", html)\n    return m.group(1) if m else None\n\n\nclass TestServerSideStorage:\n    \"\"\"Tests verifying OAuth state is stored in AsyncKeyValue storage.\"\"\"\n\n    async def test_transaction_stored_in_storage_not_memory(\n        self, oauth_proxy_with_storage, storage\n    ):\n        \"\"\"Verify OAuth transactions are stored in AsyncKeyValue, not in-memory dict.\"\"\"\n        # Register client\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:54321/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        # Start authorization flow\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:54321/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"client-state-123\",\n            code_challenge=\"challenge-abc\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n\n        # Extract transaction ID from consent redirect\n        parsed = urlparse(redirect_url)\n        assert \"/consent\" in parsed.path, \"Should redirect to consent page\"\n\n        query_params = parse_qs(parsed.query)\n        txn_id = query_params[\"txn_id\"][0]\n\n        # Verify transaction is NOT in the old in-memory dict\n        # (the attribute should not exist or should be empty)\n        assert (\n            not hasattr(oauth_proxy_with_storage, \"_oauth_transactions\")\n            or len(getattr(oauth_proxy_with_storage, \"_oauth_transactions\", {})) == 0\n        )\n\n        # Verify transaction IS in storage backend\n        transaction = await storage.get(collection=\"mcp-oauth-transactions\", key=txn_id)\n        assert transaction is not None, \"Transaction should be in storage\"\n\n        # Verify transaction has expected structure\n        assert transaction[\"client_id\"] == \"test-client\"\n        assert transaction[\"client_redirect_uri\"] == \"http://localhost:54321/callback\"\n        assert transaction[\"client_state\"] == \"client-state-123\"\n        assert transaction[\"code_challenge\"] == \"challenge-abc\"\n        assert transaction[\"scopes\"] == [\"read\", \"write\"]\n\n    async def test_authorization_code_stored_in_storage(\n        self, oauth_proxy_with_storage, storage\n    ):\n        \"\"\"Verify authorization codes are stored in AsyncKeyValue storage.\"\"\"\n        # Register client\n        client = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:54321/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        # Create a test app with OAuth routes\n        app = Starlette(routes=oauth_proxy_with_storage.get_routes())\n\n        with TestClient(app) as test_client:\n            # Start authorization flow\n            params = AuthorizationParams(\n                redirect_uri=AnyUrl(\"http://localhost:54321/callback\"),\n                redirect_uri_provided_explicitly=True,\n                state=\"client-state\",\n                code_challenge=\"challenge-xyz\",\n                scopes=[\"read\"],\n            )\n\n            redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n\n            # Extract txn_id from consent redirect\n            parsed = urlparse(redirect_url)\n            query_params = parse_qs(parsed.query)\n            txn_id = query_params[\"txn_id\"][0]\n\n            # Simulate consent approval\n            # First, get the consent page to establish CSRF cookie\n            consent_response = test_client.get(\n                f\"/consent?txn_id={txn_id}\", follow_redirects=False\n            )\n\n            # Extract CSRF token from response (it's in the HTML form)\n            csrf_token = None\n            if consent_response.status_code == 200:\n                # For this test, we'll generate a CSRF token manually\n                # In production, this comes from the consent page HTML\n                csrf_token = secrets.token_urlsafe(32)\n\n            # Approve consent with CSRF token\n            # Set cookies on client instance to avoid deprecation warning\n            for k, v in consent_response.cookies.items():\n                test_client.cookies.set(k, v)\n            approval_response = test_client.post(\n                \"/consent\",\n                data={\n                    \"action\": \"approve\",\n                    \"txn_id\": txn_id,\n                    \"csrf_token\": csrf_token if csrf_token else \"\",\n                },\n                follow_redirects=False,\n            )\n\n            # After approval, authorization code should be in storage\n            # The code is returned in the redirect URL\n            if approval_response.status_code in (302, 303):\n                location = approval_response.headers.get(\"location\", \"\")\n                callback_params = parse_qs(urlparse(location).query)\n\n                if \"code\" in callback_params:\n                    auth_code = callback_params[\"code\"][0]\n\n                    # Verify code is NOT in old in-memory dict\n                    assert (\n                        not hasattr(oauth_proxy_with_storage, \"_client_codes\")\n                        or len(getattr(oauth_proxy_with_storage, \"_client_codes\", {}))\n                        == 0\n                    )\n\n                    # Verify code IS in storage\n                    code_data = await storage.get(\n                        collection=\"mcp-authorization-codes\", key=auth_code\n                    )\n                    assert code_data is not None, (\n                        \"Authorization code should be in storage\"\n                    )\n                    assert code_data[\"client_id\"] == \"test-client\"\n                    assert code_data[\"scopes\"] == [\"read\"]\n\n    async def test_storage_collections_are_isolated(self, oauth_proxy_with_storage):\n        \"\"\"Verify that transactions, codes, and clients use separate collections.\"\"\"\n        # Register a client\n        client = OAuthClientInformationFull(\n            client_id=\"isolation-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        # Start authorization to create transaction\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:12345/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"test-state\",\n            code_challenge=\"test-challenge\",\n            scopes=[\"read\"],\n        )\n\n        await oauth_proxy_with_storage.authorize(client, params)\n\n        # Get all collections from storage\n        storage = oauth_proxy_with_storage._client_storage\n\n        # Verify client is in client collection\n        client_data = await storage.get(\n            collection=\"mcp-oauth-proxy-clients\", key=\"isolation-test-client\"\n        )\n        assert client_data is not None\n\n        # Verify we can list transactions separately\n        # (This tests that collections are properly namespaced)\n        transactions = await storage.keys(collection=\"mcp-oauth-transactions\")\n\n        assert len(transactions) > 0, \"Should have at least one transaction\"\n\n        # Verify transaction keys don't collide with client keys\n        for txn_key in transactions:\n            assert txn_key != \"isolation-test-client\"\n\n\nclass TestConsentFlowRedirects:\n    \"\"\"Tests for consent flow redirect behavior.\"\"\"\n\n    async def test_authorize_redirects_to_consent_page(self, oauth_proxy_with_storage):\n        \"\"\"Verify authorize() redirects to /consent instead of upstream.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"consent-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:8080/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:8080/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"test-state\",\n            code_challenge=\"\",\n            scopes=[\"read\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n\n        # Should redirect to consent page, not upstream\n        assert \"/consent\" in redirect_url\n        assert \"github.com\" not in redirect_url\n        assert \"?txn_id=\" in redirect_url\n\n    async def test_consent_page_contains_transaction_id(self, oauth_proxy_with_storage):\n        \"\"\"Verify consent page receives and displays transaction ID.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"txn-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:9090/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:9090/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"test-state\",\n            code_challenge=\"test-challenge\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n\n        # Extract txn_id parameter\n        parsed = urlparse(redirect_url)\n        query = parse_qs(parsed.query)\n\n        assert \"txn_id\" in query\n        txn_id = query[\"txn_id\"][0]\n        assert len(txn_id) > 0\n\n        # Create test client\n        app = Starlette(routes=oauth_proxy_with_storage.get_routes())\n\n        with TestClient(app) as test_client:\n            # Request consent page\n            response = test_client.get(\n                f\"/consent?txn_id={txn_id}\", follow_redirects=False\n            )\n\n            assert response.status_code == 200\n            # Consent page should contain transaction reference\n            assert txn_id.encode() in response.content or b\"consent\" in response.content\n\n\nclass TestCSRFProtection:\n    \"\"\"Tests for CSRF protection in consent flow.\"\"\"\n\n    async def test_consent_requires_csrf_token(self, oauth_proxy_with_storage):\n        \"\"\"Verify consent submission requires valid CSRF token.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"csrf-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:7070/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:7070/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"test-state\",\n            code_challenge=\"\",\n            scopes=[\"read\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n        parsed = urlparse(redirect_url)\n        query = parse_qs(parsed.query)\n        txn_id = query[\"txn_id\"][0]\n\n        app = Starlette(routes=oauth_proxy_with_storage.get_routes())\n\n        with TestClient(app) as test_client:\n            # Try to submit consent WITHOUT CSRF token\n            response = test_client.post(\n                \"/consent\",\n                data={\"action\": \"approve\", \"txn_id\": txn_id},\n                # No CSRF token!\n                follow_redirects=False,\n            )\n\n            # Should reject or require CSRF\n            # (Implementation may vary - checking for error response)\n            assert response.status_code in (\n                400,\n                403,\n                302,\n            )  # Error or redirect to error\n\n    async def test_consent_cookie_established_on_page_visit(\n        self, oauth_proxy_with_storage\n    ):\n        \"\"\"Verify consent page establishes CSRF cookie.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"cookie-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:6060/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:6060/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"test-state\",\n            code_challenge=\"\",\n            scopes=[\"read\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n        parsed = urlparse(redirect_url)\n        query = parse_qs(parsed.query)\n        txn_id = query[\"txn_id\"][0]\n\n        app = Starlette(routes=oauth_proxy_with_storage.get_routes())\n\n        with TestClient(app) as test_client:\n            # Visit consent page\n            response = test_client.get(\n                f\"/consent?txn_id={txn_id}\", follow_redirects=False\n            )\n\n            # Should set cookies for CSRF protection\n            assert response.status_code == 200\n            # Cookie may be set via Set-Cookie header\n            cookies = response.cookies\n            # Look for any CSRF-related cookie (implementation dependent)\n            assert len(cookies) > 0 or \"csrf\" in response.text.lower(), (\n                \"Consent page should establish CSRF protection\"\n            )\n\n\nclass TestCSRFDoubleSubmit:\n    \"\"\"Tests for CSRF double-submit cookie validation (GHSA-rww4-4w9c-7733 bypass).\"\"\"\n\n    async def test_consent_rejected_without_csrf_cookie(self, oauth_proxy_with_storage):\n        \"\"\"Submitting a valid CSRF token without the matching cookie should be rejected.\n\n        This prevents an attacker from using their own tx_id/csrf_token to CSRF\n        the victim's browser into approving consent.\n        \"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_with_storage,\n            \"csrf-double-submit-client\",\n            \"http://localhost:9090/callback\",\n        )\n\n        app = Starlette(routes=oauth_proxy_with_storage.get_routes())\n        with TestClient(app) as test_client:\n            # Visit consent page to populate the transaction with a CSRF token\n            consent_resp = test_client.get(f\"/consent?txn_id={txn_id}\")\n            assert consent_resp.status_code == 200\n            csrf_token = _extract_csrf(consent_resp.text)\n            assert csrf_token\n\n        # Simulate the attack: use a FRESH client (no cookies from the consent\n        # page) to submit the form with a valid CSRF token — as if the attacker\n        # tricked the victim's browser into POSTing their tx_id/csrf_token.\n        with TestClient(app) as attacker_client:\n            response = attacker_client.post(\n                \"/consent\",\n                data={\n                    \"action\": \"approve\",\n                    \"txn_id\": txn_id,\n                    \"csrf_token\": csrf_token,\n                },\n                follow_redirects=False,\n            )\n            assert response.status_code == 403\n\n\nclass TestStoragePersistence:\n    \"\"\"Tests for state persistence across storage backends.\"\"\"\n\n    async def test_transaction_persists_after_retrieval(self, oauth_proxy_with_storage):\n        \"\"\"Verify transaction can be retrieved multiple times (until deleted).\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"persist-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:5050/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:5050/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"persist-state\",\n            code_challenge=\"persist-challenge\",\n            scopes=[\"read\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n        parsed = urlparse(redirect_url)\n        query = parse_qs(parsed.query)\n        txn_id = query[\"txn_id\"][0]\n\n        storage = oauth_proxy_with_storage._client_storage\n\n        # Retrieve transaction multiple times\n        txn1 = await storage.get(collection=\"mcp-oauth-transactions\", key=txn_id)\n        assert txn1 is not None\n\n        txn2 = await storage.get(collection=\"mcp-oauth-transactions\", key=txn_id)\n        assert txn2 is not None\n\n        # Should be the same data\n        assert txn1[\"client_id\"] == txn2[\"client_id\"]\n        assert txn1[\"client_state\"] == txn2[\"client_state\"]\n\n    async def test_storage_uses_pydantic_adapter(self, oauth_proxy_with_storage):\n        \"\"\"Verify that PydanticAdapter serializes/deserializes correctly.\"\"\"\n        client = OAuthClientInformationFull(\n            client_id=\"pydantic-test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:4040/callback\")],\n        )\n        await oauth_proxy_with_storage.register_client(client)\n\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(\"http://localhost:4040/callback\"),\n            redirect_uri_provided_explicitly=True,\n            state=\"pydantic-state\",\n            code_challenge=\"pydantic-challenge\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        redirect_url = await oauth_proxy_with_storage.authorize(client, params)\n        parsed = urlparse(redirect_url)\n        query = parse_qs(parsed.query)\n        txn_id = query[\"txn_id\"][0]\n\n        # Retrieve using PydanticAdapter (which is what the proxy uses)\n        transaction_store = oauth_proxy_with_storage._transaction_store\n        txn_model = await transaction_store.get(key=txn_id)\n\n        # Should be a Pydantic model instance\n        assert isinstance(txn_model, OAuthTransaction)\n        assert txn_model.client_id == \"pydantic-test-client\"\n        assert txn_model.client_state == \"pydantic-state\"\n        assert txn_model.code_challenge == \"pydantic-challenge\"\n        assert txn_model.scopes == [\"read\", \"write\"]\n\n\nclass TestConsentSecurity:\n    \"\"\"Tests for consent page security features.\"\"\"\n\n    async def test_consent_sets_xfo_header(self, oauth_proxy_https):\n        \"\"\"Verify consent page sets X-Frame-Options header to prevent clickjacking.\"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-a\", \"http://localhost:5001/callback\"\n        )\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            r = c.get(f\"/consent?txn_id={txn_id}\")\n            assert r.status_code == 200\n            assert r.headers.get(\"X-Frame-Options\") == \"DENY\"\n\n    async def test_deny_sets_cookie_and_redirects_with_error(self, oauth_proxy_https):\n        \"\"\"Verify denying consent sets signed cookie and redirects with error.\"\"\"\n        client_redirect = \"http://localhost:5002/callback\"\n        txn_id, _ = await _start_flow(oauth_proxy_https, \"client-b\", client_redirect)\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            consent = c.get(f\"/consent?txn_id={txn_id}\")\n            csrf = _extract_csrf(consent.text)\n            assert csrf\n            # Persist consent page cookies on client instance to avoid per-request deprecation\n            for k, v in consent.cookies.items():\n                c.cookies.set(k, v)\n            r = c.post(\n                \"/consent\",\n                data={\"action\": \"deny\", \"txn_id\": txn_id, \"csrf_token\": csrf},\n                follow_redirects=False,\n            )\n            assert r.status_code in (302, 303)\n            loc = r.headers.get(\"location\", \"\")\n            parsed = urlparse(loc)\n            assert parsed.scheme == \"http\" and parsed.netloc.startswith(\"localhost\")\n            q = parse_qs(parsed.query)\n            assert q.get(\"error\") == [\"access_denied\"]\n            assert q.get(\"state\") == [\"client-state-xyz\"]\n            # Signed denied cookie should be set\n            assert \"MCP_DENIED_CLIENTS\" in \";\\n\".join(\n                r.headers.get(\"set-cookie\", \"\").splitlines()\n            )\n\n    async def test_approve_sets_cookie_and_redirects_to_upstream(\n        self, oauth_proxy_https\n    ):\n        \"\"\"Verify approving consent sets signed cookie and redirects to upstream.\"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-c\", \"http://localhost:5003/callback\"\n        )\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            consent = c.get(f\"/consent?txn_id={txn_id}\")\n            csrf = _extract_csrf(consent.text)\n            assert csrf\n            for k, v in consent.cookies.items():\n                c.cookies.set(k, v)\n            r = c.post(\n                \"/consent\",\n                data={\"action\": \"approve\", \"txn_id\": txn_id, \"csrf_token\": csrf},\n                follow_redirects=False,\n            )\n            assert r.status_code in (302, 303)\n            loc = r.headers.get(\"location\", \"\")\n            assert loc.startswith(\"https://github.com/login/oauth/authorize\")\n            assert f\"state={txn_id}\" in loc\n            # Signed approved cookie should be set with __Host- prefix for HTTPS\n            set_cookie = \";\\n\".join(r.headers.get(\"set-cookie\", \"\").splitlines())\n            assert \"__Host-MCP_APPROVED_CLIENTS\" in set_cookie\n\n    async def test_tampered_cookie_is_ignored(self, oauth_proxy_https):\n        \"\"\"Verify tampered approval cookie is ignored and consent page shown.\"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-d\", \"http://localhost:5004/callback\"\n        )\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            # Create a tampered cookie (invalid signature)\n            # Value format: payload.signature; using wrong signature to force failure\n            tampered_value = \"W10=.invalidsig\"\n            c.cookies.set(\"__Host-MCP_APPROVED_CLIENTS\", tampered_value)\n            r = c.get(f\"/consent?txn_id={txn_id}\", follow_redirects=False)\n            # Should not auto-redirect to upstream; should show consent page\n            assert r.status_code == 200\n            # httpx returns a URL object; compare path or stringify\n            assert urlparse(str(r.request.url)).path == \"/consent\"\n\n    async def test_autoapprove_cookie_skips_consent(self, oauth_proxy_https):\n        \"\"\"Verify valid approval cookie auto-approves and redirects to upstream.\"\"\"\n        client_id = \"client-e\"\n        redirect = \"http://localhost:5005/callback\"\n        txn_id, _ = await _start_flow(oauth_proxy_https, client_id, redirect)\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            # Approve once to set approved cookie\n            consent = c.get(f\"/consent?txn_id={txn_id}\")\n            csrf = _extract_csrf(consent.text)\n            for k, v in consent.cookies.items():\n                c.cookies.set(k, v)\n            r = c.post(\n                \"/consent\",\n                data={\n                    \"action\": \"approve\",\n                    \"txn_id\": txn_id,\n                    \"csrf_token\": csrf if csrf else \"\",\n                },\n                follow_redirects=False,\n            )\n            # Extract approved cookie value\n            set_cookie = \";\\n\".join(r.headers.get(\"set-cookie\", \"\").splitlines())\n            m = re.search(r\"__Host-MCP_APPROVED_CLIENTS=([^;]+)\", set_cookie)\n            assert m, \"approved cookie should be set\"\n            approved_cookie = m.group(1)\n\n            # Start a new flow for the same client and redirect\n            new_txn, _ = await _start_flow(oauth_proxy_https, client_id, redirect)\n            # Should auto-redirect to upstream when visiting consent due to cookie\n            c.cookies.set(\"__Host-MCP_APPROVED_CLIENTS\", approved_cookie)\n            r2 = c.get(f\"/consent?txn_id={new_txn}\", follow_redirects=False)\n            assert r2.status_code in (302, 303)\n            assert r2.headers.get(\"location\", \"\").startswith(\n                \"https://github.com/login/oauth/authorize\"\n            )\n"
  },
  {
    "path": "tests/server/auth/test_oauth_consent_page.py",
    "content": "\"\"\"Tests for OAuth Proxy consent page display, CSP policy, and consent binding cookie.\"\"\"\n\nimport re\nimport secrets\nimport time\nfrom unittest.mock import Mock\nfrom urllib.parse import parse_qs, urlparse\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.server.auth.provider import AuthorizationParams\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom mcp.types import Icon\nfrom pydantic import AnyUrl\nfrom starlette.applications import Starlette\nfrom starlette.testclient import TestClient\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth.auth import AccessToken, TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.models import OAuthTransaction\n\n\nclass _Verifier(TokenVerifier):\n    \"\"\"Minimal token verifier for security tests.\"\"\"\n\n    def __init__(self):\n        self.required_scopes = [\"read\"]\n\n    async def verify_token(self, token: str):\n        return AccessToken(\n            token=token, client_id=\"c\", scopes=self.required_scopes, expires_at=None\n        )\n\n\n@pytest.fixture\ndef oauth_proxy_https():\n    \"\"\"OAuthProxy configured with HTTPS base_url for __Host- cookies.\"\"\"\n    return OAuthProxy(\n        upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n        upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n        upstream_client_id=\"client-id\",\n        upstream_client_secret=\"client-secret\",\n        token_verifier=_Verifier(),\n        base_url=\"https://myserver.example\",\n        client_storage=MemoryStore(),\n        jwt_signing_key=\"test-secret\",\n    )\n\n\nasync def _start_flow(\n    proxy: OAuthProxy, client_id: str, redirect: str\n) -> tuple[str, str]:\n    \"\"\"Register client and start auth; returns (txn_id, consent_url).\"\"\"\n    await proxy.register_client(\n        OAuthClientInformationFull(\n            client_id=client_id,\n            client_secret=\"s\",\n            redirect_uris=[AnyUrl(redirect)],\n        )\n    )\n    params = AuthorizationParams(\n        redirect_uri=AnyUrl(redirect),\n        redirect_uri_provided_explicitly=True,\n        state=\"client-state-xyz\",\n        code_challenge=\"challenge\",\n        scopes=[\"read\"],\n    )\n    consent_url = await proxy.authorize(\n        OAuthClientInformationFull(\n            client_id=client_id,\n            client_secret=\"s\",\n            redirect_uris=[AnyUrl(redirect)],\n        ),\n        params,\n    )\n    qs = parse_qs(urlparse(consent_url).query)\n    return qs[\"txn_id\"][0], consent_url\n\n\ndef _extract_csrf(html: str) -> str | None:\n    \"\"\"Extract CSRF token from HTML form.\"\"\"\n    m = re.search(r\"name=\\\"csrf_token\\\"\\s+value=\\\"([^\\\"]+)\\\"\", html)\n    return m.group(1) if m else None\n\n\nclass TestConsentPageServerIcon:\n    \"\"\"Tests for server icon display in OAuth consent screen.\"\"\"\n\n    async def test_consent_screen_displays_server_icon(self):\n        \"\"\"Test that consent screen shows server's custom icon when available.\"\"\"\n\n        # Create mock JWT verifier\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\"]\n        verifier.verify_token = Mock(return_value=None)\n\n        # Create OAuthProxy\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=verifier,\n            base_url=\"https://proxy.example.com\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n        )\n\n        # Create FastMCP server with custom icon\n\n        server = FastMCP(\n            name=\"My Custom Server\",\n            auth=proxy,\n            icons=[Icon(src=\"https://example.com/custom-icon.png\")],\n            website_url=\"https://example.com\",\n        )\n\n        # Create HTTP app\n        app = server.http_app()\n\n        # Register a test client with the proxy\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        # Create a transaction manually\n\n        txn_id = \"test-txn-id\"\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=\"test-client\",\n            client_redirect_uri=\"http://localhost:12345/callback\",\n            client_state=\"client-state\",\n            code_challenge=\"challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\"],\n            created_at=time.time(),\n        )\n        await proxy._transaction_store.put(key=txn_id, value=transaction)\n\n        # Make request to consent page\n        with TestClient(app) as client:\n            response = client.get(f\"/consent?txn_id={txn_id}\")\n\n            # Check that response is successful\n            assert response.status_code == 200\n\n            # Check that HTML contains custom icon\n            assert \"https://example.com/custom-icon.png\" in response.text\n\n            # Check that server name is used as alt text\n            assert 'alt=\"My Custom Server\"' in response.text\n\n    async def test_consent_screen_falls_back_to_fastmcp_logo(self):\n        \"\"\"Test that consent screen shows FastMCP logo when no server icon provided.\"\"\"\n\n        # Create mock JWT verifier\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\"]\n        verifier.verify_token = Mock(return_value=None)\n\n        # Create OAuthProxy\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=verifier,\n            base_url=\"https://proxy.example.com\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n        )\n\n        # Create FastMCP server without icon\n        server = FastMCP(name=\"Server Without Icon\", auth=proxy)\n\n        # Create HTTP app\n        app = server.http_app()\n\n        # Register a test client\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        # Create a transaction\n\n        txn_id = \"test-txn-id\"\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=\"test-client\",\n            client_redirect_uri=\"http://localhost:12345/callback\",\n            client_state=\"client-state\",\n            code_challenge=\"challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\"],\n            created_at=time.time(),\n        )\n        await proxy._transaction_store.put(key=txn_id, value=transaction)\n\n        # Make request to consent page\n        with TestClient(app) as client:\n            response = client.get(f\"/consent?txn_id={txn_id}\")\n\n            # Check that response is successful\n            assert response.status_code == 200\n\n            # Check that HTML contains FastMCP logo\n            assert \"gofastmcp.com/assets/brand/blue-logo.png\" in response.text\n\n            # Check that alt text is still the server name\n            assert 'alt=\"Server Without Icon\"' in response.text\n\n    async def test_consent_screen_escapes_server_name(self):\n        \"\"\"Test that server name is properly HTML-escaped.\"\"\"\n\n        # Create mock JWT verifier\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\"]\n        verifier.verify_token = Mock(return_value=None)\n\n        # Create OAuthProxy\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=verifier,\n            base_url=\"https://proxy.example.com\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n        )\n\n        # Create FastMCP server with special characters in name\n        server = FastMCP(\n            name='<script>alert(\"xss\")</script>Server',\n            auth=proxy,\n            icons=[Icon(src=\"https://example.com/icon.png\")],\n        )\n\n        # Create HTTP app\n        app = server.http_app()\n\n        # Register a test client\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        # Create a transaction\n\n        txn_id = \"test-txn-id\"\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=\"test-client\",\n            client_redirect_uri=\"http://localhost:12345/callback\",\n            client_state=\"client-state\",\n            code_challenge=\"challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\"],\n            created_at=time.time(),\n        )\n        await proxy._transaction_store.put(key=txn_id, value=transaction)\n\n        # Make request to consent page\n        with TestClient(app) as client:\n            response = client.get(f\"/consent?txn_id={txn_id}\")\n\n            # Check that response is successful\n            assert response.status_code == 200\n\n            # Check that script tag is escaped\n            assert \"<script>\" not in response.text\n            assert \"&lt;script&gt;\" in response.text\n            assert (\n                'alt=\"&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;Server\"'\n                in response.text\n            )\n\n\nclass TestConsentCSPPolicy:\n    \"\"\"Tests for Content Security Policy customization on consent page.\"\"\"\n\n    async def test_default_csp_omits_form_action(self):\n        \"\"\"Test that default CSP omits form-action to avoid Chrome redirect chain issues.\"\"\"\n\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\"]\n        verifier.verify_token = Mock(return_value=None)\n\n        # Create OAuthProxy with default CSP (no custom CSP)\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=verifier,\n            base_url=\"https://proxy.example.com\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n        )\n\n        server = FastMCP(name=\"Test Server\", auth=proxy)\n        app = server.http_app()\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        txn_id = \"test-txn-id\"\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=\"test-client\",\n            client_redirect_uri=\"http://localhost:12345/callback\",\n            client_state=\"client-state\",\n            code_challenge=\"challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\"],\n            created_at=time.time(),\n        )\n        await proxy._transaction_store.put(key=txn_id, value=transaction)\n\n        with TestClient(app) as client:\n            response = client.get(f\"/consent?txn_id={txn_id}\")\n\n            assert response.status_code == 200\n            # Default CSP should be present but WITHOUT form-action\n            assert 'http-equiv=\"Content-Security-Policy\"' in response.text\n            assert \"form-action\" not in response.text\n\n    async def test_empty_csp_disables_csp_meta_tag(self):\n        \"\"\"Test that empty string CSP disables CSP meta tag entirely.\"\"\"\n\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\"]\n        verifier.verify_token = Mock(return_value=None)\n\n        # Create OAuthProxy with empty CSP to disable it\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=verifier,\n            base_url=\"https://proxy.example.com\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n            consent_csp_policy=\"\",  # Empty string disables CSP\n        )\n\n        server = FastMCP(name=\"Test Server\", auth=proxy)\n        app = server.http_app()\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        txn_id = \"test-txn-id\"\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=\"test-client\",\n            client_redirect_uri=\"http://localhost:12345/callback\",\n            client_state=\"client-state\",\n            code_challenge=\"challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\"],\n            created_at=time.time(),\n        )\n        await proxy._transaction_store.put(key=txn_id, value=transaction)\n\n        with TestClient(app) as client:\n            response = client.get(f\"/consent?txn_id={txn_id}\")\n\n            assert response.status_code == 200\n            # CSP meta tag should NOT be present\n            assert 'http-equiv=\"Content-Security-Policy\"' not in response.text\n\n    async def test_custom_csp_policy_is_used(self):\n        \"\"\"Test that custom CSP policy is applied to consent page.\"\"\"\n\n        verifier = Mock(spec=TokenVerifier)\n        verifier.required_scopes = [\"read\"]\n        verifier.verify_token = Mock(return_value=None)\n\n        # Create OAuthProxy with custom CSP policy\n        custom_csp = \"default-src 'self'; script-src 'none'\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://oauth.example.com/authorize\",\n            upstream_token_endpoint=\"https://oauth.example.com/token\",\n            upstream_client_id=\"upstream-client\",\n            upstream_client_secret=\"upstream-secret\",\n            token_verifier=verifier,\n            base_url=\"https://proxy.example.com\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n            consent_csp_policy=custom_csp,\n        )\n\n        server = FastMCP(name=\"Test Server\", auth=proxy)\n        app = server.http_app()\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client\",\n            client_secret=\"test-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:12345/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        txn_id = \"test-txn-id\"\n        transaction = OAuthTransaction(\n            txn_id=txn_id,\n            client_id=\"test-client\",\n            client_redirect_uri=\"http://localhost:12345/callback\",\n            client_state=\"client-state\",\n            code_challenge=\"challenge\",\n            code_challenge_method=\"S256\",\n            scopes=[\"read\"],\n            created_at=time.time(),\n        )\n        await proxy._transaction_store.put(key=txn_id, value=transaction)\n\n        with TestClient(app) as client:\n            response = client.get(f\"/consent?txn_id={txn_id}\")\n\n            assert response.status_code == 200\n            # Custom CSP should be present (HTML-escaped)\n            assert 'http-equiv=\"Content-Security-Policy\"' in response.text\n            # Check for the HTML-escaped version (single quotes become &#x27;)\n            import html\n\n            assert html.escape(custom_csp, quote=True) in response.text\n            # Default form-action should NOT be present (we're using custom)\n            assert \"form-action\" not in response.text\n\n\nclass TestConsentBindingCookie:\n    \"\"\"Tests for consent binding cookie that prevents confused deputy attacks.\n\n    GHSA-rww4-4w9c-7733: Without browser-binding between consent approval and\n    the IdP callback, an attacker can intercept the upstream authorization URL\n    and send it to a victim whose browser completes the flow.\n    \"\"\"\n\n    async def test_approve_sets_consent_binding_cookie(self, oauth_proxy_https):\n        \"\"\"Approving consent must set a signed consent binding cookie.\"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-binding\", \"http://localhost:6001/callback\"\n        )\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            consent = c.get(f\"/consent?txn_id={txn_id}\")\n            csrf = _extract_csrf(consent.text)\n            assert csrf\n            for k, v in consent.cookies.items():\n                c.cookies.set(k, v)\n            r = c.post(\n                \"/consent\",\n                data={\"action\": \"approve\", \"txn_id\": txn_id, \"csrf_token\": csrf},\n                follow_redirects=False,\n            )\n            assert r.status_code in (302, 303)\n            set_cookie_header = r.headers.get(\"set-cookie\", \"\")\n            assert \"__Host-MCP_CONSENT_BINDING\" in set_cookie_header\n\n    async def test_auto_approve_sets_consent_binding_cookie(self, oauth_proxy_https):\n        \"\"\"Auto-approve path (previously approved client) must also set the binding cookie.\"\"\"\n        client_id = \"client-autobinding\"\n        redirect = \"http://localhost:6002/callback\"\n        txn_id, _ = await _start_flow(oauth_proxy_https, client_id, redirect)\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            # First: approve manually to get the approved cookie\n            consent = c.get(f\"/consent?txn_id={txn_id}\")\n            csrf = _extract_csrf(consent.text)\n            assert csrf\n            for k, v in consent.cookies.items():\n                c.cookies.set(k, v)\n            r = c.post(\n                \"/consent\",\n                data={\"action\": \"approve\", \"txn_id\": txn_id, \"csrf_token\": csrf},\n                follow_redirects=False,\n            )\n            # Extract approved cookie\n            m = re.search(\n                r\"__Host-MCP_APPROVED_CLIENTS=([^;]+)\",\n                r.headers.get(\"set-cookie\", \"\"),\n            )\n            assert m\n            approved_cookie = m.group(1)\n\n            # Second: start new flow, auto-approve should set binding cookie\n            new_txn, _ = await _start_flow(oauth_proxy_https, client_id, redirect)\n            c.cookies.set(\"__Host-MCP_APPROVED_CLIENTS\", approved_cookie)\n            r2 = c.get(f\"/consent?txn_id={new_txn}\", follow_redirects=False)\n            assert r2.status_code in (302, 303)\n            set_cookie_header = r2.headers.get(\"set-cookie\", \"\")\n            assert \"__Host-MCP_CONSENT_BINDING\" in set_cookie_header\n\n    async def test_parallel_flows_do_not_interfere(self, oauth_proxy_https):\n        \"\"\"Multiple concurrent consent flows in the same browser must not clobber each other.\n\n        Uses two different clients so the second flow also shows a consent form\n        (auto-approve only kicks in for the same client+redirect pair).\n        \"\"\"\n        txn1, _ = await _start_flow(\n            oauth_proxy_https, \"client-par-a\", \"http://localhost:6010/callback\"\n        )\n        txn2, _ = await _start_flow(\n            oauth_proxy_https, \"client-par-b\", \"http://localhost:6011/callback\"\n        )\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            # Approve first flow\n            consent1 = c.get(f\"/consent?txn_id={txn1}\")\n            csrf1 = _extract_csrf(consent1.text)\n            assert csrf1\n            for k, v in consent1.cookies.items():\n                c.cookies.set(k, v)\n            r1 = c.post(\n                \"/consent\",\n                data={\"action\": \"approve\", \"txn_id\": txn1, \"csrf_token\": csrf1},\n                follow_redirects=False,\n            )\n            assert r1.status_code in (302, 303)\n            for k, v in r1.cookies.items():\n                c.cookies.set(k, v)\n\n            # Approve second flow (different client, so consent form is shown)\n            consent2 = c.get(f\"/consent?txn_id={txn2}\")\n            csrf2 = _extract_csrf(consent2.text)\n            assert csrf2\n            for k, v in consent2.cookies.items():\n                c.cookies.set(k, v)\n            r2 = c.post(\n                \"/consent\",\n                data={\"action\": \"approve\", \"txn_id\": txn2, \"csrf_token\": csrf2},\n                follow_redirects=False,\n            )\n            assert r2.status_code in (302, 303)\n            for k, v in r2.cookies.items():\n                c.cookies.set(k, v)\n\n            # Both transactions should have consent tokens\n            txn1_model = await oauth_proxy_https._transaction_store.get(key=txn1)\n            txn2_model = await oauth_proxy_https._transaction_store.get(key=txn2)\n            assert txn1_model is not None and txn1_model.consent_token\n            assert txn2_model is not None and txn2_model.consent_token\n\n            # First flow's callback should still work (cookie has both bindings)\n            r_cb1 = c.get(\n                f\"/auth/callback?code=fake&state={txn1}\", follow_redirects=False\n            )\n            # Should NOT be 403 — the binding for txn1 should still be in the cookie.\n            # It will fail at token exchange (500) but not at consent verification.\n            assert r_cb1.status_code != 403\n\n    async def test_idp_callback_rejects_missing_consent_cookie(self, oauth_proxy_https):\n        \"\"\"IdP callback must reject requests without the consent binding cookie.\n\n        This is the core confused deputy scenario: a different browser (the victim)\n        hits the callback without the cookie that was set on the attacker's browser.\n        \"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-nocd\", \"http://localhost:6003/callback\"\n        )\n        # Manually set consent_token on transaction (simulating consent approval)\n        txn_model = await oauth_proxy_https._transaction_store.get(key=txn_id)\n        assert txn_model is not None\n        txn_model.consent_token = secrets.token_urlsafe(32)\n        await oauth_proxy_https._transaction_store.put(\n            key=txn_id, value=txn_model, ttl=15 * 60\n        )\n\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            # Hit callback WITHOUT the consent binding cookie\n            r = c.get(\n                f\"/auth/callback?code=fake-code&state={txn_id}\",\n                follow_redirects=False,\n            )\n            assert r.status_code == 403\n            assert (\n                \"session mismatch\" in r.text.lower() or \"Authorization Error\" in r.text\n            )\n\n    async def test_idp_callback_rejects_wrong_consent_cookie(self, oauth_proxy_https):\n        \"\"\"IdP callback must reject requests with a tampered consent binding cookie.\"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-wrongcd\", \"http://localhost:6004/callback\"\n        )\n        txn_model = await oauth_proxy_https._transaction_store.get(key=txn_id)\n        assert txn_model is not None\n        txn_model.consent_token = secrets.token_urlsafe(32)\n        await oauth_proxy_https._transaction_store.put(\n            key=txn_id, value=txn_model, ttl=15 * 60\n        )\n\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            # Set a wrong/tampered consent binding cookie\n            c.cookies.set(\"__Host-MCP_CONSENT_BINDING\", \"wrong-token.invalidsig\")\n            r = c.get(\n                f\"/auth/callback?code=fake-code&state={txn_id}\",\n                follow_redirects=False,\n            )\n            assert r.status_code == 403\n\n    async def test_idp_callback_rejects_missing_consent_token_on_transaction(\n        self, oauth_proxy_https\n    ):\n        \"\"\"IdP callback must reject when transaction has no consent_token set.\"\"\"\n        txn_id, _ = await _start_flow(\n            oauth_proxy_https, \"client-notxntoken\", \"http://localhost:6005/callback\"\n        )\n        # Transaction exists but consent_token is None (consent was never completed)\n        app = Starlette(routes=oauth_proxy_https.get_routes())\n        with TestClient(app) as c:\n            r = c.get(\n                f\"/auth/callback?code=fake-code&state={txn_id}\",\n                follow_redirects=False,\n            )\n            assert r.status_code == 403\n\n    async def test_consent_disabled_skips_binding_check(self):\n        \"\"\"When require_authorization_consent=False, the binding check is skipped.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n            upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n            upstream_client_id=\"client-id\",\n            upstream_client_secret=\"client-secret\",\n            token_verifier=_Verifier(),\n            base_url=\"https://myserver.example\",\n            client_storage=MemoryStore(),\n            jwt_signing_key=\"test-secret\",\n            require_authorization_consent=False,\n        )\n        client_id = \"client-noconsent\"\n        redirect = \"http://localhost:6006/callback\"\n        await proxy.register_client(\n            OAuthClientInformationFull(\n                client_id=client_id,\n                client_secret=\"s\",\n                redirect_uris=[AnyUrl(redirect)],\n            )\n        )\n        params = AuthorizationParams(\n            redirect_uri=AnyUrl(redirect),\n            redirect_uri_provided_explicitly=True,\n            state=\"st\",\n            code_challenge=\"ch\",\n            scopes=[\"read\"],\n        )\n        upstream_url = await proxy.authorize(\n            OAuthClientInformationFull(\n                client_id=client_id,\n                client_secret=\"s\",\n                redirect_uris=[AnyUrl(redirect)],\n            ),\n            params,\n        )\n        # With consent disabled, authorize returns upstream URL directly\n        assert upstream_url.startswith(\"https://github.com/login/oauth/authorize\")\n        qs = parse_qs(urlparse(upstream_url).query)\n        txn_id = qs[\"state\"][0]\n\n        # The transaction should have no consent_token\n        txn_model = await proxy._transaction_store.get(key=txn_id)\n        assert txn_model is not None\n        assert txn_model.consent_token is None\n\n        # IdP callback should NOT reject due to missing consent cookie\n        # (it will fail at token exchange, but not at the consent check)\n        app = Starlette(routes=proxy.get_routes())\n        with TestClient(app) as c:\n            r = c.get(\n                f\"/auth/callback?code=fake-code&state={txn_id}\",\n                follow_redirects=False,\n            )\n            # Should NOT be 403 (consent binding rejection)\n            # It will be 500 because the fake code can't be exchanged with GitHub,\n            # but that's fine — we're verifying the consent check was skipped.\n            assert r.status_code != 403\n"
  },
  {
    "path": "tests/server/auth/test_oauth_mounting.py",
    "content": "\"\"\"Tests for OAuth .well-known routes when FastMCP apps are mounted in parent ASGI apps.\n\nThis test file validates the fix for issue #2077 where .well-known/oauth-protected-resource\nreturns 404 at root level when a FastMCP app is mounted under a path prefix.\n\nThe fix uses MCP SDK 1.17+ which implements RFC 9728 path-scoped well-known URLs.\n\"\"\"\n\nimport httpx\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom pydantic import AnyHttpUrl\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import RemoteAuthProvider\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n\n@pytest.fixture\ndef test_tokens():\n    \"\"\"Standard test tokens fixture.\"\"\"\n    return {\n        \"test_token\": {\n            \"client_id\": \"test-client\",\n            \"scopes\": [\"read\", \"write\"],\n        }\n    }\n\n\nclass TestOAuthMounting:\n    \"\"\"Test OAuth .well-known routes with mounted FastMCP apps.\"\"\"\n\n    async def test_well_known_with_direct_deployment(self, test_tokens):\n        \"\"\"Test that .well-known routes work when app is deployed directly (not mounted).\n\n        This is the baseline - it should work as expected.\n        Per RFC 9728, if the resource is at /mcp, the well-known endpoint is at\n        /.well-known/oauth-protected-resource/mcp (path-scoped).\n        \"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # RFC 9728: path-scoped well-known URL\n            # Resource is at /mcp, so well-known should be at /.well-known/oauth-protected-resource/mcp\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n            assert response.status_code == 200\n\n            data = response.json()\n            assert data[\"resource\"] == \"https://api.example.com/mcp\"\n            assert data[\"authorization_servers\"] == [\"https://auth.example.com/\"]\n\n    async def test_well_known_with_mounted_app(self, test_tokens):\n        \"\"\"Test that .well-known routes work when explicitly mounted at root.\n\n        This test uses the CANONICAL pattern for mounting:\n        - base_url includes the mount prefix (\"/api\")\n        - mcp_path is just the internal MCP path (\"/mcp\")\n        - These combine: base_url + mcp_path = actual URL\n\n        The well-known routes are mounted at root level for RFC 9728 compliance.\n        \"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        # CANONICAL PATTERN: base_url includes the mount prefix\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com/api\",  # Includes /api mount prefix\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_app = mcp.http_app(path=\"/mcp\")\n\n        # Pass just the internal mcp_path, NOT the full mount path\n        # The auth provider will combine base_url + mcp_path internally\n        well_known_routes = auth_provider.get_well_known_routes(mcp_path=\"/mcp\")\n\n        parent_app = Starlette(\n            routes=[\n                *well_known_routes,  # Well-known routes at root level\n                Mount(\"/api\", app=mcp_app),  # MCP app under /api\n            ],\n            lifespan=mcp_app.lifespan,\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=parent_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The CORRECT RFC 9728 path-scoped well-known URL at root\n            # Resource is at /api/mcp, so well-known is at /.well-known/oauth-protected-resource/api/mcp\n            response = await client.get(\"/.well-known/oauth-protected-resource/api/mcp\")\n            assert response.status_code == 200\n\n            data = response.json()\n            assert data[\"resource\"] == \"https://api.example.com/api/mcp\"\n            assert data[\"authorization_servers\"] == [\"https://auth.example.com/\"]\n\n            # There will also be an extra route at /api/.well-known/oauth-protected-resource/mcp\n            # (from the mounted MCP app), but we don't care about that as long as the correct one exists\n\n    async def test_mcp_endpoint_with_mounted_app(self, test_tokens):\n        \"\"\"Test that MCP endpoint works correctly when mounted.\n\n        This confirms the MCP functionality itself works with mounting.\n        \"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n\n        @mcp.tool\n        def test_tool(message: str) -> str:\n            return f\"Echo: {message}\"\n\n        mcp_app = mcp.http_app(path=\"/mcp\")\n\n        # Mount the MCP app under /api prefix\n        parent_app = Starlette(\n            routes=[\n                Mount(\"/api\", app=mcp_app),\n            ],\n            lifespan=mcp_app.lifespan,\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=parent_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The MCP endpoint should work at /api/mcp (mounted correctly)\n            # This is a basic connectivity test\n            response = await client.get(\"/api/mcp\")\n\n            # We expect either 200 (if no auth required for GET) or 401 (if auth required)\n            # The key is that it's NOT 404\n            assert response.status_code in [200, 401, 405]\n\n    async def test_nested_mounting(self, test_tokens):\n        \"\"\"Test .well-known routes with deeply nested mounts.\n\n        Uses CANONICAL pattern: base_url includes all mount prefixes.\n        \"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        # CANONICAL PATTERN: base_url includes full mount path /outer/inner\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com/outer/inner\",  # Includes nested mount path\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_app = mcp.http_app(path=\"/mcp\")\n\n        # Pass just the internal mcp_path\n        well_known_routes = auth_provider.get_well_known_routes(mcp_path=\"/mcp\")\n\n        # Create nested mounts\n        inner_app = Starlette(\n            routes=[Mount(\"/inner\", app=mcp_app)],\n        )\n        outer_app = Starlette(\n            routes=[\n                *well_known_routes,  # Well-known routes at root level\n                Mount(\"/outer\", app=inner_app),\n            ],\n            lifespan=mcp_app.lifespan,\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=outer_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # RFC 9728: path-scoped well-known URL for nested mounting\n            # Resource is at /outer/inner/mcp, so well-known is at /.well-known/oauth-protected-resource/outer/inner/mcp\n            response = await client.get(\n                \"/.well-known/oauth-protected-resource/outer/inner/mcp\"\n            )\n            assert response.status_code == 200\n\n            data = response.json()\n            assert data[\"resource\"] == \"https://api.example.com/outer/inner/mcp\"\n\n    async def test_oauth_authorization_server_metadata_with_base_url_and_issuer_url(\n        self, test_tokens\n    ):\n        \"\"\"Test OAuth authorization server metadata when base_url and issuer_url differ.\n\n        This validates the fix for issue #2287 where operational OAuth endpoints\n        (/authorize, /token) should be declared at base_url in the metadata,\n        not at issuer_url.\n\n        Scenario: FastMCP server mounted at /api prefix\n        - issuer_url: https://api.example.com (root level)\n        - base_url: https://api.example.com/api (includes mount prefix)\n        - Expected: metadata declares endpoints at base_url\n        \"\"\"\n        # Create OAuth proxy with different base_url and issuer_url\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_provider = OAuthProxy(\n            upstream_authorization_endpoint=\"https://upstream.example.com/authorize\",\n            upstream_token_endpoint=\"https://upstream.example.com/token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=token_verifier,\n            base_url=\"https://api.example.com/api\",  # Includes mount prefix\n            issuer_url=\"https://api.example.com\",  # Root level\n            client_storage=MemoryStore(),\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_app = mcp.http_app(path=\"/mcp\")\n\n        # Get well-known routes for mounting at root\n        well_known_routes = auth_provider.get_well_known_routes(mcp_path=\"/mcp\")\n\n        # Mount the app under /api prefix\n        parent_app = Starlette(\n            routes=[\n                *well_known_routes,  # Well-known routes at root level\n                Mount(\"/api\", app=mcp_app),  # MCP app under /api\n            ],\n            lifespan=mcp_app.lifespan,\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=parent_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # Fetch the authorization server metadata\n            response = await client.get(\"/.well-known/oauth-authorization-server\")\n            assert response.status_code == 200\n\n            metadata = response.json()\n\n            # CRITICAL: The metadata should declare endpoints at base_url,\n            # not issuer_url, because that's where they're actually mounted\n            assert (\n                metadata[\"authorization_endpoint\"]\n                == \"https://api.example.com/api/authorize\"\n            )\n            assert metadata[\"token_endpoint\"] == \"https://api.example.com/api/token\"\n            assert (\n                metadata[\"registration_endpoint\"]\n                == \"https://api.example.com/api/register\"\n            )\n\n            # The issuer field should use base_url (where the server is actually running)\n            # Note: MCP SDK may or may not add a trailing slash\n            assert metadata[\"issuer\"] in [\n                \"https://api.example.com/api\",\n                \"https://api.example.com/api/\",\n            ]\n\n    async def test_oauth_authorization_server_metadata_path_aware_discovery(\n        self, test_tokens\n    ):\n        \"\"\"Test RFC 8414 path-aware discovery when issuer_url has a path.\n\n        This validates the fix for issue #2527 where authorization server metadata\n        should be exposed at a path-aware URL when issuer_url has a path component.\n\n        When issuer_url defaults to base_url (e.g., http://example.com/api), the\n        authorization server metadata should be at:\n        /.well-known/oauth-authorization-server/api\n\n        This is consistent with how protected resource metadata already works\n        (RFC 9728) and complies with RFC 8414 path-aware discovery.\n        \"\"\"\n        # Create OAuth proxy where issuer_url defaults to base_url (which has a path)\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_provider = OAuthProxy(\n            upstream_authorization_endpoint=\"https://upstream.example.com/authorize\",\n            upstream_token_endpoint=\"https://upstream.example.com/token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=token_verifier,\n            base_url=\"https://api.example.com/api\",  # Has path, no explicit issuer_url\n            client_storage=MemoryStore(),\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_app = mcp.http_app(path=\"/mcp\")\n\n        # Get well-known routes - should include path-aware authorization server metadata\n        well_known_routes = auth_provider.get_well_known_routes(mcp_path=\"/mcp\")\n\n        # Find the authorization server metadata route\n        auth_server_routes = [\n            r for r in well_known_routes if \"oauth-authorization-server\" in r.path\n        ]\n        assert len(auth_server_routes) == 1\n\n        # The route should be path-aware (RFC 8414)\n        assert (\n            auth_server_routes[0].path == \"/.well-known/oauth-authorization-server/api\"\n        )\n\n        # Find the protected resource metadata route for comparison\n        protected_resource_routes = [\n            r for r in well_known_routes if \"oauth-protected-resource\" in r.path\n        ]\n        assert len(protected_resource_routes) == 1\n        # Protected resource should also be path-aware (RFC 9728)\n        assert (\n            protected_resource_routes[0].path\n            == \"/.well-known/oauth-protected-resource/api/mcp\"\n        )\n\n        # Mount the app and verify the routes are accessible\n        parent_app = Starlette(\n            routes=[\n                *well_known_routes,\n                Mount(\"/api\", app=mcp_app),\n            ],\n            lifespan=mcp_app.lifespan,\n        )\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=parent_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # Path-aware authorization server metadata should be accessible\n            response = await client.get(\"/.well-known/oauth-authorization-server/api\")\n            assert response.status_code == 200\n\n            metadata = response.json()\n            assert (\n                metadata[\"authorization_endpoint\"]\n                == \"https://api.example.com/api/authorize\"\n            )\n            assert metadata[\"token_endpoint\"] == \"https://api.example.com/api/token\"\n\n            # Path-aware protected resource metadata should also work\n            response = await client.get(\"/.well-known/oauth-protected-resource/api/mcp\")\n            assert response.status_code == 200\n\n    async def test_oauth_authorization_server_metadata_root_issuer(self, test_tokens):\n        \"\"\"Test that root-level issuer_url still uses root discovery path.\n\n        When issuer_url is explicitly set to root (no path), the authorization\n        server metadata should remain at the root path:\n        /.well-known/oauth-authorization-server\n\n        This maintains backwards compatibility with the documented mounting pattern.\n        \"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_provider = OAuthProxy(\n            upstream_authorization_endpoint=\"https://upstream.example.com/authorize\",\n            upstream_token_endpoint=\"https://upstream.example.com/token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=token_verifier,\n            base_url=\"https://api.example.com/api\",\n            issuer_url=\"https://api.example.com\",  # Explicitly root\n            client_storage=MemoryStore(),\n        )\n\n        well_known_routes = auth_provider.get_well_known_routes(mcp_path=\"/mcp\")\n\n        # Find the authorization server metadata route\n        auth_server_routes = [\n            r for r in well_known_routes if \"oauth-authorization-server\" in r.path\n        ]\n        assert len(auth_server_routes) == 1\n\n        # Should be at root (no path suffix) when issuer_url is root\n        assert auth_server_routes[0].path == \"/.well-known/oauth-authorization-server\"\n"
  },
  {
    "path": "tests/server/auth/test_oauth_proxy_redirect_validation.py",
    "content": "\"\"\"Tests for OAuth proxy redirect URI validation.\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.shared.auth import InvalidRedirectUriError\nfrom pydantic import AnyHttpUrl, AnyUrl\n\nfrom fastmcp.server.auth.auth import TokenVerifier\nfrom fastmcp.server.auth.cimd import CIMDDocument\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\nfrom fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient\n\n# Standard public IP used for DNS mocking in tests\nTEST_PUBLIC_IP = \"93.184.216.34\"\n\n\nclass MockTokenVerifier(TokenVerifier):\n    \"\"\"Mock token verifier for testing.\"\"\"\n\n    def __init__(self):\n        self.required_scopes = []\n\n    async def verify_token(self, token: str) -> dict | None:  # type: ignore[override]\n        return {\"sub\": \"test-user\"}\n\n\nclass TestProxyDCRClient:\n    \"\"\"Test ProxyDCRClient redirect URI validation.\"\"\"\n\n    def test_default_allows_all(self):\n        \"\"\"Test that default configuration allows all URIs for DCR compatibility.\"\"\"\n        client = ProxyDCRClient(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000\")],\n        )\n\n        # All URIs should be allowed by default for DCR compatibility\n        assert client.validate_redirect_uri(AnyUrl(\"http://localhost:3000\")) == AnyUrl(\n            \"http://localhost:3000\"\n        )\n        assert client.validate_redirect_uri(AnyUrl(\"http://localhost:8080\")) == AnyUrl(\n            \"http://localhost:8080\"\n        )\n        assert client.validate_redirect_uri(AnyUrl(\"http://127.0.0.1:3000\")) == AnyUrl(\n            \"http://127.0.0.1:3000\"\n        )\n        assert client.validate_redirect_uri(AnyUrl(\"http://example.com\")) == AnyUrl(\n            \"http://example.com\"\n        )\n        assert client.validate_redirect_uri(\n            AnyUrl(\"https://claude.ai/api/mcp/auth_callback\")\n        ) == AnyUrl(\"https://claude.ai/api/mcp/auth_callback\")\n\n    def test_custom_patterns(self):\n        \"\"\"Test custom redirect URI patterns.\"\"\"\n        client = ProxyDCRClient(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000\")],\n            allowed_redirect_uri_patterns=[\n                \"http://localhost:*\",\n                \"https://app.example.com/*\",\n            ],\n        )\n\n        # Allowed by patterns\n        assert client.validate_redirect_uri(AnyUrl(\"http://localhost:3000\"))\n        assert client.validate_redirect_uri(AnyUrl(\"https://app.example.com/callback\"))\n\n        # Not allowed by patterns - will fallback to base validation\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://127.0.0.1:3000\"))\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(\n                AnyUrl(\"cursor://anysphere.cursor-mcp/oauth/callback\")\n            )\n\n    def test_default_not_applied_when_custom_patterns_supplied(self):\n        \"\"\"Test that default validation is not applied when custom patterns are supplied.\"\"\"\n        allowed_patterns = [\n            \"cursor://anysphere.cursor-mcp/oauth/callback\",\n            \"https://app.example.com/*\",\n        ]\n\n        client = ProxyDCRClient(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000\")],\n            allowed_redirect_uri_patterns=allowed_patterns,\n        )\n\n        assert client.validate_redirect_uri(\n            AnyUrl(\"https://app.example.com/oauth/callback\")\n        )\n        assert client.validate_redirect_uri(\n            AnyUrl(\"cursor://anysphere.cursor-mcp/oauth/callback\")\n        )\n\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://localhost:3000\"))\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://127.0.0.1:3000\"))\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"https://example.com\"))\n\n    def test_empty_list_allows_none(self):\n        \"\"\"Test that empty pattern list allows no URIs.\"\"\"\n        client = ProxyDCRClient(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000\")],\n            allowed_redirect_uri_patterns=[],\n        )\n\n        # Nothing should be allowed (except the pre-registered redirect_uris via fallback)\n        # Pre-registered URI should work via fallback to base validation\n        assert client.validate_redirect_uri(AnyUrl(\"http://localhost:3000\"))\n\n        # Non-registered URIs should be rejected\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://example.com\"))\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"https://anywhere.com:9999/path\"))\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://localhost:5000\"))\n\n    def test_none_redirect_uri(self):\n        \"\"\"Test that None redirect URI uses default behavior.\"\"\"\n        client = ProxyDCRClient(\n            client_id=\"test\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000\")],\n        )\n\n        # None should use the first registered URI\n        result = client.validate_redirect_uri(None)\n        assert result == AnyUrl(\"http://localhost:3000\")\n\n    def test_cimd_none_redirect_uri_single_exact(self):\n        \"\"\"CIMD clients may omit redirect_uri only when a single exact URI exists.\"\"\"\n        cimd_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n        )\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=cimd_doc,\n        )\n\n        result = client.validate_redirect_uri(None)\n        assert result == AnyUrl(\"http://localhost:3000/callback\")\n\n    def test_cimd_none_redirect_uri_respects_proxy_patterns(self):\n        \"\"\"CIMD fallback redirect_uri must still satisfy proxy allowlist patterns.\"\"\"\n        cimd_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"https://evil.com/callback\"],\n        )\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=cimd_doc,\n            allowed_redirect_uri_patterns=[\"http://localhost:*\"],\n        )\n\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(None)\n\n    def test_cimd_none_redirect_uri_wildcard_rejected(self):\n        \"\"\"CIMD clients must specify redirect_uri when only wildcard patterns exist.\"\"\"\n        cimd_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:*/callback\"],\n        )\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=cimd_doc,\n        )\n\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(None)\n\n    def test_cimd_empty_proxy_allowlist_rejects_redirect_uri(self):\n        \"\"\"An explicit empty proxy allowlist should reject all CIMD redirect URIs.\"\"\"\n        cimd_doc = CIMDDocument(\n            client_id=AnyHttpUrl(\"https://example.com/client.json\"),\n            redirect_uris=[\"http://localhost:3000/callback\"],\n        )\n        client = ProxyDCRClient(\n            client_id=\"https://example.com/client.json\",\n            client_secret=None,\n            redirect_uris=None,\n            cimd_document=cimd_doc,\n            allowed_redirect_uri_patterns=[],\n        )\n\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://localhost:3000/callback\"))\n\n\nclass TestOAuthProxyRedirectValidation:\n    \"\"\"Test OAuth proxy with redirect URI validation.\"\"\"\n\n    def test_proxy_default_allows_all(self):\n        \"\"\"Test that OAuth proxy defaults to allowing all URIs for DCR compatibility.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # The proxy should store None for default (allow all)\n        assert proxy._allowed_client_redirect_uris is None\n\n    def test_proxy_custom_patterns(self):\n        \"\"\"Test OAuth proxy with custom redirect patterns.\"\"\"\n        custom_patterns = [\"http://localhost:*\", \"https://*.myapp.com/*\"]\n\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            allowed_client_redirect_uris=custom_patterns,\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        assert proxy._allowed_client_redirect_uris == custom_patterns\n\n    def test_proxy_empty_list_validation(self):\n        \"\"\"Test OAuth proxy with empty list (allow none).\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            allowed_client_redirect_uris=[],\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        assert proxy._allowed_client_redirect_uris == []\n\n    async def test_proxy_register_client_uses_patterns(self):\n        \"\"\"Test that registered clients use the configured patterns.\"\"\"\n        custom_patterns = [\"https://app.example.com/*\"]\n\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            allowed_client_redirect_uris=custom_patterns,\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Register a client\n        from mcp.shared.auth import OAuthClientInformationFull\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"new-client\",\n            client_secret=\"new-secret\",\n            redirect_uris=[AnyUrl(\"https://app.example.com/callback\")],\n        )\n\n        await proxy.register_client(client_info)\n\n        # Get the registered client\n        registered = await proxy.get_client(\n            \"new-client\"\n        )  # Use the client ID we registered\n        assert isinstance(registered, ProxyDCRClient)\n        assert registered.allowed_redirect_uri_patterns == custom_patterns\n\n    async def test_proxy_unregistered_client_returns_none(self):\n        \"\"\"Test that unregistered clients return None.\"\"\"\n        custom_patterns = [\"http://localhost:*\", \"http://127.0.0.1:*\"]\n\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            allowed_client_redirect_uris=custom_patterns,\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        # Get an unregistered client\n        client = await proxy.get_client(\"unknown-client\")\n        assert client is None\n\n\nclass TestOAuthProxyCIMDClient:\n    \"\"\"Test that CIMD clients obtained via proxy carry their document and apply dual validation.\"\"\"\n\n    @pytest.fixture\n    def mock_dns(self):\n        \"\"\"Mock DNS resolution to return test public IP.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[TEST_PUBLIC_IP],\n        ):\n            yield\n\n    async def test_proxy_get_client_returns_cimd_client(self, httpx_mock, mock_dns):\n        \"\"\"CIMD client obtained via proxy's get_client has cimd_document attached.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"CIMD App\",\n            \"redirect_uris\": [\"http://localhost:*/callback\"],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        client = await proxy.get_client(url)\n        assert isinstance(client, ProxyDCRClient)\n        assert client.cimd_document is not None\n        assert client.cimd_document.client_name == \"CIMD App\"\n        assert client.client_id == url\n\n    async def test_proxy_cimd_dual_redirect_validation(self, httpx_mock, mock_dns):\n        \"\"\"CIMD client from proxy enforces both CIMD redirect_uris and proxy patterns.\"\"\"\n        url = \"https://example.com/client.json\"\n        doc_data = {\n            \"client_id\": url,\n            \"client_name\": \"Dual Validation App\",\n            \"redirect_uris\": [\n                \"http://localhost:3000/callback\",\n                \"https://evil.com/callback\",\n            ],\n            \"token_endpoint_auth_method\": \"none\",\n        }\n        httpx_mock.add_response(\n            json=doc_data,\n            headers={\"content-length\": \"200\"},\n        )\n\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://auth.example.com/authorize\",\n            upstream_token_endpoint=\"https://auth.example.com/token\",\n            upstream_client_id=\"test-client\",\n            upstream_client_secret=\"test-secret\",\n            token_verifier=MockTokenVerifier(),\n            base_url=\"http://localhost:8000\",\n            allowed_client_redirect_uris=[\"http://localhost:*\"],\n            jwt_signing_key=\"test-secret\",\n            client_storage=MemoryStore(),\n        )\n\n        client = await proxy.get_client(url)\n        assert client is not None\n\n        # In CIMD AND matches proxy pattern → accepted\n        assert client.validate_redirect_uri(AnyUrl(\"http://localhost:3000/callback\"))\n\n        # In CIMD but NOT in proxy pattern → rejected\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"https://evil.com/callback\"))\n\n        # NOT in CIMD but matches proxy pattern → rejected\n        with pytest.raises(InvalidRedirectUriError):\n            client.validate_redirect_uri(AnyUrl(\"http://localhost:9999/other\"))\n"
  },
  {
    "path": "tests/server/auth/test_oauth_proxy_storage.py",
    "content": "\"\"\"Tests for OAuth proxy with persistent storage.\"\"\"\n\nimport tempfile\nimport warnings\nfrom collections.abc import AsyncGenerator\nfrom pathlib import Path\nfrom unittest.mock import AsyncMock, Mock\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom key_value.aio.protocols import AsyncKeyValue\nfrom key_value.aio.stores.filetree import FileTreeStore\nfrom key_value.aio.stores.memory import MemoryStore\nfrom mcp.shared.auth import OAuthClientInformationFull\nfrom pydantic import AnyUrl\n\nfrom fastmcp.server.auth.auth import TokenVerifier\nfrom fastmcp.server.auth.oauth_proxy import OAuthProxy\n\n\nclass TestOAuthProxyStorage:\n    \"\"\"Tests for OAuth proxy client storage functionality.\"\"\"\n\n    @pytest.fixture\n    def jwt_verifier(self):\n        \"\"\"Create a mock JWT verifier.\"\"\"\n        verifier = Mock()\n        verifier.required_scopes = [\"read\", \"write\"]\n        verifier.verify_token = AsyncMock(return_value=None)\n        return verifier\n\n    @pytest.fixture\n    async def temp_storage(self) -> AsyncGenerator[FileTreeStore, None]:\n        \"\"\"Create file-based storage for testing.\"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", UserWarning)\n                yield FileTreeStore(data_directory=Path(temp_dir))\n\n    @pytest.fixture\n    def memory_storage(self) -> MemoryStore:\n        \"\"\"Create in-memory storage for testing.\"\"\"\n        return MemoryStore()\n\n    def create_proxy(\n        self, jwt_verifier: TokenVerifier, storage: AsyncKeyValue | None = None\n    ) -> OAuthProxy:\n        \"\"\"Create an OAuth proxy with specified storage.\"\"\"\n        return OAuthProxy(\n            upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n            upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://myserver.com\",\n            redirect_path=\"/auth/callback\",\n            client_storage=storage,\n            jwt_signing_key=\"test-secret\",\n        )\n\n    async def test_register_and_get_client(self, jwt_verifier, temp_storage):\n        \"\"\"Test registering and retrieving a client.\"\"\"\n        proxy = self.create_proxy(jwt_verifier, storage=temp_storage)\n\n        # Register client\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-client-123\",\n            client_secret=\"secret-456\",\n            redirect_uris=[AnyUrl(\"http://localhost:8080/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            scope=\"read write\",\n        )\n        await proxy.register_client(client_info)\n\n        # Get client back\n        client = await proxy.get_client(\"test-client-123\")\n        assert client is not None\n        assert client.client_id == \"test-client-123\"\n        # Proxy uses token_endpoint_auth_method=\"none\", so client_secret is not stored\n        assert client.client_secret is None\n        assert client.scope == \"read write\"\n\n    async def test_client_persists_across_proxy_instances(\n        self, jwt_verifier: TokenVerifier, temp_storage: AsyncKeyValue\n    ):\n        \"\"\"Test that clients persist when proxy is recreated.\"\"\"\n        # First proxy registers client\n        proxy1 = self.create_proxy(jwt_verifier, storage=temp_storage)\n        client_info = OAuthClientInformationFull(\n            client_id=\"persistent-client\",\n            client_secret=\"persistent-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:9999/callback\")],\n            scope=\"openid profile\",\n        )\n        await proxy1.register_client(client_info)\n\n        # Second proxy can retrieve it\n        proxy2 = self.create_proxy(jwt_verifier, storage=temp_storage)\n        client = await proxy2.get_client(\"persistent-client\")\n        assert client is not None\n        # Proxy uses token_endpoint_auth_method=\"none\", so client_secret is not stored\n        assert client.client_secret is None\n        assert client.scope == \"openid profile\"\n\n    async def test_nonexistent_client_returns_none(\n        self, jwt_verifier: TokenVerifier, temp_storage: AsyncKeyValue\n    ):\n        \"\"\"Test that requesting non-existent client returns None.\"\"\"\n        proxy = self.create_proxy(jwt_verifier, storage=temp_storage)\n        client = await proxy.get_client(\"does-not-exist\")\n        assert client is None\n\n    async def test_proxy_dcr_client_redirect_validation(\n        self, jwt_verifier: TokenVerifier, temp_storage: AsyncKeyValue\n    ):\n        \"\"\"Test that OAuthProxyClient is created with redirect URI patterns.\"\"\"\n        proxy = OAuthProxy(\n            upstream_authorization_endpoint=\"https://github.com/login/oauth/authorize\",\n            upstream_token_endpoint=\"https://github.com/login/oauth/access_token\",\n            upstream_client_id=\"test-client-id\",\n            upstream_client_secret=\"test-client-secret\",\n            token_verifier=jwt_verifier,\n            base_url=\"https://myserver.com\",\n            allowed_client_redirect_uris=[\"http://localhost:*\"],\n            client_storage=temp_storage,\n            jwt_signing_key=\"test-secret\",\n        )\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"test-proxy-client\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:8080/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        # Get client back - should be OAuthProxyClient\n        client = await proxy.get_client(\"test-proxy-client\")\n        assert client is not None\n\n        # OAuthProxyClient should validate dynamic localhost ports\n        validated = client.validate_redirect_uri(\n            AnyUrl(\"http://localhost:12345/callback\")\n        )\n        assert validated is not None\n\n    async def test_in_memory_storage_option(self, jwt_verifier):\n        \"\"\"Test using in-memory storage explicitly.\"\"\"\n        storage = MemoryStore()\n        proxy = self.create_proxy(jwt_verifier, storage=storage)\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"memory-client\",\n            client_secret=\"memory-secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:8080/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        client = await proxy.get_client(\"memory-client\")\n        assert client is not None\n\n        # Create new proxy with same storage instance\n        proxy2 = self.create_proxy(jwt_verifier, storage=storage)\n        client2 = await proxy2.get_client(\"memory-client\")\n        assert client2 is not None\n\n        # But new storage instance won't have it\n        proxy3 = self.create_proxy(jwt_verifier, storage=MemoryStore())\n        client3 = await proxy3.get_client(\"memory-client\")\n        assert client3 is None\n\n    async def test_storage_data_structure(self, jwt_verifier, temp_storage):\n        \"\"\"Test that storage uses proper structured format.\"\"\"\n        proxy = self.create_proxy(jwt_verifier, storage=temp_storage)\n\n        client_info = OAuthClientInformationFull(\n            client_id=\"structured-client\",\n            client_secret=\"secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:8080/callback\")],\n        )\n        await proxy.register_client(client_info)\n\n        # Check raw storage data\n        raw_data = await temp_storage.get(\n            collection=\"mcp-oauth-proxy-clients\", key=\"structured-client\"\n        )\n        assert raw_data is not None\n        assert raw_data == snapshot(\n            {\n                \"redirect_uris\": [\"http://localhost:8080/callback\"],\n                \"token_endpoint_auth_method\": \"none\",\n                \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n                \"response_types\": [\"code\"],\n                \"scope\": \"read write\",\n                \"client_name\": None,\n                \"client_uri\": None,\n                \"logo_uri\": None,\n                \"contacts\": None,\n                \"tos_uri\": None,\n                \"policy_uri\": None,\n                \"jwks_uri\": None,\n                \"jwks\": None,\n                \"software_id\": None,\n                \"software_version\": None,\n                \"client_id\": \"structured-client\",\n                \"client_secret\": None,\n                \"client_id_issued_at\": None,\n                \"client_secret_expires_at\": None,\n                \"allowed_redirect_uri_patterns\": None,\n                \"cimd_document\": None,\n                \"cimd_fetched_at\": None,\n            }\n        )\n"
  },
  {
    "path": "tests/server/auth/test_oidc_proxy.py",
    "content": "\"\"\"Comprehensive tests for OIDC Proxy Provider functionality.\"\"\"\n\nimport json\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom httpx import Response\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.oidc_proxy import OIDCConfiguration, OIDCProxy\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nTEST_ISSUER = \"https://example.com\"\nTEST_AUTHORIZATION_ENDPOINT = \"https://example.com/authorize\"\nTEST_TOKEN_ENDPOINT = \"https://example.com/oauth/token\"\n\nTEST_CONFIG_URL = AnyHttpUrl(\"https://example.com/.well-known/openid-configuration\")\nTEST_CLIENT_ID = \"test-client-id\"\nTEST_CLIENT_SECRET = \"test-client-secret\"\nTEST_BASE_URL = AnyHttpUrl(\"https://example.com:8000/\")\n\n\n# =============================================================================\n# Test Fixtures\n# =============================================================================\n\n\n@pytest.fixture\ndef valid_oidc_configuration_dict():\n    \"\"\"Create a valid OIDC configuration dict for testing.\"\"\"\n    return {\n        \"issuer\": TEST_ISSUER,\n        \"authorization_endpoint\": TEST_AUTHORIZATION_ENDPOINT,\n        \"token_endpoint\": TEST_TOKEN_ENDPOINT,\n        \"jwks_uri\": \"https://example.com/.well-known/jwks.json\",\n        \"response_types_supported\": [\"code\"],\n        \"subject_types_supported\": [\"public\"],\n        \"id_token_signing_alg_values_supported\": [\"RS256\"],\n    }\n\n\n@pytest.fixture\ndef invalid_oidc_configuration_dict():\n    \"\"\"Create an invalid OIDC configuration dict for testing.\"\"\"\n    return {\n        \"issuer\": TEST_ISSUER,\n        \"authorization_endpoint\": TEST_AUTHORIZATION_ENDPOINT,\n        \"token_endpoint\": TEST_TOKEN_ENDPOINT,\n        \"jwks_uri\": \"https://example.com/.well-known/jwks.json\",\n    }\n\n\n@pytest.fixture\ndef valid_google_oidc_configuration_dict():\n    \"\"\"Create a valid Google OIDC configuration dict for testing.\n\n    See: https://accounts.google.com/.well-known/openid-configuration\n    \"\"\"\n    google_config_str = \"\"\"\n    {\n      \"issuer\": \"https://accounts.google.com\",\n      \"authorization_endpoint\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n      \"device_authorization_endpoint\": \"https://oauth2.googleapis.com/device/code\",\n      \"token_endpoint\": \"https://oauth2.googleapis.com/token\",\n      \"userinfo_endpoint\": \"https://openidconnect.googleapis.com/v1/userinfo\",\n      \"revocation_endpoint\": \"https://oauth2.googleapis.com/revoke\",\n      \"jwks_uri\": \"https://www.googleapis.com/oauth2/v3/certs\",\n      \"response_types_supported\": [\n        \"code\",\n        \"token\",\n        \"id_token\",\n        \"code token\",\n        \"code id_token\",\n        \"token id_token\",\n        \"code token id_token\",\n        \"none\"\n      ],\n      \"response_modes_supported\": [\n        \"query\",\n        \"fragment\",\n        \"form_post\"\n      ],\n      \"subject_types_supported\": [\n        \"public\"\n      ],\n      \"id_token_signing_alg_values_supported\": [\n        \"RS256\"\n      ],\n      \"scopes_supported\": [\n        \"openid\",\n        \"email\",\n        \"profile\"\n      ],\n      \"token_endpoint_auth_methods_supported\": [\n        \"client_secret_post\",\n        \"client_secret_basic\"\n      ],\n      \"claims_supported\": [\n        \"aud\",\n        \"email\",\n        \"email_verified\",\n        \"exp\",\n        \"family_name\",\n        \"given_name\",\n        \"iat\",\n        \"iss\",\n        \"name\",\n        \"picture\",\n        \"sub\"\n      ],\n      \"code_challenge_methods_supported\": [\n        \"plain\",\n        \"S256\"\n      ],\n      \"grant_types_supported\": [\n        \"authorization_code\",\n        \"refresh_token\",\n        \"urn:ietf:params:oauth:grant-type:device_code\",\n        \"urn:ietf:params:oauth:grant-type:jwt-bearer\"\n      ]\n    }\n    \"\"\"\n\n    return json.loads(google_config_str)\n\n\n@pytest.fixture\ndef valid_auth0_oidc_configuration_dict():\n    \"\"\"Create a valid Auth0 OIDC configuration dict for testing.\n\n    See: https://<tenant>.us.auth0.com/.well-known/openid-configuration\n    \"\"\"\n    auth0_config_str = \"\"\"\n    {\n      \"issuer\": \"https://example.us.auth0.com/\",\n      \"authorization_endpoint\": \"https://example.us.auth0.com/authorize\",\n      \"token_endpoint\": \"https://example.us.auth0.com/oauth/token\",\n      \"device_authorization_endpoint\": \"https://example.us.auth0.com/oauth/device/code\",\n      \"userinfo_endpoint\": \"https://example.us.auth0.com/userinfo\",\n      \"mfa_challenge_endpoint\": \"https://example.us.auth0.com/mfa/challenge\",\n      \"jwks_uri\": \"https://example.us.auth0.com/.well-known/jwks.json\",\n      \"registration_endpoint\": \"https://example.us.auth0.com/oidc/register\",\n      \"revocation_endpoint\": \"https://example.us.auth0.com/oauth/revoke\",\n      \"scopes_supported\": [\n        \"openid\",\n        \"profile\",\n        \"offline_access\",\n        \"name\",\n        \"given_name\",\n        \"family_name\",\n        \"nickname\",\n        \"email\",\n        \"email_verified\",\n        \"picture\",\n        \"created_at\",\n        \"identities\",\n        \"phone\",\n        \"address\"\n      ],\n      \"response_types_supported\": [\n        \"code\",\n        \"token\",\n        \"id_token\",\n        \"code token\",\n        \"code id_token\",\n        \"token id_token\",\n        \"code token id_token\"\n      ],\n      \"code_challenge_methods_supported\": [\n        \"S256\",\n        \"plain\"\n      ],\n      \"response_modes_supported\": [\n        \"query\",\n        \"fragment\",\n        \"form_post\"\n      ],\n      \"subject_types_supported\": [\n        \"public\"\n      ],\n      \"token_endpoint_auth_methods_supported\": [\n        \"client_secret_basic\",\n        \"client_secret_post\",\n        \"private_key_jwt\",\n        \"tls_client_auth\",\n        \"self_signed_tls_client_auth\"\n      ],\n      \"token_endpoint_auth_signing_alg_values_supported\": [\n        \"RS256\",\n        \"RS384\",\n        \"PS256\"\n      ],\n      \"claims_supported\": [\n        \"aud\",\n        \"auth_time\",\n        \"created_at\",\n        \"email\",\n        \"email_verified\",\n        \"exp\",\n        \"family_name\",\n        \"given_name\",\n        \"iat\",\n        \"identities\",\n        \"iss\",\n        \"name\",\n        \"nickname\",\n        \"phone_number\",\n        \"picture\",\n        \"sub\"\n      ],\n      \"request_uri_parameter_supported\": false,\n      \"request_parameter_supported\": true,\n      \"id_token_signing_alg_values_supported\": [\n        \"HS256\",\n        \"RS256\",\n        \"PS256\"\n      ],\n      \"tls_client_certificate_bound_access_tokens\": true,\n      \"request_object_signing_alg_values_supported\": [\n        \"RS256\",\n        \"RS384\",\n        \"PS256\"\n      ],\n      \"backchannel_logout_supported\": true,\n      \"backchannel_logout_session_supported\": true,\n      \"end_session_endpoint\": \"https://example.us.auth0.com/oidc/logout\",\n      \"backchannel_authentication_endpoint\": \"https://example.us.auth0.com/bc-authorize\",\n      \"backchannel_token_delivery_modes_supported\": [\n        \"poll\"\n      ],\n      \"global_token_revocation_endpoint\": \"https://example.us.auth0.com/oauth/global-token-revocation/connection/{connectionName}\",\n      \"global_token_revocation_endpoint_auth_methods_supported\": [\n        \"global-token-revocation+jwt\"\n      ]\n    }\n    \"\"\"\n\n    return json.loads(auth0_config_str)\n\n\n# =============================================================================\n# Test Classes\n# =============================================================================\n\n\ndef validate_config(config, source_dict):\n    \"\"\"Validate an OIDC configuration against the source dict.\"\"\"\n    for source_key, source_value in source_dict.items():\n        config_value = getattr(config, source_key, None)\n        if not hasattr(config, source_key):\n            continue\n\n        config_value = getattr(config, source_key, None)\n        if isinstance(config_value, AnyHttpUrl):\n            config_value = str(config_value)\n\n        assert config_value == source_value\n\n\nclass TestOIDCConfiguration:\n    \"\"\"Tests for OIDC configuration.\"\"\"\n\n    def test_default_configuration(self, valid_oidc_configuration_dict):\n        \"\"\"Test default configuration with valid dict.\"\"\"\n        config = OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n        validate_config(config, valid_oidc_configuration_dict)\n\n    def test_default_configuration_with_issuer_trailing_slash(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test default configuration with valid dict and issuer trailing slash.\"\"\"\n        valid_oidc_configuration_dict[\"issuer\"] += \"/\"\n        config = OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n        validate_config(config, valid_oidc_configuration_dict)\n\n    def test_explicit_strict_configuration(self, valid_oidc_configuration_dict):\n        \"\"\"Test default configuration with explicit True strict setting and valid dict.\"\"\"\n        valid_oidc_configuration_dict[\"strict\"] = True\n        config = OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n        validate_config(config, valid_oidc_configuration_dict)\n\n    def test_explicit_strict_configuration_with_issuer_trailing_slash(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test default configuration with explicit True strict setting, valid dict and issuer trailing slash.\"\"\"\n        valid_oidc_configuration_dict[\"issuer\"] += \"/\"\n        config = OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n        validate_config(config, valid_oidc_configuration_dict)\n\n    def test_default_configuration_raises_error(self, invalid_oidc_configuration_dict):\n        \"\"\"Test default configuration with invalid dict.\"\"\"\n        with pytest.raises(ValueError, match=\"Missing required configuration metadata\"):\n            OIDCConfiguration.model_validate(invalid_oidc_configuration_dict)\n\n    def test_explicit_strict_configuration_raises_error(\n        self, invalid_oidc_configuration_dict\n    ):\n        \"\"\"Test default configuration with explicit True strict setting and invalid dict.\"\"\"\n        invalid_oidc_configuration_dict[\"strict\"] = True\n        with pytest.raises(ValueError, match=\"Missing required configuration metadata\"):\n            OIDCConfiguration.model_validate(invalid_oidc_configuration_dict)\n\n    def test_bad_url_raises_error(self, valid_oidc_configuration_dict):\n        \"\"\"Test default configuration with bad URL setting.\"\"\"\n        valid_oidc_configuration_dict[\"issuer\"] = \"not-a-URL\"\n        with pytest.raises(ValueError, match=\"Invalid URL for configuration metadata\"):\n            OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n\n    def test_explicit_strict_with_bad_url_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test default configuration with explicit True strict setting and bad URL setting.\"\"\"\n        valid_oidc_configuration_dict[\"strict\"] = True\n        valid_oidc_configuration_dict[\"issuer\"] = \"not-a-URL\"\n        with pytest.raises(ValueError, match=\"Invalid URL for configuration metadata\"):\n            OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n\n    def test_not_strict_configuration(self):\n        \"\"\"Test default configuration with explicit False strict setting.\"\"\"\n        config = OIDCConfiguration.model_validate({\"strict\": False})\n\n        assert config.issuer is None\n        assert config.authorization_endpoint is None\n        assert config.token_endpoint is None\n        assert config.jwks_uri is None\n        assert config.response_types_supported is None\n        assert config.subject_types_supported is None\n        assert config.id_token_signing_alg_values_supported is None\n\n    def test_not_strict_configuration_with_invalid_config(\n        self, invalid_oidc_configuration_dict\n    ):\n        \"\"\"Test default configuration with explicit False strict setting.\"\"\"\n        invalid_oidc_configuration_dict[\"strict\"] = False\n        config = OIDCConfiguration.model_validate(invalid_oidc_configuration_dict)\n\n        validate_config(config, invalid_oidc_configuration_dict)\n\n    def test_not_strict_configuration_with_bad_url(self, valid_oidc_configuration_dict):\n        \"\"\"Test default configuration with explicit False strict setting.\"\"\"\n        valid_oidc_configuration_dict[\"strict\"] = False\n        valid_oidc_configuration_dict[\"issuer\"] = \"not-a-url\"\n        config = OIDCConfiguration.model_validate(valid_oidc_configuration_dict)\n\n        validate_config(config, valid_oidc_configuration_dict)\n\n    def test_google_configuration(self, valid_google_oidc_configuration_dict):\n        \"\"\"Test Google configuration.\"\"\"\n        config = OIDCConfiguration.model_validate(valid_google_oidc_configuration_dict)\n\n        validate_config(config, valid_google_oidc_configuration_dict)\n\n    def test_auth0_configuration(self, valid_auth0_oidc_configuration_dict):\n        \"\"\"Test Auth0 configuration.\"\"\"\n        config = OIDCConfiguration.model_validate(valid_auth0_oidc_configuration_dict)\n\n        validate_config(config, valid_auth0_oidc_configuration_dict)\n\n\ndef validate_get_oidc_configuration(oidc_configuration, strict, timeout_seconds):\n    \"\"\"Validate get_oidc_configuration call.\"\"\"\n    with patch(\"httpx.get\") as mock_get:\n        mock_response = MagicMock(spec=Response)\n        mock_response.json.return_value = oidc_configuration\n        mock_get.return_value = mock_response\n\n        config = OIDCConfiguration.get_oidc_configuration(\n            config_url=TEST_CONFIG_URL,\n            strict=strict,\n            timeout_seconds=timeout_seconds,\n        )\n\n        validate_config(config, oidc_configuration)\n\n        mock_get.assert_called_once()\n\n        call_args = mock_get.call_args\n        assert str(call_args[0][0]) == str(TEST_CONFIG_URL)\n\n        return call_args\n\n\nclass TestGetOIDCConfiguration:\n    \"\"\"Tests for getting OIDC configuration.\"\"\"\n\n    def test_get_oidc_configuration(self, valid_oidc_configuration_dict):\n        \"\"\"Test with valid response and explicit timeout.\"\"\"\n        call_args = validate_get_oidc_configuration(\n            valid_oidc_configuration_dict, True, 10\n        )\n        assert call_args[1][\"timeout\"] == 10\n\n    def test_get_oidc_configuration_no_timeout(self, valid_oidc_configuration_dict):\n        \"\"\"Test with valid response and no timeout.\"\"\"\n        call_args = validate_get_oidc_configuration(\n            valid_oidc_configuration_dict, True, None\n        )\n        assert \"timeout\" not in call_args[1]\n\n    def test_get_oidc_configuration_raises_error(\n        self, invalid_oidc_configuration_dict\n    ) -> None:\n        \"\"\"Test with invalid response.\"\"\"\n        with pytest.raises(ValueError, match=\"Missing required configuration metadata\"):\n            validate_get_oidc_configuration(invalid_oidc_configuration_dict, True, 10)\n\n    def test_get_oidc_configuration_not_strict(\n        self, invalid_oidc_configuration_dict\n    ) -> None:\n        \"\"\"Test with invalid response and strict set to False.\"\"\"\n        with patch(\"httpx.get\") as mock_get:\n            mock_response = MagicMock(spec=Response)\n            mock_response.json.return_value = invalid_oidc_configuration_dict\n            mock_get.return_value = mock_response\n\n            OIDCConfiguration.get_oidc_configuration(\n                config_url=TEST_CONFIG_URL,\n                strict=False,\n                timeout_seconds=10,\n            )\n\n            mock_get.assert_called_once()\n\n            call_args = mock_get.call_args\n            assert str(call_args[0][0]) == str(TEST_CONFIG_URL)\n\n\ndef validate_proxy(mock_get, proxy, oidc_config):\n    \"\"\"Validate OIDC proxy.\"\"\"\n    mock_get.assert_called_once()\n\n    call_args = mock_get.call_args\n    assert str(call_args[0][0]) == str(TEST_CONFIG_URL)\n\n    assert proxy._upstream_authorization_endpoint == TEST_AUTHORIZATION_ENDPOINT\n    assert proxy._upstream_token_endpoint == TEST_TOKEN_ENDPOINT\n    assert proxy._upstream_client_id == TEST_CLIENT_ID\n    assert proxy._upstream_client_secret is not None\n    assert proxy._upstream_client_secret.get_secret_value() == TEST_CLIENT_SECRET\n    assert str(proxy.base_url) == str(TEST_BASE_URL)\n    assert proxy.oidc_config == oidc_config\n\n\nclass TestOIDCProxyInitialization:\n    \"\"\"Tests for OIDC proxy initialization.\"\"\"\n\n    def test_default_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test default initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n    def test_timeout_seconds_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test timeout seconds initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                timeout_seconds=12,\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            call_args = mock_get.call_args\n            assert call_args[1][\"timeout_seconds\"] == 12\n\n    def test_token_verifier_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test token verifier initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                algorithm=\"RS256\",\n                audience=\"oidc-proxy-test-audience\",\n                required_scopes=[\"required\", \"scopes\"],\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            assert isinstance(proxy._token_validator, JWTVerifier)\n\n            assert proxy._token_validator.algorithm == \"RS256\"\n            assert proxy._token_validator.audience == \"oidc-proxy-test-audience\"\n            assert proxy._token_validator.required_scopes == [\"required\", \"scopes\"]\n\n    def test_extra_parameters_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test other parameters initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                audience=\"oidc-proxy-test-audience\",\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            assert proxy._extra_authorize_params == {\n                \"audience\": \"oidc-proxy-test-audience\"\n            }\n            assert proxy._extra_token_params == {\"audience\": \"oidc-proxy-test-audience\"}\n\n    def test_other_parameters_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test other parameters initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                redirect_path=\"/oidc/proxy\",\n                allowed_client_redirect_uris=[\"http://localhost:*\"],\n                token_endpoint_auth_method=\"client_secret_post\",\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            assert proxy._redirect_path == \"/oidc/proxy\"\n            assert proxy._allowed_client_redirect_uris == [\"http://localhost:*\"]\n            assert proxy._token_endpoint_auth_method == \"client_secret_post\"\n\n    def test_no_config_url_initialization_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test no config URL initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            with pytest.raises(ValueError, match=\"Missing required config URL\"):\n                OIDCProxy(\n                    config_url=None,  # type: ignore\n                    client_id=TEST_CLIENT_ID,\n                    client_secret=TEST_CLIENT_SECRET,\n                    base_url=TEST_BASE_URL,\n                    jwt_signing_key=\"test-secret\",\n                )\n\n    def test_no_client_id_initialization_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test no client id initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            with pytest.raises(ValueError, match=\"Missing required client id\"):\n                OIDCProxy(\n                    config_url=TEST_CONFIG_URL,\n                    client_id=None,  # type: ignore\n                    client_secret=TEST_CLIENT_SECRET,\n                    base_url=TEST_BASE_URL,\n                )\n\n    def test_no_client_secret_initialization_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test no client secret initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            with pytest.raises(\n                ValueError,\n                match=\"Either client_secret or jwt_signing_key must be provided\",\n            ):\n                OIDCProxy(\n                    config_url=TEST_CONFIG_URL,\n                    client_id=TEST_CLIENT_ID,\n                    client_secret=None,\n                    base_url=TEST_BASE_URL,\n                )\n\n    def test_no_base_url_initialization_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test no base URL initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            with pytest.raises(ValueError, match=\"Missing required base URL\"):\n                OIDCProxy(\n                    config_url=TEST_CONFIG_URL,\n                    client_id=TEST_CLIENT_ID,\n                    client_secret=TEST_CLIENT_SECRET,\n                    base_url=None,  # type: ignore\n                )\n\n    def test_custom_token_verifier_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test initialization with custom token verifier.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            # Create custom verifier for opaque tokens\n            custom_verifier = IntrospectionTokenVerifier(\n                introspection_url=\"https://example.com/oauth/introspect\",\n                client_id=\"introspection-client\",\n                client_secret=\"introspection-secret\",\n                required_scopes=[\"custom\", \"scopes\"],\n            )\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                token_verifier=custom_verifier,\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            # Verify the custom verifier is used\n            assert proxy._token_validator is custom_verifier\n            assert isinstance(proxy._token_validator, IntrospectionTokenVerifier)\n\n            # Verify required_scopes are properly loaded from the custom verifier\n            assert proxy.required_scopes == [\"custom\", \"scopes\"]\n\n    def test_custom_token_verifier_with_algorithm_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test that providing algorithm with custom verifier raises error.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            custom_verifier = IntrospectionTokenVerifier(\n                introspection_url=\"https://example.com/oauth/introspect\",\n                client_id=\"introspection-client\",\n                client_secret=\"introspection-secret\",\n            )\n\n            with pytest.raises(\n                ValueError,\n                match=\"Cannot specify 'algorithm' when providing a custom token_verifier\",\n            ):\n                OIDCProxy(\n                    config_url=TEST_CONFIG_URL,\n                    client_id=TEST_CLIENT_ID,\n                    client_secret=TEST_CLIENT_SECRET,\n                    base_url=TEST_BASE_URL,\n                    token_verifier=custom_verifier,\n                    algorithm=\"RS256\",  # This should cause an error\n                    jwt_signing_key=\"test-secret\",\n                )\n\n    def test_custom_token_verifier_with_required_scopes_raises_error(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test that providing required_scopes with custom verifier raises error.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            custom_verifier = IntrospectionTokenVerifier(\n                introspection_url=\"https://example.com/oauth/introspect\",\n                client_id=\"introspection-client\",\n                client_secret=\"introspection-secret\",\n            )\n\n            with pytest.raises(\n                ValueError,\n                match=\"Cannot specify 'required_scopes' when providing a custom token_verifier\",\n            ):\n                OIDCProxy(\n                    config_url=TEST_CONFIG_URL,\n                    client_id=TEST_CLIENT_ID,\n                    client_secret=TEST_CLIENT_SECRET,\n                    base_url=TEST_BASE_URL,\n                    token_verifier=custom_verifier,\n                    required_scopes=[\"read\", \"write\"],  # This should cause an error\n                    jwt_signing_key=\"test-secret\",\n                )\n\n    def test_custom_token_verifier_with_audience_allowed(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Test that providing audience with custom verifier is allowed (for OAuth flow).\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            custom_verifier = IntrospectionTokenVerifier(\n                introspection_url=\"https://example.com/oauth/introspect\",\n                client_id=\"introspection-client\",\n                client_secret=\"introspection-secret\",\n            )\n\n            # This should NOT raise an error - audience is for OAuth flow\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                token_verifier=custom_verifier,\n                audience=\"test-audience\",  # Should be allowed for OAuth flow\n                jwt_signing_key=\"test-secret\",\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n            assert proxy._extra_authorize_params == {\"audience\": \"test-audience\"}\n            assert proxy._extra_token_params == {\"audience\": \"test-audience\"}\n\n    def test_extra_authorize_params_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test extra authorize params initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                extra_authorize_params={\n                    \"prompt\": \"consent\",\n                    \"access_type\": \"offline\",\n                },\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            assert proxy._extra_authorize_params == {\n                \"prompt\": \"consent\",\n                \"access_type\": \"offline\",\n            }\n            # Token params should be empty since we didn't set them\n            assert proxy._extra_token_params == {}\n\n    def test_extra_token_params_initialization(self, valid_oidc_configuration_dict):\n        \"\"\"Test extra token params initialization.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                extra_token_params={\"custom_param\": \"custom_value\"},\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            # Authorize params should be empty since we didn't set them\n            assert proxy._extra_authorize_params == {}\n            assert proxy._extra_token_params == {\"custom_param\": \"custom_value\"}\n\n    def test_extra_params_merge_with_audience(self, valid_oidc_configuration_dict):\n        \"\"\"Test that extra params merge with audience, with user params taking precedence.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                audience=\"original-audience\",\n                jwt_signing_key=\"test-secret\",\n                extra_authorize_params={\n                    \"prompt\": \"consent\",\n                    \"audience\": \"overridden-audience\",  # Should override the audience param\n                },\n                extra_token_params={\"custom\": \"value\"},\n            )\n\n            validate_proxy(mock_get, proxy, oidc_config)\n\n            # User's extra_authorize_params should override audience\n            assert proxy._extra_authorize_params == {\n                \"audience\": \"overridden-audience\",\n                \"prompt\": \"consent\",\n            }\n            # Token params should have both audience (from audience param) and custom\n            assert proxy._extra_token_params == {\n                \"audience\": \"original-audience\",\n                \"custom\": \"value\",\n            }\n"
  },
  {
    "path": "tests/server/auth/test_oidc_proxy_token.py",
    "content": "\"\"\"Tests for OIDC Proxy verify_id_token functionality.\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp.server.auth.oauth_proxy.models import UpstreamTokenSet\nfrom fastmcp.server.auth.oidc_proxy import OIDCConfiguration, OIDCProxy\nfrom fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier\n\nTEST_ISSUER = \"https://example.com\"\nTEST_AUTHORIZATION_ENDPOINT = \"https://example.com/authorize\"\nTEST_TOKEN_ENDPOINT = \"https://example.com/oauth/token\"\n\nTEST_CONFIG_URL = AnyHttpUrl(\"https://example.com/.well-known/openid-configuration\")\nTEST_CLIENT_ID = \"test-client-id\"\nTEST_CLIENT_SECRET = \"test-client-secret\"\nTEST_BASE_URL = AnyHttpUrl(\"https://example.com:8000/\")\n\n\n# =============================================================================\n# Shared Fixtures\n# =============================================================================\n\n\n@pytest.fixture\ndef valid_oidc_configuration_dict():\n    \"\"\"Create a valid OIDC configuration dict for testing.\"\"\"\n    return {\n        \"issuer\": TEST_ISSUER,\n        \"authorization_endpoint\": TEST_AUTHORIZATION_ENDPOINT,\n        \"token_endpoint\": TEST_TOKEN_ENDPOINT,\n        \"jwks_uri\": \"https://example.com/.well-known/jwks.json\",\n        \"response_types_supported\": [\"code\"],\n        \"subject_types_supported\": [\"public\"],\n        \"id_token_signing_alg_values_supported\": [\"RS256\"],\n    }\n\n\n# =============================================================================\n# Test Helpers\n# =============================================================================\n\n\ndef _make_upstream_token_set(*, id_token: str | None = None) -> UpstreamTokenSet:\n    \"\"\"Create an UpstreamTokenSet with optional id_token.\"\"\"\n    raw_token_data: dict[str, str] = {\"access_token\": \"opaque-access-token\"}\n    if id_token is not None:\n        raw_token_data[\"id_token\"] = id_token\n    return UpstreamTokenSet(\n        upstream_token_id=\"test-id\",\n        access_token=\"opaque-access-token\",\n        refresh_token=None,\n        refresh_token_expires_at=None,\n        expires_at=9999999999.0,\n        token_type=\"Bearer\",\n        scope=\"openid\",\n        client_id=\"test-client\",\n        created_at=1000000000.0,\n        raw_token_data=raw_token_data,\n    )\n\n\n# =============================================================================\n# Test Classes\n# =============================================================================\n\n\nclass TestVerifyIdToken:\n    \"\"\"Tests for verify_id_token functionality.\"\"\"\n\n    def test_verify_id_token_disabled_by_default(self, valid_oidc_configuration_dict):\n        \"\"\"Default behavior: verify the access_token.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n            )\n            token_set = _make_upstream_token_set(id_token=\"jwt-id-token\")\n\n            assert proxy._get_verification_token(token_set) == \"opaque-access-token\"\n\n    def test_verify_id_token_returns_id_token(self, valid_oidc_configuration_dict):\n        \"\"\"When enabled, verify the id_token instead of access_token.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n            token_set = _make_upstream_token_set(id_token=\"jwt-id-token\")\n\n            assert proxy._get_verification_token(token_set) == \"jwt-id-token\"\n\n    def test_verify_id_token_returns_none_when_missing(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"When enabled but id_token is absent, return None.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n            token_set = _make_upstream_token_set(id_token=None)\n\n            assert proxy._get_verification_token(token_set) is None\n\n    def test_verify_id_token_works_with_custom_verifier(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"verify_id_token can be combined with a custom token_verifier.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            custom_verifier = IntrospectionTokenVerifier(\n                introspection_url=\"https://example.com/oauth/introspect\",\n                client_id=\"introspection-client\",\n                client_secret=\"introspection-secret\",\n            )\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n                token_verifier=custom_verifier,\n            )\n            token_set = _make_upstream_token_set(id_token=\"jwt-id-token\")\n\n            assert proxy._get_verification_token(token_set) == \"jwt-id-token\"\n            assert proxy._token_validator is custom_verifier\n\n    def test_verify_id_token_survives_refresh_without_id_token(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"id_token from original auth is preserved when refresh omits it.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n\n            token_set = _make_upstream_token_set(id_token=\"original-id-token\")\n\n            # Simulate a refresh response that omits id_token —\n            # the merge in exchange_refresh_token should preserve it\n            refresh_response = {\n                \"access_token\": \"new-access-token\",\n                \"token_type\": \"Bearer\",\n            }\n            token_set.raw_token_data = {**token_set.raw_token_data, **refresh_response}\n            token_set.access_token = \"new-access-token\"\n\n            assert proxy._get_verification_token(token_set) == \"original-id-token\"\n\n    def test_verify_id_token_updated_when_refresh_includes_it(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"id_token is updated when refresh response includes a new one.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n\n            token_set = _make_upstream_token_set(id_token=\"original-id-token\")\n\n            # Simulate a refresh response that includes a new id_token\n            refresh_response = {\n                \"access_token\": \"new-access-token\",\n                \"id_token\": \"refreshed-id-token\",\n            }\n            token_set.raw_token_data = {**token_set.raw_token_data, **refresh_response}\n            token_set.access_token = \"new-access-token\"\n\n            assert proxy._get_verification_token(token_set) == \"refreshed-id-token\"\n\n    def test_verify_id_token_uses_client_id_as_verifier_audience(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"When verify_id_token is enabled, the verifier audience should be\n        client_id (matching id_token.aud), not the API audience parameter.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                audience=\"https://api.example.com\",\n                verify_id_token=True,\n            )\n\n            assert isinstance(proxy._token_validator, JWTVerifier)\n            assert proxy._token_validator.audience == TEST_CLIENT_ID\n\n            # The API audience should still be sent upstream\n            assert (\n                proxy._extra_authorize_params[\"audience\"] == \"https://api.example.com\"\n            )\n            assert proxy._extra_token_params[\"audience\"] == \"https://api.example.com\"\n\n    def test_verify_id_token_without_audience_uses_client_id(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"When verify_id_token is enabled without an audience param,\n        the verifier audience should still be client_id.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n\n            assert isinstance(proxy._token_validator, JWTVerifier)\n            assert proxy._token_validator.audience == TEST_CLIENT_ID\n\n    def test_verify_id_token_does_not_enforce_scopes_on_verifier(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"When verify_id_token is enabled, required_scopes should not be\n        passed to the JWTVerifier since id_tokens don't carry scope claims.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                required_scopes=[\"read\", \"write\"],\n                verify_id_token=True,\n            )\n\n            assert isinstance(proxy._token_validator, JWTVerifier)\n            assert proxy._token_validator.required_scopes == []\n\n            # Scopes should still be advertised via the proxy's required_scopes\n            assert proxy.required_scopes == [\"read\", \"write\"]\n\n            # Derived scope state should also be recomputed\n            assert proxy._default_scope_str == \"read write\"\n            assert proxy.client_registration_options is not None\n            assert proxy.client_registration_options.valid_scopes == [\n                \"read\",\n                \"write\",\n            ]\n\n\nclass TestUsesAlternateVerification:\n    \"\"\"Tests for _uses_alternate_verification intent-based flag.\"\"\"\n\n    def test_disabled_by_default(self, valid_oidc_configuration_dict):\n        \"\"\"OIDCProxy without verify_id_token returns False.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n            )\n\n            assert proxy._uses_alternate_verification() is False\n\n    def test_enabled_with_verify_id_token(self, valid_oidc_configuration_dict):\n        \"\"\"OIDCProxy with verify_id_token=True returns True.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n\n            assert proxy._uses_alternate_verification() is True\n\n    def test_scope_patch_applied_when_tokens_identical(\n        self, valid_oidc_configuration_dict\n    ):\n        \"\"\"Regression test: scopes must be patched even when id_token and\n        access_token carry the same JWT value (fixes #3461).\"\"\"\n        with patch(\n            \"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration\"\n        ) as mock_get:\n            oidc_config = OIDCConfiguration.model_validate(\n                valid_oidc_configuration_dict\n            )\n            mock_get.return_value = oidc_config\n\n            proxy = OIDCProxy(\n                config_url=TEST_CONFIG_URL,\n                client_id=TEST_CLIENT_ID,\n                client_secret=TEST_CLIENT_SECRET,\n                base_url=TEST_BASE_URL,\n                jwt_signing_key=\"test-secret\",\n                verify_id_token=True,\n            )\n\n            # Same JWT for both access_token and id_token — the scenario\n            # that triggered the bug.\n            same_jwt = \"eyJhbGciOiJSUzI1NiJ9.identical-token\"\n            token_set = UpstreamTokenSet(\n                upstream_token_id=\"test-id\",\n                access_token=same_jwt,\n                refresh_token=None,\n                refresh_token_expires_at=None,\n                expires_at=9999999999.0,\n                token_type=\"Bearer\",\n                scope=\"openid offline_access\",\n                client_id=\"test-client\",\n                created_at=1000000000.0,\n                raw_token_data={\n                    \"access_token\": same_jwt,\n                    \"id_token\": same_jwt,\n                },\n            )\n\n            # _uses_alternate_verification should be True regardless of\n            # token value equality\n            assert proxy._uses_alternate_verification() is True\n            # _get_verification_token returns the id_token (same value)\n            assert proxy._get_verification_token(token_set) == same_jwt\n            # The key point: even though the tokens are equal, the intent\n            # flag ensures load_access_token will patch scopes\n"
  },
  {
    "path": "tests/server/auth/test_redirect_validation.py",
    "content": "\"\"\"Tests for redirect URI validation in OAuth flows.\"\"\"\n\nfrom pydantic import AnyUrl\n\nfrom fastmcp.server.auth.redirect_validation import (\n    DEFAULT_LOCALHOST_PATTERNS,\n    matches_allowed_pattern,\n    validate_redirect_uri,\n)\n\n\nclass TestMatchesAllowedPattern:\n    \"\"\"Test wildcard pattern matching for redirect URIs.\"\"\"\n\n    def test_exact_match(self):\n        \"\"\"Test exact URI matching without wildcards.\"\"\"\n        assert matches_allowed_pattern(\n            \"http://localhost:3000/callback\", \"http://localhost:3000/callback\"\n        )\n        assert not matches_allowed_pattern(\n            \"http://localhost:3000/callback\", \"http://localhost:3001/callback\"\n        )\n\n    def test_port_wildcard(self):\n        \"\"\"Test wildcard matching for ports.\"\"\"\n        pattern = \"http://localhost:*/callback\"\n        assert matches_allowed_pattern(\"http://localhost:3000/callback\", pattern)\n        assert matches_allowed_pattern(\"http://localhost:54321/callback\", pattern)\n        assert not matches_allowed_pattern(\"http://example.com:3000/callback\", pattern)\n\n    def test_path_wildcard(self):\n        \"\"\"Test wildcard matching for paths.\"\"\"\n        pattern = \"http://localhost:3000/*\"\n        assert matches_allowed_pattern(\"http://localhost:3000/callback\", pattern)\n        assert matches_allowed_pattern(\"http://localhost:3000/auth/callback\", pattern)\n        assert not matches_allowed_pattern(\"http://localhost:3001/callback\", pattern)\n\n    def test_subdomain_wildcard(self):\n        \"\"\"Test wildcard matching for subdomains.\"\"\"\n        pattern = \"https://*.example.com/callback\"\n        assert matches_allowed_pattern(\"https://app.example.com/callback\", pattern)\n        assert matches_allowed_pattern(\"https://api.example.com/callback\", pattern)\n        assert not matches_allowed_pattern(\"https://example.com/callback\", pattern)\n        assert not matches_allowed_pattern(\"http://app.example.com/callback\", pattern)\n\n    def test_multiple_wildcards(self):\n        \"\"\"Test patterns with multiple wildcards.\"\"\"\n        pattern = \"https://*.example.com:*/auth/*\"\n        assert matches_allowed_pattern(\n            \"https://app.example.com:8080/auth/callback\", pattern\n        )\n        assert matches_allowed_pattern(\n            \"https://api.example.com:3000/auth/redirect\", pattern\n        )\n        assert not matches_allowed_pattern(\n            \"http://app.example.com:8080/auth/callback\", pattern\n        )\n\n\nclass TestValidateRedirectUri:\n    \"\"\"Test redirect URI validation with pattern lists.\"\"\"\n\n    def test_none_redirect_uri_allowed(self):\n        \"\"\"Test that None redirect URI is always allowed.\"\"\"\n        assert validate_redirect_uri(None, None)\n        assert validate_redirect_uri(None, [])\n        assert validate_redirect_uri(None, [\"http://localhost:*\"])\n\n    def test_default_allows_all(self):\n        \"\"\"Test that None (default) allows all URIs for DCR compatibility.\"\"\"\n        # All URIs should be allowed when None is provided (DCR compatibility)\n        assert validate_redirect_uri(\"http://localhost:3000\", None)\n        assert validate_redirect_uri(\"http://127.0.0.1:8080\", None)\n        assert validate_redirect_uri(\"http://example.com\", None)\n        assert validate_redirect_uri(\"https://app.example.com\", None)\n        assert validate_redirect_uri(\"https://claude.ai/api/mcp/auth_callback\", None)\n\n    def test_empty_list_allows_none(self):\n        \"\"\"Test that empty list allows no redirect URIs.\"\"\"\n        assert not validate_redirect_uri(\"http://localhost:3000\", [])\n        assert not validate_redirect_uri(\"http://example.com\", [])\n        assert not validate_redirect_uri(\"https://anywhere.com:9999/path\", [])\n\n    def test_custom_patterns(self):\n        \"\"\"Test validation with custom pattern list.\"\"\"\n        patterns = [\n            \"http://localhost:*\",\n            \"https://app.example.com/*\",\n            \"https://*.trusted.io/*\",\n        ]\n\n        # Allowed URIs\n        assert validate_redirect_uri(\"http://localhost:3000\", patterns)\n        assert validate_redirect_uri(\"https://app.example.com/callback\", patterns)\n        assert validate_redirect_uri(\"https://api.trusted.io/auth\", patterns)\n\n        # Rejected URIs\n        assert not validate_redirect_uri(\"http://127.0.0.1:3000\", patterns)\n        assert not validate_redirect_uri(\"https://other.example.com/callback\", patterns)\n        assert not validate_redirect_uri(\"http://app.example.com/callback\", patterns)\n\n    def test_anyurl_conversion(self):\n        \"\"\"Test that AnyUrl objects are properly converted to strings.\"\"\"\n        patterns = [\"http://localhost:*\"]\n        uri = AnyUrl(\"http://localhost:3000/callback\")\n        assert validate_redirect_uri(uri, patterns)\n\n        uri = AnyUrl(\"http://example.com/callback\")\n        assert not validate_redirect_uri(uri, patterns)\n\n\nclass TestSecurityBypass:\n    \"\"\"Test protection against redirect URI security bypass attacks.\"\"\"\n\n    def test_userinfo_bypass_blocked(self):\n        \"\"\"Test that userinfo-style bypasses are blocked.\n\n        Attack: http://localhost@evil.com/callback would match http://localhost:*\n        with naive string matching, but actually points to evil.com.\n        \"\"\"\n        pattern = \"http://localhost:*\"\n\n        # These should be blocked - the \"host\" is actually in the userinfo\n        assert not matches_allowed_pattern(\n            \"http://localhost@evil.com/callback\", pattern\n        )\n        assert not matches_allowed_pattern(\n            \"http://localhost:3000@malicious.io/callback\", pattern\n        )\n        assert not matches_allowed_pattern(\n            \"http://user:pass@localhost:3000/callback\", pattern\n        )\n\n    def test_userinfo_bypass_with_subdomain_pattern(self):\n        \"\"\"Test userinfo bypass with subdomain wildcard patterns.\"\"\"\n        pattern = \"https://*.example.com/callback\"\n\n        # Blocked: userinfo tricks\n        assert not matches_allowed_pattern(\n            \"https://app.example.com@attacker.com/callback\", pattern\n        )\n        assert not matches_allowed_pattern(\n            \"https://user:pass@app.example.com/callback\", pattern\n        )\n\n    def test_legitimate_uris_still_work(self):\n        \"\"\"Test that legitimate URIs work after security hardening.\"\"\"\n        pattern = \"http://localhost:*\"\n        assert matches_allowed_pattern(\"http://localhost:3000/callback\", pattern)\n        assert matches_allowed_pattern(\"http://localhost:8080/auth\", pattern)\n\n        pattern = \"https://*.example.com/callback\"\n        assert matches_allowed_pattern(\"https://app.example.com/callback\", pattern)\n\n    def test_scheme_mismatch_blocked(self):\n        \"\"\"Test that scheme mismatches are blocked.\"\"\"\n        assert not matches_allowed_pattern(\n            \"http://localhost:3000/callback\", \"https://localhost:*\"\n        )\n        assert not matches_allowed_pattern(\n            \"https://localhost:3000/callback\", \"http://localhost:*\"\n        )\n\n    def test_host_mismatch_blocked(self):\n        \"\"\"Test that host mismatches are blocked even with wildcards.\"\"\"\n        pattern = \"http://localhost:*\"\n        assert not matches_allowed_pattern(\"http://127.0.0.1:3000/callback\", pattern)\n        assert not matches_allowed_pattern(\"http://example.com:3000/callback\", pattern)\n\n\nclass TestDefaultPatterns:\n    \"\"\"Test the default localhost patterns constant.\"\"\"\n\n    def test_default_patterns_exist(self):\n        \"\"\"Test that default patterns are defined.\"\"\"\n        assert DEFAULT_LOCALHOST_PATTERNS is not None\n        assert len(DEFAULT_LOCALHOST_PATTERNS) > 0\n\n    def test_default_patterns_include_localhost(self):\n        \"\"\"Test that default patterns include localhost variations.\"\"\"\n        assert \"http://localhost:*\" in DEFAULT_LOCALHOST_PATTERNS\n        assert \"http://127.0.0.1:*\" in DEFAULT_LOCALHOST_PATTERNS\n\n    def test_explicit_localhost_patterns(self):\n        \"\"\"Test that explicitly passing DEFAULT_LOCALHOST_PATTERNS restricts to localhost.\"\"\"\n        # Localhost patterns should be allowed\n        assert validate_redirect_uri(\n            \"http://localhost:3000\", DEFAULT_LOCALHOST_PATTERNS\n        )\n        assert validate_redirect_uri(\n            \"http://127.0.0.1:8080\", DEFAULT_LOCALHOST_PATTERNS\n        )\n\n        # Non-localhost should be rejected\n        assert not validate_redirect_uri(\n            \"http://example.com\", DEFAULT_LOCALHOST_PATTERNS\n        )\n        assert not validate_redirect_uri(\n            \"https://claude.ai/api/mcp/auth_callback\", DEFAULT_LOCALHOST_PATTERNS\n        )\n"
  },
  {
    "path": "tests/server/auth/test_remote_auth_provider.py",
    "content": "import httpx\nimport pytest\nfrom pydantic import AnyHttpUrl\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import RemoteAuthProvider\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n\n@pytest.fixture\ndef test_tokens():\n    \"\"\"Standard test tokens fixture for all auth tests.\"\"\"\n    return {\n        \"test_token\": {\n            \"client_id\": \"test-client\",\n            \"scopes\": [\"read\", \"write\"],\n        }\n    }\n\n\nclass TestRemoteAuthProvider:\n    \"\"\"Test suite for RemoteAuthProvider.\"\"\"\n\n    def test_init(self, test_tokens):\n        \"\"\"Test RemoteAuthProvider initialization.\"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_servers = [AnyHttpUrl(\"https://auth.example.com\")]\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=auth_servers,\n            base_url=\"https://api.example.com\",\n        )\n\n        assert provider.token_verifier is token_verifier\n        assert provider.authorization_servers == auth_servers\n        assert provider.base_url == AnyHttpUrl(\"https://api.example.com/\")\n\n    async def test_verify_token_delegates_to_verifier(self, test_tokens):\n        \"\"\"Test that verify_token delegates to the token verifier.\"\"\"\n        # Use a different token for this specific test\n        tokens = {\n            \"valid_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n        # Valid token\n        result = await provider.verify_token(\"valid_token\")\n        assert result is not None\n        assert result.token == \"valid_token\"\n        assert result.client_id == \"test-client\"\n\n        # Invalid token\n        result = await provider.verify_token(\"invalid_token\")\n        assert result is None\n\n    def test_get_routes_creates_protected_resource_routes(self, test_tokens):\n        \"\"\"Test that get_routes creates protected resource routes.\"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        auth_servers = [AnyHttpUrl(\"https://auth.example.com\")]\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=auth_servers,\n            base_url=\"https://api.example.com\",\n        )\n\n        routes = provider.get_routes()\n        assert len(routes) == 1\n\n        # Check that the route is the OAuth protected resource metadata endpoint\n        # When called without mcp_path, it creates route at /.well-known/oauth-protected-resource\n        route = routes[0]\n        assert route.path == \"/.well-known/oauth-protected-resource\"\n        assert route.methods is not None\n        assert \"GET\" in route.methods\n\n    def test_get_resource_url_with_well_known_path(self):\n        \"\"\"Test _get_resource_url returns correct URL for .well-known path.\"\"\"\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n        metadata_url = provider._get_resource_url(\n            \"/.well-known/oauth-protected-resource/mcp\"\n        )\n        assert metadata_url == AnyHttpUrl(\n            \"https://api.example.com/.well-known/oauth-protected-resource/mcp\"\n        )\n\n    def test_get_resource_url_with_nested_base_url(self):\n        \"\"\"Test _get_resource_url returns correct URL for .well-known path with nested base_url.\"\"\"\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com/v1/\",\n        )\n\n        metadata_url = provider._get_resource_url(\n            \"/.well-known/oauth-protected-resource/mcp\"\n        )\n        assert metadata_url == AnyHttpUrl(\n            \"https://api.example.com/v1/.well-known/oauth-protected-resource/mcp\"\n        )\n\n    def test_get_resource_url_handles_trailing_slash(self):\n        \"\"\"Test _get_resource_url handles trailing slash correctly.\"\"\"\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com/\",\n        )\n\n        metadata_url = provider._get_resource_url(\n            \"/.well-known/oauth-protected-resource/mcp\"\n        )\n        assert metadata_url == AnyHttpUrl(\n            \"https://api.example.com/.well-known/oauth-protected-resource/mcp\"\n        )\n\n\nclass TestRemoteAuthProviderIntegration:\n    \"\"\"Integration tests for RemoteAuthProvider with FastMCP server.\"\"\"\n\n    @pytest.fixture\n    def basic_auth_provider(self, test_tokens):\n        \"\"\"Basic RemoteAuthProvider fixture for testing.\"\"\"\n        token_verifier = StaticTokenVerifier(tokens=test_tokens)\n        return RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n    def _create_test_auth_provider(\n        self, base_url=\"https://api.example.com\", test_tokens=None, **kwargs\n    ):\n        \"\"\"Helper to create a test RemoteAuthProvider with StaticTokenVerifier.\"\"\"\n        tokens = kwargs.get(\n            \"tokens\",\n            test_tokens\n            or {\n                \"test_token\": {\n                    \"client_id\": \"test-client\",\n                    \"scopes\": [\"read\", \"write\"],\n                }\n            },\n        )\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        return RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=base_url,\n        )\n\n    async def test_protected_resource_metadata_endpoint_status_code(\n        self, basic_auth_provider\n    ):\n        \"\"\"Test that the protected resource metadata endpoint returns 200.\"\"\"\n        mcp = FastMCP(\"test-server\", auth=basic_auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n            assert response.status_code == 200\n\n    async def test_protected_resource_metadata_endpoint_resource_field(self):\n        \"\"\"Test that the protected resource metadata endpoint returns correct resource field.\"\"\"\n        auth_provider = self._create_test_auth_provider()\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n            data = response.json()\n\n            # This is the key test - ensure resource field contains the full MCP URL\n            assert data[\"resource\"] == \"https://api.example.com/mcp\"\n\n    async def test_protected_resource_metadata_endpoint_authorization_servers_field(\n        self,\n    ):\n        \"\"\"Test that the protected resource metadata endpoint returns correct authorization_servers field.\"\"\"\n        auth_provider = self._create_test_auth_provider()\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n            data = response.json()\n\n            assert data[\"authorization_servers\"] == [\"https://auth.example.com/\"]\n\n    @pytest.mark.parametrize(\n        \"base_url,expected_resource\",\n        [\n            (\"https://api.example.com\", \"https://api.example.com/mcp\"),\n            (\"https://api.example.com/\", \"https://api.example.com/mcp\"),\n            (\"https://api.example.com/v1/\", \"https://api.example.com/v1/mcp\"),\n        ],\n    )\n    async def test_base_url_configurations(self, base_url: str, expected_resource: str):\n        \"\"\"Test different base_url configurations.\"\"\"\n        from urllib.parse import urlparse\n\n        auth_provider = self._create_test_auth_provider(base_url=base_url)\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        # Extract the path from the expected resource to construct metadata URL\n        resource_parsed = urlparse(expected_resource)\n        # Remove leading slash if present to avoid double slashes\n        resource_path = resource_parsed.path.lstrip(\"/\")\n        metadata_path = f\"/.well-known/oauth-protected-resource/{resource_path}\"\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://test.example.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(metadata_path)\n\n            assert response.status_code == 200\n            data = response.json()\n            assert data[\"resource\"] == expected_resource\n\n    async def test_multiple_authorization_servers_resource_field(self):\n        \"\"\"Test resource field with multiple authorization servers.\"\"\"\n        auth_servers = [\n            AnyHttpUrl(\"https://auth1.example.com\"),\n            AnyHttpUrl(\"https://auth2.example.com\"),\n        ]\n\n        auth_provider = self._create_test_auth_provider()\n        # Override the authorization servers\n        auth_provider.authorization_servers = auth_servers\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            data = response.json()\n            assert data[\"resource\"] == \"https://api.example.com/mcp\"\n\n    async def test_multiple_authorization_servers_list(self):\n        \"\"\"Test authorization_servers field with multiple authorization servers.\"\"\"\n        auth_servers = [\n            AnyHttpUrl(\"https://auth1.example.com\"),\n            AnyHttpUrl(\"https://auth2.example.com\"),\n        ]\n\n        auth_provider = self._create_test_auth_provider()\n        # Override the authorization servers\n        auth_provider.authorization_servers = auth_servers\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://api.example.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            data = response.json()\n            assert set(data[\"authorization_servers\"]) == {\n                \"https://auth1.example.com/\",\n                \"https://auth2.example.com/\",\n            }\n\n    async def test_token_verification_with_valid_auth_succeeds(self):\n        \"\"\"Test that requests with valid auth token succeed.\"\"\"\n        # Note: This test focuses on HTTP-level authentication behavior\n        # For the RemoteAuthProvider, the key test is that the OAuth discovery\n        # endpoint correctly reports the resource server URL, which is tested above\n\n        # This is primarily testing that the token verifier integration works\n        tokens = {\n            \"valid_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n        # Test that the provider correctly delegates to the token verifier\n        result = await provider.verify_token(\"valid_token\")\n        assert result is not None\n        assert result.token == \"valid_token\"\n        assert result.client_id == \"test-client\"\n\n        result = await provider.verify_token(\"invalid_token\")\n        assert result is None\n\n    async def test_token_verification_with_invalid_auth_fails(self):\n        \"\"\"Test that the provider correctly rejects invalid tokens.\"\"\"\n        tokens = {\n            \"valid_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://api.example.com\",\n        )\n\n        # Test that invalid tokens are rejected\n        result = await provider.verify_token(\"invalid_token\")\n        assert result is None\n\n    async def test_issue_1348_oauth_discovery_returns_correct_url(self):\n        \"\"\"Test that RemoteAuthProvider correctly returns the full MCP endpoint URL.\n\n        This test confirms that RemoteAuthProvider works correctly and returns\n        the resource URL with the MCP path appended to the base URL.\n        \"\"\"\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://accounts.google.com\")],\n            base_url=\"https://my-server.com\",\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            assert response.status_code == 200\n            data = response.json()\n\n            # The RemoteAuthProvider correctly returns the full MCP endpoint URL\n            assert data[\"resource\"] == \"https://my-server.com/mcp\"\n            assert data[\"authorization_servers\"] == [\"https://accounts.google.com/\"]\n\n    async def test_resource_name_field(self):\n        \"\"\"Test that RemoteAuthProvider correctly returns the resource_name.\n\n        This test confirms that RemoteAuthProvider works correctly and returns\n        the exact resource_name specified.\n        \"\"\"\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://accounts.google.com\")],\n            base_url=\"https://my-server.com\",\n            resource_name=\"My Test Resource\",\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            assert response.status_code == 200\n            data = response.json()\n\n            # The RemoteAuthProvider correctly returns the resource_name\n            assert data[\"resource_name\"] == \"My Test Resource\"\n\n    async def test_resource_documentation_field(self):\n        \"\"\"Test that RemoteAuthProvider correctly returns the resource_documentation.\n\n        This test confirms that RemoteAuthProvider works correctly and returns\n        the exact resource_documentation specified.\n        \"\"\"\n        tokens = {\n            \"test_token\": {\n                \"client_id\": \"test-client\",\n                \"scopes\": [\"read\"],\n            }\n        }\n        token_verifier = StaticTokenVerifier(tokens=tokens)\n        auth_provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://accounts.google.com\")],\n            base_url=\"https://my-server.com\",\n            resource_documentation=AnyHttpUrl(\n                \"https://doc.my-server.com/resource-docs\"\n            ),\n        )\n\n        mcp = FastMCP(\"test-server\", auth=auth_provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            # The metadata URL is path-aware per RFC 9728\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            assert response.status_code == 200\n            data = response.json()\n\n            # The RemoteAuthProvider correctly returns the resource_documentation\n            assert (\n                data[\"resource_documentation\"]\n                == \"https://doc.my-server.com/resource-docs\"\n            )\n\n    async def test_scopes_supported_overrides_metadata(self):\n        \"\"\"Test that scopes_supported parameter overrides what's in metadata.\"\"\"\n        token_verifier = StaticTokenVerifier(\n            tokens={\n                \"test\": {\"client_id\": \"c\", \"scopes\": [\"read\"]},\n            },\n            required_scopes=[\"read\"],\n        )\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://my-server.com\",\n            scopes_supported=[\"api://my-api/read\"],\n        )\n\n        mcp = FastMCP(\"test-server\", auth=provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            assert response.status_code == 200\n            data = response.json()\n            assert data[\"scopes_supported\"] == [\"api://my-api/read\"]\n\n    async def test_scopes_supported_defaults_to_verifier(self):\n        \"\"\"Test that metadata uses verifier scopes_supported when parameter not set.\"\"\"\n        token_verifier = StaticTokenVerifier(\n            tokens={\n                \"test\": {\"client_id\": \"c\", \"scopes\": [\"read\"]},\n            },\n            required_scopes=[\"read\"],\n        )\n\n        provider = RemoteAuthProvider(\n            token_verifier=token_verifier,\n            authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n            base_url=\"https://my-server.com\",\n        )\n\n        mcp = FastMCP(\"test-server\", auth=provider)\n        mcp_http_app = mcp.http_app()\n\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=mcp_http_app),\n            base_url=\"https://my-server.com\",\n        ) as client:\n            response = await client.get(\"/.well-known/oauth-protected-resource/mcp\")\n\n            assert response.status_code == 200\n            data = response.json()\n            assert data[\"scopes_supported\"] == [\"read\"]\n"
  },
  {
    "path": "tests/server/auth/test_ssrf_protection.py",
    "content": "\"\"\"Tests for SSRF-safe HTTP utilities.\n\nThis module tests the ssrf.py module which provides SSRF-protected HTTP fetching.\n\"\"\"\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport httpx\nimport pytest\n\nfrom fastmcp.server.auth.ssrf import (\n    SSRFError,\n    SSRFFetchError,\n    is_ip_allowed,\n    ssrf_safe_fetch,\n    validate_url,\n)\n\n\nclass TestIsIPAllowed:\n    \"\"\"Tests for is_ip_allowed function.\"\"\"\n\n    def test_public_ipv4_allowed(self):\n        \"\"\"Public IPv4 addresses should be allowed.\"\"\"\n        assert is_ip_allowed(\"8.8.8.8\") is True\n        assert is_ip_allowed(\"1.1.1.1\") is True\n        assert is_ip_allowed(\"93.184.216.34\") is True\n\n    def test_private_ipv4_blocked(self):\n        \"\"\"Private IPv4 addresses should be blocked.\"\"\"\n        assert is_ip_allowed(\"192.168.1.1\") is False\n        assert is_ip_allowed(\"10.0.0.1\") is False\n        assert is_ip_allowed(\"172.16.0.1\") is False\n\n    def test_loopback_blocked(self):\n        \"\"\"Loopback addresses should be blocked.\"\"\"\n        assert is_ip_allowed(\"127.0.0.1\") is False\n        assert is_ip_allowed(\"::1\") is False\n\n    def test_link_local_blocked(self):\n        \"\"\"Link-local addresses (AWS metadata) should be blocked.\"\"\"\n        assert is_ip_allowed(\"169.254.169.254\") is False\n\n    def test_rfc6598_cgnat_blocked(self):\n        \"\"\"RFC6598 Carrier-Grade NAT addresses should be blocked.\"\"\"\n        assert is_ip_allowed(\"100.64.0.1\") is False\n        assert is_ip_allowed(\"100.100.100.100\") is False\n\n    def test_ipv4_mapped_ipv6_blocked_if_private(self):\n        \"\"\"IPv4-mapped IPv6 addresses should check the embedded IPv4.\"\"\"\n        assert is_ip_allowed(\"::ffff:127.0.0.1\") is False\n        assert is_ip_allowed(\"::ffff:192.168.1.1\") is False\n\n\nclass TestValidateURL:\n    \"\"\"Tests for validate_url function.\"\"\"\n\n    async def test_http_rejected(self):\n        \"\"\"HTTP URLs should be rejected (HTTPS required).\"\"\"\n        with pytest.raises(SSRFError, match=\"must use HTTPS\"):\n            await validate_url(\"http://example.com/path\")\n\n    async def test_missing_host_rejected(self):\n        \"\"\"URLs without host should be rejected.\"\"\"\n        with pytest.raises(SSRFError, match=\"must have a host\"):\n            await validate_url(\"https:///path\")\n\n    async def test_root_path_rejected_when_required(self):\n        \"\"\"Root paths should be rejected when require_path=True.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"93.184.216.34\"],\n        ):\n            with pytest.raises(SSRFError, match=\"non-root path\"):\n                await validate_url(\"https://example.com/\", require_path=True)\n\n    async def test_private_ip_rejected(self):\n        \"\"\"URLs resolving to private IPs should be rejected.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"192.168.1.1\"],\n        ):\n            with pytest.raises(SSRFError, match=\"blocked IP\"):\n                await validate_url(\"https://example.com/path\")\n\n\nclass TestSSRFSafeFetch:\n    \"\"\"Tests for ssrf_safe_fetch function.\"\"\"\n\n    async def test_private_ip_blocked(self):\n        \"\"\"Fetch to private IP should be blocked.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"192.168.1.1\"],\n        ):\n            with pytest.raises(SSRFError, match=\"blocked IP\"):\n                await ssrf_safe_fetch(\"https://internal.example.com/api\")\n\n    async def test_cgnat_blocked(self):\n        \"\"\"Fetch to RFC6598 CGNAT IP should be blocked.\"\"\"\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"100.64.0.1\"],\n        ):\n            with pytest.raises(SSRFError, match=\"blocked IP\"):\n                await ssrf_safe_fetch(\"https://cgnat.example.com/api\")\n\n    async def test_connects_to_pinned_ip(self):\n        \"\"\"Verify connection uses pinned IP, not re-resolved DNS.\"\"\"\n        resolved_ip = \"93.184.216.34\"\n\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=[resolved_ip],\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {\"content-length\": \"15\"}\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n            async def aiter_bytes():\n                yield b'{\"data\": \"test\"}'\n\n            mock_stream.aiter_bytes = aiter_bytes\n\n            mock_client = AsyncMock()\n            mock_client.stream = MagicMock(return_value=mock_stream)\n            mock_client.__aenter__.return_value = mock_client\n            mock_client.__aexit__ = AsyncMock(return_value=None)\n            mock_client_class.return_value = mock_client\n\n            await ssrf_safe_fetch(\"https://example.com/api\")\n\n            # Verify URL contains pinned IP\n            call_args = mock_client.stream.call_args\n            url_called = call_args[0][1]\n            assert resolved_ip in url_called\n\n    async def test_fallback_to_second_ip(self):\n        \"\"\"If the first IP fails, the next resolved IP should be tried.\"\"\"\n        resolved_ips = [\"2001:4860:4860::8888\", \"93.184.216.34\"]\n\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=resolved_ips,\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            request = httpx.Request(\"GET\", \"https://example.com/api\")\n\n            first_client = AsyncMock()\n            first_client.stream = MagicMock(\n                side_effect=httpx.RequestError(\"boom\", request=request)\n            )\n            first_client.__aenter__.return_value = first_client\n            first_client.__aexit__ = AsyncMock(return_value=None)\n\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {\"content-length\": \"2\"}\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n            async def aiter_bytes():\n                yield b\"ok\"\n\n            mock_stream.aiter_bytes = aiter_bytes\n\n            second_client = AsyncMock()\n            second_client.stream = MagicMock(return_value=mock_stream)\n            second_client.__aenter__.return_value = second_client\n            second_client.__aexit__ = AsyncMock(return_value=None)\n\n            mock_client_class.side_effect = [first_client, second_client]\n\n            content = await ssrf_safe_fetch(\"https://example.com/api\")\n            assert content == b\"ok\"\n\n            call_args = second_client.stream.call_args\n            url_called = call_args[0][1]\n            assert resolved_ips[1] in url_called\n\n    async def test_host_header_set(self):\n        \"\"\"Verify Host header is set to original hostname.\"\"\"\n        resolved_ip = \"93.184.216.34\"\n        original_host = \"example.com\"\n\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=[resolved_ip],\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {\"content-length\": \"15\"}\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n            async def aiter_bytes():\n                yield b'{\"data\": \"test\"}'\n\n            mock_stream.aiter_bytes = aiter_bytes\n\n            mock_client = AsyncMock()\n            mock_client.stream = MagicMock(return_value=mock_stream)\n            mock_client.__aenter__.return_value = mock_client\n            mock_client.__aexit__ = AsyncMock(return_value=None)\n            mock_client_class.return_value = mock_client\n\n            await ssrf_safe_fetch(f\"https://{original_host}/api\")\n\n            # Verify Host header\n            call_kwargs = mock_client.stream.call_args[1]\n            assert call_kwargs[\"headers\"][\"Host\"] == original_host\n\n    async def test_response_size_limit(self):\n        \"\"\"Verify response size limit is enforced via streaming.\"\"\"\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=[\"93.184.216.34\"],\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            # Response larger than default 5KB (no Content-Length, so streaming enforces)\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {}  # No Content-Length to force streaming check\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n            async def aiter_bytes():\n                # Yield 10KB total\n                for _ in range(10):\n                    yield b\"x\" * 1024\n\n            mock_stream.aiter_bytes = aiter_bytes\n\n            mock_client = AsyncMock()\n            mock_client.stream = MagicMock(return_value=mock_stream)\n            mock_client.__aenter__.return_value = mock_client\n            mock_client.__aexit__ = AsyncMock(return_value=None)\n            mock_client_class.return_value = mock_client\n\n            with pytest.raises(SSRFFetchError, match=\"too large\"):\n                await ssrf_safe_fetch(\"https://example.com/api\")\n\n\nclass TestJWKSSSRFProtection:\n    \"\"\"Tests for SSRF protection in JWTVerifier JWKS fetching.\"\"\"\n\n    async def test_jwks_private_ip_blocked(self):\n        \"\"\"JWKS fetch to private IP should be blocked.\"\"\"\n        from fastmcp.server.auth.providers.jwt import JWTVerifier\n\n        verifier = JWTVerifier(\n            jwks_uri=\"https://internal.example.com/.well-known/jwks.json\",\n            issuer=\"https://issuer.example.com\",\n            ssrf_safe=True,\n        )\n\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"192.168.1.1\"],\n        ):\n            with pytest.raises(ValueError, match=\"Failed to fetch JWKS\"):\n                # Create a dummy token to trigger JWKS fetch\n                await verifier._get_jwks_key(\"test-kid\")\n\n    async def test_jwks_cgnat_blocked(self):\n        \"\"\"JWKS fetch to RFC6598 CGNAT IP should be blocked.\"\"\"\n        from fastmcp.server.auth.providers.jwt import JWTVerifier\n\n        verifier = JWTVerifier(\n            jwks_uri=\"https://cgnat.example.com/.well-known/jwks.json\",\n            issuer=\"https://issuer.example.com\",\n            ssrf_safe=True,\n        )\n\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"100.64.0.1\"],\n        ):\n            with pytest.raises(ValueError, match=\"Failed to fetch JWKS\"):\n                await verifier._get_jwks_key(\"test-kid\")\n\n    async def test_jwks_loopback_blocked(self):\n        \"\"\"JWKS fetch to loopback should be blocked.\"\"\"\n        from fastmcp.server.auth.providers.jwt import JWTVerifier\n\n        verifier = JWTVerifier(\n            jwks_uri=\"https://localhost/.well-known/jwks.json\",\n            issuer=\"https://issuer.example.com\",\n            ssrf_safe=True,\n        )\n\n        with patch(\n            \"fastmcp.server.auth.ssrf.resolve_hostname\",\n            return_value=[\"127.0.0.1\"],\n        ):\n            with pytest.raises(ValueError, match=\"Failed to fetch JWKS\"):\n                await verifier._get_jwks_key(\"test-kid\")\n\n\nclass TestIPv6URLFormatting:\n    \"\"\"Tests for proper IPv6 address bracketing in URLs.\"\"\"\n\n    def test_format_ip_for_url_ipv4(self):\n        \"\"\"IPv4 addresses should not be bracketed.\"\"\"\n        from fastmcp.server.auth.ssrf import format_ip_for_url\n\n        assert format_ip_for_url(\"8.8.8.8\") == \"8.8.8.8\"\n        assert format_ip_for_url(\"192.168.1.1\") == \"192.168.1.1\"\n\n    def test_format_ip_for_url_ipv6(self):\n        \"\"\"IPv6 addresses should be bracketed for URL use.\"\"\"\n        from fastmcp.server.auth.ssrf import format_ip_for_url\n\n        assert format_ip_for_url(\"2001:db8::1\") == \"[2001:db8::1]\"\n        assert format_ip_for_url(\"::1\") == \"[::1]\"\n        assert format_ip_for_url(\"fe80::1\") == \"[fe80::1]\"\n\n    def test_format_ip_for_url_invalid(self):\n        \"\"\"Invalid IP strings should be returned unchanged.\"\"\"\n        from fastmcp.server.auth.ssrf import format_ip_for_url\n\n        assert format_ip_for_url(\"not-an-ip\") == \"not-an-ip\"\n        assert format_ip_for_url(\"\") == \"\"\n\n    async def test_ipv6_pinned_url_is_valid(self):\n        \"\"\"Verify IPv6 addresses are properly bracketed in pinned URLs.\"\"\"\n        resolved_ipv6 = \"2001:4860:4860::8888\"\n\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=[resolved_ipv6],\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {\"content-length\": \"10\"}\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n            async def aiter_bytes():\n                yield b'{\"key\": 1}'\n\n            mock_stream.aiter_bytes = aiter_bytes\n\n            mock_client = AsyncMock()\n            mock_client.stream = MagicMock(return_value=mock_stream)\n            mock_client.__aenter__.return_value = mock_client\n            mock_client.__aexit__ = AsyncMock(return_value=None)\n            mock_client_class.return_value = mock_client\n\n            await ssrf_safe_fetch(\"https://example.com/api\")\n\n            # Verify the URL contains bracketed IPv6 address\n            call_args = mock_client.stream.call_args\n            url_called = call_args[0][1]\n\n            # IPv6 should be bracketed: https://[2001:4860:4860::8888]:443/path\n            assert f\"[{resolved_ipv6}]\" in url_called, (\n                f\"Expected bracketed IPv6 [{resolved_ipv6}] in URL, got {url_called}\"\n            )\n\n\nclass TestStreamingResponseSizeLimit:\n    \"\"\"Tests for streaming-based response size enforcement.\"\"\"\n\n    async def test_size_limit_enforced_during_streaming(self):\n        \"\"\"Verify that size limit is enforced as chunks are received, not after.\"\"\"\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=[\"93.184.216.34\"],\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            chunks_yielded = []\n\n            async def aiter_bytes():\n                # Yield chunks that exceed the limit\n                for i in range(10):\n                    chunk = b\"x\" * 1024  # 1KB per chunk\n                    chunks_yielded.append(chunk)\n                    yield chunk\n\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {}  # No content-length to force streaming check\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n            mock_stream.aiter_bytes = aiter_bytes\n\n            mock_client = AsyncMock()\n            mock_client.stream = MagicMock(return_value=mock_stream)\n            mock_client.__aenter__.return_value = mock_client\n            mock_client.__aexit__ = AsyncMock(return_value=None)\n            mock_client_class.return_value = mock_client\n\n            with pytest.raises(SSRFFetchError, match=\"too large\"):\n                await ssrf_safe_fetch(\"https://example.com/api\", max_size=5120)\n\n            # Verify we stopped after exceeding the limit (should be ~6 chunks for 5KB limit)\n            # This confirms we're enforcing during streaming, not after downloading all\n            assert len(chunks_yielded) <= 7, (\n                f\"Downloaded {len(chunks_yielded)} chunks (expected <=7 for streaming enforcement)\"\n            )\n\n    async def test_content_length_header_checked_first(self):\n        \"\"\"Verify Content-Length header is checked before streaming.\"\"\"\n        with (\n            patch(\n                \"fastmcp.server.auth.ssrf.resolve_hostname\",\n                return_value=[\"93.184.216.34\"],\n            ),\n            patch(\"httpx.AsyncClient\") as mock_client_class,\n        ):\n            mock_stream = MagicMock()\n            mock_stream.status_code = 200\n            mock_stream.headers = {\"content-length\": \"10240\"}  # 10KB\n            mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)\n            mock_stream.__aexit__ = AsyncMock(return_value=None)\n\n            # aiter_bytes should never be called if Content-Length is checked\n            mock_stream.aiter_bytes = MagicMock(\n                side_effect=AssertionError(\"Should not stream\")\n            )\n\n            mock_client = AsyncMock()\n            mock_client.stream = MagicMock(return_value=mock_stream)\n            mock_client.__aenter__.return_value = mock_client\n            mock_client.__aexit__ = AsyncMock(return_value=None)\n            mock_client_class.return_value = mock_client\n\n            with pytest.raises(SSRFFetchError, match=\"too large\"):\n                await ssrf_safe_fetch(\"https://example.com/api\", max_size=5120)\n"
  },
  {
    "path": "tests/server/auth/test_static_token_verifier.py",
    "content": "\"\"\"Tests for StaticTokenVerifier integration with FastMCP.\"\"\"\n\nimport httpx\n\nfrom fastmcp.server import FastMCP\nfrom fastmcp.server.auth import AccessToken\nfrom fastmcp.server.auth.providers.jwt import StaticTokenVerifier\n\n\nclass TestStaticTokenVerifier:\n    \"\"\"Test StaticTokenVerifier integration with FastMCP server.\"\"\"\n\n    def test_static_token_verifier_creation(self):\n        \"\"\"Test creating a FastMCP server with StaticTokenVerifier.\"\"\"\n        verifier = StaticTokenVerifier(\n            {\"test-token\": {\"client_id\": \"test-client\", \"scopes\": [\"read\", \"write\"]}}\n        )\n\n        server = FastMCP(\"TestServer\", auth=verifier)\n        assert server.auth is verifier\n\n    async def test_static_token_verifier_verify_token(self):\n        \"\"\"Test StaticTokenVerifier token verification.\"\"\"\n        verifier = StaticTokenVerifier(\n            {\n                \"valid-token\": {\n                    \"client_id\": \"test-client\",\n                    \"scopes\": [\"read\", \"write\"],\n                    \"expires_at\": None,\n                },\n                \"scoped-token\": {\"client_id\": \"limited-client\", \"scopes\": [\"read\"]},\n            }\n        )\n\n        # Test valid token\n        result = await verifier.verify_token(\"valid-token\")\n        assert isinstance(result, AccessToken)\n        assert result.client_id == \"test-client\"\n        assert result.scopes == [\"read\", \"write\"]\n        assert result.token == \"valid-token\"\n        assert result.expires_at is None\n\n        # Test token with different scopes\n        result = await verifier.verify_token(\"scoped-token\")\n        assert isinstance(result, AccessToken)\n        assert result.client_id == \"limited-client\"\n        assert result.scopes == [\"read\"]\n\n        # Test invalid token\n        result = await verifier.verify_token(\"invalid-token\")\n        assert result is None\n\n    async def test_server_with_token_verifier_http_app(self):\n        \"\"\"Test that FastMCP server works with StaticTokenVerifier for HTTP requests.\"\"\"\n        verifier = StaticTokenVerifier(\n            {\"test-token\": {\"client_id\": \"test-client\", \"scopes\": [\"read\", \"write\"]}}\n        )\n\n        server = FastMCP(\"TestServer\", auth=verifier)\n\n        @server.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Create HTTP app\n        app = server.http_app(transport=\"http\")\n\n        # Test unauthenticated request gets 401 (use exact path match to avoid redirect)\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app), base_url=\"http://test\"\n        ) as client:\n            response = await client.post(\"/mcp\")\n            assert response.status_code == 401\n            assert \"WWW-Authenticate\" in response.headers\n\n    async def test_server_with_token_verifier_redirect_behavior(self):\n        \"\"\"Test that FastMCP server redirects non-matching paths correctly.\"\"\"\n        verifier = StaticTokenVerifier(\n            {\"test-token\": {\"client_id\": \"test-client\", \"scopes\": [\"read\", \"write\"]}}\n        )\n\n        server = FastMCP(\"TestServer\", auth=verifier)\n\n        @server.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Create HTTP app (default path is /mcp)\n        app = server.http_app(transport=\"http\")\n\n        # Test that non-matching path gets 307 redirect\n        async with httpx.AsyncClient(\n            transport=httpx.ASGITransport(app=app), base_url=\"http://test\"\n        ) as client:\n            response = await client.post(\"/mcp/\", follow_redirects=False)\n            assert response.status_code == 307\n            assert response.headers[\"location\"] == \"http://test/mcp\"\n\n    def test_server_rejects_both_oauth_and_token_verifier(self):\n        \"\"\"Test that server raises error when both OAuth and TokenVerifier provided.\"\"\"\n        from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider\n\n        oauth_provider = InMemoryOAuthProvider(\"http://test.com\")\n        token_verifier = StaticTokenVerifier({\"token\": {\"client_id\": \"test\"}})\n\n        # This should work - OAuth provider\n        server1 = FastMCP(\"Test1\", auth=oauth_provider)\n        assert server1.auth is oauth_provider\n\n        # This should work - TokenVerifier\n        server2 = FastMCP(\"Test2\", auth=token_verifier)\n        assert server2.auth is token_verifier\n"
  },
  {
    "path": "tests/server/http/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/http/test_bearer_auth_backend.py",
    "content": "\"\"\"Tests for BearerAuthBackend integration with TokenVerifier.\"\"\"\n\nimport pytest\nfrom mcp.server.auth.middleware.bearer_auth import BearerAuthBackend\nfrom starlette.requests import HTTPConnection\n\nfrom fastmcp.server.auth import AccessToken, TokenVerifier\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\n\n\nclass TestBearerAuthBackendTokenVerifierIntegration:\n    \"\"\"Test BearerAuthBackend works with TokenVerifier protocol.\"\"\"\n\n    @pytest.fixture\n    def rsa_key_pair(self) -> RSAKeyPair:\n        \"\"\"Generate RSA key pair for testing.\"\"\"\n        return RSAKeyPair.generate()\n\n    @pytest.fixture\n    def jwt_verifier(self, rsa_key_pair: RSAKeyPair) -> JWTVerifier:\n        \"\"\"Create JWTVerifier for testing.\"\"\"\n        return JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n        )\n\n    @pytest.fixture\n    def valid_token(self, rsa_key_pair: RSAKeyPair) -> str:\n        \"\"\"Create a valid test token.\"\"\"\n        return rsa_key_pair.create_token(\n            subject=\"test-user\",\n            issuer=\"https://test.example.com\",\n            audience=\"https://api.example.com\",\n            scopes=[\"read\", \"write\"],\n        )\n\n    def test_bearer_auth_backend_constructor_accepts_token_verifier(\n        self, jwt_verifier: JWTVerifier\n    ):\n        \"\"\"Test that BearerAuthBackend constructor accepts TokenVerifier.\"\"\"\n        # This should not raise an error\n        backend = BearerAuthBackend(jwt_verifier)\n        assert isinstance(backend.token_verifier, TokenVerifier)\n        assert backend.token_verifier is jwt_verifier\n\n    async def test_bearer_auth_backend_authenticate_with_valid_token(\n        self, jwt_verifier: JWTVerifier, valid_token: str\n    ):\n        \"\"\"Test BearerAuthBackend authentication with valid token.\"\"\"\n        backend = BearerAuthBackend(jwt_verifier)\n\n        # Create mock HTTPConnection with Authorization header\n        scope = {\n            \"type\": \"http\",\n            \"headers\": [(b\"authorization\", f\"Bearer {valid_token}\".encode())],\n        }\n        conn = HTTPConnection(scope)\n\n        result = await backend.authenticate(conn)\n\n        assert result is not None\n        credentials, user = result\n        assert credentials.scopes == [\"read\", \"write\"]\n        assert user.username == \"test-user\"\n        assert hasattr(user, \"access_token\")\n        assert user.access_token.token == valid_token\n\n    async def test_bearer_auth_backend_authenticate_with_invalid_token(\n        self, jwt_verifier: JWTVerifier\n    ):\n        \"\"\"Test BearerAuthBackend authentication with invalid token.\"\"\"\n        backend = BearerAuthBackend(jwt_verifier)\n\n        # Create mock HTTPConnection with invalid Authorization header\n        scope = {\n            \"type\": \"http\",\n            \"headers\": [(b\"authorization\", b\"Bearer invalid-token\")],\n        }\n        conn = HTTPConnection(scope)\n\n        result = await backend.authenticate(conn)\n        assert result is None\n\n    async def test_bearer_auth_backend_authenticate_with_no_header(\n        self, jwt_verifier: JWTVerifier\n    ):\n        \"\"\"Test BearerAuthBackend authentication with no Authorization header.\"\"\"\n        backend = BearerAuthBackend(jwt_verifier)\n\n        # Create mock HTTPConnection without Authorization header\n        scope = {\n            \"type\": \"http\",\n            \"headers\": [],\n        }\n        conn = HTTPConnection(scope)\n\n        result = await backend.authenticate(conn)\n        assert result is None\n\n    async def test_bearer_auth_backend_authenticate_with_non_bearer_token(\n        self, jwt_verifier: JWTVerifier\n    ):\n        \"\"\"Test BearerAuthBackend authentication with non-Bearer token.\"\"\"\n        backend = BearerAuthBackend(jwt_verifier)\n\n        # Create mock HTTPConnection with Basic auth header\n        scope = {\n            \"type\": \"http\",\n            \"headers\": [(b\"authorization\", b\"Basic dXNlcjpwYXNz\")],\n        }\n        conn = HTTPConnection(scope)\n\n        result = await backend.authenticate(conn)\n        assert result is None\n\n\nclass MockTokenVerifier:\n    \"\"\"Mock TokenVerifier for testing backend integration.\"\"\"\n\n    def __init__(self, return_value: AccessToken | None = None):\n        self.return_value = return_value\n        self.verify_token_calls = []\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Mock verify_token method.\"\"\"\n        self.verify_token_calls.append(token)\n        return self.return_value\n\n\nclass TestBearerAuthBackendWithMockVerifier:\n    \"\"\"Test BearerAuthBackend with mock TokenVerifier.\"\"\"\n\n    async def test_backend_calls_verify_token_method(self):\n        \"\"\"Test that BearerAuthBackend calls verify_token on the verifier.\"\"\"\n        mock_access_token = AccessToken(\n            token=\"test-token\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n            expires_at=None,\n        )\n        mock_verifier = MockTokenVerifier(return_value=mock_access_token)\n        backend = BearerAuthBackend(mock_verifier)\n\n        scope = {\n            \"type\": \"http\",\n            \"headers\": [(b\"authorization\", b\"Bearer test-token\")],\n        }\n        conn = HTTPConnection(scope)\n\n        result = await backend.authenticate(conn)\n\n        # Should have called verify_token with the token\n        assert mock_verifier.verify_token_calls == [\"test-token\"]\n\n        # Should return authentication result\n        assert result is not None\n        credentials, user = result\n        assert credentials.scopes == [\"read\"]\n        assert user.username == \"test-client\"\n\n    async def test_backend_handles_verify_token_none_result(self):\n        \"\"\"Test that BearerAuthBackend handles None result from verify_token.\"\"\"\n        mock_verifier = MockTokenVerifier(return_value=None)\n        backend = BearerAuthBackend(mock_verifier)\n\n        scope = {\n            \"type\": \"http\",\n            \"headers\": [(b\"authorization\", b\"Bearer invalid-token\")],\n        }\n        conn = HTTPConnection(scope)\n\n        result = await backend.authenticate(conn)\n\n        # Should have called verify_token\n        assert mock_verifier.verify_token_calls == [\"invalid-token\"]\n\n        # Should return None for authentication failure\n        assert result is None\n"
  },
  {
    "path": "tests/server/http/test_custom_routes.py",
    "content": "import pytest\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.http import create_sse_app, create_streamable_http_app\n\n\nclass TestCustomRoutes:\n    @pytest.fixture\n    def server_with_custom_route(self):\n        \"\"\"Create a FastMCP server with a custom route.\"\"\"\n        server = FastMCP()\n\n        @server.custom_route(\"/custom-route\", methods=[\"GET\"])\n        async def custom_route(request: Request):\n            return JSONResponse({\"message\": \"custom route\"})\n\n        return server\n\n    def test_custom_routes_apply_filtering_http_app(self, server_with_custom_route):\n        \"\"\"Test that custom routes are included when using server.http_app().\"\"\"\n        # Get the app via server.http_app()\n        app = server_with_custom_route.http_app()\n\n        # Verify that the custom route is included\n        custom_route_found = False\n        for route in app.routes:\n            if isinstance(route, Route) and route.path == \"/custom-route\":\n                custom_route_found = True\n                break\n\n        assert custom_route_found, \"Custom route was not found in app routes\"\n\n    def test_custom_routes_via_streamable_http_app_direct(\n        self, server_with_custom_route\n    ):\n        \"\"\"Test that custom routes are included when using create_streamable_http_app directly.\"\"\"\n        # Create the app by calling the constructor function directly\n        app = create_streamable_http_app(\n            server=server_with_custom_route, streamable_http_path=\"/api\"\n        )\n\n        # Verify that the custom route is included\n        custom_route_found = False\n        for route in app.routes:\n            if isinstance(route, Route) and route.path == \"/custom-route\":\n                custom_route_found = True\n                break\n\n        assert custom_route_found, \"Custom route was not found in app routes\"\n\n    def test_custom_routes_via_sse_app_direct(self, server_with_custom_route):\n        \"\"\"Test that custom routes are included when using create_sse_app directly.\"\"\"\n        # Create the app by calling the constructor function directly\n        app = create_sse_app(\n            server=server_with_custom_route, message_path=\"/message\", sse_path=\"/sse/\"\n        )\n\n        # Verify that the custom route is included\n        custom_route_found = False\n        for route in app.routes:\n            if isinstance(route, Route) and route.path == \"/custom-route\":\n                custom_route_found = True\n                break\n\n        assert custom_route_found, \"Custom route was not found in app routes\"\n\n    def test_multiple_custom_routes(\n        self,\n    ):\n        \"\"\"Test that multiple custom routes are included in both methods.\"\"\"\n        server = FastMCP()\n\n        custom_paths = [\"/route1\", \"/route2\", \"/route3\"]\n\n        # Add multiple custom routes\n        for path in custom_paths:\n\n            @server.custom_route(path, methods=[\"GET\"])\n            async def custom_route(request: Request):\n                return JSONResponse({\"message\": f\"route {path}\"})\n\n        # Test with server.http_app()\n        app1 = server.http_app()\n\n        # Test with direct constructor call\n        app2 = create_streamable_http_app(server=server, streamable_http_path=\"/api\")\n\n        # Check all routes are in both apps\n        for path in custom_paths:\n            # Check in app1\n            route_in_app1 = any(\n                isinstance(route, Route) and route.path == path for route in app1.routes\n            )\n            assert route_in_app1, f\"Route {path} not found in server.http_app()\"\n\n            # Check in app2\n            route_in_app2 = any(\n                isinstance(route, Route) and route.path == path for route in app2.routes\n            )\n            assert route_in_app2, (\n                f\"Route {path} not found in create_streamable_http_app()\"\n            )\n"
  },
  {
    "path": "tests/server/http/test_http_auth_middleware.py",
    "content": "import pytest\nfrom mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware\nfrom starlette.routing import Route\nfrom starlette.testclient import TestClient\n\nfrom fastmcp.server import FastMCP\nfrom fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair\nfrom fastmcp.server.http import create_streamable_http_app\n\n\nclass TestStreamableHTTPAppResourceMetadataURL:\n    \"\"\"Test resource_metadata_url logic in create_streamable_http_app.\"\"\"\n\n    @pytest.fixture\n    def rsa_key_pair(self) -> RSAKeyPair:\n        \"\"\"Generate RSA key pair for testing.\"\"\"\n        return RSAKeyPair.generate()\n\n    @pytest.fixture\n    def bearer_auth_provider(self, rsa_key_pair):\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://issuer\",\n            audience=\"https://audience\",\n            base_url=\"https://resource.example.com\",\n        )\n        return provider\n\n    def test_auth_endpoint_wrapped_with_require_auth_middleware(\n        self, bearer_auth_provider\n    ):\n        \"\"\"Test that auth-protected endpoints use RequireAuthMiddleware.\"\"\"\n        server = FastMCP(name=\"TestServer\")\n\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=bearer_auth_provider,\n        )\n\n        route = next(r for r in app.routes if isinstance(r, Route) and r.path == \"/mcp\")\n\n        # When auth is enabled, endpoint should use RequireAuthMiddleware\n        assert isinstance(route.endpoint, RequireAuthMiddleware)\n\n    def test_auth_endpoint_has_correct_methods(self, rsa_key_pair):\n        \"\"\"Test that auth-protected endpoints have correct HTTP methods.\"\"\"\n        provider = JWTVerifier(\n            public_key=rsa_key_pair.public_key,\n            issuer=\"https://issuer\",\n            audience=\"https://audience\",\n            base_url=\"https://resource.example.com/\",\n        )\n        server = FastMCP(name=\"TestServer\")\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=provider,\n        )\n        route = next(r for r in app.routes if isinstance(r, Route) and r.path == \"/mcp\")\n\n        # Verify RequireAuthMiddleware is applied\n        assert isinstance(route.endpoint, RequireAuthMiddleware)\n        # Verify methods include GET, POST, DELETE for streamable-http\n        expected_methods = {\"GET\", \"POST\", \"DELETE\"}\n        assert route.methods is not None\n        assert expected_methods.issubset(set(route.methods))\n\n    def test_no_auth_provider_mounts_without_middleware(self, rsa_key_pair):\n        \"\"\"Test that endpoints without auth are not wrapped with middleware.\"\"\"\n        server = FastMCP(name=\"TestServer\")\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=None,\n        )\n        route = next(r for r in app.routes if isinstance(r, Route) and r.path == \"/mcp\")\n        # Without auth, no RequireAuthMiddleware should be applied\n        assert not isinstance(route.endpoint, RequireAuthMiddleware)\n\n    def test_authenticated_requests_still_require_auth(self, bearer_auth_provider):\n        \"\"\"Test that actual requests (not OPTIONS) still require authentication.\"\"\"\n        server = FastMCP(name=\"TestServer\")\n        app = create_streamable_http_app(\n            server=server,\n            streamable_http_path=\"/mcp\",\n            auth=bearer_auth_provider,\n        )\n\n        # Test POST request without auth - should fail with 401\n        with TestClient(app) as client:\n            response = client.post(\"/mcp\")\n            assert response.status_code == 401\n            assert \"www-authenticate\" in response.headers\n"
  },
  {
    "path": "tests/server/http/test_http_dependencies.py",
    "content": "import json\n\nimport pytest\nfrom mcp.types import TextContent, TextResourceContents\n\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import SSETransport, StreamableHttpTransport\nfrom fastmcp.server.dependencies import get_http_request\nfrom fastmcp.server.server import FastMCP\nfrom fastmcp.utilities.tests import run_server_async\n\n\ndef fastmcp_server():\n    server = FastMCP()\n\n    # Add a tool\n    @server.tool\n    def get_headers_tool() -> dict[str, str]:\n        \"\"\"Get the HTTP headers from the request.\"\"\"\n        request = get_http_request()\n\n        return dict(request.headers)\n\n    @server.resource(uri=\"request://headers\")\n    async def get_headers_resource() -> str:\n        import json\n\n        request = get_http_request()\n        return json.dumps(dict(request.headers))\n\n    # Add a prompt\n    @server.prompt\n    def get_headers_prompt() -> str:\n        \"\"\"Get the HTTP headers from the request.\"\"\"\n        request = get_http_request()\n\n        return json.dumps(dict(request.headers))\n\n    return server\n\n\n@pytest.fixture\nasync def shttp_server():\n    \"\"\"Start a test server with StreamableHttp transport.\"\"\"\n    server = fastmcp_server()\n    async with run_server_async(server, transport=\"http\") as url:\n        yield url\n\n\n@pytest.fixture\nasync def sse_server():\n    \"\"\"Start a test server with SSE transport.\"\"\"\n    server = fastmcp_server()\n    async with run_server_async(server, transport=\"sse\") as url:\n        yield url\n\n\nasync def test_http_headers_resource_shttp(shttp_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(\n            shttp_server, headers={\"X-DEMO-HEADER\": \"ABC\"}\n        )\n    ) as client:\n        raw_result = await client.read_resource(\"request://headers\")\n        assert isinstance(raw_result[0], TextResourceContents)\n        json_result = json.loads(raw_result[0].text)\n        assert \"x-demo-header\" in json_result\n        assert json_result[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_http_headers_resource_sse(sse_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-DEMO-HEADER\": \"ABC\"})\n    ) as client:\n        raw_result = await client.read_resource(\"request://headers\")\n        assert isinstance(raw_result[0], TextResourceContents)\n        json_result = json.loads(raw_result[0].text)\n        assert \"x-demo-header\" in json_result\n        assert json_result[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_http_headers_tool_shttp(shttp_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(\n            shttp_server, headers={\"X-DEMO-HEADER\": \"ABC\"}\n        )\n    ) as client:\n        result = await client.call_tool(\"get_headers_tool\")\n        assert \"x-demo-header\" in result.data\n        assert result.data[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_http_headers_tool_sse(sse_server: str):\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-DEMO-HEADER\": \"ABC\"})\n    ) as client:\n        result = await client.call_tool(\"get_headers_tool\")\n        assert \"x-demo-header\" in result.data\n        assert result.data[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_http_headers_prompt_shttp(shttp_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=StreamableHttpTransport(\n            shttp_server, headers={\"X-DEMO-HEADER\": \"ABC\"}\n        )\n    ) as client:\n        result = await client.get_prompt(\"get_headers_prompt\")\n        assert isinstance(result.messages[0].content, TextContent)\n        json_result = json.loads(result.messages[0].content.text)\n        assert \"x-demo-header\" in json_result\n        assert json_result[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_http_headers_prompt_sse(sse_server: str):\n    \"\"\"Test getting HTTP headers from the server.\"\"\"\n    async with Client(\n        transport=SSETransport(sse_server, headers={\"X-DEMO-HEADER\": \"ABC\"})\n    ) as client:\n        result = await client.get_prompt(\"get_headers_prompt\")\n        assert isinstance(result.messages[0].content, TextContent)\n        json_result = json.loads(result.messages[0].content.text)\n        assert \"x-demo-header\" in json_result\n        assert json_result[\"x-demo-header\"] == \"ABC\"\n\n\nasync def test_get_http_headers_excludes_content_type(sse_server: str):\n    \"\"\"Test that get_http_headers() excludes content-type header (issue #3097).\n\n    This prevents HTTP 415 errors when forwarding headers to downstream APIs\n    that require specific Content-Type headers (e.g., application/vnd.api+json).\n    \"\"\"\n    from fastmcp.server.dependencies import get_http_headers\n\n    server = FastMCP()\n\n    @server.tool\n    def check_excluded_headers() -> dict[str, str]:\n        \"\"\"Check that problematic headers are excluded from get_http_headers().\"\"\"\n        return get_http_headers()\n\n    async with run_server_async(server, transport=\"sse\") as url:\n        async with Client(\n            transport=SSETransport(\n                url,\n                headers={\n                    \"Content-Type\": \"application/json\",\n                    \"Accept\": \"application/json\",\n                    \"X-Custom-Header\": \"should-be-included\",\n                },\n            )\n        ) as client:\n            result = await client.call_tool(\"check_excluded_headers\")\n            headers = result.data\n\n            # These headers should be excluded\n            assert \"content-type\" not in headers\n            assert \"accept\" not in headers\n            assert \"host\" not in headers\n            assert \"content-length\" not in headers\n\n            # Custom headers should be included\n            assert \"x-custom-header\" in headers\n            assert headers[\"x-custom-header\"] == \"should-be-included\"\n"
  },
  {
    "path": "tests/server/http/test_http_middleware.py",
    "content": "\"\"\"Tests for middleware in HTTP apps.\"\"\"\n\nfrom collections.abc import Callable\nfrom typing import Any\n\nimport httpx\nfrom httpx import ASGITransport\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import BaseRoute, Route\nfrom starlette.types import ASGIApp\n\nfrom fastmcp.server import FastMCP\nfrom fastmcp.server.http import create_sse_app, create_streamable_http_app\n\n\nclass HeaderMiddleware(BaseHTTPMiddleware):\n    \"\"\"Simple middleware that adds a custom header to responses.\"\"\"\n\n    def __init__(self, app: ASGIApp, header_name: str, header_value: str):\n        super().__init__(app)\n        self.header_name = header_name\n        self.header_value = header_value\n\n    async def dispatch(self, request: Request, call_next: Callable):\n        response = await call_next(request)\n        response.headers[self.header_name] = self.header_value\n        return response\n\n\nclass RequestModifierMiddleware(BaseHTTPMiddleware):\n    \"\"\"Middleware that adds a value to request state.\"\"\"\n\n    def __init__(self, app: ASGIApp, key: str, value: Any):\n        super().__init__(app)\n        self.key = key\n        self.value = value\n\n    async def dispatch(self, request: Request, call_next: Callable):\n        request.state.custom_value = {self.key: self.value}\n        return await call_next(request)\n\n\nasync def endpoint_handler(request: Request):\n    \"\"\"Endpoint that returns request state or headers.\"\"\"\n    if hasattr(request.state, \"custom_value\"):\n        return JSONResponse({\"state\": request.state.custom_value})\n    return JSONResponse({\"message\": \"Hello, world!\"})\n\n\nasync def test_sse_app_with_custom_middleware():\n    \"\"\"Test that custom middleware works with SSE app.\"\"\"\n    server = FastMCP(name=\"TestServer\")\n\n    # Create custom middleware\n    # TODO(ty): remove when Starlette Middleware typing is supported\n    custom_middleware = [\n        Middleware(\n            HeaderMiddleware,  # type: ignore[arg-type]\n            header_name=\"X-Custom-Header\",\n            header_value=\"test-value\",\n        )\n    ]\n\n    # Add a test route to server's additional routes\n    routes: list[BaseRoute] = [Route(\"/test\", endpoint_handler)]\n    server._additional_http_routes = routes\n\n    # Create the app with custom middleware\n    app = server.http_app(transport=\"sse\", middleware=custom_middleware)\n\n    # Create a test client\n    transport = ASGITransport(app=app)\n    async with httpx.AsyncClient(\n        transport=transport, base_url=\"http://testserver\"\n    ) as client:\n        response = await client.get(\"/test\")\n\n        # Verify middleware was applied\n        assert response.status_code == 200\n        assert response.headers[\"X-Custom-Header\"] == \"test-value\"\n\n\nasync def test_streamable_http_app_with_custom_middleware():\n    \"\"\"Test that custom middleware works with StreamableHTTP app.\"\"\"\n    server = FastMCP(name=\"TestServer\")\n\n    # Create custom middleware\n    # TODO(ty): remove when Starlette Middleware typing is supported\n    custom_middleware = [\n        Middleware(\n            HeaderMiddleware,  # type: ignore[arg-type]\n            header_name=\"X-Custom-Header\",\n            header_value=\"test-value\",\n        )\n    ]\n\n    # Add a test route to server's additional routes\n    routes: list[BaseRoute] = [Route(\"/test\", endpoint_handler)]\n    server._additional_http_routes = routes\n\n    # Create the app with custom middleware\n    app = server.http_app(transport=\"http\", middleware=custom_middleware)\n\n    # Create a test client\n    transport = ASGITransport(app=app)\n    async with httpx.AsyncClient(\n        transport=transport, base_url=\"http://testserver\"\n    ) as client:\n        response = await client.get(\"/test\")\n\n        # Verify middleware was applied\n        assert response.status_code == 200\n        assert response.headers[\"X-Custom-Header\"] == \"test-value\"\n\n\nasync def test_create_sse_app_with_custom_middleware():\n    \"\"\"Test that custom middleware works with create_sse_app function.\"\"\"\n    server = FastMCP(name=\"TestServer\")\n\n    # Create custom middleware\n    # TODO(ty): remove when Starlette Middleware typing is supported\n    custom_middleware = [\n        Middleware(\n            RequestModifierMiddleware,  # type: ignore[arg-type]\n            key=\"modified_by\",\n            value=\"middleware\",\n        )\n    ]\n\n    # Add a test route\n    additional_routes: list[BaseRoute] = [Route(\"/test\", endpoint_handler)]\n\n    # Create the app with custom middleware\n    app = create_sse_app(\n        server=server,\n        message_path=\"/message\",\n        sse_path=\"/sse/\",\n        middleware=custom_middleware,\n        routes=additional_routes,\n    )\n\n    # Create a test client\n    transport = ASGITransport(app=app)\n    async with httpx.AsyncClient(\n        transport=transport, base_url=\"http://testserver\"\n    ) as client:\n        response = await client.get(\"/test\")\n\n        # Verify middleware was applied\n        assert response.status_code == 200\n        data = response.json()\n        assert \"state\" in data\n        assert data[\"state\"][\"modified_by\"] == \"middleware\"\n\n\nasync def test_create_streamable_http_app_with_custom_middleware():\n    \"\"\"Test that custom middleware works with create_streamable_http_app function.\"\"\"\n    server = FastMCP(name=\"TestServer\")\n\n    # Create custom middleware\n    # TODO(ty): remove when Starlette Middleware typing is supported\n    custom_middleware = [\n        Middleware(\n            RequestModifierMiddleware,  # type: ignore[arg-type]\n            key=\"modified_by\",\n            value=\"middleware\",\n        )\n    ]\n\n    # Add a test route\n    additional_routes: list[BaseRoute] = [Route(\"/test\", endpoint_handler)]\n\n    # Create the app with custom middleware\n    app = create_streamable_http_app(\n        server=server,\n        streamable_http_path=\"/streamable\",\n        middleware=custom_middleware,\n        routes=additional_routes,\n    )\n\n    # Create a test client\n    transport = ASGITransport(app=app)\n    async with httpx.AsyncClient(\n        transport=transport, base_url=\"http://testserver\"\n    ) as client:\n        response = await client.get(\"/test\")\n\n        # Verify middleware was applied\n        assert response.status_code == 200\n        data = response.json()\n        assert \"state\" in data\n        assert data[\"state\"][\"modified_by\"] == \"middleware\"\n\n\nasync def test_multiple_middleware_ordering():\n    \"\"\"Test that multiple middleware are applied in the correct order.\"\"\"\n    server = FastMCP(name=\"TestServer\")\n\n    # Create multiple middleware\n    # TODO(ty): remove when Starlette Middleware typing is supported\n    custom_middleware = [\n        Middleware(\n            HeaderMiddleware,  # type: ignore[arg-type]\n            header_name=\"X-First-Header\",\n            header_value=\"first\",\n        ),\n        Middleware(\n            HeaderMiddleware,  # type: ignore[arg-type]\n            header_name=\"X-Second-Header\",\n            header_value=\"second\",\n        ),\n    ]\n\n    # Add a test route to server's additional routes\n    routes: list[BaseRoute] = [Route(\"/test\", endpoint_handler)]\n    server._additional_http_routes = routes\n\n    # Create the app with custom middleware\n    app = server.http_app(transport=\"sse\", middleware=custom_middleware)\n\n    # Create a test client\n    transport = ASGITransport(app=app)\n    async with httpx.AsyncClient(\n        transport=transport, base_url=\"http://testserver\"\n    ) as client:\n        response = await client.get(\"/test\")\n\n        # Verify both middleware were applied\n        assert response.status_code == 200\n        assert response.headers[\"X-First-Header\"] == \"first\"\n        assert response.headers[\"X-Second-Header\"] == \"second\"\n"
  },
  {
    "path": "tests/server/http/test_stale_access_token.py",
    "content": "\"\"\"\nTest for issue #1863: get_access_token() returns stale token after OAuth refresh.\n\nThis test demonstrates the bug where auth_context_var holds a stale token,\nbut the current HTTP request (via request_ctx) has a fresh token.\n\nThe test should FAIL with the current implementation and PASS after the fix.\n\"\"\"\n\nfrom unittest.mock import MagicMock\n\nfrom mcp.server.auth.middleware.auth_context import auth_context_var\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\nfrom mcp.server.lowlevel.server import request_ctx\nfrom mcp.shared.context import RequestContext\nfrom starlette.requests import Request\n\nfrom fastmcp.server.auth import AccessToken\nfrom fastmcp.server.dependencies import get_access_token\n\n\nclass TestStaleAccessToken:\n    \"\"\"Test that get_access_token returns fresh token from request scope.\"\"\"\n\n    def test_get_access_token_prefers_request_scope_over_stale_context_var(self):\n        \"\"\"\n        Regression test for issue #1863.\n\n        Scenario:\n        - auth_context_var has a STALE token (set at HTTP middleware level)\n        - request_ctx.request.scope[\"user\"] has a FRESH token (per MCP message)\n        - get_access_token() should return the FRESH token\n\n        This simulates the case where:\n        1. A Streamable HTTP session was established with token A\n        2. auth_context_var was set to token A during session setup\n        3. Token expired, client refreshed, got token B\n        4. New MCP message arrives with token B in the request\n        5. get_access_token() should return token B, not stale token A\n        \"\"\"\n        # Create STALE token (in auth_context_var)\n        # Using FastMCP's AccessToken to avoid conversion issues\n        stale_token = AccessToken(\n            token=\"stale-token-from-initial-auth\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n        )\n        stale_user = AuthenticatedUser(stale_token)\n\n        # Create FRESH token (in request.scope[\"user\"])\n        fresh_token = AccessToken(\n            token=\"fresh-token-after-refresh\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n        )\n        fresh_user = AuthenticatedUser(fresh_token)\n\n        # Create a mock request with fresh token in scope\n        scope = {\n            \"type\": \"http\",\n            \"user\": fresh_user,\n            \"auth\": MagicMock(),\n        }\n        mock_request = Request(scope)\n\n        # Create a mock RequestContext with the request\n        mock_request_context = MagicMock(spec=RequestContext)\n        mock_request_context.request = mock_request\n\n        # Set up the context vars:\n        # - auth_context_var has STALE token\n        # - request_ctx has request with FRESH token\n        auth_token = auth_context_var.set(stale_user)\n        request_token = request_ctx.set(mock_request_context)\n\n        try:\n            # Call get_access_token - should return FRESH token\n            result = get_access_token()\n\n            # Assert we get the FRESH token, not the stale one\n            assert result is not None, \"Expected an access token but got None\"\n            assert result.token == \"fresh-token-after-refresh\", (\n                f\"Expected fresh token 'fresh-token-after-refresh' but got '{result.token}'. \"\n                \"get_access_token() is returning the stale token from auth_context_var \"\n                \"instead of the fresh token from request.scope['user'].\"\n            )\n        finally:\n            # Clean up context vars\n            auth_context_var.reset(auth_token)\n            request_ctx.reset(request_token)\n\n    def test_get_access_token_falls_back_to_context_var_when_no_request(self):\n        \"\"\"\n        Verify that get_access_token falls back to auth_context_var\n        when there's no HTTP request available.\n        \"\"\"\n        # Create token in auth_context_var using FastMCP's AccessToken\n        token = AccessToken(\n            token=\"context-var-token\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n        )\n        user = AuthenticatedUser(token)\n\n        # Set up auth_context_var but NOT request_ctx\n        auth_token = auth_context_var.set(user)\n\n        try:\n            result = get_access_token()\n\n            assert result is not None\n            assert result.token == \"context-var-token\"\n        finally:\n            auth_context_var.reset(auth_token)\n\n    def test_get_access_token_returns_none_when_no_auth(self):\n        \"\"\"\n        Verify that get_access_token returns None when there's no\n        authenticated user anywhere.\n        \"\"\"\n        result = get_access_token()\n        assert result is None\n\n    def test_get_access_token_falls_back_when_scope_user_is_not_authenticated(self):\n        \"\"\"\n        Verify that get_access_token falls back to auth_context_var when\n        scope[\"user\"] exists but is not an AuthenticatedUser (e.g., UnauthenticatedUser).\n        \"\"\"\n        from starlette.authentication import UnauthenticatedUser\n\n        # Create token in auth_context_var\n        token = AccessToken(\n            token=\"context-var-token\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n        )\n        user = AuthenticatedUser(token)\n\n        # Create request with UnauthenticatedUser in scope\n        scope = {\n            \"type\": \"http\",\n            \"user\": UnauthenticatedUser(),\n        }\n        mock_request = Request(scope)\n        mock_request_context = MagicMock(spec=RequestContext)\n        mock_request_context.request = mock_request\n\n        auth_token = auth_context_var.set(user)\n        request_token = request_ctx.set(mock_request_context)\n\n        try:\n            result = get_access_token()\n\n            # Should fall back to auth_context_var since scope user is unauthenticated\n            assert result is not None\n            assert result.token == \"context-var-token\"\n        finally:\n            auth_context_var.reset(auth_token)\n            request_ctx.reset(request_token)\n"
  },
  {
    "path": "tests/server/middleware/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/middleware/test_caching.py",
    "content": "\"\"\"Tests for response caching middleware.\"\"\"\n\nimport sys\nimport tempfile\nimport warnings\nfrom pathlib import Path\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport mcp.types\nimport pytest\nfrom inline_snapshot import snapshot\nfrom key_value.aio.stores.filetree import (\n    FileTreeStore,\n    FileTreeV1CollectionSanitizationStrategy,\n    FileTreeV1KeySanitizationStrategy,\n)\nfrom key_value.aio.stores.memory import MemoryStore\nfrom key_value.aio.wrappers.statistics.wrapper import (\n    GetStatistics,\n    KVStoreCollectionStatistics,\n    PutStatistics,\n)\nfrom mcp.server.lowlevel.helper_types import ReadResourceContents\nfrom mcp.types import TextContent, TextResourceContents\nfrom pydantic import AnyUrl, BaseModel\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.client.client import CallToolResult, Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.prompts.base import Message, Prompt\nfrom fastmcp.prompts.function_prompt import FunctionPrompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.server.middleware.caching import (\n    CachableToolResult,\n    CallToolSettings,\n    ResponseCachingMiddleware,\n    ResponseCachingStatistics,\n    _make_call_tool_cache_key,\n    _make_get_prompt_cache_key,\n    _make_read_resource_cache_key,\n)\nfrom fastmcp.server.middleware.middleware import CallNext, MiddlewareContext\nfrom fastmcp.tools.base import Tool, ToolResult\n\nTEST_URI = AnyUrl(\"https://test_uri\")\n\nSAMPLE_READ_RESOURCE_CONTENTS = ReadResourceContents(\n    content=\"test_text\",\n    mime_type=\"text/plain\",\n)\n\n\ndef sample_resource_fn() -> list[ReadResourceContents]:\n    return [SAMPLE_READ_RESOURCE_CONTENTS]\n\n\ndef sample_prompt_fn() -> Message:\n    return Message(\"test_text\")\n\n\nSAMPLE_RESOURCE = Resource.from_function(\n    fn=sample_resource_fn, uri=TEST_URI, name=\"test_resource\"\n)\n\nSAMPLE_PROMPT = Prompt.from_function(fn=sample_prompt_fn, name=\"test_prompt\")\nSAMPLE_GET_PROMPT_RESULT = mcp.types.GetPromptResult(\n    messages=[Message(\"test_text\").to_mcp_prompt_message()]\n)\nSAMPLE_TOOL = Tool(name=\"test_tool\", parameters={\"param1\": \"value1\", \"param2\": 42})\nSAMPLE_TOOL_RESULT = ToolResult(\n    content=[TextContent(type=\"text\", text=\"test_text\")],\n    structured_content={\"result\": \"test_result\"},\n)\nSAMPLE_TOOL_RESULT_LARGE = ToolResult(\n    content=[TextContent(type=\"text\", text=\"test_text\" * 100)],\n    structured_content={\"result\": \"test_result\"},\n)\n\n\nclass CrazyModel(BaseModel):\n    a: int\n    b: int\n    c: str\n    d: float\n    e: bool\n    f: list[int]\n    g: dict[str, int]\n    h: list[dict[str, int]]\n    i: dict[str, list[int]]\n\n\n@pytest.fixture\ndef crazy_model() -> CrazyModel:\n    return CrazyModel(\n        a=5,\n        b=10,\n        c=\"test\",\n        d=1.0,\n        e=True,\n        f=[1, 2, 3],\n        g={\"a\": 1, \"b\": 2},\n        h=[{\"a\": 1, \"b\": 2}],\n        i={\"a\": [1, 2]},\n    )\n\n\nclass TrackingCalculator:\n    add_calls: int\n    multiply_calls: int\n    crazy_calls: int\n    very_large_response_calls: int\n\n    def __init__(self):\n        self.add_calls = 0\n        self.multiply_calls = 0\n        self.crazy_calls = 0\n        self.very_large_response_calls = 0\n\n    def add(self, a: int, b: int) -> int:\n        self.add_calls += 1\n        return a + b\n\n    def multiply(self, a: int, b: int) -> int:\n        self.multiply_calls += 1\n        return a * b\n\n    def very_large_response(self) -> str:\n        self.very_large_response_calls += 1\n        return \"istenchars\" * 100000  # 1,000,000 characters, 1mb\n\n    def crazy(self, a: CrazyModel) -> CrazyModel:\n        self.crazy_calls += 1\n        return a\n\n    def how_to_calculate(self, a: int, b: int) -> str:\n        return f\"To calculate {a} + {b}, you need to add {a} and {b} together.\"\n\n    def get_add_calls(self) -> str:\n        return str(self.add_calls)\n\n    def get_multiply_calls(self) -> str:\n        return str(self.multiply_calls)\n\n    def get_crazy_calls(self) -> str:\n        return str(self.crazy_calls)\n\n    async def update_tool_list(self, context: Context):\n        import mcp.types\n\n        await context.send_notification(mcp.types.ToolListChangedNotification())\n\n    def add_tools(self, fastmcp: FastMCP, prefix: str = \"\"):\n        _ = fastmcp.add_tool(tool=Tool.from_function(fn=self.add, name=f\"{prefix}add\"))\n        _ = fastmcp.add_tool(\n            tool=Tool.from_function(fn=self.multiply, name=f\"{prefix}multiply\")\n        )\n        _ = fastmcp.add_tool(\n            tool=Tool.from_function(fn=self.crazy, name=f\"{prefix}crazy\")\n        )\n        _ = fastmcp.add_tool(\n            tool=Tool.from_function(\n                fn=self.very_large_response, name=f\"{prefix}very_large_response\"\n            )\n        )\n        _ = fastmcp.add_tool(\n            tool=Tool.from_function(\n                fn=self.update_tool_list, name=f\"{prefix}update_tool_list\"\n            )\n        )\n\n    def add_prompts(self, fastmcp: FastMCP, prefix: str = \"\"):\n        _ = fastmcp.add_prompt(\n            prompt=FunctionPrompt.from_function(\n                fn=self.how_to_calculate, name=f\"{prefix}how_to_calculate\"\n            )\n        )\n\n    def add_resources(self, fastmcp: FastMCP, prefix: str = \"\"):\n        _ = fastmcp.add_resource(\n            resource=Resource.from_function(\n                fn=self.get_add_calls,\n                uri=\"resource://add_calls\",\n                name=f\"{prefix}add_calls\",\n            )\n        )\n        _ = fastmcp.add_resource(\n            resource=Resource.from_function(\n                fn=self.get_multiply_calls,\n                uri=\"resource://multiply_calls\",\n                name=f\"{prefix}multiply_calls\",\n            )\n        )\n        _ = fastmcp.add_resource(\n            resource=Resource.from_function(\n                fn=self.get_crazy_calls,\n                uri=\"resource://crazy_calls\",\n                name=f\"{prefix}crazy_calls\",\n            )\n        )\n\n\n@pytest.fixture\ndef tracking_calculator() -> TrackingCalculator:\n    return TrackingCalculator()\n\n\n@pytest.fixture\ndef mock_context() -> MiddlewareContext[mcp.types.CallToolRequestParams]:\n    \"\"\"Create a mock middleware context for tool calls.\"\"\"\n    context = MagicMock(spec=MiddlewareContext[mcp.types.CallToolRequestParams])\n    context.message = mcp.types.CallToolRequestParams(\n        name=\"test_tool\", arguments={\"param1\": \"value1\", \"param2\": 42}\n    )\n    context.method = \"tools/call\"\n    return context\n\n\n@pytest.fixture\ndef mock_call_next() -> CallNext[mcp.types.CallToolRequestParams, ToolResult]:\n    \"\"\"Create a mock call_next function.\"\"\"\n    return AsyncMock(\n        return_value=ToolResult(\n            content=[TextContent(type=\"text\", text=\"test result\")],\n            structured_content={\"result\": \"success\", \"value\": 123},\n        )\n    )\n\n\n@pytest.fixture\ndef sample_tool_result() -> ToolResult:\n    \"\"\"Create a sample tool result for testing.\"\"\"\n    return ToolResult(\n        content=[TextContent(type=\"text\", text=\"cached result\")],\n        structured_content={\"cached\": True, \"data\": \"test\"},\n    )\n\n\nclass TestResponseCachingMiddleware:\n    \"\"\"Test ResponseCachingMiddleware functionality.\"\"\"\n\n    def test_initialization(self):\n        \"\"\"Test middleware initialization.\"\"\"\n        assert ResponseCachingMiddleware(\n            call_tool_settings=CallToolSettings(\n                included_tools=[\"tool1\"],\n                excluded_tools=[\"tool2\"],\n            ),\n        )\n\n    @pytest.mark.parametrize(\n        (\"tool_name\", \"included_tools\", \"excluded_tools\", \"result\"),\n        [\n            (\"tool\", [\"tool\", \"tool2\"], [], True),\n            (\"tool\", [\"second tool\", \"third tool\"], [], False),\n            (\"tool\", [], [\"tool\"], False),\n            (\"tool\", [], [\"second tool\"], True),\n            (\"tool\", [\"tool\", \"second tool\"], [\"tool\"], False),\n            (\"tool\", [\"tool\", \"second tool\"], [\"second tool\"], True),\n        ],\n        ids=[\n            \"tool is included\",\n            \"tool is not included\",\n            \"tool is excluded\",\n            \"tool is not excluded\",\n            \"tool is included and excluded (excluded takes precedence)\",\n            \"tool is included and not excluded\",\n        ],\n    )\n    def test_tool_call_filtering(\n        self,\n        tool_name: str,\n        included_tools: list[str],\n        excluded_tools: list[str],\n        result: bool,\n    ):\n        \"\"\"Test tool filtering logic.\"\"\"\n\n        middleware1 = ResponseCachingMiddleware(\n            call_tool_settings=CallToolSettings(\n                included_tools=included_tools, excluded_tools=excluded_tools\n            ),\n        )\n        assert middleware1._matches_tool_cache_settings(tool_name=tool_name) is result\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\",\n    reason=\"SQLite caching tests are flaky on Windows due to temp directory issues.\",\n)\nclass TestResponseCachingMiddlewareIntegration:\n    \"\"\"Integration tests with real FastMCP server.\"\"\"\n\n    @pytest.fixture(params=[\"memory\", \"filetree\"])\n    async def caching_server(\n        self,\n        tracking_calculator: TrackingCalculator,\n        request: pytest.FixtureRequest,\n    ):\n        \"\"\"Create a FastMCP server for caching tests.\"\"\"\n        mcp = FastMCP(\"CachingTestServer\", dereference_schemas=False)\n\n        with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\", UserWarning)\n                file_store = FileTreeStore(\n                    data_directory=Path(temp_dir),\n                    key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(\n                        Path(temp_dir)\n                    ),\n                    collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(\n                        Path(temp_dir)\n                    ),\n                )\n            response_caching_middleware = ResponseCachingMiddleware(\n                cache_storage=file_store\n                if request.param == \"filetree\"\n                else MemoryStore(),\n            )\n\n            mcp.add_middleware(middleware=response_caching_middleware)\n\n            tracking_calculator.add_tools(fastmcp=mcp)\n            tracking_calculator.add_resources(fastmcp=mcp)\n            tracking_calculator.add_prompts(fastmcp=mcp)\n\n            yield mcp\n\n    @pytest.fixture\n    def non_caching_server(self, tracking_calculator: TrackingCalculator):\n        \"\"\"Create a FastMCP server for non-caching tests.\"\"\"\n        mcp = FastMCP(\"NonCachingTestServer\")\n        tracking_calculator.add_tools(fastmcp=mcp)\n        return mcp\n\n    async def test_list_tools(\n        self, caching_server: FastMCP, tracking_calculator: TrackingCalculator\n    ):\n        \"\"\"Test that tool list caching works with a real FastMCP server.\"\"\"\n\n        async with Client(caching_server) as client:\n            pre_tool_list: list[mcp.types.Tool] = await client.list_tools()\n            assert len(pre_tool_list) == 5\n\n            # Add a tool and make sure it's missing from the list tool response\n            _ = caching_server.add_tool(\n                tool=Tool.from_function(fn=tracking_calculator.add, name=\"add_2\")\n            )\n\n            post_tool_list: list[mcp.types.Tool] = await client.list_tools()\n            assert len(post_tool_list) == 5\n\n            assert pre_tool_list == post_tool_list\n\n    async def test_call_tool(\n        self,\n        caching_server: FastMCP,\n        tracking_calculator: TrackingCalculator,\n    ):\n        \"\"\"Test that caching works with a real FastMCP server.\"\"\"\n        tracking_calculator.add_tools(fastmcp=caching_server)\n\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            call_tool_result_one: CallToolResult = await client.call_tool(\n                \"add\", {\"a\": 5, \"b\": 3}\n            )\n\n            assert tracking_calculator.add_calls == 1\n            call_tool_result_two: CallToolResult = await client.call_tool(\n                \"add\", {\"a\": 5, \"b\": 3}\n            )\n            assert call_tool_result_one == call_tool_result_two\n\n    async def test_call_tool_very_large_value(\n        self,\n        caching_server: FastMCP,\n        tracking_calculator: TrackingCalculator,\n    ):\n        \"\"\"Test that caching works with a real FastMCP server.\"\"\"\n        tracking_calculator.add_tools(fastmcp=caching_server)\n\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            call_tool_result_one: CallToolResult = await client.call_tool(\n                \"very_large_response\", {}\n            )\n\n            assert tracking_calculator.very_large_response_calls == 1\n            call_tool_result_two: CallToolResult = await client.call_tool(\n                \"very_large_response\", {}\n            )\n            assert call_tool_result_one == call_tool_result_two\n            assert tracking_calculator.very_large_response_calls == 2\n\n    async def test_call_tool_crazy_value(\n        self,\n        caching_server: FastMCP,\n        tracking_calculator: TrackingCalculator,\n        crazy_model: CrazyModel,\n    ):\n        \"\"\"Test that caching works with a real FastMCP server.\"\"\"\n        tracking_calculator.add_tools(fastmcp=caching_server)\n\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            call_tool_result_one: CallToolResult = await client.call_tool(\n                \"crazy\", {\"a\": crazy_model}\n            )\n\n            assert tracking_calculator.crazy_calls == 1\n            call_tool_result_two: CallToolResult = await client.call_tool(\n                \"crazy\", {\"a\": crazy_model}\n            )\n            assert call_tool_result_one == call_tool_result_two\n            assert tracking_calculator.crazy_calls == 1\n\n    async def test_list_resources(\n        self, caching_server: FastMCP, tracking_calculator: TrackingCalculator\n    ):\n        \"\"\"Test that list resources caching works with a real FastMCP server.\"\"\"\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            pre_resource_list: list[mcp.types.Resource] = await client.list_resources()\n\n            assert len(pre_resource_list) == 3\n\n            tracking_calculator.add_resources(fastmcp=caching_server)\n\n            post_resource_list: list[mcp.types.Resource] = await client.list_resources()\n            assert len(post_resource_list) == 3\n\n            assert pre_resource_list == post_resource_list\n\n    async def test_read_resource(\n        self, caching_server: FastMCP, tracking_calculator: TrackingCalculator\n    ):\n        \"\"\"Test that get resources caching works with a real FastMCP server.\"\"\"\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            pre_resource = await client.read_resource(uri=\"resource://add_calls\")\n            assert isinstance(pre_resource[0], TextResourceContents)\n            assert pre_resource[0].text == \"0\"\n\n            tracking_calculator.add_calls = 1\n\n            post_resource = await client.read_resource(uri=\"resource://add_calls\")\n            assert isinstance(post_resource[0], TextResourceContents)\n            assert post_resource[0].text == \"0\"\n            assert pre_resource == post_resource\n\n    async def test_list_prompts(\n        self, caching_server: FastMCP, tracking_calculator: TrackingCalculator\n    ):\n        \"\"\"Test that list prompts caching works with a real FastMCP server.\"\"\"\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            pre_prompt_list: list[mcp.types.Prompt] = await client.list_prompts()\n\n            assert len(pre_prompt_list) == 1\n\n            tracking_calculator.add_prompts(fastmcp=caching_server)\n\n            post_prompt_list: list[mcp.types.Prompt] = await client.list_prompts()\n\n            assert len(post_prompt_list) == 1\n\n            assert pre_prompt_list == post_prompt_list\n\n    async def test_get_prompts(\n        self, caching_server: FastMCP, tracking_calculator: TrackingCalculator\n    ):\n        \"\"\"Test that get prompts caching works with a real FastMCP server.\"\"\"\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            pre_prompt = await client.get_prompt(\n                name=\"how_to_calculate\", arguments={\"a\": 5, \"b\": 3}\n            )\n\n            pre_prompt_content = pre_prompt.messages[0].content\n            assert isinstance(pre_prompt_content, TextContent)\n            assert (\n                pre_prompt_content.text\n                == \"To calculate 5 + 3, you need to add 5 and 3 together.\"\n            )\n\n            tracking_calculator.add_prompts(fastmcp=caching_server)\n\n            post_prompt = await client.get_prompt(\n                name=\"how_to_calculate\", arguments={\"a\": 5, \"b\": 3}\n            )\n\n            assert pre_prompt == post_prompt\n\n    async def test_statistics(\n        self,\n        caching_server: FastMCP,\n    ):\n        \"\"\"Test that statistics are collected correctly.\"\"\"\n        caching_middleware = caching_server.middleware[0]\n        assert isinstance(caching_middleware, ResponseCachingMiddleware)\n\n        async with Client[FastMCPTransport](transport=caching_server) as client:\n            statistics = caching_middleware.statistics()\n            assert statistics == snapshot(ResponseCachingStatistics())\n\n            _ = await client.call_tool(\"add\", {\"a\": 5, \"b\": 3})\n\n            statistics = caching_middleware.statistics()\n            assert statistics == snapshot(\n                ResponseCachingStatistics(\n                    list_tools=KVStoreCollectionStatistics(\n                        get=GetStatistics(count=2, hit=1, miss=1),\n                        put=PutStatistics(count=1),\n                    ),\n                    call_tool=KVStoreCollectionStatistics(\n                        get=GetStatistics(count=1, miss=1), put=PutStatistics(count=1)\n                    ),\n                )\n            )\n\n            _ = await client.call_tool(\"add\", {\"a\": 5, \"b\": 3})\n\n            statistics = caching_middleware.statistics()\n            assert statistics == snapshot(\n                ResponseCachingStatistics(\n                    list_tools=KVStoreCollectionStatistics(\n                        get=GetStatistics(count=2, hit=1, miss=1),\n                        put=PutStatistics(count=1),\n                    ),\n                    call_tool=KVStoreCollectionStatistics(\n                        get=GetStatistics(count=2, hit=1, miss=1),\n                        put=PutStatistics(count=1),\n                    ),\n                )\n            )\n\n\nclass TestCachableToolResult:\n    def test_wrap_and_unwrap(self):\n        tool_result = ToolResult(\n            \"unstructured content\",\n            structured_content={\"structured\": \"content\"},\n            meta={\"meta\": \"data\"},\n        )\n\n        cached_tool_result = CachableToolResult.wrap(tool_result).unwrap()\n\n        assert cached_tool_result.content == tool_result.content\n        assert cached_tool_result.structured_content == tool_result.structured_content\n        assert cached_tool_result.meta == tool_result.meta\n\n\nclass TestCachingWithImportedServerPrefixes:\n    \"\"\"Test that caching preserves prefixes from imported servers.\n\n    Regression tests for issue #2300: ResponseCachingMiddleware was losing\n    prefix information when caching components from imported servers.\n    \"\"\"\n\n    @pytest.fixture\n    async def parent_with_imported_child(self, tracking_calculator: TrackingCalculator):\n        \"\"\"Create a parent server with an imported child server (prefixed).\"\"\"\n        child = FastMCP(\"child\")\n        tracking_calculator.add_tools(fastmcp=child)\n        tracking_calculator.add_resources(fastmcp=child)\n        tracking_calculator.add_prompts(fastmcp=child)\n\n        parent = FastMCP(\"parent\")\n        parent.add_middleware(ResponseCachingMiddleware())\n        parent.mount(child, namespace=\"child\")\n\n        return parent\n\n    async def test_tool_prefixes_preserved_after_cache_hit(\n        self, parent_with_imported_child: FastMCP\n    ):\n        \"\"\"Tool names should retain prefix after being served from cache.\"\"\"\n        async with Client(parent_with_imported_child) as client:\n            # First call populates cache\n            tools_first = await client.list_tools()\n            tool_names_first = [t.name for t in tools_first]\n\n            # Second call should come from cache\n            tools_cached = await client.list_tools()\n            tool_names_cached = [t.name for t in tools_cached]\n\n            # All tools should have prefix in both calls\n            assert all(name.startswith(\"child_\") for name in tool_names_first)\n            assert all(name.startswith(\"child_\") for name in tool_names_cached)\n            assert tool_names_first == tool_names_cached\n\n    async def test_resource_prefixes_preserved_after_cache_hit(\n        self, parent_with_imported_child: FastMCP\n    ):\n        \"\"\"Resource URIs should retain prefix after being served from cache.\"\"\"\n        async with Client(parent_with_imported_child) as client:\n            # First call populates cache\n            resources_first = await client.list_resources()\n            resource_uris_first = [str(r.uri) for r in resources_first]\n\n            # Second call should come from cache\n            resources_cached = await client.list_resources()\n            resource_uris_cached = [str(r.uri) for r in resources_cached]\n\n            # All resources should have prefix in URI path in both calls\n            # Resources get path-style prefix: resource://child/path\n            assert all(\"://child/\" in uri for uri in resource_uris_first)\n            assert all(\"://child/\" in uri for uri in resource_uris_cached)\n            assert resource_uris_first == resource_uris_cached\n\n    async def test_prompt_prefixes_preserved_after_cache_hit(\n        self, parent_with_imported_child: FastMCP\n    ):\n        \"\"\"Prompt names should retain prefix after being served from cache.\"\"\"\n        async with Client(parent_with_imported_child) as client:\n            # First call populates cache\n            prompts_first = await client.list_prompts()\n            prompt_names_first = [p.name for p in prompts_first]\n\n            # Second call should come from cache\n            prompts_cached = await client.list_prompts()\n            prompt_names_cached = [p.name for p in prompts_cached]\n\n            # All prompts should have prefix in both calls\n            assert all(name.startswith(\"child_\") for name in prompt_names_first)\n            assert all(name.startswith(\"child_\") for name in prompt_names_cached)\n            assert prompt_names_first == prompt_names_cached\n\n    async def test_prefixed_tool_callable_after_cache_hit(\n        self,\n        parent_with_imported_child: FastMCP,\n        tracking_calculator: TrackingCalculator,\n    ):\n        \"\"\"Prefixed tools should be callable after cache populates.\"\"\"\n        async with Client(parent_with_imported_child) as client:\n            # Trigger cache population\n            await client.list_tools()\n            await client.list_tools()  # From cache\n\n            # Tool should be callable with prefixed name\n            result = await client.call_tool(\"child_add\", {\"a\": 5, \"b\": 3})\n            assert not result.is_error\n            assert tracking_calculator.add_calls == 1\n\n\nclass TestCacheKeyGeneration:\n    def test_call_tool_key_is_hashed_and_does_not_include_raw_input(self):\n        msg = mcp.types.CallToolRequestParams(\n            name=\"toolX\",\n            arguments={\"password\": \"secret\", \"path\": \"../../etc/passwd\"},\n        )\n\n        key = _make_call_tool_cache_key(msg)\n\n        assert len(key) == 64\n        assert \"secret\" not in key\n        assert \"../../etc/passwd\" not in key\n\n    def test_read_resource_key_is_hashed_and_does_not_include_raw_uri(self):\n        msg = mcp.types.ReadResourceRequestParams(\n            uri=AnyUrl(\"file:///tmp/../../etc/shadow?token=abcd\")\n        )\n\n        key = _make_read_resource_cache_key(msg)\n\n        assert len(key) == 64\n        assert \"shadow\" not in key\n        assert \"token=abcd\" not in key\n\n    def test_get_prompt_key_is_hashed_and_stable(self):\n        msg = mcp.types.GetPromptRequestParams(\n            name=\"promptY\",\n            arguments={\"api_key\": \"ABC123\", \"scope\": \"admin\"},\n        )\n\n        key = _make_get_prompt_cache_key(msg)\n\n        assert len(key) == 64\n        assert \"ABC123\" not in key\n        assert key == _make_get_prompt_cache_key(msg)\n"
  },
  {
    "path": "tests/server/middleware/test_dereference.py",
    "content": "\"\"\"Tests for DereferenceRefsMiddleware.\"\"\"\n\nfrom enum import Enum\n\nimport pydantic\n\nfrom fastmcp import Client, FastMCP\n\n\nclass Color(Enum):\n    RED = \"red\"\n    GREEN = \"green\"\n    BLUE = \"blue\"\n\n\nclass PaintRequest(pydantic.BaseModel):\n    color: Color\n    opacity: float = 1.0\n\n\nclass TestDereferenceRefsMiddleware:\n    \"\"\"End-to-end tests for the dereference_schemas server kwarg.\"\"\"\n\n    async def test_dereference_schemas_true_inlines_refs(self):\n        \"\"\"With dereference_schemas=True (default), tool schemas have $ref inlined.\"\"\"\n        mcp = FastMCP(\"test\", dereference_schemas=True)\n\n        @mcp.tool\n        def paint(request: PaintRequest) -> str:\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n\n        schema = tools[0].inputSchema\n        # $defs should be removed — everything inlined\n        assert \"$defs\" not in schema\n        # The Color enum should be inlined into the request property\n        assert \"$ref\" not in str(schema)\n\n    async def test_dereference_schemas_false_preserves_refs(self):\n        \"\"\"With dereference_schemas=False, $ref and $defs are preserved.\"\"\"\n        mcp = FastMCP(\"test\", dereference_schemas=False)\n\n        @mcp.tool\n        def paint(request: PaintRequest) -> str:\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n\n        schema = tools[0].inputSchema\n        # $defs should still be present\n        assert \"$defs\" in schema\n\n    async def test_default_is_true(self):\n        \"\"\"Default behavior dereferences $ref.\"\"\"\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def paint(request: PaintRequest) -> str:\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n\n        schema = tools[0].inputSchema\n        assert \"$defs\" not in schema\n\n    async def test_does_not_mutate_original_tool(self):\n        \"\"\"Middleware should not mutate the shared Tool object.\"\"\"\n        mcp = FastMCP(\"test\", dereference_schemas=True)\n\n        @mcp.tool\n        def paint(request: PaintRequest) -> str:\n            return \"ok\"\n\n        # Get the original tool's parameters before middleware runs\n        original_tools = await mcp._local_provider._list_tools()\n        assert \"$defs\" in original_tools[0].parameters\n\n        # List tools through the client (triggers middleware)\n        async with Client(mcp) as client:\n            await client.list_tools()\n\n        # The original tool stored in the server should still have $defs\n        tools_after = await mcp._local_provider._list_tools()\n        assert \"$defs\" in tools_after[0].parameters\n\n    async def test_output_schema_dereferenced(self):\n        \"\"\"Middleware also dereferences output_schema when present.\"\"\"\n        mcp = FastMCP(\"test\", dereference_schemas=True)\n\n        @mcp.tool\n        def paint(request: PaintRequest) -> PaintRequest:\n            return request\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n\n        tool = tools[0]\n        # Both input and output schemas should be dereferenced\n        assert \"$defs\" not in tool.inputSchema\n        if tool.outputSchema is not None:\n            assert \"$defs\" not in tool.outputSchema\n\n    async def test_resource_templates_dereferenced(self):\n        \"\"\"Middleware dereferences resource template schemas.\"\"\"\n        mcp = FastMCP(\"test\", dereference_schemas=True)\n\n        @mcp.resource(\"paint://{color}\")\n        def get_paint(color: Color) -> str:\n            return f\"paint: {color}\"\n\n        async with Client(mcp) as client:\n            templates = await client.list_resource_templates()\n\n        # Resource templates also get their schemas dereferenced\n        # (only if the template parameters have $ref)\n        assert len(templates) == 1\n\n    async def test_no_ref_schemas_unchanged(self):\n        \"\"\"Tools without $ref should pass through unmodified.\"\"\"\n        mcp = FastMCP(\"test\", dereference_schemas=True)\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n\n        schema = tools[0].inputSchema\n        # Simple schema should not have $defs regardless\n        assert \"$defs\" not in schema\n        assert schema[\"properties\"][\"a\"][\"type\"] == \"integer\"\n"
  },
  {
    "path": "tests/server/middleware/test_error_handling.py",
    "content": "\"\"\"Tests for error handling middleware.\"\"\"\n\nimport logging\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport pytest\nfrom mcp import McpError\n\nfrom fastmcp.exceptions import NotFoundError, ToolError\nfrom fastmcp.server.middleware.error_handling import (\n    ErrorHandlingMiddleware,\n    RetryMiddleware,\n)\nfrom fastmcp.server.middleware.middleware import MiddlewareContext\n\n\n@pytest.fixture\ndef mock_context():\n    \"\"\"Create a mock middleware context.\"\"\"\n    context = MagicMock(spec=MiddlewareContext)\n    context.method = \"test_method\"\n    return context\n\n\n@pytest.fixture\ndef mock_call_next():\n    \"\"\"Create a mock call_next function.\"\"\"\n    return AsyncMock(return_value=\"test_result\")\n\n\nclass TestErrorHandlingMiddleware:\n    \"\"\"Test error handling middleware functionality.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        assert middleware.logger.name == \"fastmcp.errors\"\n        assert middleware.include_traceback is False\n        assert middleware.error_callback is None\n        assert middleware.transform_errors is True\n        assert middleware.error_counts == {}\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n        logger = logging.getLogger(\"custom\")\n        callback = MagicMock()\n\n        middleware = ErrorHandlingMiddleware(\n            logger=logger,\n            include_traceback=True,\n            error_callback=callback,\n            transform_errors=False,\n        )\n        assert middleware.logger is logger\n        assert middleware.include_traceback is True\n        assert middleware.error_callback is callback\n        assert middleware.transform_errors is False\n\n    def test_log_error_basic(self, mock_context, caplog):\n        \"\"\"Test basic error logging.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        error = ValueError(\"test error\")\n\n        with caplog.at_level(logging.ERROR):\n            middleware._log_error(error, mock_context)\n\n        assert \"Error in test_method: ValueError: test error\" in caplog.text\n        assert \"ValueError:test_method\" in middleware.error_counts\n        assert middleware.error_counts[\"ValueError:test_method\"] == 1\n\n    def test_log_error_with_traceback(self, mock_context, caplog):\n        \"\"\"Test error logging with traceback.\"\"\"\n        middleware = ErrorHandlingMiddleware(include_traceback=True)\n        error = ValueError(\"test error\")\n\n        with caplog.at_level(logging.ERROR):\n            middleware._log_error(error, mock_context)\n\n        assert \"Error in test_method: ValueError: test error\" in caplog.text\n        # The traceback is added to the log message\n        assert \"Error in test_method: ValueError: test error\" in caplog.text\n\n    def test_log_error_with_callback(self, mock_context):\n        \"\"\"Test error logging with callback.\"\"\"\n        callback = MagicMock()\n        middleware = ErrorHandlingMiddleware(error_callback=callback)\n        error = ValueError(\"test error\")\n\n        middleware._log_error(error, mock_context)\n\n        callback.assert_called_once_with(error, mock_context)\n\n    def test_log_error_callback_exception(self, mock_context, caplog):\n        \"\"\"Test error logging when callback raises exception.\"\"\"\n        callback = MagicMock(side_effect=RuntimeError(\"callback error\"))\n        middleware = ErrorHandlingMiddleware(error_callback=callback)\n        error = ValueError(\"test error\")\n\n        with caplog.at_level(logging.ERROR):\n            middleware._log_error(error, mock_context)\n\n        assert \"Error in error callback: callback error\" in caplog.text\n\n    def test_transform_error_mcp_error(self, mock_context):\n        \"\"\"Test that MCP errors are not transformed.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        from mcp.types import ErrorData\n\n        error = McpError(ErrorData(code=-32001, message=\"test error\"))\n\n        result = middleware._transform_error(error, mock_context)\n\n        assert result is error\n\n    def test_transform_error_disabled(self, mock_context):\n        \"\"\"Test error transformation when disabled.\"\"\"\n        middleware = ErrorHandlingMiddleware(transform_errors=False)\n        error = ValueError(\"test error\")\n\n        result = middleware._transform_error(error, mock_context)\n\n        assert result is error\n\n    def test_transform_error_value_error(self, mock_context):\n        \"\"\"Test transforming ValueError.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        error = ValueError(\"test error\")\n\n        result = middleware._transform_error(error, mock_context)\n\n        assert isinstance(result, McpError)\n        assert result.error.code == -32602\n        assert \"Invalid params: test error\" in result.error.message\n\n    def test_transform_error_not_found_for_resource_method(self):\n        \"\"\"Test that not-found errors use -32002 for resource methods.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        resource_context = MagicMock(spec=MiddlewareContext)\n        resource_context.method = \"resources/read\"\n\n        for error in [\n            FileNotFoundError(\"test error\"),\n            NotFoundError(\"test error\"),\n        ]:\n            result = middleware._transform_error(error, resource_context)\n\n            assert isinstance(result, McpError)\n            assert result.error.code == -32002\n            assert \"Resource not found: test error\" in result.error.message\n\n    def test_transform_error_not_found_for_non_resource_method(self, mock_context):\n        \"\"\"Test that not-found errors use -32001 for non-resource methods.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n\n        for error in [\n            FileNotFoundError(\"test error\"),\n            NotFoundError(\"test error\"),\n        ]:\n            result = middleware._transform_error(error, mock_context)\n\n            assert isinstance(result, McpError)\n            assert result.error.code == -32001\n            assert \"Not found: test error\" in result.error.message\n\n    def test_transform_error_permission_error(self, mock_context):\n        \"\"\"Test transforming PermissionError.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        error = PermissionError(\"test error\")\n\n        result = middleware._transform_error(error, mock_context)\n\n        assert isinstance(result, McpError)\n        assert result.error.code == -32000\n        assert \"Permission denied: test error\" in result.error.message\n\n    def test_transform_error_timeout_error(self, mock_context):\n        \"\"\"Test transforming TimeoutError.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        error = TimeoutError(\"test error\")\n\n        result = middleware._transform_error(error, mock_context)\n\n        assert isinstance(result, McpError)\n        assert result.error.code == -32000\n        assert \"Request timeout: test error\" in result.error.message\n\n    def test_transform_error_generic(self, mock_context):\n        \"\"\"Test transforming generic error.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        error = RuntimeError(\"test error\")\n\n        result = middleware._transform_error(error, mock_context)\n\n        assert isinstance(result, McpError)\n        assert result.error.code == -32603\n        assert \"Internal error: test error\" in result.error.message\n\n    async def test_on_message_success(self, mock_context, mock_call_next):\n        \"\"\"Test successful message handling.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.called\n\n    async def test_on_message_error_transform(self, mock_context, caplog):\n        \"\"\"Test error handling with transformation.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        mock_call_next = AsyncMock(side_effect=ValueError(\"test error\"))\n\n        with caplog.at_level(logging.ERROR):\n            with pytest.raises(McpError) as exc_info:\n                await middleware.on_message(mock_context, mock_call_next)\n\n        assert isinstance(exc_info.value, McpError)\n        assert exc_info.value.error.code == -32602\n        assert \"Invalid params: test error\" in exc_info.value.error.message\n        assert \"Error in test_method: ValueError: test error\" in caplog.text\n\n    async def test_on_message_error_transform_tool_error(self, mock_context, caplog):\n        \"\"\"Test error handling with transformation and cause type.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        tool_error = ToolError(\"test error\")\n        tool_error.__cause__ = ValueError()\n        mock_call_next = AsyncMock(side_effect=tool_error)\n\n        with caplog.at_level(logging.ERROR):\n            with pytest.raises(McpError) as exc_info:\n                await middleware.on_message(mock_context, mock_call_next)\n\n        assert isinstance(exc_info.value, McpError)\n        assert exc_info.value.error.code == -32602\n        assert \"Invalid params: test error\" in exc_info.value.error.message\n        assert \"Error in test_method: ToolError: test error\" in caplog.text\n\n    def test_get_error_stats(self, mock_context):\n        \"\"\"Test getting error statistics.\"\"\"\n        middleware = ErrorHandlingMiddleware()\n        error1 = ValueError(\"error1\")\n        error2 = ValueError(\"error2\")\n        error3 = RuntimeError(\"error3\")\n\n        middleware._log_error(error1, mock_context)\n        middleware._log_error(error2, mock_context)\n        middleware._log_error(error3, mock_context)\n\n        stats = middleware.get_error_stats()\n        assert stats[\"ValueError:test_method\"] == 2\n        assert stats[\"RuntimeError:test_method\"] == 1\n\n\nclass TestRetryMiddleware:\n    \"\"\"Test retry middleware functionality.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = RetryMiddleware()\n        assert middleware.max_retries == 3\n        assert middleware.base_delay == 1.0\n        assert middleware.max_delay == 60.0\n        assert middleware.backoff_multiplier == 2.0\n        assert middleware.retry_exceptions == (ConnectionError, TimeoutError)\n        assert middleware.logger.name == \"fastmcp.retry\"\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n        logger = logging.getLogger(\"custom\")\n        middleware = RetryMiddleware(\n            max_retries=5,\n            base_delay=2.0,\n            max_delay=120.0,\n            backoff_multiplier=3.0,\n            retry_exceptions=(ValueError, RuntimeError),\n            logger=logger,\n        )\n        assert middleware.max_retries == 5\n        assert middleware.base_delay == 2.0\n        assert middleware.max_delay == 120.0\n        assert middleware.backoff_multiplier == 3.0\n        assert middleware.retry_exceptions == (ValueError, RuntimeError)\n        assert middleware.logger is logger\n\n    def test_should_retry_true(self):\n        \"\"\"Test retry decision for retryable errors.\"\"\"\n        middleware = RetryMiddleware()\n\n        assert middleware._should_retry(ConnectionError()) is True\n        assert middleware._should_retry(TimeoutError()) is True\n\n    def test_should_retry_false(self):\n        \"\"\"Test retry decision for non-retryable errors.\"\"\"\n        middleware = RetryMiddleware()\n\n        assert middleware._should_retry(ValueError()) is False\n        assert middleware._should_retry(RuntimeError()) is False\n\n    def test_calculate_delay(self):\n        \"\"\"Test delay calculation.\"\"\"\n        middleware = RetryMiddleware(\n            base_delay=1.0, backoff_multiplier=2.0, max_delay=10.0\n        )\n\n        assert middleware._calculate_delay(0) == 1.0\n        assert middleware._calculate_delay(1) == 2.0\n        assert middleware._calculate_delay(2) == 4.0\n        assert middleware._calculate_delay(3) == 8.0\n        assert middleware._calculate_delay(4) == 10.0  # capped at max_delay\n\n    async def test_on_request_success_first_try(self, mock_context, mock_call_next):\n        \"\"\"Test successful request on first try.\"\"\"\n        middleware = RetryMiddleware()\n\n        result = await middleware.on_request(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.call_count == 1\n\n    async def test_on_request_success_after_retries(self, mock_context, caplog):\n        \"\"\"Test successful request after retries.\"\"\"\n        middleware = RetryMiddleware(base_delay=0.01)  # Fast retry for testing\n\n        # Fail first two attempts, succeed on third\n        mock_call_next = AsyncMock(\n            side_effect=[\n                ConnectionError(\"connection failed\"),\n                ConnectionError(\"connection failed\"),\n                \"test_result\",\n            ]\n        )\n\n        with caplog.at_level(logging.WARNING):\n            result = await middleware.on_request(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.call_count == 3\n        assert \"Retrying in\" in caplog.text\n\n    async def test_on_request_max_retries_exceeded(self, mock_context, caplog):\n        \"\"\"Test request failing after max retries.\"\"\"\n        middleware = RetryMiddleware(max_retries=2, base_delay=0.01)\n\n        # Fail all attempts\n        mock_call_next = AsyncMock(side_effect=ConnectionError(\"connection failed\"))\n\n        with caplog.at_level(logging.WARNING):\n            with pytest.raises(ConnectionError):\n                await middleware.on_request(mock_context, mock_call_next)\n\n        assert mock_call_next.call_count == 3  # initial + 2 retries\n        assert \"Retrying in\" in caplog.text\n\n    async def test_on_request_non_retryable_error(self, mock_context):\n        \"\"\"Test non-retryable error is not retried.\"\"\"\n        middleware = RetryMiddleware()\n        mock_call_next = AsyncMock(side_effect=ValueError(\"non-retryable\"))\n\n        with pytest.raises(ValueError):\n            await middleware.on_request(mock_context, mock_call_next)\n\n        assert mock_call_next.call_count == 1  # No retries\n\n\n@pytest.fixture\ndef error_handling_server():\n    \"\"\"Create a FastMCP server specifically for error handling middleware tests.\"\"\"\n    from fastmcp import FastMCP\n\n    mcp = FastMCP(\"ErrorHandlingTestServer\")\n\n    @mcp.tool\n    def reliable_operation(data: str) -> str:\n        \"\"\"A reliable operation that always succeeds.\"\"\"\n        return f\"Success: {data}\"\n\n    @mcp.tool\n    def failing_operation(error_type: str = \"value\") -> str:\n        \"\"\"An operation that fails with different error types.\"\"\"\n        if error_type == \"value\":\n            raise ValueError(\"Value error occurred\")\n        elif error_type == \"file\":\n            raise FileNotFoundError(\"File not found\")\n        elif error_type == \"permission\":\n            raise PermissionError(\"Permission denied\")\n        elif error_type == \"timeout\":\n            raise TimeoutError(\"Operation timed out\")\n        elif error_type == \"generic\":\n            raise RuntimeError(\"Generic runtime error\")\n        else:\n            return \"Operation completed\"\n\n    @mcp.tool\n    def intermittent_operation(fail_rate: float = 0.5) -> str:\n        \"\"\"An operation that fails intermittently.\"\"\"\n        import random\n\n        if random.random() < fail_rate:\n            raise ConnectionError(\"Random connection failure\")\n        return \"Operation succeeded\"\n\n    @mcp.tool\n    def retryable_operation(attempt_count: int = 0) -> str:\n        \"\"\"An operation that succeeds after a few attempts.\"\"\"\n        # This is a simple way to simulate retry behavior\n        # In a real scenario, you might use external state\n        if attempt_count < 2:\n            raise ConnectionError(\"Temporary connection error\")\n        return \"Operation succeeded after retries\"\n\n    return mcp\n\n\nclass TestErrorHandlingMiddlewareIntegration:\n    \"\"\"Integration tests for error handling middleware with real FastMCP server.\"\"\"\n\n    async def test_error_handling_middleware_logs_real_errors(\n        self, error_handling_server, caplog\n    ):\n        \"\"\"Test that error handling middleware logs real errors from tools.\"\"\"\n        from fastmcp.client import Client\n\n        error_handling_server.add_middleware(ErrorHandlingMiddleware())\n\n        with caplog.at_level(logging.ERROR):\n            async with Client(error_handling_server) as client:\n                # Test different types of errors\n                with pytest.raises(Exception):\n                    await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n                with pytest.raises(Exception):\n                    await client.call_tool(\"failing_operation\", {\"error_type\": \"file\"})\n\n        log_text = caplog.text\n\n        # Should have error logs for both failures\n        assert \"Error in tools/call: ToolError:\" in log_text\n        # Should have captured both error instances\n        error_count = log_text.count(\"Error in tools/call:\")\n        assert error_count == 2\n\n    async def test_error_handling_middleware_tracks_error_statistics(\n        self, error_handling_server\n    ):\n        \"\"\"Test that error handling middleware accurately tracks error statistics.\"\"\"\n        from fastmcp.client import Client\n\n        error_middleware = ErrorHandlingMiddleware()\n        error_handling_server.add_middleware(error_middleware)\n\n        async with Client(error_handling_server) as client:\n            # Generate different types of errors\n            for _ in range(3):\n                with pytest.raises(Exception):\n                    await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n            for _ in range(2):\n                with pytest.raises(Exception):\n                    await client.call_tool(\"failing_operation\", {\"error_type\": \"file\"})\n\n        # Try some intermittent operations (some may succeed)\n        for _ in range(5):\n            try:\n                await client.call_tool(\"intermittent_operation\", {\"fail_rate\": 0.8})\n            except Exception:\n                pass  # Expected failures\n\n        # Check error statistics\n        stats = error_middleware.get_error_stats()\n\n        # Should have tracked the ToolError wrapper\n        assert \"ToolError:tools/call\" in stats\n        assert stats[\"ToolError:tools/call\"] >= 5  # At least the 5 deliberate failures\n\n    async def test_error_handling_middleware_with_success_and_failure(\n        self, error_handling_server, caplog\n    ):\n        \"\"\"Test error handling middleware with mix of successful and failed operations.\"\"\"\n        from fastmcp.client import Client\n\n        error_handling_server.add_middleware(ErrorHandlingMiddleware())\n\n        with caplog.at_level(logging.ERROR):\n            async with Client(error_handling_server) as client:\n                # Successful operation (should not generate error logs)\n                await client.call_tool(\"reliable_operation\", {\"data\": \"test\"})\n\n                # Failed operation (should generate error log)\n                with pytest.raises(Exception):\n                    await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n                # Another successful operation\n                await client.call_tool(\"reliable_operation\", {\"data\": \"test2\"})\n\n        log_text = caplog.text\n\n        # Should only have one error log (for the failed operation)\n        error_count = log_text.count(\"Error in tools/call:\")\n        assert error_count == 1\n\n    async def test_error_handling_middleware_custom_callback(\n        self, error_handling_server\n    ):\n        \"\"\"Test error handling middleware with custom error callback.\"\"\"\n        from fastmcp.client import Client\n\n        captured_errors = []\n\n        def error_callback(error, context):\n            captured_errors.append(\n                {\n                    \"error_type\": type(error).__name__,\n                    \"message\": str(error),\n                    \"method\": context.method,\n                }\n            )\n\n        error_handling_server.add_middleware(\n            ErrorHandlingMiddleware(error_callback=error_callback)\n        )\n\n        async with Client(error_handling_server) as client:\n            # Generate some errors\n            with pytest.raises(Exception):\n                await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n            with pytest.raises(Exception):\n                await client.call_tool(\"failing_operation\", {\"error_type\": \"timeout\"})\n\n        # Check that callback was called\n        assert len(captured_errors) == 2\n        assert captured_errors[0][\"error_type\"] == \"ToolError\"\n        assert captured_errors[1][\"error_type\"] == \"ToolError\"\n        assert all(error[\"method\"] == \"tools/call\" for error in captured_errors)\n\n    async def test_error_handling_middleware_transform_errors(\n        self, error_handling_server\n    ):\n        \"\"\"Test error transformation functionality.\"\"\"\n        from fastmcp.client import Client\n\n        error_handling_server.add_middleware(\n            ErrorHandlingMiddleware(transform_errors=True)\n        )\n\n        async with Client(error_handling_server) as client:\n            # All errors should still be raised, but potentially transformed\n            with pytest.raises(Exception) as exc_info:\n                await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n        # Error should still exist (may be wrapped by FastMCP)\n        assert exc_info.value is not None\n\n\nclass TestRetryMiddlewareIntegration:\n    \"\"\"Integration tests for retry middleware with real FastMCP server.\"\"\"\n\n    async def test_retry_middleware_with_transient_failures(\n        self, error_handling_server, caplog\n    ):\n        \"\"\"Test retry middleware with operations that have transient failures.\"\"\"\n        from fastmcp.client import Client\n\n        # Configure retry middleware to retry connection errors\n        error_handling_server.add_middleware(\n            RetryMiddleware(\n                max_retries=3,\n                base_delay=0.01,  # Very short delay for testing\n                retry_exceptions=(ConnectionError,),\n            )\n        )\n\n        with caplog.at_level(logging.WARNING):\n            async with Client(error_handling_server) as client:\n                # This operation fails intermittently - try several times\n                success_count = 0\n                for _ in range(5):\n                    try:\n                        await client.call_tool(\n                            \"intermittent_operation\", {\"fail_rate\": 0.7}\n                        )\n                        success_count += 1\n                    except Exception:\n                        pass  # Some failures expected even with retries\n\n        # Should have some retry log messages\n        # Note: Retry logs might not appear if the underlying errors are wrapped by FastMCP\n        # The key is that some operations should succeed due to retries\n\n    async def test_retry_middleware_with_permanent_failures(\n        self, error_handling_server\n    ):\n        \"\"\"Test that retry middleware doesn't retry non-retryable errors.\"\"\"\n        from fastmcp.client import Client\n\n        # Configure retry middleware for connection errors only\n        error_handling_server.add_middleware(\n            RetryMiddleware(\n                max_retries=3, base_delay=0.01, retry_exceptions=(ConnectionError,)\n            )\n        )\n\n        async with Client(error_handling_server) as client:\n            # Value errors should not be retried\n            with pytest.raises(Exception):\n                await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n        # Should fail immediately without retries\n\n    async def test_combined_error_handling_and_retry_middleware(\n        self, error_handling_server, caplog\n    ):\n        \"\"\"Test error handling and retry middleware working together.\"\"\"\n        from fastmcp.client import Client\n\n        # Add both middleware\n        error_handling_server.add_middleware(ErrorHandlingMiddleware())\n        error_handling_server.add_middleware(\n            RetryMiddleware(\n                max_retries=2, base_delay=0.01, retry_exceptions=(ConnectionError,)\n            )\n        )\n\n        with caplog.at_level(logging.ERROR):\n            async with Client(error_handling_server) as client:\n                # Try intermittent operation\n                try:\n                    await client.call_tool(\"intermittent_operation\", {\"fail_rate\": 0.9})\n                except Exception:\n                    pass  # May still fail even with retries\n\n                # Try permanent failure\n                with pytest.raises(Exception):\n                    await client.call_tool(\"failing_operation\", {\"error_type\": \"value\"})\n\n        log_text = caplog.text\n\n        # Should have error logs from error handling middleware\n        assert \"Error in tools/call:\" in log_text\n"
  },
  {
    "path": "tests/server/middleware/test_initialization_middleware.py",
    "content": "\"\"\"Tests for middleware support during initialization.\"\"\"\n\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport mcp.types as mt\nimport pytest\nfrom mcp import McpError\nfrom mcp.types import ErrorData, TextContent\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.tools.base import Tool\n\n\nclass InitializationMiddleware(Middleware):\n    \"\"\"Middleware that captures initialization details.\n\n    Note: Session state is NOT available during on_initialize because\n    the MCP session has not been established yet. Use instance variables\n    to store data that needs to persist across the session.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.initialized = False\n        self.client_info = None\n        self.session_data = {}\n\n    async def on_initialize(\n        self,\n        context: MiddlewareContext[mt.InitializeRequest],\n        call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n    ) -> mt.InitializeResult | None:\n        \"\"\"Capture initialization details.\"\"\"\n        self.initialized = True\n\n        # Extract client info from the initialize params\n        if hasattr(context.message, \"params\") and hasattr(\n            context.message.params, \"clientInfo\"\n        ):\n            self.client_info = context.message.params.clientInfo\n\n        # Store in instance for cross-request access\n        # (session state is not available during on_initialize)\n        self.session_data[\"client_initialized\"] = True\n        if self.client_info:\n            self.session_data[\"client_name\"] = getattr(\n                self.client_info, \"name\", \"unknown\"\n            )\n\n        return await call_next(context)\n\n\nclass ClientDetectionMiddleware(Middleware):\n    \"\"\"Middleware that detects specific clients and modifies behavior.\n\n    This demonstrates storing data in the middleware instance itself\n    for cross-request access, since context state is request-scoped.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.is_test_client = False\n        self.tools_modified = False\n        self.initialization_called = False\n\n    async def on_initialize(\n        self,\n        context: MiddlewareContext[mt.InitializeRequest],\n        call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n    ) -> mt.InitializeResult | None:\n        \"\"\"Detect test client during initialization.\"\"\"\n        self.initialization_called = True\n\n        # For testing purposes, always set it to true\n        # Store in instance variable for cross-request access\n        self.is_test_client = True\n\n        return await call_next(context)\n\n    async def on_list_tools(\n        self,\n        context: MiddlewareContext[mt.ListToolsRequest],\n        call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],\n    ) -> Sequence[Tool]:\n        \"\"\"Modify tools based on client detection.\"\"\"\n        tools = await call_next(context)\n\n        # Use the instance variable set during initialization\n        if self.is_test_client:\n            # Add a special annotation to tools for test clients\n            for tool in tools:\n                if not hasattr(tool, \"annotations\"):\n                    tool.annotations = mt.ToolAnnotations()\n                if tool.annotations is None:\n                    tool.annotations = mt.ToolAnnotations()\n                # Mark as read-only for test clients\n                tool.annotations.readOnlyHint = True\n            self.tools_modified = True\n\n        return tools\n\n\nasync def test_simple_initialization_hook():\n    \"\"\"Test that the on_initialize hook is called.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    class SimpleInitMiddleware(Middleware):\n        def __init__(self):\n            super().__init__()\n            self.called = False\n\n        async def on_initialize(\n            self,\n            context: MiddlewareContext[mt.InitializeRequest],\n            call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n        ) -> mt.InitializeResult | None:\n            self.called = True\n            return await call_next(context)\n\n    middleware = SimpleInitMiddleware()\n    server.add_middleware(middleware)\n\n    # Connect client\n    async with Client(server):\n        # Middleware should have been called\n        assert middleware.called is True, \"on_initialize was not called\"\n\n\nasync def test_middleware_receives_initialization():\n    \"\"\"Test that middleware can intercept initialization requests.\"\"\"\n    server = FastMCP(\"TestServer\")\n    middleware = InitializationMiddleware()\n    server.add_middleware(middleware)\n\n    @server.tool\n    def test_tool(x: int) -> str:\n        return f\"Result: {x}\"\n\n    # Connect client\n    async with Client(server) as client:\n        # Middleware should have been called during initialization\n        assert middleware.initialized is True\n\n        # Test that the tool still works\n        result = await client.call_tool(\"test_tool\", {\"x\": 42})\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Result: 42\"\n\n\nasync def test_client_detection_middleware():\n    \"\"\"Test middleware that detects specific clients and modifies behavior.\"\"\"\n    server = FastMCP(\"TestServer\")\n    middleware = ClientDetectionMiddleware()\n    server.add_middleware(middleware)\n\n    @server.tool\n    def example_tool() -> str:\n        return \"example\"\n\n    # Connect with a client\n    async with Client(server) as client:\n        # Middleware should have been called during initialization\n        assert middleware.initialization_called is True\n        assert middleware.is_test_client is True\n\n        # List tools to trigger modification\n        tools = await client.list_tools()\n        assert len(tools) == 1\n        assert middleware.tools_modified is True\n\n        # Check that the tool has the modified annotation\n        tool = tools[0]\n        assert tool.annotations is not None\n        assert tool.annotations.readOnlyHint is True\n\n\nasync def test_multiple_middleware_initialization():\n    \"\"\"Test that multiple middleware can handle initialization.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    init_mw = InitializationMiddleware()\n    detect_mw = ClientDetectionMiddleware()\n\n    server.add_middleware(init_mw)\n    server.add_middleware(detect_mw)\n\n    @server.tool\n    def test_tool() -> str:\n        return \"test\"\n\n    async with Client(server) as client:\n        # Both middleware should have processed initialization\n        assert init_mw.initialized is True\n        assert detect_mw.initialization_called is True\n        assert detect_mw.is_test_client is True\n\n        # List tools to check detection worked\n        await client.list_tools()\n        assert detect_mw.tools_modified is True\n\n\nasync def test_session_state_persists_across_tool_calls():\n    \"\"\"Test that session-scoped state persists across multiple tool calls.\n\n    Session state is only available after the session is established,\n    so it can't be set during on_initialize. This test shows state set\n    during one tool call is accessible in subsequent tool calls.\n    \"\"\"\n    server = FastMCP(\"TestServer\")\n\n    class StateTrackingMiddleware(Middleware):\n        def __init__(self):\n            super().__init__()\n            self.call_count = 0\n            self.state_values = []\n\n        async def on_call_tool(\n            self,\n            context: MiddlewareContext[mt.CallToolRequestParams],\n            call_next: CallNext[mt.CallToolRequestParams, Any],\n        ) -> Any:\n            self.call_count += 1\n\n            if context.fastmcp_context:\n                # Read existing state\n                counter = await context.fastmcp_context.get_state(\"call_counter\")\n                self.state_values.append(counter)\n\n                # Increment and save\n                new_counter = (counter or 0) + 1\n                await context.fastmcp_context.set_state(\"call_counter\", new_counter)\n\n            return await call_next(context)\n\n    middleware = StateTrackingMiddleware()\n    server.add_middleware(middleware)\n\n    @server.tool\n    def test_tool() -> str:\n        return \"success\"\n\n    async with Client(server) as client:\n        # First call - state should be None initially\n        result = await client.call_tool(\"test_tool\", {})\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"success\"\n\n        # Second call - state should show previous value (1)\n        result = await client.call_tool(\"test_tool\", {})\n        assert isinstance(result.content[0], TextContent)\n\n        # Third call - state should show previous value (2)\n        result = await client.call_tool(\"test_tool\", {})\n        assert isinstance(result.content[0], TextContent)\n\n        # Verify state persisted across calls within the session\n        assert middleware.call_count == 3\n        # First call saw None, second saw 1, third saw 2\n        assert middleware.state_values == [None, 1, 2]\n\n\nasync def test_middleware_can_access_initialize_result():\n    \"\"\"Test that middleware can access the InitializeResult from call_next().\n\n    This verifies that the initialize response is returned through the middleware\n    chain, not just sent directly via the responder (fixes #2504).\n    \"\"\"\n    server = FastMCP(\"TestServer\")\n\n    class ResponseCapturingMiddleware(Middleware):\n        def __init__(self):\n            super().__init__()\n            self.initialize_result: mt.InitializeResult | None = None\n\n        async def on_initialize(\n            self,\n            context: MiddlewareContext[mt.InitializeRequest],\n            call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n        ) -> mt.InitializeResult | None:\n            # Call next and capture the result\n            result = await call_next(context)\n            self.initialize_result = result\n            return result\n\n    middleware = ResponseCapturingMiddleware()\n    server.add_middleware(middleware)\n\n    async with Client(server):\n        # Middleware should have captured the InitializeResult\n        assert middleware.initialize_result is not None\n        assert isinstance(middleware.initialize_result, mt.InitializeResult)\n\n        # Verify the result contains expected server info\n        assert middleware.initialize_result.serverInfo.name == \"TestServer\"\n        assert middleware.initialize_result.protocolVersion is not None\n        assert middleware.initialize_result.capabilities is not None\n\n\nasync def test_middleware_mcp_error_during_initialization():\n    \"\"\"Test that McpError raised in middleware during initialization is sent to client.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    class ErrorThrowingMiddleware(Middleware):\n        async def on_initialize(\n            self,\n            context: MiddlewareContext[mt.InitializeRequest],\n            call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n        ) -> mt.InitializeResult | None:\n            raise McpError(\n                ErrorData(\n                    code=mt.INVALID_PARAMS, message=\"Invalid initialization parameters\"\n                )\n            )\n\n    server.add_middleware(ErrorThrowingMiddleware())\n\n    with pytest.raises(McpError) as exc_info:\n        async with Client(server):\n            pass\n\n    assert exc_info.value.error.message == \"Invalid initialization parameters\"\n    assert exc_info.value.error.code == mt.INVALID_PARAMS\n\n\nasync def test_middleware_mcp_error_before_call_next():\n    \"\"\"Test McpError raised before calling next middleware.\"\"\"\n    server = FastMCP(\"TestServer\")\n\n    class EarlyErrorMiddleware(Middleware):\n        async def on_initialize(\n            self,\n            context: MiddlewareContext[mt.InitializeRequest],\n            call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n        ) -> mt.InitializeResult | None:\n            raise McpError(\n                ErrorData(code=mt.INVALID_REQUEST, message=\"Request validation failed\")\n            )\n\n    server.add_middleware(EarlyErrorMiddleware())\n\n    with pytest.raises(McpError) as exc_info:\n        async with Client(server):\n            pass\n\n    assert exc_info.value.error.message == \"Request validation failed\"\n    assert exc_info.value.error.code == mt.INVALID_REQUEST\n\n\nasync def test_middleware_mcp_error_after_call_next():\n    \"\"\"Test that McpError raised after call_next doesn't break the connection.\n\n    When an error is raised after call_next, the responder has already completed,\n    so the error is caught but not sent to the responder (checked via _completed flag).\n    \"\"\"\n    server = FastMCP(\"TestServer\")\n\n    class PostProcessingErrorMiddleware(Middleware):\n        def __init__(self):\n            super().__init__()\n            self.error_raised = False\n\n        async def on_initialize(\n            self,\n            context: MiddlewareContext[mt.InitializeRequest],\n            call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],\n        ) -> mt.InitializeResult | None:\n            await call_next(context)\n            self.error_raised = True\n            raise McpError(\n                ErrorData(code=mt.INTERNAL_ERROR, message=\"Post-processing failed\")\n            )\n\n    middleware = PostProcessingErrorMiddleware()\n    server.add_middleware(middleware)\n\n    # Error is logged but not re-raised to prevent duplicate response\n    async with Client(server):\n        pass\n\n    assert middleware.error_raised is True\n\n\nasync def test_state_isolation_between_streamable_http_clients():\n    \"\"\"Test that different HTTP clients have isolated session state.\n\n    Each client should have its own session ID and isolated state.\n    \"\"\"\n    from fastmcp.client.transports import StreamableHttpTransport\n    from fastmcp.server.context import Context\n    from fastmcp.utilities.tests import run_server_async\n\n    server = FastMCP(\"TestServer\")\n\n    @server.tool\n    async def store_and_read(value: str, ctx: Context) -> dict:\n        \"\"\"Store a value and return session info.\"\"\"\n        existing = await ctx.get_state(\"client_value\")\n        await ctx.set_state(\"client_value\", value)\n        return {\n            \"existing\": existing,\n            \"stored\": value,\n            \"session_id\": ctx.session_id,\n        }\n\n    async with run_server_async(server, transport=\"streamable-http\") as url:\n        import json\n\n        # Client 1 stores its value\n        transport1 = StreamableHttpTransport(url=url)\n        async with Client(transport=transport1) as client1:\n            result1 = await client1.call_tool(\n                \"store_and_read\", {\"value\": \"client1-value\"}\n            )\n            data1 = json.loads(result1.content[0].text)\n            assert data1[\"existing\"] is None\n            assert data1[\"stored\"] == \"client1-value\"\n            session_id_1 = data1[\"session_id\"]\n\n        # Client 2 should have completely isolated state\n        transport2 = StreamableHttpTransport(url=url)\n        async with Client(transport=transport2) as client2:\n            result2 = await client2.call_tool(\n                \"store_and_read\", {\"value\": \"client2-value\"}\n            )\n            data2 = json.loads(result2.content[0].text)\n            # Should NOT see client1's value\n            assert data2[\"existing\"] is None\n            assert data2[\"stored\"] == \"client2-value\"\n            session_id_2 = data2[\"session_id\"]\n\n        # Session IDs should be different\n        assert session_id_1 != session_id_2\n"
  },
  {
    "path": "tests/server/middleware/test_logging.py",
    "content": "\"\"\"Tests for logging middleware.\"\"\"\n\nimport datetime\nimport logging\nfrom collections.abc import Generator\nfrom typing import Any, Literal, TypeVar\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport mcp\nimport mcp.types\nimport pytest\nfrom inline_snapshot import snapshot\nfrom pydantic import AnyUrl\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.middleware.logging import (\n    LoggingMiddleware,\n    StructuredLoggingMiddleware,\n)\nfrom fastmcp.server.middleware.middleware import CallNext, MiddlewareContext\n\nFIXED_DATE = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc)\n\nT = TypeVar(\"T\")\n\n\ndef get_log_lines(\n    caplog: pytest.LogCaptureFixture, module: str | None = None\n) -> list[str]:\n    \"\"\"Get log lines from a caplog fixture.\"\"\"\n    return [\n        record.message\n        for record in caplog.records\n        if (module or \"logging\") in record.name\n    ]\n\n\ndef new_mock_context(\n    message: T,\n    method: str | None = None,\n    source: Literal[\"server\", \"client\"] | None = None,\n    type: Literal[\"request\", \"notification\"] | None = None,\n) -> MiddlewareContext[T]:\n    \"\"\"Create a new mock middleware context.\"\"\"\n    context = MagicMock(spec=MiddlewareContext[T])\n    context.method = method or \"test_method\"\n    context.source = source or \"client\"\n    context.type = type or \"request\"\n    context.message = message\n    context.timestamp = FIXED_DATE\n    return context\n\n\n@pytest.fixture(autouse=True)\ndef mock_duration_ms() -> Generator[float, None]:\n    \"\"\"Mock duration_ms.\"\"\"\n    patched = patch(\n        \"fastmcp.server.middleware.logging._get_duration_ms\", return_value=0.02\n    )\n    patched.start()\n    yield\n    patched.stop()\n\n\n@pytest.fixture\ndef mock_context():\n    \"\"\"Create a mock middleware context.\"\"\"\n\n    return new_mock_context(\n        message=mcp.types.CallToolRequest(\n            method=\"tools/call\",\n            params=mcp.types.CallToolRequestParams(\n                name=\"test_method\",\n                arguments={\"param\": \"value\"},\n            ),\n        )\n    )\n\n\n@pytest.fixture\ndef mock_call_next() -> AsyncMock:\n    \"\"\"Create a mock call_next function.\"\"\"\n    return AsyncMock(return_value=\"test_result\")\n\n\nclass TestStructuredLoggingMiddleware:\n    \"\"\"Test structured logging middleware functionality.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = StructuredLoggingMiddleware()\n\n        assert middleware.logger.name == \"fastmcp.middleware.structured_logging\"\n        assert middleware.log_level == logging.INFO\n        assert middleware.include_payloads is False\n        assert middleware.include_payload_length is False\n        assert middleware.estimate_payload_tokens is False\n        assert middleware.structured_logging is True\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n        logger = logging.getLogger(\"custom\")\n        middleware = StructuredLoggingMiddleware(\n            logger=logger,\n            log_level=logging.DEBUG,\n            include_payloads=True,\n            include_payload_length=False,\n            estimate_payload_tokens=True,\n        )\n        assert middleware.logger is logger\n        assert middleware.log_level == logging.DEBUG\n        assert middleware.include_payloads is True\n        assert middleware.include_payload_length is False\n        assert middleware.estimate_payload_tokens is True\n\n    class TestHelperMethods:\n        def test_create_before_message(self, mock_context: MiddlewareContext[Any]):\n            \"\"\"Test message formatting without payloads.\"\"\"\n            middleware = StructuredLoggingMiddleware()\n\n            message = middleware._create_before_message(mock_context)\n\n            assert message == snapshot(\n                {\n                    \"event\": \"request_start\",\n                    \"source\": \"client\",\n                    \"method\": \"test_method\",\n                }\n            )\n\n        def test_create_message_with_payloads(\n            self, mock_context: MiddlewareContext[Any]\n        ):\n            \"\"\"Test message formatting with payloads.\"\"\"\n            middleware = StructuredLoggingMiddleware(include_payloads=True)\n\n            message = middleware._create_before_message(mock_context)\n\n            assert message == snapshot(\n                {\n                    \"event\": \"request_start\",\n                    \"source\": \"client\",\n                    \"method\": \"test_method\",\n                    \"payload\": '{\"method\":\"tools/call\",\"params\":{\"task\":null,\"_meta\":null,\"name\":\"test_method\",\"arguments\":{\"param\":\"value\"}}}',\n                    \"payload_type\": \"CallToolRequest\",\n                }\n            )\n\n        def test_calculate_response_size(self, mock_context: MiddlewareContext[Any]):\n            \"\"\"Test response size calculation.\"\"\"\n            middleware = StructuredLoggingMiddleware(include_payload_length=True)\n            message = middleware._create_before_message(mock_context)\n\n            assert message == snapshot(\n                {\n                    \"event\": \"request_start\",\n                    \"source\": \"client\",\n                    \"method\": \"test_method\",\n                    \"payload_length\": 110,\n                }\n            )\n\n        def test_calculate_response_size_with_token_estimation(\n            self, mock_context: MiddlewareContext[Any]\n        ):\n            \"\"\"Test response size calculation with token estimation.\"\"\"\n            middleware = StructuredLoggingMiddleware(\n                include_payload_length=True, estimate_payload_tokens=True\n            )\n            message = middleware._create_before_message(mock_context)\n\n            assert message == snapshot(\n                {\n                    \"event\": \"request_start\",\n                    \"source\": \"client\",\n                    \"method\": \"test_method\",\n                    \"payload_tokens\": 27,\n                    \"payload_length\": 110,\n                }\n            )\n\n    async def test_on_message_success(\n        self,\n        mock_context: MiddlewareContext[Any],\n        caplog: pytest.LogCaptureFixture,\n    ):\n        \"\"\"Test logging successful messages.\"\"\"\n        middleware = StructuredLoggingMiddleware()\n        mock_call_next = AsyncMock(return_value=\"test_result\")\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.called\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\"}',\n                '{\"event\": \"request_success\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02}',\n            ]\n        )\n\n    async def test_on_message_failure(\n        self, mock_context: MiddlewareContext[Any], caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test logging failed messages.\"\"\"\n        middleware = StructuredLoggingMiddleware()\n        mock_call_next = AsyncMock(side_effect=ValueError(\"test error\"))\n\n        with pytest.raises(ValueError):\n            await middleware.on_message(mock_context, mock_call_next)\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\"}',\n                '{\"event\": \"request_error\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02, \"error\": \"test error\"}',\n            ]\n        )\n\n\nclass TestLoggingMiddleware:\n    \"\"\"Test structured logging middleware functionality.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = LoggingMiddleware()\n        assert middleware.logger.name == \"fastmcp.middleware.logging\"\n        assert middleware.log_level == logging.INFO\n        assert middleware.include_payloads is False\n        assert middleware.include_payload_length is False\n        assert middleware.estimate_payload_tokens is False\n\n    def test_format_message(self, mock_context: MiddlewareContext[Any]):\n        \"\"\"Test message formatting.\"\"\"\n        middleware = LoggingMiddleware()\n        message = middleware._create_before_message(mock_context)\n        formatted = middleware._format_message(message)\n\n        assert formatted == snapshot(\n            \"event=request_start method=test_method source=client\"\n        )\n\n    def test_create_before_message_long_payload(\n        self, mock_context: MiddlewareContext[Any]\n    ):\n        \"\"\"Test message formatting with long payload truncation.\"\"\"\n        middleware = LoggingMiddleware(include_payloads=True, max_payload_length=10)\n\n        message = middleware._create_before_message(mock_context)\n\n        formatted = middleware._format_message(message)\n\n        assert formatted == snapshot(\n            'event=request_start method=test_method source=client payload={\"method\":... payload_type=CallToolRequest'\n        )\n\n    async def test_on_message_failure(\n        self, mock_context: MiddlewareContext[Any], caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test structured logging of failed messages.\"\"\"\n        middleware = StructuredLoggingMiddleware()\n        mock_call_next = AsyncMock(side_effect=ValueError(\"test error\"))\n\n        with pytest.raises(ValueError):\n            await middleware.on_message(mock_context, mock_call_next)\n\n        # Check that we have structured JSON logs\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\"}',\n                '{\"event\": \"request_error\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02, \"error\": \"test error\"}',\n            ]\n        )\n\n    async def test_on_message_with_pydantic_types_in_payload(\n        self,\n        mock_call_next: CallNext[Any, Any],\n        caplog: pytest.LogCaptureFixture,\n    ):\n        \"\"\"Ensure Pydantic AnyUrl in payload serializes correctly when include_payloads=True.\"\"\"\n\n        mock_context = new_mock_context(\n            message=mcp.types.ReadResourceRequest(\n                method=\"resources/read\",\n                params=mcp.types.ReadResourceRequestParams(\n                    uri=AnyUrl(\"test://example/1\"),\n                ),\n            )\n        )\n\n        middleware = StructuredLoggingMiddleware(include_payloads=True)\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\", \"payload\": \"{\\\\\"method\\\\\":\\\\\"resources/read\\\\\",\\\\\"params\\\\\":{\\\\\"task\\\\\":null,\\\\\"_meta\\\\\":null,\\\\\"uri\\\\\":\\\\\"test://example/1\\\\\"}}\", \"payload_type\": \"ReadResourceRequest\"}',\n                '{\"event\": \"request_success\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02}',\n            ]\n        )\n\n    async def test_on_message_with_resource_template_in_payload(\n        self,\n        mock_call_next: CallNext[Any, Any],\n        caplog: pytest.LogCaptureFixture,\n    ):\n        \"\"\"Ensure ResourceTemplate in payload serializes via pydantic conversion without errors.\"\"\"\n\n        mock_context = new_mock_context(\n            message=ResourceTemplate(\n                name=\"tmpl\",\n                uri_template=\"tmpl://{id}\",\n                parameters={\"id\": {\"type\": \"string\"}},\n            )\n        )\n\n        middleware = StructuredLoggingMiddleware(include_payloads=True)\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\", \"payload\": \"{\\\\\"name\\\\\":\\\\\"tmpl\\\\\",\\\\\"version\\\\\":null,\\\\\"title\\\\\":null,\\\\\"description\\\\\":null,\\\\\"icons\\\\\":null,\\\\\"tags\\\\\":[],\\\\\"meta\\\\\":null,\\\\\"task_config\\\\\":{\\\\\"mode\\\\\":\\\\\"forbidden\\\\\",\\\\\"poll_interval\\\\\":\\\\\"PT5S\\\\\"},\\\\\"uri_template\\\\\":\\\\\"tmpl://{id}\\\\\",\\\\\"mime_type\\\\\":\\\\\"text/plain\\\\\",\\\\\"parameters\\\\\":{\\\\\"id\\\\\":{\\\\\"type\\\\\":\\\\\"string\\\\\"}},\\\\\"annotations\\\\\":null}\", \"payload_type\": \"ResourceTemplate\"}',\n                '{\"event\": \"request_success\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02}',\n            ]\n        )\n\n    async def test_on_message_with_nonserializable_payload_falls_back_to_str(\n        self, mock_call_next: CallNext[Any, Any], caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Ensure non-JSONable objects fall back to string serialization in payload.\"\"\"\n\n        class NonSerializable:\n            def __str__(self) -> str:\n                return \"NON_SERIALIZABLE\"\n\n        mock_context = new_mock_context(\n            message=mcp.types.CallToolRequest(\n                method=\"tools/call\",\n                params=mcp.types.CallToolRequestParams(\n                    name=\"test_method\",\n                    arguments={\"obj\": NonSerializable()},\n                ),\n            )\n        )\n\n        middleware = StructuredLoggingMiddleware(include_payloads=True)\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\", \"payload\": \"{\\\\\"method\\\\\":\\\\\"tools/call\\\\\",\\\\\"params\\\\\":{\\\\\"task\\\\\":null,\\\\\"_meta\\\\\":null,\\\\\"name\\\\\":\\\\\"test_method\\\\\",\\\\\"arguments\\\\\":{\\\\\"obj\\\\\":\\\\\"NON_SERIALIZABLE\\\\\"}}}\", \"payload_type\": \"CallToolRequest\"}',\n                '{\"event\": \"request_success\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02}',\n            ]\n        )\n\n    async def test_on_message_with_custom_serializer_applied(\n        self, mock_call_next: CallNext[Any, Any], caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Ensure a custom serializer is used for non-JSONable payloads.\"\"\"\n\n        # Provide a serializer that replaces entire payload with a fixed string\n        def custom_serializer(_: Any) -> str:\n            return \"CUSTOM_PAYLOAD\"\n\n        mock_context = new_mock_context(\n            message=mcp.types.CallToolRequest(\n                method=\"tools/call\",\n                params=mcp.types.CallToolRequestParams(\n                    name=\"test_method\",\n                    arguments={\"obj\": \"OBJECT\"},\n                ),\n            )\n        )\n\n        middleware = StructuredLoggingMiddleware(\n            include_payloads=True, payload_serializer=custom_serializer\n        )\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"test_method\", \"source\": \"client\", \"payload\": \"CUSTOM_PAYLOAD\", \"payload_type\": \"CallToolRequest\"}',\n                '{\"event\": \"request_success\", \"method\": \"test_method\", \"source\": \"client\", \"duration_ms\": 0.02}',\n            ]\n        )\n\n\n@pytest.fixture\ndef logging_server():\n    \"\"\"Create a FastMCP server specifically for logging middleware tests.\"\"\"\n    from fastmcp import FastMCP\n\n    mcp = FastMCP(\"LoggingTestServer\")\n\n    @mcp.tool\n    def simple_operation(data: str) -> str:\n        \"\"\"A simple operation for testing logging.\"\"\"\n        return f\"Processed: {data}\"\n\n    @mcp.tool\n    def complex_operation(items: list[str], mode: str = \"default\") -> dict:\n        \"\"\"A complex operation with structured data.\"\"\"\n        return {\"processed_items\": len(items), \"mode\": mode, \"result\": \"success\"}\n\n    @mcp.tool\n    def operation_with_error(should_fail: bool = False) -> str:\n        \"\"\"An operation that can be made to fail.\"\"\"\n        if should_fail:\n            raise ValueError(\"Operation failed intentionally\")\n        return \"Operation completed successfully\"\n\n    @mcp.resource(\"log://test\")\n    def test_resource() -> str:\n        \"\"\"A test resource for logging.\"\"\"\n        return \"Test resource content\"\n\n    @mcp.prompt\n    def test_prompt() -> str:\n        \"\"\"A test prompt for logging.\"\"\"\n        return \"Test prompt content\"\n\n    return mcp\n\n\nclass TestLoggingMiddlewareIntegration:\n    \"\"\"Integration tests for logging middleware with real FastMCP server.\"\"\"\n\n    @pytest.fixture\n    def logging_server(self):\n        \"\"\"Create a FastMCP server specifically for logging middleware tests.\"\"\"\n        mcp = FastMCP(\"LoggingTestServer\")\n\n        @mcp.tool\n        def simple_operation(data: str) -> str:\n            \"\"\"A simple operation for testing logging.\"\"\"\n            return f\"Processed: {data}\"\n\n        @mcp.tool\n        def complex_operation(items: list[str], mode: str = \"default\") -> dict:\n            \"\"\"A complex operation with structured data.\"\"\"\n            return {\"processed_items\": len(items), \"mode\": mode, \"result\": \"success\"}\n\n        @mcp.tool\n        def operation_with_error(should_fail: bool = False) -> str:\n            \"\"\"An operation that can be made to fail.\"\"\"\n            if should_fail:\n                raise ValueError(\"Operation failed intentionally\")\n            return \"Operation completed successfully\"\n\n        @mcp.resource(\"log://test\")\n        def test_resource() -> str:\n            \"\"\"A test resource for logging.\"\"\"\n            return \"Test resource content\"\n\n        @mcp.prompt\n        def test_prompt() -> str:\n            \"\"\"A test prompt for logging.\"\"\"\n            return \"Test prompt content\"\n\n        return mcp\n\n    async def test_logging_middleware_logs_successful_operations(\n        self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test that logging middleware captures successful operations.\"\"\"\n        logging_middleware = LoggingMiddleware(methods=[\"tools/call\"])\n\n        logging_server.add_middleware(logging_middleware)\n\n        with caplog.at_level(logging.INFO):\n            async with Client(logging_server) as client:\n                await client.call_tool(\n                    name=\"simple_operation\", arguments={\"data\": \"test_data\"}\n                )\n                await client.call_tool(\n                    name=\"complex_operation\",\n                    arguments={\"items\": [\"a\", \"b\", \"c\"], \"mode\": \"batch\"},\n                )\n\n        # Should have processing and completion logs for both operations\n        assert get_log_lines(caplog) == snapshot(\n            [\n                \"event=request_start method=tools/call source=client\",\n                \"event=request_success method=tools/call source=client duration_ms=0.02\",\n                \"event=request_start method=tools/call source=client\",\n                \"event=request_success method=tools/call source=client duration_ms=0.02\",\n            ]\n        )\n\n    async def test_logging_middleware_logs_failures(\n        self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test that logging middleware captures failed operations.\"\"\"\n        logging_server.add_middleware(LoggingMiddleware(methods=[\"tools/call\"]))\n\n        async with Client(logging_server) as client:\n            # This should fail and be logged\n            with pytest.raises(Exception):\n                await client.call_tool(\"operation_with_error\", {\"should_fail\": True})\n\n        log_text = caplog.text\n\n        # Should have processing and failure logs\n        assert log_text.splitlines()[-1] == snapshot(\n            \"ERROR    fastmcp.middleware.logging:logging.py:122 event=request_error method=tools/call source=client duration_ms=0.02 error=Error calling tool 'operation_with_error': Operation failed intentionally\"\n        )\n\n    async def test_logging_middleware_with_payloads(\n        self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test logging middleware when configured to include payloads.\"\"\"\n\n        middleware = LoggingMiddleware(\n            include_payloads=True, max_payload_length=500, methods=[\"tools/call\"]\n        )\n        logging_server.add_middleware(middleware)\n\n        async with Client(logging_server) as client:\n            await client.call_tool(\"simple_operation\", {\"data\": \"payload_test\"})\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                'event=request_start method=tools/call source=client payload={\"task\":null,\"_meta\":null,\"name\":\"simple_operation\",\"arguments\":{\"data\":\"payload_test\"}} payload_type=CallToolRequestParams',\n                \"event=request_success method=tools/call source=client duration_ms=0.02\",\n            ]\n        )\n\n    async def test_structured_logging_middleware_produces_json(\n        self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test that structured logging middleware produces parseable JSON logs.\"\"\"\n\n        logging_middleware = StructuredLoggingMiddleware(\n            include_payloads=True, methods=[\"tools/call\"]\n        )\n\n        logging_server.add_middleware(logging_middleware)\n\n        async with Client(logging_server) as client:\n            await client.call_tool(\n                name=\"simple_operation\", arguments={\"data\": \"json_test\"}\n            )\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"tools/call\", \"source\": \"client\", \"payload\": \"{\\\\\"task\\\\\":null,\\\\\"_meta\\\\\":null,\\\\\"name\\\\\":\\\\\"simple_operation\\\\\",\\\\\"arguments\\\\\":{\\\\\"data\\\\\":\\\\\"json_test\\\\\"}}\", \"payload_type\": \"CallToolRequestParams\"}',\n                '{\"event\": \"request_success\", \"method\": \"tools/call\", \"source\": \"client\", \"duration_ms\": 0.02}',\n            ]\n        )\n\n    async def test_structured_logging_middleware_handles_errors(\n        self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test structured logging of errors with JSON format.\"\"\"\n\n        logging_middleware = StructuredLoggingMiddleware(methods=[\"tools/call\"])\n\n        logging_server.add_middleware(logging_middleware)\n\n        with caplog.at_level(logging.INFO):\n            async with Client(logging_server) as client:\n                with pytest.raises(Exception):\n                    await client.call_tool(\n                        \"operation_with_error\", {\"should_fail\": True}\n                    )\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                '{\"event\": \"request_start\", \"method\": \"tools/call\", \"source\": \"client\"}',\n                '{\"event\": \"request_error\", \"method\": \"tools/call\", \"source\": \"client\", \"duration_ms\": 0.02, \"error\": \"Error calling tool \\'operation_with_error\\': Operation failed intentionally\"}',\n            ]\n        )\n\n    async def test_logging_middleware_with_different_operations(\n        self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture\n    ):\n        \"\"\"Test logging middleware with various MCP operations.\"\"\"\n\n        logging_server.add_middleware(\n            LoggingMiddleware(\n                methods=[\n                    \"tools/call\",\n                    \"resources/list\",\n                    \"prompts/get\",\n                    \"resources/read\",\n                ]\n            )\n        )\n\n        async with Client(logging_server) as client:\n            # Test different operation types\n            await client.call_tool(\"simple_operation\", {\"data\": \"test\"})\n            await client.read_resource(\"log://test\")\n            await client.get_prompt(\"test_prompt\")\n            await client.list_resources()\n\n        assert get_log_lines(caplog) == snapshot(\n            [\n                \"event=request_start method=tools/call source=client\",\n                \"event=request_success method=tools/call source=client duration_ms=0.02\",\n                \"event=request_start method=resources/read source=client\",\n                \"event=request_success method=resources/read source=client duration_ms=0.02\",\n                \"event=request_start method=prompts/get source=client\",\n                \"event=request_success method=prompts/get source=client duration_ms=0.02\",\n                \"event=request_start method=resources/list source=client\",\n                \"event=request_success method=resources/list source=client duration_ms=0.02\",\n            ]\n        )\n\n    async def test_logging_middleware_custom_configuration(\n        self, logging_server: FastMCP\n    ):\n        \"\"\"Test logging middleware with custom logger configuration.\"\"\"\n        import io\n        import logging\n\n        # Create custom logger\n        log_buffer = io.StringIO()\n        handler = logging.StreamHandler(log_buffer)\n        custom_logger = logging.getLogger(\"custom_logging_test\")\n        custom_logger.addHandler(handler)\n        custom_logger.setLevel(logging.DEBUG)\n\n        logging_server.add_middleware(\n            LoggingMiddleware(\n                logger=custom_logger,\n                log_level=logging.DEBUG,\n                include_payloads=True,\n                methods=[\"tools/call\"],\n            )\n        )\n\n        async with Client(logging_server) as client:\n            await client.call_tool(\"simple_operation\", {\"data\": \"custom_test\"})\n\n        # Check that our custom logger captured the logs\n        log_output = log_buffer.getvalue()\n        assert log_output == snapshot(\"\"\"\\\nevent=request_start method=tools/call source=client payload={\"task\":null,\"_meta\":null,\"name\":\"simple_operation\",\"arguments\":{\"data\":\"custom_test\"}} payload_type=CallToolRequestParams\nevent=request_success method=tools/call source=client duration_ms=0.02\n\"\"\")\n"
  },
  {
    "path": "tests/server/middleware/test_middleware.py",
    "content": "from collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import Any\n\nimport mcp.types\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.tools.base import ToolResult\n\n\n@dataclass\nclass Recording:\n    # the hook is the name of the hook that was called, e.g. \"on_list_tools\"\n    hook: str\n    context: MiddlewareContext\n    result: mcp.types.ServerResult | None\n\n\nclass RecordingMiddleware(Middleware):\n    \"\"\"A middleware that automatically records all method calls.\"\"\"\n\n    def __init__(self, name: str | None = None):\n        super().__init__()\n        self.calls: list[Recording] = []\n        self.name = name\n\n    def __getattribute__(self, name: str) -> Callable:\n        \"\"\"Dynamically create recording methods for any on_* method.\"\"\"\n        if name.startswith(\"on_\"):\n\n            async def record_and_call(\n                context: MiddlewareContext, call_next: Callable\n            ) -> Any:\n                result = await call_next(context)\n\n                self.calls.append(Recording(hook=name, context=context, result=result))\n\n                return result\n\n            return record_and_call\n\n        return super().__getattribute__(name)\n\n    def get_calls(\n        self, method: str | None = None, hook: str | None = None\n    ) -> list[Recording]:\n        \"\"\"\n        Get all recorded calls for a specific method or hook.\n        Args:\n            method: The method to filter by (e.g. \"tools/list\")\n            hook: The hook to filter by (e.g. \"on_list_tools\")\n        Returns:\n            A list of recorded calls.\n        \"\"\"\n        calls = []\n        for recording in self.calls:\n            if method and hook:\n                if recording.context.method == method and recording.hook == hook:\n                    calls.append(recording)\n            elif method:\n                if recording.context.method == method:\n                    calls.append(recording)\n            elif hook:\n                if recording.hook == hook:\n                    calls.append(recording)\n            else:\n                calls.append(recording)\n        return calls\n\n    def assert_called(\n        self,\n        hook: str | None = None,\n        method: str | None = None,\n        times: int | None = None,\n        at_least: int | None = None,\n    ) -> bool:\n        \"\"\"Assert that a hook was called a specific number of times.\"\"\"\n\n        if times is not None and at_least is not None:\n            raise ValueError(\"Cannot specify both times and at_least\")\n        elif times is None and at_least is None:\n            times = 1\n\n        calls = self.get_calls(hook=hook, method=method)\n        actual_times = len(calls)\n        identifier = dict(hook=hook, method=method)\n\n        if times is not None:\n            assert actual_times == times, (\n                f\"Expected {times} calls for {identifier}, \"\n                f\"but was called {actual_times} times\"\n            )\n        elif at_least is not None:\n            assert actual_times >= at_least, (\n                f\"Expected at least {at_least} calls for {identifier}, \"\n                f\"but was called {actual_times} times\"\n            )\n        return True\n\n    def assert_not_called(self, hook: str | None = None, method: str | None = None):\n        \"\"\"Assert that a hook was not called.\"\"\"\n        calls = self.get_calls(hook=hook, method=method)\n        assert len(calls) == 0, f\"Expected {hook!r} to not be called\"\n        return True\n\n    def reset(self):\n        \"\"\"Clear all recorded calls.\"\"\"\n        self.calls.clear()\n\n\n@pytest.fixture\ndef recording_middleware():\n    \"\"\"Fixture that provides a recording middleware instance.\"\"\"\n    middleware = RecordingMiddleware(name=\"recording_middleware\")\n    yield middleware\n\n\n@pytest.fixture\ndef mcp_server(recording_middleware):\n    mcp = FastMCP()\n\n    @mcp.tool(tags={\"add-tool\"})\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    @mcp.resource(\"resource://test\")\n    def test_resource() -> str:\n        return \"test resource\"\n\n    @mcp.resource(\"resource://test-template/{x}\")\n    def test_resource_with_path(x: int) -> str:\n        return f\"test resource with {x}\"\n\n    @mcp.prompt\n    def test_prompt(x: str) -> str:\n        return f\"test prompt with {x}\"\n\n    @mcp.tool\n    async def progress_tool(context: Context) -> None:\n        await context.report_progress(progress=1, total=10, message=\"test\")\n\n    @mcp.tool\n    async def log_tool(context: Context) -> None:\n        await context.info(message=\"test log\")\n\n    @mcp.tool\n    async def sample_tool(context: Context) -> None:\n        await context.sample(\"hello\")\n\n    mcp.add_middleware(recording_middleware)\n\n    # Register progress handler\n    @mcp._mcp_server.progress_notification()\n    async def handle_progress(\n        progress_token: str | int,\n        progress: float,\n        total: float | None,\n        message: str | None,\n    ):\n        print(\"HI\")\n\n    return mcp\n\n\nclass TestMiddlewareHooks:\n    async def test_call_tool(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        assert recording_middleware.assert_called(at_least=9)\n        assert recording_middleware.assert_called(method=\"tools/call\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_call_tool\", at_least=1)\n\n    async def test_read_resource(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.read_resource(\"resource://test\")\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n    async def test_read_resource_template(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.read_resource(\"resource://test-template/1\")\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n    async def test_get_prompt(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.get_prompt(\"test_prompt\", {\"x\": \"test\"})\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"prompts/get\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_get_prompt\", at_least=1)\n\n    async def test_list_tools(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.list_tools()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"tools/list\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_list_tools\", at_least=1)\n\n        # Verify the middleware receives a list of tools\n        list_tools_calls = recording_middleware.get_calls(hook=\"on_list_tools\")\n        assert len(list_tools_calls) > 0\n        result = list_tools_calls[0].result\n        assert isinstance(result, list)\n\n    async def test_list_resources(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.list_resources()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/list\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_list_resources\", at_least=1)\n\n        # Verify the middleware receives a list of resources\n        list_resources_calls = recording_middleware.get_calls(hook=\"on_list_resources\")\n        assert len(list_resources_calls) > 0\n        result = list_resources_calls[0].result\n        assert isinstance(result, list)\n\n    async def test_list_resource_templates(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.list_resource_templates()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(\n            method=\"resources/templates/list\", at_least=3\n        )\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(\n            hook=\"on_list_resource_templates\", at_least=1\n        )\n\n        # Verify the middleware receives a list of resource templates\n        list_templates_calls = recording_middleware.get_calls(\n            hook=\"on_list_resource_templates\"\n        )\n        assert len(list_templates_calls) > 0\n        result = list_templates_calls[0].result\n        assert isinstance(result, list)\n\n    async def test_list_prompts(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.list_prompts()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"prompts/list\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_list_prompts\", at_least=1)\n\n        # Verify the middleware receives a list of prompts\n        list_prompts_calls = recording_middleware.get_calls(hook=\"on_list_prompts\")\n        assert len(list_prompts_calls) > 0\n        result = list_prompts_calls[0].result\n        assert isinstance(result, list)\n\n    async def test_initialize(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        async with Client(mcp_server) as client:\n            await client.ping()\n\n        assert recording_middleware.assert_called(at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_initialize\", at_least=1)\n\n    async def test_list_tools_filtering_middleware(self):\n        \"\"\"Test that middleware can filter tools.\"\"\"\n\n        class FilteringMiddleware(Middleware):\n            async def on_list_tools(self, context: MiddlewareContext, call_next):\n                result = await call_next(context)\n                # Filter out tools with \"private\" tag - simple list filtering\n                filtered_tools = [tool for tool in result if \"private\" not in tool.tags]\n                return filtered_tools\n\n        server = FastMCP(\"TestServer\")\n\n        @server.tool\n        def public_tool(name: str) -> str:\n            return f\"Hello {name}\"\n\n        @server.tool(tags={\"private\"})\n        def private_tool(secret: str) -> str:\n            return f\"Secret: {secret}\"\n\n        server.add_middleware(FilteringMiddleware())\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n\n        assert len(tools) == 1\n        assert tools[0].name == \"public_tool\"\n\n    async def test_list_resources_filtering_middleware(self):\n        \"\"\"Test that middleware can filter resources.\"\"\"\n\n        class FilteringMiddleware(Middleware):\n            async def on_list_resources(self, context: MiddlewareContext, call_next):\n                result = await call_next(context)\n                # Filter out resources with \"private\" tag\n                filtered_resources = [\n                    resource for resource in result if \"private\" not in resource.tags\n                ]\n                return filtered_resources\n\n        server = FastMCP(\"TestServer\")\n\n        @server.resource(\"resource://public\")\n        def public_resource() -> str:\n            return \"public data\"\n\n        @server.resource(\"resource://private\", tags={\"private\"})\n        def private_resource() -> str:\n            return \"private data\"\n\n        server.add_middleware(FilteringMiddleware())\n\n        async with Client(server) as client:\n            resources = await client.list_resources()\n\n        assert len(resources) == 1\n        assert str(resources[0].uri) == \"resource://public\"\n\n    async def test_list_resource_templates_filtering_middleware(self):\n        \"\"\"Test that middleware can filter resource templates.\"\"\"\n\n        class FilteringMiddleware(Middleware):\n            async def on_list_resource_templates(\n                self, context: MiddlewareContext, call_next\n            ):\n                result = await call_next(context)\n                # Filter out templates with \"private\" tag\n                filtered_templates = [\n                    template for template in result if \"private\" not in template.tags\n                ]\n                return filtered_templates\n\n        server = FastMCP(\"TestServer\")\n\n        @server.resource(\"resource://public/{x}\")\n        def public_template(x: str) -> str:\n            return f\"public {x}\"\n\n        @server.resource(\"resource://private/{x}\", tags={\"private\"})\n        def private_template(x: str) -> str:\n            return f\"private {x}\"\n\n        server.add_middleware(FilteringMiddleware())\n\n        async with Client(server) as client:\n            templates = await client.list_resource_templates()\n\n        assert len(templates) == 1\n        assert str(templates[0].uriTemplate) == \"resource://public/{x}\"\n\n    async def test_list_prompts_filtering_middleware(self):\n        \"\"\"Test that middleware can filter prompts.\"\"\"\n\n        class FilteringMiddleware(Middleware):\n            async def on_list_prompts(self, context: MiddlewareContext, call_next):\n                result = await call_next(context)\n                # Filter out prompts with \"private\" tag\n                filtered_prompts = [\n                    prompt for prompt in result if \"private\" not in prompt.tags\n                ]\n                return filtered_prompts\n\n        server = FastMCP(\"TestServer\")\n\n        @server.prompt\n        def public_prompt(name: str) -> str:\n            return f\"Hello {name}\"\n\n        @server.prompt(tags={\"private\"})\n        def private_prompt(secret: str) -> str:\n            return f\"Secret: {secret}\"\n\n        server.add_middleware(FilteringMiddleware())\n\n        async with Client(server) as client:\n            prompts = await client.list_prompts()\n\n        assert len(prompts) == 1\n        assert prompts[0].name == \"public_prompt\"\n\n    async def test_call_tool_middleware(self):\n        server = FastMCP()\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        class CallToolMiddleware(Middleware):\n            async def on_call_tool(\n                self,\n                context: MiddlewareContext[mcp.types.CallToolRequestParams],\n                call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],\n            ):\n                # modify argument\n                if context.message.name == \"add\":\n                    assert context.message.arguments is not None\n                    args = context.message.arguments\n                    assert isinstance(args[\"a\"], int)\n                    args[\"a\"] += 100\n\n                result = await call_next(context)\n\n                # modify result\n                if context.message.name == \"add\":\n                    assert result.structured_content is not None\n                    content = result.structured_content\n                    assert isinstance(content[\"result\"], int)\n                    content[\"result\"] += 5\n\n                return result\n\n        server.add_middleware(CallToolMiddleware())\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        assert isinstance(result.structured_content[\"result\"], int)\n        assert result.structured_content[\"result\"] == 108\n\n\nclass TestApplyMiddlewareParameter:\n    \"\"\"Tests for run_middleware parameter on execution methods.\"\"\"\n\n    async def test_call_tool_with_run_middleware_true(self):\n        \"\"\"Middleware is applied when run_middleware=True (default).\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        server.add_middleware(recording)\n\n        result = await server.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        assert result.structured_content[\"result\"] == 3  # type: ignore[union-attr,index]\n        assert recording.assert_called(hook=\"on_call_tool\", times=1)\n\n    async def test_call_tool_with_run_middleware_false(self):\n        \"\"\"Middleware is NOT applied when run_middleware=False.\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        server.add_middleware(recording)\n\n        result = await server.call_tool(\"add\", {\"a\": 1, \"b\": 2}, run_middleware=False)\n\n        assert result.structured_content[\"result\"] == 3  # type: ignore[union-attr,index]\n        # Middleware should not have been called\n        assert len(recording.calls) == 0\n\n    async def test_read_resource_with_run_middleware_true(self):\n        \"\"\"Middleware is applied when run_middleware=True (default).\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"test content\"\n\n        server.add_middleware(recording)\n\n        result = await server.read_resource(\"resource://test\")\n\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"test content\"\n        assert recording.assert_called(hook=\"on_read_resource\", times=1)\n\n    async def test_read_resource_with_run_middleware_false(self):\n        \"\"\"Middleware is NOT applied when run_middleware=False.\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"test content\"\n\n        server.add_middleware(recording)\n\n        result = await server.read_resource(\"resource://test\", run_middleware=False)\n\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"test content\"\n        # Middleware should not have been called\n        assert len(recording.calls) == 0\n\n    async def test_read_resource_template_with_run_middleware_false(self):\n        \"\"\"Templates also skip middleware when run_middleware=False.\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.resource(\"resource://items/{item_id}\")\n        def get_item(item_id: int) -> str:\n            return f\"item {item_id}\"\n\n        server.add_middleware(recording)\n\n        result = await server.read_resource(\"resource://items/42\", run_middleware=False)\n\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"item 42\"\n        assert len(recording.calls) == 0\n\n    async def test_render_prompt_with_run_middleware_true(self):\n        \"\"\"Middleware is applied when run_middleware=True (default).\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.prompt\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        server.add_middleware(recording)\n\n        result = await server.render_prompt(\"greet\", {\"name\": \"World\"})\n\n        assert len(result.messages) == 1\n        # content is TextContent | EmbeddedResource, but we know it's TextContent from the test\n        assert isinstance(result.messages[0].content, mcp.types.TextContent)\n        assert result.messages[0].content.text == \"Hello, World!\"\n        assert recording.assert_called(hook=\"on_get_prompt\", times=1)\n\n    async def test_render_prompt_with_run_middleware_false(self):\n        \"\"\"Middleware is NOT applied when run_middleware=False.\"\"\"\n        recording = RecordingMiddleware()\n        server = FastMCP()\n\n        @server.prompt\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        server.add_middleware(recording)\n\n        result = await server.render_prompt(\n            \"greet\", {\"name\": \"World\"}, run_middleware=False\n        )\n\n        assert len(result.messages) == 1\n        # content is TextContent | EmbeddedResource, but we know it's TextContent from the test\n        assert isinstance(result.messages[0].content, mcp.types.TextContent)\n        assert result.messages[0].content.text == \"Hello, World!\"\n        # Middleware should not have been called\n        assert len(recording.calls) == 0\n\n    async def test_middleware_modification_skipped_when_run_middleware_false(self):\n        \"\"\"Middleware that modifies args/results is skipped.\"\"\"\n\n        class ModifyingMiddleware(Middleware):\n            async def on_call_tool(self, context: MiddlewareContext, call_next):\n                # Double the 'a' argument\n                assert context.message.arguments is not None\n                context.message.arguments[\"a\"] *= 2\n                return await call_next(context)\n\n        server = FastMCP()\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        server.add_middleware(ModifyingMiddleware())\n\n        # With middleware: a=5 becomes a=10, result = 10 + 3 = 13\n        result_with = await server.call_tool(\"add\", {\"a\": 5, \"b\": 3})\n        assert result_with.structured_content[\"result\"] == 13  # type: ignore[union-attr,index]\n\n        # Without middleware: a=5 stays a=5, result = 5 + 3 = 8\n        result_without = await server.call_tool(\n            \"add\", {\"a\": 5, \"b\": 3}, run_middleware=False\n        )\n        assert result_without.structured_content[\"result\"] == 8  # type: ignore[union-attr,index]\n"
  },
  {
    "path": "tests/server/middleware/test_middleware_nested.py",
    "content": "from collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import Any\n\nimport mcp.types\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.tools.base import ToolResult\n\n\n@dataclass\nclass Recording:\n    # the hook is the name of the hook that was called, e.g. \"on_list_tools\"\n    hook: str\n    context: MiddlewareContext\n    result: mcp.types.ServerResult | None\n\n\nclass RecordingMiddleware(Middleware):\n    \"\"\"A middleware that automatically records all method calls.\"\"\"\n\n    def __init__(self, name: str | None = None):\n        super().__init__()\n        self.calls: list[Recording] = []\n        self.name = name\n\n    def __getattribute__(self, name: str) -> Callable:\n        \"\"\"Dynamically create recording methods for any on_* method.\"\"\"\n        if name.startswith(\"on_\"):\n\n            async def record_and_call(\n                context: MiddlewareContext, call_next: Callable\n            ) -> Any:\n                result = await call_next(context)\n\n                self.calls.append(Recording(hook=name, context=context, result=result))\n\n                return result\n\n            return record_and_call\n\n        return super().__getattribute__(name)\n\n    def get_calls(\n        self, method: str | None = None, hook: str | None = None\n    ) -> list[Recording]:\n        \"\"\"\n        Get all recorded calls for a specific method or hook.\n        Args:\n            method: The method to filter by (e.g. \"tools/list\")\n            hook: The hook to filter by (e.g. \"on_list_tools\")\n        Returns:\n            A list of recorded calls.\n        \"\"\"\n        calls = []\n        for recording in self.calls:\n            if method and hook:\n                if recording.context.method == method and recording.hook == hook:\n                    calls.append(recording)\n            elif method:\n                if recording.context.method == method:\n                    calls.append(recording)\n            elif hook:\n                if recording.hook == hook:\n                    calls.append(recording)\n            else:\n                calls.append(recording)\n        return calls\n\n    def assert_called(\n        self,\n        hook: str | None = None,\n        method: str | None = None,\n        times: int | None = None,\n        at_least: int | None = None,\n    ) -> bool:\n        \"\"\"Assert that a hook was called a specific number of times.\"\"\"\n\n        if times is not None and at_least is not None:\n            raise ValueError(\"Cannot specify both times and at_least\")\n        elif times is None and at_least is None:\n            times = 1\n\n        calls = self.get_calls(hook=hook, method=method)\n        actual_times = len(calls)\n        identifier = dict(hook=hook, method=method)\n\n        if times is not None:\n            assert actual_times == times, (\n                f\"Expected {times} calls for {identifier}, \"\n                f\"but was called {actual_times} times\"\n            )\n        elif at_least is not None:\n            assert actual_times >= at_least, (\n                f\"Expected at least {at_least} calls for {identifier}, \"\n                f\"but was called {actual_times} times\"\n            )\n        return True\n\n    def assert_not_called(self, hook: str | None = None, method: str | None = None):\n        \"\"\"Assert that a hook was not called.\"\"\"\n        calls = self.get_calls(hook=hook, method=method)\n        assert len(calls) == 0, f\"Expected {hook!r} to not be called\"\n        return True\n\n    def reset(self):\n        \"\"\"Clear all recorded calls.\"\"\"\n        self.calls.clear()\n\n\n@pytest.fixture\ndef recording_middleware():\n    \"\"\"Fixture that provides a recording middleware instance.\"\"\"\n    middleware = RecordingMiddleware(name=\"recording_middleware\")\n    yield middleware\n\n\n@pytest.fixture\ndef mcp_server(recording_middleware):\n    mcp = FastMCP()\n\n    @mcp.tool(tags={\"add-tool\"})\n    def add(a: int, b: int) -> int:\n        return a + b\n\n    @mcp.resource(\"resource://test\")\n    def test_resource() -> str:\n        return \"test resource\"\n\n    @mcp.resource(\"resource://test-template/{x}\")\n    def test_resource_with_path(x: int) -> str:\n        return f\"test resource with {x}\"\n\n    @mcp.prompt\n    def test_prompt(x: str) -> str:\n        return f\"test prompt with {x}\"\n\n    @mcp.tool\n    async def progress_tool(context: Context) -> None:\n        await context.report_progress(progress=1, total=10, message=\"test\")\n\n    @mcp.tool\n    async def log_tool(context: Context) -> None:\n        await context.info(message=\"test log\")\n\n    @mcp.tool\n    async def sample_tool(context: Context) -> None:\n        await context.sample(\"hello\")\n\n    mcp.add_middleware(recording_middleware)\n\n    # Register progress handler\n    @mcp._mcp_server.progress_notification()\n    async def handle_progress(\n        progress_token: str | int,\n        progress: float,\n        total: float | None,\n        message: str | None,\n    ):\n        print(\"HI\")\n\n    return mcp\n\n\nclass TestNestedMiddlewareHooks:\n    @pytest.fixture\n    @staticmethod\n    def nested_middleware():\n        return RecordingMiddleware(name=\"nested_middleware\")\n\n    @pytest.fixture\n    def nested_mcp_server(self, nested_middleware: RecordingMiddleware):\n        mcp = FastMCP(name=\"Nested MCP\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"test resource\"\n\n        @mcp.resource(\"resource://test-template/{x}\")\n        def test_resource_with_path(x: int) -> str:\n            return f\"test resource with {x}\"\n\n        @mcp.prompt\n        def test_prompt(x: str) -> str:\n            return f\"test prompt with {x}\"\n\n        @mcp.tool\n        async def progress_tool(context: Context) -> None:\n            await context.report_progress(progress=1, total=10, message=\"test\")\n\n        @mcp.tool\n        async def log_tool(context: Context) -> None:\n            await context.info(message=\"test log\")\n\n        @mcp.tool\n        async def sample_tool(context: Context) -> None:\n            await context.sample(\"hello\")\n\n        mcp.add_middleware(nested_middleware)\n\n        return mcp\n\n    async def test_call_tool_on_parent_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"tools/call\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_call_tool\", at_least=1)\n\n        assert nested_middleware.assert_called(method=\"tools/call\", times=0)\n\n    async def test_call_tool_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.call_tool(\"nested_add\", {\"a\": 1, \"b\": 2})\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"tools/call\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_call_tool\", at_least=1)\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(method=\"tools/call\", at_least=3)\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_call_tool\", at_least=1)\n\n    async def test_read_resource_on_parent_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.read_resource(\"resource://test\")\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n        assert nested_middleware.assert_called(times=0)\n\n    async def test_read_resource_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.read_resource(\"resource://nested/test\")\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n    async def test_read_resource_template_on_parent_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.read_resource(\"resource://test-template/1\")\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n        assert nested_middleware.assert_called(times=0)\n\n    async def test_read_resource_template_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.read_resource(\"resource://nested/test-template/1\")\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(method=\"resources/read\", at_least=3)\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_read_resource\", at_least=1)\n\n    async def test_get_prompt_on_parent_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.get_prompt(\"test_prompt\", {\"x\": \"test\"})\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"prompts/get\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_get_prompt\", at_least=1)\n\n        assert nested_middleware.assert_called(times=0)\n\n    async def test_get_prompt_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.get_prompt(\"nested_test_prompt\", {\"x\": \"test\"})\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"prompts/get\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_get_prompt\", at_least=1)\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(method=\"prompts/get\", at_least=3)\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_get_prompt\", at_least=1)\n\n    async def test_list_tools_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.list_tools()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"tools/list\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_list_tools\", at_least=1)\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(method=\"tools/list\", at_least=3)\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_list_tools\", at_least=1)\n\n    async def test_list_resources_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.list_resources()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(method=\"resources/list\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_list_resources\", at_least=1)\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(method=\"resources/list\", at_least=3)\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_list_resources\", at_least=1)\n\n    async def test_list_resource_templates_on_nested_server(\n        self,\n        mcp_server: FastMCP,\n        nested_mcp_server: FastMCP,\n        recording_middleware: RecordingMiddleware,\n        nested_middleware: RecordingMiddleware,\n    ):\n        mcp_server.mount(nested_mcp_server, namespace=\"nested\")\n\n        async with Client(mcp_server) as client:\n            await client.list_resource_templates()\n\n        assert recording_middleware.assert_called(at_least=3)\n        assert recording_middleware.assert_called(\n            method=\"resources/templates/list\", at_least=3\n        )\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert recording_middleware.assert_called(\n            hook=\"on_list_resource_templates\", at_least=1\n        )\n\n        assert nested_middleware.assert_called(at_least=3)\n        assert nested_middleware.assert_called(\n            method=\"resources/templates/list\", at_least=3\n        )\n        assert nested_middleware.assert_called(hook=\"on_message\", at_least=1)\n        assert nested_middleware.assert_called(hook=\"on_request\", at_least=1)\n        assert nested_middleware.assert_called(\n            hook=\"on_list_resource_templates\", at_least=1\n        )\n\n\nclass TestProxyServer:\n    async def test_call_tool(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        # proxy server will have its tools listed as well as called in order to\n        # apply transforms and filters prior to the call.\n        proxy_server = FastMCP.as_proxy(mcp_server, name=\"Proxy Server\")\n        async with Client(proxy_server) as client:\n            await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        assert recording_middleware.assert_called(at_least=6)\n        assert recording_middleware.assert_called(method=\"tools/call\", at_least=3)\n        assert recording_middleware.assert_called(method=\"tools/list\", at_least=3)\n        assert recording_middleware.assert_called(hook=\"on_message\", at_least=2)\n        assert recording_middleware.assert_called(hook=\"on_request\", at_least=2)\n        assert recording_middleware.assert_called(hook=\"on_call_tool\", at_least=1)\n        assert recording_middleware.assert_called(hook=\"on_list_tools\", at_least=1)\n\n    async def test_proxied_tags_are_visible_to_middleware(\n        self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware\n    ):\n        \"\"\"Tests that tags on remote FastMCP servers are visible to middleware\n        via proxy. See https://github.com/PrefectHQ/fastmcp/issues/1300\"\"\"\n        proxy_server = FastMCP.as_proxy(mcp_server, name=\"Proxy Server\")\n\n        TAGS = []\n\n        class TagMiddleware(Middleware):\n            async def on_list_tools(self, context: MiddlewareContext, call_next):\n                nonlocal TAGS\n                result = await call_next(context)\n                for tool in result:\n                    TAGS.append(tool.tags)\n                return result\n\n        proxy_server.add_middleware(TagMiddleware())\n\n        async with Client(proxy_server) as client:\n            await client.list_tools()\n\n        assert TAGS == [{\"add-tool\"}, set(), set(), set()]\n\n\nclass TestToolCallDenial:\n    \"\"\"Test denying tool calls in middleware using ToolError.\"\"\"\n\n    async def test_deny_tool_call_with_tool_error(self):\n        \"\"\"Test that middleware can deny tool calls by raising ToolError.\"\"\"\n\n        class AuthMiddleware(Middleware):\n            async def on_call_tool(\n                self,\n                context: MiddlewareContext[mcp.types.CallToolRequestParams],\n                call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],\n            ) -> ToolResult:\n                tool_name = context.message.name\n                if tool_name.lower() == \"restricted_tool\":\n                    raise ToolError(\"Access denied: tool is disabled\")\n                return await call_next(context)\n\n        server = FastMCP(\"TestServer\")\n\n        @server.tool\n        def allowed_tool(x: int) -> int:\n            \"\"\"This tool is allowed.\"\"\"\n            return x * 2\n\n        @server.tool\n        def restricted_tool(x: int) -> int:\n            \"\"\"This tool should be denied by middleware.\"\"\"\n            return x * 3\n\n        server.add_middleware(AuthMiddleware())\n\n        async with Client(server) as client:\n            # Allowed tool should work normally\n            result = await client.call_tool(\"allowed_tool\", {\"x\": 5})\n            assert result.structured_content is not None\n            assert result.structured_content[\"result\"] == 10\n\n            # Restricted tool should raise ToolError\n            with pytest.raises(ToolError) as exc_info:\n                await client.call_tool(\"restricted_tool\", {\"x\": 5})\n\n            # Verify the error message is preserved\n            assert \"Access denied: tool is disabled\" in str(exc_info.value)\n\n    async def test_middleware_can_selectively_deny_tools(self):\n        \"\"\"Test that middleware can deny specific tools while allowing others.\"\"\"\n\n        denied_tools = set()\n\n        class SelectiveAuthMiddleware(Middleware):\n            async def on_call_tool(\n                self,\n                context: MiddlewareContext[mcp.types.CallToolRequestParams],\n                call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],\n            ) -> ToolResult:\n                tool_name = context.message.name\n\n                # Deny tools that start with \"admin_\"\n                if tool_name.startswith(\"admin_\"):\n                    denied_tools.add(tool_name)\n                    raise ToolError(\n                        f\"Access denied: {tool_name} requires admin privileges\"\n                    )\n\n                return await call_next(context)\n\n        server = FastMCP(\"TestServer\")\n\n        @server.tool\n        def public_tool(x: int) -> int:\n            \"\"\"Public tool available to all.\"\"\"\n            return x + 1\n\n        @server.tool\n        def admin_delete(item_id: str) -> str:\n            \"\"\"Admin tool that should be denied.\"\"\"\n            return f\"Deleted {item_id}\"\n\n        @server.tool\n        def admin_config(setting: str, value: str) -> str:\n            \"\"\"Another admin tool that should be denied.\"\"\"\n            return f\"Set {setting} to {value}\"\n\n        server.add_middleware(SelectiveAuthMiddleware())\n\n        async with Client(server) as client:\n            # Public tool should work\n            result = await client.call_tool(\"public_tool\", {\"x\": 10})\n            assert result.structured_content is not None\n            assert result.structured_content[\"result\"] == 11\n\n            # Admin tools should be denied\n            with pytest.raises(ToolError) as exc_info:\n                await client.call_tool(\"admin_delete\", {\"item_id\": \"test123\"})\n            assert \"requires admin privileges\" in str(exc_info.value)\n\n            with pytest.raises(ToolError) as exc_info:\n                await client.call_tool(\n                    \"admin_config\", {\"setting\": \"debug\", \"value\": \"true\"}\n                )\n            assert \"requires admin privileges\" in str(exc_info.value)\n\n        # Verify both admin tools were denied\n        assert denied_tools == {\"admin_delete\", \"admin_config\"}\n\n\nclass TestMiddlewareRequestState:\n    \"\"\"Non-serializable state set in middleware must be visible to tools/resources.\n\n    Regression test for https://github.com/PrefectHQ/fastmcp/issues/3228.\n    \"\"\"\n\n    async def test_non_serializable_state_from_middleware_visible_in_tool(self):\n        server = FastMCP(\"test\")\n\n        sentinel = object()\n\n        class StateMiddleware(Middleware):\n            async def on_call_tool(\n                self, context: MiddlewareContext, call_next: CallNext\n            ) -> Any:\n                assert context.fastmcp_context is not None\n                await context.fastmcp_context.set_state(\n                    \"obj\", sentinel, serializable=False\n                )\n                return await call_next(context)\n\n        server.add_middleware(StateMiddleware())\n\n        @server.tool()\n        async def read_it(ctx: Context) -> str:\n            val = await ctx.get_state(\"obj\")\n            return \"found\" if val is sentinel else \"missing\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"read_it\")\n            assert result.content[0].text == \"found\"\n\n    async def test_non_serializable_state_from_middleware_visible_in_resource(self):\n        server = FastMCP(\"test\")\n\n        sentinel = object()\n\n        class StateMiddleware(Middleware):\n            async def on_read_resource(\n                self, context: MiddlewareContext, call_next: CallNext\n            ) -> Any:\n                assert context.fastmcp_context is not None\n                await context.fastmcp_context.set_state(\n                    \"obj\", sentinel, serializable=False\n                )\n                return await call_next(context)\n\n        server.add_middleware(StateMiddleware())\n\n        @server.resource(\"test://data\")\n        async def read_it(ctx: Context) -> str:\n            val = await ctx.get_state(\"obj\")\n            return \"found\" if val is sentinel else \"missing\"\n\n        async with Client(server) as client:\n            result = await client.read_resource(\"test://data\")\n            assert result[0].text == \"found\"\n"
  },
  {
    "path": "tests/server/middleware/test_ping.py",
    "content": "\"\"\"Tests for ping middleware.\"\"\"\n\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport anyio\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.middleware.ping import PingMiddleware\n\n\nclass TestPingMiddlewareInit:\n    \"\"\"Test PingMiddleware initialization.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = PingMiddleware()\n        assert middleware.interval_ms == 30000\n        assert middleware._active_sessions == set()\n\n    def test_init_custom(self):\n        \"\"\"Test custom interval initialization.\"\"\"\n        middleware = PingMiddleware(interval_ms=5000)\n        assert middleware.interval_ms == 5000\n\n    def test_init_invalid_interval_zero(self):\n        \"\"\"Test that zero interval raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"interval_ms must be positive\"):\n            PingMiddleware(interval_ms=0)\n\n    def test_init_invalid_interval_negative(self):\n        \"\"\"Test that negative interval raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"interval_ms must be positive\"):\n            PingMiddleware(interval_ms=-1000)\n\n\nclass TestPingMiddlewareOnMessage:\n    \"\"\"Test on_message hook behavior.\"\"\"\n\n    async def test_starts_ping_task_on_first_message(self):\n        \"\"\"Test that ping task is started on first message from a session.\"\"\"\n        middleware = PingMiddleware(interval_ms=1000)\n\n        mock_session = MagicMock()\n        mock_session._subscription_task_group = MagicMock()\n        mock_session._subscription_task_group.start_soon = MagicMock()\n\n        mock_context = MagicMock()\n        mock_context.fastmcp_context.session = mock_session\n\n        mock_call_next = AsyncMock(return_value=\"result\")\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"result\"\n        assert id(mock_session) in middleware._active_sessions\n        mock_session._subscription_task_group.start_soon.assert_called_once()\n\n    async def test_does_not_start_duplicate_task(self):\n        \"\"\"Test that duplicate messages from same session don't spawn duplicate tasks.\"\"\"\n        middleware = PingMiddleware(interval_ms=1000)\n\n        mock_session = MagicMock()\n        mock_session._subscription_task_group = MagicMock()\n        mock_session._subscription_task_group.start_soon = MagicMock()\n\n        mock_context = MagicMock()\n        mock_context.fastmcp_context.session = mock_session\n\n        mock_call_next = AsyncMock(return_value=\"result\")\n\n        # First message\n        await middleware.on_message(mock_context, mock_call_next)\n        # Second message from same session\n        await middleware.on_message(mock_context, mock_call_next)\n        # Third message from same session\n        await middleware.on_message(mock_context, mock_call_next)\n\n        # Should only start task once\n        assert mock_session._subscription_task_group.start_soon.call_count == 1\n\n    async def test_starts_separate_task_per_session(self):\n        \"\"\"Test that different sessions get separate ping tasks.\"\"\"\n        middleware = PingMiddleware(interval_ms=1000)\n\n        mock_session1 = MagicMock()\n        mock_session1._subscription_task_group = MagicMock()\n        mock_session1._subscription_task_group.start_soon = MagicMock()\n\n        mock_session2 = MagicMock()\n        mock_session2._subscription_task_group = MagicMock()\n        mock_session2._subscription_task_group.start_soon = MagicMock()\n\n        mock_context1 = MagicMock()\n        mock_context1.fastmcp_context.session = mock_session1\n\n        mock_context2 = MagicMock()\n        mock_context2.fastmcp_context.session = mock_session2\n\n        mock_call_next = AsyncMock(return_value=\"result\")\n\n        await middleware.on_message(mock_context1, mock_call_next)\n        await middleware.on_message(mock_context2, mock_call_next)\n\n        mock_session1._subscription_task_group.start_soon.assert_called_once()\n        mock_session2._subscription_task_group.start_soon.assert_called_once()\n        assert len(middleware._active_sessions) == 2\n\n    async def test_skips_task_when_no_task_group(self):\n        \"\"\"Test graceful handling when session has no task group.\"\"\"\n        middleware = PingMiddleware(interval_ms=1000)\n\n        mock_session = MagicMock()\n        mock_session._subscription_task_group = None\n\n        mock_context = MagicMock()\n        mock_context.fastmcp_context.session = mock_session\n\n        mock_call_next = AsyncMock(return_value=\"result\")\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"result\"\n        # Session should NOT be added if task group is None\n        assert id(mock_session) not in middleware._active_sessions\n\n    async def test_skips_when_fastmcp_context_is_none(self):\n        \"\"\"Test that middleware passes through when fastmcp_context is None.\"\"\"\n        middleware = PingMiddleware(interval_ms=1000)\n\n        mock_context = MagicMock()\n        mock_context.fastmcp_context = None\n\n        mock_call_next = AsyncMock(return_value=\"result\")\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"result\"\n        assert len(middleware._active_sessions) == 0\n\n    async def test_skips_when_request_context_is_none(self):\n        \"\"\"Test that middleware passes through when request_context is None.\"\"\"\n        middleware = PingMiddleware(interval_ms=1000)\n\n        mock_context = MagicMock()\n        mock_context.fastmcp_context = MagicMock()\n        mock_context.fastmcp_context.request_context = None\n\n        mock_call_next = AsyncMock(return_value=\"result\")\n\n        result = await middleware.on_message(mock_context, mock_call_next)\n\n        assert result == \"result\"\n        assert len(middleware._active_sessions) == 0\n\n\nclass TestPingLoop:\n    \"\"\"Test the ping loop behavior.\"\"\"\n\n    async def test_ping_loop_sends_pings_at_interval(self):\n        \"\"\"Test that ping loop sends pings at configured interval.\"\"\"\n        middleware = PingMiddleware(interval_ms=50)\n\n        mock_session = MagicMock()\n        mock_session.send_ping = AsyncMock()\n\n        session_id = id(mock_session)\n        middleware._active_sessions.add(session_id)\n\n        # Run ping loop for a short time then cancel\n        with anyio.move_on_after(0.35):\n            await middleware._ping_loop(mock_session, session_id)\n\n        # Should have sent at least 2 pings in 350ms with 50ms interval\n        assert mock_session.send_ping.call_count >= 2\n\n    async def test_ping_loop_cleans_up_on_cancellation(self):\n        \"\"\"Test that session is removed from active sessions on cancellation.\"\"\"\n        middleware = PingMiddleware(interval_ms=50)\n\n        mock_session = MagicMock()\n        mock_session.send_ping = AsyncMock()\n\n        session_id = 12345\n        middleware._active_sessions.add(session_id)\n\n        # Run and cancel the ping loop\n        with anyio.move_on_after(0.1):\n            await middleware._ping_loop(mock_session, session_id)\n\n        # Session should be cleaned up after cancellation\n        assert session_id not in middleware._active_sessions\n\n\nclass TestPingMiddlewareIntegration:\n    \"\"\"Integration tests for PingMiddleware with real FastMCP server.\"\"\"\n\n    async def test_ping_middleware_registers_session(self):\n        \"\"\"Test that PingMiddleware registers sessions on first request.\"\"\"\n        mcp = FastMCP(\"PingTestServer\")\n        middleware = PingMiddleware(interval_ms=50)\n        mcp.add_middleware(middleware)\n\n        @mcp.tool\n        def hello() -> str:\n            return \"Hello!\"\n\n        assert len(middleware._active_sessions) == 0\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"hello\")\n            assert result.content[0].text == \"Hello!\"\n\n            # Should have registered the session\n            assert len(middleware._active_sessions) == 1\n\n            # Make another request - should not add duplicate\n            await client.call_tool(\"hello\")\n            assert len(middleware._active_sessions) == 1\n\n    async def test_ping_task_cancelled_on_disconnect(self):\n        \"\"\"Test that ping task is properly cancelled when client disconnects.\"\"\"\n        mcp = FastMCP(\"PingTestServer\")\n        middleware = PingMiddleware(interval_ms=50)\n        mcp.add_middleware(middleware)\n\n        @mcp.tool\n        def hello() -> str:\n            return \"Hello!\"\n\n        async with Client(mcp) as client:\n            await client.call_tool(\"hello\")\n            # Should have one active session\n            assert len(middleware._active_sessions) == 1\n\n        # After disconnect, give a moment for cleanup\n        await anyio.sleep(0.01)\n\n        # Session should be cleaned up\n        assert len(middleware._active_sessions) == 0\n"
  },
  {
    "path": "tests/server/middleware/test_rate_limiting.py",
    "content": "\"\"\"Tests for rate limiting middleware.\"\"\"\n\nimport asyncio\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.middleware.middleware import MiddlewareContext\nfrom fastmcp.server.middleware.rate_limiting import (\n    RateLimitError,\n    RateLimitingMiddleware,\n    SlidingWindowRateLimiter,\n    SlidingWindowRateLimitingMiddleware,\n    TokenBucketRateLimiter,\n)\n\n\n@pytest.fixture\ndef mock_context():\n    \"\"\"Create a mock middleware context.\"\"\"\n    context = MagicMock(spec=MiddlewareContext)\n    context.method = \"test_method\"\n    return context\n\n\n@pytest.fixture\ndef mock_call_next():\n    \"\"\"Create a mock call_next function.\"\"\"\n    return AsyncMock(return_value=\"test_result\")\n\n\nclass TestTokenBucketRateLimiter:\n    \"\"\"Test token bucket rate limiter.\"\"\"\n\n    def test_init(self):\n        \"\"\"Test initialization.\"\"\"\n        limiter = TokenBucketRateLimiter(capacity=10, refill_rate=5.0)\n        assert limiter.capacity == 10\n        assert limiter.refill_rate == 5.0\n        assert limiter.tokens == 10\n\n    async def test_consume_success(self):\n        \"\"\"Test successful token consumption.\"\"\"\n        limiter = TokenBucketRateLimiter(capacity=10, refill_rate=5.0)\n\n        # Should be able to consume tokens initially\n        assert await limiter.consume(5) is True\n        assert await limiter.consume(3) is True\n\n    async def test_consume_failure(self):\n        \"\"\"Test failed token consumption.\"\"\"\n        limiter = TokenBucketRateLimiter(capacity=5, refill_rate=1.0)\n\n        # Consume all tokens\n        assert await limiter.consume(5) is True\n\n        # Should fail to consume more\n        assert await limiter.consume(1) is False\n\n    async def test_refill(self):\n        \"\"\"Test token refill over time.\"\"\"\n        limiter = TokenBucketRateLimiter(\n            capacity=10, refill_rate=10.0\n        )  # 10 tokens per second\n\n        # Consume all tokens\n        assert await limiter.consume(10) is True\n        assert await limiter.consume(1) is False\n\n        # Wait for refill (0.2 seconds = 2 tokens at 10/sec)\n        await asyncio.sleep(0.2)\n        assert await limiter.consume(2) is True\n\n\nclass TestSlidingWindowRateLimiter:\n    \"\"\"Test sliding window rate limiter.\"\"\"\n\n    def test_init(self):\n        \"\"\"Test initialization.\"\"\"\n        limiter = SlidingWindowRateLimiter(max_requests=10, window_seconds=60)\n        assert limiter.max_requests == 10\n        assert limiter.window_seconds == 60\n        assert len(limiter.requests) == 0\n\n    async def test_is_allowed_success(self):\n        \"\"\"Test allowing requests within limit.\"\"\"\n        limiter = SlidingWindowRateLimiter(max_requests=3, window_seconds=60)\n\n        # Should allow requests up to the limit\n        assert await limiter.is_allowed() is True\n        assert await limiter.is_allowed() is True\n        assert await limiter.is_allowed() is True\n\n    async def test_is_allowed_failure(self):\n        \"\"\"Test rejecting requests over limit.\"\"\"\n        limiter = SlidingWindowRateLimiter(max_requests=2, window_seconds=60)\n\n        # Should allow up to limit\n        assert await limiter.is_allowed() is True\n        assert await limiter.is_allowed() is True\n\n        # Should reject over limit\n        assert await limiter.is_allowed() is False\n\n    async def test_sliding_window(self):\n        \"\"\"Test sliding window behavior.\"\"\"\n        limiter = SlidingWindowRateLimiter(max_requests=2, window_seconds=1)\n\n        # Use up requests\n        assert await limiter.is_allowed() is True\n        assert await limiter.is_allowed() is True\n        assert await limiter.is_allowed() is False\n\n        # Wait for window to pass\n        await asyncio.sleep(1.1)\n\n        # Should be able to make requests again\n        assert await limiter.is_allowed() is True\n\n\nclass TestRateLimitingMiddleware:\n    \"\"\"Test rate limiting middleware.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = RateLimitingMiddleware()\n        assert middleware.max_requests_per_second == 10.0\n        assert middleware.burst_capacity == 20\n        assert middleware.get_client_id is None\n        assert middleware.global_limit is False\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n\n        def get_client_id(ctx):\n            return \"test_client\"\n\n        middleware = RateLimitingMiddleware(\n            max_requests_per_second=5.0,\n            burst_capacity=10,\n            get_client_id=get_client_id,\n            global_limit=True,\n        )\n        assert middleware.max_requests_per_second == 5.0\n        assert middleware.burst_capacity == 10\n        assert middleware.get_client_id is get_client_id\n        assert middleware.global_limit is True\n\n    def test_get_client_identifier_default(self, mock_context):\n        \"\"\"Test default client identifier.\"\"\"\n        middleware = RateLimitingMiddleware()\n        assert middleware._get_client_identifier(mock_context) == \"global\"\n\n    def test_get_client_identifier_custom(self, mock_context):\n        \"\"\"Test custom client identifier.\"\"\"\n\n        def get_client_id(ctx):\n            return \"custom_client\"\n\n        middleware = RateLimitingMiddleware(get_client_id=get_client_id)\n        assert middleware._get_client_identifier(mock_context) == \"custom_client\"\n\n    async def test_on_request_success(self, mock_context, mock_call_next):\n        \"\"\"Test successful request within rate limit.\"\"\"\n        middleware = RateLimitingMiddleware(max_requests_per_second=100.0)  # High limit\n\n        result = await middleware.on_request(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.called\n\n    async def test_on_request_rate_limited(self, mock_context, mock_call_next):\n        \"\"\"Test request rejection due to rate limiting.\"\"\"\n        middleware = RateLimitingMiddleware(\n            max_requests_per_second=1.0, burst_capacity=1\n        )\n\n        # First request should succeed\n        await middleware.on_request(mock_context, mock_call_next)\n\n        # Second request should be rate limited\n        with pytest.raises(RateLimitError, match=\"Rate limit exceeded\"):\n            await middleware.on_request(mock_context, mock_call_next)\n\n    async def test_global_rate_limiting(self, mock_context, mock_call_next):\n        \"\"\"Test global rate limiting.\"\"\"\n        middleware = RateLimitingMiddleware(\n            max_requests_per_second=1.0, burst_capacity=1, global_limit=True\n        )\n\n        # First request should succeed\n        await middleware.on_request(mock_context, mock_call_next)\n\n        # Second request should be rate limited\n        with pytest.raises(RateLimitError, match=\"Global rate limit exceeded\"):\n            await middleware.on_request(mock_context, mock_call_next)\n\n\nclass TestSlidingWindowRateLimitingMiddleware:\n    \"\"\"Test sliding window rate limiting middleware.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = SlidingWindowRateLimitingMiddleware(max_requests=100)\n        assert middleware.max_requests == 100\n        assert middleware.window_seconds == 60\n        assert middleware.get_client_id is None\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n\n        def get_client_id(ctx):\n            return \"test_client\"\n\n        middleware = SlidingWindowRateLimitingMiddleware(\n            max_requests=50, window_minutes=5, get_client_id=get_client_id\n        )\n        assert middleware.max_requests == 50\n        assert middleware.window_seconds == 300  # 5 minutes\n        assert middleware.get_client_id is get_client_id\n\n    async def test_on_request_success(self, mock_context, mock_call_next):\n        \"\"\"Test successful request within rate limit.\"\"\"\n        middleware = SlidingWindowRateLimitingMiddleware(max_requests=100)\n\n        result = await middleware.on_request(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.called\n\n    async def test_on_request_rate_limited(self, mock_context, mock_call_next):\n        \"\"\"Test request rejection due to rate limiting.\"\"\"\n        middleware = SlidingWindowRateLimitingMiddleware(max_requests=1)\n\n        # First request should succeed\n        await middleware.on_request(mock_context, mock_call_next)\n\n        # Second request should be rate limited\n        with pytest.raises(RateLimitError, match=\"Rate limit exceeded\"):\n            await middleware.on_request(mock_context, mock_call_next)\n\n\nclass TestRateLimitError:\n    \"\"\"Test rate limit error.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        error = RateLimitError()\n        assert error.error.code == -32000\n        assert error.error.message == \"Rate limit exceeded\"\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n        error = RateLimitError(\"Custom message\")\n        assert error.error.code == -32000\n        assert error.error.message == \"Custom message\"\n\n\n@pytest.fixture\ndef rate_limit_server():\n    \"\"\"Create a FastMCP server specifically for rate limiting tests.\"\"\"\n    mcp = FastMCP(\"RateLimitTestServer\")\n\n    @mcp.tool\n    def quick_action(message: str) -> str:\n        \"\"\"A quick action for testing rate limits.\"\"\"\n        return f\"Processed: {message}\"\n\n    @mcp.tool\n    def batch_process(items: list[str]) -> str:\n        \"\"\"Process multiple items.\"\"\"\n        return f\"Processed {len(items)} items\"\n\n    @mcp.tool\n    def heavy_computation() -> str:\n        \"\"\"A heavy computation that might need rate limiting.\"\"\"\n        # Simulate some work\n        import time\n\n        time.sleep(0.01)  # Very short delay\n        return \"Heavy computation complete\"\n\n    return mcp\n\n\nclass TestRateLimitingMiddlewareIntegration:\n    \"\"\"Integration tests for rate limiting middleware with real FastMCP server.\"\"\"\n\n    async def test_rate_limiting_allows_normal_usage(self, rate_limit_server):\n        \"\"\"Test that normal usage patterns are allowed through rate limiting.\"\"\"\n        # Generous rate limit\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(max_requests_per_second=50.0, burst_capacity=10)\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Normal usage should be fine\n            for i in range(5):\n                result = await client.call_tool(\n                    \"quick_action\", {\"message\": f\"task_{i}\"}\n                )\n                assert f\"Processed: task_{i}\" in str(result)\n\n    async def test_rate_limiting_blocks_rapid_requests(self, rate_limit_server):\n        \"\"\"Test that rate limiting blocks rapid successive requests.\"\"\"\n        # Use a generous burst but near-zero refill rate so tokens never\n        # replenish.  The MCP SDK sends internal messages (initialize,\n        # notifications, list_tools for validation) whose exact count\n        # varies, so we give enough burst for init + several calls, then\n        # keep firing until we hit the limit.\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(max_requests_per_second=0.001, burst_capacity=20)\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Fire enough calls to exhaust the burst.  With near-zero\n            # refill, we must eventually hit the limit.\n            hit_limit = False\n            for i in range(30):\n                try:\n                    await client.call_tool(\"quick_action\", {\"message\": str(i)})\n                except ToolError as exc:\n                    assert \"Rate limit exceeded\" in str(exc)\n                    hit_limit = True\n                    break\n            assert hit_limit, \"Rate limit was never triggered\"\n\n    async def test_rate_limiting_with_concurrent_requests(self, rate_limit_server):\n        \"\"\"Test rate limiting behavior with concurrent requests.\"\"\"\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(max_requests_per_second=15.0, burst_capacity=8)\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Fire off many concurrent requests\n            tasks = []\n            for i in range(8):\n                task = asyncio.create_task(\n                    client.call_tool(\"quick_action\", {\"message\": f\"concurrent_{i}\"})\n                )\n                tasks.append(task)\n\n            # Gather results, allowing exceptions\n            results = await asyncio.gather(*tasks, return_exceptions=True)\n\n            # With extra list_tools calls, the exact behavior is unpredictable\n            # Just verify that rate limiting is working (not all succeed)\n            successes = [r for r in results if not isinstance(r, Exception)]\n            failures = [r for r in results if isinstance(r, Exception)]\n\n            total_results = len(successes) + len(failures)\n            assert total_results == 8, f\"Expected 8 results, got {total_results}\"\n\n            # With the unpredictable list_tools calls, we just verify that the system\n            # is working (all requests should either succeed or fail with some exception)\n            assert 0 <= len(successes) <= 8, \"Should have between 0-8 successes\"\n            assert 0 <= len(failures) <= 8, \"Should have between 0-8 failures\"\n\n    async def test_sliding_window_rate_limiting(self, rate_limit_server):\n        \"\"\"Test sliding window rate limiting implementation.\"\"\"\n        rate_limit_server.add_middleware(\n            SlidingWindowRateLimitingMiddleware(\n                max_requests=6,  # 1 init + 1 list_tools + 3 calls + 1 to fail\n                window_minutes=1,  # 1-minute window\n            )\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Should allow up to the limit\n            await client.call_tool(\"quick_action\", {\"message\": \"1\"})\n            await client.call_tool(\"quick_action\", {\"message\": \"2\"})\n            await client.call_tool(\"quick_action\", {\"message\": \"3\"})\n\n            # Fourth should be blocked\n            with pytest.raises(ToolError, match=\"Rate limit exceeded\"):\n                await client.call_tool(\"quick_action\", {\"message\": \"4\"})\n\n    async def test_rate_limiting_with_different_operations(self, rate_limit_server):\n        \"\"\"Test that rate limiting applies to all types of operations.\"\"\"\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(max_requests_per_second=9.0, burst_capacity=5)\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Mix different operations\n            await client.call_tool(\"quick_action\", {\"message\": \"test\"})\n            await client.call_tool(\"heavy_computation\")\n\n            # Should be rate limited regardless of operation type\n            with pytest.raises(ToolError, match=\"Rate limit exceeded\"):\n                await client.call_tool(\"batch_process\", {\"items\": [\"a\", \"b\", \"c\"]})\n\n    async def test_custom_client_identification(self, rate_limit_server):\n        \"\"\"Test rate limiting with custom client identification.\"\"\"\n\n        def get_client_id(context):\n            # In a real scenario, this might extract from headers or context\n            return \"test_client_123\"\n\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(\n                max_requests_per_second=1.0,  # Very slow refill to ensure rate limiting triggers\n                burst_capacity=4,  # init + list_tools + call + list_tools = 4, so 2nd call fails\n                get_client_id=get_client_id,\n            )\n        )\n\n        async with Client(rate_limit_server) as client:\n            # First request should succeed\n            await client.call_tool(\"quick_action\", {\"message\": \"first\"})\n\n            # Second should be rate limited for this specific client\n            with pytest.raises(ToolError) as exc_info:\n                await client.call_tool(\"quick_action\", {\"message\": \"second\"})\n            assert \"Rate limit exceeded for client: test_client_123\" in str(\n                exc_info.value\n            )\n\n    async def test_global_rate_limiting(self, rate_limit_server):\n        \"\"\"Test global rate limiting across all clients.\"\"\"\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(\n                max_requests_per_second=6.0,\n                burst_capacity=5,  # 1 init + 2 list_tools + 2 calls before limit\n                global_limit=True,  # Accounting for initialization and list_tools calls\n            )\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Use up the global capacity\n            await client.call_tool(\"quick_action\", {\"message\": \"1\"})\n            await client.call_tool(\"quick_action\", {\"message\": \"2\"})\n\n            # Should be globally rate limited\n            with pytest.raises(ToolError, match=\"Global rate limit exceeded\"):\n                await client.call_tool(\"quick_action\", {\"message\": \"3\"})\n\n    async def test_rate_limiting_recovery_over_time(self, rate_limit_server):\n        \"\"\"Test that rate limiting allows requests again after time passes.\"\"\"\n        rate_limit_server.add_middleware(\n            RateLimitingMiddleware(\n                max_requests_per_second=10.0,  # 10 per second = 1 every 100ms\n                burst_capacity=4,\n            )\n        )\n\n        async with Client(rate_limit_server) as client:\n            # Use up capacity\n            await client.call_tool(\"quick_action\", {\"message\": \"first\"})\n\n            # Should be rate limited immediately\n            with pytest.raises(ToolError):\n                await client.call_tool(\"quick_action\", {\"message\": \"second\"})\n\n            # Wait for token bucket to refill (150ms should be enough for ~1.5 tokens)\n            await asyncio.sleep(0.15)\n\n            # Should be able to make another request\n            result = await client.call_tool(\"quick_action\", {\"message\": \"after_wait\"})\n            assert \"after_wait\" in str(result)\n"
  },
  {
    "path": "tests/server/middleware/test_response_limiting.py",
    "content": "\"\"\"Tests for ResponseLimitingMiddleware.\"\"\"\n\nimport pytest\nfrom mcp.types import ImageContent, TextContent\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware\nfrom fastmcp.tools.base import ToolResult\n\n\nclass TestResponseLimitingMiddleware:\n    \"\"\"Tests for ResponseLimitingMiddleware.\"\"\"\n\n    @pytest.fixture\n    def mcp_server(self) -> FastMCP:\n        \"\"\"Create a basic MCP server for testing.\"\"\"\n        return FastMCP(\"test-server\")\n\n    async def test_response_under_limit_passes_unchanged(self, mcp_server: FastMCP):\n        \"\"\"Test that responses under the limit pass through unchanged.\"\"\"\n        mcp_server.add_middleware(ResponseLimitingMiddleware(max_size=1_000_000))\n\n        @mcp_server.tool()\n        def small_tool() -> ToolResult:\n            return ToolResult(content=[TextContent(type=\"text\", text=\"hello world\")])\n\n        async with Client(mcp_server) as client:\n            result = await client.call_tool(\"small_tool\", {})\n            assert len(result.content) == 1\n            assert result.content[0].text == \"hello world\"\n\n    async def test_response_over_limit_is_truncated(self, mcp_server: FastMCP):\n        \"\"\"Test that responses over the limit are truncated.\"\"\"\n        mcp_server.add_middleware(ResponseLimitingMiddleware(max_size=500))\n\n        @mcp_server.tool()\n        def large_tool() -> ToolResult:\n            return ToolResult(content=[TextContent(type=\"text\", text=\"x\" * 10_000)])\n\n        async with Client(mcp_server) as client:\n            result = await client.call_tool(\"large_tool\", {})\n            assert len(result.content) == 1\n            assert \"[Response truncated due to size limit]\" in result.content[0].text\n            # Verify truncated result fits within limit\n            assert len(result.content[0].text.encode(\"utf-8\")) < 500\n\n    async def test_tool_filtering(self, mcp_server: FastMCP):\n        \"\"\"Test that tool filtering only applies to specified tools.\"\"\"\n        mcp_server.add_middleware(\n            ResponseLimitingMiddleware(max_size=100, tools=[\"limited_tool\"])\n        )\n\n        @mcp_server.tool()\n        def limited_tool() -> ToolResult:\n            return ToolResult(content=[TextContent(type=\"text\", text=\"x\" * 10_000)])\n\n        @mcp_server.tool()\n        def unlimited_tool() -> ToolResult:\n            return ToolResult(content=[TextContent(type=\"text\", text=\"y\" * 10_000)])\n\n        async with Client(mcp_server) as client:\n            # Limited tool should be truncated\n            result = await client.call_tool(\"limited_tool\", {})\n            assert \"[Response truncated\" in result.content[0].text\n\n            # Unlimited tool should pass through\n            result = await client.call_tool(\"unlimited_tool\", {})\n            assert \"y\" * 100 in result.content[0].text\n\n    async def test_empty_tools_list_limits_nothing(self, mcp_server: FastMCP):\n        \"\"\"Test that empty tools list means no tools are limited.\"\"\"\n        mcp_server.add_middleware(ResponseLimitingMiddleware(max_size=100, tools=[]))\n\n        @mcp_server.tool()\n        def any_tool() -> ToolResult:\n            return ToolResult(content=[TextContent(type=\"text\", text=\"x\" * 10_000)])\n\n        async with Client(mcp_server) as client:\n            result = await client.call_tool(\"any_tool\", {})\n            # Should NOT be truncated\n            assert \"[Response truncated\" not in result.content[0].text\n\n    async def test_custom_truncation_suffix(self, mcp_server: FastMCP):\n        \"\"\"Test that custom truncation suffix is applied.\"\"\"\n        mcp_server.add_middleware(\n            ResponseLimitingMiddleware(max_size=200, truncation_suffix=\"\\n[CUT]\")\n        )\n\n        @mcp_server.tool()\n        def large_tool() -> ToolResult:\n            return ToolResult(content=[TextContent(type=\"text\", text=\"x\" * 10_000)])\n\n        async with Client(mcp_server) as client:\n            result = await client.call_tool(\"large_tool\", {})\n            assert \"[CUT]\" in result.content[0].text\n\n    async def test_multiple_text_blocks_combined(self, mcp_server: FastMCP):\n        \"\"\"Test that multiple text blocks are combined when truncating.\"\"\"\n        mcp_server.add_middleware(ResponseLimitingMiddleware(max_size=300))\n\n        @mcp_server.tool()\n        def multi_block() -> ToolResult:\n            return ToolResult(\n                content=[\n                    TextContent(type=\"text\", text=\"First: \" + \"a\" * 500),\n                    TextContent(type=\"text\", text=\"Second: \" + \"b\" * 500),\n                ]\n            )\n\n        async with Client(mcp_server) as client:\n            result = await client.call_tool(\"multi_block\", {})\n            # Both blocks should be joined and truncated\n            assert len(result.content) == 1\n            assert \"[Response truncated\" in result.content[0].text\n\n    async def test_binary_only_content_serialized(self, mcp_server: FastMCP):\n        \"\"\"Test that binary-only responses fall back to serialized content.\"\"\"\n        mcp_server.add_middleware(ResponseLimitingMiddleware(max_size=200))\n\n        @mcp_server.tool()\n        def binary_tool() -> ToolResult:\n            return ToolResult(\n                content=[\n                    ImageContent(type=\"image\", data=\"x\" * 10_000, mimeType=\"image/png\")\n                ]\n            )\n\n        async with Client(mcp_server) as client:\n            result = await client.call_tool(\"binary_tool\", {})\n            # Should be truncated (using serialized fallback)\n            assert len(result.content) == 1\n            assert \"[Response truncated\" in result.content[0].text\n\n    async def test_default_max_size_is_1mb(self):\n        \"\"\"Test that the default max size is 1MB.\"\"\"\n        middleware = ResponseLimitingMiddleware()\n        assert middleware.max_size == 1_000_000\n\n    def test_invalid_max_size_raises(self):\n        \"\"\"Test that zero or negative max_size raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"max_size must be positive\"):\n            ResponseLimitingMiddleware(max_size=0)\n        with pytest.raises(ValueError, match=\"max_size must be positive\"):\n            ResponseLimitingMiddleware(max_size=-100)\n\n    def test_utf8_truncation_preserves_characters(self):\n        \"\"\"Test that UTF-8 truncation doesn't break multi-byte characters.\"\"\"\n        middleware = ResponseLimitingMiddleware(max_size=100)\n        # Text with multi-byte characters (emoji)\n        text = \"Hello 🌍 World 🎉 Test \" * 100\n        result = middleware._truncate_to_result(text)\n        # Should not raise and should be valid UTF-8\n        content = result.content[0]\n        assert isinstance(content, TextContent)\n        content.text.encode(\"utf-8\")\n"
  },
  {
    "path": "tests/server/middleware/test_timing.py",
    "content": "\"\"\"Tests for timing middleware.\"\"\"\n\nimport asyncio\nimport logging\nimport time\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.middleware.middleware import MiddlewareContext\nfrom fastmcp.server.middleware.timing import DetailedTimingMiddleware, TimingMiddleware\n\n\n@pytest.fixture\ndef mock_context():\n    \"\"\"Create a mock middleware context.\"\"\"\n    context = MagicMock(spec=MiddlewareContext)\n    context.method = \"test_method\"\n    return context\n\n\n@pytest.fixture\ndef mock_call_next():\n    \"\"\"Create a mock call_next function.\"\"\"\n    return AsyncMock(return_value=\"test_result\")\n\n\nclass TestTimingMiddleware:\n    \"\"\"Test timing middleware functionality.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = TimingMiddleware()\n        assert middleware.logger.name == \"fastmcp.timing\"\n        assert middleware.log_level == logging.INFO\n\n    def test_init_custom(self):\n        \"\"\"Test custom initialization.\"\"\"\n        logger = logging.getLogger(\"custom\")\n        middleware = TimingMiddleware(logger=logger, log_level=logging.DEBUG)\n        assert middleware.logger is logger\n        assert middleware.log_level == logging.DEBUG\n\n    async def test_on_request_success(self, mock_context, mock_call_next, caplog):\n        \"\"\"Test timing successful requests.\"\"\"\n        middleware = TimingMiddleware()\n\n        result = await middleware.on_request(mock_context, mock_call_next)\n\n        assert result == \"test_result\"\n        assert mock_call_next.called\n        assert \"Request test_method completed in\" in caplog.text\n        assert \"ms\" in caplog.text\n\n    async def test_on_request_failure(self, mock_context, caplog):\n        \"\"\"Test timing failed requests.\"\"\"\n        middleware = TimingMiddleware()\n        mock_call_next = AsyncMock(side_effect=ValueError(\"test error\"))\n\n        with pytest.raises(ValueError):\n            await middleware.on_request(mock_context, mock_call_next)\n\n        assert \"Request test_method failed after\" in caplog.text\n        assert \"ms: test error\" in caplog.text\n\n\nclass TestDetailedTimingMiddleware:\n    \"\"\"Test detailed timing middleware functionality.\"\"\"\n\n    def test_init_default(self):\n        \"\"\"Test default initialization.\"\"\"\n        middleware = DetailedTimingMiddleware()\n        assert middleware.logger.name == \"fastmcp.timing.detailed\"\n        assert middleware.log_level == logging.INFO\n\n    async def test_on_call_tool(self, caplog):\n        \"\"\"Test timing tool calls.\"\"\"\n        middleware = DetailedTimingMiddleware()\n        context = MagicMock()\n        context.message.name = \"test_tool\"\n        mock_call_next = AsyncMock(return_value=\"tool_result\")\n\n        result = await middleware.on_call_tool(context, mock_call_next)\n\n        assert result == \"tool_result\"\n        assert \"Tool 'test_tool' completed in\" in caplog.text\n\n    async def test_on_read_resource(self, caplog):\n        \"\"\"Test timing resource reads.\"\"\"\n        middleware = DetailedTimingMiddleware()\n        context = MagicMock()\n        context.message.uri = \"test://resource\"\n        mock_call_next = AsyncMock(return_value=\"resource_result\")\n\n        result = await middleware.on_read_resource(context, mock_call_next)\n\n        assert result == \"resource_result\"\n        assert \"Resource 'test://resource' completed in\" in caplog.text\n\n    async def test_on_get_prompt(self, caplog):\n        \"\"\"Test timing prompt retrieval.\"\"\"\n        middleware = DetailedTimingMiddleware()\n        context = MagicMock()\n        context.message.name = \"test_prompt\"\n        mock_call_next = AsyncMock(return_value=\"prompt_result\")\n\n        result = await middleware.on_get_prompt(context, mock_call_next)\n\n        assert result == \"prompt_result\"\n        assert \"Prompt 'test_prompt' completed in\" in caplog.text\n\n    async def test_on_list_tools(self, caplog):\n        \"\"\"Test timing tool listing.\"\"\"\n        middleware = DetailedTimingMiddleware()\n        context = MagicMock()\n        mock_call_next = AsyncMock(return_value=\"tools_result\")\n\n        result = await middleware.on_list_tools(context, mock_call_next)\n\n        assert result == \"tools_result\"\n        assert \"List tools completed in\" in caplog.text\n\n    async def test_operation_failure(self, caplog):\n        \"\"\"Test timing failed operations.\"\"\"\n        middleware = DetailedTimingMiddleware()\n        context = MagicMock()\n        context.message.name = \"failing_tool\"\n        mock_call_next = AsyncMock(side_effect=RuntimeError(\"operation failed\"))\n\n        with pytest.raises(RuntimeError):\n            await middleware.on_call_tool(context, mock_call_next)\n\n        assert \"Tool 'failing_tool' failed after\" in caplog.text\n        assert \"ms: operation failed\" in caplog.text\n\n\n@pytest.fixture\ndef timing_server():\n    \"\"\"Create a FastMCP server specifically for timing middleware tests.\"\"\"\n    mcp = FastMCP(\"TimingTestServer\")\n\n    @mcp.tool\n    def instant_task() -> str:\n        \"\"\"A task that completes instantly.\"\"\"\n        return \"Done instantly\"\n\n    @mcp.tool\n    def short_task() -> str:\n        \"\"\"A task that takes 0.01 seconds.\"\"\"\n        time.sleep(0.01)\n        return \"Done after 0.01 seconds\"\n\n    @mcp.tool\n    def medium_task() -> str:\n        \"\"\"A task that takes 0.02 seconds.\"\"\"\n        time.sleep(0.02)\n        return \"Done after 0.02 seconds\"\n\n    @mcp.tool\n    def failing_task() -> str:\n        \"\"\"A task that always fails.\"\"\"\n        raise ValueError(\"Task failed as expected\")\n\n    @mcp.resource(\"timer://test\")\n    def test_resource() -> str:\n        \"\"\"A resource that takes time to read.\"\"\"\n        time.sleep(0.005)\n        return \"Resource content after 0.005 seconds\"\n\n    @mcp.prompt\n    def test_prompt() -> str:\n        \"\"\"A prompt that takes time to generate.\"\"\"\n        time.sleep(0.008)\n        return \"Prompt content after 0.008 seconds\"\n\n    return mcp\n\n\nclass TestTimingMiddlewareIntegration:\n    \"\"\"Integration tests for timing middleware with real FastMCP server.\"\"\"\n\n    async def test_timing_middleware_measures_tool_execution(\n        self, timing_server, caplog\n    ):\n        \"\"\"Test that timing middleware accurately measures tool execution times.\"\"\"\n        timing_server.add_middleware(TimingMiddleware())\n\n        async with Client(timing_server) as client:\n            # Test instant task\n            await client.call_tool(\"instant_task\")\n\n            # Test short task (0.1s)\n            await client.call_tool(\"short_task\")\n\n            # Test medium task (0.15s)\n            await client.call_tool(\"medium_task\")\n\n        log_text = caplog.text\n\n        # Should have timing logs for all three calls (plus any extra list_tools calls)\n        timing_logs = [\n            line\n            for line in log_text.split(\"\\n\")\n            if \"completed in\" in line and \"ms\" in line\n        ]\n        assert (\n            len(timing_logs) >= 3\n        )  # At least 3 tool calls, may have additional list_tools calls\n\n        # Verify that longer tasks show longer timing (roughly)\n        assert \"tools/call completed in\" in log_text\n        assert \"ms\" in log_text\n\n    async def test_timing_middleware_handles_failures(self, timing_server, caplog):\n        \"\"\"Test that timing middleware measures time even for failed operations.\"\"\"\n        timing_server.add_middleware(TimingMiddleware())\n\n        async with Client(timing_server) as client:\n            # This should fail but still be timed\n            with pytest.raises(Exception):\n                await client.call_tool(\"failing_task\")\n\n        # Should log the failure with timing\n        assert \"tools/call failed after\" in caplog.text\n        assert \"ms:\" in caplog.text\n\n    async def test_detailed_timing_middleware_per_operation(\n        self, timing_server, caplog\n    ):\n        \"\"\"Test that detailed timing middleware provides operation-specific timing.\"\"\"\n        timing_server.add_middleware(DetailedTimingMiddleware())\n\n        async with Client(timing_server) as client:\n            # Test tool call\n            await client.call_tool(\"short_task\")\n\n            # Test resource read\n            await client.read_resource(\"timer://test\")\n\n            # Test prompt\n            await client.get_prompt(\"test_prompt\")\n\n            # Test listing operations\n            await client.list_tools()\n            await client.list_resources()\n            await client.list_prompts()\n\n        log_text = caplog.text\n\n        # Should have specific timing logs for each operation type\n        assert \"Tool 'short_task' completed in\" in log_text\n        assert \"Resource 'timer://test' completed in\" in log_text\n        assert \"Prompt 'test_prompt' completed in\" in log_text\n        assert \"List tools completed in\" in log_text\n        assert \"List resources completed in\" in log_text\n        assert \"List prompts completed in\" in log_text\n\n    async def test_timing_middleware_concurrent_operations(self, timing_server, caplog):\n        \"\"\"Test timing middleware with concurrent operations.\"\"\"\n        timing_server.add_middleware(TimingMiddleware())\n\n        async with Client(timing_server) as client:\n            # Run multiple operations concurrently\n            tasks = [\n                client.call_tool(\"instant_task\"),\n                client.call_tool(\"short_task\"),\n                client.call_tool(\"instant_task\"),\n            ]\n\n            await asyncio.gather(*tasks)\n\n        log_text = caplog.text\n\n        # Should have timing logs for all concurrent operations (including extra list_tools calls)\n        timing_logs = [line for line in log_text.split(\"\\n\") if \"completed in\" in line]\n        assert (\n            len(timing_logs) >= 3\n        )  # At least 3 tool calls, may have additional list_tools calls\n\n    async def test_timing_middleware_custom_logger(self, timing_server, caplog):\n        \"\"\"Test timing middleware with custom logger configuration.\"\"\"\n        import io\n        import logging\n\n        # Create a custom logger that writes to a string buffer\n        log_buffer = io.StringIO()\n        handler = logging.StreamHandler(log_buffer)\n        custom_logger = logging.getLogger(\"custom_timing\")\n        custom_logger.addHandler(handler)\n        custom_logger.setLevel(logging.DEBUG)\n\n        # Use custom logger and log level\n        timing_server.add_middleware(\n            TimingMiddleware(logger=custom_logger, log_level=logging.DEBUG)\n        )\n\n        async with Client(timing_server) as client:\n            await client.call_tool(\"instant_task\")\n\n        # Check that our custom logger was used\n        log_output = log_buffer.getvalue()\n        assert \"tools/call completed in\" in log_output\n        assert \"ms\" in log_output\n"
  },
  {
    "path": "tests/server/middleware/test_tool_injection.py",
    "content": "\"\"\"Tests for tool injection middleware.\"\"\"\n\nimport math\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp.types import Tool as SDKTool\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.client import CallToolResult\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.server.middleware.tool_injection import (\n    ToolInjectionMiddleware,\n)\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.tools.function_tool import FunctionTool\n\n\ndef multiply_fn(a: int, b: int) -> int:\n    \"\"\"Multiply two numbers.\"\"\"\n    return a * b\n\n\ndef divide_fn(a: int, b: int) -> float:\n    \"\"\"Divide two numbers.\"\"\"\n    if b == 0:\n        raise ValueError(\"Cannot divide by zero\")\n    return a / b\n\n\nmultiply_tool = Tool.from_function(fn=multiply_fn, name=\"multiply\", tags={\"math\"})\ndivide_tool = Tool.from_function(fn=divide_fn, name=\"divide\", tags={\"math\"})\n\n\nclass TestToolInjectionMiddleware:\n    \"\"\"Tests with real FastMCP server.\"\"\"\n\n    @pytest.fixture\n    def base_server(self):\n        \"\"\"Create a base FastMCP server.\"\"\"\n        mcp = FastMCP(\"BaseServer\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        @mcp.tool\n        def subtract(a: int, b: int) -> int:\n            \"\"\"Subtract two numbers.\"\"\"\n            return a - b\n\n        return mcp\n\n    async def test_list_tools_includes_injected_tools(self, base_server: FastMCP):\n        \"\"\"Test that list_tools returns both base and injected tools.\"\"\"\n\n        injected_tools: list[FunctionTool] = [\n            multiply_tool,\n            divide_tool,\n        ]\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(\n            tools=injected_tools\n        )\n        base_server.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](base_server) as client:\n            tools: list[SDKTool] = await client.list_tools()\n\n        # Should have all tools: multiply, divide, add, subtract\n        assert len(tools) == 4\n        tool_names: list[str] = [tool.name for tool in tools]\n        assert \"multiply\" in tool_names\n        assert \"divide\" in tool_names\n        assert \"add\" in tool_names\n        assert \"subtract\" in tool_names\n\n    async def test_call_injected_tool(self, base_server: FastMCP):\n        \"\"\"Test that injected tools can be called successfully.\"\"\"\n\n        injected_tools: list[FunctionTool] = [multiply_tool]\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(\n            tools=injected_tools\n        )\n        base_server.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](base_server) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"multiply\", arguments={\"a\": 7, \"b\": 6}\n            )\n\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 42\n\n    async def test_call_base_tool_still_works(self, base_server: FastMCP):\n        \"\"\"Test that base server tools still work after injecting tools.\"\"\"\n\n        injected_tools: list[FunctionTool] = [multiply_tool]\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(\n            tools=injected_tools\n        )\n        base_server.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](base_server) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"add\", arguments={\"a\": 10, \"b\": 5}\n            )\n\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 15\n\n    async def test_injected_tool_error_handling(self, base_server: FastMCP):\n        \"\"\"Test that errors in injected tools are properly handled.\"\"\"\n\n        injected_tools: list[FunctionTool] = [divide_tool]\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(\n            tools=injected_tools\n        )\n        base_server.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](base_server) as client:\n            with pytest.raises(Exception, match=\"Cannot divide by zero\"):\n                _ = await client.call_tool(name=\"divide\", arguments={\"a\": 10, \"b\": 0})\n\n    async def test_multiple_tool_injections(self, base_server: FastMCP):\n        \"\"\"Test multiple tool injection middlewares can be stacked.\"\"\"\n\n        def power(a: int, b: int) -> int:\n            \"\"\"Raise a to the power of b.\"\"\"\n            return int(math.pow(float(a), float(b)))\n\n        def modulo(a: int, b: int) -> int:\n            \"\"\"Calculate a modulo b.\"\"\"\n            return a % b\n\n        middleware1 = ToolInjectionMiddleware(\n            tools=[Tool.from_function(fn=power, name=\"power\")]\n        )\n        middleware2 = ToolInjectionMiddleware(\n            tools=[Tool.from_function(fn=modulo, name=\"modulo\")]\n        )\n\n        base_server.add_middleware(middleware1)\n        base_server.add_middleware(middleware2)\n\n        async with Client(base_server) as client:\n            tools = await client.list_tools()\n\n        # Should have all tools\n        assert len(tools) == 4\n        tool_names = [tool.name for tool in tools]\n        assert \"power\" in tool_names\n        assert \"modulo\" in tool_names\n        assert \"add\" in tool_names\n        assert \"subtract\" in tool_names\n\n        # Test that both injected tools work\n        async with Client(base_server) as client:\n            power_result = await client.call_tool(\"power\", {\"a\": 2, \"b\": 3})\n            assert power_result.structured_content is not None\n            assert isinstance(power_result.structured_content, dict)\n            assert power_result.structured_content[\"result\"] == 8\n\n            modulo_result = await client.call_tool(\"modulo\", {\"a\": 10, \"b\": 3})\n            assert modulo_result.structured_content is not None\n            assert isinstance(modulo_result.structured_content, dict)\n            assert modulo_result.structured_content[\"result\"] == 1\n\n    async def test_injected_tool_with_complex_return_type(self, base_server: FastMCP):\n        \"\"\"Test injected tools with complex return types.\"\"\"\n\n        def calculate_stats(numbers: list[int]) -> dict[str, int | float]:\n            \"\"\"Calculate statistics for a list of numbers.\"\"\"\n            return {\n                \"sum\": sum(numbers),\n                \"average\": sum(numbers) / len(numbers),\n                \"min\": min(numbers),\n                \"max\": max(numbers),\n                \"count\": len(numbers),\n            }\n\n        middleware = ToolInjectionMiddleware(\n            tools=[Tool.from_function(fn=calculate_stats, name=\"calculate_stats\")]\n        )\n        base_server.add_middleware(middleware)\n\n        async with Client(base_server) as client:\n            result = await client.call_tool(\n                \"calculate_stats\", {\"numbers\": [1, 2, 3, 4, 5]}\n            )\n\n        assert result.structured_content is not None\n\n        assert isinstance(result.structured_content, dict)\n\n        assert result.structured_content == snapshot(\n            {\"sum\": 15, \"average\": 3.0, \"min\": 1, \"max\": 5, \"count\": 5}\n        )\n\n    async def test_injected_tool_metadata_preserved(self, base_server: FastMCP):\n        \"\"\"Test that injected tool metadata is preserved.\"\"\"\n\n        def multiply(a: int, b: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return a * b\n\n        injected_tools = [Tool.from_function(fn=multiply, name=\"multiply\")]\n        middleware = ToolInjectionMiddleware(tools=injected_tools)\n        base_server.add_middleware(middleware)\n\n        async with Client(base_server) as client:\n            tools = await client.list_tools()\n\n        multiply_tool = next(t for t in tools if t.name == \"multiply\")\n        assert multiply_tool.description == \"Multiply two numbers.\"\n        assert \"a\" in multiply_tool.inputSchema[\"properties\"]\n        assert \"b\" in multiply_tool.inputSchema[\"properties\"]\n\n    async def test_injected_tool_does_not_conflict_with_base_tool(\n        self, base_server: FastMCP\n    ):\n        \"\"\"Test that injected tools with same name as base tools are called correctly.\"\"\"\n\n        def add(a: int, b: int) -> int:\n            \"\"\"Injected add that multiplies instead.\"\"\"\n            return a * b\n\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(\n            tools=[Tool.from_function(fn=add, name=\"add\")]\n        )\n        base_server.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](base_server) as client:\n            result: CallToolResult = await client.call_tool(\n                name=\"add\", arguments={\"a\": 5, \"b\": 3}\n            )\n\n        # Should use the injected tool (multiply behavior)\n        assert result.structured_content is not None\n        assert result.structured_content[\"result\"] == 15\n\n    async def test_injected_tool_bypass_filtering(self, base_server: FastMCP):\n        \"\"\"Test that injected tools bypass filtering.\"\"\"\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(\n            tools=[multiply_tool]\n        )\n        base_server.add_middleware(middleware)\n        base_server.disable(tags={\"math\"})\n\n        async with Client[FastMCPTransport](base_server) as client:\n            tools: list[SDKTool] = await client.list_tools()\n            tool_names: list[str] = [tool.name for tool in tools]\n            assert \"multiply\" in tool_names\n\n    async def test_empty_tool_injection(self, base_server: FastMCP):\n        \"\"\"Test that middleware with no tools doesn't affect behavior.\"\"\"\n        middleware: ToolInjectionMiddleware = ToolInjectionMiddleware(tools=[])\n        base_server.add_middleware(middleware)\n\n        async with Client[FastMCPTransport](base_server) as client:\n            tools: list[SDKTool] = await client.list_tools()\n            result: CallToolResult = await client.call_tool(\n                name=\"add\", arguments={\"a\": 3, \"b\": 4}\n            )\n\n        # Should only have the base tools\n        assert len(tools) == 2\n        tool_names: list[str] = [tool.name for tool in tools]\n        assert \"add\" in tool_names\n        assert \"subtract\" in tool_names\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 7\n"
  },
  {
    "path": "tests/server/mount/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/mount/test_advanced.py",
    "content": "\"\"\"Advanced mounting scenarios.\"\"\"\n\nimport pytest\nfrom mcp.types import TextContent\nfrom starlette.routing import Route\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.server.providers.wrapped_provider import _WrappedProvider\n\n\nclass TestDynamicChanges:\n    \"\"\"Test that changes to mounted servers are reflected dynamically.\"\"\"\n\n    async def test_adding_tool_after_mounting(self):\n        \"\"\"Test that tools added after mounting are accessible.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        # Mount the sub-app before adding any tools\n        main_app.mount(sub_app, \"sub\")\n\n        # Initially, there should be no tools from sub_app\n        tools = await main_app.list_tools()\n        assert not any(t.name.startswith(\"sub_\") for t in tools)\n\n        # Add a tool to the sub-app after mounting\n        @sub_app.tool\n        def dynamic_tool() -> str:\n            return \"Added after mounting\"\n\n        # The tool should be accessible through the main app\n        tools = await main_app.list_tools()\n        assert any(t.name == \"sub_dynamic_tool\" for t in tools)\n\n        # Call the dynamically added tool\n        result = await main_app.call_tool(\"sub_dynamic_tool\", {})\n        assert result.structured_content == {\"result\": \"Added after mounting\"}\n\n    async def test_removing_tool_after_mounting(self):\n        \"\"\"Test that tools removed from mounted servers are no longer accessible.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.tool\n        def temp_tool() -> str:\n            return \"Temporary tool\"\n\n        # Mount the sub-app\n        main_app.mount(sub_app, \"sub\")\n\n        # Initially, the tool should be accessible\n        tools = await main_app.list_tools()\n        assert any(t.name == \"sub_temp_tool\" for t in tools)\n\n        # Remove the tool from sub_app\n        sub_app.local_provider.remove_tool(\"temp_tool\")\n\n        # The tool should no longer be accessible\n        tools = await main_app.list_tools()\n        assert not any(t.name == \"sub_temp_tool\" for t in tools)\n\n\nclass TestCustomRouteForwarding:\n    \"\"\"Test that custom HTTP routes from mounted servers are forwarded.\"\"\"\n\n    async def test_get_additional_http_routes_empty(self):\n        \"\"\"Test _get_additional_http_routes returns empty list for server with no routes.\"\"\"\n        server = FastMCP(\"TestServer\")\n        routes = server._get_additional_http_routes()\n        assert routes == []\n\n    async def test_get_additional_http_routes_with_custom_route(self):\n        \"\"\"Test _get_additional_http_routes returns server's own routes.\"\"\"\n        server = FastMCP(\"TestServer\")\n\n        @server.custom_route(\"/test\", methods=[\"GET\"])\n        async def test_route(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"message\": \"test\"})\n\n        routes = server._get_additional_http_routes()\n        assert len(routes) == 1\n        assert isinstance(routes[0], Route)\n        assert routes[0].path == \"/test\"\n\n    async def test_mounted_servers_tracking(self):\n        \"\"\"Test that providers list tracks mounted servers correctly.\"\"\"\n        from fastmcp.server.providers.local_provider import LocalProvider\n\n        main_server = FastMCP(\"MainServer\")\n        sub_server1 = FastMCP(\"SubServer1\")\n        sub_server2 = FastMCP(\"SubServer2\")\n\n        @sub_server1.tool\n        def tool1() -> str:\n            return \"1\"\n\n        @sub_server2.tool\n        def tool2() -> str:\n            return \"2\"\n\n        # Initially only LocalProvider\n        assert len(main_server.providers) == 1\n        assert isinstance(main_server.providers[0], LocalProvider)\n\n        # Mount first server\n        main_server.mount(sub_server1, \"sub1\")\n        assert len(main_server.providers) == 2\n        # LocalProvider is at index 0, mounted provider (wrapped) at index 1\n        provider1 = main_server.providers[1]\n        assert isinstance(provider1, _WrappedProvider)\n        assert isinstance(provider1._inner, FastMCPProvider)\n        assert provider1._inner.server == sub_server1\n\n        # Mount second server\n        main_server.mount(sub_server2, \"sub2\")\n        assert len(main_server.providers) == 3\n        provider2 = main_server.providers[2]\n        assert isinstance(provider2, _WrappedProvider)\n        assert isinstance(provider2._inner, FastMCPProvider)\n        assert provider2._inner.server == sub_server2\n\n        # Verify namespacing is applied by checking tool names\n        tools = await main_server.list_tools()\n        tool_names = {t.name for t in tools}\n        assert tool_names == {\"sub1_tool1\", \"sub2_tool2\"}\n\n    async def test_multiple_routes_same_server(self):\n        \"\"\"Test that multiple custom routes from same server are all included.\"\"\"\n        server = FastMCP(\"TestServer\")\n\n        @server.custom_route(\"/route1\", methods=[\"GET\"])\n        async def route1(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"message\": \"route1\"})\n\n        @server.custom_route(\"/route2\", methods=[\"POST\"])\n        async def route2(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"message\": \"route2\"})\n\n        routes = server._get_additional_http_routes()\n        assert len(routes) == 2\n        route_paths = [route.path for route in routes if isinstance(route, Route)]\n        assert \"/route1\" in route_paths\n        assert \"/route2\" in route_paths\n\n    async def test_mounted_server_custom_routes_forwarded(self):\n        \"\"\"Test that custom routes from a mounted server appear in the parent.\n\n        Regression test for https://github.com/PrefectHQ/fastmcp/issues/3457\n        where custom_route endpoints defined on a child server were silently\n        dropped when the child was mounted onto a parent, resulting in 404s.\n        \"\"\"\n        parent = FastMCP(\"Parent\")\n        child = FastMCP(\"Child\")\n\n        @child.custom_route(\"/readyz\", methods=[\"GET\"])\n        async def readiness_check(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"status\": \"ok\"})\n\n        parent.mount(child)\n\n        routes = parent._get_additional_http_routes()\n        assert len(routes) == 1\n        assert isinstance(routes[0], Route)\n        assert routes[0].path == \"/readyz\"\n\n    async def test_mounted_server_custom_routes_with_namespace(self):\n        \"\"\"Test that custom routes from a namespaced mount are forwarded.\"\"\"\n        parent = FastMCP(\"Parent\")\n        child = FastMCP(\"Child\")\n\n        @child.custom_route(\"/health\", methods=[\"GET\"])\n        async def health(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"status\": \"ok\"})\n\n        parent.mount(child, namespace=\"child\")\n\n        routes = parent._get_additional_http_routes()\n        assert len(routes) == 1\n        assert isinstance(routes[0], Route)\n        assert routes[0].path == \"/health\"\n\n    async def test_deeply_nested_custom_routes_forwarded(self):\n        \"\"\"Test that custom routes from deeply nested mounts are collected.\"\"\"\n        root = FastMCP(\"Root\")\n        middle = FastMCP(\"Middle\")\n        leaf = FastMCP(\"Leaf\")\n\n        @leaf.custom_route(\"/leaf-health\", methods=[\"GET\"])\n        async def leaf_health(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"status\": \"ok\"})\n\n        @middle.custom_route(\"/middle-health\", methods=[\"GET\"])\n        async def middle_health(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"status\": \"ok\"})\n\n        middle.mount(leaf)\n        root.mount(middle)\n\n        routes = root._get_additional_http_routes()\n        route_paths = [r.path for r in routes if isinstance(r, Route)]\n        assert \"/leaf-health\" in route_paths\n        assert \"/middle-health\" in route_paths\n        assert len(route_paths) == 2\n\n    async def test_mounted_custom_routes_http_app_integration(self):\n        \"\"\"End-to-end: custom routes from mounted servers are reachable via http_app.\n\n        This reproduces the exact scenario from issue #3457.\n        \"\"\"\n        from starlette.testclient import TestClient\n\n        parent = FastMCP(\"Parent\")\n        child = FastMCP(\"Child\")\n\n        @child.custom_route(\"/readyz\", methods=[\"GET\"])\n        async def readiness_check(request):\n            from starlette.responses import JSONResponse\n\n            return JSONResponse({\"status\": \"ok\"})\n\n        parent.mount(child)\n\n        app = parent.http_app()\n        client = TestClient(app)\n        response = client.get(\"/readyz\")\n        assert response.status_code == 200\n        assert response.json() == {\"status\": \"ok\"}\n\n\nclass TestDeeplyNestedMount:\n    \"\"\"Test deeply nested mount scenarios (3+ levels deep).\n\n    This tests the fix for https://github.com/PrefectHQ/fastmcp/issues/2583\n    where tools/resources/prompts mounted more than 2 levels deep would fail\n    to invoke even though they were correctly listed.\n    \"\"\"\n\n    async def test_three_level_nested_tool_invocation(self):\n        \"\"\"Test invoking tools from servers mounted 3 levels deep.\"\"\"\n        root = FastMCP(\"root\")\n        middle = FastMCP(\"middle\")\n        leaf = FastMCP(\"leaf\")\n\n        @leaf.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        @middle.tool\n        def multiply(a: int, b: int) -> int:\n            return a * b\n\n        middle.mount(leaf, namespace=\"leaf\")\n        root.mount(middle, namespace=\"middle\")\n\n        # Tool at level 2 should work\n        result = await root.call_tool(\"middle_multiply\", {\"a\": 3, \"b\": 4})\n        assert result.structured_content == {\"result\": 12}\n\n        # Tool at level 3 should also work (this was the bug)\n        result = await root.call_tool(\"middle_leaf_add\", {\"a\": 5, \"b\": 7})\n        assert result.structured_content == {\"result\": 12}\n\n    async def test_three_level_nested_resource_invocation(self):\n        \"\"\"Test reading resources from servers mounted 3 levels deep.\"\"\"\n        root = FastMCP(\"root\")\n        middle = FastMCP(\"middle\")\n        leaf = FastMCP(\"leaf\")\n\n        @leaf.resource(\"leaf://data\")\n        def leaf_data() -> str:\n            return \"leaf data\"\n\n        @middle.resource(\"middle://data\")\n        def middle_data() -> str:\n            return \"middle data\"\n\n        middle.mount(leaf, namespace=\"leaf\")\n        root.mount(middle, namespace=\"middle\")\n\n        # Resource at level 2 should work\n        result = await root.read_resource(\"middle://middle/data\")\n        assert result.contents[0].content == \"middle data\"\n\n        # Resource at level 3 should also work\n        result = await root.read_resource(\"leaf://middle/leaf/data\")\n        assert result.contents[0].content == \"leaf data\"\n\n    async def test_three_level_nested_resource_template_invocation(self):\n        \"\"\"Test reading resource templates from servers mounted 3 levels deep.\"\"\"\n        root = FastMCP(\"root\")\n        middle = FastMCP(\"middle\")\n        leaf = FastMCP(\"leaf\")\n\n        @leaf.resource(\"leaf://item/{id}\")\n        def leaf_item(id: str) -> str:\n            return f\"leaf item {id}\"\n\n        @middle.resource(\"middle://item/{id}\")\n        def middle_item(id: str) -> str:\n            return f\"middle item {id}\"\n\n        middle.mount(leaf, namespace=\"leaf\")\n        root.mount(middle, namespace=\"middle\")\n\n        # Resource template at level 2 should work\n        result = await root.read_resource(\"middle://middle/item/42\")\n        assert result.contents[0].content == \"middle item 42\"\n\n        # Resource template at level 3 should also work\n        result = await root.read_resource(\"leaf://middle/leaf/item/99\")\n        assert result.contents[0].content == \"leaf item 99\"\n\n    async def test_three_level_nested_prompt_invocation(self):\n        \"\"\"Test getting prompts from servers mounted 3 levels deep.\"\"\"\n        root = FastMCP(\"root\")\n        middle = FastMCP(\"middle\")\n        leaf = FastMCP(\"leaf\")\n\n        @leaf.prompt\n        def leaf_prompt(name: str) -> str:\n            return f\"Hello from leaf: {name}\"\n\n        @middle.prompt\n        def middle_prompt(name: str) -> str:\n            return f\"Hello from middle: {name}\"\n\n        middle.mount(leaf, namespace=\"leaf\")\n        root.mount(middle, namespace=\"middle\")\n\n        # Prompt at level 2 should work\n        result = await root.render_prompt(\"middle_middle_prompt\", {\"name\": \"World\"})\n        assert isinstance(result.messages[0].content, TextContent)\n        assert \"Hello from middle: World\" in result.messages[0].content.text\n\n        # Prompt at level 3 should also work\n        result = await root.render_prompt(\"middle_leaf_leaf_prompt\", {\"name\": \"Test\"})\n        assert isinstance(result.messages[0].content, TextContent)\n        assert \"Hello from leaf: Test\" in result.messages[0].content.text\n\n    async def test_four_level_nested_tool_invocation(self):\n        \"\"\"Test invoking tools from servers mounted 4 levels deep.\"\"\"\n        root = FastMCP(\"root\")\n        level1 = FastMCP(\"level1\")\n        level2 = FastMCP(\"level2\")\n        level3 = FastMCP(\"level3\")\n\n        @level3.tool\n        def deep_tool() -> str:\n            return \"very deep\"\n\n        level2.mount(level3, namespace=\"l3\")\n        level1.mount(level2, namespace=\"l2\")\n        root.mount(level1, namespace=\"l1\")\n\n        # Verify tool is listed\n        tools = await root.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"l1_l2_l3_deep_tool\" in tool_names\n\n        # Tool at level 4 should work\n        result = await root.call_tool(\"l1_l2_l3_deep_tool\", {})\n        assert result.structured_content == {\"result\": \"very deep\"}\n\n\nclass TestToolNameOverrides:\n    \"\"\"Test tool and prompt name overrides in mount() (issue #2596).\"\"\"\n\n    async def test_tool_names_override_via_transforms(self):\n        \"\"\"Test that tool_names renames tools via ToolTransform layer.\n\n        Tool renames are applied first, then namespace prefixing.\n        So original_tool → custom_name → prefix_custom_name.\n        \"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def original_tool() -> str:\n            return \"test\"\n\n        main = FastMCP(\"Main\")\n        # tool_names renames first, then namespace is applied\n        main.mount(\n            sub,\n            namespace=\"prefix\",\n            tool_names={\"original_tool\": \"custom_name\"},\n        )\n\n        # Server introspection shows renamed + namespaced names\n        tools = await main.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"prefix_custom_name\" in tool_names\n        assert \"original_tool\" not in tool_names\n        assert \"prefix_original_tool\" not in tool_names\n        assert \"custom_name\" not in tool_names\n\n    async def test_tool_names_override_applied_in_list_tools(self):\n        \"\"\"Test that tool_names override is reflected in list_tools().\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def original_tool() -> str:\n            return \"test\"\n\n        main = FastMCP(\"Main\")\n        main.mount(\n            sub,\n            namespace=\"prefix\",\n            tool_names={\"original_tool\": \"custom_name\"},\n        )\n\n        tools = await main.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"prefix_custom_name\" in tool_names\n        assert \"prefix_original_tool\" not in tool_names\n\n    async def test_tool_call_with_overridden_name(self):\n        \"\"\"Test that overridden tool can be called by its new name.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def original_tool() -> str:\n            return \"success\"\n\n        main = FastMCP(\"Main\")\n        main.mount(\n            sub,\n            namespace=\"prefix\",\n            tool_names={\"original_tool\": \"renamed\"},\n        )\n\n        # Tool is renamed then namespaced: original_tool → renamed → prefix_renamed\n        result = await main.call_tool(\"prefix_renamed\", {})\n        assert result.structured_content == {\"result\": \"success\"}\n\n    def test_duplicate_tool_rename_targets_raises_error(self):\n        \"\"\"Test that duplicate target names in tool_renames raises ValueError.\"\"\"\n        sub = FastMCP(\"Sub\")\n        main = FastMCP(\"Main\")\n\n        with pytest.raises(ValueError, match=\"duplicate target name\"):\n            main.mount(\n                sub,\n                tool_names={\"tool_a\": \"same_name\", \"tool_b\": \"same_name\"},\n            )\n\n\nclass TestMountedServerDocketBehavior:\n    \"\"\"Regression tests for mounted server lifecycle behavior.\n\n    These tests guard against architectural changes that could accidentally\n    start Docket instances for mounted servers. Mounted servers should only\n    run their user-defined lifespan, not the full _lifespan_manager which\n    includes Docket creation.\n    \"\"\"\n\n    async def test_mounted_server_does_not_have_docket(self):\n        \"\"\"Test that a mounted server doesn't create its own Docket.\n\n        MountedProvider.lifespan() should call only the server's _lifespan\n        (user-defined lifespan), not _lifespan_manager (which includes Docket).\n        \"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        # Need a task-enabled component to trigger Docket initialization\n        @main_app.tool(task=True)\n        async def _trigger_docket() -> str:\n            return \"trigger\"\n\n        @sub_app.tool\n        def my_tool() -> str:\n            return \"test\"\n\n        main_app.mount(sub_app, \"sub\")\n\n        # After running the main app's lifespan, the sub app should not have\n        # its own Docket instance\n        async with Client(main_app) as client:\n            # The main app should have a docket (created by _lifespan_manager)\n            # because it has a task-enabled component\n            assert main_app.docket is not None\n\n            # The mounted sub app should NOT have its own docket\n            # It uses the parent's docket for background tasks\n            assert sub_app.docket is None\n\n            # But the tool should still work (prefixed as sub_my_tool)\n            result = await client.call_tool(\"sub_my_tool\", {})\n            assert result.data == \"test\"\n\n\nclass TestComponentServicePrefixLess:\n    \"\"\"Test that enable/disable works with prefix-less mounted servers.\"\"\"\n\n    async def test_enable_tool_prefixless_mount(self):\n        \"\"\"Test enabling a tool on a prefix-less mounted server.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.tool\n        def my_tool() -> str:\n            return \"test\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Initially the tool is enabled\n        tools = await main_app.list_tools()\n        assert any(t.name == \"my_tool\" for t in tools)\n\n        # Disable and re-enable\n        main_app.disable(names={\"my_tool\"}, components={\"tool\"})\n        # Verify tool is now disabled\n        tools = await main_app.list_tools()\n        assert not any(t.name == \"my_tool\" for t in tools)\n\n        main_app.enable(names={\"my_tool\"}, components={\"tool\"})\n        # Verify tool is now enabled\n        tools = await main_app.list_tools()\n        assert any(t.name == \"my_tool\" for t in tools)\n\n    async def test_enable_resource_prefixless_mount(self):\n        \"\"\"Test enabling a resource on a prefix-less mounted server.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.resource(uri=\"data://test\")\n        def my_resource() -> str:\n            return \"test data\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Disable and re-enable\n        main_app.disable(names={\"data://test\"}, components={\"resource\"})\n        # Verify resource is now disabled\n        resources = await main_app.list_resources()\n        assert not any(str(r.uri) == \"data://test\" for r in resources)\n\n        main_app.enable(names={\"data://test\"}, components={\"resource\"})\n        # Verify resource is now enabled\n        resources = await main_app.list_resources()\n        assert any(str(r.uri) == \"data://test\" for r in resources)\n\n    async def test_enable_prompt_prefixless_mount(self):\n        \"\"\"Test enabling a prompt on a prefix-less mounted server.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.prompt\n        def my_prompt() -> str:\n            return \"test prompt\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Disable and re-enable\n        main_app.disable(names={\"my_prompt\"}, components={\"prompt\"})\n        # Verify prompt is now disabled\n        prompts = await main_app.list_prompts()\n        assert not any(p.name == \"my_prompt\" for p in prompts)\n\n        main_app.enable(names={\"my_prompt\"}, components={\"prompt\"})\n        # Verify prompt is now enabled\n        prompts = await main_app.list_prompts()\n        assert any(p.name == \"my_prompt\" for p in prompts)\n"
  },
  {
    "path": "tests/server/mount/test_filtering.py",
    "content": "\"\"\"Tests for tag filtering in mounted servers.\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import NotFoundError\n\n\nclass TestParentTagFiltering:\n    \"\"\"Test that parent server tag filters apply recursively to mounted servers.\"\"\"\n\n    async def test_parent_include_tags_filters_mounted_tools(self):\n        \"\"\"Test that parent include_tags filters out non-matching mounted tools.\"\"\"\n        parent = FastMCP(\"Parent\")\n        parent.enable(tags={\"allowed\"}, only=True)\n        mounted = FastMCP(\"Mounted\")\n\n        @mounted.tool(tags={\"allowed\"})\n        def allowed_tool() -> str:\n            return \"allowed\"\n\n        @mounted.tool(tags={\"blocked\"})\n        def blocked_tool() -> str:\n            return \"blocked\"\n\n        parent.mount(mounted)\n\n        tools = await parent.list_tools()\n        tool_names = {t.name for t in tools}\n        assert \"allowed_tool\" in tool_names\n        assert \"blocked_tool\" not in tool_names\n\n        # Verify execution also respects filters\n        result = await parent.call_tool(\"allowed_tool\", {})\n        assert result.structured_content == {\"result\": \"allowed\"}\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await parent.call_tool(\"blocked_tool\", {})\n\n    async def test_parent_exclude_tags_filters_mounted_tools(self):\n        \"\"\"Test that parent exclude_tags filters out matching mounted tools.\"\"\"\n        parent = FastMCP(\"Parent\")\n        parent.disable(tags={\"blocked\"})\n        mounted = FastMCP(\"Mounted\")\n\n        @mounted.tool(tags={\"production\"})\n        def production_tool() -> str:\n            return \"production\"\n\n        @mounted.tool(tags={\"blocked\"})\n        def blocked_tool() -> str:\n            return \"blocked\"\n\n        parent.mount(mounted)\n\n        tools = await parent.list_tools()\n        tool_names = {t.name for t in tools}\n        assert \"production_tool\" in tool_names\n        assert \"blocked_tool\" not in tool_names\n\n    async def test_parent_filters_apply_to_mounted_resources(self):\n        \"\"\"Test that parent tag filters apply to mounted resources.\"\"\"\n        parent = FastMCP(\"Parent\")\n        parent.enable(tags={\"allowed\"}, only=True)\n        mounted = FastMCP(\"Mounted\")\n\n        @mounted.resource(\"resource://allowed\", tags={\"allowed\"})\n        def allowed_resource() -> str:\n            return \"allowed\"\n\n        @mounted.resource(\"resource://blocked\", tags={\"blocked\"})\n        def blocked_resource() -> str:\n            return \"blocked\"\n\n        parent.mount(mounted)\n\n        resources = await parent.list_resources()\n        resource_uris = {str(r.uri) for r in resources}\n        assert \"resource://allowed\" in resource_uris\n        assert \"resource://blocked\" not in resource_uris\n\n    async def test_parent_filters_apply_to_mounted_prompts(self):\n        \"\"\"Test that parent tag filters apply to mounted prompts.\"\"\"\n        parent = FastMCP(\"Parent\")\n        parent.disable(tags={\"blocked\"})\n        mounted = FastMCP(\"Mounted\")\n\n        @mounted.prompt(tags={\"allowed\"})\n        def allowed_prompt() -> str:\n            return \"allowed\"\n\n        @mounted.prompt(tags={\"blocked\"})\n        def blocked_prompt() -> str:\n            return \"blocked\"\n\n        parent.mount(mounted)\n\n        prompts = await parent.list_prompts()\n        prompt_names = {p.name for p in prompts}\n        assert \"allowed_prompt\" in prompt_names\n        assert \"blocked_prompt\" not in prompt_names\n"
  },
  {
    "path": "tests/server/mount/test_mount.py",
    "content": "\"\"\"Basic mounting functionality tests.\"\"\"\n\nimport logging\nimport sys\n\nimport pytest\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import SSETransport\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.tools.tool_transform import TransformedTool\n\n\nclass TestBasicMount:\n    \"\"\"Test basic mounting functionality.\"\"\"\n\n    async def test_mount_simple_server(self):\n        \"\"\"Test mounting a simple server and accessing its tool.\"\"\"\n        # Create main app and sub-app\n        main_app = FastMCP(\"MainApp\")\n\n        # Add a tool to the sub-app\n        def tool() -> str:\n            return \"This is from the sub app\"\n\n        sub_tool = Tool.from_function(tool)\n\n        transformed_tool = TransformedTool.from_tool(\n            name=\"transformed_tool\", tool=sub_tool\n        )\n\n        sub_app = FastMCP(\"SubApp\", tools=[transformed_tool, sub_tool])\n\n        # Mount the sub-app to the main app\n        main_app.mount(sub_app, \"sub\")\n\n        # Get tools from main app, should include sub_app's tools\n        tools = await main_app.list_tools()\n        assert any(t.name == \"sub_tool\" for t in tools)\n        assert any(t.name == \"sub_transformed_tool\" for t in tools)\n\n        result = await main_app.call_tool(\"sub_tool\", {})\n        assert result.structured_content == {\"result\": \"This is from the sub app\"}\n\n    async def test_mount_with_custom_separator(self):\n        \"\"\"Test mounting with a custom tool separator (deprecated but still supported).\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Mount without custom separator - custom separators are deprecated\n        main_app.mount(sub_app, \"sub\")\n\n        # Tool should be accessible with the default separator\n        tools = await main_app.list_tools()\n        assert any(t.name == \"sub_greet\" for t in tools)\n\n        # Call the tool\n        result = await main_app.call_tool(\"sub_greet\", {\"name\": \"World\"})\n        assert result.structured_content == {\"result\": \"Hello, World!\"}\n\n    @pytest.mark.parametrize(\"prefix\", [\"\", None])\n    async def test_mount_with_no_prefix(self, prefix):\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.tool\n        def sub_tool() -> str:\n            return \"This is from the sub app\"\n\n        # Mount with empty prefix but without deprecated separators\n        main_app.mount(sub_app, namespace=prefix)\n\n        tools = await main_app.list_tools()\n        # With empty prefix, the tool should keep its original name\n        assert any(t.name == \"sub_tool\" for t in tools)\n\n    async def test_mount_with_no_prefix_provided(self):\n        \"\"\"Test mounting without providing a prefix at all.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.tool\n        def sub_tool() -> str:\n            return \"This is from the sub app\"\n\n        # Mount without providing a prefix (should be None)\n        main_app.mount(sub_app)\n\n        tools = await main_app.list_tools()\n        # Without prefix, the tool should keep its original name\n        assert any(t.name == \"sub_tool\" for t in tools)\n\n        # Call the tool to verify it works\n        result = await main_app.call_tool(\"sub_tool\", {})\n        assert result.structured_content == {\"result\": \"This is from the sub app\"}\n\n    async def test_mount_tools_no_prefix(self):\n        \"\"\"Test mounting a server with tools without prefix.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.tool\n        def sub_tool() -> str:\n            return \"Sub tool result\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Verify tool is accessible with original name\n        tools = await main_app.list_tools()\n        assert any(t.name == \"sub_tool\" for t in tools)\n\n        # Test actual functionality\n        tool_result = await main_app.call_tool(\"sub_tool\", {})\n        assert tool_result.structured_content == {\"result\": \"Sub tool result\"}\n\n    async def test_mount_resources_no_prefix(self):\n        \"\"\"Test mounting a server with resources without prefix.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.resource(uri=\"data://config\")\n        def sub_resource():\n            return \"Sub resource data\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Verify resource is accessible with original URI\n        resources = await main_app.list_resources()\n        assert any(str(r.uri) == \"data://config\" for r in resources)\n\n        # Test actual functionality\n        resource_result = await main_app.read_resource(\"data://config\")\n        assert resource_result.contents[0].content == \"Sub resource data\"\n\n    async def test_mount_resource_templates_no_prefix(self):\n        \"\"\"Test mounting a server with resource templates without prefix.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.resource(uri=\"users://{user_id}/info\")\n        def sub_template(user_id: str):\n            return f\"Sub template for user {user_id}\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Verify template is accessible with original URI template\n        templates = await main_app.list_resource_templates()\n        assert any(t.uri_template == \"users://{user_id}/info\" for t in templates)\n\n        # Test actual functionality\n        template_result = await main_app.read_resource(\"users://123/info\")\n        assert template_result.contents[0].content == \"Sub template for user 123\"\n\n    async def test_mount_prompts_no_prefix(self):\n        \"\"\"Test mounting a server with prompts without prefix.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.prompt\n        def sub_prompt() -> str:\n            return \"Sub prompt content\"\n\n        # Mount without prefix\n        main_app.mount(sub_app)\n\n        # Verify prompt is accessible with original name\n        prompts = await main_app.list_prompts()\n        assert any(p.name == \"sub_prompt\" for p in prompts)\n\n        # Test actual functionality\n        prompt_result = await main_app.render_prompt(\"sub_prompt\")\n        assert prompt_result.messages is not None\n\n\nclass TestMultipleServerMount:\n    \"\"\"Test mounting multiple servers simultaneously.\"\"\"\n\n    async def test_mount_multiple_servers(self):\n        \"\"\"Test mounting multiple servers with different prefixes.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        weather_app = FastMCP(\"WeatherApp\")\n        news_app = FastMCP(\"NewsApp\")\n\n        @weather_app.tool\n        def get_forecast() -> str:\n            return \"Weather forecast\"\n\n        @news_app.tool\n        def get_headlines() -> str:\n            return \"News headlines\"\n\n        # Mount both apps\n        main_app.mount(weather_app, \"weather\")\n        main_app.mount(news_app, \"news\")\n\n        # Check both are accessible\n        tools = await main_app.list_tools()\n        assert any(t.name == \"weather_get_forecast\" for t in tools)\n        assert any(t.name == \"news_get_headlines\" for t in tools)\n\n        # Call tools from both mounted servers\n        result1 = await main_app.call_tool(\"weather_get_forecast\", {})\n        assert result1.structured_content == {\"result\": \"Weather forecast\"}\n        result2 = await main_app.call_tool(\"news_get_headlines\", {})\n        assert result2.structured_content == {\"result\": \"News headlines\"}\n\n    async def test_mount_same_prefix(self):\n        \"\"\"Test that mounting with the same prefix replaces the previous mount.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.tool\n        def first_tool() -> str:\n            return \"First app tool\"\n\n        @second_app.tool\n        def second_tool() -> str:\n            return \"Second app tool\"\n\n        # Mount first app\n        main_app.mount(first_app, \"api\")\n        tools = await main_app.list_tools()\n        assert any(t.name == \"api_first_tool\" for t in tools)\n\n        # Mount second app with same prefix\n        main_app.mount(second_app, \"api\")\n        tools = await main_app.list_tools()\n\n        # Both apps' tools should be accessible (new behavior)\n        assert any(t.name == \"api_first_tool\" for t in tools)\n        assert any(t.name == \"api_second_tool\" for t in tools)\n\n    @pytest.mark.skipif(\n        sys.platform == \"win32\", reason=\"Windows asyncio networking timeouts.\"\n    )\n    async def test_mount_with_unreachable_proxy_servers(self, caplog):\n        \"\"\"Test graceful handling when multiple mounted servers fail to connect.\"\"\"\n        caplog.set_level(logging.DEBUG, logger=\"fastmcp\")\n\n        main_app = FastMCP(\"MainApp\")\n        working_app = FastMCP(\"WorkingApp\")\n\n        @working_app.tool\n        def working_tool() -> str:\n            return \"Working tool\"\n\n        @working_app.resource(uri=\"working://data\")\n        def working_resource():\n            return \"Working resource\"\n\n        @working_app.prompt\n        def working_prompt() -> str:\n            return \"Working prompt\"\n\n        # Mount the working server\n        main_app.mount(working_app, \"working\")\n\n        # Use an unreachable port\n        unreachable_client = Client(\n            transport=SSETransport(\"http://127.0.0.1:9999/sse/\"),\n            name=\"unreachable_client\",\n        )\n\n        # Create a proxy server that will fail to connect\n        unreachable_proxy = FastMCP.as_proxy(\n            unreachable_client, name=\"unreachable_proxy\"\n        )\n\n        # Mount the unreachable proxy\n        main_app.mount(unreachable_proxy, \"unreachable\")\n\n        # All object types should work from working server despite unreachable proxy\n        async with Client(main_app, name=\"main_app_client\") as client:\n            # Test tools\n            tools = await client.list_tools()\n            tool_names = [tool.name for tool in tools]\n            assert \"working_working_tool\" in tool_names\n\n            # Test calling a tool\n            result = await client.call_tool(\"working_working_tool\", {})\n            assert result.data == \"Working tool\"\n\n            # Test resources\n            resources = await client.list_resources()\n            resource_uris = [str(resource.uri) for resource in resources]\n            assert \"working://working/data\" in resource_uris\n\n            # Test prompts\n            prompts = await client.list_prompts()\n            prompt_names = [prompt.name for prompt in prompts]\n            assert \"working_working_prompt\" in prompt_names\n\n        # Verify that errors were logged for the unreachable provider (at DEBUG level)\n        debug_messages = [\n            record.message for record in caplog.records if record.levelname == \"DEBUG\"\n        ]\n        assert any(\n            \"Error during list_tools from provider\" in msg for msg in debug_messages\n        )\n        assert any(\n            \"Error during list_resources from provider\" in msg for msg in debug_messages\n        )\n        assert any(\n            \"Error during list_prompts from provider\" in msg for msg in debug_messages\n        )\n\n\nclass TestPrefixConflictResolution:\n    \"\"\"Test that first registered provider wins when there are conflicts.\n\n    Provider semantics: 'Providers are queried in registration order; first non-None wins'\n    \"\"\"\n\n    async def test_first_server_wins_tools_no_prefix(self):\n        \"\"\"Test that first mounted server wins for tools when no prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.tool(name=\"shared_tool\")\n        def first_shared_tool() -> str:\n            return \"First app tool\"\n\n        @second_app.tool(name=\"shared_tool\")\n        def second_shared_tool() -> str:\n            return \"Second app tool\"\n\n        # Mount both apps without prefix\n        main_app.mount(first_app)\n        main_app.mount(second_app)\n\n        # list_tools returns all components; execution uses first match\n        tools = await main_app.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"shared_tool\" in tool_names\n\n        # Test that calling the tool uses the first server's implementation\n        result = await main_app.call_tool(\"shared_tool\", {})\n        assert result.structured_content == {\"result\": \"First app tool\"}\n\n    async def test_first_server_wins_tools_same_prefix(self):\n        \"\"\"Test that first mounted server wins for tools when same prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.tool(name=\"shared_tool\")\n        def first_shared_tool() -> str:\n            return \"First app tool\"\n\n        @second_app.tool(name=\"shared_tool\")\n        def second_shared_tool() -> str:\n            return \"Second app tool\"\n\n        # Mount both apps with same prefix\n        main_app.mount(first_app, \"api\")\n        main_app.mount(second_app, \"api\")\n\n        # list_tools returns all components; execution uses first match\n        tools = await main_app.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"api_shared_tool\" in tool_names\n\n        # Test that calling the tool uses the first server's implementation\n        result = await main_app.call_tool(\"api_shared_tool\", {})\n        assert result.structured_content == {\"result\": \"First app tool\"}\n\n    async def test_first_server_wins_resources_no_prefix(self):\n        \"\"\"Test that first mounted server wins for resources when no prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.resource(uri=\"shared://data\")\n        def first_resource():\n            return \"First app data\"\n\n        @second_app.resource(uri=\"shared://data\")\n        def second_resource():\n            return \"Second app data\"\n\n        # Mount both apps without prefix\n        main_app.mount(first_app)\n        main_app.mount(second_app)\n\n        # list_resources returns all components; execution uses first match\n        resources = await main_app.list_resources()\n        resource_uris = [str(r.uri) for r in resources]\n        assert \"shared://data\" in resource_uris\n\n        # Test that reading the resource uses the first server's implementation\n        result = await main_app.read_resource(\"shared://data\")\n        assert result.contents[0].content == \"First app data\"\n\n    async def test_first_server_wins_resources_same_prefix(self):\n        \"\"\"Test that first mounted server wins for resources when same prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.resource(uri=\"shared://data\")\n        def first_resource():\n            return \"First app data\"\n\n        @second_app.resource(uri=\"shared://data\")\n        def second_resource():\n            return \"Second app data\"\n\n        # Mount both apps with same prefix\n        main_app.mount(first_app, \"api\")\n        main_app.mount(second_app, \"api\")\n\n        # list_resources returns all components; execution uses first match\n        resources = await main_app.list_resources()\n        resource_uris = [str(r.uri) for r in resources]\n        assert \"shared://api/data\" in resource_uris\n\n        # Test that reading the resource uses the first server's implementation\n        result = await main_app.read_resource(\"shared://api/data\")\n        assert result.contents[0].content == \"First app data\"\n\n    async def test_first_server_wins_resource_templates_no_prefix(self):\n        \"\"\"Test that first mounted server wins for resource templates when no prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.resource(uri=\"users://{user_id}/profile\")\n        def first_template(user_id: str):\n            return f\"First app user {user_id}\"\n\n        @second_app.resource(uri=\"users://{user_id}/profile\")\n        def second_template(user_id: str):\n            return f\"Second app user {user_id}\"\n\n        # Mount both apps without prefix\n        main_app.mount(first_app)\n        main_app.mount(second_app)\n\n        # list_resource_templates returns all components; execution uses first match\n        templates = await main_app.list_resource_templates()\n        template_uris = [t.uri_template for t in templates]\n        assert \"users://{user_id}/profile\" in template_uris\n\n        # Test that reading the resource uses the first server's implementation\n        result = await main_app.read_resource(\"users://123/profile\")\n        assert result.contents[0].content == \"First app user 123\"\n\n    async def test_first_server_wins_resource_templates_same_prefix(self):\n        \"\"\"Test that first mounted server wins for resource templates when same prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.resource(uri=\"users://{user_id}/profile\")\n        def first_template(user_id: str):\n            return f\"First app user {user_id}\"\n\n        @second_app.resource(uri=\"users://{user_id}/profile\")\n        def second_template(user_id: str):\n            return f\"Second app user {user_id}\"\n\n        # Mount both apps with same prefix\n        main_app.mount(first_app, \"api\")\n        main_app.mount(second_app, \"api\")\n\n        # list_resource_templates returns all components; execution uses first match\n        templates = await main_app.list_resource_templates()\n        template_uris = [t.uri_template for t in templates]\n        assert \"users://api/{user_id}/profile\" in template_uris\n\n        # Test that reading the resource uses the first server's implementation\n        result = await main_app.read_resource(\"users://api/123/profile\")\n        assert result.contents[0].content == \"First app user 123\"\n\n    async def test_first_server_wins_prompts_no_prefix(self):\n        \"\"\"Test that first mounted server wins for prompts when no prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.prompt(name=\"shared_prompt\")\n        def first_shared_prompt() -> str:\n            return \"First app prompt\"\n\n        @second_app.prompt(name=\"shared_prompt\")\n        def second_shared_prompt() -> str:\n            return \"Second app prompt\"\n\n        # Mount both apps without prefix\n        main_app.mount(first_app)\n        main_app.mount(second_app)\n\n        # list_prompts returns all components; execution uses first match\n        prompts = await main_app.list_prompts()\n        prompt_names = [p.name for p in prompts]\n        assert \"shared_prompt\" in prompt_names\n\n        # Test that getting the prompt uses the first server's implementation\n        result = await main_app.render_prompt(\"shared_prompt\")\n        assert result.messages is not None\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"First app prompt\"\n\n    async def test_first_server_wins_prompts_same_prefix(self):\n        \"\"\"Test that first mounted server wins for prompts when same prefix is used.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        first_app = FastMCP(\"FirstApp\")\n        second_app = FastMCP(\"SecondApp\")\n\n        @first_app.prompt(name=\"shared_prompt\")\n        def first_shared_prompt() -> str:\n            return \"First app prompt\"\n\n        @second_app.prompt(name=\"shared_prompt\")\n        def second_shared_prompt() -> str:\n            return \"Second app prompt\"\n\n        # Mount both apps with same prefix\n        main_app.mount(first_app, \"api\")\n        main_app.mount(second_app, \"api\")\n\n        # list_prompts returns all components; execution uses first match\n        prompts = await main_app.list_prompts()\n        prompt_names = [p.name for p in prompts]\n        assert \"api_shared_prompt\" in prompt_names\n\n        # Test that getting the prompt uses the first server's implementation\n        result = await main_app.render_prompt(\"api_shared_prompt\")\n        assert result.messages is not None\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"First app prompt\"\n"
  },
  {
    "path": "tests/server/mount/test_prompts.py",
    "content": "\"\"\"Tests for prompt mounting.\"\"\"\n\nfrom fastmcp import FastMCP\n\n\nclass TestPrompts:\n    \"\"\"Test mounting with prompts.\"\"\"\n\n    async def test_mount_with_prompts(self):\n        \"\"\"Test mounting a server with prompts.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        assistant_app = FastMCP(\"AssistantApp\")\n\n        @assistant_app.prompt\n        def greeting(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Mount the assistant app\n        main_app.mount(assistant_app, \"assistant\")\n\n        # Prompt should be accessible through main app\n        prompts = await main_app.list_prompts()\n        assert any(p.name == \"assistant_greeting\" for p in prompts)\n\n        # Render the prompt\n        result = await main_app.render_prompt(\"assistant_greeting\", {\"name\": \"World\"})\n        assert result.messages is not None\n        # The message should contain our greeting text\n\n    async def test_adding_prompt_after_mounting(self):\n        \"\"\"Test adding a prompt after mounting.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        assistant_app = FastMCP(\"AssistantApp\")\n\n        # Mount the assistant app before adding prompts\n        main_app.mount(assistant_app, \"assistant\")\n\n        # Add a prompt after mounting\n        @assistant_app.prompt\n        def farewell(name: str) -> str:\n            return f\"Goodbye, {name}!\"\n\n        # Prompt should be accessible through main app\n        prompts = await main_app.list_prompts()\n        assert any(p.name == \"assistant_farewell\" for p in prompts)\n\n        # Render the prompt\n        result = await main_app.render_prompt(\"assistant_farewell\", {\"name\": \"World\"})\n        assert result.messages is not None\n        # The message should contain our farewell text\n"
  },
  {
    "path": "tests/server/mount/test_proxy.py",
    "content": "\"\"\"Tests for proxy server mounting.\"\"\"\n\nimport json\nfrom contextlib import asynccontextmanager\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.server.providers.proxy import FastMCPProxy\nfrom fastmcp.server.providers.wrapped_provider import _WrappedProvider\nfrom fastmcp.server.transforms import Namespace\n\n\nclass TestProxyServer:\n    \"\"\"Test mounting a proxy server.\"\"\"\n\n    async def test_mount_proxy_server(self):\n        \"\"\"Test mounting a proxy server.\"\"\"\n        # Create original server\n        original_server = FastMCP(\"OriginalServer\")\n\n        @original_server.tool\n        def get_data(query: str) -> str:\n            return f\"Data for {query}\"\n\n        # Create proxy server\n        proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server))\n\n        # Mount proxy server\n        main_app = FastMCP(\"MainApp\")\n        main_app.mount(proxy_server, \"proxy\")\n\n        # Tool should be accessible through main app\n        tools = await main_app.list_tools()\n        assert any(t.name == \"proxy_get_data\" for t in tools)\n\n        # Call the tool\n        result = await main_app.call_tool(\"proxy_get_data\", {\"query\": \"test\"})\n        assert result.structured_content == {\"result\": \"Data for test\"}\n\n    async def test_dynamically_adding_to_proxied_server(self):\n        \"\"\"Test that changes to the original server are reflected in the mounted proxy.\"\"\"\n        # Create original server\n        original_server = FastMCP(\"OriginalServer\")\n\n        # Create proxy server\n        proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server))\n\n        # Mount proxy server\n        main_app = FastMCP(\"MainApp\")\n        main_app.mount(proxy_server, \"proxy\")\n\n        # Add a tool to the original server\n        @original_server.tool\n        def dynamic_data() -> str:\n            return \"Dynamic data\"\n\n        # Tool should be accessible through main app via proxy\n        tools = await main_app.list_tools()\n        assert any(t.name == \"proxy_dynamic_data\" for t in tools)\n\n        # Call the tool\n        result = await main_app.call_tool(\"proxy_dynamic_data\", {})\n        assert result.structured_content == {\"result\": \"Dynamic data\"}\n\n    async def test_proxy_server_with_resources(self):\n        \"\"\"Test mounting a proxy server with resources.\"\"\"\n        # Create original server\n        original_server = FastMCP(\"OriginalServer\")\n\n        @original_server.resource(uri=\"config://settings\")\n        def get_config() -> str:\n            return json.dumps({\"api_key\": \"12345\"})\n\n        # Create proxy server\n        proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server))\n\n        # Mount proxy server\n        main_app = FastMCP(\"MainApp\")\n        main_app.mount(proxy_server, \"proxy\")\n\n        # Resource should be accessible through main app\n        result = await main_app.read_resource(\"config://proxy/settings\")\n        assert len(result.contents) == 1\n        config = json.loads(result.contents[0].content)\n        assert config[\"api_key\"] == \"12345\"\n\n    async def test_proxy_server_with_prompts(self):\n        \"\"\"Test mounting a proxy server with prompts.\"\"\"\n        # Create original server\n        original_server = FastMCP(\"OriginalServer\")\n\n        @original_server.prompt\n        def welcome(name: str) -> str:\n            return f\"Welcome, {name}!\"\n\n        # Create proxy server\n        proxy_server = FastMCP.as_proxy(FastMCPTransport(original_server))\n\n        # Mount proxy server\n        main_app = FastMCP(\"MainApp\")\n        main_app.mount(proxy_server, \"proxy\")\n\n        # Prompt should be accessible through main app\n        result = await main_app.render_prompt(\"proxy_welcome\", {\"name\": \"World\"})\n        assert result.messages is not None\n        # The message should contain our welcome text\n\n\nclass TestAsProxyKwarg:\n    \"\"\"Test the as_proxy kwarg.\"\"\"\n\n    async def test_as_proxy_defaults_false(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def sub_tool() -> str:\n            return \"test\"\n\n        mcp.mount(sub, \"sub\")\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is sub\n        # Verify namespace is applied\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"sub_sub_tool\"}\n\n    async def test_as_proxy_false(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def sub_tool() -> str:\n            return \"test\"\n\n        mcp.mount(sub, \"sub\", as_proxy=False)\n\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is sub\n        # Verify namespace is applied\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"sub_sub_tool\"}\n\n    async def test_as_proxy_true(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def sub_tool() -> str:\n            return \"test\"\n\n        mcp.mount(sub, \"sub\", as_proxy=True)\n\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider wrapping a proxy\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is not sub\n        assert isinstance(provider._inner.server, FastMCPProxy)\n        # Verify namespace is applied\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"sub_sub_tool\"}\n\n    async def test_lifespan_server_mounted_directly(self):\n        \"\"\"Test that servers with lifespan are mounted directly (not auto-proxied).\n\n        Since FastMCPProvider now handles lifespan via the provider lifespan interface,\n        there's no need to auto-convert to a proxy. The server is mounted directly.\n        \"\"\"\n\n        @asynccontextmanager\n        async def server_lifespan(mcp: FastMCP):\n            yield\n\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\", lifespan=server_lifespan)\n\n        @sub.tool\n        def sub_tool() -> str:\n            return \"test\"\n\n        mcp.mount(sub, \"sub\")\n\n        # Server should be mounted directly without auto-proxying\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is sub\n        # Verify namespace is applied\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"sub_sub_tool\"}\n\n    async def test_as_proxy_ignored_for_proxy_mounts_default(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n        sub_proxy = FastMCP.as_proxy(FastMCPTransport(sub))\n\n        mcp.mount(sub_proxy, \"sub\")\n\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is sub_proxy\n\n    async def test_as_proxy_ignored_for_proxy_mounts_false(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n        sub_proxy = FastMCP.as_proxy(FastMCPTransport(sub))\n\n        mcp.mount(sub_proxy, \"sub\", as_proxy=False)\n\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is sub_proxy\n\n    async def test_as_proxy_ignored_for_proxy_mounts_true(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n        sub_proxy = FastMCP.as_proxy(FastMCPTransport(sub))\n\n        mcp.mount(sub_proxy, \"sub\", as_proxy=True)\n\n        # Index 1 because LocalProvider is at index 0\n        provider = mcp.providers[1]\n        # Provider is wrapped with Namespace transform\n        assert isinstance(provider, _WrappedProvider)\n        assert len(provider._transforms) == 1\n        assert isinstance(provider._transforms[0], Namespace)\n        # Inner provider is FastMCPProvider\n        assert isinstance(provider._inner, FastMCPProvider)\n        assert provider._inner.server is sub_proxy\n\n    async def test_as_proxy_mounts_still_have_live_link(self):\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\")\n\n        mcp.mount(sub, \"sub\", as_proxy=True)\n\n        assert len(await mcp.list_tools()) == 0\n\n        @sub.tool\n        def hello():\n            return \"hi\"\n\n        assert len(await mcp.list_tools()) == 1\n\n    async def test_sub_lifespan_is_executed(self):\n        lifespan_check = []\n\n        @asynccontextmanager\n        async def lifespan(mcp: FastMCP):\n            lifespan_check.append(\"start\")\n            yield\n\n        mcp = FastMCP(\"Main\")\n        sub = FastMCP(\"Sub\", lifespan=lifespan)\n\n        @sub.tool\n        def hello():\n            return \"hi\"\n\n        mcp.mount(sub, as_proxy=True)\n\n        assert lifespan_check == []\n\n        async with Client(mcp) as client:\n            await client.call_tool(\"hello\", {})\n\n        # Lifespan is executed at least once (may be multiple times for proxy connections)\n        assert len(lifespan_check) >= 1\n        assert all(x == \"start\" for x in lifespan_check)\n"
  },
  {
    "path": "tests/server/mount/test_resources.py",
    "content": "\"\"\"Tests for resource and template mounting.\"\"\"\n\nimport json\n\nfrom fastmcp import FastMCP\n\n\nclass TestResourcesAndTemplates:\n    \"\"\"Test mounting with resources and resource templates.\"\"\"\n\n    async def test_mount_with_resources(self):\n        \"\"\"Test mounting a server with resources.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        data_app = FastMCP(\"DataApp\")\n\n        @data_app.resource(uri=\"data://users\")\n        async def get_users() -> str:\n            return \"user1, user2\"\n\n        # Mount the data app\n        main_app.mount(data_app, \"data\")\n\n        # Resource should be accessible through main app\n        resources = await main_app.list_resources()\n        assert any(str(r.uri) == \"data://data/users\" for r in resources)\n\n        # Check that resource can be accessed\n        result = await main_app.read_resource(\"data://data/users\")\n        assert len(result.contents) == 1\n        # Note: The function returns \"user1, user2\" which is not valid JSON\n        # This test should be updated to return proper JSON or check the string directly\n        assert result.contents[0].content == \"user1, user2\"\n\n    async def test_mount_with_resource_templates(self):\n        \"\"\"Test mounting a server with resource templates.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        user_app = FastMCP(\"UserApp\")\n\n        @user_app.resource(uri=\"users://{user_id}/profile\")\n        def get_user_profile(user_id: str) -> str:\n            return json.dumps({\"id\": user_id, \"name\": f\"User {user_id}\"})\n\n        # Mount the user app\n        main_app.mount(user_app, \"api\")\n\n        # Template should be accessible through main app\n        templates = await main_app.list_resource_templates()\n        assert any(t.uri_template == \"users://api/{user_id}/profile\" for t in templates)\n\n        # Check template instantiation\n        result = await main_app.read_resource(\"users://api/123/profile\")\n        assert len(result.contents) == 1\n        profile = json.loads(result.contents[0].content)\n        assert profile[\"id\"] == \"123\"\n        assert profile[\"name\"] == \"User 123\"\n\n    async def test_adding_resource_after_mounting(self):\n        \"\"\"Test adding a resource after mounting.\"\"\"\n        main_app = FastMCP(\"MainApp\")\n        data_app = FastMCP(\"DataApp\")\n\n        # Mount the data app before adding resources\n        main_app.mount(data_app, \"data\")\n\n        # Add a resource after mounting\n        @data_app.resource(uri=\"data://config\")\n        def get_config() -> str:\n            return json.dumps({\"version\": \"1.0\"})\n\n        # Resource should be accessible through main app\n        resources = await main_app.list_resources()\n        assert any(str(r.uri) == \"data://data/config\" for r in resources)\n\n        # Check access to the resource\n        result = await main_app.read_resource(\"data://data/config\")\n        assert len(result.contents) == 1\n        config = json.loads(result.contents[0].content)\n        assert config[\"version\"] == \"1.0\"\n\n\nclass TestResourceUriPrefixing:\n    \"\"\"Test that resource and resource template URIs get prefixed when mounted (names are NOT prefixed).\"\"\"\n\n    async def test_resource_uri_prefixing(self):\n        \"\"\"Test that resource URIs are prefixed when mounted (names are NOT prefixed).\"\"\"\n\n        # Create a sub-app with a resource\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.resource(\"resource://my_resource\")\n        def my_resource() -> str:\n            return \"Resource content\"\n\n        # Create main app and mount sub-app with prefix\n        main_app = FastMCP(\"MainApp\")\n        main_app.mount(sub_app, \"prefix\")\n\n        # Get resources from main app\n        resources = await main_app.list_resources()\n\n        # Should have prefixed key (using path format: resource://prefix/resource_name)\n        assert any(str(r.uri) == \"resource://prefix/my_resource\" for r in resources)\n\n        # The resource name should NOT be prefixed (only URI is prefixed)\n        resource = next(\n            r for r in resources if str(r.uri) == \"resource://prefix/my_resource\"\n        )\n        assert resource.name == \"my_resource\"\n\n    async def test_resource_template_uri_prefixing(self):\n        \"\"\"Test that resource template URIs are prefixed when mounted (names are NOT prefixed).\"\"\"\n\n        # Create a sub-app with a resource template\n        sub_app = FastMCP(\"SubApp\")\n\n        @sub_app.resource(\"resource://user/{user_id}\")\n        def user_template(user_id: str) -> str:\n            return f\"User {user_id} data\"\n\n        # Create main app and mount sub-app with prefix\n        main_app = FastMCP(\"MainApp\")\n        main_app.mount(sub_app, \"prefix\")\n\n        # Get resource templates from main app\n        templates = await main_app.list_resource_templates()\n\n        # Should have prefixed key (using path format: resource://prefix/template_uri)\n        assert any(\n            t.uri_template == \"resource://prefix/user/{user_id}\" for t in templates\n        )\n\n        # The template name should NOT be prefixed (only URI template is prefixed)\n        template = next(\n            t for t in templates if t.uri_template == \"resource://prefix/user/{user_id}\"\n        )\n        assert template.name == \"user_template\"\n\n\nclass TestMountedResourceTemplateQueryParams:\n    \"\"\"Test that resource templates with query params work on mounted servers.\"\"\"\n\n    async def test_mounted_template_with_query_param(self):\n        \"\"\"Query params in resource templates should work through mount.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"resource://greet{?name}\")\n        def greet(name: str = \"World\") -> str:\n            return f\"Hello, {name}!\"\n\n        main = FastMCP(\"Main\")\n        main.mount(sub, \"sub\")\n\n        result = await main.read_resource(\"resource://sub/greet?name=Alice\")\n        assert result.contents[0].content == \"Hello, Alice!\"\n\n    async def test_mounted_template_with_query_param_default(self):\n        \"\"\"Missing query params should use defaults through mount.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"resource://greet{?name}\")\n        def greet(name: str = \"World\") -> str:\n            return f\"Hello, {name}!\"\n\n        main = FastMCP(\"Main\")\n        main.mount(sub, \"sub\")\n\n        result = await main.read_resource(\"resource://sub/greet\")\n        assert result.contents[0].content == \"Hello, World!\"\n\n    async def test_mounted_template_with_multiple_query_params(self):\n        \"\"\"Multiple query params should all pass through mount correctly.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"resource://data/{id}{?format,verbose}\")\n        def get_data(id: str, format: str = \"json\", verbose: bool = False) -> str:\n            return f\"id={id} format={format} verbose={verbose}\"\n\n        main = FastMCP(\"Main\")\n        main.mount(sub, \"api\")\n\n        result = await main.read_resource(\n            \"resource://api/data/42?format=xml&verbose=true\"\n        )\n        assert result.contents[0].content == \"id=42 format=xml verbose=True\"\n\n    async def test_mounted_template_with_partial_query_params(self):\n        \"\"\"Providing only some query params should use defaults for the rest.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"resource://data/{id}{?format,limit}\")\n        def get_data(id: str, format: str = \"json\", limit: int = 10) -> str:\n            return f\"id={id} format={format} limit={limit}\"\n\n        main = FastMCP(\"Main\")\n        main.mount(sub, \"api\")\n\n        result = await main.read_resource(\"resource://api/data/42?limit=5\")\n        assert result.contents[0].content == \"id=42 format=json limit=5\"\n"
  },
  {
    "path": "tests/server/providers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/providers/local_provider_tools/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_context.py",
    "content": "\"\"\"Tests for tool context injection.\"\"\"\n\nimport functools\nfrom dataclasses import dataclass\n\nfrom pydantic import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.tools.base import Tool\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolContextInjection:\n    \"\"\"Test context injection in tools.\"\"\"\n\n    async def test_context_detection(self):\n        \"\"\"Test that context parameters are properly detected and excluded from schema.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def tool_with_context(x: int, ctx: Context) -> str:\n            return f\"Request: {x}\"\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"tool_with_context\"\n        # Context param should not appear in schema\n        assert \"ctx\" not in tools[0].parameters.get(\"properties\", {})\n\n    async def test_context_injection_basic(self):\n        \"\"\"Test that context is properly injected into tool calls.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def tool_with_context(x: int, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return f\"Got context with x={x}\"\n\n        result = await mcp.call_tool(\"tool_with_context\", {\"x\": 42})\n        assert result.structured_content == {\"result\": \"Got context with x=42\"}\n\n    async def test_async_context(self):\n        \"\"\"Test that context works in async functions.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        async def async_tool(x: int, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return f\"Async with x={x}\"\n\n        result = await mcp.call_tool(\"async_tool\", {\"x\": 42})\n        assert result.structured_content == {\"result\": \"Async with x=42\"}\n\n    async def test_optional_context(self):\n        \"\"\"Test that context is optional.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def no_context(x: int) -> int:\n            return x * 2\n\n        result = await mcp.call_tool(\"no_context\", {\"x\": 21})\n        assert result.structured_content == {\"result\": 42}\n\n    async def test_context_resource_access(self):\n        \"\"\"Test that context can access resources.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"test://data\")\n        def test_resource() -> str:\n            return \"resource data\"\n\n        @mcp.tool\n        async def tool_with_resource(ctx: Context) -> str:\n            result = await ctx.read_resource(\"test://data\")\n            assert len(result.contents) == 1\n            r = result.contents[0]\n            return f\"Read resource: {r.content} with mime type {r.mime_type}\"\n\n        result = await mcp.call_tool(\"tool_with_resource\", {})\n        assert result.structured_content == {\n            \"result\": \"Read resource: resource data with mime type text/plain\"\n        }\n\n    async def test_tool_decorator_with_tags(self):\n        \"\"\"Test that the tool decorator properly sets tags.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(tags={\"example\", \"test-tag\"})\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].tags == {\"example\", \"test-tag\"}\n\n    async def test_callable_object_with_context(self):\n        \"\"\"Test that a callable object can be used as a tool with context.\"\"\"\n        mcp = FastMCP()\n\n        class MyTool:\n            async def __call__(self, x: int, ctx: Context) -> int:\n                assert isinstance(ctx, Context)\n                return x + 1\n\n        mcp.add_tool(Tool.from_function(MyTool(), name=\"MyTool\"))\n\n        result = await mcp.call_tool(\"MyTool\", {\"x\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_decorated_tool_with_functools_wraps(self):\n        \"\"\"Regression test for #2524: @mcp.tool with functools.wraps decorator.\"\"\"\n\n        def custom_decorator(func):\n            @functools.wraps(func)\n            async def wrapper(*args, **kwargs):\n                return await func(*args, **kwargs)\n\n            return wrapper\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        @custom_decorator\n        async def decorated_tool(ctx: Context, query: str) -> str:\n            assert isinstance(ctx, Context)\n            return f\"query: {query}\"\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"decorated_tool\")\n        assert \"ctx\" not in tool.parameters.get(\"properties\", {})\n\n        result = await mcp.call_tool(\"decorated_tool\", {\"query\": \"test\"})\n        assert result.structured_content == {\"result\": \"query: test\"}\n"
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_decorator.py",
    "content": "\"\"\"Tests for tool decorator patterns.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Annotated\n\nimport pytest\nfrom pydantic import BaseModel, Field\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import NotFoundError\nfrom fastmcp.tools.base import Tool\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolDecorator:\n    async def test_no_tools_before_decorator(self):\n        mcp = FastMCP()\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool: 'add'\"):\n            await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n\n    async def test_tool_decorator(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        result = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_without_parentheses(self):\n        \"\"\"Test that @tool decorator works without parentheses.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        tools = await mcp.list_tools()\n        assert any(t.name == \"add\" for t in tools)\n\n        result = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_with_name(self):\n        mcp = FastMCP()\n\n        @mcp.tool(name=\"custom-add\")\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        result = await mcp.call_tool(\"custom-add\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_with_description(self):\n        mcp = FastMCP()\n\n        @mcp.tool(description=\"Add two numbers\")\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        tool = tools[0]\n        assert tool.description == \"Add two numbers\"\n\n    async def test_tool_decorator_instance_method(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            def __init__(self, x: int):\n                self.x = x\n\n            def add(self, y: int) -> int:\n                return self.x + y\n\n        obj = MyClass(10)\n        mcp.add_tool(Tool.from_function(obj.add))\n        result = await mcp.call_tool(\"add\", {\"y\": 2})\n        assert result.structured_content == {\"result\": 12}\n\n    async def test_tool_decorator_classmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            x: int = 10\n\n            @classmethod\n            def add(cls, y: int) -> int:\n                return cls.x + y\n\n        mcp.add_tool(Tool.from_function(MyClass.add))\n        result = await mcp.call_tool(\"add\", {\"y\": 2})\n        assert result.structured_content == {\"result\": 12}\n\n    async def test_tool_decorator_staticmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.tool\n            @staticmethod\n            def add(x: int, y: int) -> int:\n                return x + y\n\n        result = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_async_function(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        async def add(x: int, y: int) -> int:\n            return x + y\n\n        result = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_classmethod_error(self):\n        mcp = FastMCP()\n\n        with pytest.raises(TypeError, match=\"classmethod\"):\n\n            class MyClass:\n                @mcp.tool\n                @classmethod\n                def add(cls, y: int) -> None:\n                    pass\n\n    async def test_tool_decorator_classmethod_async_function(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            x = 10\n\n            @classmethod\n            async def add(cls, y: int) -> int:\n                return cls.x + y\n\n        mcp.add_tool(Tool.from_function(MyClass.add))\n        result = await mcp.call_tool(\"add\", {\"y\": 2})\n        assert result.structured_content == {\"result\": 12}\n\n    async def test_tool_decorator_staticmethod_async_function(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            @staticmethod\n            async def add(x: int, y: int) -> int:\n                return x + y\n\n        mcp.add_tool(Tool.from_function(MyClass.add))\n        result = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_staticmethod_order(self):\n        \"\"\"Test that the recommended decorator order works for static methods\"\"\"\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.tool\n            @staticmethod\n            def add_v1(x: int, y: int) -> int:\n                return x + y\n\n        result = await mcp.call_tool(\"add_v1\", {\"x\": 1, \"y\": 2})\n        assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_decorator_with_tags(self):\n        \"\"\"Test that the tool decorator properly sets tags.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(tags={\"example\", \"test-tag\"})\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].tags == {\"example\", \"test-tag\"}\n\n    async def test_add_tool_with_custom_name(self):\n        \"\"\"Test adding a tool with a custom name using server.add_tool().\"\"\"\n        mcp = FastMCP()\n\n        def multiply(a: int, b: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return a * b\n\n        mcp.add_tool(Tool.from_function(multiply, name=\"custom_multiply\"))\n\n        tools = await mcp.list_tools()\n        assert any(t.name == \"custom_multiply\" for t in tools)\n\n        result = await mcp.call_tool(\"custom_multiply\", {\"a\": 5, \"b\": 3})\n        assert result.structured_content == {\"result\": 15}\n\n        assert not any(t.name == \"multiply\" for t in tools)\n\n    async def test_tool_with_annotated_arguments(self):\n        \"\"\"Test that tools with annotated arguments work correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(\n            x: Annotated[int, Field(description=\"x is an int\")],\n            y: Annotated[str, Field(description=\"y is not an int\")],\n        ) -> None:\n            pass\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"add\")\n        assert tool.parameters[\"properties\"][\"x\"][\"description\"] == \"x is an int\"\n        assert tool.parameters[\"properties\"][\"y\"][\"description\"] == \"y is not an int\"\n\n    async def test_tool_with_field_defaults(self):\n        \"\"\"Test that tools with annotated arguments work correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(\n            x: int = Field(description=\"x is an int\"),\n            y: str = Field(description=\"y is not an int\"),\n        ) -> None:\n            pass\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"add\")\n        assert tool.parameters[\"properties\"][\"x\"][\"description\"] == \"x is an int\"\n        assert tool.parameters[\"properties\"][\"y\"][\"description\"] == \"y is not an int\"\n\n    async def test_tool_direct_function_call(self):\n        \"\"\"Test that tools can be registered via direct function call.\"\"\"\n        from typing import cast\n\n        from fastmcp.tools.function_tool import DecoratedTool\n\n        mcp = FastMCP()\n\n        def standalone_function(x: int, y: int) -> int:\n            \"\"\"A standalone function to be registered.\"\"\"\n            return x + y\n\n        result_fn = mcp.tool(standalone_function, name=\"direct_call_tool\")\n\n        # In new decorator mode, returns the function with metadata\n        decorated = cast(DecoratedTool, result_fn)\n        assert hasattr(result_fn, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"direct_call_tool\"\n        assert result_fn is standalone_function\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"direct_call_tool\")\n        # Tool is registered separately, not same object as decorated function\n        assert tool.name == \"direct_call_tool\"\n\n        result = await mcp.call_tool(\"direct_call_tool\", {\"x\": 5, \"y\": 3})\n        assert result.structured_content == {\"result\": 8}\n\n    async def test_tool_decorator_with_string_name(self):\n        \"\"\"Test that @tool(\"custom_name\") syntax works correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(\"string_named_tool\")\n        def my_function(x: int) -> str:\n            \"\"\"A function with a string name.\"\"\"\n            return f\"Result: {x}\"\n\n        tools = await mcp.list_tools()\n        assert any(t.name == \"string_named_tool\" for t in tools)\n        assert not any(t.name == \"my_function\" for t in tools)\n\n        result = await mcp.call_tool(\"string_named_tool\", {\"x\": 42})\n        assert result.structured_content == {\"result\": \"Result: 42\"}\n\n    async def test_tool_decorator_conflicting_names_error(self):\n        \"\"\"Test that providing both positional and keyword name raises an error.\"\"\"\n        mcp = FastMCP()\n\n        with pytest.raises(\n            TypeError,\n            match=\"Cannot specify both a name as first argument and as keyword argument\",\n        ):\n\n            @mcp.tool(\"positional_name\", name=\"keyword_name\")\n            def my_function(x: int) -> str:\n                return f\"Result: {x}\"\n\n    async def test_tool_decorator_with_output_schema(self):\n        mcp = FastMCP()\n\n        with pytest.raises(\n            ValueError, match=\"Output schemas must represent object types\"\n        ):\n\n            @mcp.tool(output_schema={\"type\": \"integer\"})\n            def my_function(x: int) -> str:\n                return f\"Result: {x}\"\n\n    async def test_tool_decorator_with_meta(self):\n        \"\"\"Test that meta parameter is passed through the tool decorator.\"\"\"\n        mcp = FastMCP()\n\n        meta_data = {\"version\": \"1.0\", \"author\": \"test\"}\n\n        @mcp.tool(meta=meta_data)\n        def multiply(a: int, b: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return a * b\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"multiply\")\n\n        assert tool.meta == meta_data\n"
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_enabled.py",
    "content": "\"\"\"Tests for tool enabled/disabled state.\"\"\"\n\nfrom dataclasses import dataclass\n\nimport pytest\nfrom pydantic import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import NotFoundError\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolEnabled:\n    async def test_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        # Tool is enabled by default\n        tools = await mcp.list_tools()\n        assert any(t.name == \"sample_tool\" for t in tools)\n\n        # Disable via server\n        mcp.disable(names={\"sample_tool\"}, components={\"tool\"})\n\n        # Tool should not be in list when disabled\n        tools = await mcp.list_tools()\n        assert not any(t.name == \"sample_tool\" for t in tools)\n\n        # Re-enable via server\n        mcp.enable(names={\"sample_tool\"}, components={\"tool\"})\n        tools = await mcp.list_tools()\n        assert any(t.name == \"sample_tool\" for t in tools)\n\n    async def test_tool_disabled_via_server(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        mcp.disable(names={\"sample_tool\"}, components={\"tool\"})\n        tools = await mcp.list_tools()\n        assert len(tools) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await mcp.call_tool(\"sample_tool\", {\"x\": 5})\n\n    async def test_tool_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        mcp.disable(names={\"sample_tool\"}, components={\"tool\"})\n        mcp.enable(names={\"sample_tool\"}, components={\"tool\"})\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n\n    async def test_tool_toggle_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        mcp.disable(names={\"sample_tool\"}, components={\"tool\"})\n        tools = await mcp.list_tools()\n        assert len(tools) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await mcp.call_tool(\"sample_tool\", {\"x\": 5})\n\n    async def test_get_tool_and_disable(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        tool = await mcp.get_tool(\"sample_tool\")\n        assert tool is not None\n\n        mcp.disable(names={\"sample_tool\"}, components={\"tool\"})\n        tools = await mcp.list_tools()\n        assert len(tools) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await mcp.call_tool(\"sample_tool\", {\"x\": 5})\n\n    async def test_cant_call_disabled_tool(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def sample_tool(x: int) -> int:\n            return x * 2\n\n        mcp.disable(names={\"sample_tool\"}, components={\"tool\"})\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await mcp.call_tool(\"sample_tool\", {\"x\": 5})\n"
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_local_provider_tools.py",
    "content": "\"\"\"Core tool return types and serialization tests.\"\"\"\n\nimport base64\nimport datetime\nimport json\nimport uuid\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom mcp.types import (\n    AudioContent,\n    EmbeddedResource,\n    ImageContent,\n    TextContent,\n)\nfrom pydantic import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.types import Audio, File, Image\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolReturnTypes:\n    async def test_string(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def string_tool() -> str:\n            return \"Hello, world!\"\n\n        result = await mcp.call_tool(\"string_tool\", {})\n        assert result.structured_content == {\"result\": \"Hello, world!\"}\n\n    async def test_bytes(self, tmp_path: Path):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def bytes_tool() -> bytes:\n            return b\"Hello, world!\"\n\n        result = await mcp.call_tool(\"bytes_tool\", {})\n        assert result.structured_content == {\"result\": \"Hello, world!\"}\n\n    async def test_uuid(self):\n        mcp = FastMCP()\n\n        test_uuid = uuid.uuid4()\n\n        @mcp.tool\n        def uuid_tool() -> uuid.UUID:\n            return test_uuid\n\n        result = await mcp.call_tool(\"uuid_tool\", {})\n        assert result.structured_content == {\"result\": str(test_uuid)}\n\n    async def test_path(self):\n        mcp = FastMCP()\n\n        test_path = Path(\"/tmp/test.txt\")\n\n        @mcp.tool\n        def path_tool() -> Path:\n            return test_path\n\n        result = await mcp.call_tool(\"path_tool\", {})\n        assert result.structured_content == {\"result\": str(test_path)}\n\n    async def test_datetime(self):\n        mcp = FastMCP()\n\n        dt = datetime.datetime(2025, 4, 25, 1, 2, 3)\n\n        @mcp.tool\n        def datetime_tool() -> datetime.datetime:\n            return dt\n\n        result = await mcp.call_tool(\"datetime_tool\", {})\n        assert result.structured_content == {\"result\": dt.isoformat()}\n\n    async def test_image(self, tmp_path: Path):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def image_tool(path: str) -> Image:\n            return Image(path)\n\n        image_path = tmp_path / \"test.png\"\n        image_path.write_bytes(b\"fake png data\")\n\n        result = await mcp.call_tool(\"image_tool\", {\"path\": str(image_path)})\n        assert result.structured_content is None\n        assert isinstance(result.content, list)\n        content = result.content[0]\n        assert isinstance(content, ImageContent)\n        assert content.type == \"image\"\n        assert content.mimeType == \"image/png\"\n        decoded = base64.b64decode(content.data)\n        assert decoded == b\"fake png data\"\n\n    async def test_audio(self, tmp_path: Path):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def audio_tool(path: str) -> Audio:\n            return Audio(path)\n\n        audio_path = tmp_path / \"test.wav\"\n        audio_path.write_bytes(b\"fake wav data\")\n\n        result = await mcp.call_tool(\"audio_tool\", {\"path\": str(audio_path)})\n        assert isinstance(result.content, list)\n        content = result.content[0]\n        assert isinstance(content, AudioContent)\n        assert content.type == \"audio\"\n        assert content.mimeType == \"audio/wav\"\n        decoded = base64.b64decode(content.data)\n        assert decoded == b\"fake wav data\"\n\n    async def test_file(self, tmp_path: Path):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def file_tool(path: str) -> File:\n            return File(path)\n\n        file_path = tmp_path / \"test.bin\"\n        file_path.write_bytes(b\"test file data\")\n\n        result = await mcp.call_tool(\"file_tool\", {\"path\": str(file_path)})\n        assert isinstance(result.content, list)\n        content = result.content[0]\n        assert isinstance(content, EmbeddedResource)\n        assert content.type == \"resource\"\n        resource = content.resource\n        assert resource.mimeType == \"application/octet-stream\"\n        assert hasattr(resource, \"blob\")\n        blob_data = getattr(resource, \"blob\")\n        decoded = base64.b64decode(blob_data)\n        assert decoded == b\"test file data\"\n        assert str(resource.uri) == file_path.resolve().as_uri()\n\n    async def test_tool_mixed_content(self, tool_server: FastMCP):\n        result = await tool_server.call_tool(\"mixed_content_tool\", {})\n        assert isinstance(result.content, list)\n        assert len(result.content) == 3\n        content1 = result.content[0]\n        content2 = result.content[1]\n        content3 = result.content[2]\n        assert isinstance(content1, TextContent)\n        assert content1.text == \"Hello\"\n        assert isinstance(content2, ImageContent)\n        assert content2.mimeType == \"application/octet-stream\"\n        assert content2.data == \"abc\"\n        assert isinstance(content3, EmbeddedResource)\n        assert content3.type == \"resource\"\n        resource = content3.resource\n        assert resource.mimeType == \"application/octet-stream\"\n        assert hasattr(resource, \"blob\")\n        blob_data = getattr(resource, \"blob\")\n        decoded = base64.b64decode(blob_data)\n        assert decoded == b\"abc\"\n\n    async def test_tool_mixed_list_with_image(\n        self, tool_server: FastMCP, tmp_path: Path\n    ):\n        \"\"\"Test that lists containing Image objects and other types are handled\n        correctly. Items now preserve their original order.\"\"\"\n        image_path = tmp_path / \"test.png\"\n        image_path.write_bytes(b\"test image data\")\n\n        result = await tool_server.call_tool(\n            \"mixed_list_fn\", {\"image_path\": str(image_path)}\n        )\n        assert isinstance(result.content, list)\n        assert len(result.content) == 4\n        content1 = result.content[0]\n        assert isinstance(content1, TextContent)\n        assert content1.text == \"text message\"\n        content2 = result.content[1]\n        assert isinstance(content2, ImageContent)\n        assert content2.mimeType == \"image/png\"\n        assert base64.b64decode(content2.data) == b\"test image data\"\n        content3 = result.content[2]\n        assert isinstance(content3, TextContent)\n        assert json.loads(content3.text) == {\"key\": \"value\"}\n        content4 = result.content[3]\n        assert isinstance(content4, TextContent)\n        assert content4.text == \"direct content\"\n\n    async def test_tool_mixed_list_with_audio(\n        self, tool_server: FastMCP, tmp_path: Path\n    ):\n        \"\"\"Test that lists containing Audio objects and other types are handled\n        correctly. Items now preserve their original order.\"\"\"\n        audio_path = tmp_path / \"test.wav\"\n        audio_path.write_bytes(b\"test audio data\")\n\n        result = await tool_server.call_tool(\n            \"mixed_audio_list_fn\", {\"audio_path\": str(audio_path)}\n        )\n        assert isinstance(result.content, list)\n        assert len(result.content) == 4\n        content1 = result.content[0]\n        assert isinstance(content1, TextContent)\n        assert content1.text == \"text message\"\n        content2 = result.content[1]\n        assert isinstance(content2, AudioContent)\n        assert content2.mimeType == \"audio/wav\"\n        assert base64.b64decode(content2.data) == b\"test audio data\"\n        content3 = result.content[2]\n        assert isinstance(content3, TextContent)\n        assert json.loads(content3.text) == {\"key\": \"value\"}\n        content4 = result.content[3]\n        assert isinstance(content4, TextContent)\n        assert content4.text == \"direct content\"\n\n    async def test_tool_mixed_list_with_file(\n        self, tool_server: FastMCP, tmp_path: Path\n    ):\n        \"\"\"Test that lists containing File objects and other types are handled\n        correctly. Items now preserve their original order.\"\"\"\n        file_path = tmp_path / \"test.bin\"\n        file_path.write_bytes(b\"test file data\")\n\n        result = await tool_server.call_tool(\n            \"mixed_file_list_fn\", {\"file_path\": str(file_path)}\n        )\n        assert isinstance(result.content, list)\n        assert len(result.content) == 4\n        content1 = result.content[0]\n        assert isinstance(content1, TextContent)\n        assert content1.text == \"text message\"\n        content2 = result.content[1]\n        assert isinstance(content2, EmbeddedResource)\n        assert content2.type == \"resource\"\n        resource = content2.resource\n        assert resource.mimeType == \"application/octet-stream\"\n        assert hasattr(resource, \"blob\")\n        blob_data = getattr(resource, \"blob\")\n        assert base64.b64decode(blob_data) == b\"test file data\"\n        content3 = result.content[2]\n        assert isinstance(content3, TextContent)\n        assert json.loads(content3.text) == {\"key\": \"value\"}\n        content4 = result.content[3]\n        assert isinstance(content4, TextContent)\n        assert content4.text == \"direct content\"\n"
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_output_schema.py",
    "content": "\"\"\"Tests for tool output schemas.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Any, Literal\n\nimport pytest\nfrom mcp.types import (\n    TextContent,\n)\nfrom pydantic import AnyUrl, BaseModel, TypeAdapter\nfrom typing_extensions import TypeAliasType, TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.tools.function_parsing import _is_object_schema\nfrom fastmcp.utilities.json_schema import compress_schema\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolOutputSchema:\n    @pytest.mark.parametrize(\"annotation\", [str, int, float, bool, list, AnyUrl])\n    async def test_simple_output_schema(self, annotation):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def f() -> annotation:\n            return \"hello\"\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n\n        type_schema = TypeAdapter(annotation).json_schema()\n        type_schema = compress_schema(type_schema, prune_titles=True)\n        assert tools[0].output_schema == {\n            \"type\": \"object\",\n            \"properties\": {\"result\": type_schema},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n\n    @pytest.mark.parametrize(\n        \"annotation\",\n        [dict[str, int | str], PersonTypedDict, PersonModel, PersonDataclass],\n    )\n    async def test_structured_output_schema(self, annotation):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def f() -> annotation:\n            return {\"name\": \"John\", \"age\": 30}\n\n        tools = await mcp.list_tools()\n\n        type_schema = compress_schema(\n            TypeAdapter(annotation).json_schema(), prune_titles=True\n        )\n        assert len(tools) == 1\n\n        actual_schema = _normalize_anyof_order(tools[0].output_schema)\n        expected_schema = _normalize_anyof_order(type_schema)\n        assert actual_schema == expected_schema\n\n    async def test_disabled_output_schema_no_structured_content(self):\n        mcp = FastMCP()\n\n        @mcp.tool(output_schema=None)\n        def f() -> int:\n            return 42\n\n        result = await mcp.call_tool(\"f\", {})\n        assert isinstance(result.content, list)\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"42\"\n        assert result.structured_content is None\n\n    async def test_manual_structured_content(self):\n        from typing import cast\n\n        from fastmcp.tools.function_tool import DecoratedTool\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def f() -> ToolResult:\n            return ToolResult(\n                content=\"Hello, world!\", structured_content={\"message\": \"Hello, world!\"}\n            )\n\n        # In new decorator mode, check metadata instead of attributes\n        from fastmcp.utilities.types import NotSet\n\n        decorated = cast(DecoratedTool, f)\n        assert hasattr(f, \"__fastmcp__\")\n        assert decorated.__fastmcp__.output_schema is NotSet\n\n        result = await mcp.call_tool(\"f\", {})\n        assert isinstance(result.content, list)\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Hello, world!\"\n        assert result.structured_content == {\"message\": \"Hello, world!\"}\n\n    async def test_output_schema_none(self):\n        \"\"\"Test that output_schema=None works correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(output_schema=None)\n        def simple_tool() -> int:\n            return 42\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"simple_tool\")\n        assert tool.output_schema is None\n\n        result = await mcp.call_tool(\"simple_tool\", {})\n        assert result.structured_content is None\n        assert isinstance(result.content, list)\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"42\"\n\n    async def test_output_schema_explicit_object(self):\n        \"\"\"Test explicit object output schema.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(\n            output_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"greeting\": {\"type\": \"string\"},\n                    \"count\": {\"type\": \"integer\"},\n                },\n                \"required\": [\"greeting\"],\n            }\n        )\n        def explicit_tool() -> dict[str, Any]:\n            return {\"greeting\": \"Hello\", \"count\": 42}\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"explicit_tool\")\n        expected_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"greeting\": {\"type\": \"string\"},\n                \"count\": {\"type\": \"integer\"},\n            },\n            \"required\": [\"greeting\"],\n        }\n        assert tool.output_schema == expected_schema\n\n        result = await mcp.call_tool(\"explicit_tool\", {})\n        assert result.structured_content == {\"greeting\": \"Hello\", \"count\": 42}\n\n    async def test_output_schema_wrapped_primitive(self):\n        \"\"\"Test wrapped primitive output schema.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def primitive_tool() -> str:\n            return \"Hello, primitives!\"\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"primitive_tool\")\n        expected_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"result\": {\"type\": \"string\"}},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n        assert tool.output_schema == expected_schema\n\n        result = await mcp.call_tool(\"primitive_tool\", {})\n        assert result.structured_content == {\"result\": \"Hello, primitives!\"}\n\n    async def test_output_schema_complex_type(self):\n        \"\"\"Test complex type output schema.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def complex_tool() -> list[dict[str, int]]:\n            return [{\"a\": 1, \"b\": 2}, {\"c\": 3, \"d\": 4}]\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"complex_tool\")\n        expected_inner_schema = compress_schema(\n            TypeAdapter(list[dict[str, int]]).json_schema(), prune_titles=True\n        )\n        expected_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"result\": expected_inner_schema},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n        assert tool.output_schema == expected_schema\n\n        result = await mcp.call_tool(\"complex_tool\", {})\n        expected_data = [{\"a\": 1, \"b\": 2}, {\"c\": 3, \"d\": 4}]\n        assert result.structured_content == {\"result\": expected_data}\n\n    async def test_output_schema_dataclass(self):\n        \"\"\"Test dataclass output schema.\"\"\"\n        mcp = FastMCP()\n\n        @dataclass\n        class User:\n            name: str\n            age: int\n\n        @mcp.tool\n        def dataclass_tool() -> User:\n            return User(name=\"Alice\", age=30)\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"dataclass_tool\")\n        expected_schema = compress_schema(\n            TypeAdapter(User).json_schema(), prune_titles=True\n        )\n        assert tool.output_schema == expected_schema\n        assert tool.output_schema and \"x-fastmcp-wrap-result\" not in tool.output_schema\n\n        result = await mcp.call_tool(\"dataclass_tool\", {})\n        assert result.structured_content == {\"name\": \"Alice\", \"age\": 30}\n\n    async def test_output_schema_mixed_content_types(self):\n        \"\"\"Test tools with mixed content and output schemas.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def mixed_output() -> list[Any]:\n            return [\n                \"text message\",\n                {\"structured\": \"data\"},\n                TextContent(type=\"text\", text=\"direct MCP content\"),\n            ]\n\n        result = await mcp.call_tool(\"mixed_output\", {})\n        assert isinstance(result.content, list)\n        assert len(result.content) == 3\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"text message\"\n        assert isinstance(result.content[1], TextContent)\n        assert result.content[1].text == '{\"structured\":\"data\"}'\n        assert isinstance(result.content[2], TextContent)\n        assert result.content[2].text == \"direct MCP content\"\n\n    async def test_wrapped_result_includes_meta_flag(self):\n        \"\"\"Wrapped results include wrap_result in meta.\"\"\"\n        server = FastMCP()\n\n        @server.tool\n        def list_tool() -> list[dict]:\n            return [{\"a\": 1}]\n\n        result = await server.call_tool(\"list_tool\", {})\n        assert result.structured_content == {\"result\": [{\"a\": 1}]}\n        assert result.meta == {\"fastmcp\": {\"wrap_result\": True}}\n\n    async def test_unwrapped_result_has_no_meta_flag(self):\n        \"\"\"Unwrapped dict results do not include wrap_result in meta.\"\"\"\n        server = FastMCP()\n\n        @server.tool\n        def dict_tool() -> dict[str, int]:\n            return {\"value\": 42}\n\n        result = await server.call_tool(\"dict_tool\", {})\n        assert result.structured_content == {\"value\": 42}\n        assert result.meta is None\n\n    async def test_output_schema_serialization_edge_cases(self):\n        \"\"\"Test edge cases in output schema serialization.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def edge_case_tool() -> tuple[int, str]:\n            return (42, \"hello\")\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"edge_case_tool\")\n\n        assert tool.output_schema and \"x-fastmcp-wrap-result\" in tool.output_schema\n\n        result = await mcp.call_tool(\"edge_case_tool\", {})\n        assert result.structured_content == {\"result\": [42, \"hello\"]}\n\n    async def test_output_schema_wraps_non_object_ref_schema(self):\n        \"\"\"Root $ref schemas should only skip wrapping when they resolve to objects.\"\"\"\n        mcp = FastMCP()\n        AliasType = TypeAliasType(\"AliasType\", Literal[\"foo\", \"bar\"])\n\n        @mcp.tool\n        def alias_tool() -> AliasType:\n            return \"foo\"\n\n        tools = await mcp.list_tools()\n        tool = next(t for t in tools if t.name == \"alias_tool\")\n\n        expected_inner_schema = compress_schema(\n            TypeAdapter(AliasType).json_schema(mode=\"serialization\"),\n            prune_titles=True,\n        )\n        assert tool.output_schema == {\n            \"type\": \"object\",\n            \"properties\": {\"result\": expected_inner_schema},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n\n        result = await mcp.call_tool(\"alias_tool\", {})\n        assert result.structured_content == {\"result\": \"foo\"}\n\n\nclass TestIsObjectSchemaRefResolution:\n    \"\"\"Tests for $ref resolution in _is_object_schema, including JSON Pointer\n    escaping and nested $defs paths.\"\"\"\n\n    def test_simple_ref_to_object(self):\n        schema = {\n            \"$ref\": \"#/$defs/MyModel\",\n            \"$defs\": {\n                \"MyModel\": {\"type\": \"object\", \"properties\": {\"x\": {\"type\": \"int\"}}}\n            },\n        }\n        assert _is_object_schema(schema) is True\n\n    def test_simple_ref_to_non_object(self):\n        schema = {\n            \"$ref\": \"#/$defs/MyEnum\",\n            \"$defs\": {\"MyEnum\": {\"enum\": [\"a\", \"b\"]}},\n        }\n        assert _is_object_schema(schema) is False\n\n    def test_nested_defs_path(self):\n        \"\"\"Refs like #/$defs/Outer/$defs/Inner should walk into nested dicts.\"\"\"\n        schema = {\n            \"$ref\": \"#/$defs/Outer/$defs/Inner\",\n            \"$defs\": {\n                \"Outer\": {\n                    \"$defs\": {\n                        \"Inner\": {\n                            \"type\": \"object\",\n                            \"properties\": {\"y\": {\"type\": \"string\"}},\n                        },\n                    },\n                },\n            },\n        }\n        assert _is_object_schema(schema) is True\n\n    def test_nested_defs_non_object(self):\n        schema = {\n            \"$ref\": \"#/$defs/Outer/$defs/Inner\",\n            \"$defs\": {\n                \"Outer\": {\n                    \"$defs\": {\n                        \"Inner\": {\"type\": \"string\"},\n                    },\n                },\n            },\n        }\n        assert _is_object_schema(schema) is False\n\n    def test_json_pointer_tilde_escape(self):\n        \"\"\"~0 should unescape to ~ and ~1 should unescape to /.\"\"\"\n        schema = {\n            \"$ref\": \"#/$defs/has~1slash~0tilde\",\n            \"$defs\": {\"has/slash~tilde\": {\"type\": \"object\", \"properties\": {}}},\n        }\n        assert _is_object_schema(schema) is True\n\n    def test_missing_nested_segment_returns_false(self):\n        schema = {\n            \"$ref\": \"#/$defs/Outer/$defs/Missing\",\n            \"$defs\": {\n                \"Outer\": {\n                    \"$defs\": {},\n                },\n            },\n        }\n        assert _is_object_schema(schema) is False\n"
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_parameters.py",
    "content": "\"\"\"Tests for tool parameters and validation.\"\"\"\n\nimport base64\nimport datetime\nimport uuid\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Annotated, Literal\n\nimport pytest\nfrom mcp.types import (\n    ImageContent,\n)\nfrom pydantic import BaseModel, Field\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.types import Image\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolParameters:\n    async def test_parameter_descriptions_with_field_annotations(self):\n        mcp = FastMCP(\"Test Server\")\n\n        @mcp.tool\n        def greet(\n            name: Annotated[str, Field(description=\"The name to greet\")],\n            title: Annotated[str, Field(description=\"Optional title\", default=\"\")],\n        ) -> str:\n            \"\"\"A greeting tool\"\"\"\n            return f\"Hello {title} {name}\"\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        tool = tools[0]\n\n        properties = tool.parameters[\"properties\"]\n        assert \"name\" in properties\n        assert properties[\"name\"][\"description\"] == \"The name to greet\"\n        assert \"title\" in properties\n        assert properties[\"title\"][\"description\"] == \"Optional title\"\n        assert properties[\"title\"][\"default\"] == \"\"\n        assert tool.parameters[\"required\"] == [\"name\"]\n\n    async def test_parameter_descriptions_with_field_defaults(self):\n        mcp = FastMCP(\"Test Server\")\n\n        @mcp.tool\n        def greet(\n            name: str = Field(description=\"The name to greet\"),\n            title: str = Field(description=\"Optional title\", default=\"\"),\n        ) -> str:\n            \"\"\"A greeting tool\"\"\"\n            return f\"Hello {title} {name}\"\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        tool = tools[0]\n\n        properties = tool.parameters[\"properties\"]\n        assert \"name\" in properties\n        assert properties[\"name\"][\"description\"] == \"The name to greet\"\n        assert \"title\" in properties\n        assert properties[\"title\"][\"description\"] == \"Optional title\"\n        assert properties[\"title\"][\"default\"] == \"\"\n        assert tool.parameters[\"required\"] == [\"name\"]\n\n    async def test_tool_with_bytes_input(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def process_image(image: bytes) -> Image:\n            return Image(data=image)\n\n        result = await mcp.call_tool(\"process_image\", {\"image\": b\"fake png data\"})\n        assert result.structured_content is None\n        assert isinstance(result.content, list)\n        assert isinstance(result.content[0], ImageContent)\n        assert result.content[0].mimeType == \"image/png\"\n        assert result.content[0].data == base64.b64encode(b\"fake png data\").decode()\n\n    async def test_tool_with_invalid_input(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool(x: int) -> int:\n            return x + 1\n\n        with pytest.raises(\n            ValidationError,\n            match=\"Input should be a valid integer\",\n        ):\n            await mcp.call_tool(\"my_tool\", {\"x\": \"not an int\"})\n\n    async def test_tool_int_coercion(self):\n        \"\"\"Test that string ints are coerced by default.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add_one(x: int) -> int:\n            return x + 1\n\n        result = await mcp.call_tool(\"add_one\", {\"x\": \"42\"})\n        assert result.structured_content == {\"result\": 43}\n\n    async def test_tool_bool_coercion(self):\n        \"\"\"Test that string bools are coerced by default.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def toggle(flag: bool) -> bool:\n            return not flag\n\n        result = await mcp.call_tool(\"toggle\", {\"flag\": \"true\"})\n        assert result.structured_content == {\"result\": False}\n\n        result = await mcp.call_tool(\"toggle\", {\"flag\": \"false\"})\n        assert result.structured_content == {\"result\": True}\n\n    async def test_annotated_field_validation(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def analyze(x: Annotated[int, Field(ge=1)]) -> None:\n            pass\n\n        with pytest.raises(\n            ValidationError,\n            match=\"Input should be greater than or equal to 1\",\n        ):\n            await mcp.call_tool(\"analyze\", {\"x\": 0})\n\n    async def test_default_field_validation(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def analyze(x: int = Field(ge=1)) -> None:\n            pass\n\n        with pytest.raises(\n            ValidationError,\n            match=\"Input should be greater than or equal to 1\",\n        ):\n            await mcp.call_tool(\"analyze\", {\"x\": 0})\n\n    async def test_default_field_is_still_required_if_no_default_specified(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def analyze(x: int = Field()) -> None:\n            pass\n\n        with pytest.raises(ValidationError, match=\"missing\"):\n            await mcp.call_tool(\"analyze\", {})\n\n    async def test_literal_type_validation_error(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def analyze(x: Literal[\"a\", \"b\"]) -> None:\n            pass\n\n        with pytest.raises(\n            ValidationError,\n            match=\"Input should be 'a' or 'b'\",\n        ):\n            await mcp.call_tool(\"analyze\", {\"x\": \"c\"})\n\n    async def test_literal_type_validation_success(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def analyze(x: Literal[\"a\", \"b\"]) -> str:\n            return x\n\n        result = await mcp.call_tool(\"analyze\", {\"x\": \"a\"})\n        assert result.structured_content == {\"result\": \"a\"}\n\n    async def test_enum_type_validation_error(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        class MyEnum(Enum):\n            RED = \"red\"\n            GREEN = \"green\"\n            BLUE = \"blue\"\n\n        @mcp.tool\n        def analyze(x: MyEnum) -> str:\n            return x.value\n\n        with pytest.raises(\n            ValidationError,\n            match=\"Input should be 'red', 'green' or 'blue'\",\n        ):\n            await mcp.call_tool(\"analyze\", {\"x\": \"some-color\"})\n\n    async def test_enum_type_validation_success(self):\n        mcp = FastMCP()\n\n        class MyEnum(Enum):\n            RED = \"red\"\n            GREEN = \"green\"\n            BLUE = \"blue\"\n\n        @mcp.tool\n        def analyze(x: MyEnum) -> str:\n            return x.value\n\n        result = await mcp.call_tool(\"analyze\", {\"x\": \"red\"})\n        assert result.structured_content == {\"result\": \"red\"}\n\n    async def test_union_type_validation(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def analyze(x: int | float) -> str:\n            return str(x)\n\n        result = await mcp.call_tool(\"analyze\", {\"x\": 1})\n        assert result.structured_content == {\"result\": \"1\"}\n\n        result = await mcp.call_tool(\"analyze\", {\"x\": 1.0})\n        assert result.structured_content == {\"result\": \"1.0\"}\n\n        with pytest.raises(\n            ValidationError,\n            match=\"Input should be a valid\",\n        ):\n            await mcp.call_tool(\"analyze\", {\"x\": \"not a number\"})\n\n    async def test_path_type(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_path(path: Path) -> str:\n            assert isinstance(path, Path)\n            return str(path)\n\n        test_path = Path(\"tmp\") / \"test.txt\"\n\n        result = await mcp.call_tool(\"send_path\", {\"path\": str(test_path)})\n        assert result.structured_content == {\"result\": str(test_path)}\n\n    async def test_path_type_error(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_path(path: Path) -> str:\n            return str(path)\n\n        with pytest.raises(ValidationError, match=\"Input is not a valid path\"):\n            await mcp.call_tool(\"send_path\", {\"path\": 1})\n\n    async def test_uuid_type(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_uuid(x: uuid.UUID) -> str:\n            assert isinstance(x, uuid.UUID)\n            return str(x)\n\n        test_uuid = uuid.uuid4()\n\n        result = await mcp.call_tool(\"send_uuid\", {\"x\": test_uuid})\n        assert result.structured_content == {\"result\": str(test_uuid)}\n\n    async def test_uuid_type_error(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_uuid(x: uuid.UUID) -> str:\n            return str(x)\n\n        with pytest.raises(ValidationError, match=\"Input should be a valid UUID\"):\n            await mcp.call_tool(\"send_uuid\", {\"x\": \"not a uuid\"})\n\n    async def test_datetime_type(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_datetime(x: datetime.datetime) -> str:\n            return x.isoformat()\n\n        dt = datetime.datetime(2025, 4, 25, 1, 2, 3)\n\n        result = await mcp.call_tool(\"send_datetime\", {\"x\": dt})\n        assert result.structured_content == {\"result\": dt.isoformat()}\n\n    async def test_datetime_type_parse_string(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_datetime(x: datetime.datetime) -> str:\n            return x.isoformat()\n\n        result = await mcp.call_tool(\"send_datetime\", {\"x\": \"2021-01-01T00:00:00\"})\n        assert result.structured_content == {\"result\": \"2021-01-01T00:00:00\"}\n\n    async def test_datetime_type_error(self):\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_datetime(x: datetime.datetime) -> str:\n            return x.isoformat()\n\n        with pytest.raises(ValidationError, match=\"Input should be a valid datetime\"):\n            await mcp.call_tool(\"send_datetime\", {\"x\": \"not a datetime\"})\n\n    async def test_date_type(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_date(x: datetime.date) -> str:\n            return x.isoformat()\n\n        result = await mcp.call_tool(\"send_date\", {\"x\": datetime.date.today()})\n        assert result.structured_content == {\n            \"result\": datetime.date.today().isoformat()\n        }\n\n    async def test_date_type_parse_string(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_date(x: datetime.date) -> str:\n            return x.isoformat()\n\n        result = await mcp.call_tool(\"send_date\", {\"x\": \"2021-01-01\"})\n        assert result.structured_content == {\"result\": \"2021-01-01\"}\n\n    async def test_timedelta_type(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_timedelta(x: datetime.timedelta) -> str:\n            return str(x)\n\n        result = await mcp.call_tool(\n            \"send_timedelta\", {\"x\": datetime.timedelta(days=1)}\n        )\n        assert result.structured_content == {\"result\": \"1 day, 0:00:00\"}\n\n    async def test_timedelta_type_parse_int(self):\n        \"\"\"Test that int input is coerced to timedelta (seconds).\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def send_timedelta(x: datetime.timedelta) -> str:\n            return str(x)\n\n        result = await mcp.call_tool(\"send_timedelta\", {\"x\": 1000})\n        assert result.structured_content is not None\n        result_str = result.structured_content[\"result\"]\n        assert (\n            \"0:16:40\" in result_str or \"16:40\" in result_str\n        )  # 1000 seconds = 16 minutes 40 seconds\n\n    async def test_annotated_string_description(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def f(x: Annotated[int, \"A number\"]):\n            return x\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].parameters[\"properties\"][\"x\"][\"description\"] == \"A number\"\n"
  },
  {
    "path": "tests/server/providers/local_provider_tools/test_tags.py",
    "content": "\"\"\"Tests for tool tags.\"\"\"\n\nfrom dataclasses import dataclass\n\nimport pytest\nfrom pydantic import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import NotFoundError\n\n\ndef _normalize_anyof_order(schema):\n    \"\"\"Normalize the order of items in anyOf arrays for consistent comparison.\"\"\"\n    if isinstance(schema, dict):\n        if \"anyOf\" in schema:\n            schema = schema.copy()\n            schema[\"anyOf\"] = sorted(schema[\"anyOf\"], key=str)\n        return {k: _normalize_anyof_order(v) for k, v in schema.items()}\n    elif isinstance(schema, list):\n        return [_normalize_anyof_order(item) for item in schema]\n    return schema\n\n\nclass PersonTypedDict(TypedDict):\n    name: str\n    age: int\n\n\nclass PersonModel(BaseModel):\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    name: str\n    age: int\n\n\nclass TestToolTags:\n    def create_server(self, include_tags=None, exclude_tags=None):\n        mcp = FastMCP()\n\n        @mcp.tool(tags={\"a\", \"b\"})\n        def tool_1() -> int:\n            return 1\n\n        @mcp.tool(tags={\"b\", \"c\"})\n        def tool_2() -> int:\n            return 2\n\n        if include_tags:\n            mcp.enable(tags=include_tags, only=True)\n        if exclude_tags:\n            mcp.disable(tags=exclude_tags)\n\n        return mcp\n\n    async def test_include_tags_all_tools(self):\n        mcp = self.create_server(include_tags={\"a\", \"b\"})\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"tool_1\", \"tool_2\"}\n\n    async def test_include_tags_some_tools(self):\n        mcp = self.create_server(include_tags={\"a\", \"z\"})\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"tool_1\"}\n\n    async def test_exclude_tags_all_tools(self):\n        mcp = self.create_server(exclude_tags={\"a\", \"b\"})\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == set()\n\n    async def test_exclude_tags_some_tools(self):\n        mcp = self.create_server(exclude_tags={\"a\", \"z\"})\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"tool_2\"}\n\n    async def test_exclude_precedence(self):\n        mcp = self.create_server(exclude_tags={\"a\"}, include_tags={\"b\"})\n        tools = await mcp.list_tools()\n        assert {t.name for t in tools} == {\"tool_2\"}\n\n    async def test_call_included_tool(self):\n        mcp = self.create_server(include_tags={\"a\"})\n        result_1 = await mcp.call_tool(\"tool_1\", {})\n        assert result_1.structured_content == {\"result\": 1}\n\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await mcp.call_tool(\"tool_2\", {})\n\n    async def test_call_excluded_tool(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        with pytest.raises(NotFoundError, match=\"Unknown tool\"):\n            await mcp.call_tool(\"tool_1\", {})\n\n        result_2 = await mcp.call_tool(\"tool_2\", {})\n        assert result_2.structured_content == {\"result\": 2}\n"
  },
  {
    "path": "tests/server/providers/openapi/__init__.py",
    "content": "\"\"\"Tests for openapi_new server components.\"\"\"\n"
  },
  {
    "path": "tests/server/providers/openapi/test_comprehensive.py",
    "content": "\"\"\"Comprehensive tests for OpenAPIProvider implementation.\"\"\"\n\nimport json\nfrom unittest.mock import AsyncMock, Mock\n\nimport httpx\nimport pytest\nfrom httpx import Response\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\n\n\ndef create_openapi_server(\n    openapi_spec: dict,\n    client,\n    name: str = \"OpenAPI Server\",\n) -> FastMCP:\n    \"\"\"Helper to create a FastMCP server with OpenAPIProvider.\"\"\"\n    provider = OpenAPIProvider(openapi_spec=openapi_spec, client=client)\n    mcp = FastMCP(name)\n    mcp.add_provider(provider)\n    return mcp\n\n\nclass TestOpenAPIComprehensive:\n    \"\"\"Comprehensive tests ensuring no functionality is lost.\"\"\"\n\n    @pytest.fixture\n    def comprehensive_openapi_spec(self):\n        \"\"\"Comprehensive OpenAPI spec covering all major features.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Comprehensive API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"components\": {\n                \"schemas\": {\n                    \"User\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"id\": {\"type\": \"integer\"},\n                            \"name\": {\"type\": \"string\"},\n                            \"email\": {\"type\": \"string\", \"format\": \"email\"},\n                            \"age\": {\"type\": \"integer\", \"minimum\": 0},\n                        },\n                        \"required\": [\"name\", \"email\"],\n                    },\n                    \"Error\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"code\": {\"type\": \"integer\"},\n                            \"message\": {\"type\": \"string\"},\n                        },\n                    },\n                },\n                \"parameters\": {\n                    \"UserId\": {\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": True,\n                        \"schema\": {\"type\": \"integer\"},\n                        \"description\": \"User identifier\",\n                    }\n                },\n            },\n            \"paths\": {\n                # Basic CRUD operations\n                \"/users\": {\n                    \"get\": {\n                        \"operationId\": \"list_users\",\n                        \"summary\": \"List all users\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"limit\",\n                                \"in\": \"query\",\n                                \"schema\": {\n                                    \"type\": \"integer\",\n                                    \"default\": 10,\n                                    \"minimum\": 1,\n                                    \"maximum\": 100,\n                                },\n                                \"description\": \"Number of users to return\",\n                            },\n                            {\n                                \"name\": \"offset\",\n                                \"in\": \"query\",\n                                \"schema\": {\n                                    \"type\": \"integer\",\n                                    \"default\": 0,\n                                    \"minimum\": 0,\n                                },\n                                \"description\": \"Number of users to skip\",\n                            },\n                            {\n                                \"name\": \"sort\",\n                                \"in\": \"query\",\n                                \"schema\": {\n                                    \"type\": \"string\",\n                                    \"enum\": [\"name\", \"email\", \"age\"],\n                                },\n                                \"description\": \"Sort field\",\n                            },\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"List of users\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/components/schemas/User\"\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    },\n                    \"post\": {\n                        \"operationId\": \"create_user\",\n                        \"summary\": \"Create a new user\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\"$ref\": \"#/components/schemas/User\"}\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"201\": {\n                                \"description\": \"User created\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/User\"}\n                                    }\n                                },\n                            },\n                            \"400\": {\n                                \"description\": \"Invalid input\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/Error\"}\n                                    }\n                                },\n                            },\n                        },\n                    },\n                },\n                \"/users/{id}\": {\n                    \"parameters\": [{\"$ref\": \"#/components/parameters/UserId\"}],\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get user by ID\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User details\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/User\"}\n                                    }\n                                },\n                            },\n                            \"404\": {\n                                \"description\": \"User not found\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/Error\"}\n                                    }\n                                },\n                            },\n                        },\n                    },\n                    \"put\": {\n                        \"operationId\": \"update_user\",\n                        \"summary\": \"Update user\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\"$ref\": \"#/components/schemas/User\"}\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User updated\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/User\"}\n                                    }\n                                },\n                            },\n                        },\n                    },\n                    \"delete\": {\n                        \"operationId\": \"delete_user\",\n                        \"summary\": \"Delete user\",\n                        \"responses\": {\n                            \"204\": {\"description\": \"User deleted\"},\n                            \"404\": {\n                                \"description\": \"User not found\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/Error\"}\n                                    }\n                                },\n                            },\n                        },\n                    },\n                },\n                # Complex parameter scenarios\n                \"/search\": {\n                    \"get\": {\n                        \"operationId\": \"search_users\",\n                        \"summary\": \"Search users with complex filters\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"q\",\n                                \"in\": \"query\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"Search query\",\n                            },\n                            {\n                                \"name\": \"filter\",\n                                \"in\": \"query\",\n                                \"style\": \"deepObject\",\n                                \"explode\": True,\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"age\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"min\": {\"type\": \"integer\"},\n                                                \"max\": {\"type\": \"integer\"},\n                                            },\n                                        },\n                                        \"name\": {\"type\": \"string\"},\n                                        \"active\": {\"type\": \"boolean\"},\n                                    },\n                                },\n                            },\n                            {\n                                \"name\": \"X-Request-ID\",\n                                \"in\": \"header\",\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"Request identifier for tracing\",\n                            },\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Search results\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"results\": {\n                                                    \"type\": \"array\",\n                                                    \"items\": {\n                                                        \"$ref\": \"#/components/schemas/User\"\n                                                    },\n                                                },\n                                                \"total\": {\"type\": \"integer\"},\n                                                \"page\": {\"type\": \"integer\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                # Parameter collision scenario\n                \"/collision/{id}\": {\n                    \"patch\": {\n                        \"operationId\": \"collision_test\",\n                        \"summary\": \"Test parameter collision handling\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"Resource ID\",\n                            },\n                            {\n                                \"name\": \"version\",\n                                \"in\": \"query\",\n                                \"schema\": {\"type\": \"integer\", \"default\": 1},\n                            },\n                            {\n                                \"name\": \"version\",\n                                \"in\": \"header\",\n                                \"schema\": {\"type\": \"string\"},\n                            },\n                        ],\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"id\": {\n                                                \"type\": \"integer\",\n                                                \"description\": \"Internal ID\",\n                                            },\n                                            \"version\": {\n                                                \"type\": \"string\",\n                                                \"description\": \"Data version\",\n                                            },\n                                            \"data\": {\"type\": \"object\"},\n                                        },\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\"200\": {\"description\": \"Updated\"}},\n                    }\n                },\n            },\n        }\n\n    @pytest.fixture\n    def openapi_31_spec(self):\n        \"\"\"OpenAPI 3.1 spec to test compatibility.\"\"\"\n        return {\n            \"openapi\": \"3.1.0\",\n            \"info\": {\"title\": \"OpenAPI 3.1 Test\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/items/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_item_31\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Item details\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"string\"},\n                                                \"name\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n\n    async def test_comprehensive_server_initialization(\n        self, comprehensive_openapi_spec\n    ):\n        \"\"\"Test server initialization with comprehensive spec.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=comprehensive_openapi_spec,\n                client=client,\n            )\n            server = FastMCP(\"Comprehensive Test Server\")\n            server.add_provider(provider)\n\n            # Should initialize successfully\n            assert server.name == \"Comprehensive Test Server\"\n            assert hasattr(provider, \"_director\")\n            assert hasattr(provider, \"_spec\")\n\n            # Test with in-memory client\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Should have created tools for all operations\n                tool_names = {tool.name for tool in tools}\n                expected_operations = {\n                    \"list_users\",\n                    \"create_user\",\n                    \"get_user\",\n                    \"update_user\",\n                    \"delete_user\",\n                    \"search_users\",\n                    \"collision_test\",\n                }\n\n                assert tool_names == expected_operations\n\n    async def test_openapi_31_compatibility(self, openapi_31_spec):\n        \"\"\"Test that OpenAPI 3.1 specs work correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=openapi_31_spec,\n                client=client,\n                name=\"OpenAPI 3.1 Test\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                assert len(tools) == 1\n                tool = tools[0]\n                assert tool.name == \"get_item_31\"\n\n    async def test_parameter_collision_handling(self, comprehensive_openapi_spec):\n        \"\"\"Test that parameter collisions are handled correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=comprehensive_openapi_spec,\n                client=client,\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                collision_tool = next(\n                    tool for tool in tools if tool.name == \"collision_test\"\n                )\n                schema = collision_tool.inputSchema\n                properties = schema[\"properties\"]\n\n                # Should have unique parameter names for colliding parameters\n                param_names = list(properties.keys())\n\n                # Should have some form of id parameters (path and body)\n                id_params = [name for name in param_names if \"id\" in name]\n                assert len(id_params) >= 2\n\n                # Should have some form of version parameters (query, header, body)\n                version_params = [name for name in param_names if \"version\" in name]\n                assert len(version_params) >= 3\n\n                # Should have other parameters\n                assert \"data\" in param_names\n\n    async def test_deep_object_parameters(self, comprehensive_openapi_spec):\n        \"\"\"Test deepObject parameter handling.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=comprehensive_openapi_spec,\n                client=client,\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                search_tool = next(\n                    tool for tool in tools if tool.name == \"search_users\"\n                )\n                schema = search_tool.inputSchema\n                properties = schema[\"properties\"]\n\n                # Should have flattened deepObject parameters\n                # The exact flattening depends on implementation\n                assert \"q\" in properties  # Regular query parameter\n\n                # Should have some form of filter parameters\n                filter_params = [name for name in properties.keys() if \"filter\" in name]\n                assert len(filter_params) > 0\n\n    async def test_request_building_and_execution(self, comprehensive_openapi_spec):\n        \"\"\"Test that requests are built and executed correctly.\"\"\"\n        # Create a mock client that tracks requests\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        # Mock successful response\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 200\n        mock_response.json.return_value = {\n            \"id\": 123,\n            \"name\": \"Test User\",\n            \"email\": \"test@example.com\",\n        }\n        mock_response.text = json.dumps(\n            {\"id\": 123, \"name\": \"Test User\", \"email\": \"test@example.com\"}\n        )\n        mock_response.raise_for_status = Mock()\n\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_openapi_spec,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            # Test GET request with path parameter\n            await mcp_client.call_tool(\"get_user\", {\"id\": 123})\n\n            # Should have made a request\n            mock_client.send.assert_called_once()\n            request = mock_client.send.call_args[0][0]\n\n            # Verify request details\n            assert request.method == \"GET\"\n            assert \"123\" in str(request.url)\n            assert \"users/123\" in str(request.url)\n\n    async def test_request_uses_localhost_fallback_when_no_base_url(\n        self, comprehensive_openapi_spec\n    ):\n        \"\"\"Test that tool uses localhost fallback when client has no base_url.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = httpx.URL(\"\")  # Empty URL, same as httpx default\n        mock_client.headers = None\n\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 200\n        mock_response.json.return_value = {\n            \"id\": 123,\n            \"name\": \"Test User\",\n            \"email\": \"test@example.com\",\n        }\n        mock_response.raise_for_status = Mock()\n\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_openapi_spec,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            await mcp_client.call_tool(\"get_user\", {\"id\": 123})\n\n        # Verify request was made to localhost fallback\n        mock_client.send.assert_called_once()\n        request = mock_client.send.call_args[0][0]\n        assert str(request.url).startswith(\"http://localhost\")\n\n    async def test_complex_request_with_body_and_parameters(\n        self, comprehensive_openapi_spec\n    ):\n        \"\"\"Test complex request with both parameters and body.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 201\n        mock_response.json.return_value = {\n            \"id\": 456,\n            \"name\": \"New User\",\n            \"email\": \"new@example.com\",\n        }\n        mock_response.raise_for_status = Mock()\n\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_openapi_spec,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            # Test POST request with body\n            await mcp_client.call_tool(\n                \"create_user\",\n                {\n                    \"name\": \"New User\",\n                    \"email\": \"new@example.com\",\n                    \"age\": 25,\n                },\n            )\n\n            # Should have made a request\n            mock_client.send.assert_called_once()\n            request = mock_client.send.call_args[0][0]\n\n            # Verify request details\n            assert request.method == \"POST\"\n            assert \"users\" in str(request.url)\n\n            # Should have JSON body\n            assert request.content is not None\n            body_data = json.loads(request.content)\n            assert body_data[\"name\"] == \"New User\"\n            assert body_data[\"email\"] == \"new@example.com\"\n            assert body_data[\"age\"] == 25\n\n    async def test_query_parameters(self, comprehensive_openapi_spec):\n        \"\"\"Test query parameter handling.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 200\n        mock_response.json.return_value = []\n        mock_response.raise_for_status = Mock()\n\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_openapi_spec,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            # Test GET request with query parameters\n            await mcp_client.call_tool(\n                \"list_users\",\n                {\n                    \"limit\": 20,\n                    \"offset\": 10,\n                    \"sort\": \"name\",\n                },\n            )\n\n            mock_client.send.assert_called_once()\n            request = mock_client.send.call_args[0][0]\n\n            # Verify query parameters in URL\n            url_str = str(request.url)\n            assert \"limit=20\" in url_str\n            assert \"offset=10\" in url_str\n            assert \"sort=name\" in url_str\n\n    async def test_error_handling(self, comprehensive_openapi_spec):\n        \"\"\"Test error handling for HTTP errors.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        # Mock HTTP error response\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 404\n        mock_response.reason_phrase = \"Not Found\"\n        mock_response.json.return_value = {\"code\": 404, \"message\": \"User not found\"}\n        mock_response.text = json.dumps({\"code\": 404, \"message\": \"User not found\"})\n\n        # Configure raise_for_status to raise HTTPStatusError\n        def raise_for_status():\n            raise httpx.HTTPStatusError(\n                \"404 Not Found\", request=Mock(), response=mock_response\n            )\n\n        mock_response.raise_for_status = raise_for_status\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_openapi_spec,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            # Should handle HTTP errors gracefully\n            with pytest.raises(Exception) as exc_info:\n                await mcp_client.call_tool(\"get_user\", {\"id\": 999})\n\n            # Error should be wrapped appropriately\n            error_message = str(exc_info.value)\n            assert \"404\" in error_message\n\n    async def test_schema_refs_resolution(self, comprehensive_openapi_spec):\n        \"\"\"Test that schema references are resolved correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=comprehensive_openapi_spec,\n                client=client,\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find create_user tool which uses schema refs\n                create_tool = next(tool for tool in tools if tool.name == \"create_user\")\n                schema = create_tool.inputSchema\n                properties = schema[\"properties\"]\n\n                # Should have resolved User schema properties\n                assert \"name\" in properties\n                assert \"email\" in properties\n                # May also have id and age depending on implementation\n\n    async def test_optional_vs_required_parameters(self, comprehensive_openapi_spec):\n        \"\"\"Test handling of optional vs required parameters.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=comprehensive_openapi_spec,\n                client=client,\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Check list_users tool - has optional query parameters\n                list_tool = next(tool for tool in tools if tool.name == \"list_users\")\n                schema = list_tool.inputSchema\n                # Query parameters should be optional\n                # (may not appear in required list)\n                # This test just ensures the schema is well-formed\n                assert \"properties\" in schema\n\n                # Check search_users tool - has required query parameter\n                search_tool = next(\n                    tool for tool in tools if tool.name == \"search_users\"\n                )\n                search_schema = search_tool.inputSchema\n                # Should have some required parameters\n                assert len(search_schema[\"properties\"]) > 0\n\n    async def test_server_performance_no_latency(self, comprehensive_openapi_spec):\n        \"\"\"Test that server initialization is fast (no code generation latency).\"\"\"\n        import time\n\n        # Time the provider creation\n        start_time = time.time()\n\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=comprehensive_openapi_spec,\n                client=client,\n            )\n\n        end_time = time.time()\n\n        # Should be fast (no code generation). Use generous threshold for slow\n        # CI runners (Windows); the intent is to catch obvious regressions\n        # (e.g., accidentally re-introducing code generation), not to benchmark.\n        initialization_time = end_time - start_time\n        assert initialization_time < 1.0\n\n        # Verify provider was created correctly\n        assert provider is not None\n        assert hasattr(provider, \"_director\")\n        assert hasattr(provider, \"_spec\")\n\n    async def test_timeout_error_produces_useful_message(\n        self, comprehensive_openapi_spec\n    ):\n        \"\"\"ReadTimeout should surface a clear error, not an empty string.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        # httpx internally raises ReadTimeout with an empty message\n        mock_client.send = AsyncMock(side_effect=httpx.ReadTimeout(\"\"))\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_openapi_spec,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            with pytest.raises(Exception) as exc_info:\n                await mcp_client.call_tool(\"get_user\", {\"id\": 1})\n\n            error_message = str(exc_info.value)\n            assert \"timed out\" in error_message\n            assert \"ReadTimeout\" in error_message\n\n\nclass TestOpenAPIPostEdgeCases:\n    \"\"\"Tests for POST request edge cases that could cause unhandled errors.\"\"\"\n\n    @pytest.fixture\n    def post_spec_with_empty_content_schema(self):\n        \"\"\"OpenAPI spec where a POST endpoint has an empty content_schema.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/items\": {\n                    \"post\": {\n                        \"operationId\": \"create_item\",\n                        \"summary\": \"Create an item\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\"type\": \"string\"},\n                                            \"value\": {\"type\": \"integer\"},\n                                        },\n                                        \"required\": [\"name\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"201\": {\n                                \"description\": \"Created\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                \"/items/{item_id}\": {\n                    \"post\": {\n                        \"operationId\": \"update_item\",\n                        \"summary\": \"Update an item\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"item_id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\"type\": \"string\"},\n                                            \"value\": {\"type\": \"integer\"},\n                                        },\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Updated\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n            },\n        }\n\n    async def test_post_with_body_params(self, post_spec_with_empty_content_schema):\n        \"\"\"POST with body parameters should build the request correctly.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 201\n        mock_response.json.return_value = {\"id\": 1, \"name\": \"Test\"}\n        mock_response.raise_for_status = Mock()\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=post_spec_with_empty_content_schema,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            result = await mcp_client.call_tool(\n                \"create_item\", {\"name\": \"Test\", \"value\": 42}\n            )\n\n        mock_client.send.assert_called_once()\n        request = mock_client.send.call_args[0][0]\n        assert request.method == \"POST\"\n        body_data = json.loads(request.content)\n        assert body_data[\"name\"] == \"Test\"\n        assert body_data[\"value\"] == 42\n        assert result is not None\n\n    async def test_post_with_path_params_and_body(\n        self, post_spec_with_empty_content_schema\n    ):\n        \"\"\"POST with both path parameters and body should route args correctly.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 200\n        mock_response.json.return_value = {\"id\": 5, \"name\": \"Updated\"}\n        mock_response.raise_for_status = Mock()\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        server = create_openapi_server(\n            openapi_spec=post_spec_with_empty_content_schema,\n            client=mock_client,\n        )\n\n        async with Client(server) as mcp_client:\n            result = await mcp_client.call_tool(\n                \"update_item\",\n                {\"item_id\": 5, \"name\": \"Updated\", \"value\": 99},\n            )\n\n        mock_client.send.assert_called_once()\n        request = mock_client.send.call_args[0][0]\n        assert request.method == \"POST\"\n        assert \"/items/5\" in str(request.url)\n        body_data = json.loads(request.content)\n        assert body_data[\"name\"] == \"Updated\"\n        assert body_data[\"value\"] == 99\n        assert \"item_id\" not in body_data\n        assert result is not None\n\n    async def test_unexpected_error_in_request_building_gives_useful_message(self):\n        \"\"\"Unexpected exceptions during request building should produce useful errors.\"\"\"\n        from fastmcp.server.providers.openapi.components import OpenAPITool\n        from fastmcp.utilities.openapi.director import RequestDirector\n        from fastmcp.utilities.openapi.models import HTTPRoute\n\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"POST\",\n            operation_id=\"test_op\",\n            parameters=[],\n            responses={},\n            response_schemas={},\n        )\n\n        mock_director = Mock(spec=RequestDirector)\n        mock_director.build.side_effect = KeyError(\"missing_param\")\n\n        tool = OpenAPITool(\n            client=mock_client,\n            route=route,\n            director=mock_director,\n            name=\"test_tool\",\n            description=\"test\",\n            parameters={},\n        )\n\n        with pytest.raises(ValueError, match=\"Error building request for POST /test\"):\n            await tool.run({\"some_arg\": \"value\"})\n"
  },
  {
    "path": "tests/server/providers/openapi/test_deepobject_style.py",
    "content": "\"\"\"Tests for deepObject style parameter handling in OpenAPIProvider.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\n\n\ndef create_openapi_server(\n    openapi_spec: dict,\n    client,\n    name: str = \"OpenAPI Server\",\n) -> FastMCP:\n    \"\"\"Helper to create a FastMCP server with OpenAPIProvider.\"\"\"\n    provider = OpenAPIProvider(openapi_spec=openapi_spec, client=client)\n    mcp = FastMCP(name)\n    mcp.add_provider(provider)\n    return mcp\n\n\nclass TestDeepObjectStyle:\n    \"\"\"Test deepObject style parameter handling in openapi_new.\"\"\"\n\n    @pytest.fixture\n    def deepobject_spec(self):\n        \"\"\"OpenAPI spec with deepObject style parameters.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"DeepObject Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/surveys\": {\n                    \"get\": {\n                        \"operationId\": \"get_surveys\",\n                        \"summary\": \"Get surveys with deepObject filtering\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"target\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"style\": \"deepObject\",\n                                \"explode\": True,\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"id\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"Target ID\",\n                                        },\n                                        \"type\": {\n                                            \"type\": \"string\",\n                                            \"enum\": [\"location\", \"organisation\"],\n                                            \"description\": \"Target type\",\n                                        },\n                                    },\n                                    \"required\": [\"type\", \"id\"],\n                                },\n                                \"description\": \"Target object for filtering\",\n                            },\n                            {\n                                \"name\": \"filters\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"style\": \"deepObject\",\n                                \"explode\": True,\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"status\": {\"type\": \"string\"},\n                                        \"category\": {\"type\": \"string\"},\n                                        \"priority\": {\"type\": \"integer\"},\n                                    },\n                                },\n                                \"description\": \"Additional filters\",\n                            },\n                            {\n                                \"name\": \"compact\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"style\": \"deepObject\",\n                                \"explode\": False,\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"format\": {\"type\": \"string\"},\n                                        \"level\": {\"type\": \"integer\"},\n                                    },\n                                },\n                                \"description\": \"Compact format options (explode=false)\",\n                            },\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Survey list\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"surveys\": {\n                                                    \"type\": \"array\",\n                                                    \"items\": {\"type\": \"object\"},\n                                                },\n                                                \"total\": {\"type\": \"integer\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                \"/users/{id}/preferences\": {\n                    \"patch\": {\n                        \"operationId\": \"update_preferences\",\n                        \"summary\": \"Update user preferences with deepObject in body\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"preferences\": {\n                                                \"type\": \"object\",\n                                                \"properties\": {\n                                                    \"theme\": {\"type\": \"string\"},\n                                                    \"notifications\": {\n                                                        \"type\": \"object\",\n                                                        \"properties\": {\n                                                            \"email\": {\n                                                                \"type\": \"boolean\"\n                                                            },\n                                                            \"push\": {\"type\": \"boolean\"},\n                                                            \"frequency\": {\n                                                                \"type\": \"string\"\n                                                            },\n                                                        },\n                                                    },\n                                                    \"privacy\": {\n                                                        \"type\": \"object\",\n                                                        \"properties\": {\n                                                            \"profile_visible\": {\n                                                                \"type\": \"boolean\"\n                                                            },\n                                                            \"analytics\": {\n                                                                \"type\": \"boolean\"\n                                                            },\n                                                        },\n                                                    },\n                                                },\n                                                \"description\": \"Nested preference object\",\n                                            }\n                                        },\n                                        \"required\": [\"preferences\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Preferences updated\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"success\": {\"type\": \"boolean\"}\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n            },\n        }\n\n    async def test_deepobject_style_parsing_from_spec(self, deepobject_spec):\n        \"\"\"Test that deepObject style parameters are correctly parsed from OpenAPI spec.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=deepobject_spec,\n                client=client,\n                name=\"DeepObject Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the surveys tool\n                surveys_tool = next(\n                    tool for tool in tools if tool.name == \"get_surveys\"\n                )\n                assert surveys_tool is not None\n\n                # Check that deepObject parameters are included in schema\n                params = surveys_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Should have the deepObject parameters\n                assert \"target\" in properties\n                assert \"filters\" in properties\n                assert \"compact\" in properties\n\n                # Check that target parameter is present\n                # (Exact schema structure may vary based on implementation)\n                target_param = properties[\"target\"]\n                # Should have some structure, exact format may vary\n                assert target_param is not None\n\n    async def test_deepobject_explode_true_handling(self, deepobject_spec):\n        \"\"\"Test deepObject with explode=true parameter handling.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=deepobject_spec,\n                client=client,\n                name=\"DeepObject Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n                surveys_tool = next(\n                    tool for tool in tools if tool.name == \"get_surveys\"\n                )\n\n                # Check that explode=true parameters are properly structured\n                params = surveys_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Target parameter with explode=true should allow individual property access\n                target_properties = properties[\"target\"][\"properties\"]\n                assert \"id\" in target_properties\n                assert \"type\" in target_properties\n                assert target_properties[\"type\"][\"enum\"] == [\"location\", \"organisation\"]\n\n    async def test_deepobject_explode_false_handling(self, deepobject_spec):\n        \"\"\"Test deepObject with explode=false parameter handling.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=deepobject_spec,\n                client=client,\n                name=\"DeepObject Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n                surveys_tool = next(\n                    tool for tool in tools if tool.name == \"get_surveys\"\n                )\n\n                # Check that explode=false parameters are handled\n                params = surveys_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Compact parameter with explode=false should still be present and valid\n                assert \"compact\" in properties\n                compact_param = properties[\"compact\"]\n                # Check that it's a valid parameter (exact structure may vary)\n                assert compact_param is not None\n                # If it has a type, it should be object\n                if \"type\" in compact_param:\n                    assert compact_param[\"type\"] == \"object\"\n\n    async def test_nested_object_structure_in_request_body(self, deepobject_spec):\n        \"\"\"Test nested object structures in request body are preserved.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=deepobject_spec,\n                client=client,\n                name=\"DeepObject Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the preferences tool\n                prefs_tool = next(\n                    tool for tool in tools if tool.name == \"update_preferences\"\n                )\n                assert prefs_tool is not None\n\n                # Check that nested object structure is preserved\n                params = prefs_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Should have path parameter\n                assert \"id\" in properties\n\n                # Should have preferences object\n                assert \"preferences\" in properties\n                prefs_param = properties[\"preferences\"]\n                assert prefs_param[\"type\"] == \"object\"\n\n                # Check nested structure\n                prefs_props = prefs_param[\"properties\"]\n                assert \"theme\" in prefs_props\n                assert \"notifications\" in prefs_props\n                assert \"privacy\" in prefs_props\n\n                # Check deeply nested objects\n                notifications = prefs_props[\"notifications\"]\n                assert notifications[\"type\"] == \"object\"\n                notif_props = notifications[\"properties\"]\n                assert \"email\" in notif_props\n                assert \"push\" in notif_props\n                assert \"frequency\" in notif_props\n\n    async def test_deepobject_tool_functionality(self, deepobject_spec):\n        \"\"\"Test that tools with deepObject parameters maintain basic functionality.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=deepobject_spec,\n                client=client,\n                name=\"DeepObject Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Should successfully create tools with deepObject parameters\n                assert len(tools) == 2\n\n                tool_names = {tool.name for tool in tools}\n                assert \"get_surveys\" in tool_names\n                assert \"update_preferences\" in tool_names\n\n                # All tools should have valid schemas\n                for tool in tools:\n                    assert tool.inputSchema is not None\n                    assert tool.inputSchema[\"type\"] == \"object\"\n                    assert \"properties\" in tool.inputSchema\n\n                    # Should have some properties\n                    assert len(tool.inputSchema[\"properties\"]) > 0\n"
  },
  {
    "path": "tests/server/providers/openapi/test_end_to_end_compatibility.py",
    "content": "\"\"\"End-to-end tests for OpenAPIProvider implementation.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\n\n\ndef create_openapi_server(\n    openapi_spec: dict,\n    client,\n    name: str = \"OpenAPI Server\",\n) -> FastMCP:\n    \"\"\"Helper to create a FastMCP server with OpenAPIProvider.\"\"\"\n    provider = OpenAPIProvider(openapi_spec=openapi_spec, client=client)\n    mcp = FastMCP(name)\n    mcp.add_provider(provider)\n    return mcp\n\n\nclass TestEndToEndFunctionality:\n    \"\"\"Test end-to-end functionality of OpenAPIProvider.\"\"\"\n\n    @pytest.fixture\n    def simple_spec(self):\n        \"\"\"Simple OpenAPI spec for testing.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get user by ID\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            },\n                            {\n                                \"name\": \"include_details\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"schema\": {\"type\": \"boolean\"},\n                            },\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"User found\"}},\n                    }\n                }\n            },\n        }\n\n    @pytest.fixture\n    def collision_spec(self):\n        \"\"\"OpenAPI spec with parameter collisions.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Collision API\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"put\": {\n                        \"operationId\": \"update_user\",\n                        \"summary\": \"Update user\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"id\": {\"type\": \"integer\"},\n                                            \"name\": {\"type\": \"string\"},\n                                        },\n                                        \"required\": [\"name\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\"200\": {\"description\": \"User updated\"}},\n                    }\n                }\n            },\n        }\n\n    async def test_tool_schema_generation(self, simple_spec):\n        \"\"\"Test that tools have correct input schemas.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=simple_spec,\n                client=client,\n                name=\"Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Should have one tool\n                assert len(tools) == 1\n\n                tool = tools[0]\n                assert tool.name == \"get_user\"\n                assert tool.description\n\n                # Check schema structure\n                schema = tool.inputSchema\n                assert schema[\"type\"] == \"object\"\n\n                properties = schema.get(\"properties\", {})\n                assert \"id\" in properties\n                assert \"include_details\" in properties\n\n                # Required fields should include path parameter\n                required = schema.get(\"required\", [])\n                assert \"id\" in required\n\n    async def test_collision_handling(self, collision_spec):\n        \"\"\"Test that parameter collision handling works correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=collision_spec,\n                client=client,\n                name=\"Collision Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Should have one tool\n                assert len(tools) == 1\n\n                tool = tools[0]\n                schema = tool.inputSchema\n\n                # Both should have collision-resolved parameters\n                properties = schema.get(\"properties\", {})\n\n                # Should have: id__path (path param), id (body param), name (body param)\n                expected_props = {\"id__path\", \"id\", \"name\"}\n                assert set(properties.keys()) == expected_props\n\n                # Required should include path param and required body params\n                required = set(schema.get(\"required\", []))\n                assert \"id__path\" in required\n                assert \"name\" in required\n\n                # Path parameter should have integer type\n                assert properties[\"id__path\"][\"type\"] == \"integer\"\n\n                # Body parameters should match\n                assert properties[\"id\"][\"type\"] == \"integer\"\n                assert properties[\"name\"][\"type\"] == \"string\"\n\n    async def test_tool_execution_parameter_mapping(self, collision_spec):\n        \"\"\"Test that tool execution with collisions works correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=collision_spec,\n                client=client,\n                name=\"Test Server\",\n            )\n\n            # Test arguments that should work with collision resolution\n            test_args = {\n                \"id__path\": 123,  # Path parameter (suffixed)\n                \"id\": 456,  # Body parameter (not suffixed)\n                \"name\": \"John Doe\",  # Body parameter\n            }\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n                tool_name = tools[0].name\n\n                # Should fail at HTTP level (not argument validation)\n                # since we don't have an actual server\n                with pytest.raises(Exception) as exc_info:\n                    await mcp_client.call_tool(tool_name, test_args)\n\n                # Should fail at HTTP level, not schema validation\n                error_msg = str(exc_info.value).lower()\n                assert \"schema\" not in error_msg\n                assert \"validation\" not in error_msg\n\n    async def test_optional_parameter_handling(self, simple_spec):\n        \"\"\"Test that optional parameters are handled correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=simple_spec,\n                client=client,\n                name=\"Test Server\",\n            )\n\n            # Test with optional parameter omitted\n            test_args_minimal = {\"id\": 123}\n\n            # Test with optional parameter included\n            test_args_full = {\"id\": 123, \"include_details\": True}\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n                tool_name = tools[0].name\n\n                # Both should fail at HTTP level (not argument validation)\n                for test_args in [test_args_minimal, test_args_full]:\n                    with pytest.raises(Exception) as exc_info:\n                        await mcp_client.call_tool(tool_name, test_args)\n\n                    error_msg = str(exc_info.value).lower()\n                    assert \"schema\" not in error_msg\n                    assert \"validation\" not in error_msg\n"
  },
  {
    "path": "tests/server/providers/openapi/test_openapi_features.py",
    "content": "\"\"\"Tests for OpenAPI feature support in OpenAPIProvider.\"\"\"\n\nfrom unittest.mock import AsyncMock, Mock\n\nimport httpx\nimport pytest\nfrom httpx import Response\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\nfrom fastmcp.server.providers.openapi.components import (\n    _extract_mime_type_from_route,\n    _redact_headers,\n)\nfrom fastmcp.server.providers.openapi.routing import MCPType, RouteMap\nfrom fastmcp.utilities.openapi.models import HTTPRoute, ResponseInfo\n\n\ndef create_openapi_server(\n    openapi_spec: dict,\n    client,\n    name: str = \"OpenAPI Server\",\n) -> FastMCP:\n    \"\"\"Helper to create a FastMCP server with OpenAPIProvider.\"\"\"\n    provider = OpenAPIProvider(openapi_spec=openapi_spec, client=client)\n    mcp = FastMCP(name)\n    mcp.add_provider(provider)\n    return mcp\n\n\nclass TestParameterHandling:\n    \"\"\"Test OpenAPI parameter handling features.\"\"\"\n\n    @pytest.fixture\n    def parameter_spec(self):\n        \"\"\"OpenAPI spec with various parameter types.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Parameter Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/search\": {\n                    \"get\": {\n                        \"operationId\": \"search_items\",\n                        \"summary\": \"Search items\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"query\",\n                                \"in\": \"query\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"Search query\",\n                            },\n                            {\n                                \"name\": \"limit\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"schema\": {\n                                    \"type\": \"integer\",\n                                    \"minimum\": 1,\n                                    \"maximum\": 100,\n                                },\n                                \"description\": \"Maximum number of results\",\n                            },\n                            {\n                                \"name\": \"tags\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"schema\": {\n                                    \"type\": \"array\",\n                                    \"items\": {\"type\": \"string\"},\n                                },\n                                \"style\": \"form\",\n                                \"explode\": True,\n                                \"description\": \"Filter by tags\",\n                            },\n                            {\n                                \"name\": \"X-API-Key\",\n                                \"in\": \"header\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"API key for authentication\",\n                            },\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Search results\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"items\": {\n                                                    \"type\": \"array\",\n                                                    \"items\": {\"type\": \"object\"},\n                                                },\n                                                \"total\": {\"type\": \"integer\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                \"/users/{id}/posts/{post_id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user_post\",\n                        \"summary\": \"Get specific user post\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                                \"description\": \"User ID\",\n                            },\n                            {\n                                \"name\": \"post_id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                                \"description\": \"Post ID\",\n                            },\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User post\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"title\": {\"type\": \"string\"},\n                                                \"content\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n            },\n        }\n\n    async def test_query_parameters_in_tools(self, parameter_spec):\n        \"\"\"Test that query parameters are properly included in tool parameters.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=parameter_spec, client=client, name=\"Parameter Test Server\"\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the search tool\n                search_tool = next(\n                    tool for tool in tools if tool.name == \"search_items\"\n                )\n                assert search_tool is not None\n\n                # Check that parameters are included in the tool's input schema\n                params = search_tool.inputSchema\n                assert params[\"type\"] == \"object\"\n\n                properties = params[\"properties\"]\n\n                # Check that key parameters are present\n                # (Schema details may vary based on implementation)\n                assert \"query\" in properties\n                assert \"limit\" in properties\n                assert \"tags\" in properties\n                assert \"X-API-Key\" in properties\n\n                # Check that parameter descriptions are included\n                assert \"description\" in properties[\"query\"], (\n                    \"Query parameter should have description\"\n                )\n                assert properties[\"query\"][\"description\"] == \"Search query\"\n                assert \"description\" in properties[\"limit\"], (\n                    \"Limit parameter should have description\"\n                )\n                assert properties[\"limit\"][\"description\"] == \"Maximum number of results\"\n                assert \"description\" in properties[\"tags\"], (\n                    \"Tags parameter should have description\"\n                )\n                assert properties[\"tags\"][\"description\"] == \"Filter by tags\"\n\n                # Check that required parameters are marked as required\n                required = params.get(\"required\", [])\n                assert \"query\" in required\n                assert \"X-API-Key\" in required\n\n    async def test_path_parameters_in_tools(self, parameter_spec):\n        \"\"\"Test that path parameters are properly included in tool parameters.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=parameter_spec, client=client, name=\"Parameter Test Server\"\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the user post tool\n                user_post_tool = next(\n                    tool for tool in tools if tool.name == \"get_user_post\"\n                )\n                assert user_post_tool is not None\n\n                # Check that path parameters are included\n                params = user_post_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Check that path parameters are present\n                assert \"id\" in properties\n                assert \"post_id\" in properties\n\n                # Path parameters should be required\n                required = params.get(\"required\", [])\n                assert \"id\" in required\n                assert \"post_id\" in required\n\n\nclass TestRequestBodyHandling:\n    \"\"\"Test OpenAPI request body handling.\"\"\"\n\n    @pytest.fixture\n    def request_body_spec(self):\n        \"\"\"OpenAPI spec with request body.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Request Body Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users\": {\n                    \"post\": {\n                        \"operationId\": \"create_user\",\n                        \"summary\": \"Create a user\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\n                                                \"type\": \"string\",\n                                                \"description\": \"User's full name\",\n                                            },\n                                            \"email\": {\n                                                \"type\": \"string\",\n                                                \"format\": \"email\",\n                                                \"description\": \"User's email address\",\n                                            },\n                                            \"age\": {\n                                                \"type\": \"integer\",\n                                                \"minimum\": 0,\n                                                \"maximum\": 150,\n                                                \"description\": \"User's age\",\n                                            },\n                                            \"preferences\": {\n                                                \"type\": \"object\",\n                                                \"properties\": {\n                                                    \"theme\": {\"type\": \"string\"},\n                                                    \"notifications\": {\n                                                        \"type\": \"boolean\"\n                                                    },\n                                                },\n                                                \"description\": \"User preferences\",\n                                            },\n                                        },\n                                        \"required\": [\"name\", \"email\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"201\": {\n                                \"description\": \"User created\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                                \"email\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n\n    async def test_request_body_properties_in_tool(self, request_body_spec):\n        \"\"\"Test that request body properties are included in tool parameters.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=request_body_spec,\n                client=client,\n                name=\"Request Body Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the create user tool\n                create_tool = next(tool for tool in tools if tool.name == \"create_user\")\n                assert create_tool is not None\n\n                # Check that request body properties are included\n                params = create_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Check that request body properties are present\n                assert \"name\" in properties\n                assert \"email\" in properties\n                assert \"age\" in properties\n                assert \"preferences\" in properties\n\n                # Check required fields from request body\n                required = params.get(\"required\", [])\n                assert \"name\" in required\n                assert \"email\" in required\n\n\nclass TestResponseSchemas:\n    \"\"\"Test OpenAPI response schema handling.\"\"\"\n\n    @pytest.fixture\n    def response_schema_spec(self):\n        \"\"\"OpenAPI spec with detailed response schemas.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Response Schema Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get user details\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User details retrieved successfully\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                                \"email\": {\"type\": \"string\"},\n                                                \"profile\": {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"bio\": {\"type\": \"string\"},\n                                                        \"avatar_url\": {\n                                                            \"type\": \"string\"\n                                                        },\n                                                    },\n                                                },\n                                            },\n                                            \"required\": [\"id\", \"name\", \"email\"],\n                                        }\n                                    }\n                                },\n                            },\n                            \"404\": {\n                                \"description\": \"User not found\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"error\": {\"type\": \"string\"},\n                                                \"code\": {\"type\": \"integer\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            },\n                        },\n                    }\n                }\n            },\n        }\n\n    async def test_tool_has_output_schema(self, response_schema_spec):\n        \"\"\"Test that tools have output schemas from response definitions.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=response_schema_spec,\n                client=client,\n                name=\"Response Schema Test Server\",\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the get user tool\n                get_user_tool = next(tool for tool in tools if tool.name == \"get_user\")\n                assert get_user_tool is not None\n\n                # Check that the tool has an output schema\n                # Note: output schema might be None if not extracted properly\n                # Let's just check the tool exists and has basic properties\n                assert get_user_tool.description is not None\n                assert get_user_tool.name == \"get_user\"\n\n\nclass TestMimeTypeExtraction:\n    \"\"\"Test MIME type extraction from route responses.\"\"\"\n\n    def test_json_response(self):\n        \"\"\"JSON content type is correctly extracted.\"\"\"\n        route = HTTPRoute(\n            path=\"/items\",\n            method=\"GET\",\n            responses={\n                \"200\": ResponseInfo(\n                    content_schema={\"application/json\": {\"type\": \"object\"}}\n                )\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"application/json\"\n\n    def test_text_plain_response(self):\n        \"\"\"Plain text content type is correctly extracted.\"\"\"\n        route = HTTPRoute(\n            path=\"/health\",\n            method=\"GET\",\n            responses={\n                \"200\": ResponseInfo(content_schema={\"text/plain\": {\"type\": \"string\"}})\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"text/plain\"\n\n    def test_text_html_response(self):\n        \"\"\"HTML content type is correctly extracted.\"\"\"\n        route = HTTPRoute(\n            path=\"/page\",\n            method=\"GET\",\n            responses={\n                \"200\": ResponseInfo(content_schema={\"text/html\": {\"type\": \"string\"}})\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"text/html\"\n\n    def test_image_response(self):\n        \"\"\"Image content type is correctly extracted.\"\"\"\n        route = HTTPRoute(\n            path=\"/avatar\",\n            method=\"GET\",\n            responses={\n                \"200\": ResponseInfo(\n                    content_schema={\"image/png\": {\"type\": \"string\", \"format\": \"binary\"}}\n                )\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"image/png\"\n\n    def test_no_responses_defaults_to_json(self):\n        \"\"\"Empty responses default to application/json.\"\"\"\n        route = HTTPRoute(path=\"/items\", method=\"GET\", responses={})\n        assert _extract_mime_type_from_route(route) == \"application/json\"\n\n    def test_no_content_schema_defaults_to_json(self):\n        \"\"\"Response without content_schema defaults to application/json.\"\"\"\n        route = HTTPRoute(\n            path=\"/items\",\n            method=\"GET\",\n            responses={\"204\": ResponseInfo(description=\"No content\")},\n        )\n        assert _extract_mime_type_from_route(route) == \"application/json\"\n\n    def test_prefers_json_when_multiple_types(self):\n        \"\"\"When both JSON and other types exist, JSON is preferred.\"\"\"\n        route = HTTPRoute(\n            path=\"/items\",\n            method=\"GET\",\n            responses={\n                \"200\": ResponseInfo(\n                    content_schema={\n                        \"text/html\": {\"type\": \"string\"},\n                        \"application/json\": {\"type\": \"object\"},\n                    }\n                )\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"application/json\"\n\n    def test_non_standard_2xx_code(self):\n        \"\"\"Falls back to any 2xx status code when standard ones are missing.\"\"\"\n        route = HTTPRoute(\n            path=\"/items\",\n            method=\"GET\",\n            responses={\n                \"206\": ResponseInfo(\n                    content_schema={\n                        \"application/octet-stream\": {\n                            \"type\": \"string\",\n                            \"format\": \"binary\",\n                        }\n                    }\n                )\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"application/octet-stream\"\n\n    def test_ignores_error_responses(self):\n        \"\"\"Only error responses (no 2xx) results in default.\"\"\"\n        route = HTTPRoute(\n            path=\"/items\",\n            method=\"GET\",\n            responses={\n                \"404\": ResponseInfo(\n                    content_schema={\"application/json\": {\"type\": \"object\"}}\n                )\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"application/json\"\n\n    def test_201_response(self):\n        \"\"\"201 Created response content type is extracted.\"\"\"\n        route = HTTPRoute(\n            path=\"/items\",\n            method=\"POST\",\n            responses={\n                \"201\": ResponseInfo(content_schema={\"text/plain\": {\"type\": \"string\"}})\n            },\n        )\n        assert _extract_mime_type_from_route(route) == \"text/plain\"\n\n    def test_media_type_without_schema(self):\n        \"\"\"Media type declared without a schema still infers MIME type.\"\"\"\n        route = HTTPRoute(\n            path=\"/health\",\n            method=\"GET\",\n            responses={\"200\": ResponseInfo(content_schema={\"text/plain\": {}})},\n        )\n        assert _extract_mime_type_from_route(route) == \"text/plain\"\n\n\nclass TestResourceTemplateMimeType:\n    \"\"\"Test that OpenAPIResourceTemplate uses inferred MIME types.\"\"\"\n\n    @pytest.fixture\n    def text_plain_spec(self):\n        \"\"\"OpenAPI spec with a text/plain resource template endpoint.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Text API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/documents/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_document\",\n                        \"summary\": \"Get document content\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Document content\",\n                                \"content\": {\n                                    \"text/plain\": {\"schema\": {\"type\": \"string\"}}\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n\n    @pytest.fixture\n    def html_spec(self):\n        \"\"\"OpenAPI spec with a text/html resource endpoint.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"HTML API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/pages/{slug}\": {\n                    \"get\": {\n                        \"operationId\": \"get_page\",\n                        \"summary\": \"Get HTML page\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"slug\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"HTML page\",\n                                \"content\": {\n                                    \"text/html\": {\"schema\": {\"type\": \"string\"}}\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n\n    async def test_resource_template_text_plain_mime_type(self, text_plain_spec):\n        \"\"\"Resource template should reflect text/plain from OpenAPI spec.\"\"\"\n        route_maps = [RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE_TEMPLATE)]\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=text_plain_spec, client=client, route_maps=route_maps\n            )\n            mcp = FastMCP(\"Test\")\n            mcp.add_provider(provider)\n            async with Client(mcp) as mcp_client:\n                templates = await mcp_client.list_resource_templates()\n                assert len(templates) == 1\n                assert templates[0].mimeType == \"text/plain\"\n\n    async def test_resource_template_html_mime_type(self, html_spec):\n        \"\"\"Resource template should reflect text/html from OpenAPI spec.\"\"\"\n        route_maps = [RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE_TEMPLATE)]\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=html_spec, client=client, route_maps=route_maps\n            )\n            mcp = FastMCP(\"Test\")\n            mcp.add_provider(provider)\n            async with Client(mcp) as mcp_client:\n                templates = await mcp_client.list_resource_templates()\n                assert len(templates) == 1\n                assert templates[0].mimeType == \"text/html\"\n\n    async def test_resource_template_defaults_json_mime_type(self):\n        \"\"\"Resource template defaults to application/json for JSON responses.\"\"\"\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"JSON API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get user\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User data\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n        route_maps = [RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE_TEMPLATE)]\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec, client=client, route_maps=route_maps\n            )\n            mcp = FastMCP(\"Test\")\n            mcp.add_provider(provider)\n            async with Client(mcp) as mcp_client:\n                templates = await mcp_client.list_resource_templates()\n                assert len(templates) == 1\n                assert templates[0].mimeType == \"application/json\"\n\n\nclass TestResourceMimeType:\n    \"\"\"Test that OpenAPIResource uses inferred MIME types.\"\"\"\n\n    async def test_resource_text_plain_mime_type(self):\n        \"\"\"Static resource should reflect text/plain from OpenAPI spec.\"\"\"\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Health API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/health\": {\n                    \"get\": {\n                        \"operationId\": \"healthcheck\",\n                        \"summary\": \"Health check\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Health status\",\n                                \"content\": {\n                                    \"text/plain\": {\"schema\": {\"type\": \"string\"}}\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n        route_maps = [RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE)]\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec, client=client, route_maps=route_maps\n            )\n            mcp = FastMCP(\"Test\")\n            mcp.add_provider(provider)\n            async with Client(mcp) as mcp_client:\n                resources = await mcp_client.list_resources()\n                assert len(resources) == 1\n                assert resources[0].mimeType == \"text/plain\"\n\n    async def test_resource_mime_type_without_schema(self):\n        \"\"\"Resource with media type but no schema still infers MIME type.\"\"\"\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Health API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/health\": {\n                    \"get\": {\n                        \"operationId\": \"healthcheck\",\n                        \"summary\": \"Health check\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Health status\",\n                                \"content\": {\"text/plain\": {}},\n                            }\n                        },\n                    }\n                }\n            },\n        }\n        route_maps = [RouteMap(methods=[\"GET\"], mcp_type=MCPType.RESOURCE)]\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec, client=client, route_maps=route_maps\n            )\n            mcp = FastMCP(\"Test\")\n            mcp.add_provider(provider)\n            async with Client(mcp) as mcp_client:\n                resources = await mcp_client.list_resources()\n                assert len(resources) == 1\n                assert resources[0].mimeType == \"text/plain\"\n\n\nclass TestValidateOutput:\n    \"\"\"Tests for the validate_output option on OpenAPIProvider.\"\"\"\n\n    @pytest.fixture\n    def spec_with_output_schema(self):\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get a user\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"A user\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                                \"email\": {\"type\": \"string\"},\n                                            },\n                                            \"required\": [\"id\", \"name\"],\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                \"/items\": {\n                    \"get\": {\n                        \"operationId\": \"list_items\",\n                        \"summary\": \"List items\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"An array of items\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"type\": \"object\",\n                                                \"properties\": {\n                                                    \"name\": {\"type\": \"string\"}\n                                                },\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n            },\n        }\n\n    async def test_validate_output_true_preserves_extracted_schema(\n        self, spec_with_output_schema\n    ):\n        \"\"\"Default validate_output=True uses the real extracted schema.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec_with_output_schema,\n                client=client,\n            )\n\n            tool = provider._tools[\"get_user\"]\n            assert tool.output_schema is not None\n            assert tool.output_schema.get(\"type\") == \"object\"\n            assert \"properties\" in tool.output_schema\n            assert \"id\" in tool.output_schema[\"properties\"]\n\n    async def test_validate_output_false_uses_permissive_schema(\n        self, spec_with_output_schema\n    ):\n        \"\"\"validate_output=False replaces the schema with a permissive one.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec_with_output_schema,\n                client=client,\n                validate_output=False,\n            )\n\n            tool = provider._tools[\"get_user\"]\n            assert tool.output_schema is not None\n            assert tool.output_schema == {\n                \"type\": \"object\",\n                \"additionalProperties\": True,\n            }\n\n    async def test_validate_output_false_preserves_wrap_result_flag(\n        self, spec_with_output_schema\n    ):\n        \"\"\"validate_output=False preserves x-fastmcp-wrap-result for array responses.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec_with_output_schema,\n                client=client,\n                validate_output=False,\n            )\n\n            # The list_items endpoint returns an array, so the extracted schema\n            # would have had x-fastmcp-wrap-result=True\n            tool = provider._tools[\"list_items\"]\n            assert tool.output_schema is not None\n            assert tool.output_schema.get(\"x-fastmcp-wrap-result\") is True\n            assert tool.output_schema.get(\"additionalProperties\") is True\n\n    async def test_validate_output_false_allows_nonconforming_response(\n        self, spec_with_output_schema\n    ):\n        \"\"\"With validate_output=False, responses that don't match the spec succeed.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        # Return extra fields not in the schema\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 200\n        mock_response.json.return_value = {\n            \"id\": 1,\n            \"name\": \"Alice\",\n            \"email\": \"alice@example.com\",\n            \"unexpected_field\": \"surprise\",\n            \"nested\": {\"deep\": True},\n        }\n        mock_response.raise_for_status = Mock()\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        provider = OpenAPIProvider(\n            openapi_spec=spec_with_output_schema,\n            client=mock_client,\n            validate_output=False,\n        )\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(provider)\n\n        async with Client(mcp) as mcp_client:\n            result = await mcp_client.call_tool(\"get_user\", {\"id\": 1})\n            assert result is not None\n            # Structured content should have the full response including extra fields\n            assert result.structured_content is not None\n            assert result.structured_content[\"unexpected_field\"] == \"surprise\"\n\n    async def test_validate_output_false_wraps_non_dict_response(\n        self, spec_with_output_schema\n    ):\n        \"\"\"Non-dict responses are wrapped even when schema says object and validate_output=False.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        # Backend returns an array even though schema says object\n        mock_response = Mock(spec=Response)\n        mock_response.status_code = 200\n        mock_response.json.return_value = [{\"id\": 1}, {\"id\": 2}]\n        mock_response.raise_for_status = Mock()\n        mock_client.send = AsyncMock(return_value=mock_response)\n\n        provider = OpenAPIProvider(\n            openapi_spec=spec_with_output_schema,\n            client=mock_client,\n            validate_output=False,\n        )\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(provider)\n\n        async with Client(mcp) as mcp_client:\n            result = await mcp_client.call_tool(\"get_user\", {\"id\": 1})\n            assert result is not None\n            # Non-dict should be wrapped so structured_content is always a dict\n            assert result.structured_content is not None\n            assert isinstance(result.structured_content, dict)\n            assert result.structured_content[\"result\"] == [{\"id\": 1}, {\"id\": 2}]\n\n    async def test_from_openapi_threads_validate_output(self, spec_with_output_schema):\n        \"\"\"FastMCP.from_openapi() correctly passes validate_output to the provider.\"\"\"\n        mock_client = Mock(spec=httpx.AsyncClient)\n        mock_client.base_url = \"https://api.example.com\"\n        mock_client.headers = None\n\n        server = FastMCP.from_openapi(\n            openapi_spec=spec_with_output_schema,\n            client=mock_client,\n            validate_output=False,\n        )\n\n        async with Client(server) as mcp_client:\n            tools = await mcp_client.list_tools()\n            get_user = next(t for t in tools if t.name == \"get_user\")\n            # With validate_output=False, the outputSchema should be permissive\n            assert get_user.outputSchema is not None\n            assert get_user.outputSchema.get(\"additionalProperties\") is True\n            # Should NOT have specific properties from the original schema\n            assert \"properties\" not in get_user.outputSchema\n\n\nclass TestRedactHeaders:\n    \"\"\"Test that non-safe headers are redacted in debug logging.\"\"\"\n\n    def test_known_sensitive_headers_are_redacted(self):\n        headers = httpx.Headers(\n            {\n                \"Authorization\": \"Bearer secret-token\",\n                \"X-API-Key\": \"my-api-key\",\n                \"Cookie\": \"session=abc123\",\n                \"Proxy-Authorization\": \"Basic creds\",\n                \"Content-Type\": \"application/json\",\n                \"Accept\": \"text/html\",\n            }\n        )\n        redacted = _redact_headers(headers)\n        assert redacted[\"authorization\"] == \"***\"\n        assert redacted[\"x-api-key\"] == \"***\"\n        assert redacted[\"cookie\"] == \"***\"\n        assert redacted[\"proxy-authorization\"] == \"***\"\n        assert redacted[\"content-type\"] == \"application/json\"\n        assert redacted[\"accept\"] == \"text/html\"\n\n    def test_arbitrary_auth_headers_are_redacted(self):\n        \"\"\"Arbitrary header names (e.g. OpenAPI apiKey-in-header) are redacted.\"\"\"\n        headers = httpx.Headers(\n            {\n                \"X-Custom-Token\": \"secret\",\n                \"X-My-Service-Key\": \"also-secret\",\n                \"Content-Type\": \"application/json\",\n            }\n        )\n        redacted = _redact_headers(headers)\n        assert redacted[\"x-custom-token\"] == \"***\"\n        assert redacted[\"x-my-service-key\"] == \"***\"\n        assert redacted[\"content-type\"] == \"application/json\"\n\n    def test_safe_only_headers(self):\n        headers = httpx.Headers({\"Content-Type\": \"application/json\"})\n        redacted = _redact_headers(headers)\n        assert redacted == {\"content-type\": \"application/json\"}\n"
  },
  {
    "path": "tests/server/providers/openapi/test_openapi_performance.py",
    "content": "\"\"\"Performance regression tests for OpenAPI parsing.\n\nThese tests ensure that large OpenAPI schemas (like GitHub's API) parse quickly\nand don't regress to the slow performance we had before optimization.\n\"\"\"\n\nimport time\nfrom typing import Any\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\n\n\nclass TestOpenAPIPerformance:\n    \"\"\"Performance tests for OpenAPI parsing with real-world large schemas.\"\"\"\n\n    # 10 second maximum timeout for this test no matter what\n    @pytest.mark.integration\n    @pytest.mark.timeout(10)\n    async def test_github_api_schema_performance(self):\n        \"\"\"\n        Test that GitHub's full API schema parses quickly.\n\n        This is a regression test to ensure our performance optimizations\n        (eliminating deepcopy, single-pass optimization, smart union adjustment)\n        continue to work. Without these optimizations, this test would take\n        multiple minutes to parse.\n\n        On a local machine, this tests passes in ~2 seconds, but in GHA CI we see\n        times as high as 6-7 seconds, so the test is asserted to pass in under\n        10. Given that, this isn't intended to be a strict performance test, but\n        rather a canary to ensure we don't regress significantly.\n        \"\"\"\n\n        # Download the full GitHub API schema (typically ~10MB)\n        response = httpx.get(\n            \"https://raw.githubusercontent.com/github/rest-api-description/refs/heads/main/descriptions-next/ghes-3.17/ghes-3.17.json\",\n            timeout=30.0,  # Allow time for download\n        )\n        response.raise_for_status()\n        schema = response.json()\n\n        # Time the parsing operation\n        start_time = time.time()\n\n        # This should complete quickly with our optimizations\n        mcp_server = FastMCP.from_openapi(schema, httpx.AsyncClient())\n\n        elapsed_time = time.time() - start_time\n\n        print(f\"OpenAPI parsing took {elapsed_time:.2f}s\")\n\n        # Verify the server was created successfully\n        assert mcp_server is not None\n\n        # Performance regression test: should complete in under 10 seconds\n        assert elapsed_time < 10.0, (\n            f\"OpenAPI parsing took {elapsed_time:.2f}s, exceeding 10s limit. \"\n            f\"This suggests a performance regression.\"\n        )\n\n        # Verify server and tools were created successfully\n        tools = await mcp_server.list_tools()\n        assert len(tools) > 500\n\n    def test_medium_schema_performance(self):\n        \"\"\"\n        Test parsing performance with a smaller synthetic schema.\n\n        This test doesn't require network access and provides a baseline\n        for performance testing in CI environments.\n        \"\"\"\n        # Create a medium-sized synthetic schema\n        schema: dict[str, Any] = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"paths\": {},\n        }\n\n        # Generate multiple paths to create a reasonably sized schema\n        for i in range(100):\n            path = f\"/test/{i}\"\n            schema[\"paths\"][path] = {\n                \"get\": {\n                    \"operationId\": f\"test_{i}\",\n                    \"parameters\": [\n                        {\"name\": \"param1\", \"in\": \"query\", \"schema\": {\"type\": \"string\"}}\n                    ],\n                    \"responses\": {\n                        \"200\": {\n                            \"description\": \"Success\",\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"id\": {\"type\": \"integer\"},\n                                            \"name\": {\"type\": \"string\"},\n                                            \"data\": {\n                                                \"type\": \"object\",\n                                                \"additionalProperties\": False,\n                                                \"properties\": {\n                                                    \"value\": {\"type\": \"string\"},\n                                                    \"metadata\": {\n                                                        \"type\": \"object\",\n                                                        \"properties\": {\n                                                            \"created\": {\n                                                                \"type\": \"string\"\n                                                            },\n                                                            \"updated\": {\n                                                                \"type\": \"string\"\n                                                            },\n                                                        },\n                                                    },\n                                                },\n                                            },\n                                        },\n                                    }\n                                }\n                            },\n                        }\n                    },\n                }\n            }\n\n        # Time the parsing\n        start_time = time.time()\n        mcp_server = FastMCP.from_openapi(schema, httpx.AsyncClient())\n        elapsed_time = time.time() - start_time\n\n        # Should be very fast for medium schemas (well under 1 second)\n        assert elapsed_time < 1.0, (\n            f\"Medium schema parsing took {elapsed_time:.3f}s, expected <1s\"\n        )\n        assert mcp_server is not None\n"
  },
  {
    "path": "tests/server/providers/openapi/test_parameter_collisions.py",
    "content": "\"\"\"Tests for parameter collision handling in OpenAPIProvider.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\n\n\ndef create_openapi_server(\n    openapi_spec: dict,\n    client,\n    name: str = \"OpenAPI Server\",\n) -> FastMCP:\n    \"\"\"Helper to create a FastMCP server with OpenAPIProvider.\"\"\"\n    provider = OpenAPIProvider(openapi_spec=openapi_spec, client=client)\n    mcp = FastMCP(name)\n    mcp.add_provider(provider)\n    return mcp\n\n\nclass TestParameterCollisions:\n    \"\"\"Test parameter name collisions between different locations (path, query, body).\"\"\"\n\n    @pytest.fixture\n    def collision_spec(self):\n        \"\"\"OpenAPI spec with parameter name collisions.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Collision Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"put\": {\n                        \"operationId\": \"update_user\",\n                        \"summary\": \"Update user with collision between path and body\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                                \"description\": \"User ID in path\",\n                            }\n                        ],\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"id\": {\n                                                \"type\": \"integer\",\n                                                \"description\": \"User ID in body (different from path)\",\n                                            },\n                                            \"name\": {\n                                                \"type\": \"string\",\n                                                \"description\": \"User name\",\n                                            },\n                                            \"email\": {\n                                                \"type\": \"string\",\n                                                \"description\": \"User email\",\n                                            },\n                                        },\n                                        \"required\": [\"name\", \"email\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User updated\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                                \"email\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                \"/search\": {\n                    \"get\": {\n                        \"operationId\": \"search_with_collision\",\n                        \"summary\": \"Search with query and header collision\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"query\",\n                                \"in\": \"query\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"Search query parameter\",\n                            },\n                            {\n                                \"name\": \"query\",\n                                \"in\": \"header\",\n                                \"required\": False,\n                                \"schema\": {\"type\": \"string\"},\n                                \"description\": \"Search query in header\",\n                            },\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Search results\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"results\": {\n                                                    \"type\": \"array\",\n                                                    \"items\": {\"type\": \"object\"},\n                                                }\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n            },\n        }\n\n    async def test_path_body_collision_handling(self, collision_spec):\n        \"\"\"Test that path and body parameters with same name are handled correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=collision_spec, client=client, name=\"Collision Test Server\"\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the update user tool\n                update_tool = next(tool for tool in tools if tool.name == \"update_user\")\n                assert update_tool is not None\n\n                # Check that both path and body 'id' parameters are included\n                params = update_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Should have both path ID and body ID (with potential suffixing)\n                # The implementation should handle this collision by suffixing one of them\n                assert \"id\" in properties  # One version of id\n\n                # Check for suffixed versions or verify both exist somehow\n                # The exact handling depends on implementation, but both should be accessible\n                param_names = list(properties.keys())\n                id_params = [name for name in param_names if \"id\" in name]\n                assert len(id_params) >= 1  # At least one id parameter\n\n                # Should also have other body parameters\n                assert \"name\" in properties\n                assert \"email\" in properties\n\n                # Required fields should include path parameter and required body fields\n                required = params.get(\"required\", [])\n                assert \"name\" in required\n                assert \"email\" in required\n                # Path parameter should be required (may be suffixed)\n                id_required = any(\"id\" in req for req in required)\n                assert id_required\n\n    async def test_query_header_collision_handling(self, collision_spec):\n        \"\"\"Test that query and header parameters with same name are handled correctly.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=collision_spec, client=client, name=\"Collision Test Server\"\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Find the search tool\n                search_tool = next(\n                    tool for tool in tools if tool.name == \"search_with_collision\"\n                )\n                assert search_tool is not None\n\n                # Check that both query and header 'query' parameters are handled\n                params = search_tool.inputSchema\n                properties = params[\"properties\"]\n\n                # Should handle the collision somehow (suffixing or other mechanism)\n                param_names = list(properties.keys())\n                query_params = [name for name in param_names if \"query\" in name]\n                assert len(query_params) >= 1  # At least one query parameter\n\n                # Required should include the required query parameter\n                required = params.get(\"required\", [])\n                query_required = any(\"query\" in req for req in required)\n                assert query_required\n\n    async def test_collision_resolution_maintains_functionality(self, collision_spec):\n        \"\"\"Test that collision resolution doesn't break basic tool functionality.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            server = create_openapi_server(\n                openapi_spec=collision_spec, client=client, name=\"Collision Test Server\"\n            )\n\n            async with Client(server) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Should successfully create tools despite collisions\n                assert len(tools) == 2\n\n                tool_names = {tool.name for tool in tools}\n                assert \"update_user\" in tool_names\n                assert \"search_with_collision\" in tool_names\n\n                # Tools should have valid schemas\n                for tool in tools:\n                    assert tool.inputSchema is not None\n                    assert tool.inputSchema[\"type\"] == \"object\"\n                    assert \"properties\" in tool.inputSchema\n"
  },
  {
    "path": "tests/server/providers/openapi/test_performance_comparison.py",
    "content": "\"\"\"Performance tests for OpenAPIProvider implementation.\"\"\"\n\nimport gc\nimport time\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\n\n\ndef create_openapi_server(\n    openapi_spec: dict,\n    client,\n    name: str = \"OpenAPI Server\",\n) -> FastMCP:\n    \"\"\"Helper to create a FastMCP server with OpenAPIProvider.\"\"\"\n    provider = OpenAPIProvider(openapi_spec=openapi_spec, client=client)\n    mcp = FastMCP(name)\n    mcp.add_provider(provider)\n    return mcp\n\n\nclass TestPerformance:\n    \"\"\"Test performance of OpenAPIProvider implementation.\"\"\"\n\n    @pytest.fixture\n    def comprehensive_spec(self):\n        \"\"\"Comprehensive OpenAPI spec for performance testing.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Performance Test API\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/users\": {\n                    \"get\": {\n                        \"operationId\": \"list_users\",\n                        \"summary\": \"List users\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"limit\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"schema\": {\"type\": \"integer\", \"default\": 10},\n                            },\n                            {\n                                \"name\": \"offset\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"schema\": {\"type\": \"integer\", \"default\": 0},\n                            },\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"Users listed\"}},\n                    },\n                    \"post\": {\n                        \"operationId\": \"create_user\",\n                        \"summary\": \"Create user\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\"type\": \"string\"},\n                                            \"email\": {\"type\": \"string\"},\n                                            \"age\": {\"type\": \"integer\"},\n                                        },\n                                        \"required\": [\"name\", \"email\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\"201\": {\"description\": \"User created\"}},\n                    },\n                },\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get user\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"User found\"}},\n                    },\n                    \"put\": {\n                        \"operationId\": \"update_user\",\n                        \"summary\": \"Update user\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\"type\": \"string\"},\n                                            \"email\": {\"type\": \"string\"},\n                                            \"age\": {\"type\": \"integer\"},\n                                        },\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\"200\": {\"description\": \"User updated\"}},\n                    },\n                    \"delete\": {\n                        \"operationId\": \"delete_user\",\n                        \"summary\": \"Delete user\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"responses\": {\"204\": {\"description\": \"User deleted\"}},\n                    },\n                },\n                \"/search\": {\n                    \"get\": {\n                        \"operationId\": \"search_users\",\n                        \"summary\": \"Search users\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"q\",\n                                \"in\": \"query\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                            },\n                            {\n                                \"name\": \"filters\",\n                                \"in\": \"query\",\n                                \"required\": False,\n                                \"style\": \"deepObject\",\n                                \"explode\": True,\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"age_min\": {\"type\": \"integer\"},\n                                        \"age_max\": {\"type\": \"integer\"},\n                                        \"status\": {\n                                            \"type\": \"string\",\n                                            \"enum\": [\"active\", \"inactive\"],\n                                        },\n                                    },\n                                },\n                            },\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"Search results\"}},\n                    }\n                },\n            },\n        }\n\n    def test_provider_initialization_performance(self, comprehensive_spec):\n        \"\"\"Test that provider initialization is fast (serverless requirement).\"\"\"\n        num_iterations = 5\n\n        # Measure provider initialization\n        times = []\n        for _ in range(num_iterations):\n            client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n            start_time = time.time()\n            provider = OpenAPIProvider(\n                openapi_spec=comprehensive_spec,\n                client=client,\n            )\n            # Ensure provider is fully initialized\n            assert provider is not None\n            end_time = time.time()\n            times.append(end_time - start_time)\n\n        avg_time = sum(times) / len(times)\n        max_acceptable_time = 0.1  # 100ms\n\n        print(f\"Average initialization time: {avg_time:.4f}s\")\n        print(f\"Performance: {'✓' if avg_time < max_acceptable_time else '✗'}\")\n\n        # Should initialize in under 100ms for serverless requirements\n        assert avg_time < max_acceptable_time, (\n            f\"Provider should initialize in under 100ms, got {avg_time:.4f}s\"\n        )\n\n    def test_server_initialization_performance(self, comprehensive_spec):\n        \"\"\"Test that full server initialization is fast.\"\"\"\n        num_iterations = 5\n\n        times = []\n        for _ in range(num_iterations):\n            client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n            start_time = time.time()\n            server = create_openapi_server(\n                openapi_spec=comprehensive_spec,\n                client=client,\n                name=\"Performance Test\",\n            )\n            # Ensure server is fully initialized\n            assert server is not None\n            end_time = time.time()\n            times.append(end_time - start_time)\n\n        avg_time = sum(times) / len(times)\n        max_acceptable_time = 0.1  # 100ms\n\n        print(f\"Average server initialization time: {avg_time:.4f}s\")\n\n        assert avg_time < max_acceptable_time, (\n            f\"Server should initialize in under 100ms, got {avg_time:.4f}s\"\n        )\n\n    async def test_functionality_after_optimization(self, comprehensive_spec):\n        \"\"\"Verify that performance optimization doesn't break functionality.\"\"\"\n        client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n\n        server = create_openapi_server(\n            openapi_spec=comprehensive_spec,\n            client=client,\n            name=\"Test Server\",\n        )\n\n        # Get tools from the server via public API\n        tools = await server.list_tools()\n\n        # Should have 6 operations in the spec\n        assert len(tools) == 6\n\n        # Expected operations\n        expected_operations = {\n            \"list_users\",\n            \"create_user\",\n            \"get_user\",\n            \"update_user\",\n            \"delete_user\",\n            \"search_users\",\n        }\n        assert {t.name for t in tools} == expected_operations\n\n    def test_memory_efficiency(self, comprehensive_spec):\n        \"\"\"Test that implementation doesn't significantly increase memory usage.\"\"\"\n\n        # Helper to count total tools across all providers\n        def count_provider_tools(server):\n            total = 0\n            for provider in server.providers:\n                if hasattr(provider, \"_tools\"):\n                    total += len(provider._tools)\n            return total\n\n        gc.collect()  # Clean up before baseline\n        baseline_refs = len(gc.get_objects())\n\n        servers = []\n        for i in range(10):\n            client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n            server = create_openapi_server(\n                openapi_spec=comprehensive_spec,\n                client=client,\n                name=f\"Memory Test Server {i}\",\n            )\n            servers.append(server)\n\n        # Servers should all be functional\n        assert len(servers) == 10\n        assert all(count_provider_tools(s) == 6 for s in servers)\n\n        # Memory usage shouldn't explode\n        gc.collect()\n        current_refs = len(gc.get_objects())\n        growth_ratio = current_refs / max(baseline_refs, 1)\n        assert growth_ratio < 3.0, (\n            f\"Memory usage grew by {growth_ratio}x, which seems excessive\"\n        )\n"
  },
  {
    "path": "tests/server/providers/openapi/test_server.py",
    "content": "\"\"\"Unit tests for OpenAPIProvider.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers.openapi import OpenAPIProvider\nfrom fastmcp.server.providers.openapi.provider import DEFAULT_TIMEOUT\n\n\nclass TestOpenAPIProviderBasicFunctionality:\n    \"\"\"Test basic OpenAPIProvider functionality.\"\"\"\n\n    @pytest.fixture\n    def simple_openapi_spec(self):\n        \"\"\"Simple OpenAPI spec for testing.\"\"\"\n        return {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"summary\": \"Get user by ID\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"integer\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User retrieved successfully\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                                \"email\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n                \"/users\": {\n                    \"post\": {\n                        \"operationId\": \"create_user\",\n                        \"summary\": \"Create a new user\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\"type\": \"string\"},\n                                            \"email\": {\"type\": \"string\"},\n                                        },\n                                        \"required\": [\"name\", \"email\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"201\": {\n                                \"description\": \"User created successfully\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\"type\": \"integer\"},\n                                                \"name\": {\"type\": \"string\"},\n                                                \"email\": {\"type\": \"string\"},\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                },\n            },\n        }\n\n    def test_provider_initialization(self, simple_openapi_spec):\n        \"\"\"Test provider initialization with OpenAPI spec.\"\"\"\n        client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n        provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=client)\n\n        # Should have initialized RequestDirector successfully\n        assert hasattr(provider, \"_director\")\n        assert hasattr(provider, \"_spec\")\n\n    def test_server_with_provider(self, simple_openapi_spec):\n        \"\"\"Test server initialization with OpenAPIProvider.\"\"\"\n        client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n        provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=client)\n\n        mcp = FastMCP(\"Test Server\")\n        mcp.add_provider(provider)\n\n        assert mcp.name == \"Test Server\"\n\n    async def test_provider_creates_tools_from_spec(self, simple_openapi_spec):\n        \"\"\"Test that provider creates tools from OpenAPI spec.\"\"\"\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=client)\n\n            mcp = FastMCP(\"Test Server\")\n            mcp.add_provider(provider)\n\n            async with Client(mcp) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                # Should have created tools for both operations\n                assert len(tools) == 2\n\n                tool_names = {tool.name for tool in tools}\n                assert \"get_user\" in tool_names\n                assert \"create_user\" in tool_names\n\n    async def test_provider_tool_execution(self, simple_openapi_spec):\n        \"\"\"Test tool execution uses RequestDirector.\"\"\"\n        mock_client = httpx.AsyncClient()\n        provider = OpenAPIProvider(openapi_spec=simple_openapi_spec, client=mock_client)\n\n        mcp = FastMCP(\"Test Server\")\n        mcp.add_provider(provider)\n\n        async with Client(mcp) as mcp_client:\n            tools = await mcp_client.list_tools()\n\n            # Should have tools using RequestDirector\n            assert len(tools) == 2\n\n            get_user_tool = next(tool for tool in tools if tool.name == \"get_user\")\n            assert get_user_tool is not None\n            assert get_user_tool.description is not None\n\n    def test_provider_creates_default_client_from_spec(self, simple_openapi_spec):\n        \"\"\"Test that omitting client creates one from the spec's servers URL.\"\"\"\n        provider = OpenAPIProvider(openapi_spec=simple_openapi_spec)\n        assert str(provider._client.base_url).rstrip(\"/\") == \"https://api.example.com\"\n        assert provider._client.timeout == httpx.Timeout(DEFAULT_TIMEOUT)\n\n    def test_provider_default_client_requires_servers(self):\n        \"\"\"Test that omitting client without servers in spec raises.\"\"\"\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"No Servers\", \"version\": \"1.0.0\"},\n            \"paths\": {},\n        }\n        with pytest.raises(ValueError, match=\"No server URL\"):\n            OpenAPIProvider(openapi_spec=spec)\n\n    def test_provider_with_empty_spec(self):\n        \"\"\"Test provider with minimal OpenAPI spec.\"\"\"\n        minimal_spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Empty API\", \"version\": \"1.0.0\"},\n            \"paths\": {},\n        }\n\n        client = httpx.AsyncClient(base_url=\"https://api.example.com\")\n        provider = OpenAPIProvider(openapi_spec=minimal_spec, client=client)\n\n        # Should handle empty paths gracefully\n        assert hasattr(provider, \"_director\")\n        assert hasattr(provider, \"_spec\")\n\n    async def test_clean_schema_output_no_unused_defs(self):\n        \"\"\"Test that unused schema definitions are removed from tool schemas.\"\"\"\n        spec_with_unused_defs = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"servers\": [{\"url\": \"https://api.example.com\"}],\n            \"paths\": {\n                \"/users\": {\n                    \"post\": {\n                        \"operationId\": \"create_user\",\n                        \"summary\": \"Create a new user\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"name\": {\"type\": \"string\", \"title\": \"Name\"},\n                                            \"active\": {\n                                                \"type\": \"boolean\",\n                                                \"title\": \"Active\",\n                                            },\n                                        },\n                                        \"required\": [\"name\", \"active\"],\n                                    }\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User created successfully\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"object\",\n                                            \"properties\": {\n                                                \"id\": {\n                                                    \"type\": \"integer\",\n                                                    \"title\": \"Id\",\n                                                },\n                                                \"name\": {\n                                                    \"type\": \"string\",\n                                                    \"title\": \"Name\",\n                                                },\n                                                \"active\": {\n                                                    \"type\": \"boolean\",\n                                                    \"title\": \"Active\",\n                                                },\n                                            },\n                                            \"required\": [\"id\", \"name\", \"active\"],\n                                            \"title\": \"User\",\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n            \"components\": {\n                \"schemas\": {\n                    \"HTTPValidationError\": {\n                        \"properties\": {\n                            \"detail\": {\n                                \"items\": {\n                                    \"$ref\": \"#/components/schemas/ValidationError\"\n                                },\n                                \"title\": \"Detail\",\n                                \"type\": \"array\",\n                            }\n                        },\n                        \"title\": \"HTTPValidationError\",\n                        \"type\": \"object\",\n                    },\n                    \"ValidationError\": {\n                        \"properties\": {\n                            \"loc\": {\n                                \"items\": {\n                                    \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}]\n                                },\n                                \"title\": \"Location\",\n                                \"type\": \"array\",\n                            },\n                            \"msg\": {\"title\": \"Message\", \"type\": \"string\"},\n                            \"type\": {\"title\": \"Error Type\", \"type\": \"string\"},\n                        },\n                        \"required\": [\"loc\", \"msg\", \"type\"],\n                        \"title\": \"ValidationError\",\n                        \"type\": \"object\",\n                    },\n                }\n            },\n        }\n\n        async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n            provider = OpenAPIProvider(\n                openapi_spec=spec_with_unused_defs, client=client\n            )\n            mcp = FastMCP(\"Test Server\")\n            mcp.add_provider(provider)\n\n            async with Client(mcp) as mcp_client:\n                tools = await mcp_client.list_tools()\n\n                assert len(tools) == 1\n                tool = tools[0]\n\n                assert tool.name == \"create_user\"\n\n                expected_input_schema = {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\", \"title\": \"Name\"},\n                        \"active\": {\"type\": \"boolean\", \"title\": \"Active\"},\n                    },\n                    \"required\": [\"name\", \"active\"],\n                }\n                assert tool.inputSchema == expected_input_schema\n\n                expected_output_schema = {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"id\": {\"type\": \"integer\", \"title\": \"Id\"},\n                        \"name\": {\"type\": \"string\", \"title\": \"Name\"},\n                        \"active\": {\"type\": \"boolean\", \"title\": \"Active\"},\n                    },\n                    \"required\": [\"id\", \"name\", \"active\"],\n                    \"title\": \"User\",\n                }\n                assert tool.outputSchema == expected_output_schema\n"
  },
  {
    "path": "tests/server/providers/proxy/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/providers/proxy/test_proxy_client.py",
    "content": "from dataclasses import dataclass\n\nimport pytest\nfrom anyio import create_task_group\nfrom mcp.types import (\n    ElicitRequestFormParams,\n    LoggingLevel,\n    ModelHint,\n    ModelPreferences,\n    TextContent,\n)\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.elicitation import ElicitRequestParams, ElicitResult\nfrom fastmcp.client.logging import LogMessage\nfrom fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.elicitation import AcceptedElicitation\nfrom fastmcp.server.providers.proxy import ProxyClient, _create_client_factory\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool(tags={\"echo\"})\n    def echo(message: str) -> str:\n        return f\"echo: {message}\"\n\n    @mcp.tool\n    async def list_roots(context: Context) -> list[str]:\n        roots = await context.list_roots()\n        return [str(r.uri) for r in roots]\n\n    @mcp.tool\n    async def sampling(\n        context: Context,\n    ) -> str:\n        result = await context.sample(\n            \"Hello, world!\",\n            system_prompt=\"You love FastMCP\",\n            temperature=0.5,\n            max_tokens=100,\n            model_preferences=\"gpt-4o\",\n        )\n        return result.text or \"\"\n\n    @dataclass\n    class Person:\n        name: str\n\n    @mcp.tool\n    async def elicit(context: Context) -> str:\n        result = await context.elicit(\n            message=\"What is your name?\",\n            response_type=Person,\n        )\n\n        if result.action == \"accept\":\n            assert isinstance(result, AcceptedElicitation)\n            assert isinstance(result.data, Person)\n            return f\"Hello, {result.data.name}!\"\n        else:\n            return \"No name provided.\"\n\n    @mcp.tool\n    async def log(\n        message: str, level: LoggingLevel, logger: str, context: Context\n    ) -> None:\n        await context.log(message=message, level=level, logger_name=logger)\n\n    @mcp.tool\n    async def report_progress(context: Context) -> int:\n        for i in range(3):\n            await context.report_progress(\n                progress=i + 1,\n                total=3,\n                message=f\"{(i + 1) / 3 * 100:.2f}% complete\",\n            )\n        return 100\n\n    return mcp\n\n\n@pytest.fixture\nasync def proxy_server(fastmcp_server: FastMCP):\n    \"\"\"\n    A proxy server that forwards interactions with the proxy client to the given fastmcp server.\n    \"\"\"\n    return FastMCP.as_proxy(ProxyClient(fastmcp_server))\n\n\nclass TestProxyClient:\n    async def test_forward_tool_meta(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `echo` tool meta.\n        \"\"\"\n        async with Client(proxy_server) as client:\n            tools = await client.list_tools()\n            echo_tool = next(t for t in tools if t.name == \"echo\")\n            assert echo_tool.meta == {\"fastmcp\": {\"tags\": [\"echo\"]}}\n\n    async def test_forward_error_response(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards an error response.\n        \"\"\"\n        async with Client(proxy_server) as client:\n            with pytest.raises(ToolError, match=\"Elicitation not supported\"):\n                await client.call_tool(\"elicit\", {})\n\n    async def test_forward_list_roots_request(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `list_roots` request.\n        \"\"\"\n        roots_handler_called = False\n\n        async def roots_handler(ctx: RequestContext):\n            nonlocal roots_handler_called\n            roots_handler_called = True\n            return []\n\n        async with Client(proxy_server, roots=roots_handler) as client:\n            await client.call_tool(\"list_roots\", {})\n\n        assert roots_handler_called\n\n    async def test_forward_list_roots_response(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `list_roots` response.\n        \"\"\"\n        async with Client(proxy_server, roots=[\"file://x/y/z\"]) as client:\n            result = await client.call_tool(\"list_roots\", {})\n            assert result.data == [\"file://x/y/z\"]\n\n    async def test_forward_sampling_request(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `sampling` request.\n        \"\"\"\n        sampling_handler_called = False\n\n        def sampling_handler(\n            messages: list[SamplingMessage],\n            params: SamplingParams,\n            ctx: RequestContext,\n        ) -> str:\n            nonlocal sampling_handler_called\n            sampling_handler_called = True\n            assert messages == [\n                SamplingMessage(\n                    role=\"user\",\n                    content=TextContent(type=\"text\", text=\"Hello, world!\"),\n                )\n            ]\n            assert params.systemPrompt == \"You love FastMCP\"\n            assert params.temperature == 0.5\n            assert params.maxTokens == 100\n            assert params.modelPreferences == ModelPreferences(\n                hints=[ModelHint(name=\"gpt-4o\")]\n            )\n            return \"\"\n\n        async with Client(proxy_server, sampling_handler=sampling_handler) as client:\n            await client.call_tool(\"sampling\", {})\n\n        assert sampling_handler_called\n\n    async def test_forward_sampling_response(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `sampling` response.\n        \"\"\"\n        async with Client(\n            proxy_server, sampling_handler=lambda *args: \"I love FastMCP\"\n        ) as client:\n            result = await client.call_tool(\"sampling\", {})\n            assert result.data == \"I love FastMCP\"\n\n    async def test_elicit_request(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `elicit` request.\n        \"\"\"\n        elicitation_handler_called = False\n\n        async def elicitation_handler(\n            message, response_type, params: ElicitRequestParams, ctx\n        ):\n            nonlocal elicitation_handler_called\n            elicitation_handler_called = True\n            assert message == \"What is your name?\"\n            assert \"Person\" in str(response_type)\n            assert isinstance(params, ElicitRequestFormParams)\n            assert params.requestedSchema == {\n                \"title\": \"Person\",\n                \"type\": \"object\",\n                \"properties\": {\"name\": {\"title\": \"Name\", \"type\": \"string\"}},\n                \"required\": [\"name\"],\n            }\n            return ElicitResult(action=\"accept\", content=response_type(name=\"Alice\"))\n\n        async with Client(\n            proxy_server, elicitation_handler=elicitation_handler\n        ) as client:\n            await client.call_tool(\"elicit\", {})\n\n        assert elicitation_handler_called\n\n    async def test_elicit_accept_response(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `elicit` accept response.\n        \"\"\"\n\n        async def elicitation_handler(\n            message, response_type, params: ElicitRequestParams, ctx\n        ):\n            return ElicitResult(action=\"accept\", content=response_type(name=\"Alice\"))\n\n        async with Client(\n            proxy_server,\n            elicitation_handler=elicitation_handler,\n        ) as client:\n            result = await client.call_tool(\"elicit\", {})\n            assert result.data == \"Hello, Alice!\"\n\n    async def test_elicit_decline_response(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `elicit` decline response.\n        \"\"\"\n\n        async def elicitation_handler(\n            message, response_type, params: ElicitRequestParams, ctx\n        ):\n            return ElicitResult(action=\"decline\")\n\n        async with Client(\n            proxy_server, elicitation_handler=elicitation_handler\n        ) as client:\n            result = await client.call_tool(\"elicit\", {})\n            assert result.data == \"No name provided.\"\n\n    async def test_log_request(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `log` request.\n        \"\"\"\n        log_handler_called = False\n\n        async def log_handler(message: LogMessage) -> None:\n            nonlocal log_handler_called\n            log_handler_called = True\n            assert message.data == \"Hello, world!\"\n            assert message.level == \"info\"\n            assert message.logger == \"test\"\n\n        async with Client(proxy_server, log_handler=log_handler) as client:\n            await client.call_tool(\n                \"log\", {\"message\": \"Hello, world!\", \"level\": \"info\", \"logger\": \"test\"}\n            )\n\n        assert log_handler_called\n\n    async def test_report_progress_request(self, proxy_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly forwards the `report_progress` request.\n        \"\"\"\n\n        EXPECTED_PROGRESS_MESSAGES = [\n            dict(progress=1, total=3, message=\"33.33% complete\"),\n            dict(progress=2, total=3, message=\"66.67% complete\"),\n            dict(progress=3, total=3, message=\"100.00% complete\"),\n        ]\n        PROGRESS_MESSAGES = []\n\n        async def progress_handler(\n            progress: float, total: float | None, message: str | None\n        ) -> None:\n            PROGRESS_MESSAGES.append(\n                dict(progress=progress, total=total, message=message)\n            )\n\n        async with Client(proxy_server, progress_handler=progress_handler) as client:\n            await client.call_tool(\"report_progress\", {})\n\n        assert PROGRESS_MESSAGES == EXPECTED_PROGRESS_MESSAGES\n\n    async def test_concurrent_log_requests_no_mixing(self, proxy_server: FastMCP):\n        \"\"\"Test that concurrent log requests don't mix handlers (fixes #1068).\"\"\"\n        results: dict[str, LogMessage] = {}\n\n        async def log_handler_a(message: LogMessage) -> None:\n            results[\"logger_a\"] = message\n\n        async def log_handler_b(message: LogMessage) -> None:\n            results[\"logger_b\"] = message\n\n        async with (\n            Client(proxy_server, log_handler=log_handler_a) as client_a,\n            Client(proxy_server, log_handler=log_handler_b) as client_b,\n        ):\n            async with create_task_group() as tg:\n                tg.start_soon(\n                    client_a.call_tool,\n                    \"log\",\n                    {\"message\": \"Hello, world!\", \"level\": \"info\", \"logger\": \"a\"},\n                )\n                tg.start_soon(\n                    client_b.call_tool,\n                    \"log\",\n                    {\"message\": \"Hello, world!\", \"level\": \"info\", \"logger\": \"b\"},\n                )\n\n        assert results[\"logger_a\"].logger == \"a\"\n        assert results[\"logger_b\"].logger == \"b\"\n\n    async def test_concurrent_elicitation_no_mixing(self, proxy_server: FastMCP):\n        \"\"\"Test that concurrent elicitation requests don't mix handlers (fixes #1068).\"\"\"\n        results = {}\n\n        async def elicitation_handler_a(\n            message: str,\n            response_type: type,\n            params: ElicitRequestParams,\n            ctx: RequestContext,\n        ) -> ElicitResult:\n            return ElicitResult(action=\"accept\", content=response_type(name=\"Alice\"))\n\n        async def elicitation_handler_b(\n            message: str,\n            response_type: type,\n            params: ElicitRequestParams,\n            ctx: RequestContext,\n        ) -> ElicitResult:\n            return ElicitResult(action=\"accept\", content=response_type(name=\"Bob\"))\n\n        async def get_and_store(name, coro):\n            result = await coro\n            results[name] = result.data\n\n        async with (\n            Client(proxy_server, elicitation_handler=elicitation_handler_a) as client_a,\n            Client(proxy_server, elicitation_handler=elicitation_handler_b) as client_b,\n        ):\n            async with create_task_group() as tg:\n                tg.start_soon(\n                    get_and_store,\n                    \"elicitation_a\",\n                    client_a.call_tool(\"elicit\", {}),\n                )\n                tg.start_soon(\n                    get_and_store,\n                    \"elicitation_b\",\n                    client_b.call_tool(\"elicit\", {}),\n                )\n\n        assert results[\"elicitation_a\"] == \"Hello, Alice!\"\n        assert results[\"elicitation_b\"] == \"Hello, Bob!\"\n\n    async def test_elicit_with_default_values(self, fastmcp_server: FastMCP):\n        \"\"\"\n        Test that the proxy client correctly handles elicitation with default values (fixes #1167).\n        \"\"\"\n\n        @fastmcp_server.tool\n        async def elicit_with_defaults(context: Context) -> str:\n            class TestModel(BaseModel):\n                content: str = Field(description=\"Your reply content\")\n                acknowledge: bool = Field(\n                    default=False, description=\"Send immediately or save as draft\"\n                )\n\n            result = await context.elicit(\n                \"Please provide input:\", response_type=TestModel\n            )\n\n            if result.action == \"accept\":\n                assert isinstance(result, AcceptedElicitation)\n                assert isinstance(result.data, TestModel)\n                return f\"Content: {result.data.content}, Acknowledge: {result.data.acknowledge}\"\n            else:\n                return f\"Elicitation {result.action}\"\n\n        proxy_server = FastMCP.as_proxy(ProxyClient(fastmcp_server))\n\n        # Test that elicitation works correctly through the proxy\n        async def elicitation_handler(\n            message: str,\n            response_type: type,\n            params: ElicitRequestParams,\n            ctx: RequestContext,\n        ):\n            # Verify the schema is correct - acknowledge should have default=False, not be nullable\n            assert isinstance(params, ElicitRequestFormParams)\n            schema = params.requestedSchema\n            assert schema[\"properties\"][\"acknowledge\"][\"type\"] == \"boolean\"\n            assert schema[\"properties\"][\"acknowledge\"][\"default\"] is False\n\n            return {\"content\": \"Test content\", \"acknowledge\": True}\n\n        async with Client(\n            proxy_server, elicitation_handler=elicitation_handler\n        ) as client:\n            result = await client.call_tool(\"elicit_with_defaults\", {})\n            assert result.data == \"Content: Test content, Acknowledge: True\"\n\n    async def test_client_factory_creates_fresh_sessions(self, fastmcp_server: FastMCP):\n        \"\"\"Test that the client factory pattern creates fresh sessions for each request.\"\"\"\n        from fastmcp.server.providers.proxy import FastMCPProxy\n\n        # Create a disconnected client (should use fresh sessions per request)\n        base_client = Client(fastmcp_server)\n\n        # Test both as_proxy convenience method and direct client_factory usage\n        proxy_via_as_proxy = FastMCP.as_proxy(base_client)\n        proxy_via_factory = FastMCPProxy(client_factory=base_client.new)\n\n        # Verify the proxies are created successfully - this tests the client factory pattern\n        assert proxy_via_as_proxy is not None\n        assert proxy_via_factory is not None\n\n        # Verify they have the expected client factory behavior\n        assert hasattr(proxy_via_as_proxy, \"_local_provider\")\n        assert hasattr(proxy_via_factory, \"_local_provider\")\n\n    async def test_connected_proxy_client_uses_fresh_sessions(\n        self, fastmcp_server: FastMCP\n    ):\n        \"\"\"Connected ProxyClient targets should create fresh sessions to avoid stale context.\"\"\"\n        async with ProxyClient(fastmcp_server) as connected_client:\n            factory = _create_client_factory(connected_client)\n\n            client_a = factory()\n            client_b = factory()\n\n            assert isinstance(client_a, Client)\n            assert isinstance(client_b, Client)\n            assert client_a is not connected_client\n            assert client_b is not connected_client\n            assert client_a is not client_b\n            assert not client_a.is_connected()\n            assert not client_b.is_connected()\n"
  },
  {
    "path": "tests/server/providers/proxy/test_proxy_server.py",
    "content": "import inspect\nimport json\nimport time\nfrom typing import Any, cast\nfrom unittest.mock import AsyncMock, patch\n\nimport mcp.types as mcp_types\nimport pytest\nfrom anyio import create_task_group\nfrom dirty_equals import Contains\nfrom mcp import McpError\nfrom mcp.types import Icon, TextContent, TextResourceContents\nfrom pydantic import AnyUrl\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.resources import ResourceContent, ResourceResult\nfrom fastmcp.server import create_proxy\nfrom fastmcp.server.providers.proxy import (\n    FastMCPProxy,\n    ProxyClient,\n    ProxyProvider,\n)\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.tools.tool_transform import (\n    ToolTransformConfig,\n)\n\nUSERS = [\n    {\"id\": \"1\", \"name\": \"Alice\", \"active\": True},\n    {\"id\": \"2\", \"name\": \"Bob\", \"active\": True},\n    {\"id\": \"3\", \"name\": \"Charlie\", \"active\": False},\n]\n\n\n@pytest.fixture\ndef fastmcp_server():\n    server = FastMCP(\"TestServer\")\n\n    # --- Tools ---\n\n    @server.tool(\n        tags={\"greet\"},\n        title=\"Greet\",\n        icons=[Icon(src=\"https://example.com/greet-icon.png\")],\n    )\n    def greet(name: str) -> str:\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.tool\n    def tool_without_description() -> str:\n        return \"Hello?\"\n\n    @server.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @server.tool\n    def error_tool():\n        \"\"\"This tool always raises an error.\"\"\"\n        raise ValueError(\"This is a test error\")\n\n    # --- Resources ---\n\n    @server.resource(\n        uri=\"resource://wave\",\n        tags={\"wave\"},\n        title=\"Wave\",\n        icons=[Icon(src=\"https://example.com/wave-icon.png\")],\n    )\n    def wave() -> str:\n        return \"👋\"\n\n    @server.resource(uri=\"data://users\")\n    async def get_users() -> str:\n        import json\n\n        return json.dumps(USERS, separators=(\",\", \":\"))\n\n    @server.resource(\n        uri=\"data://user/{user_id}\",\n        tags={\"users\"},\n        title=\"User Template\",\n        icons=[Icon(src=\"https://example.com/user-icon.png\")],\n    )\n    async def get_user(user_id: str) -> str:\n        import json\n\n        user = next((user for user in USERS if user[\"id\"] == user_id), None)\n        return json.dumps(user, separators=(\",\", \":\")) if user else \"null\"\n\n    @server.resource(uri=\"data://multi\")\n    def get_multi_content() -> ResourceResult:\n        \"\"\"Resource that returns multiple content items.\"\"\"\n        return ResourceResult(\n            contents=[\n                ResourceContent(content=\"First item\", mime_type=\"text/plain\"),\n                ResourceContent(\n                    content='{\"key\": \"value\"}', mime_type=\"application/json\"\n                ),\n                ResourceContent(\n                    content=\"# Markdown\\nContent\", mime_type=\"text/markdown\"\n                ),\n            ],\n            meta={\"count\": 3},\n        )\n\n    @server.resource(uri=\"data://multi/{id}\")\n    def get_multi_template(id: str) -> ResourceResult:\n        \"\"\"Resource template that returns multiple content items.\"\"\"\n        return ResourceResult(\n            contents=[\n                ResourceContent(content=f\"Item {id} - First\", mime_type=\"text/plain\"),\n                ResourceContent(\n                    content=f'{{\"id\": \"{id}\", \"status\": \"active\"}}',\n                    mime_type=\"application/json\",\n                ),\n            ],\n            meta={\"id\": id},\n        )\n\n    # --- Prompts ---\n\n    @server.prompt(\n        tags={\"welcome\"},\n        title=\"Welcome\",\n        icons=[Icon(src=\"https://example.com/welcome-icon.png\")],\n    )\n    def welcome(name: str) -> str:\n        return f\"Welcome to FastMCP, {name}!\"\n\n    @server.prompt\n    def image_prompt():\n        \"\"\"A prompt that returns an image.\"\"\"\n        from fastmcp.prompts.base import Message, PromptResult\n\n        return PromptResult(\n            messages=[\n                Message(\"Here is an image:\"),\n                Message(\n                    content=mcp_types.ImageContent(\n                        type=\"image\",\n                        data=\"iVBORw0KGgoAAAANSUhEUg==\",\n                        mimeType=\"image/png\",\n                    ),\n                    role=\"user\",\n                ),\n            ]\n        )\n\n    return server\n\n\n@pytest.fixture\nasync def proxy_server(fastmcp_server):\n    \"\"\"Fixture that creates a FastMCP proxy server.\"\"\"\n    return create_proxy(ProxyClient(transport=FastMCPTransport(fastmcp_server)))\n\n\nasync def test_create_proxy_with_client(fastmcp_server):\n    \"\"\"Test create_proxy with a Client.\"\"\"\n    client = ProxyClient(transport=FastMCPTransport(fastmcp_server))\n    server = create_proxy(client)\n\n    assert isinstance(server, FastMCPProxy)\n    assert isinstance(server, FastMCP)\n    assert server.name.startswith(\"FastMCPProxy-\")\n\n\nasync def test_create_proxy_with_server(fastmcp_server):\n    \"\"\"create_proxy should accept a FastMCP instance.\"\"\"\n    proxy = create_proxy(fastmcp_server)\n    async with Client(proxy) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"Test\"})\n        assert result.data == \"Hello, Test!\"\n\n\nasync def test_create_proxy_with_transport(fastmcp_server):\n    \"\"\"create_proxy should accept a ClientTransport.\"\"\"\n    proxy = create_proxy(FastMCPTransport(fastmcp_server))\n    async with Client(proxy) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"Test\"})\n        assert result.data == \"Hello, Test!\"\n\n\ndef test_create_proxy_with_url():\n    \"\"\"create_proxy should accept a URL without connecting.\"\"\"\n    proxy = create_proxy(\"http://example.com/mcp/\")\n    assert isinstance(proxy, FastMCPProxy)\n    client = cast(Client, proxy.client_factory())\n    assert isinstance(client.transport, StreamableHttpTransport)\n    assert client.transport.url == \"http://example.com/mcp/\"\n\n\n# --- Deprecated as_proxy tests (verify backwards compatibility) ---\n\n\nasync def test_as_proxy_deprecated_with_server(fastmcp_server):\n    \"\"\"FastMCP.as_proxy should work but emit deprecation warning.\"\"\"\n    import warnings\n\n    with warnings.catch_warnings(record=True) as w:\n        warnings.simplefilter(\"always\")\n        proxy = FastMCP.as_proxy(fastmcp_server)\n        assert len(w) == 1\n        assert issubclass(w[0].category, DeprecationWarning)\n        assert \"create_proxy\" in str(w[0].message)\n\n    async with Client(proxy) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"Test\"})\n        assert result.data == \"Hello, Test!\"\n\n\ndef test_as_proxy_deprecated_with_url():\n    \"\"\"FastMCP.as_proxy should work but emit deprecation warning.\"\"\"\n    import warnings\n\n    with warnings.catch_warnings(record=True) as w:\n        warnings.simplefilter(\"always\")\n        proxy = FastMCP.as_proxy(\"http://example.com/mcp/\")\n        assert len(w) == 1\n        assert issubclass(w[0].category, DeprecationWarning)\n\n    assert isinstance(proxy, FastMCPProxy)\n\n\nasync def test_proxy_with_async_client_factory():\n    \"\"\"FastMCPProxy should accept an async client_factory.\"\"\"\n\n    async def async_factory():\n        return Client(\"http://example.com/mcp/\")\n\n    proxy = FastMCPProxy(client_factory=async_factory)\n    assert isinstance(proxy, FastMCPProxy)\n    assert inspect.iscoroutinefunction(proxy.client_factory)\n    client = proxy.client_factory()\n    if inspect.isawaitable(client):\n        client = await client\n    assert isinstance(client, Client)\n    assert isinstance(client.transport, StreamableHttpTransport)\n    assert client.transport.url == \"http://example.com/mcp/\"\n\n\nclass TestTools:\n    async def test_get_tools(self, proxy_server):\n        tools = await proxy_server.list_tools()\n        assert any(t.name == \"greet\" for t in tools)\n        assert any(t.name == \"add\" for t in tools)\n        assert any(t.name == \"error_tool\" for t in tools)\n        assert any(t.name == \"tool_without_description\" for t in tools)\n\n    async def test_get_tools_meta(self, proxy_server):\n        tools = await proxy_server.list_tools()\n        greet_tool = next(t for t in tools if t.name == \"greet\")\n        assert greet_tool.title == \"Greet\"\n        assert greet_tool.meta == {\"fastmcp\": {\"tags\": [\"greet\"]}}\n        assert greet_tool.icons == [Icon(src=\"https://example.com/greet-icon.png\")]\n\n    async def test_get_transformed_tools(self):\n        \"\"\"Test that tool transformations are applied to proxied tools.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n\n        # Create server with transformation\n        server = FastMCP(\"TestServer\")\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers together.\"\"\"\n            return a + b\n\n        server.add_transform(\n            ToolTransform({\"add\": ToolTransformConfig(name=\"add_transformed\")})\n        )\n\n        proxy = create_proxy(server)\n        tools = await proxy.list_tools()\n        assert any(t.name == \"add_transformed\" for t in tools)\n        assert not any(t.name == \"add\" for t in tools)\n\n    async def test_call_transformed_tools(self):\n        \"\"\"Test calling a transformed tool through a proxy.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n\n        # Create server with transformation\n        server = FastMCP(\"TestServer\")\n\n        @server.tool\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers together.\"\"\"\n            return a + b\n\n        server.add_transform(\n            ToolTransform({\"add\": ToolTransformConfig(name=\"add_transformed\")})\n        )\n\n        proxy = create_proxy(server)\n        async with Client(proxy) as client:\n            result = await client.call_tool(\"add_transformed\", {\"a\": 1, \"b\": 2})\n        assert result.data == 3\n\n    async def test_tool_without_description(self, proxy_server):\n        tools = await proxy_server.list_tools()\n        tool = next(t for t in tools if t.name == \"tool_without_description\")\n        assert tool.description is None\n\n    async def test_list_tools_same_as_original(self, fastmcp_server, proxy_server):\n        assert await proxy_server._list_tools_mcp(\n            mcp_types.ListToolsRequest()\n        ) == await fastmcp_server._list_tools_mcp(mcp_types.ListToolsRequest())\n\n    async def test_call_tool_result_same_as_original(\n        self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy\n    ):\n        result = await fastmcp_server._call_tool_mcp(\"greet\", {\"name\": \"Alice\"})\n        proxy_result = await proxy_server._call_tool_mcp(\"greet\", {\"name\": \"Alice\"})\n\n        assert result == proxy_result\n\n    async def test_call_tool_calls_tool(self, proxy_server):\n        async with Client(proxy_server) as client:\n            proxy_result = await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n        assert proxy_result.data == 3\n\n    async def test_error_tool_raises_error(self, proxy_server):\n        with pytest.raises(ToolError, match=\"This is a test error\"):\n            async with Client(proxy_server) as client:\n                await client.call_tool(\"error_tool\", {})\n\n    async def test_call_tool_forwards_meta(self, fastmcp_server, proxy_server):\n        \"\"\"Test that metadata from proxied tool results is properly forwarded.\"\"\"\n\n        @fastmcp_server.tool\n        def tool_with_meta(value: str) -> ToolResult:\n            \"\"\"A tool that returns metadata in its result.\"\"\"\n            return ToolResult(\n                content=f\"Result: {value}\",\n                meta={\"custom_key\": \"custom_value\", \"processed\": True},\n            )\n\n        async with Client(proxy_server) as client:\n            result = await client.call_tool(\"tool_with_meta\", {\"value\": \"test\"})\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Result: test\"\n        assert result.meta == {\"custom_key\": \"custom_value\", \"processed\": True}\n\n    async def test_proxy_can_overwrite_proxied_tool(self, proxy_server):\n        \"\"\"\n        Test that a tool defined on the proxy can overwrite the proxied tool with the same name.\n        \"\"\"\n\n        @proxy_server.tool\n        def greet(name: str, extra: str = \"extra\") -> str:\n            return f\"Overwritten, {name}! {extra}\"\n\n        async with Client(proxy_server) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"Marvin\", \"extra\": \"abc\"})\n        assert result.data == \"Overwritten, Marvin! abc\"\n\n    async def test_proxy_can_list_overwritten_tool(self, proxy_server):\n        \"\"\"\n        Test that a tool defined on the proxy is listed instead of the proxied tool\n        \"\"\"\n\n        @proxy_server.tool\n        def greet(name: str, extra: str = \"extra\") -> str:\n            return f\"Overwritten, {name}! {extra}\"\n\n        async with Client(proxy_server) as client:\n            tools = await client.list_tools()\n            greet_tool = next(t for t in tools if t.name == \"greet\")\n            assert \"extra\" in greet_tool.inputSchema[\"properties\"]\n\n\nclass TestResources:\n    async def test_get_resources(self, proxy_server):\n        resources = await proxy_server.list_resources()\n        assert [r.uri for r in resources] == Contains(\n            AnyUrl(\"data://users\"),\n            AnyUrl(\"resource://wave\"),\n        )\n        assert [r.name for r in resources] == Contains(\"get_users\", \"wave\")\n\n    async def test_get_resources_meta(self, proxy_server):\n        resources = await proxy_server.list_resources()\n        wave_resource = next(r for r in resources if str(r.uri) == \"resource://wave\")\n        assert wave_resource.title == \"Wave\"\n        assert wave_resource.meta == {\"fastmcp\": {\"tags\": [\"wave\"]}}\n        assert wave_resource.icons == [Icon(src=\"https://example.com/wave-icon.png\")]\n\n    async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):\n        assert await proxy_server._list_resources_mcp(\n            mcp_types.ListResourcesRequest()\n        ) == await fastmcp_server._list_resources_mcp(mcp_types.ListResourcesRequest())\n\n    async def test_read_resource(self, proxy_server: FastMCPProxy):\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(\"resource://wave\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"👋\"\n\n    async def test_read_resource_same_as_original(self, fastmcp_server, proxy_server):\n        async with Client(fastmcp_server) as client:\n            result = await client.read_resource(\"resource://wave\")\n        async with Client(proxy_server) as client:\n            proxy_result = await client.read_resource(\"resource://wave\")\n        assert proxy_result == result\n\n    async def test_read_json_resource(self, proxy_server: FastMCPProxy):\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(\"data://users\")\n        assert len(result) == 1\n        assert isinstance(result[0], TextResourceContents)\n        # The resource returns all users serialized as JSON\n        users = json.loads(result[0].text)\n        assert users == USERS\n\n    async def test_proxy_returns_all_resource_contents(\n        self, fastmcp_server, proxy_server\n    ):\n        \"\"\"Test that proxy correctly returns all resource contents, not just the first one.\"\"\"\n        # Read from original server\n        async with Client(fastmcp_server) as client:\n            original_result = await client.read_resource(\"data://multi\")\n\n        # Read from proxy server\n        async with Client(proxy_server) as client:\n            proxy_result = await client.read_resource(\"data://multi\")\n\n        # Both should return the same number of contents\n        assert len(original_result) == len(proxy_result)\n        assert len(original_result) == 3\n\n        # Verify all contents match\n        for i, (original, proxied) in enumerate(zip(original_result, proxy_result)):\n            assert isinstance(original, TextResourceContents)\n            assert isinstance(proxied, TextResourceContents)\n            assert original.text == proxied.text, f\"Content {i} text mismatch\"\n            assert original.mimeType == proxied.mimeType, (\n                f\"Content {i} mimeType mismatch\"\n            )\n            assert original.meta == proxied.meta, f\"Content {i} meta mismatch\"\n\n        # Verify the contents are what we expect\n        assert original_result[0].text == \"First item\"\n        assert original_result[0].mimeType == \"text/plain\"\n        assert original_result[1].text == '{\"key\": \"value\"}'\n        assert original_result[1].mimeType == \"application/json\"\n        assert original_result[2].text == \"# Markdown\\nContent\"\n        assert original_result[2].mimeType == \"text/markdown\"\n\n    async def test_read_resource_returns_none_if_not_found(self, proxy_server):\n        with pytest.raises(\n            McpError, match=\"Unknown resource: 'resource://nonexistent'\"\n        ):\n            async with Client(proxy_server) as client:\n                await client.read_resource(\"resource://nonexistent\")\n\n    async def test_proxy_can_overwrite_proxied_resource(self, proxy_server):\n        \"\"\"\n        Test that a resource defined on the proxy can overwrite the proxied resource with the same URI.\n        \"\"\"\n\n        @proxy_server.resource(uri=\"resource://wave\")\n        def overwritten_wave() -> str:\n            return \"Overwritten wave! 🌊\"\n\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(\"resource://wave\")\n        assert isinstance(result[0], TextResourceContents)\n        assert result[0].text == \"Overwritten wave! 🌊\"\n\n    async def test_proxy_can_list_overwritten_resource(self, proxy_server):\n        \"\"\"\n        Test that a resource defined on the proxy is listed instead of the proxied resource\n        \"\"\"\n\n        @proxy_server.resource(uri=\"resource://wave\", name=\"overwritten_wave\")\n        def overwritten_wave() -> str:\n            return \"Overwritten wave! 🌊\"\n\n        async with Client(proxy_server) as client:\n            resources = await client.list_resources()\n            wave_resource = next(\n                r for r in resources if str(r.uri) == \"resource://wave\"\n            )\n            assert wave_resource.name == \"overwritten_wave\"\n\n\nclass TestResourceTemplates:\n    async def test_get_resource_templates(self, proxy_server):\n        templates = await proxy_server.list_resource_templates()\n        assert [t.name for t in templates] == Contains(\"get_user\")\n\n    async def test_get_resource_templates_meta(self, proxy_server):\n        templates = await proxy_server.list_resource_templates()\n        get_user_template = next(\n            t for t in templates if t.uri_template == \"data://user/{user_id}\"\n        )\n        assert get_user_template.title == \"User Template\"\n        assert get_user_template.meta == {\"fastmcp\": {\"tags\": [\"users\"]}}\n        assert get_user_template.icons == [\n            Icon(src=\"https://example.com/user-icon.png\")\n        ]\n\n    async def test_list_resource_templates_same_as_original(\n        self, fastmcp_server, proxy_server\n    ):\n        result = await fastmcp_server._list_resource_templates_mcp(\n            mcp_types.ListResourceTemplatesRequest()\n        )\n        proxy_result = await proxy_server._list_resource_templates_mcp(\n            mcp_types.ListResourceTemplatesRequest()\n        )\n        assert proxy_result == result\n\n    @pytest.mark.parametrize(\"id\", [1, 2, 3])\n    async def test_read_resource_template(self, proxy_server: FastMCPProxy, id: int):\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(f\"data://user/{id}\")\n        assert isinstance(result[0], TextResourceContents)\n        assert json.loads(result[0].text) == USERS[id - 1]\n\n    async def test_read_resource_template_same_as_original(\n        self, fastmcp_server, proxy_server\n    ):\n        async with Client(fastmcp_server) as client:\n            result = await client.read_resource(\"data://user/1\")\n        async with Client(proxy_server) as client:\n            proxy_result = await client.read_resource(\"data://user/1\")\n        assert proxy_result == result\n\n    async def test_proxy_template_returns_all_resource_contents(\n        self, fastmcp_server, proxy_server\n    ):\n        \"\"\"Test that proxy template correctly returns all resource contents.\"\"\"\n        # Read from original server\n        async with Client(fastmcp_server) as client:\n            original_result = await client.read_resource(\"data://multi/test123\")\n\n        # Read from proxy server\n        async with Client(proxy_server) as client:\n            proxy_result = await client.read_resource(\"data://multi/test123\")\n\n        # Both should return the same number of contents\n        assert len(original_result) == len(proxy_result)\n        assert len(original_result) == 2\n\n        # Verify all contents match\n        for i, (original, proxied) in enumerate(zip(original_result, proxy_result)):\n            assert isinstance(original, TextResourceContents)\n            assert isinstance(proxied, TextResourceContents)\n            assert original.text == proxied.text, f\"Content {i} text mismatch\"\n            assert original.mimeType == proxied.mimeType, (\n                f\"Content {i} mimeType mismatch\"\n            )\n\n        # Verify the contents are what we expect\n        assert original_result[0].text == \"Item test123 - First\"\n        assert original_result[0].mimeType == \"text/plain\"\n        assert original_result[1].text == '{\"id\": \"test123\", \"status\": \"active\"}'\n        assert original_result[1].mimeType == \"application/json\"\n\n    async def test_proxy_can_overwrite_proxied_resource_template(self, proxy_server):\n        \"\"\"\n        Test that a resource template defined on the proxy can overwrite the proxied template with the same URI template.\n        \"\"\"\n\n        @proxy_server.resource(uri=\"data://user/{user_id}\", name=\"overwritten_get_user\")\n        def overwritten_get_user(user_id: str) -> str:\n            return json.dumps(\n                {\n                    \"id\": user_id,\n                    \"name\": \"Overwritten User\",\n                    \"active\": True,\n                    \"extra\": \"data\",\n                }\n            )\n\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(\"data://user/1\")\n        assert isinstance(result[0], TextResourceContents)\n        user_data = json.loads(result[0].text)\n        assert user_data[\"name\"] == \"Overwritten User\"\n        assert user_data[\"extra\"] == \"data\"\n\n    async def test_proxy_can_list_overwritten_resource_template(self, proxy_server):\n        \"\"\"\n        Test that a resource template defined on the proxy is listed instead of the proxied template\n        \"\"\"\n\n        @proxy_server.resource(uri=\"data://user/{user_id}\", name=\"overwritten_get_user\")\n        def overwritten_get_user(user_id: str) -> dict[str, Any]:\n            return {\"id\": user_id, \"name\": \"Overwritten User\", \"active\": True}\n\n        async with Client(proxy_server) as client:\n            templates = await client.list_resource_templates()\n            user_template = next(\n                t for t in templates if t.uriTemplate == \"data://user/{user_id}\"\n            )\n            assert user_template.name == \"overwritten_get_user\"\n\n\nclass TestPrompts:\n    async def test_get_prompts_server_method(self, proxy_server: FastMCPProxy):\n        prompts = await proxy_server.list_prompts()\n        assert [p.name for p in prompts] == Contains(\"welcome\")\n\n    async def test_get_prompts_meta(self, proxy_server):\n        prompts = await proxy_server.list_prompts()\n        welcome_prompt = next(p for p in prompts if p.name == \"welcome\")\n        assert welcome_prompt.title == \"Welcome\"\n        assert welcome_prompt.meta == {\"fastmcp\": {\"tags\": [\"welcome\"]}}\n        assert welcome_prompt.icons == [\n            Icon(src=\"https://example.com/welcome-icon.png\")\n        ]\n\n    async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server):\n        async with Client(fastmcp_server) as client:\n            result = await client.list_prompts()\n        async with Client(proxy_server) as client:\n            proxy_result = await client.list_prompts()\n        assert proxy_result == result\n\n    async def test_render_prompt_same_as_original(\n        self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy\n    ):\n        async with Client(fastmcp_server) as client:\n            result = await client.get_prompt(\"welcome\", {\"name\": \"Alice\"})\n        async with Client(proxy_server) as client:\n            proxy_result = await client.get_prompt(\"welcome\", {\"name\": \"Alice\"})\n        assert proxy_result == result\n\n    async def test_render_prompt_calls_prompt(self, proxy_server):\n        async with Client(proxy_server) as client:\n            result = await client.get_prompt(\"welcome\", {\"name\": \"Alice\"})\n        assert result.messages[0].role == \"user\"\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Welcome to FastMCP, Alice!\"\n\n    async def test_proxy_can_overwrite_proxied_prompt(self, proxy_server):\n        \"\"\"\n        Test that a prompt defined on the proxy can overwrite the proxied prompt with the same name.\n        \"\"\"\n\n        @proxy_server.prompt\n        def welcome(name: str, extra: str = \"friend\") -> str:\n            return f\"Overwritten welcome, {name}! You are my {extra}.\"\n\n        async with Client(proxy_server) as client:\n            result = await client.get_prompt(\n                \"welcome\", {\"name\": \"Alice\", \"extra\": \"colleague\"}\n            )\n        assert result.messages[0].role == \"user\"\n        assert isinstance(result.messages[0].content, TextContent)\n        assert (\n            result.messages[0].content.text\n            == \"Overwritten welcome, Alice! You are my colleague.\"\n        )\n\n    async def test_proxy_can_list_overwritten_prompt(self, proxy_server):\n        \"\"\"\n        Test that a prompt defined on the proxy is listed instead of the proxied prompt\n        \"\"\"\n\n        @proxy_server.prompt\n        def welcome(name: str, extra: str = \"friend\") -> str:\n            return f\"Overwritten welcome, {name}! You are my {extra}.\"\n\n        async with Client(proxy_server) as client:\n            prompts = await client.list_prompts()\n            welcome_prompt = next(p for p in prompts if p.name == \"welcome\")\n            # Check that the overwritten prompt has the additional 'extra' parameter\n            param_names = [arg.name for arg in welcome_prompt.arguments or []]\n            assert \"extra\" in param_names\n\n    async def test_proxy_prompt_preserves_image_content(\n        self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy\n    ):\n        \"\"\"Test that ProxyPrompt preserves ImageContent without lossy conversion.\"\"\"\n        async with Client(fastmcp_server) as client:\n            result = await client.get_prompt(\"image_prompt\")\n        async with Client(proxy_server) as client:\n            proxy_result = await client.get_prompt(\"image_prompt\")\n\n        # The proxy result should match the original exactly\n        assert proxy_result == result\n        # Verify the image content is preserved as ImageContent, not JSON text\n        assert isinstance(proxy_result.messages[1].content, mcp_types.ImageContent)\n        assert proxy_result.messages[1].content.data == \"iVBORw0KGgoAAAANSUhEUg==\"\n        assert proxy_result.messages[1].content.mimeType == \"image/png\"\n\n\nasync def test_proxy_handles_multiple_concurrent_tasks_correctly(\n    proxy_server: FastMCPProxy,\n):\n    results = {}\n\n    async def get_and_store(name, coro):\n        results[name] = await coro()\n\n    async with create_task_group() as tg:\n        tg.start_soon(get_and_store, \"prompts\", proxy_server.list_prompts)\n        tg.start_soon(get_and_store, \"resources\", proxy_server.list_resources)\n        tg.start_soon(get_and_store, \"tools\", proxy_server.list_tools)\n\n    assert list(results) == Contains(\"resources\", \"prompts\", \"tools\")\n    assert [p.name for p in results[\"prompts\"]] == Contains(\"welcome\")\n    assert [r.uri for r in results[\"resources\"]] == Contains(\n        AnyUrl(\"data://users\"),\n        AnyUrl(\"resource://wave\"),\n    )\n    assert [r.name for r in results[\"resources\"]] == Contains(\"get_users\", \"wave\")\n    assert [t.name for t in results[\"tools\"]] == Contains(\n        \"greet\", \"add\", \"error_tool\", \"tool_without_description\"\n    )\n\n\nclass TestProxyComponentEnableDisable:\n    \"\"\"Test that enable/disable on proxy components guides users to server-level methods.\"\"\"\n\n    async def test_proxy_tool_enable_raises_not_implemented(self, proxy_server):\n        \"\"\"Test that enable() on proxy tools raises NotImplementedError.\"\"\"\n        tools = await proxy_server.list_tools()\n        tool = next(t for t in tools if t.name == \"greet\")\n\n        with pytest.raises(NotImplementedError, match=\"server.enable\"):\n            tool.enable()\n\n    async def test_proxy_tool_disable_raises_not_implemented(self, proxy_server):\n        \"\"\"Test that disable() on proxy tools raises NotImplementedError.\"\"\"\n        tools = await proxy_server.list_tools()\n        tool = next(t for t in tools if t.name == \"greet\")\n\n        with pytest.raises(NotImplementedError, match=\"server.disable\"):\n            tool.disable()\n\n    async def test_proxy_resource_enable_raises_not_implemented(self, proxy_server):\n        \"\"\"Test that enable() on proxy resources raises NotImplementedError.\"\"\"\n        resources = await proxy_server.list_resources()\n        resource = next(r for r in resources if str(r.uri) == \"resource://wave\")\n\n        with pytest.raises(NotImplementedError, match=\"server.enable\"):\n            resource.enable()\n\n    async def test_proxy_resource_disable_raises_not_implemented(self, proxy_server):\n        \"\"\"Test that disable() on proxy resources raises NotImplementedError.\"\"\"\n        resources = await proxy_server.list_resources()\n        resource = next(r for r in resources if str(r.uri) == \"resource://wave\")\n\n        with pytest.raises(NotImplementedError, match=\"server.disable\"):\n            resource.disable()\n\n    async def test_proxy_prompt_enable_raises_not_implemented(self, proxy_server):\n        \"\"\"Test that enable() on proxy prompts raises NotImplementedError.\"\"\"\n        prompts = await proxy_server.list_prompts()\n        prompt = next(p for p in prompts if p.name == \"welcome\")\n\n        with pytest.raises(NotImplementedError, match=\"server.enable\"):\n            prompt.enable()\n\n    async def test_proxy_prompt_disable_raises_not_implemented(self, proxy_server):\n        \"\"\"Test that disable() on proxy prompts raises NotImplementedError.\"\"\"\n        prompts = await proxy_server.list_prompts()\n        prompt = next(p for p in prompts if p.name == \"welcome\")\n\n        with pytest.raises(NotImplementedError, match=\"server.disable\"):\n            prompt.disable()\n\n\nclass TestProxyProviderCache:\n    \"\"\"Tests for the ProxyProvider component list caching.\"\"\"\n\n    async def test_get_tool_uses_cached_list(self, fastmcp_server):\n        \"\"\"Calling call_tool should resolve from cache after an initial list.\"\"\"\n        provider = ProxyProvider(\n            lambda: ProxyClient(FastMCPTransport(fastmcp_server)),\n        )\n        # Warm the cache via list\n        tools = await provider.list_tools()\n        assert any(t.name == \"greet\" for t in tools)\n\n        # _get_tool should resolve from cache without calling _list_tools again\n        with patch.object(\n            provider, \"_get_client\", new_callable=AsyncMock\n        ) as mock_client:\n            tool = await provider._get_tool(\"greet\")\n            assert tool is not None\n            assert tool.name == \"greet\"\n            mock_client.assert_not_called()\n\n    async def test_get_tool_fetches_on_cold_cache(self, fastmcp_server):\n        \"\"\"First _get_tool with no prior list should populate the cache.\"\"\"\n        provider = ProxyProvider(\n            lambda: ProxyClient(FastMCPTransport(fastmcp_server)),\n        )\n        assert provider._tools_cache is None\n        tool = await provider._get_tool(\"greet\")\n        assert tool is not None\n        assert provider._tools_cache is not None\n\n    async def test_cache_expires_after_ttl(self, fastmcp_server):\n        \"\"\"After TTL expires, _get_tool should re-fetch from the backend.\"\"\"\n        provider = ProxyProvider(\n            lambda: ProxyClient(FastMCPTransport(fastmcp_server)),\n            cache_ttl=0.0,\n        )\n        # Warm the cache\n        await provider._list_tools()\n        # With ttl=0 the cache is immediately stale, so _get_tool must re-fetch\n        assert provider._tools_cache is not None\n        original_ts = provider._tools_cache.timestamp\n\n        time.sleep(0.01)\n\n        await provider._get_tool(\"greet\")\n        assert provider._tools_cache.timestamp > original_ts\n\n    async def test_list_tools_refreshes_cache(self, fastmcp_server):\n        \"\"\"Explicit list_tools always refreshes the cache timestamp.\"\"\"\n        provider = ProxyProvider(\n            lambda: ProxyClient(FastMCPTransport(fastmcp_server)),\n        )\n        await provider._list_tools()\n        first_ts = provider._tools_cache.timestamp  # type: ignore[union-attr]\n\n        # Tiny sleep so monotonic clock advances\n        time.sleep(0.01)\n\n        await provider._list_tools()\n        assert provider._tools_cache.timestamp > first_ts  # type: ignore[union-attr]\n\n    async def test_cache_ttl_zero_disables_caching(self, fastmcp_server):\n        \"\"\"With cache_ttl=0, every _get_tool call should re-fetch.\"\"\"\n        provider = ProxyProvider(\n            lambda: ProxyClient(FastMCPTransport(fastmcp_server)),\n            cache_ttl=0.0,\n        )\n        # Each _get_tool call should trigger a fresh _list_tools\n        call_count = 0\n        original_list = provider._list_tools\n\n        async def counting_list():\n            nonlocal call_count\n            call_count += 1\n            return await original_list()\n\n        with patch.object(provider, \"_list_tools\", side_effect=counting_list):\n            await provider._get_tool(\"greet\")\n            await provider._get_tool(\"add\")\n        assert call_count == 2\n\n    async def test_get_resource_uses_cache(self, fastmcp_server):\n        \"\"\"Resource lookups should also use the cache.\"\"\"\n        provider = ProxyProvider(\n            lambda: ProxyClient(FastMCPTransport(fastmcp_server)),\n        )\n        await provider._list_resources()\n        with patch.object(\n            provider, \"_get_client\", new_callable=AsyncMock\n        ) as mock_client:\n            # Even if no resources match, the cache is used (no backend call)\n            await provider._get_resource(\"config://app\")\n            mock_client.assert_not_called()\n\n    async def test_call_tool_through_server_uses_cache(self, fastmcp_server):\n        \"\"\"End-to-end: calling a tool on a proxy server should only connect\n        for the actual tool execution, not for tool resolution.\"\"\"\n        proxy = create_proxy(fastmcp_server)\n        # Warm the cache by listing\n        await proxy.list_tools()\n\n        # Now call a tool — the provider's _list_tools should NOT be called\n        # because the cache is warm. The connection happens only in ProxyTool.run.\n        proxy_provider = next(\n            p for p in proxy.providers if isinstance(p, ProxyProvider)\n        )\n        with patch.object(\n            proxy_provider, \"_list_tools\", wraps=proxy_provider._list_tools\n        ) as mock_list:\n            result = await proxy.call_tool(\"greet\", {\"name\": \"Alice\"})\n            mock_list.assert_not_called()\n        assert result.content[0].text == \"Hello, Alice!\"  # type: ignore[union-attr]\n"
  },
  {
    "path": "tests/server/providers/proxy/test_stateful_proxy_client.py",
    "content": "import asyncio\nfrom dataclasses import dataclass\n\nimport pytest\nfrom anyio import create_task_group\nfrom mcp.types import LoggingLevel\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.client.logging import LogMessage\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.elicitation import AcceptedElicitation\nfrom fastmcp.server.providers.proxy import FastMCPProxy, StatefulProxyClient\nfrom fastmcp.utilities.tests import find_available_port, run_server_async\n\n\n@pytest.fixture\ndef fastmcp_server():\n    mcp = FastMCP(\"TestServer\")\n\n    states: dict[int, int] = {}\n\n    @mcp.tool\n    async def log(\n        message: str, level: LoggingLevel, logger: str, context: Context\n    ) -> None:\n        await context.log(message=message, level=level, logger_name=logger)\n\n    @mcp.tool\n    async def stateful_put(value: int, context: Context) -> None:\n        \"\"\"put a value associated with the server session\"\"\"\n        key = id(context.session)\n        states[key] = value\n\n    @mcp.tool\n    async def stateful_get(context: Context) -> int:\n        \"\"\"get the value associated with the server session\"\"\"\n        key = id(context.session)\n        try:\n            return states[key]\n        except KeyError:\n            raise ToolError(\"Value not found\")\n\n    return mcp\n\n\n@pytest.fixture\nasync def stateful_proxy_server(fastmcp_server: FastMCP):\n    client = StatefulProxyClient(transport=FastMCPTransport(fastmcp_server))\n    return FastMCPProxy(client_factory=client.new_stateful)\n\n\n@pytest.fixture\nasync def stateless_server(stateful_proxy_server: FastMCP):\n    port = find_available_port()\n    url = f\"http://127.0.0.1:{port}/mcp/\"\n\n    task = asyncio.create_task(\n        stateful_proxy_server.run_http_async(\n            host=\"127.0.0.1\", port=port, stateless_http=True\n        )\n    )\n    await stateful_proxy_server._started.wait()\n    yield url\n    task.cancel()\n    try:\n        await task\n    except asyncio.CancelledError:\n        pass\n\n\nclass TestStatefulProxyClient:\n    async def test_concurrent_log_requests_no_mixing(\n        self, stateful_proxy_server: FastMCP\n    ):\n        \"\"\"Test that concurrent log requests don't mix handlers (fixes #1068).\"\"\"\n        results: dict[str, LogMessage] = {}\n\n        async def log_handler_a(message: LogMessage) -> None:\n            results[\"logger_a\"] = message\n\n        async def log_handler_b(message: LogMessage) -> None:\n            results[\"logger_b\"] = message\n\n        async with (\n            Client(stateful_proxy_server, log_handler=log_handler_a) as client_a,\n            Client(stateful_proxy_server, log_handler=log_handler_b) as client_b,\n        ):\n            async with create_task_group() as tg:\n                tg.start_soon(\n                    client_a.call_tool,\n                    \"log\",\n                    {\"message\": \"Hello, world!\", \"level\": \"info\", \"logger\": \"a\"},\n                )\n                tg.start_soon(\n                    client_b.call_tool,\n                    \"log\",\n                    {\"message\": \"Hello, world!\", \"level\": \"info\", \"logger\": \"b\"},\n                )\n\n        assert results[\"logger_a\"].logger == \"a\"\n        assert results[\"logger_b\"].logger == \"b\"\n\n    async def test_stateful_proxy(self, stateful_proxy_server: FastMCP):\n        \"\"\"Test that the state shared across multiple calls for the same client (fixes #959).\"\"\"\n        async with Client(stateful_proxy_server) as client:\n            with pytest.raises(ToolError, match=\"Value not found\"):\n                await client.call_tool(\"stateful_get\", {})\n\n            await client.call_tool(\"stateful_put\", {\"value\": 1})\n            result = await client.call_tool(\"stateful_get\", {})\n            assert result.data == 1\n\n    async def test_stateless_proxy(self, stateless_server: str):\n        \"\"\"Test that the state will not be shared across different calls,\n        even if they are from the same client.\"\"\"\n        async with Client(stateless_server) as client:\n            await client.call_tool(\"stateful_put\", {\"value\": 1})\n\n            with pytest.raises(ToolError, match=\"Value not found\"):\n                await client.call_tool(\"stateful_get\", {})\n\n    async def test_multi_proxies_no_mixing(self):\n        \"\"\"Test that the stateful proxy client won't be mixed in multi-proxies sessions.\"\"\"\n        mcp_a, mcp_b = FastMCP(), FastMCP()\n\n        @mcp_a.tool\n        def tool_a() -> str:\n            return \"a\"\n\n        @mcp_b.tool\n        def tool_b() -> str:\n            return \"b\"\n\n        proxy_mcp_a = FastMCPProxy(\n            client_factory=StatefulProxyClient(mcp_a).new_stateful\n        )\n        proxy_mcp_b = FastMCPProxy(\n            client_factory=StatefulProxyClient(mcp_b).new_stateful\n        )\n        multi_proxy_mcp = FastMCP()\n        multi_proxy_mcp.mount(proxy_mcp_a, namespace=\"a\")\n        multi_proxy_mcp.mount(proxy_mcp_b, namespace=\"b\")\n\n        async with Client(multi_proxy_mcp) as client:\n            result_a = await client.call_tool(\"a_tool_a\", {})\n            result_b = await client.call_tool(\"b_tool_b\", {})\n            assert result_a.data == \"a\"\n            assert result_b.data == \"b\"\n\n    @pytest.mark.timeout(10)\n    async def test_stateful_proxy_elicitation_over_http(self):\n        \"\"\"Elicitation through a stateful proxy over HTTP must not hang.\n\n        When StatefulProxyClient reuses a session, the receive-loop task\n        inherits a stale request_ctx ContextVar from the first request.\n        The streamable-HTTP transport uses related_request_id to route\n        server-initiated messages (like elicitation) back to the correct\n        HTTP response stream.  A stale request_id routes to a closed\n        stream, causing the elicitation to hang forever.\n\n        This test runs the proxy over HTTP (not in-process) so the\n        transport's related_request_id routing is exercised.\n        \"\"\"\n\n        @dataclass\n        class Person:\n            name: str\n\n        backend = FastMCP(\"backend\")\n\n        @backend.tool\n        async def ask_name(ctx: Context) -> str:\n            result = await ctx.elicit(\"What is your name?\", response_type=Person)\n            if isinstance(result, AcceptedElicitation):\n                assert isinstance(result.data, Person)\n                return f\"Hello, {result.data.name}!\"\n            return \"declined\"\n\n        stateful_client = StatefulProxyClient(backend)\n        proxy = FastMCPProxy(\n            client_factory=stateful_client.new_stateful,\n            name=\"proxy\",\n        )\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content=response_type(name=\"Alice\"))\n\n        # Run the proxy over HTTP so the transport uses\n        # related_request_id routing for server-initiated messages.\n        async with run_server_async(proxy) as proxy_url:\n            async with Client(\n                proxy_url, elicitation_handler=elicitation_handler\n            ) as client:\n                result1 = await client.call_tool(\"ask_name\", {})\n                assert result1.data == \"Hello, Alice!\"\n                # Second call reuses the stateful session — this is the\n                # one that would hang without the fix.\n                result2 = await client.call_tool(\"ask_name\", {})\n                assert result2.data == \"Hello, Alice!\"\n"
  },
  {
    "path": "tests/server/providers/test_base_provider.py",
    "content": "\"\"\"Tests for base Provider class behavior.\"\"\"\n\nfrom typing import Any\n\nfrom fastmcp.server.providers.base import Provider\nfrom fastmcp.server.tasks.config import TaskConfig\nfrom fastmcp.server.transforms import Namespace\nfrom fastmcp.tools.base import Tool, ToolResult\n\n\nclass CustomTool(Tool):\n    \"\"\"A custom Tool subclass (not FunctionTool) with task support.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"optional\")\n    parameters: dict[str, Any] = {\"type\": \"object\", \"properties\": {}}\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        return ToolResult(content=\"custom result\")\n\n\nclass SimpleProvider(Provider):\n    \"\"\"Minimal provider that returns custom components from list methods.\"\"\"\n\n    def __init__(self, tools: list[Tool] | None = None):\n        super().__init__()\n        self._tools = tools or []\n\n    async def _list_tools(self) -> list[Tool]:\n        return self._tools\n\n\nclass TestBaseProviderGetTasks:\n    \"\"\"Tests for Provider.get_tasks() base implementation.\"\"\"\n\n    async def test_get_tasks_includes_custom_tool_subclasses(self):\n        \"\"\"Base Provider.get_tasks() should include custom Tool subclasses.\"\"\"\n        custom_tool = CustomTool(name=\"custom\", description=\"A custom tool\")\n        provider = SimpleProvider(tools=[custom_tool])\n\n        tasks = await provider.get_tasks()\n\n        assert len(tasks) == 1\n        assert tasks[0].name == \"custom\"\n        assert tasks[0] is custom_tool\n\n    async def test_get_tasks_filters_forbidden_custom_tools(self):\n        \"\"\"Base Provider.get_tasks() should exclude tools with forbidden task mode.\"\"\"\n\n        class ForbiddenTool(Tool):\n            task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n            parameters: dict[str, Any] = {\"type\": \"object\", \"properties\": {}}\n\n            async def run(self, arguments: dict[str, Any]) -> ToolResult:\n                return ToolResult(content=\"forbidden\")\n\n        forbidden_tool = ForbiddenTool(name=\"forbidden\", description=\"Forbidden tool\")\n        provider = SimpleProvider(tools=[forbidden_tool])\n\n        tasks = await provider.get_tasks()\n\n        assert len(tasks) == 0\n\n    async def test_get_tasks_mixed_custom_and_forbidden(self):\n        \"\"\"Base Provider.get_tasks() filters correctly with mixed task modes.\"\"\"\n\n        class ForbiddenTool(Tool):\n            task_config: TaskConfig = TaskConfig(mode=\"forbidden\")\n            parameters: dict[str, Any] = {\"type\": \"object\", \"properties\": {}}\n\n            async def run(self, arguments: dict[str, Any]) -> ToolResult:\n                return ToolResult(content=\"forbidden\")\n\n        enabled_tool = CustomTool(name=\"enabled\", description=\"Task enabled\")\n        forbidden_tool = ForbiddenTool(name=\"forbidden\", description=\"Task forbidden\")\n        provider = SimpleProvider(tools=[enabled_tool, forbidden_tool])\n\n        tasks = await provider.get_tasks()\n\n        assert len(tasks) == 1\n        assert tasks[0].name == \"enabled\"\n\n    async def test_get_tasks_applies_transforms(self):\n        \"\"\"get_tasks should apply provider transforms to component names.\"\"\"\n        tool = CustomTool(name=\"my_tool\", description=\"A tool\")\n        provider = SimpleProvider(tools=[tool])\n        provider.add_transform(Namespace(\"api\"))\n\n        tasks = await provider.get_tasks()\n\n        assert len(tasks) == 1\n        assert tasks[0].name == \"api_my_tool\"\n"
  },
  {
    "path": "tests/server/providers/test_fastmcp_provider.py",
    "content": "\"\"\"Tests for FastMCPProvider.\"\"\"\n\nimport mcp.types as mt\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.prompts.base import PromptResult\nfrom fastmcp.resources.base import ResourceResult\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.tools.base import ToolResult\n\n\nclass ToolTracingMiddleware(Middleware):\n    \"\"\"Middleware that traces tool calls.\"\"\"\n\n    def __init__(self, name: str, calls: list[str]):\n        super().__init__()\n        self._name = name\n        self._calls = calls\n\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mt.CallToolRequestParams],\n        call_next: CallNext[mt.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        self._calls.append(f\"{self._name}:before\")\n        result = await call_next(context)\n        self._calls.append(f\"{self._name}:after\")\n        return result\n\n\nclass ResourceTracingMiddleware(Middleware):\n    \"\"\"Middleware that traces resource reads.\"\"\"\n\n    def __init__(self, name: str, calls: list[str]):\n        super().__init__()\n        self._name = name\n        self._calls = calls\n\n    async def on_read_resource(\n        self,\n        context: MiddlewareContext[mt.ReadResourceRequestParams],\n        call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult],\n    ) -> ResourceResult:\n        self._calls.append(f\"{self._name}:before\")\n        result = await call_next(context)\n        self._calls.append(f\"{self._name}:after\")\n        return result\n\n\nclass PromptTracingMiddleware(Middleware):\n    \"\"\"Middleware that traces prompt gets.\"\"\"\n\n    def __init__(self, name: str, calls: list[str]):\n        super().__init__()\n        self._name = name\n        self._calls = calls\n\n    async def on_get_prompt(\n        self,\n        context: MiddlewareContext[mt.GetPromptRequestParams],\n        call_next: CallNext[mt.GetPromptRequestParams, PromptResult],\n    ) -> PromptResult:\n        self._calls.append(f\"{self._name}:before\")\n        result = await call_next(context)\n        self._calls.append(f\"{self._name}:after\")\n        return result\n\n\nclass TestToolOperations:\n    \"\"\"Test tool operations through FastMCPProvider.\"\"\"\n\n    async def test_list_tools(self):\n        \"\"\"Test listing tools from wrapped server.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def tool_one() -> str:\n            return \"one\"\n\n        @server.tool\n        def tool_two() -> str:\n            return \"two\"\n\n        provider = FastMCPProvider(server)\n        tools = await provider.list_tools()\n\n        assert len(tools) == 2\n        names = {t.name for t in tools}\n        assert names == {\"tool_one\", \"tool_two\"}\n\n    async def test_get_tool(self):\n        \"\"\"Test getting a specific tool by name.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        tool = await provider.get_tool(\"my_tool\")\n\n        assert tool is not None\n        assert tool.name == \"my_tool\"\n\n    async def test_get_nonexistent_tool_returns_none(self):\n        \"\"\"Test that getting a nonexistent tool returns None.\"\"\"\n        server = FastMCP(\"Test\")\n        provider = FastMCPProvider(server)\n\n        tool = await provider.get_tool(\"nonexistent\")\n        assert tool is None\n\n    async def test_call_tool_via_client(self):\n        \"\"\"Test calling a tool through a server using the provider.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        main = FastMCP(\"Main\")\n        main.add_provider(FastMCPProvider(sub))\n\n        async with Client(main) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert result.data == \"Hello, World!\"\n\n\nclass TestResourceOperations:\n    \"\"\"Test resource operations through FastMCPProvider.\"\"\"\n\n    async def test_list_resources(self):\n        \"\"\"Test listing resources from wrapped server.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://one\")\n        def resource_one() -> str:\n            return \"one\"\n\n        @server.resource(\"resource://two\")\n        def resource_two() -> str:\n            return \"two\"\n\n        provider = FastMCPProvider(server)\n        resources = await provider.list_resources()\n\n        assert len(resources) == 2\n        uris = {str(r.uri) for r in resources}\n        assert uris == {\"resource://one\", \"resource://two\"}\n\n    async def test_get_resource(self):\n        \"\"\"Test getting a specific resource by URI.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://data\")\n        def my_resource() -> str:\n            return \"content\"\n\n        provider = FastMCPProvider(server)\n        resource = await provider.get_resource(\"resource://data\")\n\n        assert resource is not None\n        assert str(resource.uri) == \"resource://data\"\n\n    async def test_read_resource_via_client(self):\n        \"\"\"Test reading a resource through a server using the provider.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"resource://data\")\n        def my_resource() -> str:\n            return \"content\"\n\n        main = FastMCP(\"Main\")\n        main.add_provider(FastMCPProvider(sub))\n\n        async with Client(main) as client:\n            result = await client.read_resource(\"resource://data\")\n            assert isinstance(result[0], mt.TextResourceContents)\n            assert result[0].text == \"content\"\n\n\nclass TestResourceTemplateOperations:\n    \"\"\"Test resource template operations through FastMCPProvider.\"\"\"\n\n    async def test_list_resource_templates(self):\n        \"\"\"Test listing resource templates from wrapped server.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://{id}/data\")\n        def my_template(id: str) -> str:\n            return f\"data for {id}\"\n\n        provider = FastMCPProvider(server)\n        templates = await provider.list_resource_templates()\n\n        assert len(templates) == 1\n        assert templates[0].uri_template == \"resource://{id}/data\"\n\n    async def test_get_resource_template(self):\n        \"\"\"Test getting a template that matches a URI.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://{id}/data\")\n        def my_template(id: str) -> str:\n            return f\"data for {id}\"\n\n        provider = FastMCPProvider(server)\n        template = await provider.get_resource_template(\"resource://123/data\")\n\n        assert template is not None\n\n    async def test_read_resource_template_via_client(self):\n        \"\"\"Test reading a resource via template through a server using the provider.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"resource://{id}/data\")\n        def my_template(id: str) -> str:\n            return f\"data for {id}\"\n\n        main = FastMCP(\"Main\")\n        main.add_provider(FastMCPProvider(sub))\n\n        async with Client(main) as client:\n            result = await client.read_resource(\"resource://123/data\")\n            assert isinstance(result[0], mt.TextResourceContents)\n            assert result[0].text == \"data for 123\"\n\n\nclass TestPromptOperations:\n    \"\"\"Test prompt operations through FastMCPProvider.\"\"\"\n\n    async def test_list_prompts(self):\n        \"\"\"Test listing prompts from wrapped server.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.prompt\n        def prompt_one() -> str:\n            return \"one\"\n\n        @server.prompt\n        def prompt_two() -> str:\n            return \"two\"\n\n        provider = FastMCPProvider(server)\n        prompts = await provider.list_prompts()\n\n        assert len(prompts) == 2\n        names = {p.name for p in prompts}\n        assert names == {\"prompt_one\", \"prompt_two\"}\n\n    async def test_get_prompt(self):\n        \"\"\"Test getting a specific prompt by name.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.prompt\n        def my_prompt() -> str:\n            return \"content\"\n\n        provider = FastMCPProvider(server)\n        prompt = await provider.get_prompt(\"my_prompt\")\n\n        assert prompt is not None\n        assert prompt.name == \"my_prompt\"\n\n    async def test_render_prompt_via_client(self):\n        \"\"\"Test rendering a prompt through a server using the provider.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.prompt\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        main = FastMCP(\"Main\")\n        main.add_provider(FastMCPProvider(sub))\n\n        async with Client(main) as client:\n            result = await client.get_prompt(\"greet\", {\"name\": \"World\"})\n            assert isinstance(result.messages[0].content, mt.TextContent)\n            assert result.messages[0].content.text == \"Hello, World!\"\n\n\nclass TestServerReference:\n    \"\"\"Test that provider maintains reference to wrapped server.\"\"\"\n\n    def test_server_attribute(self):\n        \"\"\"Test that provider exposes the wrapped server.\"\"\"\n        server = FastMCP(\"Test\")\n        provider = FastMCPProvider(server)\n\n        assert provider.server is server\n\n    def test_server_name_accessible(self):\n        \"\"\"Test that server name is accessible through provider.\"\"\"\n        server = FastMCP(\"MyServer\")\n        provider = FastMCPProvider(server)\n\n        assert provider.server.name == \"MyServer\"\n\n\nclass TestMiddlewareChain:\n    \"\"\"Test that middleware runs at each level of mounted servers.\"\"\"\n\n    async def test_tool_middleware_three_levels(self):\n        \"\"\"Middleware runs at parent, child, and grandchild levels for tools.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.tool\n        async def compute(x: int) -> int:\n            calls.append(\"grandchild:tool\")\n            return x * 2\n\n        grandchild.add_middleware(ToolTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(ToolTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(ToolTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"c_gc_compute\", {\"x\": 5})\n            assert result.data == 10\n\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:tool\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n        ]\n\n    async def test_resource_middleware_three_levels(self):\n        \"\"\"Middleware runs at parent, child, and grandchild levels for resources.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.resource(\"data://value\")\n        async def get_data() -> str:\n            calls.append(\"grandchild:resource\")\n            return \"result\"\n\n        grandchild.add_middleware(ResourceTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(ResourceTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(ResourceTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            result = await client.read_resource(\"data://c/gc/value\")\n            assert isinstance(result[0], mt.TextResourceContents)\n            assert result[0].text == \"result\"\n\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:resource\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n        ]\n\n    async def test_prompt_middleware_three_levels(self):\n        \"\"\"Middleware runs at parent, child, and grandchild levels for prompts.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.prompt\n        async def greet(name: str) -> str:\n            calls.append(\"grandchild:prompt\")\n            return f\"Hello, {name}!\"\n\n        grandchild.add_middleware(PromptTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(PromptTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(PromptTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            result = await client.get_prompt(\"c_gc_greet\", {\"name\": \"World\"})\n            assert isinstance(result.messages[0].content, mt.TextContent)\n            assert result.messages[0].content.text == \"Hello, World!\"\n\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:prompt\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n        ]\n\n    async def test_resource_template_middleware_three_levels(self):\n        \"\"\"Middleware runs at all levels for resource templates.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.resource(\"item://{id}\")\n        async def get_item(id: str) -> str:\n            calls.append(\"grandchild:template\")\n            return f\"item-{id}\"\n\n        grandchild.add_middleware(ResourceTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(ResourceTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(ResourceTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            result = await client.read_resource(\"item://c/gc/42\")\n            assert isinstance(result[0], mt.TextResourceContents)\n            assert result[0].text == \"item-42\"\n\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:template\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n        ]\n"
  },
  {
    "path": "tests/server/providers/test_local_provider.py",
    "content": "\"\"\"Comprehensive tests for LocalProvider.\n\nTests cover:\n- Storage operations (add/remove tools, resources, templates, prompts)\n- Provider interface (list/get operations)\n- Decorator patterns (all calling styles)\n- Tool transformations\n- Standalone usage (provider attached to multiple servers)\n- Task registration\n\"\"\"\n\nfrom typing import Any\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.server.providers.local_provider import LocalProvider\nfrom fastmcp.server.tasks import TaskConfig\nfrom fastmcp.tools.base import Tool, ToolResult\n\n\nclass TestLocalProviderStorage:\n    \"\"\"Tests for LocalProvider storage operations.\"\"\"\n\n    def test_add_tool(self):\n        \"\"\"Test adding a tool to LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        tool = Tool(\n            name=\"test_tool\",\n            description=\"A test tool\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n        )\n        provider.add_tool(tool)\n\n        assert \"tool:test_tool@\" in provider._components\n        assert provider._components[\"tool:test_tool@\"] is tool\n\n    def test_add_multiple_tools(self):\n        \"\"\"Test adding multiple tools.\"\"\"\n        provider = LocalProvider()\n\n        tool1 = Tool(\n            name=\"tool1\",\n            description=\"First tool\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n        )\n        tool2 = Tool(\n            name=\"tool2\",\n            description=\"Second tool\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n        )\n        provider.add_tool(tool1)\n        provider.add_tool(tool2)\n\n        assert \"tool:tool1@\" in provider._components\n        assert \"tool:tool2@\" in provider._components\n\n    def test_remove_tool(self):\n        \"\"\"Test removing a tool from LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        tool = Tool(\n            name=\"test_tool\",\n            description=\"A test tool\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n        )\n        provider.add_tool(tool)\n        provider.remove_tool(\"test_tool\")\n\n        assert \"tool:test_tool@\" not in provider._components\n\n    def test_remove_nonexistent_tool_raises(self):\n        \"\"\"Test that removing a nonexistent tool raises KeyError.\"\"\"\n        provider = LocalProvider()\n\n        with pytest.raises(KeyError):\n            provider.remove_tool(\"nonexistent\")\n\n    def test_add_resource(self):\n        \"\"\"Test adding a resource to LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"content\"\n\n        assert \"resource:resource://test@\" in provider._components\n\n    def test_remove_resource(self):\n        \"\"\"Test removing a resource from LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"content\"\n\n        provider.remove_resource(\"resource://test\")\n\n        assert \"resource:resource://test@\" not in provider._components\n\n    def test_add_template(self):\n        \"\"\"Test adding a resource template to LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://{id}\")\n        def template_fn(id: str) -> str:\n            return f\"Resource {id}\"\n\n        assert \"template:resource://{id}@\" in provider._components\n\n    def test_remove_template(self):\n        \"\"\"Test removing a resource template from LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://{id}\")\n        def template_fn(id: str) -> str:\n            return f\"Resource {id}\"\n\n        provider.remove_template(\"resource://{id}\")\n\n        assert \"template:resource://{id}@\" not in provider._components\n\n    def test_add_prompt(self):\n        \"\"\"Test adding a prompt to LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        prompt = Prompt(\n            name=\"test_prompt\",\n            description=\"A test prompt\",\n        )\n        provider.add_prompt(prompt)\n\n        assert \"prompt:test_prompt@\" in provider._components\n\n    def test_remove_prompt(self):\n        \"\"\"Test removing a prompt from LocalProvider.\"\"\"\n        provider = LocalProvider()\n\n        prompt = Prompt(\n            name=\"test_prompt\",\n            description=\"A test prompt\",\n        )\n        provider.add_prompt(prompt)\n        provider.remove_prompt(\"test_prompt\")\n\n        assert \"prompt:test_prompt@\" not in provider._components\n\n\nclass TestLocalProviderInterface:\n    \"\"\"Tests for LocalProvider's Provider interface.\"\"\"\n\n    async def test_list_tools_empty(self):\n        \"\"\"Test listing tools when empty.\"\"\"\n        provider = LocalProvider()\n        tools = await provider.list_tools()\n        assert tools == []\n\n    async def test_list_tools(self):\n        \"\"\"Test listing tools returns all stored tools.\"\"\"\n        provider = LocalProvider()\n\n        tool1 = Tool(name=\"tool1\", description=\"First\", parameters={\"type\": \"object\"})\n        tool2 = Tool(name=\"tool2\", description=\"Second\", parameters={\"type\": \"object\"})\n        provider.add_tool(tool1)\n        provider.add_tool(tool2)\n\n        tools = await provider.list_tools()\n        assert len(tools) == 2\n        names = {t.name for t in tools}\n        assert names == {\"tool1\", \"tool2\"}\n\n    async def test_get_tool_found(self):\n        \"\"\"Test getting a tool that exists.\"\"\"\n        provider = LocalProvider()\n\n        tool = Tool(\n            name=\"test_tool\",\n            description=\"A test tool\",\n            parameters={\"type\": \"object\"},\n        )\n        provider.add_tool(tool)\n\n        result = await provider.get_tool(\"test_tool\")\n        assert result is not None\n        assert result.name == \"test_tool\"\n\n    async def test_get_tool_not_found(self):\n        \"\"\"Test getting a tool that doesn't exist returns None.\"\"\"\n        provider = LocalProvider()\n        result = await provider.get_tool(\"nonexistent\")\n        assert result is None\n\n    async def test_list_resources(self):\n        \"\"\"Test listing resources.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"content\"\n\n        resources = await provider.list_resources()\n        assert len(resources) == 1\n        assert str(resources[0].uri) == \"resource://test\"\n\n    async def test_get_resource_found(self):\n        \"\"\"Test getting a resource that exists.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"content\"\n\n        result = await provider.get_resource(\"resource://test\")\n        assert result is not None\n        assert str(result.uri) == \"resource://test\"\n\n    async def test_get_resource_not_found(self):\n        \"\"\"Test getting a resource that doesn't exist returns None.\"\"\"\n        provider = LocalProvider()\n        result = await provider.get_resource(\"resource://nonexistent\")\n        assert result is None\n\n    async def test_list_resource_templates(self):\n        \"\"\"Test listing resource templates.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://{id}\")\n        def template_fn(id: str) -> str:\n            return f\"Resource {id}\"\n\n        templates = await provider.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].uri_template == \"resource://{id}\"\n\n    async def test_get_resource_template_match(self):\n        \"\"\"Test getting a template that matches a URI.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://{id}\")\n        def template_fn(id: str) -> str:\n            return f\"Resource {id}\"\n\n        result = await provider.get_resource_template(\"resource://123\")\n        assert result is not None\n        assert result.uri_template == \"resource://{id}\"\n\n    async def test_get_resource_template_no_match(self):\n        \"\"\"Test getting a template with no match returns None.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://{id}\")\n        def template_fn(id: str) -> str:\n            return f\"Resource {id}\"\n\n        result = await provider.get_resource_template(\"other://123\")\n        assert result is None\n\n    async def test_list_prompts(self):\n        \"\"\"Test listing prompts.\"\"\"\n        provider = LocalProvider()\n\n        prompt = Prompt(\n            name=\"test_prompt\",\n            description=\"A test prompt\",\n        )\n        provider.add_prompt(prompt)\n\n        prompts = await provider.list_prompts()\n        assert len(prompts) == 1\n        assert prompts[0].name == \"test_prompt\"\n\n    async def test_get_prompt_found(self):\n        \"\"\"Test getting a prompt that exists.\"\"\"\n        provider = LocalProvider()\n\n        prompt = Prompt(\n            name=\"test_prompt\",\n            description=\"A test prompt\",\n        )\n        provider.add_prompt(prompt)\n\n        result = await provider.get_prompt(\"test_prompt\")\n        assert result is not None\n        assert result.name == \"test_prompt\"\n\n    async def test_get_prompt_not_found(self):\n        \"\"\"Test getting a prompt that doesn't exist returns None.\"\"\"\n        provider = LocalProvider()\n        result = await provider.get_prompt(\"nonexistent\")\n        assert result is None\n\n\nclass TestLocalProviderDecorators:\n    \"\"\"Tests for LocalProvider decorator registration.\n\n    Note: Decorator calling patterns and metadata are tested in the standalone\n    decorator tests (tests/tools/test_standalone_decorator.py, etc.). These tests\n    focus on LocalProvider-specific behavior: registration into _components,\n    the enabled flag, and round-trip execution via Client.\n    \"\"\"\n\n    def test_tool_decorator_registers(self):\n        \"\"\"Tool decorator should register in _components.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        assert \"tool:my_tool@\" in provider._components\n        assert provider._components[\"tool:my_tool@\"].name == \"my_tool\"\n\n    def test_tool_decorator_with_custom_name_registers(self):\n        \"\"\"Tool with custom name should register under that name.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool(name=\"custom_name\")\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        assert \"tool:custom_name@\" in provider._components\n        assert \"tool:my_tool@\" not in provider._components\n\n    def test_tool_direct_call(self):\n        \"\"\"provider.tool(fn) should register the function.\"\"\"\n        provider = LocalProvider()\n\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        provider.tool(my_tool, name=\"direct_tool\")\n\n        assert \"tool:direct_tool@\" in provider._components\n\n    def test_tool_enabled_false(self):\n        \"\"\"Tool with enabled=False should add a Visibility transform.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool(enabled=False)\n        def disabled_tool() -> str:\n            return \"should be disabled\"\n\n        assert \"tool:disabled_tool@\" in provider._components\n        # enabled=False adds a Visibility transform to disable the tool\n        from fastmcp.server.transforms.visibility import Visibility\n\n        enabled_transforms = [\n            t for t in provider.transforms if isinstance(t, Visibility)\n        ]\n        assert len(enabled_transforms) == 1\n        assert enabled_transforms[0]._enabled is False\n        assert enabled_transforms[0].keys == {\"tool:disabled_tool@\"}\n\n    async def test_tool_enabled_false_not_listed(self):\n        \"\"\"Disabled tool should not appear in get_tools (filtering happens at server level).\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool(enabled=False)\n        def disabled_tool() -> str:\n            return \"should be disabled\"\n\n        @provider.tool\n        def enabled_tool() -> str:\n            return \"should be enabled\"\n\n        # Filtering happens at the server level, not provider level\n        server = FastMCP(\"Test\", providers=[provider])\n        tools = await server.list_tools()\n        names = {t.name for t in tools}\n        assert \"enabled_tool\" in names\n        assert \"disabled_tool\" not in names\n\n    async def test_server_enable_overrides_provider_disable(self):\n        \"\"\"Server-level enable should override provider-level disable.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool(enabled=False)\n        def my_tool() -> str:\n            return \"result\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        # Tool is disabled at provider level\n        assert await server.get_tool(\"my_tool\") is None\n\n        # Server-level enable overrides it\n        server.enable(names={\"my_tool\"})\n        tool = await server.get_tool(\"my_tool\")\n        assert tool is not None\n        assert tool.name == \"my_tool\"\n\n    async def test_tool_roundtrip(self):\n        \"\"\"Tool should execute correctly via Client.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"add\", {\"a\": 2, \"b\": 3})\n            assert result.data == 5\n\n    def test_resource_decorator_registers(self):\n        \"\"\"Resource decorator should register in _components.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\")\n        def my_resource() -> str:\n            return \"test content\"\n\n        assert \"resource:resource://test@\" in provider._components\n\n    def test_resource_with_custom_name_registers(self):\n        \"\"\"Resource with custom name should register with that name.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\", name=\"custom_name\")\n        def my_resource() -> str:\n            return \"test content\"\n\n        assert provider._components[\"resource:resource://test@\"].name == \"custom_name\"\n\n    def test_resource_enabled_false(self):\n        \"\"\"Resource with enabled=False should add a Visibility transform.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://test\", enabled=False)\n        def disabled_resource() -> str:\n            return \"should be disabled\"\n\n        assert \"resource:resource://test@\" in provider._components\n        # enabled=False adds a Visibility transform to disable the resource\n        from fastmcp.server.transforms.visibility import Visibility\n\n        enabled_transforms = [\n            t for t in provider.transforms if isinstance(t, Visibility)\n        ]\n        assert len(enabled_transforms) == 1\n        assert enabled_transforms[0]._enabled is False\n        assert enabled_transforms[0].keys == {\"resource:resource://test@\"}\n\n    async def test_resource_enabled_false_not_listed(self):\n        \"\"\"Disabled resource should not appear in get_resources (filtering at server level).\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://disabled\", enabled=False)\n        def disabled_resource() -> str:\n            return \"should be disabled\"\n\n        @provider.resource(\"resource://enabled\")\n        def enabled_resource() -> str:\n            return \"should be enabled\"\n\n        # Filtering happens at the server level, not provider level\n        server = FastMCP(\"Test\", providers=[provider])\n        resources = await server.list_resources()\n        uris = {str(r.uri) for r in resources}\n        assert \"resource://enabled\" in uris\n        assert \"resource://disabled\" not in uris\n\n    def test_template_enabled_false(self):\n        \"\"\"Template with enabled=False should add a Visibility transform.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"data://{id}\", enabled=False)\n        def disabled_template(id: str) -> str:\n            return f\"Data {id}\"\n\n        assert \"template:data://{id}@\" in provider._components\n        # enabled=False adds a Visibility transform to disable the template\n        from fastmcp.server.transforms.visibility import Visibility\n\n        enabled_transforms = [\n            t for t in provider.transforms if isinstance(t, Visibility)\n        ]\n        assert len(enabled_transforms) == 1\n        assert enabled_transforms[0]._enabled is False\n        assert enabled_transforms[0].keys == {\"template:data://{id}@\"}\n\n    async def test_template_enabled_false_not_listed(self):\n        \"\"\"Disabled template should not appear in get_resource_templates (filtering at server level).\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"data://{id}\", enabled=False)\n        def disabled_template(id: str) -> str:\n            return f\"Data {id}\"\n\n        @provider.resource(\"items://{id}\")\n        def enabled_template(id: str) -> str:\n            return f\"Item {id}\"\n\n        # Filtering happens at the server level, not provider level\n        server = FastMCP(\"Test\", providers=[provider])\n        templates = await server.list_resource_templates()\n        uris = {t.uri_template for t in templates}\n        assert \"items://{id}\" in uris\n        assert \"data://{id}\" not in uris\n\n    async def test_resource_roundtrip(self):\n        \"\"\"Resource should execute correctly via Client.\"\"\"\n        provider = LocalProvider()\n\n        @provider.resource(\"resource://greeting\")\n        def greeting() -> str:\n            return \"Hello, World!\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        async with Client(server) as client:\n            result = await client.read_resource(\"resource://greeting\")\n            assert \"Hello, World!\" in str(result)\n\n    def test_prompt_decorator_registers(self):\n        \"\"\"Prompt decorator should register in _components.\"\"\"\n        provider = LocalProvider()\n\n        @provider.prompt\n        def my_prompt() -> str:\n            return \"A prompt\"\n\n        assert \"prompt:my_prompt@\" in provider._components\n\n    def test_prompt_with_custom_name_registers(self):\n        \"\"\"Prompt with custom name should register under that name.\"\"\"\n        provider = LocalProvider()\n\n        @provider.prompt(name=\"custom_prompt\")\n        def my_prompt() -> str:\n            return \"A prompt\"\n\n        assert \"prompt:custom_prompt@\" in provider._components\n        assert \"prompt:my_prompt@\" not in provider._components\n\n    def test_prompt_enabled_false(self):\n        \"\"\"Prompt with enabled=False should add a Visibility transform.\"\"\"\n        provider = LocalProvider()\n\n        @provider.prompt(enabled=False)\n        def disabled_prompt() -> str:\n            return \"should be disabled\"\n\n        assert \"prompt:disabled_prompt@\" in provider._components\n        # enabled=False adds a Visibility transform to disable the prompt\n        from fastmcp.server.transforms.visibility import Visibility\n\n        enabled_transforms = [\n            t for t in provider.transforms if isinstance(t, Visibility)\n        ]\n        assert len(enabled_transforms) == 1\n        assert enabled_transforms[0]._enabled is False\n        assert enabled_transforms[0].keys == {\"prompt:disabled_prompt@\"}\n\n    async def test_prompt_enabled_false_not_listed(self):\n        \"\"\"Disabled prompt should not appear in get_prompts (filtering at server level).\"\"\"\n        provider = LocalProvider()\n\n        @provider.prompt(enabled=False)\n        def disabled_prompt() -> str:\n            return \"should be disabled\"\n\n        @provider.prompt\n        def enabled_prompt() -> str:\n            return \"should be enabled\"\n\n        # Filtering happens at the server level, not provider level\n        server = FastMCP(\"Test\", providers=[provider])\n        prompts = await server.list_prompts()\n        names = {p.name for p in prompts}\n        assert \"enabled_prompt\" in names\n        assert \"disabled_prompt\" not in names\n\n    async def test_prompt_roundtrip(self):\n        \"\"\"Prompt should execute correctly via Client.\"\"\"\n        provider = LocalProvider()\n\n        @provider.prompt\n        def greeting(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        async with Client(server) as client:\n            result = await client.get_prompt(\"greeting\", {\"name\": \"World\"})\n            assert \"Hello, World!\" in str(result)\n\n\nclass TestProviderToolTransformations:\n    \"\"\"Tests for tool transformations via add_transform().\"\"\"\n\n    async def test_add_transform_applies_tool_transforms(self):\n        \"\"\"Test that add_transform with ToolTransform applies tool transformations.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n        from fastmcp.tools.tool_transform import ToolTransformConfig\n\n        provider = LocalProvider()\n\n        @provider.tool\n        def my_tool(x: int) -> int:\n            return x\n\n        # Add transform layer\n        layer = ToolTransform({\"my_tool\": ToolTransformConfig(name=\"renamed_tool\")})\n        provider.add_transform(layer)\n\n        # Get tools and pass directly to transform\n        tools = await provider.list_tools()\n        transformed_tools = await layer.list_tools(tools)\n        assert len(transformed_tools) == 1\n        assert transformed_tools[0].name == \"renamed_tool\"\n\n    async def test_transform_layer_get_tool(self):\n        \"\"\"Test that ToolTransform.get_tool works correctly.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n        from fastmcp.tools.tool_transform import ToolTransformConfig\n\n        provider = LocalProvider()\n\n        @provider.tool\n        def original_tool(x: int) -> int:\n            return x\n\n        layer = ToolTransform(\n            {\"original_tool\": ToolTransformConfig(name=\"transformed_tool\")}\n        )\n\n        # Get tool through layer with call_next\n        async def get_tool(name: str, version=None):\n            return await provider._get_tool(name, version)\n\n        tool = await layer.get_tool(\"transformed_tool\", get_tool)\n        assert tool is not None\n        assert tool.name == \"transformed_tool\"\n\n        # Original name should not work\n        tool = await layer.get_tool(\"original_tool\", get_tool)\n        assert tool is None\n\n    async def test_transform_layer_description_change(self):\n        \"\"\"Test that ToolTransform can change description.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n        from fastmcp.tools.tool_transform import ToolTransformConfig\n\n        provider = LocalProvider()\n\n        @provider.tool\n        def my_tool(x: int) -> int:\n            return x\n\n        layer = ToolTransform(\n            {\"my_tool\": ToolTransformConfig(description=\"New description\")}\n        )\n\n        async def get_tool(name: str, version=None):\n            return await provider._get_tool(name, version)\n\n        tool = await layer.get_tool(\"my_tool\", get_tool)\n        assert tool is not None\n        assert tool.description == \"New description\"\n\n    async def test_provider_unaffected_by_transforms(self):\n        \"\"\"Test that provider's own tools are unchanged by layers stored on it.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n        from fastmcp.tools.tool_transform import ToolTransformConfig\n\n        provider = LocalProvider()\n\n        @provider.tool\n        def my_tool(x: int) -> int:\n            return x\n\n        # Add layer to provider (layers are applied by server, not _list_tools)\n        layer = ToolTransform({\"my_tool\": ToolTransformConfig(name=\"renamed\")})\n        provider.add_transform(layer)\n\n        # Provider's _list_tools returns raw tools (transforms applied when queried via list_tools)\n        original_tools = await provider._list_tools()\n        assert original_tools[0].name == \"my_tool\"\n\n        # Transform modifies them when applied directly\n        transformed_tools = await layer.list_tools(original_tools)\n        assert transformed_tools[0].name == \"renamed\"\n\n    def test_transform_layer_duplicate_target_name_raises_error(self):\n        \"\"\"Test that ToolTransform with duplicate target names raises ValueError.\"\"\"\n        from fastmcp.server.transforms import ToolTransform\n        from fastmcp.tools.tool_transform import ToolTransformConfig\n\n        with pytest.raises(ValueError, match=\"duplicate target name\"):\n            ToolTransform(\n                {\n                    \"tool_a\": ToolTransformConfig(name=\"same_name\"),\n                    \"tool_b\": ToolTransformConfig(name=\"same_name\"),\n                }\n            )\n\n\nclass TestLocalProviderTaskRegistration:\n    \"\"\"Tests for task registration in LocalProvider.\"\"\"\n\n    async def test_get_tasks_returns_task_eligible_tools(self):\n        \"\"\"Test that get_tasks returns tools with task support.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool(task=True)\n        async def background_tool(x: int) -> int:\n            return x\n\n        tasks = await provider.get_tasks()\n        assert len(tasks) == 1\n        assert tasks[0].name == \"background_tool\"\n\n    async def test_get_tasks_filters_forbidden_tools(self):\n        \"\"\"Test that get_tasks excludes tools with forbidden task mode.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool(task=False)\n        def sync_only_tool(x: int) -> int:\n            return x\n\n        tasks = await provider.get_tasks()\n        assert len(tasks) == 0\n\n    async def test_get_tasks_includes_custom_tool_subclasses(self):\n        \"\"\"Test that custom Tool subclasses are included in get_tasks.\"\"\"\n\n        class CustomTool(Tool):\n            task_config: TaskConfig = TaskConfig(mode=\"optional\")\n            parameters: dict[str, Any] = {\"type\": \"object\", \"properties\": {}}\n\n            async def run(self, arguments: dict[str, Any]) -> ToolResult:\n                return ToolResult(content=\"custom\")\n\n        provider = LocalProvider()\n        provider.add_tool(CustomTool(name=\"custom\", description=\"Custom tool\"))\n\n        tasks = await provider.get_tasks()\n        assert len(tasks) == 1\n        assert tasks[0].name == \"custom\"\n\n\nclass TestLocalProviderStandaloneUsage:\n    \"\"\"Tests for standalone LocalProvider usage patterns.\"\"\"\n\n    async def test_attach_provider_to_server(self):\n        \"\"\"Test that LocalProvider can be attached to a server.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n            assert any(t.name == \"greet\" for t in tools)\n\n    async def test_attach_provider_to_multiple_servers(self):\n        \"\"\"Test that same provider can be attached to multiple servers.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def shared_tool() -> str:\n            return \"shared\"\n\n        server1 = FastMCP(\"Server1\", providers=[provider])\n        server2 = FastMCP(\"Server2\", providers=[provider])\n\n        async with Client(server1) as client1:\n            tools1 = await client1.list_tools()\n            assert any(t.name == \"shared_tool\" for t in tools1)\n\n        async with Client(server2) as client2:\n            tools2 = await client2.list_tools()\n            assert any(t.name == \"shared_tool\" for t in tools2)\n\n    async def test_tools_visible_via_server_get_tools(self):\n        \"\"\"Test that provider tools are visible via server.list_tools().\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def provider_tool() -> str:\n            return \"from provider\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        tools = await server.list_tools()\n        assert any(t.name == \"provider_tool\" for t in tools)\n\n    async def test_server_decorator_and_provider_tools_coexist(self):\n        \"\"\"Test that server decorators and provider tools coexist.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def provider_tool() -> str:\n            return \"from provider\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        @server.tool\n        def server_tool() -> str:\n            return \"from server\"\n\n        tools = await server.list_tools()\n        assert any(t.name == \"provider_tool\" for t in tools)\n        assert any(t.name == \"server_tool\" for t in tools)\n\n    async def test_local_provider_first_wins_duplicates(self):\n        \"\"\"Test that LocalProvider tools take precedence over added providers.\"\"\"\n        provider = LocalProvider()\n\n        @provider.tool\n        def duplicate_tool() -> str:\n            return \"from added provider\"\n\n        server = FastMCP(\"Test\", providers=[provider])\n\n        @server.tool\n        def duplicate_tool() -> str:  # noqa: F811\n            return \"from server\"\n\n        # Server's LocalProvider is first, so its tool wins\n        tools = await server.list_tools()\n        assert any(t.name == \"duplicate_tool\" for t in tools)\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"duplicate_tool\", {})\n            assert result.data == \"from server\"\n"
  },
  {
    "path": "tests/server/providers/test_local_provider_prompts.py",
    "content": "\"\"\"Tests for prompt behavior in LocalProvider.\n\nTests cover:\n- Prompt context injection\n- Prompt decorator patterns\n\"\"\"\n\nimport pytest\nfrom mcp.types import TextContent\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.prompts.base import Prompt, PromptResult\n\n\nclass TestPromptContext:\n    async def test_prompt_context(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def prompt_fn(name: str, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return f\"Hello, {name}! {ctx.request_id}\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"prompt_fn\", {\"name\": \"World\"})\n            assert len(result.messages) == 1\n            message = result.messages[0]\n            assert message.role == \"user\"\n\n    async def test_prompt_context_with_callable_object(self):\n        mcp = FastMCP()\n\n        class MyPrompt:\n            def __call__(self, name: str, ctx: Context) -> str:\n                return f\"Hello, {name}! {ctx.request_id}\"\n\n        mcp.add_prompt(Prompt.from_function(MyPrompt(), name=\"my_prompt\"))\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"my_prompt\", {\"name\": \"World\"})\n            assert len(result.messages) == 1\n            message = result.messages[0]\n            assert message.role == \"user\"\n            assert isinstance(message.content, TextContent)\n            assert message.content.text == \"Hello, World! 1\"\n\n\nclass TestPromptDecorator:\n    async def test_prompt_decorator(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 1\n        prompt = next(p for p in prompts if p.name == \"fn\")\n        assert prompt.name == \"fn\"\n        content = await prompt.render()\n        assert isinstance(content, PromptResult)\n        assert isinstance(content.messages[0].content, TextContent)\n        assert content.messages[0].content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator_without_parentheses(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts = await mcp.list_prompts()\n        assert any(p.name == \"fn\" for p in prompts)\n\n        result = await mcp.render_prompt(\"fn\")\n        assert len(result.messages) == 1\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator_with_name(self):\n        mcp = FastMCP()\n\n        @mcp.prompt(name=\"custom_name\")\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts_list = await mcp.list_prompts()\n        assert len(prompts_list) == 1\n        prompt = next(p for p in prompts_list if p.name == \"custom_name\")\n        assert prompt.name == \"custom_name\"\n        content = await prompt.render()\n        assert isinstance(content, PromptResult)\n        assert isinstance(content.messages[0].content, TextContent)\n        assert content.messages[0].content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator_with_description(self):\n        mcp = FastMCP()\n\n        @mcp.prompt(description=\"A custom description\")\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts_list = await mcp.list_prompts()\n        assert len(prompts_list) == 1\n        prompt = next(p for p in prompts_list if p.name == \"fn\")\n        assert prompt.description == \"A custom description\"\n        content = await prompt.render()\n        assert isinstance(content, PromptResult)\n        assert isinstance(content.messages[0].content, TextContent)\n        assert content.messages[0].content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator_with_parameters(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def test_prompt(name: str, greeting: str = \"Hello\") -> str:\n            return f\"{greeting}, {name}!\"\n\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 1\n        prompt = next(p for p in prompts if p.name == \"test_prompt\")\n        assert prompt.arguments is not None\n        assert len(prompt.arguments) == 2\n        assert prompt.arguments[0].name == \"name\"\n        assert prompt.arguments[0].required is True\n        assert prompt.arguments[1].name == \"greeting\"\n        assert prompt.arguments[1].required is False\n\n        result = await mcp.render_prompt(\"test_prompt\", {\"name\": \"World\"})\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"Hello, World!\"\n\n        result = await mcp.render_prompt(\n            \"test_prompt\", {\"name\": \"World\", \"greeting\": \"Hi\"}\n        )\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"Hi, World!\"\n\n    async def test_prompt_decorator_instance_method(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            def __init__(self, prefix: str):\n                self.prefix = prefix\n\n            def test_prompt(self) -> str:\n                return f\"{self.prefix} Hello, world!\"\n\n        obj = MyClass(\"My prefix:\")\n        mcp.add_prompt(Prompt.from_function(obj.test_prompt, name=\"test_prompt\"))\n\n        result = await mcp.render_prompt(\"test_prompt\")\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"My prefix: Hello, world!\"\n\n    async def test_prompt_decorator_classmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            prefix = \"Class prefix:\"\n\n            @classmethod\n            def test_prompt(cls) -> str:\n                return f\"{cls.prefix} Hello, world!\"\n\n        mcp.add_prompt(Prompt.from_function(MyClass.test_prompt, name=\"test_prompt\"))\n\n        result = await mcp.render_prompt(\"test_prompt\")\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"Class prefix: Hello, world!\"\n\n    async def test_prompt_decorator_classmethod_error(self):\n        mcp = FastMCP()\n\n        with pytest.raises(TypeError, match=\"classmethod\"):\n\n            class MyClass:\n                @mcp.prompt\n                @classmethod\n                def test_prompt(cls) -> None:\n                    pass\n\n    async def test_prompt_decorator_staticmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.prompt\n            @staticmethod\n            def test_prompt() -> str:\n                return \"Static Hello, world!\"\n\n        result = await mcp.render_prompt(\"test_prompt\")\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"Static Hello, world!\"\n\n    async def test_prompt_decorator_async_function(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        async def test_prompt() -> str:\n            return \"Async Hello, world!\"\n\n        result = await mcp.render_prompt(\"test_prompt\")\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"Async Hello, world!\"\n\n    async def test_prompt_decorator_with_tags(self):\n        \"\"\"Test that the prompt decorator properly sets tags.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(tags={\"example\", \"test-tag\"})\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 1\n        prompt = next(p for p in prompts if p.name == \"sample_prompt\")\n        assert prompt.tags == {\"example\", \"test-tag\"}\n\n    async def test_prompt_decorator_with_string_name(self):\n        \"\"\"Test that @prompt(\\\"custom_name\\\") syntax works correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(\"string_named_prompt\")\n        def my_function() -> str:\n            \"\"\"A function with a string name.\"\"\"\n            return \"Hello from string named prompt!\"\n\n        prompts = await mcp.list_prompts()\n        assert any(p.name == \"string_named_prompt\" for p in prompts)\n        assert not any(p.name == \"my_function\" for p in prompts)\n\n        result = await mcp.render_prompt(\"string_named_prompt\")\n        assert len(result.messages) == 1\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Hello from string named prompt!\"\n\n    async def test_prompt_direct_function_call(self):\n        \"\"\"Test that prompts can be registered via direct function call.\"\"\"\n        from typing import cast\n\n        from fastmcp.prompts.function_prompt import DecoratedPrompt\n\n        mcp = FastMCP()\n\n        def standalone_function() -> str:\n            \"\"\"A standalone function to be registered.\"\"\"\n            return \"Hello from direct call!\"\n\n        result_fn = mcp.prompt(standalone_function, name=\"direct_call_prompt\")\n\n        # In new decorator mode, returns the function with metadata\n        decorated = cast(DecoratedPrompt, result_fn)\n        assert hasattr(result_fn, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"direct_call_prompt\"\n        assert result_fn is standalone_function\n\n        prompts = await mcp.list_prompts()\n        prompt = next(p for p in prompts if p.name == \"direct_call_prompt\")\n        # Prompt is registered separately, not same object as decorated function\n        assert prompt.name == \"direct_call_prompt\"\n\n        result = await mcp.render_prompt(\"direct_call_prompt\")\n        assert len(result.messages) == 1\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Hello from direct call!\"\n\n    async def test_prompt_decorator_conflicting_names_error(self):\n        \"\"\"Test that providing both positional and keyword names raises an error.\"\"\"\n        mcp = FastMCP()\n\n        with pytest.raises(\n            TypeError,\n            match=\"Cannot specify both a name as first argument and as keyword argument\",\n        ):\n\n            @mcp.prompt(\"positional_name\", name=\"keyword_name\")\n            def my_function() -> str:\n                return \"Hello, world!\"\n\n    async def test_prompt_decorator_staticmethod_order(self):\n        \"\"\"Test that both decorator orders work for static methods\"\"\"\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.prompt\n            @staticmethod\n            def test_prompt() -> str:\n                return \"Static Hello, world!\"\n\n        result = await mcp.render_prompt(\"test_prompt\")\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        assert isinstance(message.content, TextContent)\n        assert message.content.text == \"Static Hello, world!\"\n\n    async def test_prompt_decorator_with_meta(self):\n        \"\"\"Test that meta parameter is passed through the prompt decorator.\"\"\"\n        mcp = FastMCP()\n\n        meta_data = {\"version\": \"3.0\", \"type\": \"prompt\"}\n\n        @mcp.prompt(meta=meta_data)\n        def test_prompt(message: str) -> str:\n            return f\"Response: {message}\"\n\n        prompts = await mcp.list_prompts()\n        prompt = next(p for p in prompts if p.name == \"test_prompt\")\n\n        assert prompt.meta == meta_data\n\n\nclass TestPromptEnabled:\n    async def test_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        prompts = await mcp.list_prompts()\n        assert any(p.name == \"sample_prompt\" for p in prompts)\n\n        mcp.disable(names={\"sample_prompt\"}, components={\"prompt\"})\n\n        prompts = await mcp.list_prompts()\n        assert not any(p.name == \"sample_prompt\" for p in prompts)\n\n        mcp.enable(names={\"sample_prompt\"}, components={\"prompt\"})\n\n        prompts = await mcp.list_prompts()\n        assert any(p.name == \"sample_prompt\" for p in prompts)\n\n    async def test_prompt_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"sample_prompt\"}, components={\"prompt\"})\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 0\n\n    async def test_prompt_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"sample_prompt\"}, components={\"prompt\"})\n        prompts = await mcp.list_prompts()\n        assert not any(p.name == \"sample_prompt\" for p in prompts)\n\n        mcp.enable(names={\"sample_prompt\"}, components={\"prompt\"})\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 1\n\n    async def test_prompt_toggle_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"sample_prompt\"}, components={\"prompt\"})\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 0\n\n        # get_prompt() applies enabled transform, returns None for disabled\n        prompt = await mcp.get_prompt(\"sample_prompt\")\n        assert prompt is None\n\n    async def test_get_prompt_and_disable(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        prompt = await mcp.get_prompt(\"sample_prompt\")\n        assert prompt is not None\n\n        mcp.disable(names={\"sample_prompt\"}, components={\"prompt\"})\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 0\n\n        # get_prompt() applies enabled transform, returns None for disabled\n        prompt = await mcp.get_prompt(\"sample_prompt\")\n        assert prompt is None\n\n    async def test_cant_get_disabled_prompt(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"sample_prompt\"}, components={\"prompt\"})\n\n        # get_prompt() applies enabled transform, returns None for disabled\n        prompt = await mcp.get_prompt(\"sample_prompt\")\n        assert prompt is None\n\n\nclass TestPromptTags:\n    def create_server(self, include_tags=None, exclude_tags=None):\n        mcp = FastMCP()\n\n        @mcp.prompt(tags={\"a\", \"b\"})\n        def prompt_1() -> str:\n            return \"1\"\n\n        @mcp.prompt(tags={\"b\", \"c\"})\n        def prompt_2() -> str:\n            return \"2\"\n\n        if include_tags:\n            mcp.enable(tags=include_tags, only=True)\n        if exclude_tags:\n            mcp.disable(tags=exclude_tags)\n\n        return mcp\n\n    async def test_include_tags_all_prompts(self):\n        mcp = self.create_server(include_tags={\"a\", \"b\"})\n        prompts = await mcp.list_prompts()\n        assert {p.name for p in prompts} == {\"prompt_1\", \"prompt_2\"}\n\n    async def test_include_tags_some_prompts(self):\n        mcp = self.create_server(include_tags={\"a\"})\n        prompts = await mcp.list_prompts()\n        assert {p.name for p in prompts} == {\"prompt_1\"}\n\n    async def test_exclude_tags_all_prompts(self):\n        mcp = self.create_server(exclude_tags={\"a\", \"b\"})\n        prompts = await mcp.list_prompts()\n        assert {p.name for p in prompts} == set()\n\n    async def test_exclude_tags_some_prompts(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        prompts = await mcp.list_prompts()\n        assert {p.name for p in prompts} == {\"prompt_2\"}\n\n    async def test_exclude_takes_precedence_over_include(self):\n        mcp = self.create_server(exclude_tags={\"a\"}, include_tags={\"b\"})\n        prompts = await mcp.list_prompts()\n        assert {p.name for p in prompts} == {\"prompt_2\"}\n\n    async def test_read_prompt_includes_tags(self):\n        mcp = self.create_server(include_tags={\"a\"})\n        # _get_prompt applies enabled transform (tag filtering)\n        prompt = await mcp._get_prompt(\"prompt_1\")\n        result = await prompt.render({})\n        assert result.messages[0].content.text == \"1\"\n\n        prompt = await mcp.get_prompt(\"prompt_2\")\n        assert prompt is None\n\n    async def test_read_prompt_excludes_tags(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        # get_prompt applies enabled transform (tag filtering)\n        prompt = await mcp.get_prompt(\"prompt_1\")\n        assert prompt is None\n\n        prompt = await mcp.get_prompt(\"prompt_2\")\n        result = await prompt.render({})\n        assert result.messages[0].content.text == \"2\"\n"
  },
  {
    "path": "tests/server/providers/test_local_provider_resources.py",
    "content": "\"\"\"Tests for resource and template behavior in LocalProvider.\n\nTests cover:\n- Resource context injection\n- Resource templates and URI parsing\n- Resource template context injection\n- Resource decorator patterns\n- Template decorator patterns\n\"\"\"\n\nimport pytest\nfrom mcp.types import TextResourceContents\nfrom pydantic import AnyUrl\n\nfrom fastmcp import Client, Context, FastMCP\nfrom fastmcp.exceptions import NotFoundError\nfrom fastmcp.resources import (\n    Resource,\n    ResourceContent,\n    ResourceResult,\n    ResourceTemplate,\n)\n\n\nclass TestResourceContext:\n    async def test_resource_with_context_annotation_gets_context(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://test\")\n        def resource_with_context(ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return ctx.request_id\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"resource://test\"))\n            assert isinstance(result[0], TextResourceContents)\n            assert result[0].text == \"1\"\n\n\nclass TestResourceTemplates:\n    async def test_resource_with_params_not_in_uri(self):\n        \"\"\"Test that a resource with function parameters raises an error if the URI\n        parameters don't match\"\"\"\n        mcp = FastMCP()\n\n        with pytest.raises(\n            ValueError,\n            match=\"URI template must contain at least one parameter\",\n        ):\n\n            @mcp.resource(\"resource://data\")\n            def get_data_fn(param: str) -> str:\n                return f\"Data: {param}\"\n\n    async def test_resource_with_uri_params_without_args(self):\n        \"\"\"Test that a resource with URI parameters is automatically a template\"\"\"\n        mcp = FastMCP()\n\n        with pytest.raises(\n            ValueError,\n            match=\"URI parameters .* must be a subset of the function arguments\",\n        ):\n\n            @mcp.resource(\"resource://{param}\")\n            def get_data() -> str:\n                return \"Data\"\n\n    async def test_resource_with_untyped_params(self):\n        \"\"\"Test that a resource with untyped parameters raises an error\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def get_data(param) -> str:\n            return \"Data\"\n\n    async def test_resource_matching_params(self):\n        \"\"\"Test that a resource with matching URI and function parameters works\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{name}/data\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Data for test\"\n\n    async def test_resource_mismatched_params(self):\n        \"\"\"Test that mismatched parameters raise an error\"\"\"\n        mcp = FastMCP()\n\n        with pytest.raises(\n            ValueError,\n            match=\"Required function arguments .* must be a subset of the URI path parameters\",\n        ):\n\n            @mcp.resource(\"resource://{name}/data\")\n            def get_data(user: str) -> str:\n                return f\"Data for {user}\"\n\n    async def test_resource_multiple_params(self):\n        \"\"\"Test that multiple parameters work correctly\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{org}/{repo}/data\")\n        def get_data(org: str, repo: str) -> str:\n            return f\"Data for {org}/{repo}\"\n\n        result = await mcp.read_resource(\"resource://cursor/fastmcp/data\")\n        assert result.contents[0].content == \"Data for cursor/fastmcp\"\n\n    async def test_resource_multiple_mismatched_params(self):\n        \"\"\"Test that mismatched parameters raise an error\"\"\"\n        mcp = FastMCP()\n\n        with pytest.raises(\n            ValueError,\n            match=\"Required function arguments .* must be a subset of the URI path parameters\",\n        ):\n\n            @mcp.resource(\"resource://{org}/{repo}/data\")\n            def get_data_mismatched(org: str, repo_2: str) -> str:\n                return f\"Data for {org}\"\n\n    async def test_template_with_varkwargs(self):\n        \"\"\"Test that a template can have **kwargs.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"test://{x}/{y}/{z}\")\n        def func(**kwargs: int) -> str:\n            return str(sum(int(v) for v in kwargs.values()))\n\n        result = await mcp.read_resource(\"test://1/2/3\")\n        assert result.contents[0].content == \"6\"\n\n    async def test_template_with_default_params(self):\n        \"\"\"Test that a template can have default parameters.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"math://add/{x}\")\n        def add(x: int, y: int = 10) -> str:\n            return str(int(x) + y)\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].uri_template == \"math://add/{x}\"\n\n        result = await mcp.read_resource(\"math://add/5\")\n        assert result.contents[0].content == \"15\"\n\n        result2 = await mcp.read_resource(\"math://add/7\")\n        assert result2.contents[0].content == \"17\"\n\n    async def test_template_to_resource_conversion(self):\n        \"\"\"Test that a template can be converted to a resource.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{name}/data\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].uri_template == \"resource://{name}/data\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Data for test\"\n\n    async def test_template_decorator_with_tags(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\", tags={\"template\", \"test-tag\"})\n        def template_resource(param: str) -> str:\n            return f\"Template resource: {param}\"\n\n        templates = await mcp.list_resource_templates()\n        template = next(t for t in templates if t.uri_template == \"resource://{param}\")\n        assert template.tags == {\"template\", \"test-tag\"}\n\n    async def test_template_decorator_wildcard_param(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param*}\")\n        def template_resource(param: str) -> str:\n            return f\"Template resource: {param}\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Template resource: test/data\"\n\n    async def test_template_with_query_params(self):\n        \"\"\"Test RFC 6570 query parameters in resource templates.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://{id}{?format,limit}\")\n        def get_data(id: str, format: str = \"json\", limit: int = 10) -> str:\n            return f\"id={id}, format={format}, limit={limit}\"\n\n        result = await mcp.read_resource(\"data://123\")\n        assert result.contents[0].content == \"id=123, format=json, limit=10\"\n\n        result = await mcp.read_resource(\"data://123?format=xml\")\n        assert result.contents[0].content == \"id=123, format=xml, limit=10\"\n\n        result = await mcp.read_resource(\"data://123?format=csv&limit=50\")\n        assert result.contents[0].content == \"id=123, format=csv, limit=50\"\n\n    async def test_templates_match_in_order_of_definition(self):\n        \"\"\"If a wildcard template is defined first, it will take priority.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param*}\")\n        def template_resource(param: str) -> str:\n            return f\"Template resource 1: {param}\"\n\n        @mcp.resource(\"resource://{x}/{y}\")\n        def template_resource_with_params(x: str, y: str) -> str:\n            return f\"Template resource 2: {x}/{y}\"\n\n        result = await mcp.read_resource(\"resource://a/b/c\")\n        assert result.contents[0].content == \"Template resource 1: a/b/c\"\n\n        result = await mcp.read_resource(\"resource://a/b\")\n        assert result.contents[0].content == \"Template resource 1: a/b\"\n\n    async def test_templates_shadow_each_other_reorder(self):\n        \"\"\"If a wildcard template is defined second, it will *not* take priority.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{x}/{y}\")\n        def template_resource_with_params(x: str, y: str) -> str:\n            return f\"Template resource 1: {x}/{y}\"\n\n        @mcp.resource(\"resource://{param*}\")\n        def template_resource(param: str) -> str:\n            return f\"Template resource 2: {param}\"\n\n        result = await mcp.read_resource(\"resource://a/b/c\")\n        assert result.contents[0].content == \"Template resource 2: a/b/c\"\n\n        result = await mcp.read_resource(\"resource://a/b\")\n        assert result.contents[0].content == \"Template resource 1: a/b\"\n\n    async def test_resource_template_with_annotations(self):\n        \"\"\"Test that resource template annotations are visible.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\n            \"api://users/{user_id}\",\n            annotations={\"httpMethod\": \"GET\", \"Cache-Control\": \"no-cache\"},\n        )\n        def get_user(user_id: str) -> str:\n            return f\"User {user_id} data\"\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n\n        template = templates[0]\n        assert template.uri_template == \"api://users/{user_id}\"\n\n        assert template.annotations is not None\n        assert hasattr(template.annotations, \"httpMethod\")\n        assert getattr(template.annotations, \"httpMethod\") == \"GET\"\n        assert hasattr(template.annotations, \"Cache-Control\")\n        assert getattr(template.annotations, \"Cache-Control\") == \"no-cache\"\n\n\nclass TestResourceTemplateContext:\n    async def test_resource_template_context(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def resource_template(param: str, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return f\"Resource template: {param} {ctx.request_id}\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"resource://test\"))\n            assert isinstance(result[0], TextResourceContents)\n            assert result[0].text.startswith(\"Resource template: test 1\")\n\n    async def test_resource_template_context_with_callable_object(self):\n        mcp = FastMCP()\n\n        class MyResource:\n            def __call__(self, param: str, ctx: Context) -> str:\n                return f\"Resource template: {param} {ctx.request_id}\"\n\n        template = ResourceTemplate.from_function(\n            MyResource(), uri_template=\"resource://{param}\"\n        )\n        mcp.add_template(template)\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"resource://test\"))\n            assert isinstance(result[0], TextResourceContents)\n            assert result[0].text.startswith(\"Resource template: test 1\")\n\n\nclass TestResourceDecorator:\n    async def test_no_resources_before_decorator(self):\n        mcp = FastMCP()\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://data\")\n\n    async def test_resource_decorator(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"Hello, world!\"\n\n    async def test_resource_decorator_incorrect_usage(self):\n        mcp = FastMCP()\n\n        with pytest.raises(\n            TypeError, match=\"The @resource decorator was used incorrectly\"\n        ):\n\n            @mcp.resource  # Missing parentheses #type: ignore\n            def get_data() -> str:\n                return \"Hello, world!\"\n\n    async def test_resource_decorator_with_name(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\", name=\"custom-data\")\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        resources = await mcp.list_resources()\n        assert len(resources) == 1\n        assert resources[0].name == \"custom-data\"\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"Hello, world!\"\n\n    async def test_resource_decorator_with_description(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\", description=\"Data resource\")\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        resources = await mcp.list_resources()\n        assert len(resources) == 1\n        assert resources[0].description == \"Data resource\"\n\n    async def test_resource_decorator_with_tags(self):\n        \"\"\"Test that the resource decorator properly sets tags.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\", tags={\"example\", \"test-tag\"})\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        resources = await mcp.list_resources()\n        assert len(resources) == 1\n        assert resources[0].tags == {\"example\", \"test-tag\"}\n\n    async def test_resource_decorator_instance_method(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            def __init__(self, prefix: str):\n                self.prefix = prefix\n\n            def get_data(self) -> str:\n                return f\"{self.prefix} Hello, world!\"\n\n        obj = MyClass(\"My prefix:\")\n\n        mcp.add_resource(\n            Resource.from_function(\n                obj.get_data, uri=\"resource://data\", name=\"instance-resource\"\n            )\n        )\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"My prefix: Hello, world!\"\n\n    async def test_resource_decorator_classmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            prefix = \"Class prefix:\"\n\n            @classmethod\n            def get_data(cls) -> str:\n                return f\"{cls.prefix} Hello, world!\"\n\n        mcp.add_resource(\n            Resource.from_function(\n                MyClass.get_data, uri=\"resource://data\", name=\"class-resource\"\n            )\n        )\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"Class prefix: Hello, world!\"\n\n    async def test_resource_decorator_classmethod_error(self):\n        mcp = FastMCP()\n\n        with pytest.raises(TypeError, match=\"classmethod\"):\n\n            class MyClass:\n                @mcp.resource(\"resource://data\")\n                @classmethod\n                def get_data(cls) -> None:\n                    pass\n\n    async def test_resource_decorator_staticmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.resource(\"resource://data\")\n            @staticmethod\n            def get_data() -> str:\n                return \"Static Hello, world!\"\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"Static Hello, world!\"\n\n    async def test_resource_decorator_async_function(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        async def get_data() -> str:\n            return \"Async Hello, world!\"\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"Async Hello, world!\"\n\n    async def test_resource_decorator_staticmethod_order(self):\n        \"\"\"Test that both decorator orders work for static methods\"\"\"\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.resource(\"resource://data\")\n            @staticmethod\n            def get_data() -> str:\n                return \"Static Hello, world!\"\n\n        result = await mcp.read_resource(\"resource://data\")\n        assert result.contents[0].content == \"Static Hello, world!\"\n\n    async def test_resource_decorator_with_meta(self):\n        \"\"\"Test that meta parameter is passed through the resource decorator.\"\"\"\n        mcp = FastMCP()\n\n        meta_data = {\"version\": \"1.0\", \"author\": \"test\"}\n\n        @mcp.resource(\"resource://data\", meta=meta_data)\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        resources = await mcp.list_resources()\n        resource = next(r for r in resources if str(r.uri) == \"resource://data\")\n\n        assert resource.meta == meta_data\n\n    async def test_resource_content_with_meta_in_response(self):\n        \"\"\"Test that ResourceContent meta is passed through.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://widget\")\n        def get_widget() -> ResourceResult:\n            return ResourceResult(\n                [\n                    ResourceContent(\n                        content=\"<widget>content</widget>\",\n                        mime_type=\"text/html\",\n                        meta={\"csp\": \"script-src 'self'\", \"version\": \"1.0\"},\n                    )\n                ]\n            )\n\n        result = await mcp.read_resource(\"resource://widget\")\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"<widget>content</widget>\"\n        assert result.contents[0].mime_type == \"text/html\"\n        assert result.contents[0].meta == {\"csp\": \"script-src 'self'\", \"version\": \"1.0\"}\n\n    async def test_resource_content_binary_with_meta(self):\n        \"\"\"Test that ResourceContent with binary content and meta works.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://binary\")\n        def get_binary() -> ResourceResult:\n            return ResourceResult(\n                [\n                    ResourceContent(\n                        content=b\"\\x00\\x01\\x02\",\n                        meta={\"encoding\": \"raw\"},\n                    )\n                ]\n            )\n\n        result = await mcp.read_resource(\"resource://binary\")\n        assert len(result.contents) == 1\n        assert result.contents[0].content == b\"\\x00\\x01\\x02\"\n        assert result.contents[0].meta == {\"encoding\": \"raw\"}\n\n    async def test_resource_content_without_meta(self):\n        \"\"\"Test that ResourceContent without meta works (meta is None).\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://plain\")\n        def get_plain() -> ResourceResult:\n            return ResourceResult([ResourceContent(content=\"plain content\")])\n\n        result = await mcp.read_resource(\"resource://plain\")\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"plain content\"\n        assert result.contents[0].meta is None\n\n\nclass TestTemplateDecorator:\n    async def test_template_decorator(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{name}/data\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].name == \"get_data\"\n        assert templates[0].uri_template == \"resource://{name}/data\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Data for test\"\n\n    async def test_template_decorator_incorrect_usage(self):\n        mcp = FastMCP()\n\n        with pytest.raises(\n            TypeError, match=\"The @resource decorator was used incorrectly\"\n        ):\n\n            @mcp.resource  # Missing parentheses #type: ignore\n            def get_data(name: str) -> str:\n                return f\"Data for {name}\"\n\n    async def test_template_decorator_with_name(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{name}/data\", name=\"custom-template\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].name == \"custom-template\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Data for test\"\n\n    async def test_template_decorator_with_description(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{name}/data\", description=\"Template description\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].description == \"Template description\"\n\n    async def test_template_decorator_instance_method(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            def __init__(self, prefix: str):\n                self.prefix = prefix\n\n            def get_data(self, name: str) -> str:\n                return f\"{self.prefix} Data for {name}\"\n\n        obj = MyClass(\"My prefix:\")\n        template = ResourceTemplate.from_function(\n            obj.get_data,\n            uri_template=\"resource://{name}/data\",\n            name=\"instance-template\",\n        )\n        mcp.add_template(template)\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"My prefix: Data for test\"\n\n    async def test_template_decorator_classmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            prefix = \"Class prefix:\"\n\n            @classmethod\n            def get_data(cls, name: str) -> str:\n                return f\"{cls.prefix} Data for {name}\"\n\n        template = ResourceTemplate.from_function(\n            MyClass.get_data,\n            uri_template=\"resource://{name}/data\",\n            name=\"class-template\",\n        )\n        mcp.add_template(template)\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Class prefix: Data for test\"\n\n    async def test_template_decorator_staticmethod(self):\n        mcp = FastMCP()\n\n        class MyClass:\n            @mcp.resource(\"resource://{name}/data\")\n            @staticmethod\n            def get_data(name: str) -> str:\n                return f\"Static Data for {name}\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Static Data for test\"\n\n    async def test_template_decorator_async_function(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{name}/data\")\n        async def get_data(name: str) -> str:\n            return f\"Async Data for {name}\"\n\n        result = await mcp.read_resource(\"resource://test/data\")\n        assert result.contents[0].content == \"Async Data for test\"\n\n    async def test_template_decorator_with_tags(self):\n        \"\"\"Test that the template decorator properly sets tags.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\", tags={\"template\", \"test-tag\"})\n        def template_resource(param: str) -> str:\n            return f\"Template resource: {param}\"\n\n        templates = await mcp.list_resource_templates()\n        template = next(t for t in templates if t.uri_template == \"resource://{param}\")\n        assert template.tags == {\"template\", \"test-tag\"}\n\n    async def test_template_decorator_wildcard_param(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param*}\")\n        def template_resource(param: str) -> str:\n            return f\"Template resource: {param}\"\n\n        templates = await mcp.list_resource_templates()\n        template = next(t for t in templates if t.uri_template == \"resource://{param*}\")\n        assert template.uri_template == \"resource://{param*}\"\n        assert template.name == \"template_resource\"\n\n    async def test_template_decorator_with_meta(self):\n        \"\"\"Test that meta parameter is passed through the template decorator.\"\"\"\n        mcp = FastMCP()\n\n        meta_data = {\"version\": \"2.0\", \"template\": \"test\"}\n\n        @mcp.resource(\"resource://{param}/data\", meta=meta_data)\n        def get_template_data(param: str) -> str:\n            return f\"Data for {param}\"\n\n        templates = await mcp.list_resource_templates()\n        template = next(\n            t for t in templates if t.uri_template == \"resource://{param}/data\"\n        )\n\n        assert template.meta == meta_data\n\n\nclass TestResourceTags:\n    def create_server(self, include_tags=None, exclude_tags=None):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://1\", tags={\"a\", \"b\"})\n        def resource_1() -> str:\n            return \"1\"\n\n        @mcp.resource(\"resource://2\", tags={\"b\", \"c\"})\n        def resource_2() -> str:\n            return \"2\"\n\n        if include_tags:\n            mcp.enable(tags=include_tags, only=True)\n        if exclude_tags:\n            mcp.disable(tags=exclude_tags)\n\n        return mcp\n\n    async def test_include_tags_all_resources(self):\n        mcp = self.create_server(include_tags={\"a\", \"b\"})\n        resources = await mcp.list_resources()\n        assert {r.name for r in resources} == {\"resource_1\", \"resource_2\"}\n\n    async def test_include_tags_some_resources(self):\n        mcp = self.create_server(include_tags={\"a\", \"z\"})\n        resources = await mcp.list_resources()\n        assert {r.name for r in resources} == {\"resource_1\"}\n\n    async def test_exclude_tags_all_resources(self):\n        mcp = self.create_server(exclude_tags={\"a\", \"b\"})\n        resources = await mcp.list_resources()\n        assert {r.name for r in resources} == set()\n\n    async def test_exclude_tags_some_resources(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        resources = await mcp.list_resources()\n        assert {r.name for r in resources} == {\"resource_2\"}\n\n    async def test_exclude_precedence(self):\n        mcp = self.create_server(exclude_tags={\"a\"}, include_tags={\"b\"})\n        resources = await mcp.list_resources()\n        assert {r.name for r in resources} == {\"resource_2\"}\n\n    async def test_read_included_resource(self):\n        mcp = self.create_server(include_tags={\"a\"})\n        result = await mcp.read_resource(\"resource://1\")\n        assert result.contents[0].content == \"1\"\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://2\")\n\n    async def test_read_excluded_resource(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://1\")\n\n\nclass TestResourceEnabled:\n    async def test_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def sample_resource() -> str:\n            return \"Hello, world!\"\n\n        resources = await mcp.list_resources()\n        assert any(str(r.uri) == \"resource://data\" for r in resources)\n\n        mcp.disable(names={\"resource://data\"}, components={\"resource\"})\n\n        resources = await mcp.list_resources()\n        assert not any(str(r.uri) == \"resource://data\" for r in resources)\n\n        mcp.enable(names={\"resource://data\"}, components={\"resource\"})\n\n        resources = await mcp.list_resources()\n        assert any(str(r.uri) == \"resource://data\" for r in resources)\n\n    async def test_resource_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def sample_resource() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"resource://data\"}, components={\"resource\"})\n        resources = await mcp.list_resources()\n        assert len(resources) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://data\")\n\n    async def test_resource_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def sample_resource() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"resource://data\"}, components={\"resource\"})\n        resources = await mcp.list_resources()\n        assert not any(str(r.uri) == \"resource://data\" for r in resources)\n\n        mcp.enable(names={\"resource://data\"}, components={\"resource\"})\n        resources = await mcp.list_resources()\n        assert len(resources) == 1\n\n    async def test_resource_toggle_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def sample_resource() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"resource://data\"}, components={\"resource\"})\n        resources = await mcp.list_resources()\n        assert len(resources) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://data\")\n\n    async def test_get_resource_and_disable(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def sample_resource() -> str:\n            return \"Hello, world!\"\n\n        resource = await mcp.get_resource(\"resource://data\")\n        assert resource is not None\n\n        mcp.disable(names={\"resource://data\"}, components={\"resource\"})\n        resources = await mcp.list_resources()\n        assert len(resources) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://data\")\n\n    async def test_cant_read_disabled_resource(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://data\")\n        def sample_resource() -> str:\n            return \"Hello, world!\"\n\n        mcp.disable(names={\"resource://data\"}, components={\"resource\"})\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://data\")\n\n\nclass TestResourceTemplatesTags:\n    def create_server(self, include_tags=None, exclude_tags=None):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://1/{param}\", tags={\"a\", \"b\"})\n        def template_resource_1(param: str) -> str:\n            return f\"Template resource 1: {param}\"\n\n        @mcp.resource(\"resource://2/{param}\", tags={\"b\", \"c\"})\n        def template_resource_2(param: str) -> str:\n            return f\"Template resource 2: {param}\"\n\n        if include_tags:\n            mcp.enable(tags=include_tags, only=True)\n        if exclude_tags:\n            mcp.disable(tags=exclude_tags)\n\n        return mcp\n\n    async def test_include_tags_all_resources(self):\n        mcp = self.create_server(include_tags={\"a\", \"b\"})\n        templates = await mcp.list_resource_templates()\n        assert {t.name for t in templates} == {\n            \"template_resource_1\",\n            \"template_resource_2\",\n        }\n\n    async def test_include_tags_some_resources(self):\n        mcp = self.create_server(include_tags={\"a\"})\n        templates = await mcp.list_resource_templates()\n        assert {t.name for t in templates} == {\"template_resource_1\"}\n\n    async def test_exclude_tags_all_resources(self):\n        mcp = self.create_server(exclude_tags={\"a\", \"b\"})\n        templates = await mcp.list_resource_templates()\n        assert {t.name for t in templates} == set()\n\n    async def test_exclude_tags_some_resources(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        templates = await mcp.list_resource_templates()\n        assert {t.name for t in templates} == {\"template_resource_2\"}\n\n    async def test_exclude_takes_precedence_over_include(self):\n        mcp = self.create_server(exclude_tags={\"a\"}, include_tags={\"b\"})\n        templates = await mcp.list_resource_templates()\n        assert {t.name for t in templates} == {\"template_resource_2\"}\n\n    async def test_read_resource_template_includes_tags(self):\n        mcp = self.create_server(include_tags={\"a\"})\n        result = await mcp.read_resource(\"resource://1/x\")\n        assert result.contents[0].content == \"Template resource 1: x\"\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://2/x\")\n\n    async def test_read_resource_template_excludes_tags(self):\n        mcp = self.create_server(exclude_tags={\"a\"})\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://1/x\")\n\n        result = await mcp.read_resource(\"resource://2/x\")\n        assert result.contents[0].content == \"Template resource 2: x\"\n\n\nclass TestResourceTemplateEnabled:\n    async def test_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def sample_template(param: str) -> str:\n            return f\"Template: {param}\"\n\n        templates = await mcp.list_resource_templates()\n        assert any(t.uri_template == \"resource://{param}\" for t in templates)\n\n        mcp.disable(names={\"resource://{param}\"}, components={\"template\"})\n\n        templates = await mcp.list_resource_templates()\n        assert not any(t.uri_template == \"resource://{param}\" for t in templates)\n\n        mcp.enable(names={\"resource://{param}\"}, components={\"template\"})\n\n        templates = await mcp.list_resource_templates()\n        assert any(t.uri_template == \"resource://{param}\" for t in templates)\n\n    async def test_template_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def sample_template(param: str) -> str:\n            return f\"Template: {param}\"\n\n        mcp.disable(names={\"resource://{param}\"}, components={\"template\"})\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://test\")\n\n    async def test_template_toggle_enabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def sample_template(param: str) -> str:\n            return f\"Template: {param}\"\n\n        mcp.disable(names={\"resource://{param}\"}, components={\"template\"})\n        templates = await mcp.list_resource_templates()\n        assert not any(t.uri_template == \"resource://{param}\" for t in templates)\n\n        mcp.enable(names={\"resource://{param}\"}, components={\"template\"})\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n\n    async def test_template_toggle_disabled(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def sample_template(param: str) -> str:\n            return f\"Template: {param}\"\n\n        mcp.disable(names={\"resource://{param}\"}, components={\"template\"})\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://test\")\n\n    async def test_get_template_and_disable(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def sample_template(param: str) -> str:\n            return f\"Template: {param}\"\n\n        template = await mcp.get_resource_template(\"resource://{param}\")\n        assert template is not None\n\n        mcp.disable(names={\"resource://{param}\"}, components={\"template\"})\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 0\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://test\")\n\n    async def test_cant_read_disabled_template(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://{param}\")\n        def sample_template(param: str) -> str:\n            return f\"Template: {param}\"\n\n        mcp.disable(names={\"resource://{param}\"}, components={\"template\"})\n\n        with pytest.raises(NotFoundError, match=\"Unknown resource\"):\n            await mcp.read_resource(\"resource://test\")\n"
  },
  {
    "path": "tests/server/providers/test_skills_provider.py",
    "content": "\"\"\"Tests for SkillProvider, SkillsDirectoryProvider, and ClaudeSkillsProvider.\"\"\"\n\nimport json\nfrom pathlib import Path\n\nimport pytest\nfrom mcp.types import TextResourceContents\nfrom pydantic import AnyUrl\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.providers.skills import (\n    ClaudeSkillsProvider,\n    SkillProvider,\n    SkillsDirectoryProvider,\n    SkillsProvider,\n)\nfrom fastmcp.server.providers.skills._common import parse_frontmatter\n\n\nclass TestParseFrontmatter:\n    def test_no_frontmatter(self):\n        content = \"# Just markdown\\n\\nSome content.\"\n        frontmatter, body = parse_frontmatter(content)\n        assert frontmatter == {}\n        assert body == content\n\n    def test_basic_frontmatter(self):\n        content = \"\"\"---\ndescription: A test skill\nversion: \"1.0.0\"\n---\n\n# Skill Content\n\"\"\"\n        frontmatter, body = parse_frontmatter(content)\n        assert frontmatter[\"description\"] == \"A test skill\"\n        assert frontmatter[\"version\"] == \"1.0.0\"\n        assert body.strip().startswith(\"# Skill Content\")\n\n    def test_frontmatter_with_tags_list(self):\n        content = \"\"\"---\ndescription: Test\ntags: [tag1, tag2, tag3]\n---\n\nContent\n\"\"\"\n        frontmatter, body = parse_frontmatter(content)\n        assert frontmatter[\"tags\"] == [\"tag1\", \"tag2\", \"tag3\"]\n\n    def test_frontmatter_with_quoted_strings(self):\n        content = \"\"\"---\ndescription: \"A skill with quotes\"\nversion: '2.0.0'\n---\n\nContent\n\"\"\"\n        frontmatter, body = parse_frontmatter(content)\n        assert frontmatter[\"description\"] == \"A skill with quotes\"\n        assert frontmatter[\"version\"] == \"2.0.0\"\n\n\nclass TestSkillProvider:\n    \"\"\"Tests for SkillProvider - single skill folder.\"\"\"\n\n    @pytest.fixture\n    def single_skill_dir(self, tmp_path: Path) -> Path:\n        \"\"\"Create a single skill directory with files.\"\"\"\n        skill_dir = tmp_path / \"my-skill\"\n        skill_dir.mkdir()\n        (skill_dir / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: A test skill\nversion: \"1.0.0\"\n---\n\n# My Skill\n\nThis is my skill content.\n\"\"\"\n        )\n        (skill_dir / \"reference.md\").write_text(\"# Reference\\n\\nExtra docs.\")\n        (skill_dir / \"scripts\").mkdir()\n        (skill_dir / \"scripts\" / \"helper.py\").write_text('print(\"helper\")')\n        return skill_dir\n\n    def test_loads_skill_at_init(self, single_skill_dir: Path):\n        provider = SkillProvider(skill_path=single_skill_dir)\n        assert provider.skill_info.name == \"my-skill\"\n        assert provider.skill_info.description == \"A test skill\"\n        assert len(provider.skill_info.files) == 3\n\n    def test_raises_if_directory_missing(self, tmp_path: Path):\n        with pytest.raises(FileNotFoundError, match=\"Skill directory not found\"):\n            SkillProvider(skill_path=tmp_path / \"nonexistent\")\n\n    def test_raises_if_main_file_missing(self, tmp_path: Path):\n        skill_dir = tmp_path / \"no-main\"\n        skill_dir.mkdir()\n        with pytest.raises(FileNotFoundError, match=\"Main skill file not found\"):\n            SkillProvider(skill_path=skill_dir)\n\n    async def test_list_resources_default_template_mode(self, single_skill_dir: Path):\n        \"\"\"In template mode (default), only main file and manifest are resources.\"\"\"\n        provider = SkillProvider(skill_path=single_skill_dir)\n        resources = await provider.list_resources()\n\n        assert len(resources) == 2\n        names = {r.name for r in resources}\n        assert \"my-skill/SKILL.md\" in names\n        assert \"my-skill/_manifest\" in names\n\n    async def test_list_resources_supporting_files_as_resources(\n        self, single_skill_dir: Path\n    ):\n        \"\"\"In resources mode, supporting files are also exposed as resources.\"\"\"\n        provider = SkillProvider(\n            skill_path=single_skill_dir, supporting_files=\"resources\"\n        )\n        resources = await provider.list_resources()\n\n        # 2 standard + 2 supporting files\n        assert len(resources) == 4\n        names = {r.name for r in resources}\n        assert \"my-skill/SKILL.md\" in names\n        assert \"my-skill/_manifest\" in names\n        assert \"my-skill/reference.md\" in names\n        assert \"my-skill/scripts/helper.py\" in names\n\n    async def test_list_templates_default_mode(self, single_skill_dir: Path):\n        \"\"\"In template mode (default), one template is exposed.\"\"\"\n        provider = SkillProvider(skill_path=single_skill_dir)\n        templates = await provider.list_resource_templates()\n\n        assert len(templates) == 1\n        assert templates[0].name == \"my-skill_files\"\n\n    async def test_list_templates_resources_mode(self, single_skill_dir: Path):\n        \"\"\"In resources mode, no templates are exposed.\"\"\"\n        provider = SkillProvider(\n            skill_path=single_skill_dir, supporting_files=\"resources\"\n        )\n        templates = await provider.list_resource_templates()\n\n        assert templates == []\n\n    async def test_read_main_file(self, single_skill_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillProvider(skill_path=single_skill_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"skill://my-skill/SKILL.md\"))\n            assert len(result) == 1\n            assert isinstance(result[0], TextResourceContents)\n            assert \"# My Skill\" in result[0].text\n\n    async def test_read_manifest(self, single_skill_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillProvider(skill_path=single_skill_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"skill://my-skill/_manifest\"))\n            manifest = json.loads(result[0].text)\n            assert manifest[\"skill\"] == \"my-skill\"\n            assert len(manifest[\"files\"]) == 3\n            paths = {f[\"path\"] for f in manifest[\"files\"]}\n            assert \"SKILL.md\" in paths\n            assert \"reference.md\" in paths\n            assert \"scripts/helper.py\" in paths\n\n    async def test_manifest_ignores_symlink_target_outside_skill(self, tmp_path: Path):\n        skill_dir = tmp_path / \"symlinked-skill\"\n        skill_dir.mkdir()\n        (skill_dir / \"SKILL.md\").write_text(\"# Skill\\n\")\n\n        outside_file = tmp_path / \"outside.txt\"\n        outside_file.write_text(\"secret\")\n        (skill_dir / \"leak.txt\").symlink_to(outside_file)\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillProvider(skill_path=skill_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\n                AnyUrl(\"skill://symlinked-skill/_manifest\")\n            )\n            manifest = json.loads(result[0].text)\n\n        paths = {f[\"path\"] for f in manifest[\"files\"]}\n        assert \"SKILL.md\" in paths\n        assert \"leak.txt\" not in paths\n\n    async def test_read_supporting_file_via_template(self, single_skill_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillProvider(skill_path=single_skill_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"skill://my-skill/reference.md\"))\n            assert \"# Reference\" in result[0].text\n\n    async def test_read_supporting_file_via_resource_mode(self, single_skill_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(\n            SkillProvider(skill_path=single_skill_dir, supporting_files=\"resources\")\n        )\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"skill://my-skill/reference.md\"))\n            assert \"# Reference\" in result[0].text\n\n    async def test_skill_resource_meta(self, single_skill_dir: Path):\n        \"\"\"SkillResource populates meta with skill name and is_manifest.\"\"\"\n        provider = SkillProvider(skill_path=single_skill_dir)\n        resources = await provider.list_resources()\n\n        by_name = {r.name: r for r in resources}\n\n        main_meta = by_name[\"my-skill/SKILL.md\"].get_meta()\n        assert main_meta[\"fastmcp\"][\"skill\"] == {\n            \"name\": \"my-skill\",\n            \"is_manifest\": False,\n        }\n\n        manifest_meta = by_name[\"my-skill/_manifest\"].get_meta()\n        assert manifest_meta[\"fastmcp\"][\"skill\"] == {\n            \"name\": \"my-skill\",\n            \"is_manifest\": True,\n        }\n\n    async def test_skill_file_resource_meta(self, single_skill_dir: Path):\n        \"\"\"SkillFileResource populates meta with skill name.\"\"\"\n        provider = SkillProvider(\n            skill_path=single_skill_dir, supporting_files=\"resources\"\n        )\n        resources = await provider.list_resources()\n\n        by_name = {r.name: r for r in resources}\n        file_meta = by_name[\"my-skill/reference.md\"].get_meta()\n        assert file_meta[\"fastmcp\"][\"skill\"] == {\"name\": \"my-skill\"}\n\n    async def test_skill_meta_survives_mounting(self, single_skill_dir: Path):\n        \"\"\"Skill metadata in _meta is preserved when accessed through a mounted server.\"\"\"\n        child = FastMCP(\"child\")\n        child.add_provider(SkillProvider(skill_path=single_skill_dir))\n\n        parent = FastMCP(\"parent\")\n        parent.mount(child, \"skills\")\n\n        resources = await parent.list_resources()\n        by_name = {r.name: r for r in resources}\n\n        main_meta = by_name[\"my-skill/SKILL.md\"].get_meta()\n        assert main_meta[\"fastmcp\"][\"skill\"] == {\n            \"name\": \"my-skill\",\n            \"is_manifest\": False,\n        }\n\n        manifest_meta = by_name[\"my-skill/_manifest\"].get_meta()\n        assert manifest_meta[\"fastmcp\"][\"skill\"] == {\n            \"name\": \"my-skill\",\n            \"is_manifest\": True,\n        }\n\n\nclass TestSkillsDirectoryProvider:\n    \"\"\"Tests for SkillsDirectoryProvider - scans directory for skill folders.\"\"\"\n\n    @pytest.fixture\n    def skills_dir(self, tmp_path: Path) -> Path:\n        \"\"\"Create a test skills directory with sample skills.\"\"\"\n        skills_root = tmp_path / \"skills\"\n        skills_root.mkdir()\n\n        # Create a simple skill\n        simple_skill = skills_root / \"simple-skill\"\n        simple_skill.mkdir()\n        (simple_skill / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: A simple test skill\nversion: \"1.0.0\"\n---\n\n# Simple Skill\n\nThis is a simple skill for testing.\n\"\"\"\n        )\n\n        # Create a skill with supporting files\n        complex_skill = skills_root / \"complex-skill\"\n        complex_skill.mkdir()\n        (complex_skill / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: A complex skill with supporting files\n---\n\n# Complex Skill\n\nSee [reference](reference.md) for more details.\n\"\"\"\n        )\n        (complex_skill / \"reference.md\").write_text(\n            \"\"\"# Reference\n\nAdditional documentation.\n\"\"\"\n        )\n        (complex_skill / \"scripts\").mkdir()\n        (complex_skill / \"scripts\" / \"helper.py\").write_text(\n            'print(\"Hello from helper\")'\n        )\n\n        return skills_root\n\n    async def test_list_resources_discovers_skills(self, skills_dir: Path):\n        provider = SkillsDirectoryProvider(roots=skills_dir)\n        resources = await provider.list_resources()\n\n        # Should have 2 resources per skill (main file + manifest)\n        assert len(resources) == 4\n\n        # Check resource names\n        resource_names = {r.name for r in resources}\n        assert \"simple-skill/SKILL.md\" in resource_names\n        assert \"simple-skill/_manifest\" in resource_names\n        assert \"complex-skill/SKILL.md\" in resource_names\n        assert \"complex-skill/_manifest\" in resource_names\n\n    async def test_list_resources_includes_descriptions(self, skills_dir: Path):\n        provider = SkillsDirectoryProvider(roots=skills_dir)\n        resources = await provider.list_resources()\n\n        # Find the simple-skill main resource\n        simple_skill = next(r for r in resources if r.name == \"simple-skill/SKILL.md\")\n        assert simple_skill.description == \"A simple test skill\"\n\n    async def test_read_main_skill_file(self, skills_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(AnyUrl(\"skill://simple-skill/SKILL.md\"))\n            assert len(result) == 1\n            assert isinstance(result[0], TextResourceContents)\n            assert \"# Simple Skill\" in result[0].text\n\n    async def test_read_manifest(self, skills_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\n                AnyUrl(\"skill://complex-skill/_manifest\")\n            )\n            assert len(result) == 1\n            assert isinstance(result[0], TextResourceContents)\n\n            manifest = json.loads(result[0].text)\n            assert manifest[\"skill\"] == \"complex-skill\"\n            assert len(manifest[\"files\"]) == 3  # SKILL.md, reference.md, helper.py\n\n            # Check file paths\n            paths = {f[\"path\"] for f in manifest[\"files\"]}\n            assert \"SKILL.md\" in paths\n            assert \"reference.md\" in paths\n            assert \"scripts/helper.py\" in paths\n\n            # Check hashes are present\n            for file_info in manifest[\"files\"]:\n                assert file_info[\"hash\"].startswith(\"sha256:\")\n                assert file_info[\"size\"] > 0\n\n    async def test_list_resource_templates(self, skills_dir: Path):\n        provider = SkillsDirectoryProvider(roots=skills_dir)\n        templates = await provider.list_resource_templates()\n\n        # One template per skill\n        assert len(templates) == 2\n\n        template_names = {t.name for t in templates}\n        assert \"simple-skill_files\" in template_names\n        assert \"complex-skill_files\" in template_names\n\n    async def test_read_supporting_file_via_template(self, skills_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\n                AnyUrl(\"skill://complex-skill/reference.md\")\n            )\n            assert len(result) == 1\n            assert isinstance(result[0], TextResourceContents)\n            assert \"# Reference\" in result[0].text\n\n    async def test_read_nested_file_via_template(self, skills_dir: Path):\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\n                AnyUrl(\"skill://complex-skill/scripts/helper.py\")\n            )\n            assert len(result) == 1\n            assert isinstance(result[0], TextResourceContents)\n            assert \"Hello from helper\" in result[0].text\n\n    async def test_empty_skills_directory(self, tmp_path: Path):\n        empty_dir = tmp_path / \"empty\"\n        empty_dir.mkdir()\n\n        provider = SkillsDirectoryProvider(roots=empty_dir)\n        resources = await provider.list_resources()\n        assert resources == []\n\n        templates = await provider.list_resource_templates()\n        assert templates == []\n\n    async def test_nonexistent_skills_directory(self, tmp_path: Path):\n        nonexistent = tmp_path / \"does-not-exist\"\n        provider = SkillsDirectoryProvider(roots=nonexistent)\n\n        resources = await provider.list_resources()\n        assert resources == []\n\n    async def test_reload_mode(self, skills_dir: Path):\n        provider = SkillsDirectoryProvider(roots=skills_dir, reload=True)\n\n        # Initial load\n        resources = await provider.list_resources()\n        assert len(resources) == 4\n\n        # Add a new skill\n        new_skill = skills_dir / \"new-skill\"\n        new_skill.mkdir()\n        (new_skill / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: A new skill\n---\n\n# New Skill\n\"\"\"\n        )\n\n        # Reload should pick up the new skill\n        resources = await provider.list_resources()\n        assert len(resources) == 6\n\n    async def test_skill_without_frontmatter_uses_header_as_description(\n        self, tmp_path: Path\n    ):\n        skills_dir = tmp_path / \"skills\"\n        skills_dir.mkdir()\n        skill = skills_dir / \"no-frontmatter\"\n        skill.mkdir()\n        (skill / \"SKILL.md\").write_text(\"# My Skill Title\\n\\nSome content.\")\n\n        provider = SkillsDirectoryProvider(roots=skills_dir)\n        resources = await provider.list_resources()\n\n        main_resource = next(\n            r for r in resources if r.name == \"no-frontmatter/SKILL.md\"\n        )\n        assert main_resource.description == \"My Skill Title\"\n\n    async def test_supporting_files_as_resources(self, skills_dir: Path):\n        \"\"\"Test that supporting_files='resources' shows all files.\"\"\"\n        provider = SkillsDirectoryProvider(\n            roots=skills_dir, supporting_files=\"resources\"\n        )\n        resources = await provider.list_resources()\n\n        # 2 skills * 2 standard resources + complex skill has 2 supporting files\n        # simple-skill: SKILL.md, _manifest (2)\n        # complex-skill: SKILL.md, _manifest, reference.md, scripts/helper.py (4)\n        assert len(resources) == 6\n\n        names = {r.name for r in resources}\n        assert \"complex-skill/reference.md\" in names\n        assert \"complex-skill/scripts/helper.py\" in names\n\n    async def test_supporting_files_as_resources_no_templates(self, skills_dir: Path):\n        \"\"\"In resources mode, no templates should be exposed.\"\"\"\n        provider = SkillsDirectoryProvider(\n            roots=skills_dir, supporting_files=\"resources\"\n        )\n        templates = await provider.list_resource_templates()\n        assert templates == []\n\n\nclass TestMultiDirectoryProvider:\n    \"\"\"Tests for multi-directory support in SkillsDirectoryProvider.\"\"\"\n\n    @pytest.fixture\n    def multi_skills_dirs(self, tmp_path: Path) -> tuple[Path, Path]:\n        \"\"\"Create two separate skills directories.\"\"\"\n        root1 = tmp_path / \"skills1\"\n        root1.mkdir()\n        skill1 = root1 / \"skill-a\"\n        skill1.mkdir()\n        (skill1 / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: Skill A from root 1\n---\n# Skill A\n\"\"\"\n        )\n\n        root2 = tmp_path / \"skills2\"\n        root2.mkdir()\n        skill2 = root2 / \"skill-b\"\n        skill2.mkdir()\n        (skill2 / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: Skill B from root 2\n---\n# Skill B\n\"\"\"\n        )\n\n        return root1, root2\n\n    async def test_multiple_roots_discover_all_skills(self, multi_skills_dirs):\n        \"\"\"Test that skills from multiple roots are all discovered.\"\"\"\n        root1, root2 = multi_skills_dirs\n        provider = SkillsDirectoryProvider(roots=[root1, root2])\n\n        resources = await provider.list_resources()\n        # 2 skills * 2 resources each = 4 total\n        assert len(resources) == 4\n\n        resource_names = {r.name for r in resources}\n        assert \"skill-a/SKILL.md\" in resource_names\n        assert \"skill-a/_manifest\" in resource_names\n        assert \"skill-b/SKILL.md\" in resource_names\n        assert \"skill-b/_manifest\" in resource_names\n\n    async def test_duplicate_skill_names_first_wins(self, tmp_path: Path):\n        \"\"\"Test that if a skill appears in multiple roots, first one wins.\"\"\"\n        root1 = tmp_path / \"root1\"\n        root1.mkdir()\n        skill1 = root1 / \"duplicate-skill\"\n        skill1.mkdir()\n        (skill1 / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: First occurrence\n---\n# First\n\"\"\"\n        )\n\n        root2 = tmp_path / \"root2\"\n        root2.mkdir()\n        skill2 = root2 / \"duplicate-skill\"\n        skill2.mkdir()\n        (skill2 / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: Second occurrence\n---\n# Second\n\"\"\"\n        )\n\n        provider = SkillsDirectoryProvider(roots=[root1, root2])\n        resources = await provider.list_resources()\n\n        # Should only have one skill (first one)\n        assert len(resources) == 2  # SKILL.md + _manifest\n\n        # Should be the first one\n        main_resource = next(\n            r for r in resources if r.name == \"duplicate-skill/SKILL.md\"\n        )\n        assert main_resource.description == \"First occurrence\"\n\n    async def test_single_path_as_list(self, multi_skills_dirs):\n        \"\"\"Test that single path can be passed as a list.\"\"\"\n        root1, _ = multi_skills_dirs\n        provider = SkillsDirectoryProvider(roots=[root1])\n\n        resources = await provider.list_resources()\n        assert len(resources) == 2  # skill-a has 2 resources\n\n    async def test_single_path_as_string(self, multi_skills_dirs):\n        \"\"\"Test that single path can be passed as string.\"\"\"\n        root1, _ = multi_skills_dirs\n        provider = SkillsDirectoryProvider(roots=str(root1))\n\n        resources = await provider.list_resources()\n        assert len(resources) == 2\n\n    async def test_nonexistent_roots_handled_gracefully(self, tmp_path: Path):\n        \"\"\"Test that non-existent roots don't cause errors.\"\"\"\n        existent = tmp_path / \"exists\"\n        existent.mkdir()\n        skill = existent / \"test-skill\"\n        skill.mkdir()\n        (skill / \"SKILL.md\").write_text(\"# Test\\n\\nContent\")\n\n        nonexistent = tmp_path / \"does-not-exist\"\n\n        provider = SkillsDirectoryProvider(roots=[existent, nonexistent])\n        resources = await provider.list_resources()\n\n        # Should still find skills from existing root\n        assert len(resources) == 2\n\n    async def test_empty_roots_list(self, tmp_path: Path):\n        \"\"\"Test that empty roots list results in no skills.\"\"\"\n        provider = SkillsDirectoryProvider(roots=[])\n        resources = await provider.list_resources()\n        assert resources == []\n\n\nclass TestSkillsProviderAlias:\n    \"\"\"Test that SkillsProvider is a backwards-compatible alias.\"\"\"\n\n    def test_skills_provider_is_alias(self):\n        assert SkillsProvider is SkillsDirectoryProvider\n\n\nclass TestClaudeSkillsProvider:\n    def test_default_root_is_claude_skills_dir(self, tmp_path: Path, monkeypatch):\n        # Mock Path.home() to return a temp path (use tmp_path for cross-platform compatibility)\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = ClaudeSkillsProvider()\n        assert provider._roots == [tmp_path / \".claude\" / \"skills\"]\n\n    def test_main_file_name_is_skill_md(self):\n        provider = ClaudeSkillsProvider()\n        assert provider._main_file_name == \"SKILL.md\"\n\n    def test_supporting_files_parameter(self):\n        provider = ClaudeSkillsProvider(supporting_files=\"resources\")\n        assert provider._supporting_files == \"resources\"\n\n\nclass TestPathTraversalPrevention:\n    async def test_path_traversal_blocked(self, tmp_path: Path):\n        skills_dir = tmp_path / \"skills\"\n        skills_dir.mkdir()\n        skill = skills_dir / \"test-skill\"\n        skill.mkdir()\n        (skill / \"SKILL.md\").write_text(\"# Test\\n\\nContent\")\n\n        # Create a file outside the skill directory\n        secret_file = tmp_path / \"secret.txt\"\n        secret_file.write_text(\"SECRET DATA\")\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n\n        async with Client(mcp) as client:\n            # Path traversal attempts should fail (either normalized away or blocked)\n            # The important thing is that SECRET DATA is never returned\n            with pytest.raises(Exception):\n                result = await client.read_resource(\n                    AnyUrl(\"skill://test-skill/../../../secret.txt\")\n                )\n                # If we somehow got here, ensure we didn't get the secret\n                if result:\n                    for content in result:\n                        if hasattr(content, \"text\"):\n                            assert \"SECRET DATA\" not in content.text\n"
  },
  {
    "path": "tests/server/providers/test_skills_vendor_providers.py",
    "content": "\"\"\"Tests for vendor-specific skills providers.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom fastmcp.server.providers.skills import (\n    ClaudeSkillsProvider,\n    CodexSkillsProvider,\n    CopilotSkillsProvider,\n    CursorSkillsProvider,\n    GeminiSkillsProvider,\n    GooseSkillsProvider,\n    OpenCodeSkillsProvider,\n    VSCodeSkillsProvider,\n)\n\n\nclass TestVendorProviders:\n    \"\"\"Tests for vendor-specific skills providers.\"\"\"\n\n    def test_cursor_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test CursorSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = CursorSkillsProvider()\n        assert provider._roots == [tmp_path / \".cursor\" / \"skills\"]\n\n    def test_vscode_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test VSCodeSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = VSCodeSkillsProvider()\n        assert provider._roots == [tmp_path / \".copilot\" / \"skills\"]\n\n    def test_codex_skills_provider_paths(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test CodexSkillsProvider uses both system and user paths.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = CodexSkillsProvider()\n        # Path.resolve() may add /private on macOS, so compare resolved paths\n        expected_roots = [\n            Path(\"/etc/codex/skills\").resolve(),\n            (tmp_path / \".codex\" / \"skills\").resolve(),\n        ]\n        assert provider._roots == expected_roots\n\n    def test_gemini_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test GeminiSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = GeminiSkillsProvider()\n        assert provider._roots == [tmp_path / \".gemini\" / \"skills\"]\n\n    def test_goose_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test GooseSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = GooseSkillsProvider()\n        assert provider._roots == [tmp_path / \".config\" / \"agents\" / \"skills\"]\n\n    def test_copilot_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test CopilotSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = CopilotSkillsProvider()\n        assert provider._roots == [tmp_path / \".copilot\" / \"skills\"]\n\n    def test_opencode_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test OpenCodeSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = OpenCodeSkillsProvider()\n        assert provider._roots == [tmp_path / \".config\" / \"opencode\" / \"skills\"]\n\n    def test_claude_skills_provider_path(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test ClaudeSkillsProvider uses correct path.\"\"\"\n        monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n        provider = ClaudeSkillsProvider()\n        assert provider._roots == [tmp_path / \".claude\" / \"skills\"]\n\n    def test_all_providers_instantiable(self):\n        \"\"\"Test that all vendor providers can be instantiated.\"\"\"\n        providers = [\n            ClaudeSkillsProvider(),\n            CursorSkillsProvider(),\n            VSCodeSkillsProvider(),\n            CodexSkillsProvider(),\n            GeminiSkillsProvider(),\n            GooseSkillsProvider(),\n            CopilotSkillsProvider(),\n            OpenCodeSkillsProvider(),\n        ]\n\n        for provider in providers:\n            assert provider is not None\n            assert provider._main_file_name == \"SKILL.md\"\n\n    def test_all_providers_support_reload(self):\n        \"\"\"Test that all providers support reload parameter.\"\"\"\n        providers = [\n            ClaudeSkillsProvider(reload=True),\n            CursorSkillsProvider(reload=True),\n            VSCodeSkillsProvider(reload=True),\n            CodexSkillsProvider(reload=True),\n            GeminiSkillsProvider(reload=True),\n            GooseSkillsProvider(reload=True),\n            CopilotSkillsProvider(reload=True),\n            OpenCodeSkillsProvider(reload=True),\n        ]\n\n        for provider in providers:\n            assert provider._reload is True\n\n    def test_all_providers_support_supporting_files(self):\n        \"\"\"Test that all providers support supporting_files parameter.\"\"\"\n        providers = [\n            ClaudeSkillsProvider(supporting_files=\"resources\"),\n            CursorSkillsProvider(supporting_files=\"resources\"),\n            VSCodeSkillsProvider(supporting_files=\"resources\"),\n            CodexSkillsProvider(supporting_files=\"resources\"),\n            GeminiSkillsProvider(supporting_files=\"resources\"),\n            GooseSkillsProvider(supporting_files=\"resources\"),\n            CopilotSkillsProvider(supporting_files=\"resources\"),\n            OpenCodeSkillsProvider(supporting_files=\"resources\"),\n        ]\n\n        for provider in providers:\n            assert provider._supporting_files == \"resources\"\n\n    async def test_codex_scans_both_paths(self, tmp_path: Path, monkeypatch):\n        \"\"\"Test that CodexSkillsProvider scans both system and user paths.\"\"\"\n        # Mock system path\n        system_skills = tmp_path / \"etc\" / \"codex\" / \"skills\"\n        system_skills.mkdir(parents=True)\n        system_skill = system_skills / \"system-skill\"\n        system_skill.mkdir()\n        (system_skill / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: System skill\n---\n# System\n\"\"\"\n        )\n\n        # Mock user path\n        fake_home = tmp_path / \"home\" / \"user\"\n        fake_home.mkdir(parents=True)\n        monkeypatch.setattr(Path, \"home\", lambda: fake_home)\n\n        user_skills = fake_home / \".codex\" / \"skills\"\n        user_skills.mkdir(parents=True)\n        user_skill = user_skills / \"user-skill\"\n        user_skill.mkdir()\n        (user_skill / \"SKILL.md\").write_text(\n            \"\"\"---\ndescription: User skill\n---\n# User\n\"\"\"\n        )\n\n        # Create provider with mocked paths\n        # Override roots before discovery\n        provider = CodexSkillsProvider()\n        provider._roots = [system_skills, user_skills]\n        # Trigger re-discovery with new roots\n        provider._discover_skills()\n\n        resources = await provider.list_resources()\n        # Should find both skills\n        assert len(resources) == 4  # 2 skills * 2 resources each\n\n        resource_names = {r.name for r in resources}\n        assert \"system-skill/SKILL.md\" in resource_names\n        assert \"user-skill/SKILL.md\" in resource_names\n\n    async def test_nonexistent_paths_handled_gracefully(\n        self, tmp_path: Path, monkeypatch\n    ):\n        \"\"\"Test that non-existent paths don't cause errors.\"\"\"\n        # Use a path that definitely doesn't exist\n        nonexistent_home = tmp_path / \"nonexistent\" / \"home\"\n        monkeypatch.setattr(Path, \"home\", lambda: nonexistent_home)\n\n        # All providers should handle non-existent paths gracefully\n        providers = [\n            ClaudeSkillsProvider(),\n            CursorSkillsProvider(),\n            VSCodeSkillsProvider(),\n            GeminiSkillsProvider(),\n            GooseSkillsProvider(),\n            CopilotSkillsProvider(),\n            OpenCodeSkillsProvider(),\n        ]\n\n        for provider in providers:\n            resources = await provider.list_resources()\n            # Should return empty list, not raise exception\n            assert isinstance(resources, list)\n"
  },
  {
    "path": "tests/server/providers/test_transforming_provider.py",
    "content": "\"\"\"Tests for Namespace and ToolTransform.\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.providers import FastMCPProvider\nfrom fastmcp.server.transforms import Namespace, ToolTransform\nfrom fastmcp.tools.tool_transform import ToolTransformConfig\n\n\nclass TestNamespaceTransform:\n    \"\"\"Test Namespace transform transformations.\"\"\"\n\n    async def test_namespace_prefixes_tool_names(self):\n        \"\"\"Test that namespace is applied as prefix to tool names.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        # Get tools and pass directly to transform\n        tools = await provider.list_tools()\n        transformed_tools = await layer.list_tools(tools)\n\n        assert len(transformed_tools) == 1\n        assert transformed_tools[0].name == \"ns_my_tool\"\n\n    async def test_namespace_prefixes_prompt_names(self):\n        \"\"\"Test that namespace is applied as prefix to prompt names.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.prompt\n        def my_prompt() -> str:\n            return \"prompt content\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        prompts = await provider.list_prompts()\n        transformed_prompts = await layer.list_prompts(prompts)\n\n        assert len(transformed_prompts) == 1\n        assert transformed_prompts[0].name == \"ns_my_prompt\"\n\n    async def test_namespace_prefixes_resource_uris(self):\n        \"\"\"Test that namespace is inserted into resource URIs.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://data\")\n        def my_resource() -> str:\n            return \"content\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        resources = await provider.list_resources()\n        transformed_resources = await layer.list_resources(resources)\n\n        assert len(transformed_resources) == 1\n        assert str(transformed_resources[0].uri) == \"resource://ns/data\"\n\n    async def test_namespace_prefixes_template_uris(self):\n        \"\"\"Test that namespace is inserted into resource template URIs.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://{name}/data\")\n        def my_template(name: str) -> str:\n            return f\"content for {name}\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        templates = await provider.list_resource_templates()\n        transformed_templates = await layer.list_resource_templates(templates)\n\n        assert len(transformed_templates) == 1\n        assert transformed_templates[0].uri_template == \"resource://ns/{name}/data\"\n\n\nclass TestToolTransformRenames:\n    \"\"\"Test ToolTransform renaming functionality.\"\"\"\n\n    async def test_tool_rename(self):\n        \"\"\"Test tool renaming with ToolTransform.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def verbose_tool_name() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        layer = ToolTransform({\"verbose_tool_name\": ToolTransformConfig(name=\"short\")})\n\n        tools = await provider.list_tools()\n        transformed_tools = await layer.list_tools(tools)\n\n        assert len(transformed_tools) == 1\n        assert transformed_tools[0].name == \"short\"\n\n    async def test_renamed_tool_is_callable_via_mount(self):\n        \"\"\"Test that renamed tools can be called by new name via mount.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def original() -> str:\n            return \"success\"\n\n        main = FastMCP(\"Main\")\n        # Add provider with transform layer\n        provider = FastMCPProvider(sub)\n        provider.add_transform(\n            ToolTransform({\"original\": ToolTransformConfig(name=\"renamed\")})\n        )\n        main.add_provider(provider)\n\n        async with Client(main) as client:\n            result = await client.call_tool(\"renamed\", {})\n            assert result.data == \"success\"\n\n    def test_duplicate_rename_targets_raises_error(self):\n        \"\"\"Test that duplicate target names in ToolTransform raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"duplicate target name\"):\n            ToolTransform(\n                {\n                    \"tool_a\": ToolTransformConfig(name=\"same\"),\n                    \"tool_b\": ToolTransformConfig(name=\"same\"),\n                }\n            )\n\n\nclass TestTransformReverseLookup:\n    \"\"\"Test reverse lookups for routing.\"\"\"\n\n    async def test_namespace_get_tool(self):\n        \"\"\"Test that tools can be looked up by transformed name.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        # Create call_next that delegates to provider\n        async def get_tool(name: str, version=None):\n            return await provider._get_tool(name, version)\n\n        tool = await layer.get_tool(\"ns_my_tool\", get_tool)\n\n        assert tool is not None\n        assert tool.name == \"ns_my_tool\"\n\n    async def test_transform_layer_get_tool(self):\n        \"\"\"Test that renamed tools can be looked up by new name.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def original() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        layer = ToolTransform({\"original\": ToolTransformConfig(name=\"renamed\")})\n\n        async def get_tool(name: str, version=None):\n            return await provider._get_tool(name, version)\n\n        tool = await layer.get_tool(\"renamed\", get_tool)\n\n        assert tool is not None\n        assert tool.name == \"renamed\"\n\n    async def test_namespace_get_resource(self):\n        \"\"\"Test that resources can be looked up by transformed URI.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.resource(\"resource://data\")\n        def my_resource() -> str:\n            return \"content\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        async def get_resource(uri: str, version=None):\n            return await provider._get_resource(uri, version)\n\n        resource = await layer.get_resource(\"resource://ns/data\", get_resource)\n\n        assert resource is not None\n        assert str(resource.uri) == \"resource://ns/data\"\n\n    async def test_nonmatching_namespace_returns_none(self):\n        \"\"\"Test that lookups with wrong namespace return None.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        layer = Namespace(\"ns\")\n\n        async def get_tool(name: str, version=None):\n            return await provider._get_tool(name, version)\n\n        # Wrong namespace prefix\n        assert await layer.get_tool(\"wrong_my_tool\", get_tool) is None\n        # No prefix at all\n        assert await layer.get_tool(\"my_tool\", get_tool) is None\n\n\nclass TestTransformStacking:\n    \"\"\"Test stacking multiple transforms via provider add_transform.\"\"\"\n\n    async def test_stacked_namespaces_compose(self):\n        \"\"\"Test that stacked namespaces are applied in order.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        inner_layer = Namespace(\"inner\")\n        outer_layer = Namespace(\"outer\")\n\n        # Apply transforms sequentially: base -> inner -> outer\n        tools = await provider.list_tools()\n        tools = await inner_layer.list_tools(tools)\n        tools = await outer_layer.list_tools(tools)\n\n        assert len(tools) == 1\n        assert tools[0].name == \"outer_inner_my_tool\"\n\n    async def test_stacked_transforms_are_callable(self):\n        \"\"\"Test that stacked transforms still allow tool calls.\"\"\"\n        sub = FastMCP(\"Sub\")\n\n        @sub.tool\n        def my_tool() -> str:\n            return \"success\"\n\n        main = FastMCP(\"Main\")\n        provider = FastMCPProvider(sub)\n        # Add namespace layer then rename layer\n        provider.add_transform(Namespace(\"ns\"))\n        provider.add_transform(\n            ToolTransform({\"ns_my_tool\": ToolTransformConfig(name=\"short\")})\n        )\n        main.add_provider(provider)\n\n        async with Client(main) as client:\n            result = await client.call_tool(\"short\", {})\n            assert result.data == \"success\"\n\n\nclass TestNoTransformation:\n    \"\"\"Test behavior when no transformations are applied.\"\"\"\n\n    async def test_transform_passthrough(self):\n        \"\"\"Test that base Transform passes through unchanged.\"\"\"\n        from fastmcp.server.transforms import Transform\n\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        transform = Transform()\n\n        tools = await provider.list_tools()\n        transformed_tools = await transform.list_tools(tools)\n\n        assert len(transformed_tools) == 1\n        assert transformed_tools[0].name == \"my_tool\"\n\n    async def test_empty_transform_layer_passthrough(self):\n        \"\"\"Test that empty ToolTransform has no effect.\"\"\"\n        server = FastMCP(\"Test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        provider = FastMCPProvider(server)\n        layer = ToolTransform({})\n\n        tools = await provider.list_tools()\n        transformed_tools = await layer.list_tools(tools)\n\n        assert len(transformed_tools) == 1\n        assert transformed_tools[0].name == \"my_tool\"\n"
  },
  {
    "path": "tests/server/sampling/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/sampling/test_prepare_tools.py",
    "content": "\"\"\"Tests for prepare_tools helper function.\"\"\"\n\nimport pytest\n\nfrom fastmcp.server.sampling.run import prepare_tools\nfrom fastmcp.server.sampling.sampling_tool import SamplingTool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import ArgTransform, TransformedTool\n\n\nclass TestPrepareTools:\n    \"\"\"Tests for prepare_tools().\"\"\"\n\n    def test_prepare_tools_with_none(self):\n        \"\"\"Test that None returns None.\"\"\"\n        result = prepare_tools(None)\n        assert result is None\n\n    def test_prepare_tools_with_sampling_tool(self):\n        \"\"\"Test that SamplingTool instances pass through.\"\"\"\n\n        def search(query: str) -> str:\n            return f\"Results: {query}\"\n\n        sampling_tool = SamplingTool.from_function(search)\n        result = prepare_tools([sampling_tool])\n\n        assert result is not None\n        assert len(result) == 1\n        assert result[0] is sampling_tool\n\n    def test_prepare_tools_with_function(self):\n        \"\"\"Test that plain functions are converted.\"\"\"\n\n        def search(query: str) -> str:\n            \"\"\"Search function.\"\"\"\n            return f\"Results: {query}\"\n\n        result = prepare_tools([search])\n\n        assert result is not None\n        assert len(result) == 1\n        assert isinstance(result[0], SamplingTool)\n        assert result[0].name == \"search\"\n\n    def test_prepare_tools_with_function_tool(self):\n        \"\"\"Test that FunctionTool instances are converted.\"\"\"\n\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results: {query}\"\n\n        function_tool = FunctionTool.from_function(search)\n        result = prepare_tools([function_tool])\n\n        assert result is not None\n        assert len(result) == 1\n        assert isinstance(result[0], SamplingTool)\n        assert result[0].name == \"search\"\n        assert result[0].description == \"Search the web.\"\n\n    def test_prepare_tools_with_transformed_tool(self):\n        \"\"\"Test that TransformedTool instances are converted.\"\"\"\n\n        def original(query: str) -> str:\n            \"\"\"Original tool.\"\"\"\n            return f\"Results: {query}\"\n\n        function_tool = FunctionTool.from_function(original)\n        transformed_tool = TransformedTool.from_tool(\n            function_tool,\n            name=\"search_v2\",\n            transform_args={\"query\": ArgTransform(name=\"q\")},\n        )\n\n        result = prepare_tools([transformed_tool])\n\n        assert result is not None\n        assert len(result) == 1\n        assert isinstance(result[0], SamplingTool)\n        assert result[0].name == \"search_v2\"\n        assert \"q\" in result[0].parameters.get(\"properties\", {})\n\n    def test_prepare_tools_with_mixed_types(self):\n        \"\"\"Test that mixed tool types are all converted.\"\"\"\n\n        def plain_fn(x: int) -> int:\n            return x * 2\n\n        def fn_for_tool(y: int) -> int:\n            return y * 3\n\n        function_tool = FunctionTool.from_function(fn_for_tool)\n        sampling_tool = SamplingTool.from_function(lambda z: z * 4, name=\"lambda_tool\")\n\n        result = prepare_tools([plain_fn, function_tool, sampling_tool])\n\n        assert result is not None\n        assert len(result) == 3\n        assert all(isinstance(t, SamplingTool) for t in result)\n\n    def test_prepare_tools_with_invalid_type(self):\n        \"\"\"Test that invalid types raise TypeError.\"\"\"\n\n        with pytest.raises(TypeError, match=\"Expected SamplingTool, FunctionTool\"):\n            prepare_tools([\"not a tool\"])  # type: ignore[arg-type]\n\n    def test_prepare_tools_empty_list(self):\n        \"\"\"Test that empty list returns None.\"\"\"\n        result = prepare_tools([])\n        assert result is None\n"
  },
  {
    "path": "tests/server/sampling/test_sampling_tool.py",
    "content": "\"\"\"Tests for SamplingTool.\"\"\"\n\nimport pytest\nfrom mcp.server.auth.middleware.auth_context import auth_context_var\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\n\nfrom fastmcp.exceptions import AuthorizationError\nfrom fastmcp.server.auth import AccessToken, require_scopes\nfrom fastmcp.server.context import _current_transport\nfrom fastmcp.server.sampling import SamplingTool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import ArgTransform, TransformedTool\n\n\nclass TestSamplingToolFromFunction:\n    \"\"\"Tests for SamplingTool.from_function().\"\"\"\n\n    def test_from_simple_function(self):\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results for: {query}\"\n\n        tool = SamplingTool.from_function(search)\n\n        assert tool.name == \"search\"\n        assert tool.description == \"Search the web.\"\n        assert \"query\" in tool.parameters.get(\"properties\", {})\n        assert tool.fn is search\n\n    def test_from_function_with_overrides(self):\n        def search(query: str) -> str:\n            return f\"Results for: {query}\"\n\n        tool = SamplingTool.from_function(\n            search,\n            name=\"web_search\",\n            description=\"Search the internet\",\n        )\n\n        assert tool.name == \"web_search\"\n        assert tool.description == \"Search the internet\"\n\n    def test_from_lambda_requires_name(self):\n        with pytest.raises(ValueError, match=\"must provide a name for lambda\"):\n            SamplingTool.from_function(lambda x: x)\n\n    def test_from_lambda_with_name(self):\n        tool = SamplingTool.from_function(lambda x: x * 2, name=\"double\")\n\n        assert tool.name == \"double\"\n\n    def test_from_async_function(self):\n        async def async_search(query: str) -> str:\n            \"\"\"Async search.\"\"\"\n            return f\"Async results for: {query}\"\n\n        tool = SamplingTool.from_function(async_search)\n\n        assert tool.name == \"async_search\"\n        assert tool.description == \"Async search.\"\n\n    def test_multiple_parameters(self):\n        def search(query: str, limit: int = 10, include_images: bool = False) -> str:\n            \"\"\"Search with options.\"\"\"\n            return f\"Results for: {query}\"\n\n        tool = SamplingTool.from_function(search)\n        props = tool.parameters.get(\"properties\", {})\n\n        assert \"query\" in props\n        assert \"limit\" in props\n        assert \"include_images\" in props\n\n\nclass TestSamplingToolRun:\n    \"\"\"Tests for SamplingTool.run().\"\"\"\n\n    async def test_run_sync_function(self):\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        tool = SamplingTool.from_function(add)\n        result = await tool.run({\"a\": 2, \"b\": 3})\n        assert result == 5\n\n    async def test_run_async_function(self):\n        async def async_add(a: int, b: int) -> int:\n            \"\"\"Add two numbers asynchronously.\"\"\"\n            return a + b\n\n        tool = SamplingTool.from_function(async_add)\n        result = await tool.run({\"a\": 2, \"b\": 3})\n        assert result == 5\n\n    async def test_run_with_no_arguments(self):\n        def get_value() -> str:\n            \"\"\"Return a fixed value.\"\"\"\n            return \"hello\"\n\n        tool = SamplingTool.from_function(get_value)\n        result = await tool.run()\n        assert result == \"hello\"\n\n    async def test_run_with_none_arguments(self):\n        def get_value() -> str:\n            \"\"\"Return a fixed value.\"\"\"\n            return \"hello\"\n\n        tool = SamplingTool.from_function(get_value)\n        result = await tool.run(None)\n        assert result == \"hello\"\n\n\nclass TestSamplingToolSDKConversion:\n    \"\"\"Tests for SamplingTool._to_sdk_tool() internal method.\"\"\"\n\n    def test_to_sdk_tool(self):\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results for: {query}\"\n\n        tool = SamplingTool.from_function(search)\n        sdk_tool = tool._to_sdk_tool()\n\n        assert sdk_tool.name == \"search\"\n        assert sdk_tool.description == \"Search the web.\"\n        assert \"query\" in sdk_tool.inputSchema.get(\"properties\", {})\n\n\nclass TestSamplingToolFromCallableTool:\n    \"\"\"Tests for SamplingTool.from_callable_tool().\"\"\"\n\n    def test_from_function_tool(self):\n        \"\"\"Test converting a FunctionTool to SamplingTool.\"\"\"\n\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results for: {query}\"\n\n        function_tool = FunctionTool.from_function(search)\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        assert sampling_tool.name == \"search\"\n        assert sampling_tool.description == \"Search the web.\"\n        assert \"query\" in sampling_tool.parameters.get(\"properties\", {})\n        # fn is now a wrapper that calls tool.run() for proper result processing\n        assert callable(sampling_tool.fn)\n\n    def test_from_function_tool_with_overrides(self):\n        \"\"\"Test converting FunctionTool with name/description overrides.\"\"\"\n\n        def search(query: str) -> str:\n            \"\"\"Search the web.\"\"\"\n            return f\"Results for: {query}\"\n\n        function_tool = FunctionTool.from_function(search)\n        sampling_tool = SamplingTool.from_callable_tool(\n            function_tool,\n            name=\"web_search\",\n            description=\"Search the internet\",\n        )\n\n        assert sampling_tool.name == \"web_search\"\n        assert sampling_tool.description == \"Search the internet\"\n\n    def test_from_transformed_tool(self):\n        \"\"\"Test converting a TransformedTool to SamplingTool.\"\"\"\n\n        def original(query: str, limit: int) -> str:\n            \"\"\"Original tool.\"\"\"\n            return f\"Results for: {query} (limit: {limit})\"\n\n        function_tool = FunctionTool.from_function(original)\n        transformed_tool = TransformedTool.from_tool(\n            function_tool,\n            name=\"search_transformed\",\n            transform_args={\"query\": ArgTransform(name=\"q\")},\n        )\n\n        sampling_tool = SamplingTool.from_callable_tool(transformed_tool)\n\n        assert sampling_tool.name == \"search_transformed\"\n        assert sampling_tool.description == \"Original tool.\"\n        # The transformed tool should have 'q' instead of 'query'\n        assert \"q\" in sampling_tool.parameters.get(\"properties\", {})\n        assert \"limit\" in sampling_tool.parameters.get(\"properties\", {})\n\n    async def test_from_function_tool_execution(self):\n        \"\"\"Test that converted FunctionTool executes correctly.\"\"\"\n\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        function_tool = FunctionTool.from_function(add)\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        result = await sampling_tool.run({\"a\": 2, \"b\": 3})\n        assert result == 5\n\n    async def test_from_transformed_tool_execution(self):\n        \"\"\"Test that converted TransformedTool executes correctly.\"\"\"\n\n        def multiply(x: int, y: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return x * y\n\n        function_tool = FunctionTool.from_function(multiply)\n        transformed_tool = TransformedTool.from_tool(\n            function_tool,\n            transform_args={\"x\": ArgTransform(name=\"a\"), \"y\": ArgTransform(name=\"b\")},\n        )\n\n        sampling_tool = SamplingTool.from_callable_tool(transformed_tool)\n\n        # Use the transformed parameter names\n        result = await sampling_tool.run({\"a\": 3, \"b\": 4})\n        # Result should be unwrapped from ToolResult\n        assert result == 12\n\n    def test_from_invalid_tool_type(self):\n        \"\"\"Test that from_callable_tool rejects non-tool objects.\"\"\"\n\n        class NotATool:\n            pass\n\n        with pytest.raises(\n            TypeError,\n            match=\"Expected FunctionTool or TransformedTool\",\n        ):\n            SamplingTool.from_callable_tool(NotATool())  # type: ignore[arg-type]\n\n    def test_from_plain_function_fails(self):\n        \"\"\"Test that plain functions are rejected by from_callable_tool.\"\"\"\n\n        def my_function():\n            pass\n\n        with pytest.raises(TypeError, match=\"Expected FunctionTool or TransformedTool\"):\n            SamplingTool.from_callable_tool(my_function)  # type: ignore[arg-type]\n\n    async def test_from_function_tool_with_output_schema(self):\n        \"\"\"Test that FunctionTool with output_schema is handled correctly.\"\"\"\n\n        def search(query: str) -> dict:\n            \"\"\"Search for something.\"\"\"\n            return {\"results\": [\"item1\", \"item2\"], \"count\": 2}\n\n        # Create FunctionTool with x-fastmcp-wrap-result\n        function_tool = FunctionTool.from_function(\n            search,\n            output_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"results\": {\"type\": \"array\"},\n                    \"count\": {\"type\": \"integer\"},\n                },\n                \"x-fastmcp-wrap-result\": True,\n            },\n        )\n\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        # Run the tool - should unwrap the {\"result\": {...}} wrapper\n        result = await sampling_tool.run({\"query\": \"test\"})\n\n        # Should get the unwrapped dict, not ToolResult\n        assert isinstance(result, dict)\n        assert result == {\"results\": [\"item1\", \"item2\"], \"count\": 2}\n\n    async def test_from_function_tool_without_wrap_result(self):\n        \"\"\"Test that FunctionTool without x-fastmcp-wrap-result is handled correctly.\"\"\"\n\n        def get_data() -> dict:\n            \"\"\"Get some data.\"\"\"\n            return {\"status\": \"ok\", \"value\": 42}\n\n        # Create FunctionTool with output_schema but no wrap-result flag\n        function_tool = FunctionTool.from_function(\n            get_data,\n            output_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"status\": {\"type\": \"string\"},\n                    \"value\": {\"type\": \"integer\"},\n                },\n            },\n        )\n\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        # Run the tool - should return structured_content directly\n        result = await sampling_tool.run({})\n\n        assert isinstance(result, dict)\n        assert result == {\"status\": \"ok\", \"value\": 42}\n\n\nclass TestSamplingToolAuthEnforcement:\n    \"\"\"Tests that auth-protected tools enforce auth when used via sampling.\"\"\"\n\n    async def test_auth_protected_tool_blocked_without_token(self):\n        \"\"\"An auth-protected tool wrapped as SamplingTool must reject\n        calls when no valid token is present in a non-stdio transport.\"\"\"\n\n        def secret_action() -> str:\n            \"\"\"Do something privileged.\"\"\"\n            return \"secret\"\n\n        function_tool = FunctionTool.from_function(\n            secret_action,\n            auth=require_scopes(\"admin\"),\n        )\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        transport_token = _current_transport.set(\"streamable-http\")\n        try:\n            with pytest.raises(AuthorizationError, match=\"insufficient permissions\"):\n                await sampling_tool.run({})\n        finally:\n            _current_transport.reset(transport_token)\n\n    async def test_auth_protected_tool_blocked_with_wrong_scopes(self):\n        \"\"\"An auth-protected tool rejects calls when the token lacks\n        the required scopes.\"\"\"\n\n        def secret_action() -> str:\n            \"\"\"Do something privileged.\"\"\"\n            return \"secret\"\n\n        function_tool = FunctionTool.from_function(\n            secret_action,\n            auth=require_scopes(\"admin\"),\n        )\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        token = AccessToken(\n            token=\"test\",\n            client_id=\"c\",\n            scopes=[\"read\"],\n            expires_at=None,\n            claims={},\n        )\n        transport_token = _current_transport.set(\"streamable-http\")\n        auth_token = auth_context_var.set(AuthenticatedUser(token))\n        try:\n            with pytest.raises(AuthorizationError, match=\"insufficient permissions\"):\n                await sampling_tool.run({})\n        finally:\n            auth_context_var.reset(auth_token)\n            _current_transport.reset(transport_token)\n\n    async def test_auth_protected_tool_allowed_with_correct_scopes(self):\n        \"\"\"An auth-protected tool succeeds when the token has the\n        required scopes.\"\"\"\n\n        def secret_action() -> str:\n            \"\"\"Do something privileged.\"\"\"\n            return \"secret\"\n\n        function_tool = FunctionTool.from_function(\n            secret_action,\n            auth=require_scopes(\"admin\"),\n        )\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        token = AccessToken(\n            token=\"test\",\n            client_id=\"c\",\n            scopes=[\"admin\"],\n            expires_at=None,\n            claims={},\n        )\n        transport_token = _current_transport.set(\"streamable-http\")\n        auth_token = auth_context_var.set(AuthenticatedUser(token))\n        try:\n            result = await sampling_tool.run({})\n            assert result == \"secret\"\n        finally:\n            auth_context_var.reset(auth_token)\n            _current_transport.reset(transport_token)\n\n    async def test_auth_protected_tool_skipped_on_stdio(self):\n        \"\"\"Auth checks are skipped for stdio transport, matching\n        server dispatcher behavior.\"\"\"\n\n        def secret_action() -> str:\n            \"\"\"Do something privileged.\"\"\"\n            return \"secret\"\n\n        function_tool = FunctionTool.from_function(\n            secret_action,\n            auth=require_scopes(\"admin\"),\n        )\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        transport_token = _current_transport.set(\"stdio\")\n        try:\n            result = await sampling_tool.run({})\n            assert result == \"secret\"\n        finally:\n            _current_transport.reset(transport_token)\n\n    async def test_tool_without_auth_runs_normally(self):\n        \"\"\"Tools without auth still run without any auth context.\"\"\"\n\n        def public_action() -> str:\n            \"\"\"Do something public.\"\"\"\n            return \"public\"\n\n        function_tool = FunctionTool.from_function(public_action)\n        sampling_tool = SamplingTool.from_callable_tool(function_tool)\n\n        result = await sampling_tool.run({})\n        assert result == \"public\"\n\n    async def test_auth_protected_transformed_tool_blocked(self):\n        \"\"\"Auth checks also apply to TransformedTools with auth.\"\"\"\n\n        def secret_action(x: int) -> int:\n            \"\"\"Privileged computation.\"\"\"\n            return x * 2\n\n        function_tool = FunctionTool.from_function(\n            secret_action,\n            auth=require_scopes(\"compute\"),\n        )\n        transformed_tool = TransformedTool.from_tool(\n            function_tool,\n            transform_args={\"x\": ArgTransform(name=\"value\")},\n        )\n        sampling_tool = SamplingTool.from_callable_tool(transformed_tool)\n\n        transport_token = _current_transport.set(\"streamable-http\")\n        try:\n            with pytest.raises(AuthorizationError, match=\"insufficient permissions\"):\n                await sampling_tool.run({\"value\": 5})\n        finally:\n            _current_transport.reset(transport_token)\n"
  },
  {
    "path": "tests/server/tasks/__init__.py",
    "content": "\"\"\"Tests for MCP SEP-1686 background tasks.\"\"\"\n"
  },
  {
    "path": "tests/server/tasks/conftest.py",
    "content": "\"\"\"Configuration for server task tests.\"\"\"\n"
  },
  {
    "path": "tests/server/tasks/test_context_background_task.py",
    "content": "\"\"\"Tests for Context background task support (SEP-1686).\n\nTests Context API surface (unit) and background task elicitation (integration).\nIntegration tests use Client(mcp) with the real memory:// Docket backend —\nno mocking of Redis, Docket, or session internals.\n\"\"\"\n\nimport asyncio\nfrom typing import cast\n\nimport pytest\nfrom mcp import ServerSession\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.dependencies import CurrentDocket\nfrom fastmcp.server.auth import AccessToken\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.dependencies import get_access_token\nfrom fastmcp.server.elicitation import AcceptedElicitation, DeclinedElicitation\nfrom fastmcp.server.tasks.elicitation import handle_task_input\n\n# =============================================================================\n# Unit tests: Context API surface (no Redis/Docket needed)\n# =============================================================================\n\n\nclass TestContextBackgroundTaskSupport:\n    \"\"\"Tests for Context.is_background_task and related functionality.\"\"\"\n\n    def test_context_not_background_task_by_default(self):\n        \"\"\"Context should not be a background task by default.\"\"\"\n        mcp = FastMCP(\"test\")\n        ctx = Context(mcp)\n        assert ctx.is_background_task is False\n        assert ctx.task_id is None\n\n    def test_context_is_background_task_when_task_id_provided(self):\n        \"\"\"Context should be a background task when task_id is provided.\"\"\"\n        mcp = FastMCP(\"test\")\n        ctx = Context(mcp, task_id=\"test-task-123\")\n        assert ctx.is_background_task is True\n        assert ctx.task_id == \"test-task-123\"\n\n    def test_context_task_id_is_readonly(self):\n        \"\"\"task_id should be a read-only property.\"\"\"\n        mcp = FastMCP(\"test\")\n        ctx = Context(mcp, task_id=\"test-task-123\")\n        with pytest.raises(AttributeError):\n            setattr(ctx, \"task_id\", \"new-id\")\n\n\nclass TestContextSessionProperty:\n    \"\"\"Tests for Context.session property in different modes.\"\"\"\n\n    def test_session_raises_when_no_session_available(self):\n        \"\"\"session should raise RuntimeError when no session is available.\"\"\"\n        mcp = FastMCP(\"test\")\n        ctx = Context(mcp)  # No session, not a background task\n\n        with pytest.raises(RuntimeError, match=\"session is not available\"):\n            _ = ctx.session\n\n    def test_session_uses_stored_session_in_background_task(self):\n        \"\"\"session should use _session in background task mode.\"\"\"\n        mcp = FastMCP(\"test\")\n\n        class MockSession:\n            _fastmcp_state_prefix = \"test-session\"\n\n        mock_session = MockSession()\n        ctx = Context(\n            mcp, session=cast(ServerSession, mock_session), task_id=\"test-task-123\"\n        )\n\n        assert ctx.session is mock_session\n\n    def test_session_uses_stored_session_during_on_initialize(self):\n        \"\"\"session should use _session during on_initialize (no request context).\"\"\"\n        mcp = FastMCP(\"test\")\n\n        class MockSession:\n            _fastmcp_state_prefix = \"test-session\"\n\n        mock_session = MockSession()\n        ctx = Context(mcp, session=cast(ServerSession, mock_session))\n\n        assert ctx.session is mock_session\n\n\nclass TestContextElicitBackgroundTask:\n    \"\"\"Tests for Context.elicit() in background task mode.\"\"\"\n\n    async def test_elicit_raises_when_background_task_but_no_docket(self):\n        \"\"\"elicit() should raise when in background task mode but Docket unavailable.\"\"\"\n        mcp = FastMCP(\"test\")\n        ctx = Context(mcp, task_id=\"test-task-123\")\n\n        class MockSession:\n            _fastmcp_state_prefix = \"test-session\"\n\n        ctx._session = cast(ServerSession, MockSession())\n\n        with pytest.raises(RuntimeError, match=\"Docket\"):\n            await ctx.elicit(\"Need input\", str)\n\n\nclass TestElicitFailFast:\n    \"\"\"Tests for elicit_for_task fail-fast on notification push failure.\"\"\"\n\n    async def test_elicit_returns_cancel_when_notification_push_fails(self):\n        \"\"\"elicit_for_task should return cancel immediately when push_notification fails.\n\n        If the client can't receive the input_required notification, waiting\n        for a response that will never come would block for up to 1 hour.\n        Instead, we return cancel immediately (fail-fast).\n\n        This test patches ONLY push_notification — all other components\n        (Docket, Redis, session) are real via the memory:// backend.\n        \"\"\"\n        from unittest.mock import patch\n\n        from fastmcp.server.elicitation import CancelledElicitation\n\n        mcp = FastMCP(\"failfast-test\")\n        elicit_started = asyncio.Event()\n        captured: dict[str, object] = {}\n\n        @mcp.tool(task=True)\n        async def failfast_tool(ctx: Context) -> str:\n            elicit_started.set()\n            result = await ctx.elicit(\"This notification will fail\", str)\n            captured[\"result_type\"] = type(result).__name__\n            captured[\"is_cancelled\"] = isinstance(result, CancelledElicitation)\n            return \"done\"\n\n        # Patch push_notification BEFORE starting client so it's active\n        # when the tool runs in the Docket worker\n        with patch(\n            \"fastmcp.server.tasks.notifications.push_notification\",\n            side_effect=ConnectionError(\"Redis queue unavailable\"),\n        ):\n            async with Client(mcp) as client:\n                task = await client.call_tool(\"failfast_tool\", {}, task=True)\n                await asyncio.wait_for(elicit_started.wait(), timeout=5.0)\n                await task.wait(timeout=10.0)\n                result = await task.result()\n                assert result.data == \"done\"\n\n        # The tool should have received CancelledElicitation (fail-fast)\n        assert captured[\"is_cancelled\"] is True\n        assert captured[\"result_type\"] == \"CancelledElicitation\"\n\n\nclass TestContextDocumentation:\n    \"\"\"Tests to verify Context documentation and API surface.\"\"\"\n\n    def test_is_background_task_has_docstring(self):\n        \"\"\"is_background_task property should have documentation.\"\"\"\n        assert Context.is_background_task.__doc__ is not None\n        assert \"background task\" in Context.is_background_task.__doc__.lower()\n\n    def test_task_id_has_docstring(self):\n        \"\"\"task_id property should have documentation.\"\"\"\n        assert Context.task_id.fget.__doc__ is not None\n        assert \"task ID\" in Context.task_id.fget.__doc__\n\n    def test_session_has_docstring(self):\n        \"\"\"session property should document background task support.\"\"\"\n        assert Context.session.fget.__doc__ is not None\n        assert \"background task\" in Context.session.fget.__doc__.lower()\n\n\n# =============================================================================\n# Integration tests: Client(mcp) + memory:// Docket backend\n# =============================================================================\n\n\nclass TestBackgroundTaskIntegration:\n    \"\"\"Integration tests for background task context using real Docket memory backend.\n\n    These tests use Client(mcp) with the memory:// broker — no mocking.\n    The memory:// backend provides a fully functional in-memory Redis store\n    that Docket uses automatically when running tests.\n    \"\"\"\n\n    async def test_report_progress_in_background_task(self):\n        \"\"\"report_progress() should complete without error in a background task.\"\"\"\n        mcp = FastMCP(\"progress-test\")\n        progress_reported = asyncio.Event()\n\n        @mcp.tool(task=True)\n        async def progress_tool(ctx: Context) -> str:\n            await ctx.report_progress(0, 100, \"Starting...\")\n            await ctx.report_progress(50, 100, \"Half done\")\n            await ctx.report_progress(100, 100, \"Complete\")\n            progress_reported.set()\n            return \"done\"\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"progress_tool\", {}, task=True)\n            await asyncio.wait_for(progress_reported.wait(), timeout=5.0)\n            await task.wait(timeout=5.0)\n            result = await task.result()\n            assert result.data == \"done\"\n\n    async def test_context_wiring_in_background_task(self):\n        \"\"\"Context should be properly wired with task_id and session_id.\"\"\"\n        mcp = FastMCP(\"wiring-test\")\n        task_completed = asyncio.Event()\n        captured: dict[str, object] = {}\n\n        @mcp.tool(task=True)\n        async def verify_wiring(ctx: Context) -> str:\n            captured[\"task_id\"] = ctx.task_id\n            captured[\"session_id\"] = ctx.session_id\n            captured[\"is_background\"] = ctx.is_background_task\n            task_completed.set()\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"verify_wiring\", {}, task=True)\n            await asyncio.wait_for(task_completed.wait(), timeout=5.0)\n            await task.wait(timeout=5.0)\n            result = await task.result()\n            assert result.data == \"ok\"\n\n        assert captured[\"task_id\"] is not None\n        assert captured[\"session_id\"] is not None\n        assert captured[\"is_background\"] is True\n\n    async def test_origin_request_id_round_trips_through_background_task(self):\n        \"\"\"E2E: origin_request_id captured at submit time is restored in worker.\n\n        We validate this by comparing ctx.origin_request_id with the value\n        stored in Docket's Redis for this task.\n        \"\"\"\n\n        mcp = FastMCP(\"origin-request-id-roundtrip\")\n\n        @mcp.tool(task=True)\n        async def check_origin_request_id(ctx: Context, docket=CurrentDocket()) -> str:\n            assert ctx.is_background_task is True\n            assert ctx.request_context is None\n            assert ctx.task_id is not None\n\n            origin = ctx.origin_request_id\n            assert origin is not None\n            assert isinstance(origin, str)\n            assert origin != \"\"\n\n            key = docket.key(\n                f\"fastmcp:task:{ctx.session_id}:{ctx.task_id}:origin_request_id\"\n            )\n            async with docket.redis() as redis:\n                raw = await redis.get(key)\n\n            assert raw is not None\n            if isinstance(raw, bytes):\n                raw = raw.decode()\n            assert str(raw) == origin\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"check_origin_request_id\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"ok\"\n\n    async def test_elicit_accept_flow(self):\n        \"\"\"E2E: tool elicits input, client accepts via elicitation_handler.\"\"\"\n        mcp = FastMCP(\"elicit-accept-test\")\n\n        @mcp.tool(task=True)\n        async def ask_name(ctx: Context) -> str:\n            result = await ctx.elicit(\"What is your name?\", str)\n            if isinstance(result, AcceptedElicitation):\n                return f\"Hello, {result.data}!\"\n            return \"No name provided\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"Bob\"})\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"ask_name\", {}, task=True)\n            await task.wait(timeout=10.0)\n            result = await task.result()\n            assert result.data == \"Hello, Bob!\"\n\n    async def test_elicit_decline_flow(self):\n        \"\"\"E2E: tool elicits input, client declines via elicitation_handler.\"\"\"\n        mcp = FastMCP(\"elicit-decline-test\")\n\n        @mcp.tool(task=True)\n        async def optional_input(ctx: Context) -> str:\n            result = await ctx.elicit(\"Want to provide a name?\", str)\n            if isinstance(result, DeclinedElicitation):\n                return \"User declined\"\n            if isinstance(result, AcceptedElicitation):\n                return f\"Got: {result.data}\"\n            return \"Cancelled\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"decline\")\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"optional_input\", {}, task=True)\n            await task.wait(timeout=10.0)\n            result = await task.result()\n            assert result.data == \"User declined\"\n\n    async def test_elicit_with_pydantic_model(self):\n        \"\"\"E2E: tool elicits structured Pydantic input via elicitation_handler.\"\"\"\n        from pydantic import BaseModel\n\n        class UserInfo(BaseModel):\n            name: str\n            age: int\n\n        mcp = FastMCP(\"elicit-pydantic-test\")\n\n        @mcp.tool(task=True)\n        async def get_user_info(ctx: Context) -> str:\n            result = await ctx.elicit(\"Provide user info\", UserInfo)\n            if isinstance(result, AcceptedElicitation):\n                assert isinstance(result.data, UserInfo)\n                return f\"{result.data.name} is {result.data.age}\"\n            return \"No info\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"name\": \"Alice\", \"age\": 30})\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"get_user_info\", {}, task=True)\n            await task.wait(timeout=10.0)\n            result = await task.result()\n            assert result.data == \"Alice is 30\"\n\n    async def test_handle_task_input_rejects_when_not_waiting(self):\n        \"\"\"handle_task_input returns False when no task is waiting for input.\"\"\"\n        mcp = FastMCP(\"reject-test\")\n\n        @mcp.tool(task=True)\n        async def simple_tool() -> str:\n            return \"done\"\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"simple_tool\", {}, task=True)\n            await task.wait(timeout=5.0)\n\n            # Task already completed — no elicitation waiting\n            success = await handle_task_input(\n                task_id=task.task_id,\n                session_id=\"nonexistent-session\",\n                action=\"accept\",\n                content={\"value\": \"too late\"},\n                fastmcp=mcp,\n            )\n            assert success is False\n\n\nclass TestAccessTokenInBackgroundTasks:\n    \"\"\"Tests for access token availability in background tasks (#3095).\n\n    Integration tests use Client(mcp) with the real memory:// Docket backend.\n    The token snapshot/restore round-trip flows through actual Redis (fakeredis).\n\n    Note: async tests run in isolated asyncio tasks, so ContextVar changes\n    are automatically scoped — no cleanup required.\n    \"\"\"\n\n    async def test_token_round_trips_through_background_task(self):\n        \"\"\"E2E: token set at submit time is available inside the worker.\"\"\"\n        from mcp.server.auth.middleware.auth_context import auth_context_var\n        from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\n\n        mcp = FastMCP(\"token-roundtrip\")\n\n        @mcp.tool(task=True)\n        async def check_token(ctx: Context) -> str:\n            token = get_access_token()\n            if token is None:\n                return \"no-token\"\n            return f\"{token.token}|{token.client_id}\"\n\n        test_token = AccessToken(\n            token=\"roundtrip-jwt\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n            claims={\"sub\": \"user-1\"},\n        )\n        auth_context_var.set(AuthenticatedUser(test_token))\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"check_token\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"roundtrip-jwt|test-client\"\n\n    async def test_no_token_when_unauthenticated(self):\n        \"\"\"E2E: background task gets no token when nothing was set.\"\"\"\n        mcp = FastMCP(\"no-auth\")\n\n        @mcp.tool(task=True)\n        async def check_token(ctx: Context) -> str:\n            token = get_access_token()\n            return \"no-token\" if token is None else token.token\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"check_token\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"no-token\"\n\n    async def test_expired_token_returns_none(self):\n        \"\"\"get_access_token() returns None when task token has expired.\"\"\"\n        from datetime import datetime, timezone\n\n        from fastmcp.server.dependencies import _task_access_token\n\n        expired = AccessToken(\n            token=\"expired-jwt\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n            expires_at=int(datetime.now(timezone.utc).timestamp()) - 3600,\n        )\n        _task_access_token.set(expired)\n        assert get_access_token() is None\n\n    async def test_valid_token_with_future_expiry(self):\n        \"\"\"get_access_token() returns token when expiry is in the future.\"\"\"\n        from datetime import datetime, timezone\n\n        from fastmcp.server.dependencies import _task_access_token\n\n        valid = AccessToken(\n            token=\"valid-jwt\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n            expires_at=int(datetime.now(timezone.utc).timestamp()) + 3600,\n        )\n        _task_access_token.set(valid)\n        result = get_access_token()\n        assert result is not None\n        assert result.token == \"valid-jwt\"\n\n    async def test_token_without_expiry_always_valid(self):\n        \"\"\"get_access_token() returns token when no expires_at is set.\"\"\"\n        from fastmcp.server.dependencies import _task_access_token\n\n        no_expiry = AccessToken(\n            token=\"eternal-jwt\",\n            client_id=\"test-client\",\n            scopes=[\"read\"],\n        )\n        _task_access_token.set(no_expiry)\n        result = get_access_token()\n        assert result is not None\n        assert result.token == \"eternal-jwt\"\n\n\nclass TestLifespanContextInBackgroundTasks:\n    \"\"\"Tests for lifespan_context availability in background tasks (#3095).\"\"\"\n\n    def test_lifespan_context_falls_back_to_server_result(self):\n        \"\"\"lifespan_context reads from server when request_context is None.\"\"\"\n        mcp = FastMCP(\"test\")\n        mcp._lifespan_result = {\"db\": \"mock-db-connection\", \"cache\": \"mock-cache\"}\n\n        ctx = Context(mcp, task_id=\"test-task\")\n        assert ctx.request_context is None\n        assert ctx.lifespan_context == {\n            \"db\": \"mock-db-connection\",\n            \"cache\": \"mock-cache\",\n        }\n\n    def test_lifespan_context_returns_empty_dict_when_no_lifespan(self):\n        \"\"\"lifespan_context returns {} when no lifespan is configured.\"\"\"\n        mcp = FastMCP(\"test\")\n        ctx = Context(mcp, task_id=\"test-task\")\n        assert ctx.request_context is None\n        assert ctx.lifespan_context == {}\n"
  },
  {
    "path": "tests/server/tasks/test_custom_subclass_tasks.py",
    "content": "\"\"\"Tests for custom component subclasses with task support.\n\nVerifies that custom Tool, Resource, and Prompt subclasses can use\nbackground task execution by setting task_config.\n\"\"\"\n\nimport asyncio\nfrom typing import Any\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.tasks import TaskConfig\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.components import FastMCPComponent\n\n\nclass CustomTool(Tool):\n    \"\"\"A custom tool subclass with task support.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"optional\")\n    parameters: dict[str, Any] = {\"type\": \"object\", \"properties\": {}}\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        return ToolResult(content=f\"Custom tool executed with {arguments}\")\n\n\nclass CustomToolWithLogic(Tool):\n    \"\"\"A custom tool with actual async work.\"\"\"\n\n    task_config: TaskConfig = TaskConfig(mode=\"optional\")\n    parameters: dict[str, Any] = {\n        \"type\": \"object\",\n        \"properties\": {\"duration\": {\"type\": \"integer\"}},\n    }\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        duration = arguments.get(\"duration\", 0)\n        await asyncio.sleep(duration * 0.01)  # Short sleep for testing\n        return ToolResult(content=f\"Completed after {duration} units\")\n\n\nclass CustomToolForbidden(Tool):\n    \"\"\"A custom tool with task_config forbidden (default).\"\"\"\n\n    parameters: dict[str, Any] = {\"type\": \"object\", \"properties\": {}}\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        return ToolResult(content=\"Sync only\")\n\n\n@pytest.fixture\ndef custom_tool_server():\n    \"\"\"Create a server with custom tool subclasses.\"\"\"\n    mcp = FastMCP(\"custom-tool-server\")\n    mcp.add_tool(CustomTool(name=\"custom_tool\", description=\"A custom tool\"))\n    mcp.add_tool(\n        CustomToolWithLogic(name=\"custom_logic\", description=\"Custom tool with logic\")\n    )\n    mcp.add_tool(\n        CustomToolForbidden(name=\"custom_forbidden\", description=\"No task support\")\n    )\n    return mcp\n\n\nasync def test_custom_tool_sync_execution(custom_tool_server):\n    \"\"\"Custom tool executes synchronously when no task metadata.\"\"\"\n    async with Client(custom_tool_server) as client:\n        result = await client.call_tool(\"custom_tool\", {})\n        assert \"Custom tool executed\" in str(result)\n\n\nasync def test_custom_tool_background_execution(custom_tool_server):\n    \"\"\"Custom tool executes as background task when task=True.\"\"\"\n    async with Client(custom_tool_server) as client:\n        task = await client.call_tool(\"custom_tool\", {}, task=True)\n\n        assert task is not None\n        assert not task.returned_immediately\n        assert task.task_id is not None\n\n        # Wait for result\n        result = await task.result()\n        assert \"Custom tool executed\" in str(result)\n\n\nasync def test_custom_tool_with_arguments(custom_tool_server):\n    \"\"\"Custom tool receives arguments correctly in background execution.\"\"\"\n    async with Client(custom_tool_server) as client:\n        task = await client.call_tool(\"custom_logic\", {\"duration\": 1}, task=True)\n\n        assert task is not None\n        result = await task.result()\n        assert \"Completed after 1 units\" in str(result)\n\n\nasync def test_custom_tool_forbidden_sync_only(custom_tool_server):\n    \"\"\"Custom tool with forbidden mode executes sync only.\"\"\"\n    async with Client(custom_tool_server) as client:\n        # Sync execution works\n        result = await client.call_tool(\"custom_forbidden\", {})\n        assert \"Sync only\" in str(result)\n\n\nasync def test_custom_tool_forbidden_rejects_task(custom_tool_server):\n    \"\"\"Custom tool with forbidden mode returns error for task request.\"\"\"\n    async with Client(custom_tool_server) as client:\n        task = await client.call_tool(\"custom_forbidden\", {}, task=True)\n\n        # Should return immediately with error\n        assert task.returned_immediately\n\n\nasync def test_custom_tool_registers_with_docket():\n    \"\"\"Verify custom tool's register_with_docket is called during server startup.\"\"\"\n    from unittest.mock import MagicMock\n\n    tool = CustomTool(name=\"test\", description=\"test\")\n    mock_docket = MagicMock()\n\n    tool.register_with_docket(mock_docket)\n\n    # Should register self.run with docket using prefixed key\n    mock_docket.register.assert_called_once()\n    call_args = mock_docket.register.call_args\n    assert call_args[1][\"names\"] == [\"tool:test@\"]\n\n\nasync def test_custom_tool_forbidden_does_not_register():\n    \"\"\"Verify custom tool with forbidden mode doesn't register with docket.\"\"\"\n    tool = CustomToolForbidden(name=\"test\", description=\"test\")\n    mock_docket = MagicMock()\n\n    tool.register_with_docket(mock_docket)\n\n    # Should NOT register\n    mock_docket.register.assert_not_called()\n\n\n# ==============================================================================\n# Base FastMCPComponent Tests\n# ==============================================================================\n\n\nclass TestFastMCPComponentDocketMethods:\n    \"\"\"Tests for base FastMCPComponent docket integration.\"\"\"\n\n    def test_default_task_config_is_forbidden(self):\n        \"\"\"Base component defaults to task_config mode='forbidden'.\"\"\"\n        component = FastMCPComponent(name=\"test\")\n        assert component.task_config.mode == \"forbidden\"\n\n    def test_register_with_docket_is_noop(self):\n        \"\"\"Base register_with_docket does nothing (subclasses override).\"\"\"\n        component = FastMCPComponent(name=\"test\")\n        mock_docket = MagicMock()\n\n        # Should not raise, just no-op\n        component.register_with_docket(mock_docket)\n\n        # Should not have called any docket methods\n        mock_docket.register.assert_not_called()\n\n    async def test_add_to_docket_raises_when_forbidden(self):\n        \"\"\"Base add_to_docket raises RuntimeError when mode is 'forbidden'.\"\"\"\n        component = FastMCPComponent(name=\"test\")\n        mock_docket = MagicMock()\n\n        with pytest.raises(RuntimeError, match=\"task execution not supported\"):\n            await component.add_to_docket(mock_docket)\n\n    async def test_add_to_docket_raises_not_implemented_when_allowed(self):\n        \"\"\"Base add_to_docket raises NotImplementedError when not forbidden.\"\"\"\n        component = FastMCPComponent(\n            name=\"test\", task_config=TaskConfig(mode=\"optional\")\n        )\n        mock_docket = MagicMock()\n\n        with pytest.raises(\n            NotImplementedError, match=\"does not implement add_to_docket\"\n        ):\n            await component.add_to_docket(mock_docket)\n"
  },
  {
    "path": "tests/server/tasks/test_notifications.py",
    "content": "\"\"\"Tests for distributed notification queue (SEP-1686).\n\nIntegration tests verify that the notification queue works end-to-end\nusing Client(mcp) with the real memory:// Docket backend.\nNo mocking of Redis, sessions, or Docket internals.\n\"\"\"\n\nimport asyncio\n\nimport mcp.types as mcp_types\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.client.messages import MessageHandler\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.elicitation import AcceptedElicitation\nfrom fastmcp.server.tasks.notifications import (\n    get_subscriber_count,\n)\n\n\nclass NotificationCaptureHandler(MessageHandler):\n    \"\"\"Capture server notifications for test assertions.\"\"\"\n\n    def __init__(self) -> None:\n        super().__init__()\n        self.notifications: list[mcp_types.ServerNotification] = []\n\n    async def on_notification(self, message: mcp_types.ServerNotification) -> None:\n        self.notifications.append(message)\n\n    def for_method(self, method: str) -> list[mcp_types.ServerNotification]:\n        return [\n            notification\n            for notification in self.notifications\n            if notification.root.method == method\n        ]\n\n\nclass TestNotificationIntegration:\n    \"\"\"Integration tests for the notification queue using real Docket memory backend.\n\n    The elicitation flow validates the full notification pipeline:\n    1. Tool calls ctx.elicit() -> stores request in Redis -> pushes notification\n    2. Subscriber picks up notification -> sends MCP notification to client\n    3. Subscriber relays elicitation/create to client -> handler responds\n    4. Relay pushes response to Redis -> BLPOP wakes tool\n    \"\"\"\n\n    async def test_notification_delivered_during_elicitation(self):\n        \"\"\"Full E2E: notification queue delivers input_required metadata to client.\n\n        The elicitation relay handles the response via the client's\n        elicitation_handler. We verify both the notification metadata\n        structure and the end-to-end elicitation flow.\n        \"\"\"\n        mcp = FastMCP(\"notification-test\")\n        notification_handler = NotificationCaptureHandler()\n\n        @mcp.tool(task=True)\n        async def elicit_tool(ctx: Context) -> str:\n            result = await ctx.elicit(\"Enter value\", str)\n            if isinstance(result, AcceptedElicitation):\n                return f\"got: {result.data}\"\n            return \"no value\"\n\n        async def elicitation_handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"value\": \"hello\"})\n\n        async with Client(\n            mcp,\n            message_handler=notification_handler,\n            elicitation_handler=elicitation_handler,\n        ) as client:\n            task = await client.call_tool(\"elicit_tool\", {}, task=True)\n\n            await task.wait(timeout=10.0)\n            result = await task.result()\n            assert result.data == \"got: hello\"\n\n            # Verify the input_required notification was delivered with metadata\n            notification: mcp_types.ServerNotification | None = None\n            candidates = notification_handler.for_method(\"notifications/tasks/status\")\n            for candidate in reversed(candidates):\n                candidate_meta = getattr(candidate.root, \"_meta\", None)\n                related_task = (\n                    candidate_meta.get(\"io.modelcontextprotocol/related-task\")\n                    if isinstance(candidate_meta, dict)\n                    else None\n                )\n                if (\n                    isinstance(related_task, dict)\n                    and related_task.get(\"status\") == \"input_required\"\n                ):\n                    notification = candidate\n                    break\n\n            assert notification is not None, \"expected notifications/tasks/status\"\n            task_meta = getattr(notification.root, \"_meta\", None)\n            assert isinstance(task_meta, dict)\n\n            related_task = task_meta.get(\"io.modelcontextprotocol/related-task\")\n            assert isinstance(related_task, dict)\n            assert related_task.get(\"taskId\") == task.task_id\n            assert related_task.get(\"status\") == \"input_required\"\n\n            elicitation = related_task.get(\"elicitation\")\n            assert isinstance(elicitation, dict)\n            assert elicitation.get(\"message\") == \"Enter value\"\n            assert isinstance(elicitation.get(\"requestId\"), str)\n            assert isinstance(elicitation.get(\"requestedSchema\"), dict)\n\n    async def test_subscriber_started_and_cleaned_up(self):\n        \"\"\"Subscriber starts during background task and stops when client disconnects.\"\"\"\n        mcp = FastMCP(\"subscriber-test\")\n        tool_started = asyncio.Event()\n        tool_continue = asyncio.Event()\n\n        @mcp.tool(task=True)\n        async def lifecycle_tool(ctx: Context) -> str:\n            tool_started.set()\n            await asyncio.wait_for(tool_continue.wait(), timeout=10.0)\n            return \"done\"\n\n        count_before = get_subscriber_count()\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"lifecycle_tool\", {}, task=True)\n            await asyncio.wait_for(tool_started.wait(), timeout=5.0)\n\n            # While a background task is running, subscriber should be active\n            count_during = get_subscriber_count()\n            assert count_during > count_before\n\n            # Let the tool complete\n            tool_continue.set()\n            await task.wait(timeout=5.0)\n            result = await task.result()\n            assert result.data == \"done\"\n\n        # After client disconnects, subscriber should be cleaned up\n        # Allow brief time for async cleanup\n        for _ in range(20):\n            if get_subscriber_count() == count_before:\n                break\n            await asyncio.sleep(0.05)\n        assert get_subscriber_count() == count_before\n"
  },
  {
    "path": "tests/server/tasks/test_progress_dependency.py",
    "content": "\"\"\"Tests for FastMCP Progress dependency.\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.dependencies import Progress\n\n\nasync def test_progress_in_immediate_execution():\n    \"\"\"Test Progress dependency when calling tool immediately with Docket enabled.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool()\n    async def test_tool(progress: Progress = Progress()) -> str:\n        await progress.set_total(10)\n        await progress.increment()\n        await progress.set_message(\"Testing\")\n        return \"done\"\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"test_tool\", {})\n        from mcp.types import TextContent\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"done\"\n\n\nasync def test_progress_in_background_task():\n    \"\"\"Test Progress dependency in background task execution.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def test_task(progress: Progress = Progress()) -> str:\n        await progress.set_total(5)\n        await progress.increment()\n        await progress.set_message(\"Step 1\")\n        return \"done\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"test_task\", {}, task=True)\n        result = await task.result()\n        from mcp.types import TextContent\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"done\"\n\n\nasync def test_progress_tracks_multiple_increments():\n    \"\"\"Test that Progress correctly tracks multiple increment calls.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool()\n    async def count_to_ten(progress: Progress = Progress()) -> str:\n        await progress.set_total(10)\n        for i in range(10):\n            await progress.increment()\n        return \"counted\"\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"count_to_ten\", {})\n        from mcp.types import TextContent\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"counted\"\n\n\nasync def test_progress_status_message_in_background_task():\n    \"\"\"Regression test: TaskStatusResponse must include statusMessage field.\"\"\"\n    import asyncio\n\n    mcp = FastMCP(\"test\")\n    step_started = asyncio.Event()\n\n    @mcp.tool(task=True)\n    async def task_with_progress(progress: Progress = Progress()) -> str:\n        await progress.set_total(3)\n        await progress.set_message(\"Step 1 of 3\")\n        await progress.increment()\n        step_started.set()\n\n        # Give test time to poll status\n        await asyncio.sleep(0.2)\n\n        await progress.set_message(\"Step 2 of 3\")\n        await progress.increment()\n        await progress.set_message(\"Step 3 of 3\")\n        await progress.increment()\n        return \"done\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"task_with_progress\", {}, task=True)\n\n        # Wait for first step to start\n        await step_started.wait()\n\n        # Get status and verify progress message\n        status = await task.status()\n\n        # Verify statusMessage field is accessible and contains progress info\n        # Should not raise AttributeError\n        msg = status.statusMessage\n        assert msg is None or msg.startswith(\"Step\")\n\n        # Wait for completion\n        result = await task.result()\n        from mcp.types import TextContent\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"done\"\n\n\nasync def test_inmemory_progress_state():\n    \"\"\"Test that in-memory progress stores and returns state correctly.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool()\n    async def test_tool(progress: Progress = Progress()) -> dict:\n        # Initial state\n        assert progress.current is None\n        assert progress.total == 1\n        assert progress.message is None\n\n        # Set total\n        await progress.set_total(10)\n        assert progress.total == 10\n\n        # Increment\n        await progress.increment()\n        assert progress.current == 1\n\n        # Increment again\n        await progress.increment(2)\n        assert progress.current == 3\n\n        # Set message\n        await progress.set_message(\"Testing\")\n        assert progress.message == \"Testing\"\n\n        return {\n            \"current\": progress.current,\n            \"total\": progress.total,\n            \"message\": progress.message,\n        }\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"test_tool\", {})\n        from mcp.types import TextContent\n\n        assert isinstance(result.content[0], TextContent)\n        # The tool returns a dict showing the final state\n        import json\n\n        state = json.loads(result.content[0].text)\n        assert state[\"current\"] == 3\n        assert state[\"total\"] == 10\n        assert state[\"message\"] == \"Testing\"\n"
  },
  {
    "path": "tests/server/tasks/test_resource_task_meta_parameter.py",
    "content": "\"\"\"\nTests for the explicit task_meta parameter on FastMCP.read_resource().\n\nThese tests verify that the task_meta parameter provides explicit control\nover sync vs task execution for resources and resource templates.\n\"\"\"\n\nimport pytest\nfrom mcp.shared.exceptions import McpError\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.server.tasks.config import TaskMeta\n\n\nclass TestResourceTaskMetaParameter:\n    \"\"\"Tests for task_meta parameter on FastMCP.read_resource().\"\"\"\n\n    async def test_task_meta_none_returns_resource_result(self):\n        \"\"\"With task_meta=None (default), read_resource returns ResourceResult.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\")\n        async def simple_resource() -> str:\n            return \"hello world\"\n\n        result = await server.read_resource(\"data://test\")\n\n        assert result.contents[0].content == \"hello world\"\n\n    async def test_task_meta_none_on_task_enabled_resource_still_returns_result(self):\n        \"\"\"Even for task=True resources, task_meta=None returns ResourceResult.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\", task=True)\n        async def task_enabled_resource() -> str:\n            return \"hello world\"\n\n        # Without task_meta, should execute synchronously\n        result = await server.read_resource(\"data://test\")\n\n        assert result.contents[0].content == \"hello world\"\n\n    async def test_task_meta_on_forbidden_resource_raises_error(self):\n        \"\"\"Providing task_meta to a task=False resource raises McpError.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\", task=False)\n        async def sync_only_resource() -> str:\n            return \"hello\"\n\n        with pytest.raises(McpError) as exc_info:\n            await server.read_resource(\"data://test\", task_meta=TaskMeta())\n\n        assert \"does not support task-augmented execution\" in str(exc_info.value)\n\n    async def test_task_meta_fn_key_enrichment_for_resource(self):\n        \"\"\"Verify that fn_key enrichment uses Resource.make_key().\"\"\"\n        resource_uri = \"data://my-resource\"\n        expected_key = Resource.make_key(resource_uri)\n\n        assert expected_key == \"resource:data://my-resource\"\n\n    async def test_task_meta_fn_key_enrichment_for_template(self):\n        \"\"\"Verify that fn_key enrichment uses ResourceTemplate.make_key().\"\"\"\n        template_pattern = \"data://{id}\"\n        expected_key = ResourceTemplate.make_key(template_pattern)\n\n        assert expected_key == \"template:data://{id}\"\n\n\nclass TestResourceTemplateTaslMeta:\n    \"\"\"Tests for task_meta with resource templates.\"\"\"\n\n    async def test_template_task_meta_none_returns_resource_result(self):\n        \"\"\"With task_meta=None, template read returns ResourceResult.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"item://{id}\")\n        async def get_item(id: str) -> str:\n            return f\"Item {id}\"\n\n        result = await server.read_resource(\"item://42\")\n\n        assert result.contents[0].content == \"Item 42\"\n\n    async def test_template_task_meta_on_task_enabled_template_returns_result(self):\n        \"\"\"Even for task=True templates, task_meta=None returns ResourceResult.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"item://{id}\", task=True)\n        async def get_item(id: str) -> str:\n            return f\"Item {id}\"\n\n        # Without task_meta, should execute synchronously\n        result = await server.read_resource(\"item://42\")\n\n        assert result.contents[0].content == \"Item 42\"\n\n    async def test_template_task_meta_on_forbidden_template_raises_error(self):\n        \"\"\"Providing task_meta to a task=False template raises McpError.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"item://{id}\", task=False)\n        async def sync_only_template(id: str) -> str:\n            return f\"Item {id}\"\n\n        with pytest.raises(McpError) as exc_info:\n            await server.read_resource(\"item://42\", task_meta=TaskMeta())\n\n        assert \"does not support task-augmented execution\" in str(exc_info.value)\n\n\nclass TestResourceTaskMetaClientIntegration:\n    \"\"\"Tests that task_meta works correctly with the Client for resources.\"\"\"\n\n    async def test_client_read_resource_without_task_gets_immediate_result(self):\n        \"\"\"Client without task=True gets immediate result.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\", task=True)\n        async def immediate_resource() -> str:\n            return \"hello\"\n\n        async with Client(server) as client:\n            result = await client.read_resource(\"data://test\")\n\n            # Should get ReadResourceResult directly\n            assert \"hello\" in str(result)\n\n    async def test_client_read_resource_with_task_creates_task(self):\n        \"\"\"Client with task=True creates a background task.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\", task=True)\n        async def task_resource() -> str:\n            return \"hello\"\n\n        async with Client(server) as client:\n            from fastmcp.client.tasks import ResourceTask\n\n            task = await client.read_resource(\"data://test\", task=True)\n\n            assert isinstance(task, ResourceTask)\n\n            # Wait for result\n            result = await task.result()\n            assert \"hello\" in str(result)\n\n    async def test_client_read_template_with_task_creates_task(self):\n        \"\"\"Client with task=True on template creates a background task.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"item://{id}\", task=True)\n        async def get_item(id: str) -> str:\n            return f\"Item {id}\"\n\n        async with Client(server) as client:\n            from fastmcp.client.tasks import ResourceTask\n\n            task = await client.read_resource(\"item://42\", task=True)\n\n            assert isinstance(task, ResourceTask)\n\n            # Wait for result\n            result = await task.result()\n            assert \"Item 42\" in str(result)\n\n\nclass TestResourceTaskMetaDirectServerCall:\n    \"\"\"Tests for direct server read_resource calls with task_meta.\"\"\"\n\n    async def test_resource_can_read_another_resource_with_task(self):\n        \"\"\"A resource can read another resource as a background task.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://inner\", task=True)\n        async def inner_resource() -> str:\n            return \"inner data\"\n\n        @server.tool\n        async def outer_tool() -> str:\n            # Read inner resource as background task\n            result = await server.read_resource(\"data://inner\", task_meta=TaskMeta())\n            # Should get CreateTaskResult since we provided task_meta\n            return f\"Created task: {result.task.taskId}\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"outer_tool\", {})\n            assert \"Created task:\" in str(result)\n\n    async def test_resource_can_read_another_resource_synchronously(self):\n        \"\"\"A resource can read another resource synchronously (no task_meta).\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://inner\", task=True)\n        async def inner_resource() -> str:\n            return \"inner data\"\n\n        @server.tool\n        async def outer_tool() -> str:\n            # Read inner resource synchronously (no task_meta)\n            result = await server.read_resource(\"data://inner\")\n            # Should get ResourceResult directly\n            return f\"Got result: {result.contents[0].content}\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"outer_tool\", {})\n            assert \"Got result: inner data\" in str(result)\n\n    async def test_resource_can_read_template_with_task(self):\n        \"\"\"A tool can read a resource template as a background task.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"item://{id}\", task=True)\n        async def get_item(id: str) -> str:\n            return f\"Item {id}\"\n\n        @server.tool\n        async def outer_tool() -> str:\n            result = await server.read_resource(\"item://99\", task_meta=TaskMeta())\n            return f\"Created task: {result.task.taskId}\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"outer_tool\", {})\n            assert \"Created task:\" in str(result)\n\n    async def test_resource_can_read_with_custom_ttl(self):\n        \"\"\"A tool can read a resource as a background task with custom TTL.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://inner\", task=True)\n        async def inner_resource() -> str:\n            return \"inner data\"\n\n        @server.tool\n        async def outer_tool() -> str:\n            custom_ttl = 45000  # 45 seconds\n            result = await server.read_resource(\n                \"data://inner\", task_meta=TaskMeta(ttl=custom_ttl)\n            )\n            return f\"Task TTL: {result.task.ttl}\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"outer_tool\", {})\n            assert \"Task TTL: 45000\" in str(result)\n\n\nclass TestResourceTaskMetaTypeNarrowing:\n    \"\"\"Tests for type narrowing based on task_meta parameter.\"\"\"\n\n    async def test_read_resource_without_task_meta_type_is_resource_result(self):\n        \"\"\"Calling read_resource without task_meta returns ResourceResult type.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\")\n        async def simple_resource() -> str:\n            return \"hello\"\n\n        # This should type-check as ResourceResult, not the union type\n        result = await server.read_resource(\"data://test\")\n\n        # No isinstance check needed - type is narrowed by overload\n        content = result.contents[0].content\n        assert content == \"hello\"\n\n    async def test_read_resource_with_task_meta_type_is_create_task_result(self):\n        \"\"\"Calling read_resource with task_meta returns CreateTaskResult type.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"data://test\", task=True)\n        async def task_resource() -> str:\n            return \"hello\"\n\n        async with Client(server) as client:\n            # Need to use client to get full task infrastructure\n            from fastmcp.client.tasks import ResourceTask\n\n            task = await client.read_resource(\"data://test\", task=True)\n            assert isinstance(task, ResourceTask)\n\n            # For direct server call, we need the Client context for Docket\n            # This test verifies the overload works via client integration\n            result = await task.result()\n            assert \"hello\" in str(result)\n"
  },
  {
    "path": "tests/server/tasks/test_server_tasks_parameter.py",
    "content": "\"\"\"\nTests for server `tasks` parameter default inheritance.\n\nVerifies that the server's `tasks` parameter correctly sets defaults for all\ncomponents (tools, prompts, resources), and that explicit component-level\nsettings properly override the server default.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.mark.timeout(10)\nasync def test_server_tasks_true_defaults_all_components():\n    \"\"\"Server with tasks=True makes all components default to supporting tasks.\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.tool()\n    async def my_tool() -> str:\n        return \"tool result\"\n\n    @mcp.prompt()\n    async def my_prompt() -> str:\n        return \"prompt result\"\n\n    @mcp.resource(\"test://resource\")\n    async def my_resource() -> str:\n        return \"resource result\"\n\n    async with Client(mcp) as client:\n        # Verify all task-enabled components are registered with docket\n        # Components use prefixed keys: tool:name, prompt:name, resource:uri\n        docket = mcp.docket\n        assert docket is not None\n        assert \"tool:my_tool@\" in docket.tasks\n        assert \"prompt:my_prompt@\" in docket.tasks\n        assert \"resource:test://resource@\" in docket.tasks\n\n        # Tool should support background execution\n        tool_task = await client.call_tool(\"my_tool\", task=True)\n        assert not tool_task.returned_immediately\n\n        # Prompt should support background execution\n        prompt_task = await client.get_prompt(\"my_prompt\", task=True)\n        assert not prompt_task.returned_immediately\n\n        # Resource should support background execution\n        resource_task = await client.read_resource(\"test://resource\", task=True)\n        assert not resource_task.returned_immediately\n\n\nasync def test_server_tasks_false_defaults_all_components():\n    \"\"\"Server with tasks=False makes all components default to mode=forbidden.\"\"\"\n    import pytest\n    from mcp.shared.exceptions import McpError\n\n    mcp = FastMCP(\"test\", tasks=False)\n\n    @mcp.tool()\n    async def my_tool() -> str:\n        return \"tool result\"\n\n    @mcp.prompt()\n    async def my_prompt() -> str:\n        return \"prompt result\"\n\n    @mcp.resource(\"test://resource\")\n    async def my_resource() -> str:\n        return \"resource result\"\n\n    async with Client(mcp) as client:\n        # Tool with mode=\"forbidden\" returns error when called with task=True\n        tool_task = await client.call_tool(\"my_tool\", task=True)\n        assert tool_task.returned_immediately\n        result = await tool_task.result()\n        assert result.is_error\n        assert \"does not support task-augmented execution\" in str(result)\n\n        # Prompt with mode=\"forbidden\" raises McpError when called with task=True\n        with pytest.raises(McpError):\n            await client.get_prompt(\"my_prompt\", task=True)\n\n        # Resource with mode=\"forbidden\" raises McpError when called with task=True\n        with pytest.raises(McpError):\n            await client.read_resource(\"test://resource\", task=True)\n\n\nasync def test_server_tasks_none_defaults_to_false():\n    \"\"\"Server with tasks=None (or omitted) defaults to False.\"\"\"\n    mcp = FastMCP(\"test\")  # tasks=None, defaults to False\n\n    @mcp.tool()\n    async def my_tool() -> str:\n        return \"tool result\"\n\n    async with Client(mcp) as client:\n        # Tool should NOT support background execution (mode=\"forbidden\" from default)\n        tool_task = await client.call_tool(\"my_tool\", task=True)\n        assert tool_task.returned_immediately\n        result = await tool_task.result()\n        assert result.is_error\n        assert \"does not support task-augmented execution\" in str(result)\n\n\nasync def test_component_explicit_false_overrides_server_true():\n    \"\"\"Component with task=False overrides server default of tasks=True.\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.tool(task=False)\n    async def no_task_tool() -> str:\n        return \"immediate result\"\n\n    @mcp.tool()\n    async def default_tool() -> str:\n        return \"background result\"\n\n    async with Client(mcp) as client:\n        # Verify docket registration matches task settings (prefixed keys)\n        docket = mcp.docket\n        assert docket is not None\n        assert (\n            \"tool:no_task_tool@\" not in docket.tasks\n        )  # task=False means not registered\n        assert \"tool:default_tool@\" in docket.tasks  # Inherits tasks=True\n\n        # Explicit False (mode=\"forbidden\") returns error when called with task=True\n        no_task = await client.call_tool(\"no_task_tool\", task=True)\n        assert no_task.returned_immediately\n        result = await no_task.result()\n        assert result.is_error\n        assert \"does not support task-augmented execution\" in str(result)\n\n        # Default should support background execution\n        default_task = await client.call_tool(\"default_tool\", task=True)\n        assert not default_task.returned_immediately\n\n\nasync def test_component_explicit_true_overrides_server_false():\n    \"\"\"Component with task=True overrides server default of tasks=False.\"\"\"\n    mcp = FastMCP(\"test\", tasks=False)\n\n    @mcp.tool(task=True)\n    async def task_tool() -> str:\n        return \"background result\"\n\n    @mcp.tool()\n    async def default_tool() -> str:\n        return \"immediate result\"\n\n    async with Client(mcp) as client:\n        # Verify docket registration matches task settings (prefixed keys)\n        docket = mcp.docket\n        assert docket is not None\n        assert \"tool:task_tool@\" in docket.tasks  # task=True means registered\n        assert \"tool:default_tool@\" not in docket.tasks  # Inherits tasks=False\n\n        # Explicit True should support background execution despite server default\n        task = await client.call_tool(\"task_tool\", task=True)\n        assert not task.returned_immediately\n\n        # Default (mode=\"forbidden\") returns error when called with task=True\n        default = await client.call_tool(\"default_tool\", task=True)\n        assert default.returned_immediately\n        result = await default.result()\n        assert result.is_error\n\n\nasync def test_mixed_explicit_and_inherited():\n    \"\"\"Mix of explicit True/False/None on different components.\"\"\"\n    import pytest\n    from mcp.shared.exceptions import McpError\n\n    mcp = FastMCP(\"test\", tasks=True)  # Server default is True\n\n    @mcp.tool()\n    async def inherited_tool() -> str:\n        return \"inherits True\"\n\n    @mcp.tool(task=True)\n    async def explicit_true_tool() -> str:\n        return \"explicit True\"\n\n    @mcp.tool(task=False)\n    async def explicit_false_tool() -> str:\n        return \"explicit False\"\n\n    @mcp.prompt()\n    async def inherited_prompt() -> str:\n        return \"inherits True\"\n\n    @mcp.prompt(task=False)\n    async def explicit_false_prompt() -> str:\n        return \"explicit False\"\n\n    @mcp.resource(\"test://inherited\")\n    async def inherited_resource() -> str:\n        return \"inherits True\"\n\n    @mcp.resource(\"test://explicit_false\", task=False)\n    async def explicit_false_resource() -> str:\n        return \"explicit False\"\n\n    async with Client(mcp) as client:\n        # Verify docket registration matches task settings\n        # Components use prefixed keys: tool:name, prompt:name, resource:uri\n        docket = mcp.docket\n        assert docket is not None\n        # task=True (explicit or inherited) means registered (with prefixed keys)\n        assert \"tool:inherited_tool@\" in docket.tasks\n        assert \"tool:explicit_true_tool@\" in docket.tasks\n        assert \"prompt:inherited_prompt@\" in docket.tasks\n        assert \"resource:test://inherited@\" in docket.tasks\n        # task=False means NOT registered\n        assert \"tool:explicit_false_tool@\" not in docket.tasks\n        assert \"prompt:explicit_false_prompt@\" not in docket.tasks\n        assert \"resource:test://explicit_false@\" not in docket.tasks\n\n        # Tools\n        inherited = await client.call_tool(\"inherited_tool\", task=True)\n        assert not inherited.returned_immediately\n\n        explicit_true = await client.call_tool(\"explicit_true_tool\", task=True)\n        assert not explicit_true.returned_immediately\n\n        # Explicit False (mode=\"forbidden\") returns error\n        explicit_false = await client.call_tool(\"explicit_false_tool\", task=True)\n        assert explicit_false.returned_immediately\n        result = await explicit_false.result()\n        assert result.is_error\n\n        # Prompts\n        inherited_prompt_task = await client.get_prompt(\"inherited_prompt\", task=True)\n        assert not inherited_prompt_task.returned_immediately\n\n        # Explicit False prompt (mode=\"forbidden\") raises McpError\n        with pytest.raises(McpError):\n            await client.get_prompt(\"explicit_false_prompt\", task=True)\n\n        # Resources\n        inherited_resource_task = await client.read_resource(\n            \"test://inherited\", task=True\n        )\n        assert not inherited_resource_task.returned_immediately\n\n        # Explicit False resource (mode=\"forbidden\") raises McpError\n        with pytest.raises(McpError):\n            await client.read_resource(\"test://explicit_false\", task=True)\n\n\nasync def test_server_tasks_parameter_sets_component_defaults():\n    \"\"\"Server tasks parameter sets component defaults.\"\"\"\n    # Server tasks=True sets component defaults\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.tool()\n    async def tool_inherits_true() -> str:\n        return \"tool result\"\n\n    async with Client(mcp) as client:\n        # Tool inherits tasks=True from server\n        tool_task = await client.call_tool(\"tool_inherits_true\", task=True)\n        assert not tool_task.returned_immediately\n\n    # Server tasks=False sets component defaults\n    mcp2 = FastMCP(\"test2\", tasks=False)\n\n    @mcp2.tool()\n    async def tool_inherits_false() -> str:\n        return \"tool result\"\n\n    async with Client(mcp2) as client:\n        # Tool inherits tasks=False (mode=\"forbidden\") - returns error\n        tool_task = await client.call_tool(\"tool_inherits_false\", task=True)\n        assert tool_task.returned_immediately\n        result = await tool_task.result()\n        assert result.is_error\n\n\nasync def test_resource_template_inherits_server_tasks_default():\n    \"\"\"Resource templates inherit server tasks default.\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.resource(\"test://{item_id}\")\n    async def templated_resource(item_id: str) -> str:\n        return f\"resource {item_id}\"\n\n    async with Client(mcp) as client:\n        # Template should support background execution\n        resource_task = await client.read_resource(\"test://123\", task=True)\n        assert not resource_task.returned_immediately\n\n\nasync def test_multiple_components_same_name_different_tasks():\n    \"\"\"Different component types with same name can have different task settings.\"\"\"\n    import pytest\n    from mcp.shared.exceptions import McpError\n\n    mcp = FastMCP(\"test\", tasks=False)\n\n    @mcp.tool(task=True)\n    async def shared_name() -> str:\n        return \"tool result\"\n\n    @mcp.prompt()\n    async def shared_name_prompt() -> str:\n        return \"prompt result\"\n\n    async with Client(mcp) as client:\n        # Tool with explicit True should support background execution\n        tool_task = await client.call_tool(\"shared_name\", task=True)\n        assert not tool_task.returned_immediately\n\n        # Prompt inheriting False (mode=\"forbidden\") raises McpError\n        with pytest.raises(McpError):\n            await client.get_prompt(\"shared_name_prompt\", task=True)\n\n\nasync def test_task_with_custom_tool_name():\n    \"\"\"Tools with custom names work correctly as tasks (issue #2642).\n\n    When a tool is registered with a custom name different from the function\n    name, task execution should use the custom name for Docket lookup.\n    \"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    async def my_function() -> str:\n        return \"result from custom-named tool\"\n\n    mcp.tool(my_function, name=\"custom-tool-name\")\n\n    async with Client(mcp) as client:\n        # Verify the tool is registered with its custom name in Docket (prefixed key)\n        docket = mcp.docket\n        assert docket is not None\n        assert \"tool:custom-tool-name@\" in docket.tasks\n\n        # Call the tool as a task using its custom name\n        task = await client.call_tool(\"custom-tool-name\", task=True)\n        assert not task.returned_immediately\n        result = await task\n        assert result.data == \"result from custom-named tool\"\n\n\nasync def test_task_with_custom_resource_name():\n    \"\"\"Resources with custom names work correctly as tasks.\n\n    Resources are registered/looked up by their .key (URI), not their name.\n    \"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.resource(\"test://resource\", name=\"custom-resource-name\")\n    async def my_resource_func() -> str:\n        return \"result from custom-named resource\"\n\n    async with Client(mcp) as client:\n        # Verify the resource is registered with its key (prefixed URI) in Docket\n        docket = mcp.docket\n        assert docket is not None\n        assert \"resource:test://resource@\" in docket.tasks\n\n        # Call the resource as a task\n        task = await client.read_resource(\"test://resource\", task=True)\n        assert not task.returned_immediately\n        result = await task.result()\n        assert result[0].text == \"result from custom-named resource\"\n\n\nasync def test_task_with_custom_template_name():\n    \"\"\"Resource templates with custom names work correctly as tasks.\n\n    Templates are registered/looked up by their .key (uri_template), not their name.\n    \"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.resource(\"test://{item_id}\", name=\"custom-template-name\")\n    async def my_template_func(item_id: str) -> str:\n        return f\"result for {item_id}\"\n\n    async with Client(mcp) as client:\n        # Verify the template is registered with its key (prefixed uri_template) in Docket\n        docket = mcp.docket\n        assert docket is not None\n        assert \"template:test://{item_id}@\" in docket.tasks\n\n        # Call the template as a task\n        task = await client.read_resource(\"test://123\", task=True)\n        assert not task.returned_immediately\n        result = await task.result()\n        assert result[0].text == \"result for 123\"\n"
  },
  {
    "path": "tests/server/tasks/test_sync_function_task_disabled.py",
    "content": "\"\"\"\nTests that synchronous functions cannot be used as background tasks.\n\nDocket requires async functions for background execution. FastMCP raises\nValueError when task=True is used with a sync function.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.prompts.function_prompt import FunctionPrompt\nfrom fastmcp.resources.function_resource import FunctionResource\nfrom fastmcp.tools.function_tool import FunctionTool\n\n\nasync def test_sync_tool_with_explicit_task_true_raises():\n    \"\"\"Sync tool with task=True raises ValueError.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n\n        @mcp.tool(task=True)\n        def sync_tool(x: int) -> int:\n            \"\"\"A synchronous tool.\"\"\"\n            return x * 2\n\n\nasync def test_sync_tool_with_inherited_task_true_raises():\n    \"\"\"Sync tool inheriting task=True from server raises ValueError.\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n\n        @mcp.tool()  # Inherits task=True from server\n        def sync_tool(x: int) -> int:\n            \"\"\"A synchronous tool.\"\"\"\n            return x * 2\n\n\nasync def test_sync_prompt_with_explicit_task_true_raises():\n    \"\"\"Sync prompt with task=True raises ValueError.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n\n        @mcp.prompt(task=True)\n        def sync_prompt() -> str:\n            \"\"\"A synchronous prompt.\"\"\"\n            return \"Hello\"\n\n\nasync def test_sync_prompt_with_inherited_task_true_raises():\n    \"\"\"Sync prompt inheriting task=True from server raises ValueError.\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n\n        @mcp.prompt()  # Inherits task=True from server\n        def sync_prompt() -> str:\n            \"\"\"A synchronous prompt.\"\"\"\n            return \"Hello\"\n\n\nasync def test_sync_resource_with_explicit_task_true_raises():\n    \"\"\"Sync resource with task=True raises ValueError.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n\n        @mcp.resource(\"test://sync\", task=True)\n        def sync_resource() -> str:\n            \"\"\"A synchronous resource.\"\"\"\n            return \"data\"\n\n\nasync def test_sync_resource_with_inherited_task_true_raises():\n    \"\"\"Sync resource inheriting task=True from server raises ValueError.\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n\n        @mcp.resource(\"test://sync\")  # Inherits task=True from server\n        def sync_resource() -> str:\n            \"\"\"A synchronous resource.\"\"\"\n            return \"data\"\n\n\nasync def test_async_tool_with_task_true_remains_enabled():\n    \"\"\"Async tools with task=True keep task support enabled.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool(task=True)\n    async def async_tool(x: int) -> int:\n        \"\"\"An async tool.\"\"\"\n        return x * 2\n\n    # Tool should have task mode=\"optional\" and be a FunctionTool\n    tool = await mcp.get_tool(\"async_tool\")\n    assert isinstance(tool, FunctionTool)\n    assert tool.task_config.mode == \"optional\"\n\n\nasync def test_async_prompt_with_task_true_remains_enabled():\n    \"\"\"Async prompts with task=True keep task support enabled.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.prompt(task=True)\n    async def async_prompt() -> str:\n        \"\"\"An async prompt.\"\"\"\n        return \"Hello\"\n\n    # Prompt should have task mode=\"optional\" and be a FunctionPrompt\n    prompt = await mcp.get_prompt(\"async_prompt\")\n    assert isinstance(prompt, FunctionPrompt)\n    assert prompt.task_config.mode == \"optional\"\n\n\nasync def test_async_resource_with_task_true_remains_enabled():\n    \"\"\"Async resources with task=True keep task support enabled.\"\"\"\n    mcp = FastMCP(\"test\")\n\n    @mcp.resource(\"test://async\", task=True)\n    async def async_resource() -> str:\n        \"\"\"An async resource.\"\"\"\n        return \"data\"\n\n    # Resource should have task mode=\"optional\" and be a FunctionResource\n    resource = await mcp.get_resource(\"test://async\")\n    assert isinstance(resource, FunctionResource)\n    assert resource.task_config.mode == \"optional\"\n\n\nasync def test_sync_tool_with_task_false_works():\n    \"\"\"Sync tool with explicit task=False works (no error).\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.tool(task=False)  # Explicitly disable\n    def sync_tool(x: int) -> int:\n        \"\"\"A synchronous tool.\"\"\"\n        return x * 2\n\n    tool = await mcp.get_tool(\"sync_tool\")\n    assert isinstance(tool, FunctionTool)\n    assert tool.task_config.mode == \"forbidden\"\n\n\nasync def test_sync_prompt_with_task_false_works():\n    \"\"\"Sync prompt with explicit task=False works (no error).\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.prompt(task=False)  # Explicitly disable\n    def sync_prompt() -> str:\n        \"\"\"A synchronous prompt.\"\"\"\n        return \"Hello\"\n\n    prompt = await mcp.get_prompt(\"sync_prompt\")\n    assert isinstance(prompt, FunctionPrompt)\n    assert prompt.task_config.mode == \"forbidden\"\n\n\nasync def test_sync_resource_with_task_false_works():\n    \"\"\"Sync resource with explicit task=False works (no error).\"\"\"\n    mcp = FastMCP(\"test\", tasks=True)\n\n    @mcp.resource(\"test://sync\", task=False)  # Explicitly disable\n    def sync_resource() -> str:\n        \"\"\"A synchronous resource.\"\"\"\n        return \"data\"\n\n    resource = await mcp.get_resource(\"test://sync\")\n    assert isinstance(resource, FunctionResource)\n    assert resource.task_config.mode == \"forbidden\"\n\n\n# =============================================================================\n# Callable classes and staticmethods with async __call__\n# =============================================================================\n\n\nasync def test_async_callable_class_tool_with_task_true_works():\n    \"\"\"Callable class with async __call__ and task=True should work.\"\"\"\n    from fastmcp.tools import Tool\n\n    class AsyncCallableTool:\n        async def __call__(self, x: int) -> int:\n            return x * 2\n\n    # Callable classes use Tool.from_function() directly\n    tool = Tool.from_function(AsyncCallableTool(), task=True)\n    assert tool.task_config.mode == \"optional\"\n\n\nasync def test_async_callable_class_prompt_with_task_true_works():\n    \"\"\"Callable class with async __call__ and task=True should work.\"\"\"\n    from fastmcp.prompts import Prompt\n\n    class AsyncCallablePrompt:\n        async def __call__(self) -> str:\n            return \"Hello\"\n\n    # Callable classes use Prompt.from_function() directly\n    prompt = Prompt.from_function(AsyncCallablePrompt(), task=True)\n    assert prompt.task_config.mode == \"optional\"\n\n\nasync def test_sync_callable_class_tool_with_task_true_raises():\n    \"\"\"Callable class with sync __call__ and task=True should raise.\"\"\"\n    from fastmcp.tools import Tool\n\n    class SyncCallableTool:\n        def __call__(self, x: int) -> int:\n            return x * 2\n\n    with pytest.raises(\n        ValueError, match=\"uses a sync function but has task execution enabled\"\n    ):\n        Tool.from_function(SyncCallableTool(), task=True)\n"
  },
  {
    "path": "tests/server/tasks/test_task_capabilities.py",
    "content": "\"\"\"\nTests for SEP-1686 task capabilities declaration.\n\nVerifies that the server correctly advertises task support.\nTask protocol is now always enabled.\n\"\"\"\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.tasks import get_task_capabilities\n\n\nasync def test_capabilities_include_tasks():\n    \"\"\"Server capabilities always include tasks in first-class field (SEP-1686).\"\"\"\n    mcp = FastMCP(\"capability-test\")\n\n    @mcp.tool()\n    async def test_tool() -> str:\n        return \"test\"\n\n    async with Client(mcp) as client:\n        # Get server initialization result which includes capabilities\n        init_result = client.initialize_result\n\n        # Verify tasks capability is present as a first-class field (not experimental)\n        assert init_result.capabilities.tasks is not None\n        assert init_result.capabilities.tasks == get_task_capabilities()\n        # Verify it's NOT in experimental\n        assert \"tasks\" not in (init_result.capabilities.experimental or {})\n\n\nasync def test_client_uses_task_capable_session():\n    \"\"\"Client uses task-capable initialization.\"\"\"\n    mcp = FastMCP(\"client-cap-test\")\n\n    @mcp.tool()\n    async def test_tool() -> str:\n        return \"test\"\n\n    async with Client(mcp) as client:\n        # Client should have connected successfully with task capabilities\n        assert client.initialize_result is not None\n        # Session should be a ClientSession (task-capable init uses standard session)\n        assert type(client.session).__name__ == \"ClientSession\"\n"
  },
  {
    "path": "tests/server/tasks/test_task_config.py",
    "content": "\"\"\"Tests for TaskConfig (SEP-1686).\n\nTests for TaskConfig:\n- Mode enforcement (forbidden, optional, required)\n- Poll interval configuration\n\"\"\"\n\nfrom datetime import timedelta\n\nimport pytest\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import TextContent, ToolExecution\nfrom mcp.types import Tool as MCPTool\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.tasks import TaskConfig\nfrom fastmcp.tools.base import Tool\n\n\nclass TestTaskConfigNormalization:\n    \"\"\"Test that boolean task values normalize correctly to TaskConfig.\"\"\"\n\n    async def test_task_true_normalizes_to_optional(self):\n        \"\"\"task=True should normalize to TaskConfig(mode='optional').\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)  # Disable default task support\n\n        @mcp.tool(task=True)\n        async def my_tool() -> str:\n            return \"ok\"\n\n        tool = await mcp.get_tool(\"my_tool\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.mode == \"optional\"\n\n    async def test_task_false_normalizes_to_forbidden(self):\n        \"\"\"task=False should normalize to TaskConfig(mode='forbidden').\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=False)\n        async def my_tool() -> str:\n            return \"ok\"\n\n        tool = await mcp.get_tool(\"my_tool\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.mode == \"forbidden\"\n\n    async def test_task_config_passed_directly(self):\n        \"\"\"TaskConfig should be preserved when passed directly.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"required\"))\n        async def my_tool() -> str:\n            return \"ok\"\n\n        tool = await mcp.get_tool(\"my_tool\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.mode == \"required\"\n\n    async def test_default_task_inherits_server_default(self):\n        \"\"\"Default task value should inherit from server default.\"\"\"\n        # Server with tasks disabled\n        mcp_no_tasks = FastMCP(\"test\", tasks=False)\n\n        @mcp_no_tasks.tool()\n        def my_tool_sync() -> str:\n            return \"ok\"\n\n        tool = await mcp_no_tasks.get_tool(\"my_tool_sync\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.mode == \"forbidden\"\n\n        # Server with tasks enabled\n        mcp_tasks = FastMCP(\"test\", tasks=True)\n\n        @mcp_tasks.tool()\n        async def my_tool_async() -> str:\n            return \"ok\"\n\n        tool2 = await mcp_tasks.get_tool(\"my_tool_async\")\n        assert isinstance(tool2, Tool)\n        assert tool2.task_config.mode == \"optional\"\n\n\nclass TestToolModeEnforcement:\n    \"\"\"Test mode enforcement for tools.\"\"\"\n\n    @pytest.fixture\n    def server(self):\n        \"\"\"Create server with tools in different modes.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"required\"))\n        async def required_tool() -> str:\n            \"\"\"Tool that requires task execution.\"\"\"\n            return \"required result\"\n\n        @mcp.tool(task=TaskConfig(mode=\"forbidden\"))\n        async def forbidden_tool() -> str:\n            \"\"\"Tool that forbids task execution.\"\"\"\n            return \"forbidden result\"\n\n        @mcp.tool(task=TaskConfig(mode=\"optional\"))\n        async def optional_tool() -> str:\n            \"\"\"Tool that supports both modes.\"\"\"\n            return \"optional result\"\n\n        return mcp\n\n    async def test_required_mode_without_task_returns_error(self, server):\n        \"\"\"Required mode raises error when called without task metadata.\"\"\"\n        async with Client(server) as client:\n            with pytest.raises(ToolError) as exc_info:\n                await client.call_tool(\"required_tool\", {})\n\n            assert \"requires task-augmented execution\" in str(exc_info.value)\n\n    async def test_required_mode_with_task_succeeds(self, server):\n        \"\"\"Required mode succeeds when called with task metadata.\"\"\"\n        async with Client(server) as client:\n            task = await client.call_tool(\"required_tool\", {}, task=True)\n            assert task is not None\n            result = await task.result()\n            assert result.data == \"required result\"\n\n    async def test_forbidden_mode_with_task_returns_error(self, server):\n        \"\"\"Forbidden mode returns error when called with task metadata.\"\"\"\n        async with Client(server) as client:\n            # Call with task=True should fail\n            task = await client.call_tool(\"forbidden_tool\", {}, task=True)\n            assert task is not None\n            # The task should have returned immediately with an error\n            assert task.returned_immediately\n            result = await task.result()\n            # Check for error in the result\n            assert result.is_error\n\n    async def test_forbidden_mode_without_task_succeeds(self, server):\n        \"\"\"Forbidden mode succeeds when called without task metadata.\"\"\"\n        async with Client(server) as client:\n            result = await client.call_tool(\"forbidden_tool\", {})\n            assert \"forbidden result\" in str(result)\n\n    async def test_optional_mode_without_task_succeeds(self, server):\n        \"\"\"Optional mode succeeds when called without task metadata.\"\"\"\n        async with Client(server) as client:\n            result = await client.call_tool(\"optional_tool\", {})\n            assert \"optional result\" in str(result)\n\n    async def test_optional_mode_with_task_succeeds(self, server):\n        \"\"\"Optional mode succeeds when called with task metadata.\"\"\"\n        async with Client(server) as client:\n            task = await client.call_tool(\"optional_tool\", {}, task=True)\n            assert task is not None\n            result = await task.result()\n            assert result.data == \"optional result\"\n\n\nclass TestResourceModeEnforcement:\n    \"\"\"Test mode enforcement for resources.\"\"\"\n\n    @pytest.fixture\n    def server(self):\n        \"\"\"Create server with resources in different modes.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.resource(\"resource://required\", task=TaskConfig(mode=\"required\"))\n        async def required_resource() -> str:\n            \"\"\"Resource that requires task execution.\"\"\"\n            return \"required content\"\n\n        @mcp.resource(\"resource://forbidden\", task=TaskConfig(mode=\"forbidden\"))\n        async def forbidden_resource() -> str:\n            \"\"\"Resource that forbids task execution.\"\"\"\n            return \"forbidden content\"\n\n        @mcp.resource(\"resource://optional\", task=TaskConfig(mode=\"optional\"))\n        async def optional_resource() -> str:\n            \"\"\"Resource that supports both modes.\"\"\"\n            return \"optional content\"\n\n        return mcp\n\n    async def test_required_resource_without_task_returns_error(self, server):\n        \"\"\"Required mode returns error when read without task metadata.\"\"\"\n        from mcp.types import METHOD_NOT_FOUND\n\n        async with Client(server) as client:\n            with pytest.raises(McpError) as exc_info:\n                await client.read_resource(\"resource://required\")\n\n            assert exc_info.value.error.code == METHOD_NOT_FOUND\n            assert \"requires task-augmented execution\" in exc_info.value.error.message\n\n    async def test_required_resource_with_task_succeeds(self, server):\n        \"\"\"Required mode succeeds when read with task metadata.\"\"\"\n        async with Client(server) as client:\n            task = await client.read_resource(\"resource://required\", task=True)\n            assert task is not None\n            result = await task.result()\n            # Result is a list of resource contents\n            assert \"required content\" in str(result)\n\n    async def test_forbidden_resource_without_task_succeeds(self, server):\n        \"\"\"Forbidden mode succeeds when read without task metadata.\"\"\"\n        async with Client(server) as client:\n            result = await client.read_resource(\"resource://forbidden\")\n            assert \"forbidden content\" in str(result)\n\n\nclass TestPromptModeEnforcement:\n    \"\"\"Test mode enforcement for prompts.\"\"\"\n\n    @pytest.fixture\n    def server(self):\n        \"\"\"Create server with prompts in different modes.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.prompt(task=TaskConfig(mode=\"required\"))\n        async def required_prompt() -> str:\n            \"\"\"Prompt that requires task execution.\"\"\"\n            return \"required message\"\n\n        @mcp.prompt(task=TaskConfig(mode=\"forbidden\"))\n        async def forbidden_prompt() -> str:\n            \"\"\"Prompt that forbids task execution.\"\"\"\n            return \"forbidden message\"\n\n        @mcp.prompt(task=TaskConfig(mode=\"optional\"))\n        async def optional_prompt() -> str:\n            \"\"\"Prompt that supports both modes.\"\"\"\n            return \"optional message\"\n\n        return mcp\n\n    async def test_required_prompt_without_task_returns_error(self, server):\n        \"\"\"Required mode returns error when called without task metadata.\"\"\"\n        from mcp.types import METHOD_NOT_FOUND\n\n        async with Client(server) as client:\n            with pytest.raises(McpError) as exc_info:\n                await client.get_prompt(\"required_prompt\")\n\n            assert exc_info.value.error.code == METHOD_NOT_FOUND\n            assert \"requires task-augmented execution\" in exc_info.value.error.message\n\n    async def test_required_prompt_with_task_succeeds(self, server):\n        \"\"\"Required mode succeeds when called with task metadata.\"\"\"\n        async with Client(server) as client:\n            task = await client.get_prompt(\"required_prompt\", task=True)\n            assert task is not None\n            result = await task.result()\n            # Result contains the prompt messages\n            assert \"required message\" in str(result)\n\n    async def test_forbidden_prompt_without_task_succeeds(self, server):\n        \"\"\"Forbidden mode succeeds when called without task metadata.\"\"\"\n        async with Client(server) as client:\n            result = await client.get_prompt(\"forbidden_prompt\")\n            assert isinstance(result.messages[0].content, TextContent)\n            assert \"forbidden message\" in str(result.messages[0].content)\n\n\nclass TestToolExecutionMetadata:\n    \"\"\"Test that ToolExecution.taskSupport is set correctly in tool metadata.\"\"\"\n\n    async def test_optional_tool_exposes_task_support(self):\n        \"\"\"Tools with task enabled should expose taskSupport in metadata.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"optional\"))\n        async def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = next(t for t in tools if t.name == \"my_tool\")\n            assert isinstance(tool, MCPTool)\n            assert isinstance(tool.execution, ToolExecution)\n            assert tool.execution.taskSupport == \"optional\"\n\n    async def test_required_tool_exposes_task_support(self):\n        \"\"\"Tools with mode=required should expose taskSupport='required'.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"required\"))\n        async def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = next(t for t in tools if t.name == \"my_tool\")\n            assert isinstance(tool, MCPTool)\n            assert isinstance(tool.execution, ToolExecution)\n            assert tool.execution.taskSupport == \"required\"\n\n    async def test_forbidden_tool_has_no_execution(self):\n        \"\"\"Tools with mode=forbidden should not expose execution metadata.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"forbidden\"))\n        async def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = next(t for t in tools if t.name == \"my_tool\")\n            assert tool.execution is None\n\n\nclass TestSyncFunctionValidation:\n    \"\"\"Test that sync functions cannot have task execution enabled.\"\"\"\n\n    def test_sync_function_with_task_true_raises(self):\n        \"\"\"Sync functions should raise ValueError when task=True.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        with pytest.raises(ValueError, match=\"sync function\"):\n\n            @mcp.tool(task=True)\n            def sync_tool() -> str:\n                return \"ok\"\n\n    def test_sync_function_with_required_mode_raises(self):\n        \"\"\"Sync functions should raise ValueError with mode='required'.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        with pytest.raises(ValueError, match=\"sync function\"):\n\n            @mcp.tool(task=TaskConfig(mode=\"required\"))\n            def sync_tool() -> str:\n                return \"ok\"\n\n    def test_sync_function_with_optional_mode_raises(self):\n        \"\"\"Sync functions should raise ValueError with mode='optional'.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        with pytest.raises(ValueError, match=\"sync function\"):\n\n            @mcp.tool(task=TaskConfig(mode=\"optional\"))\n            def sync_tool() -> str:\n                return \"ok\"\n\n    async def test_sync_function_with_forbidden_mode_ok(self):\n        \"\"\"Sync functions should work fine with mode='forbidden'.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"forbidden\"))\n        def sync_tool() -> str:\n            return \"ok\"\n\n        tool = await mcp.get_tool(\"sync_tool\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.mode == \"forbidden\"\n\n\nclass TestPollIntervalConfiguration:\n    \"\"\"Test poll_interval configuration in TaskConfig.\"\"\"\n\n    async def test_default_poll_interval_is_5_seconds(self):\n        \"\"\"Default poll_interval should be 5 seconds.\"\"\"\n        config = TaskConfig()\n        assert config.poll_interval == timedelta(seconds=5)\n\n    async def test_custom_poll_interval_preserved(self):\n        \"\"\"Custom poll_interval should be preserved in TaskConfig.\"\"\"\n        config = TaskConfig(poll_interval=timedelta(seconds=10))\n        assert config.poll_interval == timedelta(seconds=10)\n\n    async def test_tool_inherits_poll_interval(self):\n        \"\"\"Tool should inherit poll_interval from TaskConfig.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"optional\", poll_interval=timedelta(seconds=2)))\n        async def my_tool() -> str:\n            return \"ok\"\n\n        tool = await mcp.get_tool(\"my_tool\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.poll_interval == timedelta(seconds=2)\n\n    async def test_task_true_uses_default_poll_interval(self):\n        \"\"\"task=True should use default 5 second poll_interval.\"\"\"\n        mcp = FastMCP(\"test\", tasks=False)\n\n        @mcp.tool(task=True)\n        async def my_tool() -> str:\n            return \"ok\"\n\n        tool = await mcp.get_tool(\"my_tool\")\n        assert isinstance(tool, Tool)\n        assert tool.task_config.poll_interval == timedelta(seconds=5)\n"
  },
  {
    "path": "tests/server/tasks/test_task_dependencies.py",
    "content": "\"\"\"Tests for dependency injection in background tasks.\n\nThese tests verify that Docket's dependency system works correctly when\nuser functions are queued as background tasks. Dependencies like CurrentDocket(),\nCurrentFastMCP(), and Depends() should be resolved in the worker context.\n\"\"\"\n\nfrom contextlib import asynccontextmanager\nfrom typing import Any, cast\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.dependencies import CurrentDocket, CurrentFastMCP, Depends\n\n\n@pytest.fixture\nasync def dependency_server():\n    \"\"\"Create a FastMCP server with dependency-using background tasks.\"\"\"\n    mcp = FastMCP(\"dependency-test-server\")\n\n    # Track dependency injection\n    injected_values = []\n\n    @mcp.tool(task=True)\n    async def tool_with_docket_dependency(docket=CurrentDocket()) -> str:\n        \"\"\"Background tool that uses CurrentDocket dependency.\"\"\"\n        injected_values.append((\"docket\", docket))\n        return f\"Docket: {docket is not None}\"\n\n    @mcp.tool(task=True)\n    async def tool_with_server_dependency(server=CurrentFastMCP()) -> str:\n        \"\"\"Background tool that uses CurrentFastMCP dependency.\"\"\"\n        injected_values.append((\"server\", server))\n        return f\"Server: {server.name}\"\n\n    @mcp.tool(task=True)\n    async def tool_with_custom_dependency(\n        value: int, multiplier: int = Depends(lambda: 10)\n    ) -> int:\n        \"\"\"Background tool with custom Depends().\"\"\"\n        injected_values.append((\"multiplier\", multiplier))\n        return value * multiplier\n\n    @mcp.tool(task=True)\n    async def tool_with_multiple_dependencies(\n        name: str,\n        docket=CurrentDocket(),\n        server=CurrentFastMCP(),\n    ) -> str:\n        \"\"\"Background tool with multiple dependencies.\"\"\"\n        injected_values.append((\"multi_docket\", docket))\n        injected_values.append((\"multi_server\", server))\n        return f\"{name} on {server.name}\"\n\n    @mcp.prompt(task=True)\n    async def prompt_with_server_dependency(topic: str, server=CurrentFastMCP()) -> str:\n        \"\"\"Background prompt that uses CurrentFastMCP dependency.\"\"\"\n        injected_values.append((\"prompt_server\", server))\n        return f\"Prompt from {server.name} about {topic}\"\n\n    @mcp.resource(\"file://data.txt\", task=True)\n    async def resource_with_docket_dependency(docket=CurrentDocket()) -> str:\n        \"\"\"Background resource that uses CurrentDocket dependency.\"\"\"\n        injected_values.append((\"resource_docket\", docket))\n        return f\"Resource via Docket: {docket is not None}\"\n\n    # Expose for test assertions\n    mcp._injected_values = injected_values  # type: ignore[attr-defined]\n\n    return mcp\n\n\nasync def test_background_tool_receives_docket_dependency(dependency_server):\n    \"\"\"Background tools can use CurrentDocket() and it resolves correctly.\"\"\"\n    async with Client(dependency_server) as client:\n        task = await client.call_tool(\"tool_with_docket_dependency\", {}, task=True)\n\n        # Verify it's background\n        assert not task.returned_immediately\n\n        # Get result - will execute in Docket worker\n        result = await task\n\n        # Verify dependency was injected\n        assert len(dependency_server._injected_values) == 1\n        dep_type, dep_value = dependency_server._injected_values[0]\n        assert dep_type == \"docket\"\n        assert dep_value is not None\n        assert \"Docket: True\" in result.data\n\n\nasync def test_background_tool_receives_server_dependency(dependency_server):\n    \"\"\"Background tools can use CurrentFastMCP() and get the actual FastMCP server.\"\"\"\n    dependency_server._injected_values.clear()\n\n    async with Client(dependency_server) as client:\n        task = await client.call_tool(\"tool_with_server_dependency\", {}, task=True)\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        result = await task\n\n        # Check the server instance was injected\n        assert len(dependency_server._injected_values) == 1\n        dep_type, dep_value = dependency_server._injected_values[0]\n        assert dep_type == \"server\"\n        assert dep_value is dependency_server  # Same instance!\n        assert f\"Server: {dependency_server.name}\" in result.data\n\n\nasync def test_background_tool_receives_custom_depends(dependency_server):\n    \"\"\"Background tools can use Depends() with custom functions.\"\"\"\n    dependency_server._injected_values.clear()\n\n    async with Client(dependency_server) as client:\n        task = await client.call_tool(\n            \"tool_with_custom_dependency\", {\"value\": 5}, task=True\n        )\n\n        assert not task.returned_immediately\n\n        result = await task\n\n        # Check dependency was resolved\n        assert len(dependency_server._injected_values) == 1\n        dep_type, dep_value = dependency_server._injected_values[0]\n        assert dep_type == \"multiplier\"\n        assert dep_value == 10\n        assert result.data == 50  # 5 * 10\n\n\nasync def test_background_tool_with_multiple_dependencies(dependency_server):\n    \"\"\"Background tools can have multiple dependencies injected simultaneously.\"\"\"\n    dependency_server._injected_values.clear()\n\n    async with Client(dependency_server) as client:\n        task = await client.call_tool(\n            \"tool_with_multiple_dependencies\", {\"name\": \"test\"}, task=True\n        )\n\n        assert not task.returned_immediately\n\n        await task\n\n        # Both dependencies should be injected\n        assert len(dependency_server._injected_values) == 2\n\n        dep_types = {item[0] for item in dependency_server._injected_values}\n        assert \"multi_docket\" in dep_types\n        assert \"multi_server\" in dep_types\n\n        # Verify values\n        server_dep = next(\n            v for t, v in dependency_server._injected_values if t == \"multi_server\"\n        )\n        assert server_dep is dependency_server\n\n\nasync def test_background_prompt_receives_dependencies(dependency_server):\n    \"\"\"Background prompts can use dependency injection.\"\"\"\n    dependency_server._injected_values.clear()\n\n    async with Client(dependency_server) as client:\n        task = await client.get_prompt(\n            \"prompt_with_server_dependency\", {\"topic\": \"AI\"}, task=True\n        )\n\n        assert not task.returned_immediately\n\n        await task\n\n        # Check dependency was injected\n        assert len(dependency_server._injected_values) == 1\n        dep_type, dep_value = dependency_server._injected_values[0]\n        assert dep_type == \"prompt_server\"\n        assert dep_value is dependency_server\n\n\nasync def test_background_resource_receives_dependencies(dependency_server):\n    \"\"\"Background resources can use dependency injection.\"\"\"\n    dependency_server._injected_values.clear()\n\n    async with Client(dependency_server) as client:\n        task = await client.read_resource(\"file://data.txt\", task=True)\n\n        assert not task.returned_immediately\n\n        await task\n\n        # Check dependency was injected\n        assert len(dependency_server._injected_values) == 1\n        dep_type, dep_value = dependency_server._injected_values[0]\n        assert dep_type == \"resource_docket\"\n        assert dep_value is not None\n\n\nasync def test_foreground_tool_dependencies_unaffected(dependency_server):\n    \"\"\"Synchronous tools (task=False) still get dependencies as before.\"\"\"\n    dependency_server._injected_values.clear()\n\n    @dependency_server.tool()  # task=False\n    async def sync_tool(server=CurrentFastMCP()) -> str:\n        dependency_server._injected_values.append((\"sync_server\", server))\n        return f\"Sync: {server.name}\"\n\n    async with Client(dependency_server) as client:\n        await client.call_tool(\"sync_tool\", {})\n\n        # Should execute immediately\n        assert len(dependency_server._injected_values) == 1\n        assert dependency_server._injected_values[0][1] is dependency_server\n\n\nasync def test_dependency_context_managers_cleaned_up_in_background():\n    \"\"\"Context manager dependencies are properly cleaned up after background task.\"\"\"\n    cleanup_called = []\n\n    mcp = FastMCP(\"cleanup-test\")\n\n    @asynccontextmanager\n    async def tracked_connection():\n        try:\n            cleanup_called.append(\"enter\")\n            yield \"connection\"\n        finally:\n            cleanup_called.append(\"exit\")\n\n    @mcp.tool(task=True)\n    async def use_connection(name: str, conn: str = Depends(tracked_connection)) -> str:\n        assert conn == \"connection\"\n        assert \"enter\" in cleanup_called\n        assert \"exit\" not in cleanup_called  # Still open during execution\n        return f\"Used: {conn}\"\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\"use_connection\", {\"name\": \"test\"}, task=True)\n        result = await task\n\n        # After task completes, cleanup should have been called\n        assert cleanup_called == [\"enter\", \"exit\"]\n        assert \"Used: connection\" in result.data\n\n\nasync def test_dependency_errors_propagate_to_task_failure():\n    \"\"\"If dependency resolution fails, the background task should fail.\"\"\"\n    mcp = FastMCP(\"error-test\")\n\n    async def failing_dependency():\n        raise ValueError(\"Dependency failed!\")\n\n    @mcp.tool(task=True)\n    async def tool_with_failing_dep(\n        value: str, dep: str = cast(Any, Depends(failing_dependency))\n    ) -> str:\n        return f\"Got: {dep}\"\n\n    from fastmcp.exceptions import ToolError\n\n    async with Client(mcp) as client:\n        task = await client.call_tool(\n            \"tool_with_failing_dep\", {\"value\": \"test\"}, task=True\n        )\n\n        # Task should fail due to dependency error\n        with pytest.raises(ToolError, match=\"Failed to resolve dependencies\"):\n            await task.result()\n\n        # Verify it reached failed state\n        status = await task.status()\n        assert status.status == \"failed\"\n"
  },
  {
    "path": "tests/server/tasks/test_task_elicitation_relay.py",
    "content": "\"\"\"Tests for background task elicitation relay (notifications.py).\n\nThe relay bridges distributed background tasks to clients via the standard\nMCP elicitation/create protocol. When a worker calls ctx.elicit(), the\nnotification subscriber detects the input_required notification and sends\nan elicitation/create request to the client session. The client's\nelicitation_handler fires, and the relay pushes the response to Redis\nfor the blocked worker.\n\nThese tests use Client(mcp) with the real memory:// Docket backend.\n\"\"\"\n\nimport asyncio\nfrom dataclasses import dataclass\n\nfrom pydantic import BaseModel\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.elicitation import ElicitResult\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.elicitation import (\n    AcceptedElicitation,\n    CancelledElicitation,\n    DeclinedElicitation,\n)\n\n\nclass TestElicitationRelay:\n    \"\"\"E2E tests for elicitation flowing through the standard MCP protocol.\"\"\"\n\n    async def test_accept_via_elicitation_handler(self):\n        \"\"\"Tool elicits, client handler accepts, tool gets the value.\"\"\"\n        mcp = FastMCP(\"relay-accept\")\n\n        @mcp.tool(task=True)\n        async def ask_name(ctx: Context) -> str:\n            result = await ctx.elicit(\"What is your name?\", str)\n            if isinstance(result, AcceptedElicitation):\n                return f\"Hello, {result.data}!\"\n            return \"No name\"\n\n        async def handler(message, response_type, params, ctx):\n            assert message == \"What is your name?\"\n            return ElicitResult(action=\"accept\", content={\"value\": \"Alice\"})\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"ask_name\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"Hello, Alice!\"\n\n    async def test_decline_via_elicitation_handler(self):\n        \"\"\"Tool elicits, client handler declines, tool gets DeclinedElicitation.\"\"\"\n        mcp = FastMCP(\"relay-decline\")\n\n        @mcp.tool(task=True)\n        async def optional_input(ctx: Context) -> str:\n            result = await ctx.elicit(\"Provide a name?\", str)\n            if isinstance(result, DeclinedElicitation):\n                return \"User declined\"\n            if isinstance(result, AcceptedElicitation):\n                return f\"Got: {result.data}\"\n            return \"Cancelled\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"decline\")\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"optional_input\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"User declined\"\n\n    async def test_cancel_via_elicitation_handler(self):\n        \"\"\"Tool elicits, client handler cancels, tool gets CancelledElicitation.\"\"\"\n        mcp = FastMCP(\"relay-cancel\")\n\n        @mcp.tool(task=True)\n        async def cancellable(ctx: Context) -> str:\n            result = await ctx.elicit(\"Input?\", str)\n            if isinstance(result, CancelledElicitation):\n                return \"Cancelled\"\n            return \"Not cancelled\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"cancel\")\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"cancellable\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"Cancelled\"\n\n    async def test_dataclass_round_trips_through_relay(self):\n        \"\"\"Structured dataclass type round-trips through the relay.\"\"\"\n        mcp = FastMCP(\"relay-dataclass\")\n\n        @dataclass\n        class UserInfo:\n            name: str\n            age: int\n\n        @mcp.tool(task=True)\n        async def get_user(ctx: Context) -> str:\n            result = await ctx.elicit(\"Provide user info\", UserInfo)\n            if isinstance(result, AcceptedElicitation):\n                assert isinstance(result.data, UserInfo)\n                return f\"{result.data.name} is {result.data.age}\"\n            return \"No info\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(action=\"accept\", content={\"name\": \"Bob\", \"age\": 30})\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"get_user\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"Bob is 30\"\n\n    async def test_pydantic_model_round_trips_through_relay(self):\n        \"\"\"Structured Pydantic model round-trips through the relay.\"\"\"\n        mcp = FastMCP(\"relay-pydantic\")\n\n        class Config(BaseModel):\n            host: str\n            port: int\n\n        @mcp.tool(task=True)\n        async def get_config(ctx: Context) -> str:\n            result = await ctx.elicit(\"Server config?\", Config)\n            if isinstance(result, AcceptedElicitation):\n                assert isinstance(result.data, Config)\n                return f\"{result.data.host}:{result.data.port}\"\n            return \"No config\"\n\n        async def handler(message, response_type, params, ctx):\n            return ElicitResult(\n                action=\"accept\", content={\"host\": \"localhost\", \"port\": 8080}\n            )\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"get_config\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"localhost:8080\"\n\n    async def test_multiple_sequential_elicitations(self):\n        \"\"\"Tool calls ctx.elicit() twice, both go through the relay.\"\"\"\n        mcp = FastMCP(\"relay-multi\")\n\n        @mcp.tool(task=True)\n        async def two_questions(ctx: Context) -> str:\n            r1 = await ctx.elicit(\"First name?\", str)\n            r2 = await ctx.elicit(\"Last name?\", str)\n            if isinstance(r1, AcceptedElicitation) and isinstance(\n                r2, AcceptedElicitation\n            ):\n                return f\"{r1.data} {r2.data}\"\n            return \"Incomplete\"\n\n        call_count = 0\n\n        async def handler(message, response_type, params, ctx):\n            nonlocal call_count\n            call_count += 1\n            if call_count == 1:\n                assert message == \"First name?\"\n                return ElicitResult(action=\"accept\", content={\"value\": \"Jane\"})\n            else:\n                assert message == \"Last name?\"\n                return ElicitResult(action=\"accept\", content={\"value\": \"Doe\"})\n\n        async with Client(mcp, elicitation_handler=handler) as client:\n            task = await client.call_tool(\"two_questions\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"Jane Doe\"\n            assert call_count == 2\n\n    async def test_no_elicitation_handler_returns_cancel(self):\n        \"\"\"Without an elicitation_handler, the relay fails and task gets cancel.\"\"\"\n        mcp = FastMCP(\"relay-no-handler\")\n\n        @mcp.tool(task=True)\n        async def needs_input(ctx: Context) -> str:\n            result = await ctx.elicit(\"Input?\", str)\n            if isinstance(result, CancelledElicitation):\n                return \"Cancelled as expected\"\n            if isinstance(result, AcceptedElicitation):\n                return f\"Got: {result.data}\"\n            return \"Other\"\n\n        async with Client(mcp) as client:\n            task = await client.call_tool(\"needs_input\", {}, task=True)\n            result = await asyncio.wait_for(task.result(), timeout=15.0)\n            assert result.data == \"Cancelled as expected\"\n"
  },
  {
    "path": "tests/server/tasks/test_task_meta_parameter.py",
    "content": "\"\"\"\nTests for the explicit task_meta parameter on FastMCP.call_tool().\n\nThese tests verify that the task_meta parameter provides explicit control\nover sync vs task execution, replacing implicit contextvar-based behavior.\n\"\"\"\n\nimport mcp.types\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.server.tasks.config import TaskMeta\nfrom fastmcp.tools.base import Tool, ToolResult\n\n\nclass TestTaskMetaParameter:\n    \"\"\"Tests for task_meta parameter on FastMCP.call_tool().\"\"\"\n\n    async def test_task_meta_none_returns_tool_result(self):\n        \"\"\"With task_meta=None (default), call_tool returns ToolResult.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool\n        async def simple_tool(x: int) -> int:\n            return x * 2\n\n        result = await server.call_tool(\"simple_tool\", {\"x\": 5})\n\n        first_content = result.content[0]\n        assert isinstance(first_content, mcp.types.TextContent)\n        assert first_content.text == \"10\"\n\n    async def test_task_meta_none_on_task_enabled_tool_still_returns_tool_result(self):\n        \"\"\"Even for task=True tools, task_meta=None returns ToolResult synchronously.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def task_enabled_tool(x: int) -> int:\n            return x * 2\n\n        # Without task_meta, should execute synchronously\n        result = await server.call_tool(\"task_enabled_tool\", {\"x\": 5})\n\n        first_content = result.content[0]\n        assert isinstance(first_content, mcp.types.TextContent)\n        assert first_content.text == \"10\"\n\n    async def test_task_meta_on_forbidden_tool_raises_error(self):\n        \"\"\"Providing task_meta to a task=False tool raises ToolError.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=False)\n        async def sync_only_tool(x: int) -> int:\n            return x * 2\n\n        # Error is raised before docket is needed (McpError wrapped as ToolError)\n        with pytest.raises(ToolError) as exc_info:\n            await server.call_tool(\"sync_only_tool\", {\"x\": 5}, task_meta=TaskMeta())\n\n        assert \"does not support task-augmented execution\" in str(exc_info.value)\n\n    async def test_task_meta_fn_key_auto_populated_in_call_tool(self):\n        \"\"\"fn_key is auto-populated from tool name in call_tool().\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def auto_key_tool() -> str:\n            return \"done\"\n\n        # Verify fn_key starts as None\n        task_meta = TaskMeta()\n        assert task_meta.fn_key is None\n\n        # call_tool enriches the task_meta before passing to _run\n        # We test this via the client integration path\n        async with Client(server) as client:\n            result = await client.call_tool(\"auto_key_tool\", {}, task=True)\n            # Should succeed because fn_key was auto-populated\n            from fastmcp.client.tasks import ToolTask\n\n            assert isinstance(result, ToolTask)\n\n    async def test_task_meta_fn_key_enrichment_logic(self):\n        \"\"\"Verify that fn_key enrichment uses Tool.make_key().\"\"\"\n        # Direct test of the enrichment logic\n        tool_name = \"my_tool\"\n        expected_key = Tool.make_key(tool_name)\n\n        assert expected_key == \"tool:my_tool\"\n\n\nclass TestTaskMetaTTL:\n    \"\"\"Tests for task_meta.ttl behavior.\"\"\"\n\n    async def test_task_with_custom_ttl_creates_task(self):\n        \"\"\"task_meta.ttl is passed through when creating tasks.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def ttl_tool() -> str:\n            return \"done\"\n\n        custom_ttl_ms = 30000  # 30 seconds\n\n        async with Client(server) as client:\n            # Use client.call_tool with task=True and ttl\n            task = await client.call_tool(\"ttl_tool\", {}, task=True, ttl=custom_ttl_ms)\n\n            from fastmcp.client.tasks import ToolTask\n\n            assert isinstance(task, ToolTask)\n\n            # Verify task completes successfully\n            result = await task.result()\n            assert \"done\" in str(result)\n\n    async def test_task_without_ttl_uses_default(self):\n        \"\"\"task_meta.ttl=None uses docket.execution_ttl default.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def default_ttl_tool() -> str:\n            return \"done\"\n\n        async with Client(server) as client:\n            # Use client.call_tool with task=True, default ttl\n            task = await client.call_tool(\"default_ttl_tool\", {}, task=True)\n\n            from fastmcp.client.tasks import ToolTask\n\n            assert isinstance(task, ToolTask)\n\n            # Verify task completes successfully\n            result = await task.result()\n            assert \"done\" in str(result)\n\n\nclass TrackingMiddleware(Middleware):\n    \"\"\"Middleware that tracks tool calls.\"\"\"\n\n    def __init__(self, calls: list[str]):\n        super().__init__()\n        self._calls = calls\n\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mcp.types.CallToolRequestParams],\n        call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        if context.method:\n            self._calls.append(context.method)\n        return await call_next(context)\n\n\nclass TestTaskMetaMiddleware:\n    \"\"\"Tests that task_meta is properly propagated through middleware.\"\"\"\n\n    async def test_task_meta_propagated_through_middleware(self):\n        \"\"\"task_meta is passed through middleware chain.\"\"\"\n        server = FastMCP(\"test\")\n        middleware_saw_request: list[str] = []\n\n        @server.tool(task=True)\n        async def middleware_test_tool() -> str:\n            return \"done\"\n\n        server.add_middleware(TrackingMiddleware(middleware_saw_request))\n\n        async with Client(server) as client:\n            # Use client to trigger the middleware chain\n            task = await client.call_tool(\"middleware_test_tool\", {}, task=True)\n\n            # Middleware should have run\n            assert \"tools/call\" in middleware_saw_request\n\n            # And task should have been created\n            from fastmcp.client.tasks import ToolTask\n\n            assert isinstance(task, ToolTask)\n\n\nclass TestTaskMetaClientIntegration:\n    \"\"\"Tests that task_meta works correctly with the Client.\"\"\"\n\n    async def test_client_task_true_maps_to_task_meta(self):\n        \"\"\"Client's task=True creates proper task_meta on server.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def client_test_tool(x: int) -> int:\n            return x * 2\n\n        async with Client(server) as client:\n            # Client passes task=True, server receives as task_meta\n            task = await client.call_tool(\"client_test_tool\", {\"x\": 5}, task=True)\n\n            # Should get back a ToolTask (client wrapper)\n            from fastmcp.client.tasks import ToolTask\n\n            assert isinstance(task, ToolTask)\n\n            # Wait for result\n            result = await task.result()\n            assert \"10\" in str(result)\n\n    async def test_client_without_task_gets_immediate_result(self):\n        \"\"\"Client without task=True gets immediate result.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def immediate_tool(x: int) -> int:\n            return x * 2\n\n        async with Client(server) as client:\n            # No task=True, should execute synchronously\n            result = await client.call_tool(\"immediate_tool\", {\"x\": 5})\n\n            # Should get CallToolResult directly\n            assert \"10\" in str(result)\n\n    async def test_client_task_with_custom_ttl(self):\n        \"\"\"Client can pass custom TTL for task execution.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def custom_ttl_tool() -> str:\n            return \"done\"\n\n        custom_ttl_ms = 60000  # 60 seconds\n\n        async with Client(server) as client:\n            task = await client.call_tool(\n                \"custom_ttl_tool\", {}, task=True, ttl=custom_ttl_ms\n            )\n\n            from fastmcp.client.tasks import ToolTask\n\n            assert isinstance(task, ToolTask)\n\n            # Verify task completes successfully\n            result = await task.result()\n            assert \"done\" in str(result)\n\n\nclass TestTaskMetaDirectServerCall:\n    \"\"\"Tests for direct server calls (tool calling another tool).\"\"\"\n\n    async def test_tool_can_call_another_tool_with_task(self):\n        \"\"\"A tool can call another tool as a background task.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def inner_tool(x: int) -> int:\n            return x * 2\n\n        @server.tool\n        async def outer_tool(x: int) -> str:\n            # Call inner tool as background task\n            result = await server.call_tool(\n                \"inner_tool\", {\"x\": x}, task_meta=TaskMeta()\n            )\n            # Should get CreateTaskResult since we're in server context\n            return f\"Created task: {result.task.taskId}\"\n\n        async with Client(server) as client:\n            # Call outer_tool which internally calls inner_tool with task_meta\n            result = await client.call_tool(\"outer_tool\", {\"x\": 5})\n            # The outer tool should have successfully created a background task\n            assert \"Created task:\" in str(result)\n\n    async def test_tool_can_call_another_tool_synchronously(self):\n        \"\"\"A tool can call another tool synchronously (no task_meta).\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def inner_tool(x: int) -> int:\n            return x * 2\n\n        @server.tool\n        async def outer_tool(x: int) -> str:\n            # Call inner tool synchronously (no task_meta)\n            result = await server.call_tool(\"inner_tool\", {\"x\": x})\n            # Should get ToolResult directly\n            first_content = result.content[0]\n            assert isinstance(first_content, mcp.types.TextContent)\n            return f\"Got result: {first_content.text}\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"outer_tool\", {\"x\": 5})\n            assert \"Got result: 10\" in str(result)\n\n    async def test_tool_can_call_another_tool_with_custom_ttl(self):\n        \"\"\"A tool can call another tool as a background task with custom TTL.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(task=True)\n        async def inner_tool(x: int) -> int:\n            return x * 2\n\n        @server.tool\n        async def outer_tool(x: int) -> str:\n            custom_ttl = 45000  # 45 seconds\n            result = await server.call_tool(\n                \"inner_tool\", {\"x\": x}, task_meta=TaskMeta(ttl=custom_ttl)\n            )\n            return f\"Task TTL: {result.task.ttl}\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"outer_tool\", {\"x\": 5})\n            # The inner tool task should have the custom TTL\n            assert \"Task TTL: 45000\" in str(result)\n"
  },
  {
    "path": "tests/server/tasks/test_task_metadata.py",
    "content": "\"\"\"\nTests for SEP-1686 related-task metadata in protocol responses.\n\nPer the spec, all task-related responses MUST include\nio.modelcontextprotocol/related-task in _meta.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def metadata_server():\n    \"\"\"Create a server for testing metadata.\"\"\"\n    mcp = FastMCP(\"metadata-test\")\n\n    @mcp.tool(task=True)\n    async def test_tool(value: int) -> int:\n        return value * 2\n\n    return mcp\n\n\nasync def test_tasks_get_includes_related_task_metadata(metadata_server: FastMCP):\n    \"\"\"tasks/get response includes io.modelcontextprotocol/related-task in _meta.\"\"\"\n    async with Client(metadata_server) as client:\n        # Submit a task\n        task = await client.call_tool(\"test_tool\", {\"value\": 5}, task=True)\n        task_id = task.task_id\n\n        # Get status via client (which uses protocol properly)\n        status = await client.get_task_status(task_id)\n\n        # GetTaskResult is returned from response with metadata\n        # Verify the protocol included related-task metadata by checking the response worked\n        assert status.taskId == task_id\n        assert status.status in [\"working\", \"completed\"]\n\n\nasync def test_tasks_result_includes_related_task_metadata(metadata_server: FastMCP):\n    \"\"\"tasks/result response includes io.modelcontextprotocol/related-task in _meta.\"\"\"\n    async with Client(metadata_server) as client:\n        # Submit and complete a task\n        task = await client.call_tool(\"test_tool\", {\"value\": 7}, task=True)\n        result = await task.result()\n\n        # Result should have metadata (added by task.result() or protocol)\n        # Just verify the result is valid and contains the expected value\n        assert result.content\n        assert result.data == 14  # 7 * 2\n\n\nasync def test_tasks_list_includes_related_task_metadata(metadata_server: FastMCP):\n    \"\"\"tasks/list response includes io.modelcontextprotocol/related-task in _meta.\"\"\"\n    async with Client(metadata_server) as client:\n        # List tasks via client (which uses protocol properly)\n        result = await client.list_tasks()\n\n        # Verify list_tasks works and returns proper structure\n        assert \"tasks\" in result\n        assert isinstance(result[\"tasks\"], list)\n"
  },
  {
    "path": "tests/server/tasks/test_task_methods.py",
    "content": "\"\"\"\nTests for task protocol methods.\n\nTests the tasks/get, tasks/result, and tasks/list JSON-RPC protocol methods.\n\"\"\"\n\nimport asyncio\n\nimport pytest\nfrom mcp.shared.exceptions import McpError\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def endpoint_server():\n    \"\"\"Create a server with background tasks and HTTP transport.\"\"\"\n    mcp = FastMCP(\"endpoint-test-server\")\n\n    @mcp.tool(task=True)  # Enable background execution\n    async def quick_tool(value: int) -> int:\n        \"\"\"Returns the value immediately.\"\"\"\n        return value * 2\n\n    @mcp.tool(task=True)  # Enable background execution\n    async def error_tool() -> str:\n        \"\"\"Always raises an error.\"\"\"\n        raise RuntimeError(\"Task failed!\")\n\n    @mcp.tool(task=True)  # Enable background execution\n    async def slow_tool() -> str:\n        \"\"\"A slow tool for testing cancellation.\"\"\"\n        await asyncio.sleep(10)\n        return \"done\"\n\n    return mcp\n\n\nasync def test_tasks_get_endpoint_returns_status(endpoint_server):\n    \"\"\"POST /tasks/get returns task status.\"\"\"\n    async with Client(endpoint_server) as client:\n        # Submit a task\n        task = await client.call_tool(\"quick_tool\", {\"value\": 21}, task=True)\n\n        # Check status immediately - should be submitted or working\n        status = await task.status()\n        assert status.taskId == task.task_id\n        assert status.status in [\"working\", \"completed\"]\n\n        # Wait for completion\n        await task.wait(timeout=2.0)\n\n        # Check again - should be completed\n        status = await task.status()\n        assert status.status == \"completed\"\n\n\nasync def test_tasks_get_endpoint_includes_poll_interval(endpoint_server):\n    \"\"\"Task status includes pollFrequency hint.\"\"\"\n    async with Client(endpoint_server) as client:\n        task = await client.call_tool(\"quick_tool\", {\"value\": 42}, task=True)\n\n        status = await task.status()\n        assert status.pollInterval is not None\n        assert isinstance(status.pollInterval, int)\n\n\nasync def test_tasks_result_endpoint_returns_result_when_completed(endpoint_server):\n    \"\"\"POST /tasks/result returns the tool result when completed.\"\"\"\n    async with Client(endpoint_server) as client:\n        task = await client.call_tool(\"quick_tool\", {\"value\": 21}, task=True)\n\n        # Wait for completion and get result\n        result = await task.result()\n        assert result.data == 42  # 21 * 2\n\n\nasync def test_tasks_result_endpoint_errors_if_not_completed(endpoint_server):\n    \"\"\"POST /tasks/result returns error if task not completed yet.\"\"\"\n    # Create a task that won't complete until signaled\n    completion_signal = asyncio.Event()\n\n    @endpoint_server.tool(task=True)  # Enable background execution\n    async def blocked_tool() -> str:\n        await completion_signal.wait()\n        return \"done\"\n\n    async with Client(endpoint_server) as client:\n        task = await client.call_tool(\"blocked_tool\", task=True)\n\n        # Try to get result immediately (task still running)\n        with pytest.raises(Exception):  # Should raise or return error\n            await client.get_task_result(task.task_id)\n\n        # Cleanup - signal completion\n        completion_signal.set()\n\n\nasync def test_tasks_result_endpoint_errors_if_task_not_found(endpoint_server):\n    \"\"\"POST /tasks/result returns error for non-existent task.\"\"\"\n    async with Client(endpoint_server) as client:\n        # Try to get result for non-existent task\n        with pytest.raises(Exception):\n            await client.get_task_result(\"non-existent-task-id\")\n\n\nasync def test_tasks_result_endpoint_returns_error_for_failed_task(endpoint_server):\n    \"\"\"POST /tasks/result returns error information for failed tasks.\"\"\"\n    async with Client(endpoint_server) as client:\n        task = await client.call_tool(\"error_tool\", task=True)\n\n        # Wait for task to fail\n        await task.wait(state=\"failed\", timeout=2.0)\n\n        # Getting result should raise or return error info\n        with pytest.raises(Exception) as exc_info:\n            await task.result()\n\n        assert (\n            \"failed\" in str(exc_info.value).lower()\n            or \"error\" in str(exc_info.value).lower()\n        )\n\n\nasync def test_tasks_list_endpoint_session_isolation(endpoint_server):\n    \"\"\"list_tasks returns only tasks submitted by this client.\"\"\"\n    # Since client tracks tasks locally, this tests client-side tracking\n    async with Client(endpoint_server) as client:\n        # Submit multiple tasks (server generates IDs)\n        tasks = []\n        for i in range(3):\n            task = await client.call_tool(\"quick_tool\", {\"value\": i}, task=True)\n            tasks.append(task)\n\n        # Wait for all to complete\n        for task in tasks:\n            await task.wait(timeout=2.0)\n\n        # List tasks - should see all 3\n        response = await client.list_tasks()\n        returned_ids = [t[\"taskId\"] for t in response[\"tasks\"]]\n        task_ids = [t.task_id for t in tasks]\n        assert len(returned_ids) == 3\n        assert all(tid in task_ids for tid in returned_ids)\n\n\nasync def test_get_status_nonexistent_task_raises_error(endpoint_server):\n    \"\"\"Getting status for nonexistent task raises MCP error (per SEP-1686 SDK behavior).\"\"\"\n    async with Client(endpoint_server) as client:\n        # Try to get status for task that was never created\n        # Per SDK implementation: raises ValueError which becomes JSON-RPC error\n        with pytest.raises(McpError, match=\"Task nonexistent-task-id not found\"):\n            await client.get_task_status(\"nonexistent-task-id\")\n\n\nasync def test_task_cancellation_workflow(endpoint_server):\n    \"\"\"Task can be cancelled, transitioning to cancelled state.\"\"\"\n    async with Client(endpoint_server) as client:\n        # Submit slow task\n        task = await client.call_tool(\"slow_tool\", {}, task=True)\n\n        # Give it a moment to start\n        await asyncio.sleep(0.1)\n\n        # Cancel the task\n        await task.cancel()\n\n        # Give cancellation a moment to process\n        await asyncio.sleep(0.1)\n\n        # Task should be in cancelled state\n        status = await task.status()\n        assert status.status == \"cancelled\"\n\n\n@pytest.mark.timeout(10)\nasync def test_task_cancellation_interrupts_running_coroutine(endpoint_server):\n    \"\"\"Task cancellation actually interrupts the running coroutine.\n\n    This verifies that when a task is cancelled, the underlying asyncio\n    coroutine receives CancelledError rather than continuing to completion.\n    Requires pydocket >= 0.16.2.\n\n    See: https://github.com/PrefectHQ/fastmcp/issues/2679\n    \"\"\"\n    started = asyncio.Event()\n    was_interrupted = asyncio.Event()\n    completed_normally = asyncio.Event()\n\n    @endpoint_server.tool(task=True)\n    async def interruptible_tool() -> str:\n        started.set()\n        try:\n            await asyncio.sleep(60)\n            completed_normally.set()\n            return \"completed\"\n        except asyncio.CancelledError:\n            was_interrupted.set()\n            raise\n\n    async with Client(endpoint_server) as client:\n        task = await client.call_tool(\"interruptible_tool\", {}, task=True)\n\n        # Wait for the tool to actually start executing\n        await asyncio.wait_for(started.wait(), timeout=5.0)\n\n        # Cancel the task\n        await task.cancel()\n\n        # Wait for cancellation to propagate\n        await asyncio.wait_for(was_interrupted.wait(), timeout=5.0)\n\n        # The coroutine should have been interrupted, not completed normally\n        assert was_interrupted.is_set(), \"Task was not interrupted by cancellation\"\n        assert not completed_normally.is_set(), (\n            \"Task completed instead of being cancelled\"\n        )\n"
  },
  {
    "path": "tests/server/tasks/test_task_mount.py",
    "content": "\"\"\"\nTests for MCP SEP-1686 task protocol support through mounted servers.\n\nVerifies that tasks work seamlessly when calling tools/prompts/resources\non mounted child servers through a parent server.\n\"\"\"\n\nimport asyncio\n\nimport mcp.types as mt\nimport pytest\nfrom docket import Docket\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.prompts.base import PromptResult\nfrom fastmcp.resources.base import ResourceResult\nfrom fastmcp.server.dependencies import CurrentDocket, CurrentFastMCP\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.server.tasks import TaskConfig\nfrom fastmcp.tools.base import ToolResult\n\n\n@pytest.fixture(autouse=True)\ndef reset_docket_memory_server():\n    \"\"\"Reset the shared Docket memory server between tests.\n\n    Docket uses a class-level FakeServer instance for memory:// URLs which\n    persists between tests, causing test isolation issues. This fixture\n    clears that shared state before each test.\n    \"\"\"\n    # Clear the shared FakeServer before each test\n    if hasattr(Docket, \"_memory_server\"):\n        delattr(Docket, \"_memory_server\")\n    yield\n    # Clean up after test as well\n    if hasattr(Docket, \"_memory_server\"):\n        delattr(Docket, \"_memory_server\")\n\n\n@pytest.fixture\ndef child_server():\n    \"\"\"Create a child server with task-enabled components.\"\"\"\n    mcp = FastMCP(\"child-server\")\n\n    @mcp.tool(task=True)\n    async def multiply(a: int, b: int) -> int:\n        \"\"\"Multiply two numbers.\"\"\"\n        return a * b\n\n    @mcp.tool(task=True)\n    async def slow_child_tool(duration: float = 0.1) -> str:\n        \"\"\"A child tool that takes time to execute.\"\"\"\n        await asyncio.sleep(duration)\n        return \"child completed\"\n\n    @mcp.tool(task=False)\n    async def sync_child_tool(message: str) -> str:\n        \"\"\"Child tool that only supports synchronous execution.\"\"\"\n        return f\"child sync: {message}\"\n\n    @mcp.prompt(task=True)\n    async def child_prompt(topic: str) -> str:\n        \"\"\"A child prompt that can execute as a task.\"\"\"\n        return f\"Here is information about {topic} from the child server.\"\n\n    @mcp.resource(\"child://data.txt\", task=True)\n    async def child_resource() -> str:\n        \"\"\"A child resource that can be read as a task.\"\"\"\n        return \"Data from child server\"\n\n    @mcp.resource(\"child://item/{item_id}.json\", task=True)\n    async def child_item_resource(item_id: str) -> str:\n        \"\"\"A child resource template that can execute as a task.\"\"\"\n        return f'{{\"itemId\": \"{item_id}\", \"source\": \"child\"}}'\n\n    return mcp\n\n\n@pytest.fixture\ndef parent_server(child_server):\n    \"\"\"Create a parent server with the child mounted.\"\"\"\n    parent = FastMCP(\"parent-server\")\n\n    @parent.tool(task=True)\n    async def parent_tool(value: int) -> int:\n        \"\"\"A tool on the parent server.\"\"\"\n        return value * 10\n\n    # Mount child with prefix\n    parent.mount(child_server, namespace=\"child\")\n\n    return parent\n\n\n@pytest.fixture\ndef parent_server_no_prefix(child_server):\n    \"\"\"Create a parent server with child mounted without prefix.\"\"\"\n    parent = FastMCP(\"parent-no-prefix\")\n    parent.mount(child_server)  # No prefix\n    return parent\n\n\nclass TestMountedToolTasks:\n    \"\"\"Test task execution for mounted tools.\"\"\"\n\n    async def test_mounted_tool_task_returns_task_object(self, parent_server):\n        \"\"\"Mounted tool called with task=True returns a task object.\"\"\"\n        async with Client(parent_server) as client:\n            # Tool name is prefixed: child_multiply\n            task = await client.call_tool(\"child_multiply\", {\"a\": 6, \"b\": 7}, task=True)\n\n            assert task is not None\n            assert hasattr(task, \"task_id\")\n            assert isinstance(task.task_id, str)\n            assert len(task.task_id) > 0\n\n    async def test_mounted_tool_task_executes_in_background(self, parent_server):\n        \"\"\"Mounted tool task executes in background.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.call_tool(\"child_multiply\", {\"a\": 3, \"b\": 4}, task=True)\n\n            # Should execute in background\n            assert not task.returned_immediately\n\n    async def test_mounted_tool_task_returns_correct_result(\n        self, parent_server: FastMCP\n    ):\n        \"\"\"Mounted tool task returns correct result.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.call_tool(\"child_multiply\", {\"a\": 8, \"b\": 9}, task=True)\n\n            result = await task.result()\n            assert result.data == 72\n\n    async def test_mounted_tool_task_status(self, parent_server):\n        \"\"\"Can poll task status for mounted tool.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.call_tool(\n                \"child_slow_child_tool\", {\"duration\": 0.5}, task=True\n            )\n\n            # Check status while running\n            status = await task.status()\n            assert status.status in [\"working\", \"completed\"]\n\n            # Wait for completion\n            await task.wait(timeout=2.0)\n\n            # Check status after completion\n            status = await task.status()\n            assert status.status == \"completed\"\n\n    @pytest.mark.timeout(10)\n    async def test_mounted_tool_task_cancellation(self, parent_server):\n        \"\"\"Can cancel a mounted tool task.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.call_tool(\n                \"child_slow_child_tool\", {\"duration\": 10.0}, task=True\n            )\n\n            # Let it start\n            await asyncio.sleep(0.1)\n\n            # Cancel the task\n            await task.cancel()\n\n            # Check status\n            status = await task.status()\n            assert status.status == \"cancelled\"\n\n    async def test_graceful_degradation_sync_mounted_tool(self, parent_server):\n        \"\"\"Sync-only mounted tool returns error with task=True.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.call_tool(\n                \"child_sync_child_tool\", {\"message\": \"hello\"}, task=True\n            )\n\n            # Should return immediately with an error\n            assert task.returned_immediately\n\n            result = await task.result()\n            assert result.is_error\n\n    async def test_parent_and_mounted_tools_both_work(self, parent_server):\n        \"\"\"Both parent and mounted tools work as tasks.\"\"\"\n        async with Client(parent_server) as client:\n            # Parent tool\n            parent_task = await client.call_tool(\"parent_tool\", {\"value\": 5}, task=True)\n            # Mounted tool\n            child_task = await client.call_tool(\n                \"child_multiply\", {\"a\": 2, \"b\": 3}, task=True\n            )\n\n            parent_result = await parent_task.result()\n            child_result = await child_task.result()\n\n            assert parent_result.data == 50\n            assert child_result.data == 6\n\n\nclass TestMountedToolTasksNoPrefix:\n    \"\"\"Test task execution for mounted tools without prefix.\"\"\"\n\n    async def test_mounted_tool_without_prefix_task_works(\n        self, parent_server_no_prefix\n    ):\n        \"\"\"Mounted tool without prefix works as task.\"\"\"\n        async with Client(parent_server_no_prefix) as client:\n            # No prefix, so tool keeps original name\n            task = await client.call_tool(\"multiply\", {\"a\": 5, \"b\": 6}, task=True)\n\n            assert not task.returned_immediately\n\n            result = await task.result()\n            assert result.data == 30\n\n\nclass TestMountedPromptTasks:\n    \"\"\"Test task execution for mounted prompts.\"\"\"\n\n    async def test_mounted_prompt_task_returns_task_object(self, parent_server):\n        \"\"\"Mounted prompt called with task=True returns a task object.\"\"\"\n        async with Client(parent_server) as client:\n            # Prompt name is prefixed: child_child_prompt\n            task = await client.get_prompt(\n                \"child_child_prompt\", {\"topic\": \"FastMCP\"}, task=True\n            )\n\n            assert task is not None\n            assert hasattr(task, \"task_id\")\n            assert isinstance(task.task_id, str)\n\n    async def test_mounted_prompt_task_executes_in_background(self, parent_server):\n        \"\"\"Mounted prompt task executes in background.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.get_prompt(\n                \"child_child_prompt\", {\"topic\": \"testing\"}, task=True\n            )\n\n            assert not task.returned_immediately\n\n    async def test_mounted_prompt_task_returns_correct_result(\n        self, parent_server: FastMCP\n    ):\n        \"\"\"Mounted prompt task returns correct result.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.get_prompt(\n                \"child_child_prompt\", {\"topic\": \"MCP protocol\"}, task=True\n            )\n\n            result = await task.result()\n            assert \"MCP protocol\" in result.messages[0].content.text\n            assert \"child server\" in result.messages[0].content.text\n\n\nclass TestMountedResourceTasks:\n    \"\"\"Test task execution for mounted resources.\"\"\"\n\n    async def test_mounted_resource_task_returns_task_object(self, parent_server):\n        \"\"\"Mounted resource read with task=True returns a task object.\"\"\"\n        async with Client(parent_server) as client:\n            # Resource URI is prefixed: child://child/data.txt\n            task = await client.read_resource(\"child://child/data.txt\", task=True)\n\n            assert task is not None\n            assert hasattr(task, \"task_id\")\n            assert isinstance(task.task_id, str)\n\n    async def test_mounted_resource_task_executes_in_background(self, parent_server):\n        \"\"\"Mounted resource task executes in background.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.read_resource(\"child://child/data.txt\", task=True)\n\n            assert not task.returned_immediately\n\n    async def test_mounted_resource_task_returns_correct_result(self, parent_server):\n        \"\"\"Mounted resource task returns correct result.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.read_resource(\"child://child/data.txt\", task=True)\n\n            result = await task.result()\n            assert len(result) > 0\n            assert \"Data from child server\" in result[0].text\n\n    async def test_mounted_resource_template_task(self, parent_server):\n        \"\"\"Mounted resource template with task=True works.\"\"\"\n        async with Client(parent_server) as client:\n            task = await client.read_resource(\"child://child/item/99.json\", task=True)\n\n            assert not task.returned_immediately\n\n            result = await task.result()\n            assert '\"itemId\": \"99\"' in result[0].text\n            assert '\"source\": \"child\"' in result[0].text\n\n\nclass TestMountedTaskDependencies:\n    \"\"\"Test that dependencies work correctly in mounted task execution.\"\"\"\n\n    async def test_mounted_task_receives_docket_dependency(self):\n        \"\"\"Mounted tool task receives CurrentDocket dependency.\"\"\"\n        child = FastMCP(\"dep-child\")\n        received_docket = []\n\n        @child.tool(task=True)\n        async def tool_with_docket(docket: CurrentDocket = CurrentDocket()) -> str:  # type: ignore[invalid-type-form]\n            received_docket.append(docket)\n            return f\"docket available: {docket is not None}\"\n\n        parent = FastMCP(\"dep-parent\")\n        parent.mount(child, namespace=\"child\")\n\n        async with Client(parent) as client:\n            task = await client.call_tool(\"child_tool_with_docket\", {}, task=True)\n            result = await task.result()\n\n            assert \"docket available: True\" in str(result)\n            assert len(received_docket) == 1\n            assert received_docket[0] is not None\n\n    async def test_mounted_task_receives_server_dependency(self):\n        \"\"\"Mounted tool task receives CurrentFastMCP dependency.\"\"\"\n        child = FastMCP(\"server-dep-child\")\n        received_server = []\n\n        @child.tool(task=True)\n        async def tool_with_server(server: CurrentFastMCP = CurrentFastMCP()) -> str:  # type: ignore[invalid-type-form]\n            received_server.append(server)\n            return f\"server name: {server.name}\"\n\n        parent = FastMCP(\"server-dep-parent\")\n        parent.mount(child, namespace=\"child\")\n\n        async with Client(parent) as client:\n            task = await client.call_tool(\"child_tool_with_server\", {}, task=True)\n            await task.result()\n\n            # The server should be the child server since that's where the tool is defined\n            assert len(received_server) == 1\n            # Note: It might be parent or child depending on implementation\n            assert received_server[0] is not None\n\n\nclass TestMultipleMounts:\n    \"\"\"Test tasks with multiple mounted servers.\"\"\"\n\n    async def test_tasks_work_with_multiple_mounts(self):\n        \"\"\"Tasks work correctly with multiple mounted servers.\"\"\"\n        child1 = FastMCP(\"child1\")\n        child2 = FastMCP(\"child2\")\n\n        @child1.tool(task=True)\n        async def add(a: int, b: int) -> int:\n            return a + b\n\n        @child2.tool(task=True)\n        async def subtract(a: int, b: int) -> int:\n            return a - b\n\n        parent = FastMCP(\"multi-parent\")\n        parent.mount(child1, namespace=\"math1\")\n        parent.mount(child2, namespace=\"math2\")\n\n        async with Client(parent) as client:\n            task1 = await client.call_tool(\"math1_add\", {\"a\": 10, \"b\": 5}, task=True)\n            task2 = await client.call_tool(\n                \"math2_subtract\", {\"a\": 10, \"b\": 5}, task=True\n            )\n\n            result1 = await task1.result()\n            result2 = await task2.result()\n\n            assert result1.data == 15\n            assert result2.data == 5\n\n\nclass TestMountedFunctionNameCollisions:\n    \"\"\"Test task execution when mounted servers have identically-named functions.\"\"\"\n\n    async def test_multiple_mounts_with_same_function_names(self):\n        \"\"\"Two mounted servers with identically-named functions don't collide.\"\"\"\n        child1 = FastMCP(\"child1\")\n        child2 = FastMCP(\"child2\")\n\n        @child1.tool(task=True)\n        async def process(value: int) -> int:\n            return value * 2  # Double\n\n        @child2.tool(task=True)\n        async def process(value: int) -> int:  # noqa: F811\n            return value * 3  # Triple\n\n        parent = FastMCP(\"parent\")\n        parent.mount(child1, namespace=\"c1\")\n        parent.mount(child2, namespace=\"c2\")\n\n        async with Client(parent) as client:\n            # Both should execute their own implementation\n            task1 = await client.call_tool(\"c1_process\", {\"value\": 10}, task=True)\n            task2 = await client.call_tool(\"c2_process\", {\"value\": 10}, task=True)\n\n            result1 = await task1.result()\n            result2 = await task2.result()\n\n            assert result1.data == 20  # child1's process (doubles)\n            assert result2.data == 30  # child2's process (triples)\n\n    async def test_no_prefix_mount_collision(self):\n        \"\"\"No-prefix mounts with same tool name - last mount wins.\"\"\"\n        child1 = FastMCP(\"child1\")\n        child2 = FastMCP(\"child2\")\n\n        @child1.tool(task=True)\n        async def process(value: int) -> int:\n            return value * 2\n\n        @child2.tool(task=True)\n        async def process(value: int) -> int:  # noqa: F811\n            return value * 3\n\n        parent = FastMCP(\"parent\")\n        parent.mount(child1)  # No prefix\n        parent.mount(child2)  # No prefix - overwrites child1's \"process\"\n\n        async with Client(parent) as client:\n            # Last mount wins - child2's process should execute\n            task = await client.call_tool(\"process\", {\"value\": 10}, task=True)\n            result = await task.result()\n            assert result.data == 30  # child2's process (triples)\n\n    async def test_nested_mount_prefix_accumulation(self):\n        \"\"\"Nested mounts accumulate prefixes correctly for tasks.\"\"\"\n        grandchild = FastMCP(\"gc\")\n        child = FastMCP(\"child\")\n        parent = FastMCP(\"parent\")\n\n        @grandchild.tool(task=True)\n        async def deep_tool() -> str:\n            return \"deep\"\n\n        child.mount(grandchild, namespace=\"gc\")\n        parent.mount(child, namespace=\"child\")\n\n        async with Client(parent) as client:\n            # Tool should be accessible and execute correctly\n            task = await client.call_tool(\"child_gc_deep_tool\", {}, task=True)\n            result = await task.result()\n            assert result.data == \"deep\"\n\n\nclass TestMountedTaskList:\n    \"\"\"Test task listing with mounted servers.\"\"\"\n\n    async def test_list_tasks_includes_mounted_tasks(self, parent_server):\n        \"\"\"Task list includes tasks from mounted server tools.\"\"\"\n        async with Client(parent_server) as client:\n            # Create tasks on both parent and mounted tools\n            parent_task = await client.call_tool(\"parent_tool\", {\"value\": 1}, task=True)\n            child_task = await client.call_tool(\n                \"child_multiply\", {\"a\": 2, \"b\": 2}, task=True\n            )\n\n            # Wait for completion\n            await parent_task.wait(timeout=2.0)\n            await child_task.wait(timeout=2.0)\n\n            # List all tasks - returns dict with \"tasks\" key\n            tasks_response = await client.list_tasks()\n\n            task_ids = [t[\"taskId\"] for t in tasks_response[\"tasks\"]]\n            assert parent_task.task_id in task_ids\n            assert child_task.task_id in task_ids\n\n\nclass TestMountedTaskConfigModes:\n    \"\"\"Test TaskConfig mode enforcement for mounted tools.\"\"\"\n\n    @pytest.fixture\n    def child_with_modes(self):\n        \"\"\"Create a child server with tools in all three TaskConfig modes.\"\"\"\n        mcp = FastMCP(\"child-modes\", tasks=False)\n\n        @mcp.tool(task=TaskConfig(mode=\"optional\"))\n        async def optional_tool() -> str:\n            \"\"\"Tool that supports both sync and task execution.\"\"\"\n            return \"optional result\"\n\n        @mcp.tool(task=TaskConfig(mode=\"required\"))\n        async def required_tool() -> str:\n            \"\"\"Tool that requires task execution.\"\"\"\n            return \"required result\"\n\n        @mcp.tool(task=TaskConfig(mode=\"forbidden\"))\n        async def forbidden_tool() -> str:\n            \"\"\"Tool that forbids task execution.\"\"\"\n            return \"forbidden result\"\n\n        return mcp\n\n    @pytest.fixture\n    def parent_with_modes(self, child_with_modes):\n        \"\"\"Create a parent server with the child mounted.\"\"\"\n        parent = FastMCP(\"parent-modes\")\n        parent.mount(child_with_modes, namespace=\"child\")\n        return parent\n\n    async def test_optional_mode_sync_through_mount(self, parent_with_modes):\n        \"\"\"Optional mode tool works without task through mount.\"\"\"\n        async with Client(parent_with_modes) as client:\n            result = await client.call_tool(\"child_optional_tool\", {})\n            assert \"optional result\" in str(result)\n\n    async def test_optional_mode_task_through_mount(self, parent_with_modes):\n        \"\"\"Optional mode tool works with task through mount.\"\"\"\n        async with Client(parent_with_modes) as client:\n            task = await client.call_tool(\"child_optional_tool\", {}, task=True)\n            assert task is not None\n            result = await task.result()\n            assert result.data == \"optional result\"\n\n    async def test_required_mode_with_task_through_mount(self, parent_with_modes):\n        \"\"\"Required mode tool succeeds with task through mount.\"\"\"\n        async with Client(parent_with_modes) as client:\n            task = await client.call_tool(\"child_required_tool\", {}, task=True)\n            assert task is not None\n            result = await task.result()\n            assert result.data == \"required result\"\n\n    async def test_required_mode_without_task_through_mount(self, parent_with_modes):\n        \"\"\"Required mode tool errors without task through mount.\"\"\"\n        from fastmcp.exceptions import ToolError\n\n        async with Client(parent_with_modes) as client:\n            with pytest.raises(ToolError) as exc_info:\n                await client.call_tool(\"child_required_tool\", {})\n\n            assert \"requires task-augmented execution\" in str(exc_info.value)\n\n    async def test_forbidden_mode_sync_through_mount(self, parent_with_modes):\n        \"\"\"Forbidden mode tool works without task through mount.\"\"\"\n        async with Client(parent_with_modes) as client:\n            result = await client.call_tool(\"child_forbidden_tool\", {})\n            assert \"forbidden result\" in str(result)\n\n    async def test_forbidden_mode_with_task_through_mount(self, parent_with_modes):\n        \"\"\"Forbidden mode tool degrades gracefully with task through mount.\"\"\"\n        async with Client(parent_with_modes) as client:\n            task = await client.call_tool(\"child_forbidden_tool\", {}, task=True)\n\n            # Should return immediately (graceful degradation)\n            assert task.returned_immediately\n\n            result = await task.result()\n            # Result is available but may indicate error or sync execution\n            assert result is not None\n\n\n# -----------------------------------------------------------------------------\n# Middleware classes for tracing tests\n# -----------------------------------------------------------------------------\n\n\nclass ToolTracingMiddleware(Middleware):\n    \"\"\"Middleware that traces tool calls.\"\"\"\n\n    def __init__(self, name: str, calls: list[str]):\n        super().__init__()\n        self._name = name\n        self._calls = calls\n\n    async def on_call_tool(\n        self,\n        context: MiddlewareContext[mt.CallToolRequestParams],\n        call_next: CallNext[mt.CallToolRequestParams, ToolResult],\n    ) -> ToolResult:\n        self._calls.append(f\"{self._name}:before\")\n        result = await call_next(context)\n        self._calls.append(f\"{self._name}:after\")\n        return result\n\n\nclass ResourceTracingMiddleware(Middleware):\n    \"\"\"Middleware that traces resource reads.\"\"\"\n\n    def __init__(self, name: str, calls: list[str]):\n        super().__init__()\n        self._name = name\n        self._calls = calls\n\n    async def on_read_resource(\n        self,\n        context: MiddlewareContext[mt.ReadResourceRequestParams],\n        call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult],\n    ) -> ResourceResult:\n        self._calls.append(f\"{self._name}:before\")\n        result = await call_next(context)\n        self._calls.append(f\"{self._name}:after\")\n        return result\n\n\nclass PromptTracingMiddleware(Middleware):\n    \"\"\"Middleware that traces prompt gets.\"\"\"\n\n    def __init__(self, name: str, calls: list[str]):\n        super().__init__()\n        self._name = name\n        self._calls = calls\n\n    async def on_get_prompt(\n        self,\n        context: MiddlewareContext[mt.GetPromptRequestParams],\n        call_next: CallNext[mt.GetPromptRequestParams, PromptResult],\n    ) -> PromptResult:\n        self._calls.append(f\"{self._name}:before\")\n        result = await call_next(context)\n        self._calls.append(f\"{self._name}:after\")\n        return result\n\n\nclass TestMiddlewareWithMountedTasks:\n    \"\"\"Test that middleware runs at all levels when executing background tasks.\n\n    For background tasks, middleware runs during task submission (wrapping the MCP\n    request handling that queues to Docket). The actual function execution happens\n    later in the Docket worker, after the middleware chain completes.\n    \"\"\"\n\n    async def test_tool_middleware_runs_with_background_task(self):\n        \"\"\"Middleware runs at parent, child, and grandchild levels for tool tasks.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.tool(task=True)\n        async def compute(x: int) -> int:\n            calls.append(\"grandchild:tool\")\n            return x * 2\n\n        grandchild.add_middleware(ToolTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(ToolTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(ToolTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            task = await client.call_tool(\"c_gc_compute\", {\"x\": 5}, task=True)\n            result = await task.result()\n            assert result.data == 10\n\n        # Middleware runs during task submission (before/after queuing to Docket)\n        # Function executes later in Docket worker\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n            \"grandchild:tool\",  # Executes in Docket after middleware completes\n        ]\n\n    async def test_resource_middleware_runs_with_background_task(self):\n        \"\"\"Middleware runs at parent, child, and grandchild levels for resource tasks.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.resource(\"data://value\", task=True)\n        async def get_data() -> str:\n            calls.append(\"grandchild:resource\")\n            return \"result\"\n\n        grandchild.add_middleware(ResourceTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(ResourceTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(ResourceTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            task = await client.read_resource(\"data://c/gc/value\", task=True)\n            result = await task.result()\n            assert result[0].text == \"result\"\n\n        # Middleware runs during task submission, function in Docket\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n            \"grandchild:resource\",\n        ]\n\n    async def test_prompt_middleware_runs_with_background_task(self):\n        \"\"\"Middleware runs at parent, child, and grandchild levels for prompt tasks.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.prompt(task=True)\n        async def greet(name: str) -> str:\n            calls.append(\"grandchild:prompt\")\n            return f\"Hello, {name}!\"\n\n        grandchild.add_middleware(PromptTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(PromptTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(PromptTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            task = await client.get_prompt(\"c_gc_greet\", {\"name\": \"World\"}, task=True)\n            result = await task.result()\n            assert result.messages[0].content.text == \"Hello, World!\"\n\n        # Middleware runs during task submission, function in Docket\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n            \"grandchild:prompt\",\n        ]\n\n    async def test_resource_template_middleware_runs_with_background_task(self):\n        \"\"\"Middleware runs at all levels for resource template tasks.\"\"\"\n        calls: list[str] = []\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.resource(\"item://{id}\", task=True)\n        async def get_item(id: str) -> str:\n            calls.append(\"grandchild:template\")\n            return f\"item-{id}\"\n\n        grandchild.add_middleware(ResourceTracingMiddleware(\"grandchild\", calls))\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n        child.add_middleware(ResourceTracingMiddleware(\"child\", calls))\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n        parent.add_middleware(ResourceTracingMiddleware(\"parent\", calls))\n\n        async with Client(parent) as client:\n            task = await client.read_resource(\"item://c/gc/42\", task=True)\n            result = await task.result()\n            assert result[0].text == \"item-42\"\n\n        # Middleware runs during task submission, function in Docket\n        assert calls == [\n            \"parent:before\",\n            \"child:before\",\n            \"grandchild:before\",\n            \"grandchild:after\",\n            \"child:after\",\n            \"parent:after\",\n            \"grandchild:template\",\n        ]\n\n\nclass TestMountedTasksWithTaskMetaParameter:\n    \"\"\"Test mounted components called directly with task_meta parameter.\n\n    These tests verify the programmatic API where server.call_tool() or\n    server.read_resource() is called with an explicit task_meta parameter,\n    as opposed to using the Client with task=True.\n\n    Direct server calls require a running server context, so we use an outer\n    tool that makes the direct call internally.\n    \"\"\"\n\n    async def test_mounted_tool_with_task_meta_creates_task(self):\n        \"\"\"Mounted tool called with task_meta returns CreateTaskResult.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        child = FastMCP(\"Child\")\n\n        @child.tool(task=True)\n        async def add(a: int, b: int) -> int:\n            return a + b\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"child\")\n\n        @parent.tool\n        async def outer() -> str:\n            # Direct call with task_meta from within server context\n            result = await parent.call_tool(\n                \"child_add\", {\"a\": 2, \"b\": 3}, task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n\n    async def test_mounted_resource_with_task_meta_creates_task(self):\n        \"\"\"Mounted resource called with task_meta returns CreateTaskResult.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        child = FastMCP(\"Child\")\n\n        @child.resource(\"data://info\", task=True)\n        async def get_info() -> str:\n            return \"child info\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"child\")\n\n        @parent.tool\n        async def outer() -> str:\n            result = await parent.read_resource(\n                \"data://child/info\", task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n\n    async def test_mounted_template_with_task_meta_creates_task(self):\n        \"\"\"Mounted resource template with task_meta returns CreateTaskResult.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        child = FastMCP(\"Child\")\n\n        @child.resource(\"item://{id}\", task=True)\n        async def get_item(id: str) -> str:\n            return f\"item-{id}\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"child\")\n\n        @parent.tool\n        async def outer() -> str:\n            result = await parent.read_resource(\n                \"item://child/42\", task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n\n    async def test_deeply_nested_tool_with_task_meta(self):\n        \"\"\"Three-level nested tool works with task_meta.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.tool(task=True)\n        async def compute(n: int) -> int:\n            return n * 3\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n\n        @parent.tool\n        async def outer() -> str:\n            result = await parent.call_tool(\n                \"c_gc_compute\", {\"n\": 7}, task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n\n    async def test_deeply_nested_template_with_task_meta(self):\n        \"\"\"Three-level nested template works with task_meta.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.resource(\"doc://{name}\", task=True)\n        async def get_doc(name: str) -> str:\n            return f\"doc: {name}\"\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n\n        @parent.tool\n        async def outer() -> str:\n            result = await parent.read_resource(\n                \"doc://c/gc/readme\", task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n\n    async def test_mounted_prompt_with_task_meta_creates_task(self):\n        \"\"\"Mounted prompt called with task_meta returns CreateTaskResult.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        child = FastMCP(\"Child\")\n\n        @child.prompt(task=True)\n        async def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"child\")\n\n        @parent.tool\n        async def outer() -> str:\n            result = await parent.render_prompt(\n                \"child_greet\", {\"name\": \"World\"}, task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n\n    async def test_deeply_nested_prompt_with_task_meta(self):\n        \"\"\"Three-level nested prompt works with task_meta.\"\"\"\n        from fastmcp.server.tasks.config import TaskMeta\n\n        grandchild = FastMCP(\"Grandchild\")\n\n        @grandchild.prompt(task=True)\n        async def describe(topic: str) -> str:\n            return f\"Information about {topic}\"\n\n        child = FastMCP(\"Child\")\n        child.mount(grandchild, namespace=\"gc\")\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, namespace=\"c\")\n\n        @parent.tool\n        async def outer() -> str:\n            result = await parent.render_prompt(\n                \"c_gc_describe\", {\"topic\": \"FastMCP\"}, task_meta=TaskMeta(ttl=300)\n            )\n            return f\"task:{result.task.taskId}\"\n\n        async with Client(parent) as client:\n            result = await client.call_tool(\"outer\", {})\n            assert \"task:\" in str(result)\n"
  },
  {
    "path": "tests/server/tasks/test_task_prompts.py",
    "content": "\"\"\"\nTests for SEP-1686 background task support for prompts.\n\nTests that prompts with task=True can execute in background.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.tasks import PromptTask\n\n\n@pytest.fixture\nasync def prompt_server():\n    \"\"\"Create a FastMCP server with task-enabled prompts.\"\"\"\n    mcp = FastMCP(\"prompt-test-server\")\n\n    @mcp.prompt()\n    async def simple_prompt(topic: str) -> str:\n        \"\"\"A simple prompt template.\"\"\"\n        return f\"Write about: {topic}\"\n\n    @mcp.prompt(task=True)\n    async def background_prompt(topic: str, depth: str = \"detailed\") -> str:\n        \"\"\"A prompt that can execute in background.\"\"\"\n        return f\"Write a {depth} analysis of: {topic}\"\n\n    return mcp\n\n\nasync def test_synchronous_prompt_unchanged(prompt_server):\n    \"\"\"Prompts without task metadata execute synchronously as before.\"\"\"\n    async with Client(prompt_server) as client:\n        # Regular call without task metadata\n        result = await client.get_prompt(\"simple_prompt\", {\"topic\": \"AI\"})\n\n        # Should execute immediately and return result\n        assert \"Write about: AI\" in str(result)\n\n\nasync def test_prompt_with_task_metadata_returns_immediately(prompt_server):\n    \"\"\"Prompts with task metadata return immediately with PromptTask object.\"\"\"\n    async with Client(prompt_server) as client:\n        # Call with task metadata\n        task = await client.get_prompt(\"background_prompt\", {\"topic\": \"AI\"}, task=True)\n\n        # Should return a PromptTask object immediately\n        assert isinstance(task, PromptTask)\n        assert isinstance(task.task_id, str)\n        assert len(task.task_id) > 0\n\n\nasync def test_prompt_task_executes_in_background(prompt_server):\n    \"\"\"Prompt task executes via Docket in background.\"\"\"\n    async with Client(prompt_server) as client:\n        task = await client.get_prompt(\n            \"background_prompt\",\n            {\"topic\": \"Machine Learning\", \"depth\": \"comprehensive\"},\n            task=True,\n        )\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        # Get the result\n        result = await task.result()\n        assert \"comprehensive\" in result.messages[0].content.text.lower()\n\n\nasync def test_forbidden_mode_prompt_rejects_task_calls(prompt_server):\n    \"\"\"Prompts with task=False (mode=forbidden) reject task-augmented calls.\"\"\"\n    from mcp.shared.exceptions import McpError\n    from mcp.types import METHOD_NOT_FOUND\n\n    @prompt_server.prompt(task=False)  # Explicitly disable task support\n    async def sync_only_prompt(topic: str) -> str:\n        return f\"Sync prompt: {topic}\"\n\n    async with Client(prompt_server) as client:\n        # Calling with task=True when task=False should raise McpError\n        import pytest\n\n        with pytest.raises(McpError) as exc_info:\n            await client.get_prompt(\"sync_only_prompt\", {\"topic\": \"test\"}, task=True)\n\n        # New behavior: mode=\"forbidden\" returns METHOD_NOT_FOUND error\n        assert exc_info.value.error.code == METHOD_NOT_FOUND\n        assert (\n            \"does not support task-augmented execution\" in exc_info.value.error.message\n        )\n"
  },
  {
    "path": "tests/server/tasks/test_task_protocol.py",
    "content": "\"\"\"\nTests for SEP-1686 protocol-level task handling.\n\nGeneric protocol tests that use tools as test fixtures.\nTests metadata, notifications, and error handling at the protocol level.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def task_enabled_server():\n    \"\"\"Create a FastMCP server with task-enabled tools.\"\"\"\n    mcp = FastMCP(\"task-test-server\")\n\n    @mcp.tool(task=True)\n    async def simple_tool(message: str) -> str:\n        \"\"\"A simple tool for testing.\"\"\"\n        return f\"Processed: {message}\"\n\n    @mcp.tool(task=True)\n    async def failing_tool() -> str:\n        \"\"\"A tool that always fails.\"\"\"\n        raise ValueError(\"This tool always fails\")\n\n    return mcp\n\n\nasync def test_task_metadata_includes_task_id_and_ttl(task_enabled_server):\n    \"\"\"Task metadata properly includes server-generated taskId and ttl.\"\"\"\n    async with Client(task_enabled_server) as client:\n        # Submit with specific ttl (server generates task ID)\n        task = await client.call_tool(\n            \"simple_tool\",\n            {\"message\": \"test\"},\n            task=True,\n            ttl=30000,\n        )\n        assert task\n        assert not task.returned_immediately\n\n        # Server should have generated a task ID\n        assert task.task_id is not None\n        assert isinstance(task.task_id, str)\n\n\nasync def test_task_notification_sent_after_submission(task_enabled_server):\n    \"\"\"Server sends an initial task status notification after submission.\"\"\"\n\n    @task_enabled_server.tool(task=True)\n    async def background_tool(message: str) -> str:\n        return f\"Processed: {message}\"\n\n    async with Client(task_enabled_server) as client:\n        task = await client.call_tool(\"background_tool\", {\"message\": \"test\"}, task=True)\n        assert task\n        assert not task.returned_immediately\n\n        # Verify we can query the task\n        status = await task.status()\n        assert status.taskId == task.task_id\n\n\nasync def test_failed_task_stores_error(task_enabled_server):\n    \"\"\"Failed tasks store the error in results.\"\"\"\n\n    @task_enabled_server.tool(task=True)\n    async def failing_task_tool() -> str:\n        raise ValueError(\"This tool always fails\")\n\n    async with Client(task_enabled_server) as client:\n        task = await client.call_tool(\"failing_task_tool\", task=True)\n        assert task\n        assert not task.returned_immediately\n\n        # Wait for task to fail\n        status = await task.wait(state=\"failed\", timeout=2.0)\n        assert status.status == \"failed\"\n"
  },
  {
    "path": "tests/server/tasks/test_task_proxy.py",
    "content": "\"\"\"\nTests for MCP SEP-1686 task protocol behavior through proxy servers.\n\nProxy servers explicitly forbid task-augmented execution. All proxy components\n(tools, prompts, resources) have task_config.mode=\"forbidden\".\n\nClients connecting through proxies can:\n- Execute tools/prompts/resources normally (sync execution)\n- NOT use task-augmented execution (task=True fails gracefully for tools,\n  raises McpError for prompts/resources)\n\"\"\"\n\nimport pytest\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import TextContent, TextResourceContents\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.transports import FastMCPTransport\nfrom fastmcp.server import create_proxy\n\n\n@pytest.fixture\ndef backend_server() -> FastMCP:\n    \"\"\"Create a backend server with task-enabled components.\n\n    The backend has tasks enabled, but the proxy should NOT forward\n    task execution - it should treat all components as forbidden.\n    \"\"\"\n    mcp = FastMCP(\"backend-server\")\n\n    @mcp.tool(task=True)\n    async def add_numbers(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @mcp.tool(task=False)\n    async def sync_only_tool(message: str) -> str:\n        \"\"\"Tool that only supports synchronous execution.\"\"\"\n        return f\"sync: {message}\"\n\n    @mcp.prompt(task=True)\n    async def greeting_prompt(name: str) -> str:\n        \"\"\"A prompt that can execute as a task.\"\"\"\n        return f\"Hello, {name}! Welcome to the system.\"\n\n    @mcp.resource(\"data://info.txt\", task=True)\n    async def info_resource() -> str:\n        \"\"\"A resource that can be read as a task.\"\"\"\n        return \"Important information from the backend\"\n\n    @mcp.resource(\"data://user/{user_id}.json\", task=True)\n    async def user_resource(user_id: str) -> str:\n        \"\"\"A resource template that can execute as a task.\"\"\"\n        return f'{{\"id\": \"{user_id}\", \"name\": \"User {user_id}\"}}'\n\n    return mcp\n\n\n@pytest.fixture\ndef proxy_server(backend_server: FastMCP) -> FastMCP:\n    \"\"\"Create a proxy server that forwards to the backend.\"\"\"\n    return create_proxy(FastMCPTransport(backend_server))\n\n\nclass TestProxyToolsSyncExecution:\n    \"\"\"Test that tools work normally through proxy (sync execution).\"\"\"\n\n    async def test_tool_sync_execution_works(self, proxy_server: FastMCP):\n        \"\"\"Tool called without task=True works through proxy.\"\"\"\n        async with Client(proxy_server) as client:\n            result = await client.call_tool(\"add_numbers\", {\"a\": 5, \"b\": 3})\n            assert \"8\" in str(result)\n\n    async def test_sync_only_tool_works(self, proxy_server: FastMCP):\n        \"\"\"Sync-only tool works through proxy.\"\"\"\n        async with Client(proxy_server) as client:\n            result = await client.call_tool(\"sync_only_tool\", {\"message\": \"test\"})\n            assert \"sync: test\" in str(result)\n\n\nclass TestProxyToolsTaskForbidden:\n    \"\"\"Test that tools with task=True are forbidden through proxy.\"\"\"\n\n    async def test_tool_task_returns_error_immediately(self, proxy_server: FastMCP):\n        \"\"\"Tool called with task=True through proxy returns error immediately.\"\"\"\n        async with Client(proxy_server) as client:\n            task = await client.call_tool(\"add_numbers\", {\"a\": 5, \"b\": 3}, task=True)\n\n            # Should return immediately (forbidden behavior)\n            assert task.returned_immediately\n\n            # Result should be an error\n            result = await task.result()\n            assert result.is_error\n\n    async def test_sync_only_tool_task_returns_error_immediately(\n        self, proxy_server: FastMCP\n    ):\n        \"\"\"Sync-only tool with task=True also returns error immediately.\"\"\"\n        async with Client(proxy_server) as client:\n            task = await client.call_tool(\n                \"sync_only_tool\", {\"message\": \"test\"}, task=True\n            )\n\n            assert task.returned_immediately\n            result = await task.result()\n            assert result.is_error\n\n\nclass TestProxyPromptsSyncExecution:\n    \"\"\"Test that prompts work normally through proxy (sync execution).\"\"\"\n\n    async def test_prompt_sync_execution_works(self, proxy_server: FastMCP):\n        \"\"\"Prompt called without task=True works through proxy.\"\"\"\n        async with Client(proxy_server) as client:\n            result = await client.get_prompt(\"greeting_prompt\", {\"name\": \"Alice\"})\n            assert isinstance(result.messages[0].content, TextContent)\n            assert \"Hello, Alice!\" in result.messages[0].content.text\n\n\nclass TestProxyPromptsTaskForbidden:\n    \"\"\"Test that prompts with task=True are forbidden through proxy.\"\"\"\n\n    async def test_prompt_task_raises_mcp_error(self, proxy_server: FastMCP):\n        \"\"\"Prompt called with task=True through proxy raises McpError.\"\"\"\n        async with Client(proxy_server) as client:\n            with pytest.raises(McpError) as exc_info:\n                await client.get_prompt(\"greeting_prompt\", {\"name\": \"Alice\"}, task=True)\n\n            assert \"does not support task-augmented execution\" in str(exc_info.value)\n\n\nclass TestProxyResourcesSyncExecution:\n    \"\"\"Test that resources work normally through proxy (sync execution).\"\"\"\n\n    async def test_resource_sync_execution_works(self, proxy_server: FastMCP):\n        \"\"\"Resource read without task=True works through proxy.\"\"\"\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(\"data://info.txt\")\n            assert isinstance(result[0], TextResourceContents)\n            assert \"Important information from the backend\" in result[0].text\n\n    async def test_resource_template_sync_execution_works(self, proxy_server: FastMCP):\n        \"\"\"Resource template without task=True works through proxy.\"\"\"\n        async with Client(proxy_server) as client:\n            result = await client.read_resource(\"data://user/42.json\")\n            assert isinstance(result[0], TextResourceContents)\n            assert '\"id\": \"42\"' in result[0].text\n\n\nclass TestProxyResourcesTaskForbidden:\n    \"\"\"Test that resources with task=True are forbidden through proxy.\"\"\"\n\n    async def test_resource_task_raises_mcp_error(self, proxy_server: FastMCP):\n        \"\"\"Resource read with task=True through proxy raises McpError.\"\"\"\n        async with Client(proxy_server) as client:\n            with pytest.raises(McpError) as exc_info:\n                await client.read_resource(\"data://info.txt\", task=True)\n\n            assert \"does not support task-augmented execution\" in str(exc_info.value)\n\n    async def test_resource_template_task_raises_mcp_error(self, proxy_server: FastMCP):\n        \"\"\"Resource template with task=True through proxy raises McpError.\"\"\"\n        async with Client(proxy_server) as client:\n            with pytest.raises(McpError) as exc_info:\n                await client.read_resource(\"data://user/42.json\", task=True)\n\n            assert \"does not support task-augmented execution\" in str(exc_info.value)\n"
  },
  {
    "path": "tests/server/tasks/test_task_resources.py",
    "content": "\"\"\"\nTests for SEP-1686 background task support for resources.\n\nTests that resources with task=True can execute in background.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.tasks import ResourceTask\n\n\n@pytest.fixture\nasync def resource_server():\n    \"\"\"Create a FastMCP server with task-enabled resources.\"\"\"\n    mcp = FastMCP(\"resource-test-server\")\n\n    @mcp.resource(\"file://data.txt\")\n    async def simple_resource() -> str:\n        \"\"\"A simple resource.\"\"\"\n        return \"Simple content\"\n\n    @mcp.resource(\"file://large.txt\", task=True)\n    async def background_resource() -> str:\n        \"\"\"A resource that can execute in background.\"\"\"\n        return \"Large file content that takes time to load\"\n\n    @mcp.resource(\"file://user/{user_id}/data.json\", task=True)\n    async def template_resource(user_id: str) -> str:\n        \"\"\"A resource template that can execute in background.\"\"\"\n        return f'{{\"userId\": \"{user_id}\", \"data\": \"value\"}}'\n\n    return mcp\n\n\nasync def test_synchronous_resource_unchanged(resource_server):\n    \"\"\"Resources without task metadata execute synchronously as before.\"\"\"\n    async with Client(resource_server) as client:\n        # Regular call without task metadata\n        result = await client.read_resource(\"file://data.txt\")\n\n        # Should execute immediately and return result\n        assert \"Simple content\" in str(result)\n\n\nasync def test_resource_with_task_metadata_returns_immediately(resource_server):\n    \"\"\"Resources with task metadata return immediately with ResourceTask object.\"\"\"\n    async with Client(resource_server) as client:\n        # Call with task metadata\n        task = await client.read_resource(\"file://large.txt\", task=True)\n\n        # Should return a ResourceTask object immediately\n        assert isinstance(task, ResourceTask)\n        assert isinstance(task.task_id, str)\n        assert len(task.task_id) > 0\n\n\nasync def test_resource_task_executes_in_background(resource_server):\n    \"\"\"Resource task executes via Docket in background.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://large.txt\", task=True)\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        # Get the result\n        result = await task.result()\n        assert len(result) > 0\n        assert result[0].text == \"Large file content that takes time to load\"\n\n\nasync def test_resource_template_with_task(resource_server):\n    \"\"\"Resource templates with task=True execute in background.\"\"\"\n    async with Client(resource_server) as client:\n        task = await client.read_resource(\"file://user/123/data.json\", task=True)\n\n        # Verify background execution\n        assert not task.returned_immediately\n\n        # Get the result\n        result = await task.result()\n        assert '\"userId\": \"123\"' in result[0].text\n\n\nasync def test_forbidden_mode_resource_rejects_task_calls(resource_server):\n    \"\"\"Resources with task=False (mode=forbidden) reject task-augmented calls.\"\"\"\n    import pytest\n    from mcp.shared.exceptions import McpError\n    from mcp.types import METHOD_NOT_FOUND\n\n    @resource_server.resource(\n        \"file://sync.txt/\", task=False\n    )  # Explicitly disable task support\n    async def sync_only_resource() -> str:\n        return \"Sync content\"\n\n    async with Client(resource_server) as client:\n        # Calling with task=True when task=False should raise McpError\n        with pytest.raises(McpError) as exc_info:\n            await client.read_resource(\"file://sync.txt\", task=True)\n\n        # New behavior: mode=\"forbidden\" returns METHOD_NOT_FOUND error\n        assert exc_info.value.error.code == METHOD_NOT_FOUND\n        assert (\n            \"does not support task-augmented execution\" in exc_info.value.error.message\n        )\n"
  },
  {
    "path": "tests/server/tasks/test_task_return_types.py",
    "content": "\"\"\"\nTests to verify all return types work identically with task=True.\n\nThese tests ensure that enabling background task support doesn't break\nexisting functionality - any tool/prompt/resource should work exactly\nthe same whether task=True or task=False.\n\"\"\"\n\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import UUID\n\nimport pytest\nfrom pydantic import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.utilities.types import Audio, File, Image\n\n\nclass UserData(BaseModel):\n    \"\"\"Example structured output.\"\"\"\n\n    name: str\n    age: int\n    active: bool\n\n\n@pytest.fixture\nasync def return_type_server():\n    \"\"\"Server with tools that return various types.\"\"\"\n    mcp = FastMCP(\"return-type-test\")\n\n    # String return\n    @mcp.tool(task=True)\n    async def return_string() -> str:\n        return \"Hello, World!\"\n\n    # Integer return\n    @mcp.tool(task=True)\n    async def return_int() -> int:\n        return 42\n\n    # Float return\n    @mcp.tool(task=True)\n    async def return_float() -> float:\n        return 3.14159\n\n    # Boolean return\n    @mcp.tool(task=True)\n    async def return_bool() -> bool:\n        return True\n\n    # Dict return\n    @mcp.tool(task=True)\n    async def return_dict() -> dict[str, int]:\n        return {\"count\": 100, \"total\": 500}\n\n    # List return\n    @mcp.tool(task=True)\n    async def return_list() -> list[str]:\n        return [\"apple\", \"banana\", \"cherry\"]\n\n    # BaseModel return (structured output)\n    @mcp.tool(task=True)\n    async def return_model() -> UserData:\n        return UserData(name=\"Alice\", age=30, active=True)\n\n    # None/null return\n    @mcp.tool(task=True)\n    async def return_none() -> None:\n        return None\n\n    return mcp\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_type,expected_value\",\n    [\n        (\"return_string\", str, \"Hello, World!\"),\n        (\"return_int\", int, 42),\n        (\"return_float\", float, 3.14159),\n        (\"return_bool\", bool, True),\n        (\"return_dict\", dict, {\"count\": 100, \"total\": 500}),\n        (\"return_list\", list, [\"apple\", \"banana\", \"cherry\"]),\n        (\"return_none\", type(None), None),\n    ],\n)\nasync def test_task_basic_types(\n    return_type_server: FastMCP,\n    tool_name: str,\n    expected_type: type,\n    expected_value: Any,\n):\n    \"\"\"Task mode returns basic types correctly.\"\"\"\n    async with Client(return_type_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert isinstance(result.data, expected_type)\n        assert result.data == expected_value\n\n\nasync def test_task_model_return(return_type_server):\n    \"\"\"Task mode returns same BaseModel (as dict) as immediate mode.\"\"\"\n    async with Client(return_type_server) as client:\n        task = await client.call_tool(\"return_model\", task=True)\n        result = await task\n\n        # Client deserializes to dynamic class (type name lost with title pruning)\n        assert result.data.__class__.__name__ == \"Root\"\n        assert result.data.name == \"Alice\"\n        assert result.data.age == 30\n        assert result.data.active is True\n\n\nasync def test_task_vs_immediate_equivalence(return_type_server):\n    \"\"\"Verify task mode and immediate mode return identical results.\"\"\"\n    async with Client(return_type_server) as client:\n        # Test a few types to verify equivalence\n        tools_to_test = [\"return_string\", \"return_int\", \"return_dict\"]\n\n        for tool_name in tools_to_test:\n            # Call as task\n            task = await client.call_tool(tool_name, task=True)\n            task_result = await task\n\n            # Call immediately (server should decline background execution when no task meta)\n            immediate_result = await client.call_tool(tool_name)\n\n            # Results should be identical\n            assert task_result.data == immediate_result.data, (\n                f\"Mismatch for {tool_name}\"\n            )\n\n\n@pytest.fixture\nasync def prompt_return_server():\n    \"\"\"Server with prompts that return various message structures.\"\"\"\n    mcp = FastMCP(\"prompt-return-test\")\n\n    @mcp.prompt(task=True)\n    async def single_message_prompt() -> str:\n        \"\"\"Return a single string message.\"\"\"\n        return \"Single message content\"\n\n    @mcp.prompt(task=True)\n    async def multi_message_prompt() -> list[str]:\n        \"\"\"Return multiple messages.\"\"\"\n        return [\n            \"First message\",\n            \"Second message\",\n            \"Third message\",\n        ]\n\n    return mcp\n\n\nasync def test_prompt_task_single_message(prompt_return_server):\n    \"\"\"Prompt task returns single message correctly.\"\"\"\n    async with Client(prompt_return_server) as client:\n        task = await client.get_prompt(\"single_message_prompt\", task=True)\n        result = await task\n\n        assert len(result.messages) == 1\n        assert result.messages[0].content.text == \"Single message content\"\n\n\nasync def test_prompt_task_multiple_messages(prompt_return_server):\n    \"\"\"Prompt task returns multiple messages correctly.\"\"\"\n    async with Client(prompt_return_server) as client:\n        task = await client.get_prompt(\"multi_message_prompt\", task=True)\n        result = await task\n\n        assert len(result.messages) == 3\n        assert result.messages[0].content.text == \"First message\"\n        assert result.messages[1].content.text == \"Second message\"\n        assert result.messages[2].content.text == \"Third message\"\n\n\n@pytest.fixture\nasync def resource_return_server():\n    \"\"\"Server with resources that return various content types.\"\"\"\n    mcp = FastMCP(\"resource-return-test\")\n\n    @mcp.resource(\"text://simple\", task=True)\n    async def simple_text() -> str:\n        \"\"\"Return simple text content.\"\"\"\n        return \"Simple text resource\"\n\n    @mcp.resource(\"data://json\", task=True)\n    async def json_data() -> str:\n        \"\"\"Return JSON-like data.\"\"\"\n        import json\n\n        return json.dumps({\"key\": \"value\", \"count\": 123})\n\n    return mcp\n\n\nasync def test_resource_task_text_content(resource_return_server):\n    \"\"\"Resource task returns text content correctly.\"\"\"\n    async with Client(resource_return_server) as client:\n        task = await client.read_resource(\"text://simple\", task=True)\n        contents = await task\n\n        assert len(contents) == 1\n        assert contents[0].text == \"Simple text resource\"\n\n\nasync def test_resource_task_json_content(resource_return_server):\n    \"\"\"Resource task returns structured content correctly.\"\"\"\n    async with Client(resource_return_server) as client:\n        task = await client.read_resource(\"data://json\", task=True)\n        contents = await task\n\n        # Content should be JSON serialized\n        assert len(contents) == 1\n        import json\n\n        data = json.loads(contents[0].text)\n        assert data == {\"key\": \"value\", \"count\": 123}\n\n\n# ==============================================================================\n# Binary & Special Types\n# ==============================================================================\n\n\n@pytest.fixture\nasync def binary_type_server():\n    \"\"\"Server with tools returning binary and special types.\"\"\"\n    mcp = FastMCP(\"binary-test\")\n\n    @mcp.tool(task=True)\n    async def return_bytes() -> bytes:\n        return b\"Hello bytes!\"\n\n    @mcp.tool(task=True)\n    async def return_uuid() -> UUID:\n        return UUID(\"12345678-1234-5678-1234-567812345678\")\n\n    @mcp.tool(task=True)\n    async def return_path() -> Path:\n        return Path(\"/tmp/test.txt\")\n\n    @mcp.tool(task=True)\n    async def return_datetime() -> datetime:\n        return datetime(2025, 11, 5, 12, 30, 45)\n\n    return mcp\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_type,assertion_fn\",\n    [\n        (\n            \"return_bytes\",\n            str,\n            lambda r: \"Hello bytes!\" in r.data or \"SGVsbG8gYnl0ZXMh\" in r.data,\n        ),\n        (\n            \"return_uuid\",\n            str,\n            lambda r: r.data == \"12345678-1234-5678-1234-567812345678\",\n        ),\n        (\n            \"return_path\",\n            str,\n            lambda r: \"tmp\" in r.data and \"test.txt\" in r.data,\n        ),\n        (\n            \"return_datetime\",\n            datetime,\n            lambda r: r.data == datetime(2025, 11, 5, 12, 30, 45),\n        ),\n    ],\n)\nasync def test_task_binary_types(\n    binary_type_server: FastMCP,\n    tool_name: str,\n    expected_type: type,\n    assertion_fn: Any,\n):\n    \"\"\"Task mode handles binary and special types.\"\"\"\n    async with Client(binary_type_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert isinstance(result.data, expected_type)\n        assert assertion_fn(result)\n\n\n# ==============================================================================\n# Collection Varieties\n# ==============================================================================\n\n\n@pytest.fixture\nasync def collection_server():\n    \"\"\"Server with tools returning various collection types.\"\"\"\n    mcp = FastMCP(\"collection-test\")\n\n    @mcp.tool(task=True)\n    async def return_tuple() -> tuple[int, str, bool]:\n        return (42, \"hello\", True)\n\n    @mcp.tool(task=True)\n    async def return_set() -> set[int]:\n        return {1, 2, 3}\n\n    @mcp.tool(task=True)\n    async def return_empty_list() -> list[str]:\n        return []\n\n    @mcp.tool(task=True)\n    async def return_empty_dict() -> dict[str, Any]:\n        return {}\n\n    return mcp\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_type,expected_value\",\n    [\n        (\"return_tuple\", list, [42, \"hello\", True]),\n        (\"return_set\", set, {1, 2, 3}),\n        (\"return_empty_list\", list, []),\n    ],\n)\nasync def test_task_collection_types(\n    collection_server: FastMCP,\n    tool_name: str,\n    expected_type: type,\n    expected_value: Any,\n):\n    \"\"\"Task mode handles collection types.\"\"\"\n    async with Client(collection_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert isinstance(result.data, expected_type)\n        assert result.data == expected_value\n\n\nasync def test_task_empty_dict_return(collection_server):\n    \"\"\"Task mode handles empty dict return.\"\"\"\n    async with Client(collection_server) as client:\n        task = await client.call_tool(\"return_empty_dict\", task=True)\n        result = await task\n        # Empty structured content becomes None in data\n        assert result.data is None\n        # But structured content is still {}\n        assert result.structured_content == {}\n\n\n# ==============================================================================\n# Media Types (Image, Audio, File)\n# ==============================================================================\n\n\n@pytest.fixture\nasync def media_server(tmp_path):\n    \"\"\"Server with tools returning media types.\"\"\"\n    mcp = FastMCP(\"media-test\")\n\n    # Create test files\n    test_image = tmp_path / \"test.png\"\n    test_image.write_bytes(b\"\\x89PNG\\r\\n\\x1a\\n\" + b\"fake png data\")\n\n    test_audio = tmp_path / \"test.mp3\"\n    test_audio.write_bytes(b\"ID3\" + b\"fake mp3 data\")\n\n    test_file = tmp_path / \"test.txt\"\n    test_file.write_text(\"test file content\")\n\n    @mcp.tool(task=True)\n    async def return_image_path() -> Image:\n        return Image(path=str(test_image))\n\n    @mcp.tool(task=True)\n    async def return_image_data() -> Image:\n        return Image(data=test_image.read_bytes(), format=\"png\")\n\n    @mcp.tool(task=True)\n    async def return_audio() -> Audio:\n        return Audio(path=str(test_audio))\n\n    @mcp.tool(task=True)\n    async def return_file() -> File:\n        return File(path=str(test_file))\n\n    return mcp\n\n\n@pytest.mark.parametrize(\n    \"tool_name,assertion_fn\",\n    [\n        (\n            \"return_image_path\",\n            lambda r: len(r.content) == 1 and r.content[0].type == \"image\",\n        ),\n        (\n            \"return_image_data\",\n            lambda r: (\n                len(r.content) == 1\n                and r.content[0].type == \"image\"\n                and r.content[0].mimeType == \"image/png\"\n            ),\n        ),\n        (\n            \"return_audio\",\n            lambda r: len(r.content) == 1 and r.content[0].type in [\"text\", \"audio\"],\n        ),\n        (\n            \"return_file\",\n            lambda r: len(r.content) == 1 and r.content[0].type == \"resource\",\n        ),\n    ],\n)\nasync def test_task_media_types(\n    media_server: FastMCP,\n    tool_name: str,\n    assertion_fn: Any,\n):\n    \"\"\"Task mode handles media types (Image, Audio, File).\"\"\"\n    async with Client(media_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert assertion_fn(result)\n\n\n# ==============================================================================\n# Structured Types (TypedDict, dataclass, unions)\n# ==============================================================================\n\n\nclass PersonTypedDict(TypedDict):\n    \"\"\"Example TypedDict.\"\"\"\n\n    name: str\n    age: int\n\n\n@dataclass\nclass PersonDataclass:\n    \"\"\"Example dataclass.\"\"\"\n\n    name: str\n    age: int\n\n\n@pytest.fixture\nasync def structured_type_server():\n    \"\"\"Server with tools returning structured types.\"\"\"\n    mcp = FastMCP(\"structured-test\")\n\n    @mcp.tool(task=True)\n    async def return_typeddict() -> PersonTypedDict:\n        return {\"name\": \"Bob\", \"age\": 25}\n\n    @mcp.tool(task=True)\n    async def return_dataclass() -> PersonDataclass:\n        return PersonDataclass(name=\"Charlie\", age=35)\n\n    @mcp.tool(task=True)\n    async def return_union() -> str | int:\n        return \"string value\"\n\n    @mcp.tool(task=True)\n    async def return_union_int() -> str | int:\n        return 123\n\n    @mcp.tool(task=True)\n    async def return_optional() -> str | None:\n        return \"has value\"\n\n    @mcp.tool(task=True)\n    async def return_optional_none() -> str | None:\n        return None\n\n    return mcp\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_name,expected_age\",\n    [\n        (\"return_typeddict\", \"Bob\", 25),\n        (\"return_dataclass\", \"Charlie\", 35),\n    ],\n)\nasync def test_task_structured_dict_types(\n    structured_type_server: FastMCP,\n    tool_name: str,\n    expected_name: str,\n    expected_age: int,\n):\n    \"\"\"Task mode handles TypedDict and dataclass returns.\"\"\"\n    async with Client(structured_type_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        # Both deserialize to dynamic Root class\n        assert result.data.name == expected_name\n        assert result.data.age == expected_age\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_type,expected_value\",\n    [\n        (\"return_union\", str, \"string value\"),\n        (\"return_union_int\", int, 123),\n    ],\n)\nasync def test_task_union_types(\n    structured_type_server: FastMCP,\n    tool_name: str,\n    expected_type: type,\n    expected_value: Any,\n):\n    \"\"\"Task mode handles union type branches.\"\"\"\n    async with Client(structured_type_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert isinstance(result.data, expected_type)\n        assert result.data == expected_value\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_type,expected_value\",\n    [\n        (\"return_optional\", str, \"has value\"),\n        (\"return_optional_none\", type(None), None),\n    ],\n)\nasync def test_task_optional_types(\n    structured_type_server: FastMCP,\n    tool_name: str,\n    expected_type: type,\n    expected_value: Any,\n):\n    \"\"\"Task mode handles Optional types.\"\"\"\n    async with Client(structured_type_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert isinstance(result.data, expected_type)\n        assert result.data == expected_value\n\n\n# ==============================================================================\n# MCP Content Blocks\n# ==============================================================================\n\n\n@pytest.fixture\nasync def mcp_content_server(tmp_path):\n    \"\"\"Server with tools returning MCP content blocks.\"\"\"\n    import base64\n\n    from mcp.types import (\n        AnyUrl,\n        EmbeddedResource,\n        ImageContent,\n        ResourceLink,\n        TextContent,\n        TextResourceContents,\n    )\n\n    mcp = FastMCP(\"content-test\")\n\n    test_image = tmp_path / \"content.png\"\n    test_image.write_bytes(b\"\\x89PNG\\r\\n\\x1a\\n\" + b\"content\")\n\n    @mcp.tool(task=True)\n    async def return_text_content() -> TextContent:\n        return TextContent(type=\"text\", text=\"Direct text content\")\n\n    @mcp.tool(task=True)\n    async def return_image_content() -> ImageContent:\n        return ImageContent(\n            type=\"image\",\n            data=base64.b64encode(test_image.read_bytes()).decode(),\n            mimeType=\"image/png\",\n        )\n\n    @mcp.tool(task=True)\n    async def return_embedded_resource() -> EmbeddedResource:\n        return EmbeddedResource(\n            type=\"resource\",\n            resource=TextResourceContents(\n                uri=AnyUrl(\"test://resource\"), text=\"embedded\"\n            ),\n        )\n\n    @mcp.tool(task=True)\n    async def return_resource_link() -> ResourceLink:\n        return ResourceLink(\n            type=\"resource_link\", uri=AnyUrl(\"test://linked\"), name=\"Test Resource\"\n        )\n\n    @mcp.tool(task=True)\n    async def return_mixed_content() -> list[TextContent | ImageContent]:\n        return [\n            TextContent(type=\"text\", text=\"First block\"),\n            ImageContent(\n                type=\"image\",\n                data=base64.b64encode(test_image.read_bytes()).decode(),\n                mimeType=\"image/png\",\n            ),\n            TextContent(type=\"text\", text=\"Third block\"),\n        ]\n\n    return mcp\n\n\n@pytest.mark.parametrize(\n    \"tool_name,assertion_fn\",\n    [\n        (\n            \"return_text_content\",\n            lambda r: (\n                len(r.content) == 1\n                and r.content[0].type == \"text\"\n                and r.content[0].text == \"Direct text content\"\n            ),\n        ),\n        (\n            \"return_image_content\",\n            lambda r: (\n                len(r.content) == 1\n                and r.content[0].type == \"image\"\n                and r.content[0].mimeType == \"image/png\"\n            ),\n        ),\n        (\n            \"return_embedded_resource\",\n            lambda r: len(r.content) == 1 and r.content[0].type == \"resource\",\n        ),\n        (\n            \"return_resource_link\",\n            lambda r: (\n                len(r.content) == 1\n                and r.content[0].type == \"resource_link\"\n                and str(r.content[0].uri) == \"test://linked\"\n            ),\n        ),\n    ],\n)\nasync def test_task_mcp_content_types(\n    mcp_content_server: FastMCP,\n    tool_name: str,\n    assertion_fn: Any,\n):\n    \"\"\"Task mode handles MCP content block types.\"\"\"\n    async with Client(mcp_content_server) as client:\n        task = await client.call_tool(tool_name, task=True)\n        result = await task\n        assert assertion_fn(result)\n\n\nasync def test_task_mixed_content_return(mcp_content_server):\n    \"\"\"Task mode handles mixed content list return.\"\"\"\n    async with Client(mcp_content_server) as client:\n        task = await client.call_tool(\"return_mixed_content\", task=True)\n        result = await task\n        assert len(result.content) == 3\n        assert result.content[0].type == \"text\"\n        assert result.content[0].text == \"First block\"\n        assert result.content[1].type == \"image\"\n        assert result.content[2].type == \"text\"\n        assert result.content[2].text == \"Third block\"\n"
  },
  {
    "path": "tests/server/tasks/test_task_security.py",
    "content": "\"\"\"\nTests for session-based task ID isolation (CRITICAL SECURITY).\n\nEnsures that tasks are properly scoped to sessions and clients cannot\naccess each other's tasks.\n\"\"\"\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def task_server():\n    \"\"\"Create a server with background tasks enabled.\"\"\"\n    mcp = FastMCP(\"security-test-server\")\n\n    @mcp.tool(task=True)  # Enable background execution\n    async def secret_tool(data: str) -> str:\n        \"\"\"A tool that processes sensitive data.\"\"\"\n        return f\"Secret result: {data}\"\n\n    return mcp\n\n\nasync def test_same_session_can_access_all_its_tasks(task_server):\n    \"\"\"A single session can access all tasks it created.\"\"\"\n    async with Client(task_server) as client:\n        # Submit multiple tasks\n        task1 = await client.call_tool(\n            \"secret_tool\", {\"data\": \"first\"}, task=True, task_id=\"task-1\"\n        )\n        task2 = await client.call_tool(\n            \"secret_tool\", {\"data\": \"second\"}, task=True, task_id=\"task-2\"\n        )\n\n        # Wait for both to complete\n        await task1.wait(timeout=2.0)\n        await task2.wait(timeout=2.0)\n\n        # Should be able to access both\n        result1 = await task1.result()\n        result2 = await task2.result()\n\n        assert \"first\" in str(result1.data)\n        assert \"second\" in str(result2.data)\n"
  },
  {
    "path": "tests/server/tasks/test_task_status_notifications.py",
    "content": "\"\"\"\nTests for notifications/tasks/status subscription mechanism (SEP-1686 lines 436-444).\n\nPer the spec, servers MAY send notifications/tasks/status when task state changes.\nThis is an optional optimization that reduces client polling frequency.\n\nThese tests verify that the subscription mechanism works correctly without breaking\nexisting functionality. Notification delivery is best-effort and clients MUST NOT\nrely on receiving them.\n\"\"\"\n\nimport asyncio\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def notification_server():\n    \"\"\"Create a server for testing task status notifications.\"\"\"\n    mcp = FastMCP(\"notification-test\")\n\n    @mcp.tool(task=True)\n    async def quick_task(value: int) -> int:\n        \"\"\"Quick task that completes immediately.\"\"\"\n        return value * 2\n\n    @mcp.tool(task=True)\n    async def slow_task(duration: float = 0.1) -> str:\n        \"\"\"Slow task for testing working status.\"\"\"\n        await asyncio.sleep(duration)\n        return \"completed\"\n\n    @mcp.tool(task=True)\n    async def failing_task() -> str:\n        \"\"\"Task that always fails.\"\"\"\n        raise ValueError(\"Task failed intentionally\")\n\n    @mcp.prompt(task=True)\n    async def test_prompt(name: str) -> str:\n        \"\"\"Test prompt for background execution.\"\"\"\n        await asyncio.sleep(0.05)\n        return f\"Hello, {name}!\"\n\n    @mcp.resource(\"test://resource\", task=True)\n    async def test_resource() -> str:\n        \"\"\"Test resource for background execution.\"\"\"\n        await asyncio.sleep(0.05)\n        return \"resource content\"\n\n    return mcp\n\n\nasync def test_subscription_spawned_for_tool_task(notification_server: FastMCP):\n    \"\"\"Subscription task is spawned when tool task is created.\"\"\"\n    async with Client(notification_server) as client:\n        # Create task - should spawn subscription\n        task = await client.call_tool(\"quick_task\", {\"value\": 5}, task=True)\n\n        # Task should complete normally\n        result = await task\n        assert result.data == 10\n\n        # Subscription should clean up automatically\n        # (No way to directly test, but shouldn't cause issues)\n\n\nasync def test_subscription_handles_task_completion(notification_server: FastMCP):\n    \"\"\"Subscription properly handles task completion and cleanup.\"\"\"\n    async with Client(notification_server) as client:\n        # Multiple tasks should each get their own subscription\n        task1 = await client.call_tool(\"quick_task\", {\"value\": 1}, task=True)\n        task2 = await client.call_tool(\"quick_task\", {\"value\": 2}, task=True)\n        task3 = await client.call_tool(\"quick_task\", {\"value\": 3}, task=True)\n\n        # All should complete successfully\n        result1 = await task1\n        result2 = await task2\n        result3 = await task3\n\n        assert result1.data == 2\n        assert result2.data == 4\n        assert result3.data == 6\n\n        # Subscriptions should all clean up\n        # Give them a moment\n        await asyncio.sleep(0.1)\n\n\nasync def test_subscription_handles_task_failure(notification_server: FastMCP):\n    \"\"\"Subscription properly handles task failure.\"\"\"\n    async with Client(notification_server) as client:\n        task = await client.call_tool(\"failing_task\", {}, task=True)\n\n        # Task should fail\n        with pytest.raises(Exception):\n            await task\n\n        # Subscription should handle failure and clean up\n        await asyncio.sleep(0.1)\n\n\nasync def test_subscription_for_prompt_tasks(notification_server: FastMCP):\n    \"\"\"Subscriptions work for prompt tasks.\"\"\"\n    async with Client(notification_server) as client:\n        task = await client.get_prompt(\"test_prompt\", {\"name\": \"World\"}, task=True)\n\n        result = await task\n        # Prompt result has messages\n        assert result\n\n        # Subscription should clean up\n        await asyncio.sleep(0.1)\n\n\nasync def test_subscription_for_resource_tasks(notification_server: FastMCP):\n    \"\"\"Subscriptions work for resource tasks.\"\"\"\n    async with Client(notification_server) as client:\n        task = await client.read_resource(\"test://resource\", task=True)\n\n        result = await task\n        assert result  # Resource contents\n\n        # Subscription should clean up\n        await asyncio.sleep(0.1)\n\n\nasync def test_subscriptions_cleanup_on_session_disconnect(\n    notification_server: FastMCP,\n):\n    \"\"\"Subscriptions are cleaned up when session disconnects.\"\"\"\n    # Start session and create task\n    async with Client(notification_server) as client:\n        task = await client.call_tool(\"slow_task\", {\"duration\": 1.0}, task=True)\n        task_id = task.task_id\n        # Disconnect before task completes (session __aexit__ cancels subscriptions)\n\n    # Session is now closed, subscription should be cancelled\n    # Task continues in Docket but notification subscription is gone\n    # This test passing means no crash occurred during cleanup\n    assert task_id  # Task was created\n\n\nasync def test_multiple_concurrent_subscriptions(notification_server: FastMCP):\n    \"\"\"Multiple concurrent tasks each have their own subscription.\"\"\"\n    async with Client(notification_server) as client:\n        # Start many tasks concurrently\n        tasks = []\n        for i in range(10):\n            task = await client.call_tool(\"quick_task\", {\"value\": i}, task=True)\n            tasks.append(task)\n\n        # All should complete\n        results = await asyncio.gather(*tasks)\n        assert len(results) == 10\n\n        # All subscriptions should clean up\n        await asyncio.sleep(0.1)\n"
  },
  {
    "path": "tests/server/tasks/test_task_tools.py",
    "content": "\"\"\"\nTests for server-side tool task behavior.\n\nTests tool-specific task handling, parallel to test_task_prompts.py\nand test_task_resources.py.\n\"\"\"\n\nimport asyncio\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.client.tasks import ToolTask\n\n\n@pytest.fixture\nasync def tool_server():\n    \"\"\"Create a FastMCP server with task-enabled tools.\"\"\"\n    mcp = FastMCP(\"tool-task-server\")\n\n    @mcp.tool(task=True)\n    async def simple_tool(message: str) -> str:\n        \"\"\"A simple tool for testing.\"\"\"\n        return f\"Processed: {message}\"\n\n    @mcp.tool(task=False)\n    async def sync_only_tool(message: str) -> str:\n        \"\"\"Tool with task=False.\"\"\"\n        return f\"Sync: {message}\"\n\n    return mcp\n\n\nasync def test_synchronous_tool_call_unchanged(tool_server):\n    \"\"\"Tools without task metadata execute synchronously as before.\"\"\"\n    async with Client(tool_server) as client:\n        # Regular call without task metadata\n        result = await client.call_tool(\"simple_tool\", {\"message\": \"hello\"})\n\n        # Should execute immediately and return result\n        assert \"Processed: hello\" in str(result)\n\n\nasync def test_tool_with_task_metadata_returns_immediately(tool_server):\n    \"\"\"Tools with task metadata return immediately with ToolTask object.\"\"\"\n    async with Client(tool_server) as client:\n        # Call with task metadata\n        task = await client.call_tool(\"simple_tool\", {\"message\": \"test\"}, task=True)\n        assert task\n        assert not task.returned_immediately\n\n        assert isinstance(task, ToolTask)\n        assert isinstance(task.task_id, str)\n        assert len(task.task_id) > 0\n\n\nasync def test_tool_task_executes_in_background(tool_server):\n    \"\"\"Tool task is submitted to Docket and executes in background.\"\"\"\n    execution_started = asyncio.Event()\n    execution_completed = asyncio.Event()\n\n    @tool_server.tool(task=True)\n    async def coordinated_tool() -> str:\n        \"\"\"Tool with coordination points.\"\"\"\n        execution_started.set()\n        await execution_completed.wait()\n        return \"completed\"\n\n    async with Client(tool_server) as client:\n        task = await client.call_tool(\"coordinated_tool\", task=True)\n        assert task\n        assert not task.returned_immediately\n\n        # Wait for execution to start\n        await asyncio.wait_for(execution_started.wait(), timeout=2.0)\n\n        # Task should still be working\n        status = await task.status()\n        assert status.status in [\"working\"]\n\n        # Signal completion\n        execution_completed.set()\n        await task.wait(timeout=2.0)\n\n        result = await task.result()\n        assert result.data == \"completed\"\n\n\nasync def test_forbidden_mode_tool_rejects_task_calls(tool_server):\n    \"\"\"Tools with task=False (mode=forbidden) reject task-augmented calls.\"\"\"\n    async with Client(tool_server) as client:\n        # Calling with task=True when task=False should return error\n        task = await client.call_tool(\"sync_only_tool\", {\"message\": \"test\"}, task=True)\n        assert task\n        assert task.returned_immediately\n\n        result = await task.result()\n        # New behavior: mode=\"forbidden\" returns an error\n        assert result.is_error\n        assert \"does not support task-augmented execution\" in str(result)\n"
  },
  {
    "path": "tests/server/tasks/test_task_ttl.py",
    "content": "\"\"\"\nTests for SEP-1686 ttl parameter handling.\n\nPer the spec, servers MUST return ttl in all tasks/get responses,\nand results should be retained for ttl milliseconds after completion.\n\"\"\"\n\nimport asyncio\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\n\n\n@pytest.fixture\nasync def keepalive_server():\n    \"\"\"Create a server for testing ttl behavior.\"\"\"\n    mcp = FastMCP(\"keepalive-test\")\n\n    @mcp.tool(task=True)\n    async def quick_task(value: int) -> int:\n        return value * 2\n\n    @mcp.tool(task=True)\n    async def slow_task() -> str:\n        await asyncio.sleep(1)\n        return \"done\"\n\n    return mcp\n\n\nasync def test_keepalive_returned_in_submitted_state(keepalive_server: FastMCP):\n    \"\"\"ttl is returned in tasks/get even when task is submitted/working.\"\"\"\n    async with Client(keepalive_server) as client:\n        # Submit task with explicit ttl\n        task = await client.call_tool(\n            \"slow_task\",\n            {},\n            task=True,\n            ttl=30000,  # 30 seconds (client-requested)\n        )\n\n        # Check status immediately - should be submitted or working\n        status = await task.status()\n        assert status.status in [\"working\"]\n\n        # ttl should be present per spec (MUST return in all responses)\n        # TODO: Docket uses a global execution_ttl for all tasks, not per-task TTLs.\n        # The spec allows servers to override client-requested TTL (line 431).\n        # FastMCP returns the server's actual global TTL (60000ms default from Docket).\n        # If Docket gains per-task TTL support, update this to verify client-requested TTL is respected.\n        assert status.ttl == 60000  # Server's global TTL, not client-requested 30000\n\n\nasync def test_keepalive_returned_in_completed_state(keepalive_server: FastMCP):\n    \"\"\"ttl is returned in tasks/get after task completes.\"\"\"\n    async with Client(keepalive_server) as client:\n        # Submit and complete task\n        task = await client.call_tool(\n            \"quick_task\",\n            {\"value\": 5},\n            task=True,\n            ttl=45000,  # Client-requested TTL\n        )\n        await task.wait(timeout=2.0)\n\n        # Check status - should be completed\n        status = await task.status()\n        assert status.status == \"completed\"\n\n        # TODO: Docket uses global execution_ttl, not per-task TTLs.\n        # Server returns its global TTL (60000ms), not the client-requested 45000ms.\n        # This is spec-compliant - servers MAY override requested TTL (spec line 431).\n        assert status.ttl == 60000  # Server's global TTL, not client-requested 45000\n\n\nasync def test_default_keepalive_when_not_specified(keepalive_server: FastMCP):\n    \"\"\"Default ttl is used when client doesn't specify.\"\"\"\n    async with Client(keepalive_server) as client:\n        # Submit without explicit ttl\n        task = await client.call_tool(\"quick_task\", {\"value\": 3}, task=True)\n        await task.wait(timeout=2.0)\n\n        status = await task.status()\n        # Should have default ttl (60000ms = 60 seconds)\n        assert status.ttl == 60000\n"
  },
  {
    "path": "tests/server/telemetry/__init__.py",
    "content": "\"\"\"Tests for FastMCP server telemetry.\"\"\"\n"
  },
  {
    "path": "tests/server/telemetry/test_provider_tracing.py",
    "content": "\"\"\"Tests for provider-level OpenTelemetry tracing.\"\"\"\n\nfrom __future__ import annotations\n\nfrom opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter\n\nfrom fastmcp import FastMCP\n\n\nclass TestFastMCPProviderTracing:\n    \"\"\"Tests for FastMCPProvider delegation tracing.\"\"\"\n\n    async def test_mounted_tool_creates_delegate_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        # Create a child server with a tool\n        child = FastMCP(\"child-server\")\n\n        @child.tool()\n        def child_tool() -> str:\n            return \"child result\"\n\n        # Create parent server and mount child with namespace\n        parent = FastMCP(\"parent-server\")\n        parent.mount(child, namespace=\"child\")\n\n        # Call the tool through the parent (namespace uses underscore, not slash)\n        result = await parent.call_tool(\"child_child_tool\", {})\n        assert \"child result\" in str(result)\n\n        # Check spans: should have parent tool span, delegate span, and child tool span\n        spans = trace_exporter.get_finished_spans()\n        span_names = [s.name for s in spans]\n\n        # Parent server creates \"tools/call child_child_tool\"\n        assert \"tools/call child_child_tool\" in span_names\n        # FastMCPProvider creates \"delegate child_tool\"\n        assert \"delegate child_tool\" in span_names\n        # Child server creates \"tools/call child_tool\"\n        assert \"tools/call child_tool\" in span_names\n\n        # Verify delegate span has correct attributes\n        delegate_span = next(s for s in spans if s.name == \"delegate child_tool\")\n        assert delegate_span.attributes is not None\n        assert delegate_span.attributes[\"fastmcp.provider.type\"] == \"FastMCPProvider\"\n        assert delegate_span.attributes[\"fastmcp.component.key\"] == \"child_tool\"\n\n    async def test_mounted_resource_creates_delegate_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        # Create a child server with a resource\n        child = FastMCP(\"child-server\")\n\n        @child.resource(\"data://config\")\n        def child_config() -> str:\n            return \"config data\"\n\n        # Create parent server and mount child with namespace\n        parent = FastMCP(\"parent-server\")\n        parent.mount(child, namespace=\"child\")\n\n        # Read the resource through the parent (namespace is in the URI path)\n        result = await parent.read_resource(\"data://child/config\")\n        assert \"config data\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        span_names = [s.name for s in spans]\n\n        # Should have delegate span for resource\n        assert any(\n            \"delegate\" in name and \"data://config\" in name for name in span_names\n        )\n\n    async def test_mounted_prompt_creates_delegate_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        # Create a child server with a prompt\n        child = FastMCP(\"child-server\")\n\n        @child.prompt()\n        def child_prompt() -> str:\n            return \"Hello from child!\"\n\n        # Create parent server and mount child with namespace\n        parent = FastMCP(\"parent-server\")\n        parent.mount(child, namespace=\"child\")\n\n        # Render the prompt through the parent (namespace uses underscore)\n        result = await parent.render_prompt(\"child_child_prompt\", {})\n        assert \"Hello from child!\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        span_names = [s.name for s in spans]\n\n        # Should have delegate span for prompt\n        assert \"delegate child_prompt\" in span_names\n\n        # Verify delegate span has correct attributes\n        delegate_span = next(s for s in spans if s.name == \"delegate child_prompt\")\n        assert delegate_span.attributes is not None\n        assert delegate_span.attributes[\"fastmcp.provider.type\"] == \"FastMCPProvider\"\n\n\nclass TestProviderSpanHierarchy:\n    \"\"\"Tests for span parent-child relationships in mounted servers.\"\"\"\n\n    async def test_delegate_span_is_child_of_server_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        # Create nested server structure\n        child = FastMCP(\"child\")\n\n        @child.tool()\n        def greet() -> str:\n            return \"Hello\"\n\n        parent = FastMCP(\"parent\")\n        parent.mount(child, namespace=\"ns\")\n\n        await parent.call_tool(\"ns_greet\", {})\n\n        spans = trace_exporter.get_finished_spans()\n\n        # Find the spans\n        parent_span = next(s for s in spans if s.name == \"tools/call ns_greet\")\n        delegate_span = next(s for s in spans if s.name == \"delegate greet\")\n        child_span = next(s for s in spans if s.name == \"tools/call greet\")\n\n        # Verify parent-child relationships\n        assert delegate_span.parent is not None\n        assert child_span.parent is not None\n        assert delegate_span.parent.span_id == parent_span.context.span_id\n        assert child_span.parent.span_id == delegate_span.context.span_id\n"
  },
  {
    "path": "tests/server/telemetry/test_server_tracing.py",
    "content": "\"\"\"Tests for server-level OpenTelemetry tracing.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import patch\n\nimport pytest\nfrom opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter\nfrom opentelemetry.trace import SpanKind, StatusCode\n\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import NotFoundError, ToolError\nfrom fastmcp.server.auth import AccessToken\n\n\nclass TestToolTracing:\n    async def test_call_tool_creates_span(self, trace_exporter: InMemorySpanExporter):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        result = await mcp.call_tool(\"greet\", {\"name\": \"World\"})\n        assert \"Hello, World!\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"tools/call greet\"\n        assert span.kind == SpanKind.SERVER\n        assert span.attributes is not None\n        # Standard MCP semantic conventions\n        assert span.attributes[\"mcp.method.name\"] == \"tools/call\"\n        # Standard RPC semantic conventions\n        assert span.attributes[\"rpc.system\"] == \"mcp\"\n        assert span.attributes[\"rpc.service\"] == \"test-server\"\n        assert span.attributes[\"rpc.method\"] == \"tools/call\"\n        # FastMCP-specific attributes\n        assert span.attributes[\"fastmcp.server.name\"] == \"test-server\"\n        assert span.attributes[\"fastmcp.component.type\"] == \"tool\"\n        assert span.attributes[\"fastmcp.component.key\"] == \"tool:greet@\"\n\n    async def test_call_tool_with_error_sets_status(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.tool()\n        def failing_tool() -> str:\n            raise ValueError(\"Something went wrong\")\n\n        with pytest.raises(ToolError):\n            await mcp.call_tool(\"failing_tool\", {})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"tools/call failing_tool\"\n        assert span.status.status_code == StatusCode.ERROR\n        assert len(span.events) > 0  # Exception recorded\n\n    async def test_call_nonexistent_tool_sets_error(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        with pytest.raises(NotFoundError):\n            await mcp.call_tool(\"nonexistent\", {})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"tools/call nonexistent\"\n        assert span.status.status_code == StatusCode.ERROR\n\n\nclass TestResourceTracing:\n    async def test_read_resource_creates_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.resource(\"config://app\")\n        def get_config() -> str:\n            return \"app_config_data\"\n\n        result = await mcp.read_resource(\"config://app\")\n        assert \"app_config_data\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"resources/read config://app\"\n        assert span.kind == SpanKind.SERVER\n        assert span.attributes is not None\n        # Standard MCP semantic conventions\n        assert span.attributes[\"mcp.method.name\"] == \"resources/read\"\n        assert span.attributes[\"mcp.resource.uri\"] == \"config://app\"\n        # Standard RPC semantic conventions\n        assert span.attributes[\"rpc.system\"] == \"mcp\"\n        assert span.attributes[\"rpc.service\"] == \"test-server\"\n        assert span.attributes[\"rpc.method\"] == \"resources/read\"\n        # FastMCP-specific attributes\n        assert span.attributes[\"fastmcp.server.name\"] == \"test-server\"\n        assert span.attributes[\"fastmcp.component.type\"] == \"resource\"\n        assert span.attributes[\"fastmcp.component.key\"] == \"resource:config://app@\"\n\n    async def test_read_resource_template_creates_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.resource(\"users://{user_id}/profile\")\n        def get_user_profile(user_id: str) -> str:\n            return f\"profile for {user_id}\"\n\n        result = await mcp.read_resource(\"users://123/profile\")\n        assert \"profile for 123\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"resources/read users://123/profile\"\n        assert span.kind == SpanKind.SERVER\n        assert span.attributes is not None\n        # Standard MCP semantic conventions\n        assert span.attributes[\"mcp.method.name\"] == \"resources/read\"\n        assert span.attributes[\"mcp.resource.uri\"] == \"users://123/profile\"\n        # Standard RPC semantic conventions\n        assert span.attributes[\"rpc.method\"] == \"resources/read\"\n        # Template component type is set by get_span_attributes\n        assert span.attributes[\"fastmcp.component.type\"] == \"resource_template\"\n        assert (\n            span.attributes[\"fastmcp.component.key\"]\n            == \"template:users://{user_id}/profile@\"\n        )\n\n    async def test_read_nonexistent_resource_sets_error(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        with pytest.raises(NotFoundError):\n            await mcp.read_resource(\"nonexistent://resource\")\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"resources/read nonexistent://resource\"\n        assert span.status.status_code == StatusCode.ERROR\n\n\nclass TestPromptTracing:\n    async def test_render_prompt_creates_span(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.prompt()\n        def greeting(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        result = await mcp.render_prompt(\"greeting\", {\"name\": \"World\"})\n        assert \"Hello, World!\" in str(result)\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"prompts/get greeting\"\n        assert span.kind == SpanKind.SERVER\n        assert span.attributes is not None\n        # Standard MCP semantic conventions\n        assert span.attributes[\"mcp.method.name\"] == \"prompts/get\"\n        # Standard RPC semantic conventions\n        assert span.attributes[\"rpc.system\"] == \"mcp\"\n        assert span.attributes[\"rpc.service\"] == \"test-server\"\n        assert span.attributes[\"rpc.method\"] == \"prompts/get\"\n        # FastMCP-specific attributes\n        assert span.attributes[\"fastmcp.server.name\"] == \"test-server\"\n        assert span.attributes[\"fastmcp.component.type\"] == \"prompt\"\n        assert span.attributes[\"fastmcp.component.key\"] == \"prompt:greeting@\"\n\n    async def test_render_nonexistent_prompt_sets_error(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        with pytest.raises(NotFoundError):\n            await mcp.render_prompt(\"nonexistent\", {})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.name == \"prompts/get nonexistent\"\n        assert span.status.status_code == StatusCode.ERROR\n\n\nclass TestAuthAttributesOnSpans:\n    async def test_tool_span_includes_auth_attributes_when_authenticated(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        test_token = AccessToken(\n            token=\"test-token\",\n            client_id=\"test-client-123\",\n            scopes=[\"read\", \"write\"],\n        )\n\n        with patch(\n            \"fastmcp.server.dependencies.get_access_token\", return_value=test_token\n        ):\n            await mcp.call_tool(\"greet\", {\"name\": \"World\"})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.attributes is not None\n        assert span.attributes[\"enduser.id\"] == \"test-client-123\"\n        assert span.attributes[\"enduser.scope\"] == \"read write\"\n\n    async def test_resource_span_includes_auth_attributes_when_authenticated(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.resource(\"config://app\")\n        def get_config() -> str:\n            return \"config_data\"\n\n        test_token = AccessToken(\n            token=\"test-token\",\n            client_id=\"user-456\",\n            scopes=[\"config:read\"],\n        )\n\n        with patch(\n            \"fastmcp.server.dependencies.get_access_token\", return_value=test_token\n        ):\n            await mcp.read_resource(\"config://app\")\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.attributes is not None\n        assert span.attributes[\"enduser.id\"] == \"user-456\"\n        assert span.attributes[\"enduser.scope\"] == \"config:read\"\n\n    async def test_prompt_span_includes_auth_attributes_when_authenticated(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.prompt()\n        def greeting(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        test_token = AccessToken(\n            token=\"test-token\",\n            client_id=\"prompt-user\",\n            scopes=[\"prompts\"],\n        )\n\n        with patch(\n            \"fastmcp.server.dependencies.get_access_token\", return_value=test_token\n        ):\n            await mcp.render_prompt(\"greeting\", {\"name\": \"World\"})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.attributes is not None\n        assert span.attributes[\"enduser.id\"] == \"prompt-user\"\n        assert span.attributes[\"enduser.scope\"] == \"prompts\"\n\n    async def test_span_omits_auth_attributes_when_not_authenticated(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # No mock - get_access_token returns None by default (no auth context)\n        await mcp.call_tool(\"greet\", {\"name\": \"World\"})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.attributes is not None\n        # Auth attributes should not be present\n        assert \"enduser.id\" not in span.attributes\n        assert \"enduser.scope\" not in span.attributes\n\n    async def test_span_omits_scope_when_no_scopes(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        mcp = FastMCP(\"test-server\")\n\n        @mcp.tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        test_token = AccessToken(\n            token=\"test-token\",\n            client_id=\"client-no-scopes\",\n            scopes=[],  # Empty scopes\n        )\n\n        with patch(\n            \"fastmcp.server.dependencies.get_access_token\", return_value=test_token\n        ):\n            await mcp.call_tool(\"greet\", {\"name\": \"World\"})\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n\n        span = spans[0]\n        assert span.attributes is not None\n        assert span.attributes[\"enduser.id\"] == \"client-no-scopes\"\n        # Scope attribute should not be present when scopes list is empty\n        assert \"enduser.scope\" not in span.attributes\n"
  },
  {
    "path": "tests/server/test_app_state.py",
    "content": "from fastmcp.server import FastMCP\nfrom fastmcp.server.http import create_sse_app, create_streamable_http_app\n\n\ndef test_http_app_sets_mcp_server_state():\n    server = FastMCP(name=\"StateTest\")\n    app = server.http_app()\n    assert app.state.fastmcp_server is server\n\n\ndef test_http_app_sse_sets_mcp_server_state():\n    server = FastMCP(name=\"StateTest\")\n    app = server.http_app(transport=\"sse\")\n    assert app.state.fastmcp_server is server\n\n\ndef test_create_streamable_http_app_sets_state():\n    server = FastMCP(name=\"StateTest\")\n    app = create_streamable_http_app(server, \"/mcp/\")\n    assert app.state.fastmcp_server is server\n\n\ndef test_create_sse_app_sets_state():\n    server = FastMCP(name=\"StateTest\")\n    app = create_sse_app(server, message_path=\"/message\", sse_path=\"/sse/\")\n    assert app.state.fastmcp_server is server\n"
  },
  {
    "path": "tests/server/test_auth_integration.py",
    "content": "import base64\nimport hashlib\nimport secrets\nimport time\nimport unittest.mock\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nimport pytest\nfrom mcp.server.auth.provider import (\n    AccessToken,\n    AuthorizationCode,\n    AuthorizationParams,\n    OAuthAuthorizationServerProvider,\n    RefreshToken,\n    construct_redirect_uri,\n)\nfrom mcp.server.auth.routes import (\n    create_auth_routes,\n)\nfrom mcp.server.auth.settings import (\n    ClientRegistrationOptions,\n    RevocationOptions,\n)\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthToken,\n)\nfrom pydantic import AnyHttpUrl\nfrom starlette.applications import Starlette\n\n\n# Mock OAuth provider for testing\nclass MockOAuthProvider(OAuthAuthorizationServerProvider):\n    def __init__(self):\n        self.clients = {}\n        self.auth_codes = {}  # code -> {client_id, code_challenge, redirect_uri}\n        self.tokens = {}  # token -> {client_id, scopes, expires_at}\n        self.refresh_tokens = {}  # refresh_token -> access_token\n\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        return self.clients.get(client_id)\n\n    async def register_client(self, client_info: OAuthClientInformationFull):\n        self.clients[client_info.client_id] = client_info\n\n    async def authorize(\n        self, client: OAuthClientInformationFull, params: AuthorizationParams\n    ) -> str:\n        # toy authorize implementation which just immediately generates an authorization\n        # code and completes the redirect\n        if client.client_id is None:\n            raise ValueError(\"client_id is required\")\n        code = AuthorizationCode(\n            code=f\"code_{int(time.time())}\",\n            client_id=client.client_id,\n            code_challenge=params.code_challenge,\n            redirect_uri=params.redirect_uri,\n            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,\n            expires_at=time.time() + 300,\n            scopes=params.scopes or [\"read\", \"write\"],\n        )\n        self.auth_codes[code.code] = code\n\n        return construct_redirect_uri(\n            str(params.redirect_uri), code=code.code, state=params.state\n        )\n\n    async def load_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: str\n    ) -> AuthorizationCode | None:\n        return self.auth_codes.get(authorization_code)\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode\n    ) -> OAuthToken:\n        assert authorization_code.code in self.auth_codes\n\n        # Generate an access token and refresh token\n        access_token = f\"access_{secrets.token_hex(32)}\"\n        refresh_token = f\"refresh_{secrets.token_hex(32)}\"\n\n        # Store the tokens\n        if client.client_id is None:\n            raise ValueError(\"client_id is required\")\n        self.tokens[access_token] = AccessToken(\n            token=access_token,\n            client_id=client.client_id,\n            scopes=authorization_code.scopes,\n            expires_at=int(time.time()) + 3600,\n        )\n\n        self.refresh_tokens[refresh_token] = access_token\n\n        # Remove the used code\n        del self.auth_codes[authorization_code.code]\n\n        return OAuthToken(\n            access_token=access_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\"read write\",\n            refresh_token=refresh_token,\n        )\n\n    async def load_refresh_token(\n        self, client: OAuthClientInformationFull, refresh_token: str\n    ) -> RefreshToken | None:\n        old_access_token = self.refresh_tokens.get(refresh_token)\n        if old_access_token is None:\n            return None\n        token_info = self.tokens.get(old_access_token)\n        if token_info is None:\n            return None\n\n        # Create a RefreshToken object that matches what is expected in later code\n        refresh_obj = RefreshToken(\n            token=refresh_token,\n            client_id=token_info.client_id,\n            scopes=token_info.scopes,\n            expires_at=token_info.expires_at,\n        )\n\n        return refresh_obj\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshToken,\n        scopes: list[str],\n    ) -> OAuthToken:\n        # Check if refresh token exists\n        assert refresh_token.token in self.refresh_tokens\n\n        old_access_token = self.refresh_tokens[refresh_token.token]\n\n        # Check if the access token exists\n        assert old_access_token in self.tokens\n\n        # Check if the token was issued to this client\n        token_info = self.tokens[old_access_token]\n        assert token_info.client_id == client.client_id\n\n        # Generate a new access token and refresh token\n        new_access_token = f\"access_{secrets.token_hex(32)}\"\n        new_refresh_token = f\"refresh_{secrets.token_hex(32)}\"\n\n        # Store the new tokens\n        if client.client_id is None:\n            raise ValueError(\"client_id is required\")\n        self.tokens[new_access_token] = AccessToken(\n            token=new_access_token,\n            client_id=client.client_id,\n            scopes=scopes or token_info.scopes,\n            expires_at=int(time.time()) + 3600,\n        )\n\n        self.refresh_tokens[new_refresh_token] = new_access_token\n\n        # Remove the old tokens\n        del self.refresh_tokens[refresh_token.token]\n        del self.tokens[old_access_token]\n\n        return OAuthToken(\n            access_token=new_access_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\" \".join(scopes) if scopes else \" \".join(token_info.scopes),\n            refresh_token=new_refresh_token,\n        )\n\n    async def load_access_token(self, token: str) -> AccessToken | None:\n        token_info = self.tokens.get(token)\n\n        # Check if token is expired\n        # if token_info.expires_at < int(time.time()):\n        #     raise InvalidTokenError(\"Access token has expired\")\n\n        return token_info and AccessToken(\n            token=token,\n            client_id=token_info.client_id,\n            scopes=token_info.scopes,\n            expires_at=token_info.expires_at,\n        )\n\n    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:\n        match token:\n            case RefreshToken():\n                # Remove the refresh token\n                del self.refresh_tokens[token.token]\n\n            case AccessToken():\n                # Remove the access token\n                del self.tokens[token.token]\n\n                # Also remove any refresh tokens that point to this access token\n                for refresh_token, access_token in list(self.refresh_tokens.items()):\n                    if access_token == token.token:\n                        del self.refresh_tokens[refresh_token]\n\n\n@pytest.fixture\ndef mock_oauth_provider():\n    return MockOAuthProvider()\n\n\n@pytest.fixture\ndef auth_app(mock_oauth_provider):\n    # Create auth router\n    auth_routes = create_auth_routes(\n        mock_oauth_provider,\n        AnyHttpUrl(\"https://auth.example.com\"),\n        AnyHttpUrl(\"https://docs.example.com\"),\n        client_registration_options=ClientRegistrationOptions(\n            enabled=True,\n            valid_scopes=[\"read\", \"write\", \"profile\"],\n            default_scopes=[\"read\", \"write\"],\n        ),\n        revocation_options=RevocationOptions(enabled=True),\n    )\n\n    # Create Starlette app\n    app = Starlette(routes=auth_routes)\n\n    return app\n\n\n@pytest.fixture\nasync def test_client(auth_app):\n    async with httpx.AsyncClient(\n        transport=httpx.ASGITransport(app=auth_app), base_url=\"https://mcptest.com\"\n    ) as client:\n        yield client\n\n\n@pytest.fixture\nasync def registered_client(test_client: httpx.AsyncClient, request):\n    \"\"\"Create and register a test client.\n\n    Parameters can be customized via indirect parameterization:\n    @pytest.mark.parametrize(\"registered_client\",\n                            [{\"grant_types\": [\"authorization_code\"]}],\n                            indirect=True)\n    \"\"\"\n    # Default client metadata\n    client_metadata = {\n        \"redirect_uris\": [\"https://client.example.com/callback\"],\n        \"client_name\": \"Test Client\",\n        \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n    }\n\n    # Override with any parameters from the test\n    if hasattr(request, \"param\") and request.param:\n        client_metadata.update(request.param)\n\n    response = await test_client.post(\"/register\", json=client_metadata)\n    assert response.status_code == 201, f\"Failed to register client: {response.content}\"\n\n    client_info = response.json()\n    return client_info\n\n\n@pytest.fixture\ndef pkce_challenge():\n    \"\"\"Create a PKCE challenge with code_verifier and code_challenge.\"\"\"\n    code_verifier = \"some_random_verifier_string\"\n    code_challenge = (\n        base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())\n        .decode()\n        .rstrip(\"=\")\n    )\n\n    return {\"code_verifier\": code_verifier, \"code_challenge\": code_challenge}\n\n\n@pytest.fixture\nasync def auth_code(test_client, registered_client, pkce_challenge, request):\n    \"\"\"Get an authorization code.\n\n    Parameters can be customized via indirect parameterization:\n    @pytest.mark.parametrize(\"auth_code\",\n                            [{\"redirect_uri\": \"https://client.example.com/other-callback\"}],\n                            indirect=True)\n    \"\"\"\n    # Default authorize params\n    auth_params = {\n        \"response_type\": \"code\",\n        \"client_id\": registered_client[\"client_id\"],\n        \"redirect_uri\": \"https://client.example.com/callback\",\n        \"code_challenge\": pkce_challenge[\"code_challenge\"],\n        \"code_challenge_method\": \"S256\",\n        \"state\": \"test_state\",\n    }\n\n    # Override with any parameters from the test\n    if hasattr(request, \"param\") and request.param:\n        auth_params.update(request.param)\n\n    response = await test_client.get(\"/authorize\", params=auth_params)\n    assert response.status_code == 302, f\"Failed to get auth code: {response.content}\"\n\n    # Extract the authorization code\n    redirect_url = response.headers[\"location\"]\n    parsed_url = urlparse(redirect_url)\n    query_params = parse_qs(parsed_url.query)\n\n    assert \"code\" in query_params, f\"No code in response: {query_params}\"\n    auth_code = query_params[\"code\"][0]\n\n    return {\n        \"code\": auth_code,\n        \"redirect_uri\": auth_params[\"redirect_uri\"],\n        \"state\": query_params.get(\"state\", [None])[0],\n    }\n\n\n@pytest.fixture\nasync def tokens(test_client, registered_client, auth_code, pkce_challenge, request):\n    \"\"\"Exchange authorization code for tokens.\n\n    Parameters can be customized via indirect parameterization:\n    @pytest.mark.parametrize(\"tokens\",\n                            [{\"code_verifier\": \"wrong_verifier\"}],\n                            indirect=True)\n    \"\"\"\n    # Default token request params\n    token_params = {\n        \"grant_type\": \"authorization_code\",\n        \"client_id\": registered_client[\"client_id\"],\n        \"client_secret\": registered_client[\"client_secret\"],\n        \"code\": auth_code[\"code\"],\n        \"code_verifier\": pkce_challenge[\"code_verifier\"],\n        \"redirect_uri\": auth_code[\"redirect_uri\"],\n    }\n\n    # Override with any parameters from the test\n    if hasattr(request, \"param\") and request.param:\n        token_params.update(request.param)\n\n    response = await test_client.post(\"/token\", data=token_params)\n\n    # Don't assert success here since some tests will intentionally cause errors\n    return {\n        \"response\": response,\n        \"params\": token_params,\n    }\n\n\nclass TestAuthEndpoints:\n    async def test_metadata_endpoint(self, test_client: httpx.AsyncClient):\n        \"\"\"Test the OAuth 2.1 metadata endpoint.\"\"\"\n        print(\"Sending request to metadata endpoint\")\n        response = await test_client.get(\"/.well-known/oauth-authorization-server\")\n        print(f\"Got response: {response.status_code}\")\n        if response.status_code != 200:\n            print(f\"Response content: {response.content}\")\n        assert response.status_code == 200\n\n        metadata = response.json()\n        assert metadata[\"issuer\"] == \"https://auth.example.com/\"\n        assert (\n            metadata[\"authorization_endpoint\"] == \"https://auth.example.com/authorize\"\n        )\n        assert metadata[\"token_endpoint\"] == \"https://auth.example.com/token\"\n        assert metadata[\"registration_endpoint\"] == \"https://auth.example.com/register\"\n        assert metadata[\"revocation_endpoint\"] == \"https://auth.example.com/revoke\"\n        assert metadata[\"response_types_supported\"] == [\"code\"]\n        assert metadata[\"code_challenge_methods_supported\"] == [\"S256\"]\n        assert set(metadata[\"token_endpoint_auth_methods_supported\"]) == {\n            \"client_secret_post\",\n            \"client_secret_basic\",\n        }\n        assert metadata[\"grant_types_supported\"] == [\n            \"authorization_code\",\n            \"refresh_token\",\n        ]\n        assert metadata[\"service_documentation\"] == \"https://docs.example.com/\"\n\n    async def test_token_validation_error(self, test_client: httpx.AsyncClient):\n        \"\"\"Test token endpoint error - missing client_id returns auth error.\"\"\"\n        # Missing required fields - SDK validates client_id first\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                # Missing code, code_verifier, client_id, etc.\n            },\n        )\n        error_response = response.json()\n        # SDK validates client_id before other fields, returning unauthorized_client\n        # (FastMCP's OAuthProxy transforms this to invalid_client, but this test\n        # uses the SDK's create_auth_routes directly)\n        assert error_response[\"error\"] == \"unauthorized_client\"\n        assert \"error_description\" in error_response\n\n    async def test_token_invalid_auth_code(\n        self, test_client, registered_client, pkce_challenge\n    ):\n        \"\"\"Test token endpoint error - authorization code does not exist.\"\"\"\n        # Try to use a non-existent authorization code\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": \"non_existent_auth_code\",\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        print(f\"Status code: {response.status_code}\")\n        print(f\"Response body: {response.content}\")\n        print(f\"Response JSON: {response.json()}\")\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_grant\"\n        assert (\n            \"authorization code does not exist\" in error_response[\"error_description\"]\n        )\n\n    async def test_token_expired_auth_code(\n        self,\n        test_client,\n        registered_client,\n        auth_code,\n        pkce_challenge,\n        mock_oauth_provider,\n    ):\n        \"\"\"Test token endpoint error - authorization code has expired.\"\"\"\n        # Get the current time for our time mocking\n        current_time = time.time()\n\n        # Find the auth code object\n        code_value = auth_code[\"code\"]\n        found_code = None\n        for code_obj in mock_oauth_provider.auth_codes.values():\n            if code_obj.code == code_value:\n                found_code = code_obj\n                break\n\n        assert found_code is not None\n\n        # Authorization codes are typically short-lived (5 minutes = 300 seconds)\n        # So we'll mock time to be 10 minutes (600 seconds) in the future\n        with unittest.mock.patch(\"time.time\", return_value=current_time + 600):\n            # Try to use the expired authorization code\n            response = await test_client.post(\n                \"/token\",\n                data={\n                    \"grant_type\": \"authorization_code\",\n                    \"client_id\": registered_client[\"client_id\"],\n                    \"client_secret\": registered_client[\"client_secret\"],\n                    \"code\": code_value,\n                    \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                    \"redirect_uri\": auth_code[\"redirect_uri\"],\n                },\n            )\n            assert response.status_code == 400\n            error_response = response.json()\n            assert error_response[\"error\"] == \"invalid_grant\"\n            assert (\n                \"authorization code has expired\" in error_response[\"error_description\"]\n            )\n\n    @pytest.mark.parametrize(\n        \"registered_client\",\n        [\n            {\n                \"redirect_uris\": [\n                    \"https://client.example.com/callback\",\n                    \"https://client.example.com/other-callback\",\n                ]\n            }\n        ],\n        indirect=True,\n    )\n    async def test_token_redirect_uri_mismatch(\n        self, test_client, registered_client, auth_code, pkce_challenge\n    ):\n        \"\"\"Test token endpoint error - redirect URI mismatch.\"\"\"\n        # Try to use the code with a different redirect URI\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                # Different from the one used in /authorize\n                \"redirect_uri\": \"https://client.example.com/other-callback\",\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_request\"\n        assert \"redirect_uri did not match\" in error_response[\"error_description\"]\n\n    async def test_token_code_verifier_mismatch(\n        self, test_client, registered_client, auth_code\n    ):\n        \"\"\"Test token endpoint error - PKCE code verifier mismatch.\"\"\"\n        # Try to use the code with an incorrect code verifier\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                # Different from the one used to create challenge\n                \"code_verifier\": \"incorrect_code_verifier\",\n                \"redirect_uri\": auth_code[\"redirect_uri\"],\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_grant\"\n        assert \"incorrect code_verifier\" in error_response[\"error_description\"]\n\n    async def test_token_invalid_refresh_token(self, test_client, registered_client):\n        \"\"\"Test token endpoint error - refresh token does not exist.\"\"\"\n        # Try to use a non-existent refresh token\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"refresh_token\": \"non_existent_refresh_token\",\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_grant\"\n        assert \"refresh token does not exist\" in error_response[\"error_description\"]\n\n    async def test_token_expired_refresh_token(\n        self,\n        test_client,\n        registered_client,\n        auth_code,\n        pkce_challenge,\n        mock_oauth_provider,\n    ):\n        \"\"\"Test token endpoint error - refresh token has expired.\"\"\"\n        # Step 1: First, let's create a token and refresh token at the current time\n        current_time = time.time()\n\n        # Exchange authorization code for tokens normally\n        token_response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": auth_code[\"redirect_uri\"],\n            },\n        )\n        assert token_response.status_code == 200\n        tokens = token_response.json()\n        refresh_token = tokens[\"refresh_token\"]\n\n        # Step 2: Time travel forward 4 hours (tokens expire in 1 hour by default)\n        # Mock the time.time() function to return a value 4 hours in the future\n        with unittest.mock.patch(\n            \"time.time\", return_value=current_time + 14400\n        ):  # 4 hours = 14400 seconds\n            # Try to use the refresh token which should now be considered expired\n            response = await test_client.post(\n                \"/token\",\n                data={\n                    \"grant_type\": \"refresh_token\",\n                    \"client_id\": registered_client[\"client_id\"],\n                    \"client_secret\": registered_client[\"client_secret\"],\n                    \"refresh_token\": refresh_token,\n                },\n            )\n\n            # In the \"future\", the token should be considered expired\n            assert response.status_code == 400\n            error_response = response.json()\n            assert error_response[\"error\"] == \"invalid_grant\"\n            assert \"refresh token has expired\" in error_response[\"error_description\"]\n\n    async def test_token_invalid_scope(\n        self, test_client, registered_client, auth_code, pkce_challenge\n    ):\n        \"\"\"Test token endpoint error - invalid scope in refresh token request.\"\"\"\n        # Exchange authorization code for tokens\n        token_response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": auth_code[\"redirect_uri\"],\n            },\n        )\n        assert token_response.status_code == 200\n\n        tokens = token_response.json()\n        refresh_token = tokens[\"refresh_token\"]\n\n        # Try to use refresh token with an invalid scope\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"refresh_token\": refresh_token,\n                \"scope\": \"read write invalid_scope\",  # Adding an invalid scope\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_scope\"\n        assert \"cannot request scope\" in error_response[\"error_description\"]\n\n    async def test_client_registration(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider\n    ):\n        \"\"\"Test client registration.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"client_uri\": \"https://client.example.com\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201, response.content\n\n        client_info = response.json()\n        assert \"client_id\" in client_info\n        assert \"client_secret\" in client_info\n        assert client_info[\"client_name\"] == \"Test Client\"\n        assert client_info[\"redirect_uris\"] == [\"https://client.example.com/callback\"]\n\n        # Verify that the client was registered\n        # assert await mock_oauth_provider.clients_store.get_client(\n        #     client_info[\"client_id\"]\n        # ) is not None\n\n    async def test_client_registration_missing_required_fields(\n        self, test_client: httpx.AsyncClient\n    ):\n        \"\"\"Test client registration with missing required fields.\"\"\"\n        # Missing redirect_uris which is a required field\n        client_metadata = {\n            \"client_name\": \"Test Client\",\n            \"client_uri\": \"https://client.example.com\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert error_data[\"error_description\"] == \"redirect_uris: Field required\"\n\n    async def test_client_registration_invalid_uri(\n        self, test_client: httpx.AsyncClient\n    ):\n        \"\"\"Test client registration with invalid URIs.\"\"\"\n        # Invalid redirect_uri format\n        client_metadata = {\n            \"redirect_uris\": [\"not-a-valid-uri\"],\n            \"client_name\": \"Test Client\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert error_data[\"error_description\"] == (\n            \"redirect_uris.0: Input should be a valid URL, relative URL without a base\"\n        )\n\n    async def test_client_registration_empty_redirect_uris(\n        self, test_client: httpx.AsyncClient\n    ):\n        \"\"\"Test client registration with empty redirect_uris array.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [],  # Empty array\n            \"client_name\": \"Test Client\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert (\n            error_data[\"error_description\"]\n            == \"redirect_uris: List should have at least 1 item after validation, not 0\"\n        )\n\n    async def test_authorize_form_post(\n        self,\n        test_client: httpx.AsyncClient,\n        mock_oauth_provider: MockOAuthProvider,\n        pkce_challenge,\n    ):\n        \"\"\"Test the authorization endpoint using POST with form-encoded data.\"\"\"\n        # Register a client\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # Use POST with form-encoded data for authorization\n        response = await test_client.post(\n            \"/authorize\",\n            data={\n                \"response_type\": \"code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_form_state\",\n            },\n        )\n        assert response.status_code == 302\n\n        # Extract the authorization code from the redirect URL\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"code\" in query_params\n        assert query_params[\"state\"][0] == \"test_form_state\"\n\n    async def test_authorization_get(\n        self,\n        test_client: httpx.AsyncClient,\n        mock_oauth_provider: MockOAuthProvider,\n        pkce_challenge,\n    ):\n        \"\"\"Test the full authorization flow.\"\"\"\n        # 1. Register a client\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # 2. Request authorization using GET with query params\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n        assert response.status_code == 302\n\n        # 3. Extract the authorization code from the redirect URL\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"code\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n        auth_code = query_params[\"code\"][0]\n\n        # 4. Exchange the authorization code for tokens\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 200\n\n        token_response = response.json()\n        assert \"access_token\" in token_response\n        assert \"token_type\" in token_response\n        assert \"refresh_token\" in token_response\n        assert \"expires_in\" in token_response\n        assert token_response[\"token_type\"] == \"Bearer\"\n\n        # 5. Verify the access token\n        access_token = token_response[\"access_token\"]\n        refresh_token = token_response[\"refresh_token\"]\n\n        # Create a test client with the token\n        auth_info = await mock_oauth_provider.load_access_token(access_token)\n        assert auth_info\n        assert auth_info.client_id == client_info[\"client_id\"]\n        assert \"read\" in auth_info.scopes\n        assert \"write\" in auth_info.scopes\n\n        # 6. Refresh the token\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],\n                \"refresh_token\": refresh_token,\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 200\n\n        new_token_response = response.json()\n        assert \"access_token\" in new_token_response\n        assert \"refresh_token\" in new_token_response\n        assert new_token_response[\"access_token\"] != access_token\n        assert new_token_response[\"refresh_token\"] != refresh_token\n\n        # 7. Revoke the token\n        response = await test_client.post(\n            \"/revoke\",\n            data={\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],\n                \"token\": new_token_response[\"access_token\"],\n            },\n        )\n        assert response.status_code == 200\n\n        # Verify that the token was revoked\n        assert (\n            await mock_oauth_provider.load_access_token(\n                new_token_response[\"access_token\"]\n            )\n            is None\n        )\n\n    async def test_revoke_invalid_token(self, test_client, registered_client):\n        \"\"\"Test revoking an invalid token.\"\"\"\n        response = await test_client.post(\n            \"/revoke\",\n            data={\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"token\": \"invalid_token\",\n            },\n        )\n        # per RFC, this should return 200 even if the token is invalid\n        assert response.status_code == 200\n\n    async def test_revoke_with_malformed_token(self, test_client, registered_client):\n        response = await test_client.post(\n            \"/revoke\",\n            data={\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"token\": 123,\n                \"token_type_hint\": \"asdf\",\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_request\"\n        assert \"token_type_hint\" in error_response[\"error_description\"]\n\n    async def test_client_registration_disallowed_scopes(\n        self, test_client: httpx.AsyncClient\n    ):\n        \"\"\"Test client registration with scopes that are not allowed.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"scope\": \"read write profile admin\",  # 'admin' is not in valid_scopes\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert \"scope\" in error_data[\"error_description\"]\n        assert \"admin\" in error_data[\"error_description\"]\n\n    async def test_client_registration_default_scopes(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider\n    ):\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            # No scope specified\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # Verify client was registered successfully\n        assert client_info[\"scope\"] == \"read write\"\n\n        # Retrieve the client from the store to verify default scopes\n        registered_client = await mock_oauth_provider.get_client(\n            client_info[\"client_id\"]\n        )\n        assert registered_client is not None\n\n        # Check that default scopes were applied\n        assert registered_client.scope == \"read write\"\n\n    async def test_client_registration_invalid_grant_type(\n        self, test_client: httpx.AsyncClient\n    ):\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\"],\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert (\n            error_data[\"error_description\"]\n            == \"grant_types must be authorization_code and refresh_token\"\n        )\n"
  },
  {
    "path": "tests/server/test_auth_integration_errors.py",
    "content": "import base64\nimport hashlib\nimport secrets\nimport time\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nimport pytest\nfrom mcp.server.auth.provider import (\n    AccessToken,\n    AuthorizationCode,\n    AuthorizationParams,\n    OAuthAuthorizationServerProvider,\n    RefreshToken,\n    construct_redirect_uri,\n)\nfrom mcp.server.auth.routes import (\n    create_auth_routes,\n)\nfrom mcp.server.auth.settings import (\n    ClientRegistrationOptions,\n    RevocationOptions,\n)\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthToken,\n)\nfrom pydantic import AnyHttpUrl\nfrom starlette.applications import Starlette\n\n\n# Mock OAuth provider for testing\nclass MockOAuthProvider(OAuthAuthorizationServerProvider):\n    def __init__(self):\n        self.clients = {}\n        self.auth_codes = {}  # code -> {client_id, code_challenge, redirect_uri}\n        self.tokens = {}  # token -> {client_id, scopes, expires_at}\n        self.refresh_tokens = {}  # refresh_token -> access_token\n\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        return self.clients.get(client_id)\n\n    async def register_client(self, client_info: OAuthClientInformationFull):\n        self.clients[client_info.client_id] = client_info\n\n    async def authorize(\n        self, client: OAuthClientInformationFull, params: AuthorizationParams\n    ) -> str:\n        # toy authorize implementation which just immediately generates an authorization\n        # code and completes the redirect\n        if client.client_id is None:\n            raise ValueError(\"client_id is required\")\n        code = AuthorizationCode(\n            code=f\"code_{int(time.time())}\",\n            client_id=client.client_id,\n            code_challenge=params.code_challenge,\n            redirect_uri=params.redirect_uri,\n            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,\n            expires_at=time.time() + 300,\n            scopes=params.scopes or [\"read\", \"write\"],\n        )\n        self.auth_codes[code.code] = code\n\n        return construct_redirect_uri(\n            str(params.redirect_uri), code=code.code, state=params.state\n        )\n\n    async def load_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: str\n    ) -> AuthorizationCode | None:\n        return self.auth_codes.get(authorization_code)\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode\n    ) -> OAuthToken:\n        assert authorization_code.code in self.auth_codes\n\n        # Generate an access token and refresh token\n        access_token = f\"access_{secrets.token_hex(32)}\"\n        refresh_token = f\"refresh_{secrets.token_hex(32)}\"\n\n        # Store the tokens\n        if client.client_id is None:\n            raise ValueError(\"client_id is required\")\n        self.tokens[access_token] = AccessToken(\n            token=access_token,\n            client_id=client.client_id,\n            scopes=authorization_code.scopes,\n            expires_at=int(time.time()) + 3600,\n        )\n\n        self.refresh_tokens[refresh_token] = access_token\n\n        # Remove the used code\n        del self.auth_codes[authorization_code.code]\n\n        return OAuthToken(\n            access_token=access_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\"read write\",\n            refresh_token=refresh_token,\n        )\n\n    async def load_refresh_token(\n        self, client: OAuthClientInformationFull, refresh_token: str\n    ) -> RefreshToken | None:\n        old_access_token = self.refresh_tokens.get(refresh_token)\n        if old_access_token is None:\n            return None\n        token_info = self.tokens.get(old_access_token)\n        if token_info is None:\n            return None\n\n        # Create a RefreshToken object that matches what is expected in later code\n        refresh_obj = RefreshToken(\n            token=refresh_token,\n            client_id=token_info.client_id,\n            scopes=token_info.scopes,\n            expires_at=token_info.expires_at,\n        )\n\n        return refresh_obj\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshToken,\n        scopes: list[str],\n    ) -> OAuthToken:\n        # Check if refresh token exists\n        assert refresh_token.token in self.refresh_tokens\n\n        old_access_token = self.refresh_tokens[refresh_token.token]\n\n        # Check if the access token exists\n        assert old_access_token in self.tokens\n\n        # Check if the token was issued to this client\n        token_info = self.tokens[old_access_token]\n        assert token_info.client_id == client.client_id\n\n        # Generate a new access token and refresh token\n        new_access_token = f\"access_{secrets.token_hex(32)}\"\n        new_refresh_token = f\"refresh_{secrets.token_hex(32)}\"\n\n        # Store the new tokens\n        if client.client_id is None:\n            raise ValueError(\"client_id is required\")\n        self.tokens[new_access_token] = AccessToken(\n            token=new_access_token,\n            client_id=client.client_id,\n            scopes=scopes or token_info.scopes,\n            expires_at=int(time.time()) + 3600,\n        )\n\n        self.refresh_tokens[new_refresh_token] = new_access_token\n\n        # Remove the old tokens\n        del self.refresh_tokens[refresh_token.token]\n        del self.tokens[old_access_token]\n\n        return OAuthToken(\n            access_token=new_access_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\" \".join(scopes) if scopes else \" \".join(token_info.scopes),\n            refresh_token=new_refresh_token,\n        )\n\n    async def load_access_token(self, token: str) -> AccessToken | None:\n        token_info = self.tokens.get(token)\n\n        return token_info and AccessToken(\n            token=token,\n            client_id=token_info.client_id,\n            scopes=token_info.scopes,\n            expires_at=token_info.expires_at,\n        )\n\n    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:\n        match token:\n            case RefreshToken():\n                # Remove the refresh token\n                del self.refresh_tokens[token.token]\n\n            case AccessToken():\n                # Remove the access token\n                del self.tokens[token.token]\n\n                # Also remove any refresh tokens that point to this access token\n                for refresh_token, access_token in list(self.refresh_tokens.items()):\n                    if access_token == token.token:\n                        del self.refresh_tokens[refresh_token]\n\n\n@pytest.fixture\ndef mock_oauth_provider():\n    return MockOAuthProvider()\n\n\n@pytest.fixture\ndef auth_app(mock_oauth_provider):\n    # Create auth router\n    auth_routes = create_auth_routes(\n        mock_oauth_provider,\n        AnyHttpUrl(\"https://auth.example.com\"),\n        AnyHttpUrl(\"https://docs.example.com\"),\n        client_registration_options=ClientRegistrationOptions(\n            enabled=True,\n            valid_scopes=[\"read\", \"write\", \"profile\"],\n            default_scopes=[\"read\", \"write\"],\n        ),\n        revocation_options=RevocationOptions(enabled=True),\n    )\n\n    # Create Starlette app\n    app = Starlette(routes=auth_routes)\n\n    return app\n\n\n@pytest.fixture\nasync def test_client(auth_app):\n    async with httpx.AsyncClient(\n        transport=httpx.ASGITransport(app=auth_app), base_url=\"https://mcptest.com\"\n    ) as client:\n        yield client\n\n\n@pytest.fixture\nasync def registered_client(test_client: httpx.AsyncClient, request):\n    \"\"\"Create and register a test client.\n\n    Parameters can be customized via indirect parameterization:\n    @pytest.mark.parametrize(\"registered_client\",\n                            [{\"grant_types\": [\"authorization_code\"]}],\n                            indirect=True)\n    \"\"\"\n    # Default client metadata\n    client_metadata = {\n        \"redirect_uris\": [\"https://client.example.com/callback\"],\n        \"client_name\": \"Test Client\",\n        \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n    }\n\n    # Override with any parameters from the test\n    if hasattr(request, \"param\") and request.param:\n        client_metadata.update(request.param)\n\n    response = await test_client.post(\"/register\", json=client_metadata)\n    assert response.status_code == 201, f\"Failed to register client: {response.content}\"\n\n    client_info = response.json()\n    return client_info\n\n\n@pytest.fixture\ndef pkce_challenge():\n    \"\"\"Create a PKCE challenge with code_verifier and code_challenge.\"\"\"\n    code_verifier = \"some_random_verifier_string\"\n    code_challenge = (\n        base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())\n        .decode()\n        .rstrip(\"=\")\n    )\n\n    return {\"code_verifier\": code_verifier, \"code_challenge\": code_challenge}\n\n\nclass TestAuthorizeEndpointErrors:\n    \"\"\"Test error handling in the OAuth authorization endpoint.\"\"\"\n\n    async def test_authorize_missing_client_id(\n        self, test_client: httpx.AsyncClient, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with missing client_id.\n\n        According to the OAuth2.0 spec, if client_id is missing, the server should\n        inform the resource owner and NOT redirect.\n        \"\"\"\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                # Missing client_id\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"state\": \"test_state\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n            },\n        )\n\n        # Should NOT redirect, should show an error page\n        assert response.status_code == 400\n        # The response should include an error message about missing client_id\n        assert \"client_id\" in response.text.lower()\n\n    async def test_authorize_invalid_client_id(\n        self, test_client: httpx.AsyncClient, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with invalid client_id.\n\n        According to the OAuth2.0 spec, if client_id is invalid, the server should\n        inform the resource owner and NOT redirect.\n        \"\"\"\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": \"invalid_client_id_that_does_not_exist\",\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"state\": \"test_state\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n            },\n        )\n\n        # Should NOT redirect, should show an error page\n        assert response.status_code == 400\n        # The response should include an error message about invalid client_id\n        assert \"client\" in response.text.lower()\n\n    async def test_authorize_missing_redirect_uri(\n        self, test_client: httpx.AsyncClient, registered_client, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with missing redirect_uri.\n\n        If client has only one registered redirect_uri, it can be omitted.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Missing redirect_uri\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect to the registered redirect_uri\n        assert response.status_code == 302, response.content\n        redirect_url = response.headers[\"location\"]\n        assert redirect_url.startswith(\"https://client.example.com/callback\")\n\n    async def test_authorize_invalid_redirect_uri(\n        self, test_client: httpx.AsyncClient, registered_client, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with invalid redirect_uri.\n\n        According to the OAuth2.0 spec, if redirect_uri is invalid or doesn't match,\n        the server should inform the resource owner and NOT redirect.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Non-matching URI\n                \"redirect_uri\": \"https://attacker.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should NOT redirect, should show an error page\n        assert response.status_code == 400, response.content\n        # The response should include an error message about redirect_uri mismatch\n        assert \"redirect\" in response.text.lower()\n\n    @pytest.mark.parametrize(\n        \"registered_client\",\n        [\n            {\n                \"redirect_uris\": [\n                    \"https://client.example.com/callback\",\n                    \"https://client.example.com/other-callback\",\n                ]\n            }\n        ],\n        indirect=True,\n    )\n    async def test_authorize_missing_redirect_uri_multiple_registered(\n        self, test_client: httpx.AsyncClient, registered_client, pkce_challenge\n    ):\n        \"\"\"Test endpoint with missing redirect_uri with multiple registered URIs.\n\n        If client has multiple registered redirect_uris, redirect_uri must be provided.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Missing redirect_uri\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should NOT redirect, should return a 400 error\n        assert response.status_code == 400\n        # The response should include an error message about missing redirect_uri\n        assert \"redirect_uri\" in response.text.lower()\n\n    async def test_authorize_unsupported_response_type(\n        self, test_client: httpx.AsyncClient, registered_client, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with unsupported response_type.\n\n        According to the OAuth2.0 spec, for other errors like unsupported_response_type,\n        the server should redirect with error parameters.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"token\",  # Unsupported (we only support \"code\")\n                \"client_id\": registered_client[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"unsupported_response_type\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n    async def test_authorize_missing_response_type(\n        self, test_client: httpx.AsyncClient, registered_client, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with missing response_type.\n\n        Missing required parameter should result in invalid_request error.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                # Missing response_type\n                \"client_id\": registered_client[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"invalid_request\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n    async def test_authorize_missing_pkce_challenge(\n        self, test_client: httpx.AsyncClient, registered_client\n    ):\n        \"\"\"Test authorization endpoint with missing PKCE code_challenge.\n\n        Missing PKCE parameters should result in invalid_request error.\n        \"\"\"\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Missing code_challenge\n                \"state\": \"test_state\",\n                # using default URL\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"invalid_request\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n    async def test_authorize_invalid_scope(\n        self, test_client: httpx.AsyncClient, registered_client, pkce_challenge\n    ):\n        \"\"\"Test authorization endpoint with invalid scope.\n\n        Invalid scope should redirect with invalid_scope error.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"scope\": \"invalid_scope_that_does_not_exist\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"invalid_scope\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n"
  },
  {
    "path": "tests/server/test_context.py",
    "content": "from typing import Any, cast\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom mcp.types import ModelPreferences\n\nfrom fastmcp.server.context import (\n    Context,\n    reset_transport,\n    set_transport,\n)\nfrom fastmcp.server.sampling.run import _parse_model_preferences\nfrom fastmcp.server.server import FastMCP\n\n\n@pytest.fixture\ndef context():\n    return Context(fastmcp=FastMCP())\n\n\nclass TestParseModelPreferences:\n    def test_parse_model_preferences_string(self, context):\n        mp = _parse_model_preferences(\"claude-haiku-4-5\")\n        assert isinstance(mp, ModelPreferences)\n        assert mp.hints is not None\n        assert mp.hints[0].name == \"claude-haiku-4-5\"\n\n    def test_parse_model_preferences_list(self, context):\n        mp = _parse_model_preferences([\"claude-haiku-4-5\", \"claude\"])\n        assert isinstance(mp, ModelPreferences)\n        assert mp.hints is not None\n        assert [h.name for h in mp.hints] == [\"claude-haiku-4-5\", \"claude\"]\n\n    def test_parse_model_preferences_object(self, context):\n        obj = ModelPreferences(hints=[])\n        assert _parse_model_preferences(obj) is obj\n\n    def test_parse_model_preferences_invalid_type(self, context):\n        with pytest.raises(ValueError):\n            _parse_model_preferences(model_preferences=123)  # pyright: ignore[reportArgumentType] # type: ignore[invalid-argument-type]\n\n\nclass TestSessionId:\n    def test_session_id_with_http_headers(self, context):\n        \"\"\"Test that session_id returns the value from mcp-session-id header.\"\"\"\n        from mcp.server.lowlevel.server import request_ctx\n        from mcp.shared.context import RequestContext\n\n        mock_headers = {\"mcp-session-id\": \"test-session-123\"}\n\n        token = request_ctx.set(\n            RequestContext(\n                request_id=0,\n                meta=None,\n                session=MagicMock(wraps={}),\n                lifespan_context=MagicMock(),\n                request=MagicMock(headers=mock_headers),\n            )\n        )\n\n        try:\n            assert context.session_id == \"test-session-123\"\n        finally:\n            request_ctx.reset(token)\n\n    def test_session_id_without_http_headers(self, context):\n        \"\"\"Test that session_id returns a UUID when no HTTP headers are available.\n\n        For STDIO/SSE/in-memory transports, we generate a UUID and cache it\n        on the session for consistency with state operations.\n        \"\"\"\n        import uuid\n\n        from mcp.server.lowlevel.server import request_ctx\n        from mcp.shared.context import RequestContext\n\n        mock_session = MagicMock(wraps={})\n        token = request_ctx.set(\n            RequestContext(\n                request_id=0,\n                meta=None,\n                session=mock_session,\n                lifespan_context=MagicMock(),\n            )\n        )\n\n        try:\n            # session_id should be a valid UUID for non-HTTP transports\n            session_id = context.session_id\n            assert uuid.UUID(session_id)  # Valid UUID format\n            # Should be cached on session\n            assert mock_session._fastmcp_state_prefix == session_id\n        finally:\n            request_ctx.reset(token)\n\n\nclass TestContextState:\n    \"\"\"Test suite for Context state functionality.\"\"\"\n\n    async def test_context_state_basic(self):\n        \"\"\"Test basic get/set/delete state operations.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()  # Use same session for consistent id()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            # Initially empty\n            assert await context.get_state(\"test1\") is None\n            assert await context.get_state(\"test2\") is None\n\n            # Set values\n            await context.set_state(\"test1\", \"value\")\n            await context.set_state(\"test2\", 2)\n\n            # Retrieve values\n            assert await context.get_state(\"test1\") == \"value\"\n            assert await context.get_state(\"test2\") == 2\n\n            # Update value\n            await context.set_state(\"test1\", \"new_value\")\n            assert await context.get_state(\"test1\") == \"new_value\"\n\n            # Delete value\n            await context.delete_state(\"test1\")\n            assert await context.get_state(\"test1\") is None\n\n    async def test_context_state_session_isolation(self):\n        \"\"\"Test that different sessions have isolated state.\"\"\"\n        server = FastMCP(\"test\")\n        session_a = MagicMock()\n        session_b = MagicMock()\n\n        async with Context(fastmcp=server, session=session_a) as context1:\n            await context1.set_state(\"key\", \"value-from-A\")\n\n        async with Context(fastmcp=server, session=session_b) as context2:\n            # Session B should not see session A's state\n            assert await context2.get_state(\"key\") is None\n            await context2.set_state(\"key\", \"value-from-B\")\n            assert await context2.get_state(\"key\") == \"value-from-B\"\n\n        # Verify session A's state is still intact\n        async with Context(fastmcp=server, session=session_a) as context3:\n            assert await context3.get_state(\"key\") == \"value-from-A\"\n\n    async def test_context_state_persists_across_requests(self):\n        \"\"\"Test that state persists across multiple context instances (requests).\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()  # Same session = same id()\n\n        # First request sets state\n        async with Context(fastmcp=server, session=mock_session) as context1:\n            await context1.set_state(\"counter\", 1)\n\n        # Second request in same session sees the state\n        async with Context(fastmcp=server, session=mock_session) as context2:\n            counter = await context2.get_state(\"counter\")\n            assert counter == 1\n            await context2.set_state(\"counter\", counter + 1)\n\n        # Third request sees updated state\n        async with Context(fastmcp=server, session=mock_session) as context3:\n            assert await context3.get_state(\"counter\") == 2\n\n    async def test_context_state_nested_contexts_share_state(self):\n        \"\"\"Test that nested contexts within the same session share state.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context1:\n            await context1.set_state(\"key\", \"outer-value\")\n\n            async with Context(fastmcp=server, session=mock_session) as context2:\n                # Nested context sees same state (same session)\n                assert await context2.get_state(\"key\") == \"outer-value\"\n\n                # Nested context can modify shared state\n                await context2.set_state(\"key\", \"inner-value\")\n\n            # Outer context sees the modification\n            assert await context1.get_state(\"key\") == \"inner-value\"\n\n    async def test_two_clients_same_key_isolated_by_session(self):\n        \"\"\"Test that two different clients can store the same key independently.\n\n        Each client gets an auto-generated session ID, and their state is isolated.\n        \"\"\"\n        import json\n\n        from fastmcp import Client\n\n        server = FastMCP(\"test\")\n        stored_session_ids: list[str] = []\n\n        @server.tool\n        async def store_and_read(value: str, ctx: Context) -> dict:\n            \"\"\"Store a value and return all state info.\"\"\"\n            stored_session_ids.append(ctx.session_id)\n            existing = await ctx.get_state(\"shared_key\")\n            await ctx.set_state(\"shared_key\", value)\n            new_value = await ctx.get_state(\"shared_key\")\n            return {\n                \"session_id\": ctx.session_id,\n                \"existing_value\": existing,\n                \"new_value\": new_value,\n            }\n\n        # Client 1 stores \"value-from-client-1\"\n        async with Client(server) as client1:\n            result1 = await client1.call_tool(\n                \"store_and_read\", {\"value\": \"value-from-client-1\"}\n            )\n            data1 = json.loads(result1.content[0].text)\n            assert data1[\"existing_value\"] is None  # First write\n            assert data1[\"new_value\"] == \"value-from-client-1\"\n            session_id_1 = data1[\"session_id\"]\n\n        # Client 2 stores \"value-from-client-2\" with the SAME key\n        async with Client(server) as client2:\n            result2 = await client2.call_tool(\n                \"store_and_read\", {\"value\": \"value-from-client-2\"}\n            )\n            data2 = json.loads(result2.content[0].text)\n            # Client 2 should NOT see client 1's value (different session)\n            assert data2[\"existing_value\"] is None\n            assert data2[\"new_value\"] == \"value-from-client-2\"\n            session_id_2 = data2[\"session_id\"]\n\n        # Verify session IDs were auto-generated and are different\n        assert session_id_1 is not None\n        assert session_id_2 is not None\n        assert session_id_1 != session_id_2\n\n        # Client 1 reconnects and should still see their value\n        async with Client(server) as client1_again:\n            # But this is a NEW session (new connection = new session ID)\n            result3 = await client1_again.call_tool(\n                \"store_and_read\", {\"value\": \"value-from-client-1-again\"}\n            )\n            data3 = json.loads(result3.content[0].text)\n            # New session, so existing value is None\n            assert data3[\"existing_value\"] is None\n            assert data3[\"session_id\"] != session_id_1  # Different session\n\n\nclass TestContextStateSerializable:\n    \"\"\"Tests for the serializable parameter on set_state.\"\"\"\n\n    async def test_set_state_serializable_false_stores_arbitrary_objects(self):\n        \"\"\"Non-serializable objects can be stored with serializable=False.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        class MyClient:\n            def __init__(self):\n                self.connected = True\n\n        client = MyClient()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            await context.set_state(\"client\", client, serializable=False)\n            result = await context.get_state(\"client\")\n            assert result is client\n            assert result.connected is True\n\n    async def test_set_state_serializable_false_does_not_persist_across_requests(self):\n        \"\"\"Non-serializable state is request-scoped and gone in a new context.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            await context.set_state(\"key\", object(), serializable=False)\n            assert await context.get_state(\"key\") is not None\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            assert await context.get_state(\"key\") is None\n\n    async def test_set_state_serializable_true_rejects_non_serializable(self):\n        \"\"\"Default set_state raises TypeError for non-serializable values.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            with pytest.raises(TypeError, match=\"serializable=False\"):\n                await context.set_state(\"key\", object())\n\n    async def test_set_state_serializable_false_shadows_session_state(self):\n        \"\"\"Request-scoped state shadows session-scoped state for the same key.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            await context.set_state(\"key\", \"session-value\")\n            assert await context.get_state(\"key\") == \"session-value\"\n\n            await context.set_state(\"key\", \"request-value\", serializable=False)\n            assert await context.get_state(\"key\") == \"request-value\"\n\n    async def test_delete_state_removes_from_both_stores(self):\n        \"\"\"delete_state clears both request-scoped and session-scoped values.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            await context.set_state(\"key\", \"session-value\")\n            await context.set_state(\"key\", \"request-value\", serializable=False)\n            assert await context.get_state(\"key\") == \"request-value\"\n\n            await context.delete_state(\"key\")\n            assert await context.get_state(\"key\") is None\n\n    async def test_serializable_state_still_persists_across_requests(self):\n        \"\"\"Serializable state (default) still persists across requests.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            await context.set_state(\"key\", \"persistent\")\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            assert await context.get_state(\"key\") == \"persistent\"\n\n    async def test_serializable_write_clears_request_scoped_shadow(self):\n        \"\"\"Writing serializable state clears any request-scoped shadow for the same key.\"\"\"\n        server = FastMCP(\"test\")\n        mock_session = MagicMock()\n\n        async with Context(fastmcp=server, session=mock_session) as context:\n            await context.set_state(\"key\", \"request-value\", serializable=False)\n            assert await context.get_state(\"key\") == \"request-value\"\n\n            # Serializable write should clear the shadow\n            await context.set_state(\"key\", \"session-value\")\n            assert await context.get_state(\"key\") == \"session-value\"\n\n\nclass TestContextMeta:\n    \"\"\"Test suite for Context meta functionality.\"\"\"\n\n    def test_request_context_meta_access(self, context):\n        \"\"\"Test that meta can be accessed from request context.\"\"\"\n        from mcp.server.lowlevel.server import request_ctx\n        from mcp.shared.context import RequestContext\n\n        # Create a mock meta object with attributes\n        class MockMeta:\n            def __init__(self):\n                self.user_id = \"user-123\"\n                self.trace_id = \"trace-456\"\n                self.custom_field = \"custom-value\"\n\n        mock_meta = MockMeta()\n\n        token = request_ctx.set(\n            RequestContext(\n                request_id=0,\n                meta=cast(Any, mock_meta),  # Mock object for testing\n                session=MagicMock(wraps={}),\n                lifespan_context=MagicMock(),\n            )\n        )\n\n        # Access meta through context\n        retrieved_meta = context.request_context.meta\n        assert retrieved_meta is not None\n        assert retrieved_meta.user_id == \"user-123\"\n        assert retrieved_meta.trace_id == \"trace-456\"\n        assert retrieved_meta.custom_field == \"custom-value\"\n\n        request_ctx.reset(token)\n\n    def test_request_context_meta_none(self, context):\n        \"\"\"Test that context handles None meta gracefully.\"\"\"\n        from mcp.server.lowlevel.server import request_ctx\n        from mcp.shared.context import RequestContext\n\n        token = request_ctx.set(\n            RequestContext(\n                request_id=0,\n                meta=None,\n                session=MagicMock(wraps={}),\n                lifespan_context=MagicMock(),\n            )\n        )\n\n        # Access meta through context\n        retrieved_meta = context.request_context.meta\n        assert retrieved_meta is None\n\n        request_ctx.reset(token)\n\n\nclass TestTransport:\n    \"\"\"Test suite for Context transport property.\"\"\"\n\n    def test_transport_returns_none_outside_server_context(self, context):\n        \"\"\"Test that transport returns None when not in a server context.\"\"\"\n        assert context.transport is None\n\n    def test_transport_returns_stdio(self, context):\n        \"\"\"Test that transport returns 'stdio' when set.\"\"\"\n        token = set_transport(\"stdio\")\n        try:\n            assert context.transport == \"stdio\"\n        finally:\n            reset_transport(token)\n\n    def test_transport_returns_sse(self, context):\n        \"\"\"Test that transport returns 'sse' when set.\"\"\"\n        token = set_transport(\"sse\")\n        try:\n            assert context.transport == \"sse\"\n        finally:\n            reset_transport(token)\n\n    def test_transport_returns_streamable_http(self, context):\n        \"\"\"Test that transport returns 'streamable-http' when set.\"\"\"\n        token = set_transport(\"streamable-http\")\n        try:\n            assert context.transport == \"streamable-http\"\n        finally:\n            reset_transport(token)\n\n    def test_transport_reset(self, context):\n        \"\"\"Test that transport resets correctly.\"\"\"\n        assert context.transport is None\n        token = set_transport(\"stdio\")\n        assert context.transport == \"stdio\"\n        reset_transport(token)\n        assert context.transport is None\n\n\nclass TestTransportIntegration:\n    \"\"\"Integration tests for transport property with actual server/client.\"\"\"\n\n    async def test_transport_in_tool_via_client(self):\n        \"\"\"Test that transport is accessible from within a tool via Client.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n        observed_transport = None\n\n        @mcp.tool\n        def get_transport(ctx: Context) -> str:\n            nonlocal observed_transport\n            observed_transport = ctx.transport\n            return observed_transport or \"none\"\n\n        # Client uses in-memory transport which doesn't set transport type\n        # so we expect None here (the transport is only set by run_* methods)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_transport\", {})\n            assert observed_transport is None\n            assert result.data == \"none\"\n\n    async def test_transport_set_manually_is_visible_in_tool(self):\n        \"\"\"Test that manually set transport is visible from within a tool.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n        observed_transport = None\n\n        @mcp.tool\n        def get_transport(ctx: Context) -> str:\n            nonlocal observed_transport\n            observed_transport = ctx.transport\n            return observed_transport or \"none\"\n\n        # Manually set transport before running\n        token = set_transport(\"stdio\")\n        try:\n            async with Client(mcp) as client:\n                result = await client.call_tool(\"get_transport\", {})\n                assert observed_transport == \"stdio\"\n                assert result.data == \"stdio\"\n        finally:\n            reset_transport(token)\n\n    async def test_transport_set_via_http_middleware(self):\n        \"\"\"Test that transport is set per-request via HTTP middleware.\"\"\"\n        from fastmcp import Client\n        from fastmcp.client.transports import StreamableHttpTransport\n        from fastmcp.utilities.tests import run_server_async\n\n        mcp = FastMCP(\"test\")\n        observed_transport = None\n\n        @mcp.tool\n        def get_transport(ctx: Context) -> str:\n            nonlocal observed_transport\n            observed_transport = ctx.transport\n            return observed_transport or \"none\"\n\n        async with run_server_async(mcp, transport=\"streamable-http\") as url:\n            transport = StreamableHttpTransport(url=url)\n            async with Client(transport=transport) as client:\n                result = await client.call_tool(\"get_transport\", {})\n                assert observed_transport == \"streamable-http\"\n                assert result.data == \"streamable-http\"\n"
  },
  {
    "path": "tests/server/test_dependencies.py",
    "content": "\"\"\"Tests for Docket-style dependency injection in FastMCP.\"\"\"\n\nfrom contextlib import asynccontextmanager, contextmanager\n\nimport mcp.types as mcp_types\nimport pytest\nfrom mcp.types import TextContent, TextResourceContents\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.dependencies import CurrentContext, Depends, Shared\nfrom fastmcp.server.context import Context\n\nHUZZAH = \"huzzah!\"\n\n\nclass Connection:\n    \"\"\"Test connection that tracks whether it's currently open.\"\"\"\n\n    def __init__(self):\n        self.is_open = False\n\n    async def __aenter__(self):\n        self.is_open = True\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        self.is_open = False\n\n\n@asynccontextmanager\nasync def get_connection():\n    \"\"\"Dependency that provides an open connection.\"\"\"\n    async with Connection() as conn:\n        yield conn\n\n\n@pytest.fixture\ndef mcp():\n    \"\"\"Create a FastMCP server for testing.\"\"\"\n    return FastMCP(\"test-server\")\n\n\nasync def test_depends_with_sync_function(mcp: FastMCP):\n    \"\"\"Test that Depends works with sync dependency functions.\"\"\"\n\n    def get_config() -> dict[str, str]:\n        return {\"api_key\": \"secret123\", \"endpoint\": \"https://api.example.com\"}\n\n    @mcp.tool()\n    def fetch_data(query: str, config: dict[str, str] = Depends(get_config)) -> str:\n        return (\n            f\"Fetching '{query}' from {config['endpoint']} with key {config['api_key']}\"\n        )\n\n    result = await mcp.call_tool(\"fetch_data\", {\"query\": \"users\"})\n    assert result.structured_content is not None\n    text = result.structured_content[\"result\"]\n    assert \"Fetching 'users' from https://api.example.com\" in text\n    assert \"secret123\" in text\n\n\nasync def test_depends_with_async_function(mcp: FastMCP):\n    \"\"\"Test that Depends works with async dependency functions.\"\"\"\n\n    async def get_user_id() -> int:\n        return 42\n\n    @mcp.tool()\n    async def greet_user(name: str, user_id: int = Depends(get_user_id)) -> str:\n        return f\"Hello {name}, your ID is {user_id}\"\n\n    result = await mcp.call_tool(\"greet_user\", {\"name\": \"Alice\"})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == \"Hello Alice, your ID is 42\"\n\n\nasync def test_depends_with_async_context_manager(mcp: FastMCP):\n    \"\"\"Test that Depends works with async context managers for resource management.\"\"\"\n    cleanup_called = False\n\n    @asynccontextmanager\n    async def get_database():\n        db = \"db_connection\"\n        try:\n            yield db\n        finally:\n            nonlocal cleanup_called\n            cleanup_called = True\n\n    @mcp.tool()\n    async def query_db(sql: str, db: str = Depends(get_database)) -> str:\n        return f\"Executing '{sql}' on {db}\"\n\n    result = await mcp.call_tool(\"query_db\", {\"sql\": \"SELECT * FROM users\"})\n    assert result.structured_content is not None\n    assert (\n        \"Executing 'SELECT * FROM users' on db_connection\"\n        in result.structured_content[\"result\"]\n    )\n    assert cleanup_called\n\n\nasync def test_nested_dependencies(mcp: FastMCP):\n    \"\"\"Test that dependencies can depend on other dependencies.\"\"\"\n\n    def get_base_url() -> str:\n        return \"https://api.example.com\"\n\n    def get_api_client(base_url: str = Depends(get_base_url)) -> dict[str, str]:\n        return {\"base_url\": base_url, \"version\": \"v1\"}\n\n    @mcp.tool()\n    async def call_api(\n        endpoint: str, client: dict[str, str] = Depends(get_api_client)\n    ) -> str:\n        return f\"Calling {client['base_url']}/{client['version']}/{endpoint}\"\n\n    result = await mcp.call_tool(\"call_api\", {\"endpoint\": \"users\"})\n    assert result.structured_content is not None\n    assert (\n        result.structured_content[\"result\"]\n        == \"Calling https://api.example.com/v1/users\"\n    )\n\n\nasync def test_dependencies_excluded_from_schema(mcp: FastMCP):\n    \"\"\"Test that dependency parameters don't appear in the tool schema.\"\"\"\n\n    def get_config() -> dict[str, str]:\n        return {\"key\": \"value\"}\n\n    @mcp.tool()\n    async def my_tool(\n        name: str, age: int, config: dict[str, str] = Depends(get_config)\n    ) -> str:\n        return f\"{name} is {age} years old\"\n\n    result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n    tool = next(t for t in result.tools if t.name == \"my_tool\")\n\n    assert \"name\" in tool.inputSchema[\"properties\"]\n    assert \"age\" in tool.inputSchema[\"properties\"]\n    assert \"config\" not in tool.inputSchema[\"properties\"]\n    assert len(tool.inputSchema[\"properties\"]) == 2\n\n\nasync def test_current_context_dependency(mcp: FastMCP):\n    \"\"\"Test that CurrentContext dependency provides access to FastMCP Context.\"\"\"\n\n    @mcp.tool()\n    def use_context(ctx: Context = CurrentContext()) -> str:\n        assert isinstance(ctx, Context)\n        return HUZZAH\n\n    result = await mcp.call_tool(\"use_context\", {})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == HUZZAH\n\n\nasync def test_current_context_and_legacy_context_coexist(mcp: FastMCP):\n    \"\"\"Test that CurrentContext dependency and legacy Context injection work together.\"\"\"\n\n    @mcp.tool()\n    def use_both_contexts(\n        legacy_ctx: Context,\n        dep_ctx: Context = CurrentContext(),\n    ) -> str:\n        assert isinstance(legacy_ctx, Context)\n        assert isinstance(dep_ctx, Context)\n        assert legacy_ctx is dep_ctx\n        return HUZZAH\n\n    result = await mcp.call_tool(\"use_both_contexts\", {})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == HUZZAH\n\n\nasync def test_backward_compat_context_still_works(mcp: FastMCP):\n    \"\"\"Test that existing Context injection via type annotation still works.\"\"\"\n\n    @mcp.tool()\n    async def get_request_id(ctx: Context) -> str:\n        return ctx.request_id\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"get_request_id\", {})\n        assert len(result.content) == 1\n        content = result.content[0]\n        assert isinstance(content, TextContent)\n        assert len(content.text) > 0\n\n\nasync def test_sync_tool_with_async_dependency(mcp: FastMCP):\n    \"\"\"Test that sync tools work with async dependencies.\"\"\"\n\n    async def fetch_config() -> str:\n        return \"loaded_config\"\n\n    @mcp.tool()\n    def process_data(value: int, config: str = Depends(fetch_config)) -> str:\n        return f\"Processing {value} with {config}\"\n\n    result = await mcp.call_tool(\"process_data\", {\"value\": 100})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == \"Processing 100 with loaded_config\"\n\n\nasync def test_dependency_caching(mcp: FastMCP):\n    \"\"\"Test that dependencies are cached within a single tool call.\"\"\"\n    call_count = 0\n\n    def expensive_dependency() -> int:\n        nonlocal call_count\n        call_count += 1\n        return 42\n\n    @mcp.tool()\n    async def tool_with_cached_dep(\n        dep1: int = Depends(expensive_dependency),\n        dep2: int = Depends(expensive_dependency),\n    ) -> str:\n        return f\"{dep1} + {dep2} = {dep1 + dep2}\"\n\n    result = await mcp.call_tool(\"tool_with_cached_dep\", {})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == \"42 + 42 = 84\"\n    assert call_count == 1\n\n\nasync def test_context_and_depends_together(mcp: FastMCP):\n    \"\"\"Test that Context type injection and Depends can be used together.\"\"\"\n\n    def get_multiplier() -> int:\n        return 10\n\n    @mcp.tool()\n    async def mixed_deps(\n        value: int, ctx: Context, multiplier: int = Depends(get_multiplier)\n    ) -> str:\n        assert isinstance(ctx, Context)\n        assert ctx.request_id\n        assert len(ctx.request_id) > 0\n        return (\n            f\"Request {ctx.request_id}: {value} * {multiplier} = {value * multiplier}\"\n        )\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"mixed_deps\", {\"value\": 5})\n        assert len(result.content) == 1\n        content = result.content[0]\n        assert isinstance(content, TextContent)\n        assert \"5 * 10 = 50\" in content.text\n        assert \"Request \" in content.text\n\n\nasync def test_resource_with_dependency(mcp: FastMCP):\n    \"\"\"Test that resources support dependency injection.\"\"\"\n\n    def get_storage_path() -> str:\n        return \"/data/config\"\n\n    @mcp.resource(\"config://settings\")\n    async def get_settings(storage: str = Depends(get_storage_path)) -> str:\n        return f\"Settings loaded from {storage}\"\n\n    result = await mcp.read_resource(\"config://settings\")\n    assert len(result.contents) == 1\n    assert result.contents[0].content == \"Settings loaded from /data/config\"\n\n\nasync def test_resource_with_context_and_dependency(mcp: FastMCP):\n    \"\"\"Test that resources can use both Context and Depends.\"\"\"\n\n    def get_prefix() -> str:\n        return \"DATA\"\n\n    @mcp.resource(\"config://info\")\n    async def get_info(ctx: Context, prefix: str = Depends(get_prefix)) -> str:\n        return f\"{prefix}: Request {ctx.request_id}\"\n\n    async with Client(mcp) as client:\n        result = await client.read_resource(\"config://info\")\n        assert len(result) == 1\n        content = result[0]\n        assert isinstance(content, TextResourceContents)\n        assert \"DATA: Request \" in content.text\n        assert len(content.text.split(\"Request \")[1]) > 0\n\n\nasync def test_prompt_with_dependency(mcp: FastMCP):\n    \"\"\"Test that prompts support dependency injection.\"\"\"\n\n    def get_tone() -> str:\n        return \"friendly and helpful\"\n\n    @mcp.prompt()\n    async def custom_prompt(topic: str, tone: str = Depends(get_tone)) -> str:\n        return f\"Write about {topic} in a {tone} tone\"\n\n    result = await mcp.render_prompt(\"custom_prompt\", {\"topic\": \"Python\"})\n    assert len(result.messages) == 1\n    message = result.messages[0]\n    content = message.content\n    assert isinstance(content, TextContent)\n    assert content.text == \"Write about Python in a friendly and helpful tone\"\n\n\nasync def test_prompt_with_context_and_dependency(mcp: FastMCP):\n    \"\"\"Test that prompts can use both Context and Depends.\"\"\"\n\n    def get_style() -> str:\n        return \"concise\"\n\n    @mcp.prompt()\n    async def styled_prompt(\n        query: str, ctx: Context, style: str = Depends(get_style)\n    ) -> str:\n        assert isinstance(ctx, Context)\n        assert ctx.request_id\n        return f\"Answer '{query}' in a {style} style\"\n\n    async with Client(mcp) as client:\n        result = await client.get_prompt(\"styled_prompt\", {\"query\": \"What is MCP?\"})\n        assert len(result.messages) == 1\n        message = result.messages[0]\n        content = message.content\n        assert isinstance(content, TextContent)\n        assert content.text == \"Answer 'What is MCP?' in a concise style\"\n\n\nasync def test_resource_template_with_dependency(mcp: FastMCP):\n    \"\"\"Test that resource templates support dependency injection.\"\"\"\n\n    def get_base_path() -> str:\n        return \"/var/data\"\n\n    @mcp.resource(\"data://{filename}\")\n    async def get_file(filename: str, base_path: str = Depends(get_base_path)) -> str:\n        return f\"Reading {base_path}/{filename}\"\n\n    result = await mcp.read_resource(\"data://config.txt\")\n    assert len(result.contents) == 1\n    assert result.contents[0].content == \"Reading /var/data/config.txt\"\n\n\nasync def test_resource_template_with_context_and_dependency(mcp: FastMCP):\n    \"\"\"Test that resource templates can use both Context and Depends.\"\"\"\n\n    def get_version() -> str:\n        return \"v2\"\n\n    @mcp.resource(\"api://{endpoint}\")\n    async def call_endpoint(\n        endpoint: str, ctx: Context, version: str = Depends(get_version)\n    ) -> str:\n        assert isinstance(ctx, Context)\n        assert ctx.request_id\n        return f\"Calling {version}/{endpoint}\"\n\n    async with Client(mcp) as client:\n        result = await client.read_resource(\"api://users\")\n        assert len(result) == 1\n        content = result[0]\n        assert isinstance(content, TextResourceContents)\n        assert content.text == \"Calling v2/users\"\n\n\nasync def test_async_tool_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that context manager dependencies stay open during async tool execution.\n\n    Context managers must remain open while the async function executes, not just\n    while it's being called (which only returns a coroutine).\n    \"\"\"\n\n    @mcp.tool()\n    async def query_data(\n        query: str,\n        connection: Connection = Depends(get_connection),\n    ) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open}\"\n\n    result = await mcp.call_tool(\"query_data\", {\"query\": \"test\"})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == \"open=True\"\n\n\nasync def test_async_resource_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that context manager dependencies stay open during async resource execution.\"\"\"\n\n    @mcp.resource(\"data://config\")\n    async def load_config(connection: Connection = Depends(get_connection)) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open}\"\n\n    result = await mcp.read_resource(\"data://config\")\n    assert result.contents[0].content == \"open=True\"\n\n\nasync def test_async_resource_template_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that context manager dependencies stay open during async resource template execution.\"\"\"\n\n    @mcp.resource(\"user://{user_id}\")\n    async def get_user(\n        user_id: str,\n        connection: Connection = Depends(get_connection),\n    ) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open},user={user_id}\"\n\n    result = await mcp.read_resource(\"user://123\")\n    assert isinstance(result.contents[0].content, str)\n    assert \"open=True\" in result.contents[0].content\n\n\nasync def test_async_prompt_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that context manager dependencies stay open during async prompt execution.\"\"\"\n\n    @mcp.prompt()\n    async def research_prompt(\n        topic: str,\n        connection: Connection = Depends(get_connection),\n    ) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open},topic={topic}\"\n\n    result = await mcp.render_prompt(\"research_prompt\", {\"topic\": \"AI\"})\n    message = result.messages[0]\n    content = message.content\n    assert isinstance(content, TextContent)\n    assert \"open=True\" in content.text\n\n\nasync def test_argument_validation_with_dependencies(mcp: FastMCP):\n    \"\"\"Test that user arguments are still validated when dependencies are present.\"\"\"\n\n    def get_config() -> dict[str, str]:\n        return {\"key\": \"value\"}\n\n    @mcp.tool()\n    async def validated_tool(\n        age: int,  # Should validate type\n        config: dict[str, str] = Depends(get_config),\n    ) -> str:\n        return f\"age={age}\"\n\n    # Valid argument\n    result = await mcp.call_tool(\"validated_tool\", {\"age\": 25})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == \"age=25\"\n\n    # Invalid argument type should fail validation\n    import pydantic\n\n    with pytest.raises(pydantic.ValidationError):\n        await mcp.call_tool(\"validated_tool\", {\"age\": \"not a number\"})\n\n\nasync def test_connection_dependency_excluded_from_tool_schema(mcp: FastMCP):\n    \"\"\"Test that Connection dependency parameter is excluded from tool schema.\"\"\"\n\n    @mcp.tool()\n    async def with_connection(\n        name: str,\n        connection: Connection = Depends(get_connection),\n    ) -> str:\n        return name\n\n    result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n    tool = next(t for t in result.tools if t.name == \"with_connection\")\n\n    assert \"name\" in tool.inputSchema[\"properties\"]\n    assert \"connection\" not in tool.inputSchema[\"properties\"]\n\n\nasync def test_sync_tool_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that sync context manager dependencies work with tools.\"\"\"\n    conn = Connection()\n\n    @contextmanager\n    def get_sync_connection():\n        conn.is_open = True\n        try:\n            yield conn\n        finally:\n            conn.is_open = False\n\n    @mcp.tool()\n    async def query_sync(\n        query: str,\n        connection: Connection = Depends(get_sync_connection),\n    ) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open}\"\n\n    result = await mcp.call_tool(\"query_sync\", {\"query\": \"test\"})\n    assert result.structured_content is not None\n    assert result.structured_content[\"result\"] == \"open=True\"\n    assert not conn.is_open\n\n\nasync def test_sync_resource_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that sync context manager dependencies work with resources.\"\"\"\n    conn = Connection()\n\n    @contextmanager\n    def get_sync_connection():\n        conn.is_open = True\n        try:\n            yield conn\n        finally:\n            conn.is_open = False\n\n    @mcp.resource(\"data://sync\")\n    async def load_sync(connection: Connection = Depends(get_sync_connection)) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open}\"\n\n    result = await mcp.read_resource(\"data://sync\")\n    assert result.contents[0].content == \"open=True\"\n    assert not conn.is_open\n\n\nasync def test_sync_resource_template_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that sync context manager dependencies work with resource templates.\"\"\"\n    conn = Connection()\n\n    @contextmanager\n    def get_sync_connection():\n        conn.is_open = True\n        try:\n            yield conn\n        finally:\n            conn.is_open = False\n\n    @mcp.resource(\"item://{item_id}\")\n    async def get_item(\n        item_id: str,\n        connection: Connection = Depends(get_sync_connection),\n    ) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open},item={item_id}\"\n\n    result = await mcp.read_resource(\"item://456\")\n    assert isinstance(result.contents[0].content, str)\n    assert \"open=True\" in result.contents[0].content\n    assert not conn.is_open\n\n\nasync def test_sync_prompt_context_manager_stays_open(mcp: FastMCP):\n    \"\"\"Test that sync context manager dependencies work with prompts.\"\"\"\n    conn = Connection()\n\n    @contextmanager\n    def get_sync_connection():\n        conn.is_open = True\n        try:\n            yield conn\n        finally:\n            conn.is_open = False\n\n    @mcp.prompt()\n    async def sync_prompt(\n        topic: str,\n        connection: Connection = Depends(get_sync_connection),\n    ) -> str:\n        assert connection.is_open\n        return f\"open={connection.is_open},topic={topic}\"\n\n    result = await mcp.render_prompt(\"sync_prompt\", {\"topic\": \"test\"})\n    message = result.messages[0]\n    content = message.content\n    assert isinstance(content, TextContent)\n    assert \"open=True\" in content.text\n    assert not conn.is_open\n\n\nasync def test_external_user_cannot_override_dependency(mcp: FastMCP):\n    \"\"\"Test that external MCP clients cannot override dependency parameters.\"\"\"\n\n    def get_admin_status() -> str:\n        return \"not_admin\"\n\n    @mcp.tool()\n    async def check_permission(\n        action: str, admin: str = Depends(get_admin_status)\n    ) -> str:\n        return f\"action={action},admin={admin}\"\n\n    # Verify dependency is NOT in the schema\n    result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n    tool = next(t for t in result.tools if t.name == \"check_permission\")\n    assert \"admin\" not in tool.inputSchema[\"properties\"]\n\n    # Normal call - dependency is resolved\n    result = await mcp.call_tool(\"check_permission\", {\"action\": \"read\"})\n    assert result.structured_content is not None\n    assert \"admin=not_admin\" in result.structured_content[\"result\"]\n\n    # Try to override dependency - rejected (not in schema)\n    import pydantic\n\n    with pytest.raises(pydantic.ValidationError):\n        await mcp.call_tool(\"check_permission\", {\"action\": \"read\", \"admin\": \"hacker\"})\n\n\nasync def test_prompt_dependency_cannot_be_overridden_externally(mcp: FastMCP):\n    \"\"\"Test that external callers cannot override prompt dependencies.\n\n    This is a security test - dependencies should NEVER be overridable from\n    outside the server, even for prompts which don't validate against strict schemas.\n    \"\"\"\n\n    def get_secret() -> str:\n        return \"real_secret\"\n\n    @mcp.prompt()\n    async def secure_prompt(topic: str, secret: str = Depends(get_secret)) -> str:\n        return f\"Topic: {topic}, Secret: {secret}\"\n\n    # Normal call - should use dependency\n    result = await mcp.render_prompt(\"secure_prompt\", {\"topic\": \"test\"})\n    message = result.messages[0]\n    content = message.content\n    assert isinstance(content, TextContent)\n    assert \"Secret: real_secret\" in content.text\n\n    # Try to override dependency - should be ignored/rejected\n    result = await mcp.render_prompt(\n        \"secure_prompt\",\n        {\"topic\": \"test\", \"secret\": \"HACKED\"},  # Attempt override\n    )\n    message = result.messages[0]\n    content = message.content\n    assert isinstance(content, TextContent)\n    # Should still use real dependency, not hacked value\n    assert \"Secret: real_secret\" in content.text\n    assert \"HACKED\" not in content.text\n\n\nasync def test_resource_dependency_cannot_be_overridden_externally(mcp: FastMCP):\n    \"\"\"Test that external callers cannot override resource dependencies.\"\"\"\n\n    def get_api_key() -> str:\n        return \"real_api_key\"\n\n    @mcp.resource(\"data://config\")\n    async def get_config(api_key: str = Depends(get_api_key)) -> str:\n        return f\"API Key: {api_key}\"\n\n    # Normal call\n    result = await mcp.read_resource(\"data://config\")\n    assert isinstance(result.contents[0].content, str)\n    assert \"API Key: real_api_key\" in result.contents[0].content\n\n    # Resources don't accept arguments from clients (static URI)\n    # so this scenario is less of a concern, but documenting it\n\n\nasync def test_resource_template_dependency_cannot_be_overridden_externally(\n    mcp: FastMCP,\n):\n    \"\"\"Test that external callers cannot override resource template dependencies.\n\n    Resource templates extract parameters from the URI path, so there's a risk\n    that a dependency parameter name could match a URI parameter.\n    \"\"\"\n\n    def get_auth_token() -> str:\n        return \"real_token\"\n\n    @mcp.resource(\"user://{user_id}\")\n    async def get_user(user_id: str, token: str = Depends(get_auth_token)) -> str:\n        return f\"User: {user_id}, Token: {token}\"\n\n    # Normal call\n    result = await mcp.read_resource(\"user://123\")\n    assert isinstance(result.contents[0].content, str)\n    assert \"User: 123, Token: real_token\" in result.contents[0].content\n\n    # Try to inject token via URI (shouldn't be possible with this pattern)\n    # But if URI was user://{token}, it could extract it\n\n\nasync def test_resource_template_uri_cannot_match_dependency_name(mcp: FastMCP):\n    \"\"\"Test that URI parameters cannot have the same name as dependencies.\n\n    If a URI template tries to use a parameter name that's also a dependency,\n    the template creation should fail because the dependency is excluded from\n    the user-facing signature.\n    \"\"\"\n\n    def get_token() -> str:\n        return \"real_token\"\n\n    # This should fail - {token} in URI but token is a dependency parameter\n    with pytest.raises(ValueError, match=\"URI parameters.*must be a subset\"):\n\n        @mcp.resource(\"auth://{token}/validate\")\n        async def validate(token: str = Depends(get_token)) -> str:\n            return f\"Validating with: {token}\"\n\n\nasync def test_toolerror_propagates_from_dependency(mcp: FastMCP):\n    \"\"\"ToolError raised in a dependency should propagate unchanged (issue #2633).\n\n    When a dependency raises ToolError, it should not be wrapped in RuntimeError.\n    This allows developers to use ToolError for validation in dependencies.\n    \"\"\"\n    from fastmcp.exceptions import ToolError\n\n    def validate_client_id() -> str:\n        raise ToolError(\"Client ID is required - select a client first\")\n\n    @mcp.tool()\n    async def my_tool(client_id: str = Depends(validate_client_id)) -> str:\n        return f\"Working with client: {client_id}\"\n\n    async with Client(mcp) as client:\n        # ToolError is converted to an error result by the server\n        result = await client.call_tool(\"my_tool\", {}, raise_on_error=False)\n        assert result.is_error\n        # The original error message should be preserved (not wrapped in RuntimeError)\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Client ID is required - select a client first\"\n\n\nasync def test_validation_error_propagates_from_dependency(mcp: FastMCP):\n    \"\"\"ValidationError raised in a dependency should propagate unchanged.\"\"\"\n    from fastmcp.exceptions import ValidationError\n\n    def validate_input() -> str:\n        raise ValidationError(\"Invalid input format\")\n\n    @mcp.tool()\n    async def tool_with_validation(val: str = Depends(validate_input)) -> str:\n        return val\n\n    async with Client(mcp) as client:\n        # ValidationError is re-raised by the server and becomes an error result\n        # The original error message should be preserved (not wrapped in RuntimeError)\n        result = await client.call_tool(\n            \"tool_with_validation\", {}, raise_on_error=False\n        )\n        assert result.is_error\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Invalid input format\"\n\n\nclass TestDependencyInjection:\n    \"\"\"Tests for the uncalled-for DI engine.\"\"\"\n\n    def test_is_docket_available(self):\n        \"\"\"Test is_docket_available returns True when docket is installed.\"\"\"\n        from fastmcp.server.dependencies import is_docket_available\n\n        assert is_docket_available() is True\n\n    def test_require_docket_passes_when_installed(self):\n        \"\"\"Test require_docket doesn't raise when docket is installed.\"\"\"\n        from fastmcp.server.dependencies import require_docket\n\n        require_docket(\"test feature\")\n\n    def test_dependency_class_exists(self):\n        \"\"\"Test Dependency and Depends are importable from fastmcp.\"\"\"\n        from fastmcp.dependencies import Dependency, Depends\n\n        assert Dependency is not None\n        assert Depends is not None\n\n    def test_depends_works(self):\n        \"\"\"Test Depends() creates proper dependency wrapper.\"\"\"\n        from uncalled_for.resolution import _Depends\n\n        from fastmcp.dependencies import Depends\n\n        def get_value() -> str:\n            return \"test_value\"\n\n        dep = Depends(get_value)\n        assert isinstance(dep, _Depends)\n        assert dep.factory is get_value\n\n    async def test_depends_import_from_fastmcp(self):\n        \"\"\"Test that Depends can be imported from fastmcp.dependencies.\"\"\"\n        from fastmcp.dependencies import Depends\n\n        def get_config() -> dict:\n            return {\"key\": \"value\"}\n\n        dep = Depends(get_config)\n        assert dep is not None\n\n    def test_get_dependency_parameters(self):\n        \"\"\"Test get_dependency_parameters finds dependency defaults.\"\"\"\n        from uncalled_for import get_dependency_parameters\n        from uncalled_for.resolution import _Depends\n\n        from fastmcp.dependencies import Depends\n\n        def get_db() -> str:\n            return \"database\"\n\n        def my_func(name: str, db: str = Depends(get_db)) -> str:\n            return f\"{name}: {db}\"\n\n        deps = get_dependency_parameters(my_func)\n        assert \"db\" in deps\n        db_dep = deps[\"db\"]\n        assert isinstance(db_dep, _Depends)\n        assert db_dep.factory is get_db\n\n\nclass TestAuthDependencies:\n    \"\"\"Tests for authentication dependencies (CurrentAccessToken, TokenClaim).\"\"\"\n\n    def test_current_access_token_is_importable(self):\n        \"\"\"Test that CurrentAccessToken can be imported.\"\"\"\n        from fastmcp.server.dependencies import CurrentAccessToken\n\n        assert CurrentAccessToken is not None\n\n    def test_token_claim_is_importable(self):\n        \"\"\"Test that TokenClaim can be imported.\"\"\"\n        from fastmcp.server.dependencies import TokenClaim\n\n        assert TokenClaim is not None\n\n    def test_current_access_token_is_dependency(self):\n        \"\"\"Test that CurrentAccessToken is a Dependency instance.\"\"\"\n        from fastmcp.dependencies import Dependency\n        from fastmcp.server.dependencies import _CurrentAccessToken\n\n        dep = _CurrentAccessToken()\n        assert isinstance(dep, Dependency)\n\n    def test_token_claim_creates_dependency(self):\n        \"\"\"Test that TokenClaim creates a Dependency instance.\"\"\"\n        from fastmcp.dependencies import Dependency\n        from fastmcp.server.dependencies import TokenClaim, _TokenClaim\n\n        dep = TokenClaim(\"oid\")\n        assert isinstance(dep, _TokenClaim)\n        assert isinstance(dep, Dependency)\n        assert dep.claim_name == \"oid\"\n\n    async def test_current_access_token_raises_without_token(self):\n        \"\"\"Test that CurrentAccessToken raises when no token is available.\"\"\"\n        from fastmcp.server.dependencies import _CurrentAccessToken\n\n        dep = _CurrentAccessToken()\n        with pytest.raises(RuntimeError, match=\"No access token found\"):\n            await dep.__aenter__()\n\n    async def test_token_claim_raises_without_token(self):\n        \"\"\"Test that TokenClaim raises when no token is available.\"\"\"\n        from fastmcp.server.dependencies import _TokenClaim\n\n        dep = _TokenClaim(\"oid\")\n        with pytest.raises(RuntimeError, match=\"No access token available\"):\n            await dep.__aenter__()\n\n    async def test_current_access_token_excluded_from_tool_schema(self, mcp: FastMCP):\n        \"\"\"Test that CurrentAccessToken dependency is excluded from tool schema.\"\"\"\n        import mcp.types as mcp_types\n\n        from fastmcp.server.auth import AccessToken\n        from fastmcp.server.dependencies import CurrentAccessToken\n\n        @mcp.tool()\n        async def tool_with_token(\n            name: str,\n            token: AccessToken = CurrentAccessToken(),\n        ) -> str:\n            return name\n\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        tool = next(t for t in result.tools if t.name == \"tool_with_token\")\n\n        assert \"name\" in tool.inputSchema[\"properties\"]\n        assert \"token\" not in tool.inputSchema[\"properties\"]\n\n    async def test_token_claim_excluded_from_tool_schema(self, mcp: FastMCP):\n        \"\"\"Test that TokenClaim dependency is excluded from tool schema.\"\"\"\n        import mcp.types as mcp_types\n\n        from fastmcp.server.dependencies import TokenClaim\n\n        @mcp.tool()\n        async def tool_with_claim(\n            name: str,\n            user_id: str = TokenClaim(\"oid\"),\n        ) -> str:\n            return name\n\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        tool = next(t for t in result.tools if t.name == \"tool_with_claim\")\n\n        assert \"name\" in tool.inputSchema[\"properties\"]\n        assert \"user_id\" not in tool.inputSchema[\"properties\"]\n\n    def test_current_access_token_exported_from_all(self):\n        \"\"\"Test that CurrentAccessToken is exported from __all__.\"\"\"\n        from fastmcp.server import dependencies\n\n        assert \"CurrentAccessToken\" in dependencies.__all__\n\n    def test_token_claim_exported_from_all(self):\n        \"\"\"Test that TokenClaim is exported from __all__.\"\"\"\n        from fastmcp.server import dependencies\n\n        assert \"TokenClaim\" in dependencies.__all__\n\n\nclass TestSharedDependencies:\n    \"\"\"Tests for Shared() dependencies that resolve once and are reused.\"\"\"\n\n    async def test_shared_sync_function(self, mcp: FastMCP):\n        \"\"\"Shared dependency from a sync function resolves and is reused.\"\"\"\n\n        call_count = 0\n\n        def get_config() -> dict[str, str]:\n            nonlocal call_count\n            call_count += 1\n            return {\"key\": \"value\"}\n\n        @mcp.tool()\n        async def tool_a(config: dict[str, str] = Shared(get_config)) -> str:\n            return config[\"key\"]\n\n        @mcp.tool()\n        async def tool_b(config: dict[str, str] = Shared(get_config)) -> str:\n            return config[\"key\"]\n\n        async with Client(mcp) as client:\n            result_a = await client.call_tool(\"tool_a\", {})\n            result_b = await client.call_tool(\"tool_b\", {})\n\n        assert result_a.content[0].text == \"value\"\n        assert result_b.content[0].text == \"value\"\n        assert call_count == 1\n\n    async def test_shared_async_function(self, mcp: FastMCP):\n        \"\"\"Shared dependency from an async function resolves and is reused.\"\"\"\n\n        call_count = 0\n\n        async def get_session() -> str:\n            nonlocal call_count\n            call_count += 1\n            return \"session-abc\"\n\n        @mcp.tool()\n        async def tool_a(session: str = Shared(get_session)) -> str:\n            return session\n\n        @mcp.tool()\n        async def tool_b(session: str = Shared(get_session)) -> str:\n            return session\n\n        async with Client(mcp) as client:\n            result_a = await client.call_tool(\"tool_a\", {})\n            result_b = await client.call_tool(\"tool_b\", {})\n\n        assert result_a.content[0].text == \"session-abc\"\n        assert result_b.content[0].text == \"session-abc\"\n        assert call_count == 1\n\n    async def test_shared_async_context_manager(self, mcp: FastMCP):\n        \"\"\"Shared dependency from an async context manager stays open across calls.\"\"\"\n\n        enter_count = 0\n\n        @asynccontextmanager\n        async def get_connection():\n            nonlocal enter_count\n            enter_count += 1\n            conn = Connection()\n            async with conn:\n                yield conn\n\n        @mcp.tool()\n        async def tool_a(conn: Connection = Shared(get_connection)) -> bool:\n            return conn.is_open\n\n        @mcp.tool()\n        async def tool_b(conn: Connection = Shared(get_connection)) -> bool:\n            return conn.is_open\n\n        async with Client(mcp) as client:\n            result_a = await client.call_tool(\"tool_a\", {})\n            result_b = await client.call_tool(\"tool_b\", {})\n\n        assert result_a.content[0].text == \"true\"\n        assert result_b.content[0].text == \"true\"\n        assert enter_count == 1\n\n    async def test_shared_with_depends(self, mcp: FastMCP):\n        \"\"\"Shared and Depends can coexist in the same tool.\"\"\"\n\n        shared_calls = 0\n        depends_calls = 0\n\n        def get_config() -> str:\n            nonlocal shared_calls\n            shared_calls += 1\n            return \"shared-config\"\n\n        def get_request_id() -> str:\n            nonlocal depends_calls\n            depends_calls += 1\n            return \"request-123\"\n\n        @mcp.tool()\n        async def my_tool(\n            config: str = Shared(get_config),\n            request_id: str = Depends(get_request_id),\n        ) -> str:\n            return f\"{config}/{request_id}\"\n\n        async with Client(mcp) as client:\n            result1 = await client.call_tool(\"my_tool\", {})\n            result2 = await client.call_tool(\"my_tool\", {})\n\n        assert result1.content[0].text == \"shared-config/request-123\"\n        assert result2.content[0].text == \"shared-config/request-123\"\n        assert shared_calls == 1\n        assert depends_calls == 2\n\n    async def test_shared_excluded_from_schema(self, mcp: FastMCP):\n        \"\"\"Shared dependencies are not exposed in the tool schema.\"\"\"\n\n        def get_db() -> str:\n            return \"db\"\n\n        @mcp.tool()\n        async def my_tool(name: str, db: str = Shared(get_db)) -> str:\n            return name\n\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        tool = next(t for t in result.tools if t.name == \"my_tool\")\n\n        assert \"name\" in tool.inputSchema[\"properties\"]\n        assert \"db\" not in tool.inputSchema[\"properties\"]\n\n    async def test_shared_in_resource(self, mcp: FastMCP):\n        \"\"\"Shared dependencies work in resource functions.\"\"\"\n\n        call_count = 0\n\n        def get_config() -> str:\n            nonlocal call_count\n            call_count += 1\n            return \"resource-config\"\n\n        @mcp.resource(\"test://config\")\n        async def config_resource(config: str = Shared(get_config)) -> str:\n            return config\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"test://config\")\n            assert result[0].text == \"resource-config\"\n\n            result = await client.read_resource(\"test://config\")\n            assert result[0].text == \"resource-config\"\n            assert call_count == 1\n\n    async def test_shared_in_prompt(self, mcp: FastMCP):\n        \"\"\"Shared dependencies work in prompt functions.\"\"\"\n\n        call_count = 0\n\n        def get_system_prompt() -> str:\n            nonlocal call_count\n            call_count += 1\n            return \"You are a helpful assistant.\"\n\n        @mcp.prompt()\n        async def my_prompt(topic: str, system: str = Shared(get_system_prompt)) -> str:\n            return f\"{system} Talk about {topic}.\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"my_prompt\", {\"topic\": \"dogs\"})\n            assert (\n                \"You are a helpful assistant. Talk about dogs.\"\n                in result.messages[0].content.text\n            )\n\n            result = await client.get_prompt(\"my_prompt\", {\"topic\": \"cats\"})\n            assert (\n                \"You are a helpful assistant. Talk about cats.\"\n                in result.messages[0].content.text\n            )\n            assert call_count == 1\n"
  },
  {
    "path": "tests/server/test_dependencies_advanced.py",
    "content": "\"\"\"Advanced tests for dependency injection in FastMCP: context annotations, vendored DI, and auth dependencies.\"\"\"\n\nimport inspect\n\nimport mcp.types as mcp_types\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.dependencies import CurrentContext\nfrom fastmcp.server.context import Context\n\n\nclass Connection:\n    \"\"\"Test connection that tracks whether it's currently open.\"\"\"\n\n    def __init__(self):\n        self.is_open = False\n\n    async def __aenter__(self):\n        self.is_open = True\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        self.is_open = False\n\n\n@pytest.fixture\ndef mcp():\n    \"\"\"Create a FastMCP server for testing.\"\"\"\n    return FastMCP(\"test-server\")\n\n\nclass TestTransformContextAnnotations:\n    \"\"\"Tests for the transform_context_annotations function.\"\"\"\n\n    async def test_optional_context_degrades_to_none_without_active_context(self):\n        \"\"\"Optional Context should resolve to None when no context is active.\"\"\"\n        from fastmcp.server.dependencies import transform_context_annotations\n\n        async def fn_with_optional_ctx(name: str, ctx: Context | None = None) -> str:\n            return name\n\n        transform_context_annotations(fn_with_optional_ctx)\n        sig = inspect.signature(fn_with_optional_ctx)\n        ctx_dependency = sig.parameters[\"ctx\"].default\n\n        resolved_ctx = await ctx_dependency.__aenter__()\n        try:\n            assert resolved_ctx is None\n        finally:\n            await ctx_dependency.__aexit__(None, None, None)\n\n    async def test_optional_context_still_injected_in_foreground_requests(\n        self, mcp: FastMCP\n    ):\n        \"\"\"Optional Context should still be injected for normal MCP requests.\"\"\"\n\n        @mcp.tool()\n        async def tool_with_optional_context(\n            name: str, ctx: Context | None = None\n        ) -> str:\n            if ctx is None:\n                return f\"missing:{name}\"\n            return f\"present:{ctx.session_id}:{name}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_with_optional_context\", {\"name\": \"x\"})\n            assert result.content[0].text.startswith(\"present:\")\n\n    async def test_basic_context_transformation(self, mcp: FastMCP):\n        \"\"\"Test basic Context type annotation is transformed.\"\"\"\n\n        @mcp.tool()\n        async def tool_with_context(name: str, ctx: Context) -> str:\n            return f\"session={ctx.session_id}, name={name}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_with_context\", {\"name\": \"test\"})\n            assert \"session=\" in result.content[0].text\n            assert \"name=test\" in result.content[0].text\n\n    async def test_transform_with_var_params(self):\n        \"\"\"Test transform_context_annotations handles *args and **kwargs correctly.\"\"\"\n        from fastmcp.server.dependencies import transform_context_annotations\n\n        # This function can't be a tool (FastMCP doesn't support *args/**kwargs),\n        # but transform should handle it gracefully for signature inspection\n        async def fn_with_var_params(\n            first: str, ctx: Context, *args: str, **kwargs: str\n        ) -> str:\n            return f\"first={first}\"\n\n        transform_context_annotations(fn_with_var_params)\n        sig = inspect.signature(fn_with_var_params)\n\n        # Verify structure is preserved\n        param_kinds = {p.name: p.kind for p in sig.parameters.values()}\n        assert param_kinds[\"first\"] == inspect.Parameter.POSITIONAL_OR_KEYWORD\n        assert param_kinds[\"ctx\"] == inspect.Parameter.POSITIONAL_OR_KEYWORD\n        assert param_kinds[\"args\"] == inspect.Parameter.VAR_POSITIONAL\n        assert param_kinds[\"kwargs\"] == inspect.Parameter.VAR_KEYWORD\n\n        # ctx should now have a default\n        assert sig.parameters[\"ctx\"].default is not inspect.Parameter.empty\n\n    async def test_context_keyword_only(self, mcp: FastMCP):\n        \"\"\"Test Context transformation preserves keyword-only parameter semantics.\"\"\"\n        from fastmcp.server.dependencies import transform_context_annotations\n\n        # Define function with keyword-only Context param\n        async def fn_with_kw_only(a: str, *, ctx: Context, b: str = \"default\") -> str:\n            return f\"a={a}, b={b}\"\n\n        # Transform and check signature structure\n        transform_context_annotations(fn_with_kw_only)\n        sig = inspect.signature(fn_with_kw_only)\n        params = list(sig.parameters.values())\n\n        # 'a' should be POSITIONAL_OR_KEYWORD\n        assert params[0].name == \"a\"\n        assert params[0].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD\n\n        # 'ctx' should still be KEYWORD_ONLY (after transformation)\n        ctx_param = sig.parameters[\"ctx\"]\n        assert ctx_param.kind == inspect.Parameter.KEYWORD_ONLY\n\n        # 'b' should still be KEYWORD_ONLY\n        b_param = sig.parameters[\"b\"]\n        assert b_param.kind == inspect.Parameter.KEYWORD_ONLY\n\n    async def test_context_with_annotated(self, mcp: FastMCP):\n        \"\"\"Test Context with Annotated type is transformed.\"\"\"\n        from typing import Annotated\n\n        @mcp.tool()\n        async def tool_with_annotated_ctx(\n            name: str, ctx: Annotated[Context, \"custom annotation\"]\n        ) -> str:\n            return f\"session={ctx.session_id}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_with_annotated_ctx\", {\"name\": \"test\"})\n            assert \"session=\" in result.content[0].text\n\n    async def test_context_already_has_dependency_default(self, mcp: FastMCP):\n        \"\"\"Test that Context with existing Depends default is not re-transformed.\"\"\"\n\n        @mcp.tool()\n        async def tool_with_explicit_context(\n            name: str, ctx: Context = CurrentContext()\n        ) -> str:\n            return f\"session={ctx.session_id}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\n                \"tool_with_explicit_context\", {\"name\": \"test\"}\n            )\n            assert \"session=\" in result.content[0].text\n\n    async def test_multiple_context_params(self, mcp: FastMCP):\n        \"\"\"Test multiple Context-typed parameters are all transformed.\"\"\"\n\n        @mcp.tool()\n        async def tool_with_multiple_ctx(\n            name: str, ctx1: Context, ctx2: Context\n        ) -> str:\n            # Both should refer to same context\n            assert ctx1.session_id == ctx2.session_id\n            return f\"same={ctx1 is ctx2}\"\n\n        # Both ctx params should be excluded from schema\n        result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n        tool = next(t for t in result.tools if t.name == \"tool_with_multiple_ctx\")\n        assert \"name\" in tool.inputSchema[\"properties\"]\n        assert \"ctx1\" not in tool.inputSchema[\"properties\"]\n        assert \"ctx2\" not in tool.inputSchema[\"properties\"]\n\n    async def test_context_in_class_method(self, mcp: FastMCP):\n        \"\"\"Test Context transformation works with bound methods.\"\"\"\n\n        class MyTools:\n            def __init__(self, prefix: str):\n                self.prefix = prefix\n\n            async def greet(self, name: str, ctx: Context) -> str:\n                return f\"{self.prefix} {name}, session={ctx.session_id}\"\n\n        tools = MyTools(\"Hello\")\n        mcp.tool()(tools.greet)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert \"Hello World\" in result.content[0].text\n            assert \"session=\" in result.content[0].text\n\n    async def test_context_in_static_method(self, mcp: FastMCP):\n        \"\"\"Test Context transformation works with static methods.\"\"\"\n\n        class MyTools:\n            @staticmethod\n            async def static_tool(name: str, ctx: Context) -> str:\n                return f\"name={name}, session={ctx.session_id}\"\n\n        mcp.tool()(MyTools.static_tool)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"static_tool\", {\"name\": \"test\"})\n            assert \"name=test\" in result.content[0].text\n            assert \"session=\" in result.content[0].text\n\n    async def test_context_in_callable_class(self, mcp: FastMCP):\n        \"\"\"Test Context transformation works with callable class instances.\"\"\"\n        from fastmcp.tools import Tool\n\n        class CallableTool:\n            def __init__(self, multiplier: int):\n                self.multiplier = multiplier\n\n            async def __call__(self, x: int, ctx: Context) -> str:\n                return f\"result={x * self.multiplier}, session={ctx.session_id}\"\n\n        # Use Tool.from_function directly (mcp.tool() decorator doesn't support callable instances)\n        tool = Tool.from_function(CallableTool(3))\n        mcp.add_tool(tool)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"CallableTool\", {\"x\": 5})\n            assert \"result=15\" in result.content[0].text\n            assert \"session=\" in result.content[0].text\n\n    async def test_context_param_reordering(self, mcp: FastMCP):\n        \"\"\"Test that Context params are reordered correctly to maintain valid signature.\"\"\"\n        from fastmcp.server.dependencies import transform_context_annotations\n\n        # Context in middle without default - should be moved after non-default params\n        async def fn_with_middle_ctx(a: str, ctx: Context, b: str) -> str:\n            return f\"{a},{b}\"\n\n        transform_context_annotations(fn_with_middle_ctx)\n        sig = inspect.signature(fn_with_middle_ctx)\n        params = list(sig.parameters.values())\n\n        # After transform: a, b should come before ctx (which now has default)\n        param_names = [p.name for p in params]\n        assert param_names == [\"a\", \"b\", \"ctx\"]\n\n        # ctx should have a default now\n        assert sig.parameters[\"ctx\"].default is not inspect.Parameter.empty\n\n    async def test_context_resource(self, mcp: FastMCP):\n        \"\"\"Test Context transformation works with resources.\"\"\"\n\n        @mcp.resource(\"data://test\")\n        async def resource_with_ctx(ctx: Context) -> str:\n            return f\"session={ctx.session_id}\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"data://test\")\n            assert len(result) == 1\n            assert \"session=\" in result[0].text\n\n    async def test_context_resource_template(self, mcp: FastMCP):\n        \"\"\"Test Context transformation works with resource templates.\"\"\"\n\n        @mcp.resource(\"item://{item_id}\")\n        async def template_with_ctx(item_id: str, ctx: Context) -> str:\n            return f\"item={item_id}, session={ctx.session_id}\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"item://123\")\n            assert len(result) == 1\n            assert \"item=123\" in result[0].text\n            assert \"session=\" in result[0].text\n\n    async def test_context_prompt(self, mcp: FastMCP):\n        \"\"\"Test Context transformation works with prompts.\"\"\"\n\n        @mcp.prompt()\n        async def prompt_with_ctx(topic: str, ctx: Context) -> str:\n            return f\"Write about {topic} (session: {ctx.session_id})\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"prompt_with_ctx\", {\"topic\": \"AI\"})\n            assert \"Write about AI\" in result.messages[0].content.text\n            assert \"session:\" in result.messages[0].content.text\n"
  },
  {
    "path": "tests/server/test_event_store.py",
    "content": "\"\"\"Tests for the EventStore implementation.\"\"\"\n\nimport pytest\nfrom mcp.server.streamable_http import EventMessage\nfrom mcp.types import JSONRPCMessage, JSONRPCRequest\n\nfrom fastmcp.server.event_store import EventEntry, EventStore, StreamEventList\n\n\nclass TestEventEntry:\n    def test_event_entry_with_message(self):\n        entry = EventEntry(\n            event_id=\"event-1\",\n            stream_id=\"stream-1\",\n            message={\"jsonrpc\": \"2.0\", \"method\": \"test\", \"id\": 1},\n        )\n        assert entry.event_id == \"event-1\"\n        assert entry.stream_id == \"stream-1\"\n        assert entry.message == {\"jsonrpc\": \"2.0\", \"method\": \"test\", \"id\": 1}\n\n    def test_event_entry_without_message(self):\n        entry = EventEntry(\n            event_id=\"event-1\",\n            stream_id=\"stream-1\",\n            message=None,\n        )\n        assert entry.message is None\n\n\nclass TestStreamEventList:\n    def test_stream_event_list(self):\n        stream_list = StreamEventList(event_ids=[\"event-1\", \"event-2\", \"event-3\"])\n        assert stream_list.event_ids == [\"event-1\", \"event-2\", \"event-3\"]\n\n    def test_stream_event_list_empty(self):\n        stream_list = StreamEventList(event_ids=[])\n        assert stream_list.event_ids == []\n\n\nclass TestEventStore:\n    @pytest.fixture\n    def event_store(self):\n        return EventStore(max_events_per_stream=5, ttl=3600)\n\n    @pytest.fixture\n    def sample_message(self):\n        return JSONRPCMessage(root=JSONRPCRequest(jsonrpc=\"2.0\", method=\"test\", id=1))\n\n    async def test_store_event_returns_event_id(self, event_store, sample_message):\n        event_id = await event_store.store_event(\"stream-1\", sample_message)\n        assert event_id is not None\n        assert isinstance(event_id, str)\n        assert len(event_id) > 0\n\n    async def test_store_event_priming_event(self, event_store):\n        \"\"\"Test storing a priming event (message=None).\"\"\"\n        event_id = await event_store.store_event(\"stream-1\", None)\n        assert event_id is not None\n\n    async def test_store_multiple_events(self, event_store, sample_message):\n        event_ids = []\n        for _ in range(3):\n            event_id = await event_store.store_event(\"stream-1\", sample_message)\n            event_ids.append(event_id)\n\n        # All event IDs should be unique\n        assert len(set(event_ids)) == 3\n\n    async def test_replay_events_after_returns_stream_id(\n        self, event_store, sample_message\n    ):\n        # Store some events\n        first_event_id = await event_store.store_event(\"stream-1\", sample_message)\n        await event_store.store_event(\"stream-1\", sample_message)\n\n        # Replay events after the first one\n        replayed_events: list[EventMessage] = []\n\n        async def callback(event: EventMessage):\n            replayed_events.append(event)\n\n        stream_id = await event_store.replay_events_after(first_event_id, callback)\n        assert stream_id == \"stream-1\"\n        assert len(replayed_events) == 1\n\n    async def test_replay_events_after_skips_priming_events(self, event_store):\n        \"\"\"Priming events (message=None) should not be replayed.\"\"\"\n        # Store a priming event\n        priming_id = await event_store.store_event(\"stream-1\", None)\n\n        # Store a real event\n        real_message = JSONRPCMessage(\n            root=JSONRPCRequest(jsonrpc=\"2.0\", method=\"test\", id=1)\n        )\n        await event_store.store_event(\"stream-1\", real_message)\n\n        # Replay after priming event\n        replayed_events: list[EventMessage] = []\n\n        async def callback(event: EventMessage):\n            replayed_events.append(event)\n\n        await event_store.replay_events_after(priming_id, callback)\n\n        # Only the real event should be replayed\n        assert len(replayed_events) == 1\n\n    async def test_replay_events_after_unknown_event_id(self, event_store):\n        replayed_events: list[EventMessage] = []\n\n        async def callback(event: EventMessage):\n            replayed_events.append(event)\n\n        result = await event_store.replay_events_after(\"unknown-event-id\", callback)\n        assert result is None\n        assert len(replayed_events) == 0\n\n    async def test_max_events_per_stream_trims_old_events(self, event_store):\n        \"\"\"Test that old events are trimmed when max_events_per_stream is exceeded.\"\"\"\n        # Store more events than the limit\n        event_ids = []\n        for i in range(7):\n            msg = JSONRPCMessage(\n                root=JSONRPCRequest(jsonrpc=\"2.0\", method=f\"test-{i}\", id=i)\n            )\n            event_id = await event_store.store_event(\"stream-1\", msg)\n            event_ids.append(event_id)\n\n        # The first 2 events should have been trimmed (7 - 5 = 2)\n        # Trying to replay from the first event should fail\n        replayed_events: list[EventMessage] = []\n\n        async def callback(event: EventMessage):\n            replayed_events.append(event)\n\n        result = await event_store.replay_events_after(event_ids[0], callback)\n        assert result is None  # First event was trimmed\n\n        # But replaying from a more recent event should work\n        result = await event_store.replay_events_after(event_ids[3], callback)\n        assert result == \"stream-1\"\n\n    async def test_multiple_streams_are_isolated(self, event_store):\n        \"\"\"Events from different streams should not interfere with each other.\"\"\"\n        msg1 = JSONRPCMessage(\n            root=JSONRPCRequest(jsonrpc=\"2.0\", method=\"stream1-test\", id=1)\n        )\n        msg2 = JSONRPCMessage(\n            root=JSONRPCRequest(jsonrpc=\"2.0\", method=\"stream2-test\", id=2)\n        )\n\n        stream1_event = await event_store.store_event(\"stream-1\", msg1)\n        await event_store.store_event(\"stream-1\", msg1)\n\n        stream2_event = await event_store.store_event(\"stream-2\", msg2)\n        await event_store.store_event(\"stream-2\", msg2)\n\n        # Replay stream 1\n        stream1_replayed: list[EventMessage] = []\n\n        async def callback1(event: EventMessage):\n            stream1_replayed.append(event)\n\n        stream_id = await event_store.replay_events_after(stream1_event, callback1)\n        assert stream_id == \"stream-1\"\n        assert len(stream1_replayed) == 1\n\n        # Replay stream 2\n        stream2_replayed: list[EventMessage] = []\n\n        async def callback2(event: EventMessage):\n            stream2_replayed.append(event)\n\n        stream_id = await event_store.replay_events_after(stream2_event, callback2)\n        assert stream_id == \"stream-2\"\n        assert len(stream2_replayed) == 1\n\n    async def test_default_storage_is_memory(self):\n        \"\"\"Test that EventStore defaults to in-memory storage.\"\"\"\n        event_store = EventStore()\n        msg = JSONRPCMessage(root=JSONRPCRequest(jsonrpc=\"2.0\", method=\"test\", id=1))\n\n        event_id = await event_store.store_event(\"stream-1\", msg)\n        assert event_id is not None\n\n        replayed: list[EventMessage] = []\n\n        async def callback(event: EventMessage):\n            replayed.append(event)\n\n        # Store another event and replay\n        await event_store.store_event(\"stream-1\", msg)\n        await event_store.replay_events_after(event_id, callback)\n        assert len(replayed) == 1\n\n\nclass TestEventStoreIntegration:\n    \"\"\"Integration tests for EventStore with actual message types.\"\"\"\n\n    async def test_roundtrip_jsonrpc_message(self):\n        event_store = EventStore()\n\n        # Create a realistic JSON-RPC request wrapped in JSONRPCMessage\n        original_msg = JSONRPCMessage(\n            root=JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                method=\"tools/call\",\n                id=\"request-123\",\n                params={\"name\": \"my_tool\", \"arguments\": {\"x\": 1, \"y\": 2}},\n            )\n        )\n\n        # Store it\n        event_id = await event_store.store_event(\"stream-1\", original_msg)\n\n        # Store another event so we have something to replay\n        second_msg = JSONRPCMessage(\n            root=JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                method=\"tools/call\",\n                id=\"request-456\",\n                params={\"name\": \"my_tool\", \"arguments\": {\"x\": 3, \"y\": 4}},\n            )\n        )\n        await event_store.store_event(\"stream-1\", second_msg)\n\n        # Replay and verify the message content\n        replayed: list[EventMessage] = []\n\n        async def callback(event: EventMessage):\n            replayed.append(event)\n\n        await event_store.replay_events_after(event_id, callback)\n\n        assert len(replayed) == 1\n        assert isinstance(replayed[0].message.root, JSONRPCRequest)\n        assert replayed[0].message.root.method == \"tools/call\"\n        assert replayed[0].message.root.id == \"request-456\"\n"
  },
  {
    "path": "tests/server/test_file_server.py",
    "content": "from pathlib import Path\n\nimport mcp.types as mcp_types\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.resources import ResourceContent, ResourceResult\n\n\n@pytest.fixture()\ndef test_dir(tmp_path_factory) -> Path:\n    \"\"\"Create a temporary directory with test files.\"\"\"\n    tmp = tmp_path_factory.mktemp(\"test_files\")\n\n    # Create test files\n    (tmp / \"example.py\").write_text(\"print('hello world')\")\n    (tmp / \"readme.md\").write_text(\"# Test Directory\\nThis is a test.\")\n    (tmp / \"config.json\").write_text('{\"test\": true}')\n\n    return tmp\n\n\n@pytest.fixture\ndef mcp() -> FastMCP:\n    mcp = FastMCP()\n\n    return mcp\n\n\n@pytest.fixture(autouse=True)\ndef resources(mcp: FastMCP, test_dir: Path) -> FastMCP:\n    @mcp.resource(\"dir://test_dir\")\n    def list_test_dir() -> ResourceResult:\n        \"\"\"List the files in the test directory\"\"\"\n        files = [str(f) for f in test_dir.iterdir()]\n        return ResourceResult([ResourceContent(f) for f in files])\n\n    @mcp.resource(\"file://test_dir/example.py\")\n    def read_example_py() -> str:\n        \"\"\"Read the example.py file\"\"\"\n        try:\n            return (test_dir / \"example.py\").read_text()\n        except FileNotFoundError:\n            return \"File not found\"\n\n    @mcp.resource(\"file://test_dir/readme.md\")\n    def read_readme_md() -> str:\n        \"\"\"Read the readme.md file\"\"\"\n        try:\n            return (test_dir / \"readme.md\").read_text()\n        except FileNotFoundError:\n            return \"File not found\"\n\n    @mcp.resource(\"file://test_dir/config.json\")\n    def read_config_json() -> str:\n        \"\"\"Read the config.json file\"\"\"\n        try:\n            return (test_dir / \"config.json\").read_text()\n        except FileNotFoundError:\n            return \"File not found\"\n\n    return mcp\n\n\n@pytest.fixture(autouse=True)\ndef tools(mcp: FastMCP, test_dir: Path) -> FastMCP:\n    @mcp.tool\n    def delete_file(path: str) -> bool:\n        # ensure path is in test_dir\n        if Path(path).resolve().parent != test_dir:\n            raise ValueError(f\"Path must be in test_dir: {path}\")\n        Path(path).unlink()\n        return True\n\n    return mcp\n\n\nasync def test_list_resources(mcp: FastMCP):\n    result = await mcp._list_resources_mcp(mcp_types.ListResourcesRequest())\n    assert len(result.resources) == 4\n\n    assert [str(r.uri) for r in result.resources] == [\n        \"dir://test_dir\",\n        \"file://test_dir/example.py\",\n        \"file://test_dir/readme.md\",\n        \"file://test_dir/config.json\",\n    ]\n\n\nasync def test_read_resource_dir(mcp: FastMCP):\n    res_result = await mcp._read_resource_mcp(\"dir://test_dir\")\n    assert isinstance(res_result, mcp_types.ReadResourceResult)\n    # ResourceResult splits lists into multiple contents (one per file path)\n    assert len(res_result.contents) == 3\n    # Extract file paths from each content\n    files = [\n        item.text\n        for item in res_result.contents\n        if isinstance(item, mcp_types.TextResourceContents)\n    ]\n\n    assert sorted([Path(f).name for f in files]) == [\n        \"config.json\",\n        \"example.py\",\n        \"readme.md\",\n    ]\n\n\nasync def test_read_resource_file(mcp: FastMCP):\n    res_result = await mcp._read_resource_mcp(\"file://test_dir/example.py\")\n    assert isinstance(res_result, mcp_types.ReadResourceResult)\n    assert len(res_result.contents) == 1\n    res = res_result.contents[0]\n    assert isinstance(res, mcp_types.TextResourceContents)\n    assert res.text == \"print('hello world')\"\n\n\nasync def test_delete_file(mcp: FastMCP, test_dir: Path):\n    await mcp._call_tool_mcp(\n        \"delete_file\", arguments=dict(path=str(test_dir / \"example.py\"))\n    )\n    assert not (test_dir / \"example.py\").exists()\n\n\nasync def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path):\n    await mcp._call_tool_mcp(\n        \"delete_file\", arguments=dict(path=str(test_dir / \"example.py\"))\n    )\n    res_result = await mcp._read_resource_mcp(\"file://test_dir/example.py\")\n    assert isinstance(res_result, mcp_types.ReadResourceResult)\n    assert len(res_result.contents) == 1\n    res = res_result.contents[0]\n    assert isinstance(res, mcp_types.TextResourceContents)\n    assert res.text == \"File not found\"\n"
  },
  {
    "path": "tests/server/test_icons.py",
    "content": "\"\"\"Tests for icon support across all MCP object types.\"\"\"\n\nfrom mcp.types import Icon\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.prompts import Message, Prompt\nfrom fastmcp.resources import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.tools import Tool\n\n\nclass TestServerIcons:\n    \"\"\"Test icon support at the server/implementation level.\"\"\"\n\n    async def test_server_with_icons_and_website_url(self):\n        \"\"\"Test that server accepts icons and websiteUrl in constructor.\"\"\"\n        icons = [\n            Icon(\n                src=\"https://example.com/icon.png\",\n                mimeType=\"image/png\",\n                sizes=[\"48x48\"],\n            ),\n            Icon(\n                src=\"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=\",\n                mimeType=\"image/svg+xml\",\n                sizes=[\"any\"],\n            ),\n        ]\n\n        mcp = FastMCP(\n            name=\"TestServer\",\n            version=\"1.0.0\",\n            website_url=\"https://example.com\",\n            icons=icons,\n        )\n\n        # Verify that icons and website_url are passed to the underlying server\n        async with Client(mcp) as client:\n            server_info = client.initialize_result.serverInfo\n            assert server_info.websiteUrl == \"https://example.com\"\n            assert server_info.icons == icons\n\n    async def test_server_without_icons_and_website_url(self):\n        \"\"\"Test that server works without icons and websiteUrl.\"\"\"\n        mcp = FastMCP(name=\"TestServer\")\n\n        async with Client(mcp) as client:\n            server_info = client.initialize_result.serverInfo\n            assert server_info.websiteUrl is None\n            assert server_info.icons is None\n\n\nclass TestToolIcons:\n    \"\"\"Test icon support for tools.\"\"\"\n\n    async def test_tool_with_icons(self):\n        \"\"\"Test that tools can have icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        icons = [\n            Icon(src=\"https://example.com/tool-icon.png\", mimeType=\"image/png\"),\n        ]\n\n        @mcp.tool(icons=icons)\n        def my_tool(name: str) -> str:\n            \"\"\"A tool with an icon.\"\"\"\n            return f\"Hello, {name}!\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            tool = tools[0]\n            assert tool.icons == icons\n\n    async def test_tool_from_function_with_icons(self):\n        \"\"\"Test creating a tool from a function with icons.\"\"\"\n        icons = [Icon(src=\"https://example.com/icon.png\")]\n\n        def my_function(x: int) -> int:\n            \"\"\"A function.\"\"\"\n            return x * 2\n\n        tool = Tool.from_function(my_function, icons=icons)\n        assert tool.icons == icons\n\n        # Verify it converts to MCP tool correctly\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.icons == icons\n\n    async def test_tool_without_icons(self):\n        \"\"\"Test that tools work without icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def my_tool(name: str) -> str:\n            \"\"\"A tool without an icon.\"\"\"\n            return f\"Hello, {name}!\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            tool = tools[0]\n            assert tool.icons is None\n\n\nclass TestResourceIcons:\n    \"\"\"Test icon support for resources.\"\"\"\n\n    async def test_resource_with_icons(self):\n        \"\"\"Test that resources can have icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        icons = [Icon(src=\"https://example.com/resource-icon.png\")]\n\n        @mcp.resource(\"test://resource\", icons=icons)\n        def my_resource() -> str:\n            \"\"\"A resource with an icon.\"\"\"\n            return \"Resource content\"\n\n        async with Client(mcp) as client:\n            resources = await client.list_resources()\n            assert len(resources) == 1\n            resource = resources[0]\n            assert resource.icons == icons\n\n    async def test_resource_from_function_with_icons(self):\n        \"\"\"Test creating a resource from a function with icons.\"\"\"\n        icons = [Icon(src=\"https://example.com/icon.png\")]\n\n        def my_function() -> str:\n            \"\"\"A function.\"\"\"\n            return \"content\"\n\n        resource = Resource.from_function(\n            my_function,\n            uri=\"test://resource\",\n            icons=icons,\n        )\n        assert resource.icons == icons\n\n        # Verify it converts to MCP resource correctly\n        mcp_resource = resource.to_mcp_resource()\n        assert mcp_resource.icons == icons\n\n    async def test_resource_without_icons(self):\n        \"\"\"Test that resources work without icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.resource(\"test://resource\")\n        def my_resource() -> str:\n            \"\"\"A resource without an icon.\"\"\"\n            return \"Resource content\"\n\n        async with Client(mcp) as client:\n            resources = await client.list_resources()\n            assert len(resources) == 1\n            resource = resources[0]\n            assert resource.icons is None\n\n\nclass TestResourceTemplateIcons:\n    \"\"\"Test icon support for resource templates.\"\"\"\n\n    async def test_resource_template_with_icons(self):\n        \"\"\"Test that resource templates can have icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        icons = [Icon(src=\"https://example.com/template-icon.png\")]\n\n        @mcp.resource(\"test://resource/{id}\", icons=icons)\n        def my_template(id: str) -> str:\n            \"\"\"A resource template with an icon.\"\"\"\n            return f\"Resource {id}\"\n\n        async with Client(mcp) as client:\n            templates = await client.list_resource_templates()\n            assert len(templates) == 1\n            template = templates[0]\n            assert template.icons == icons\n\n    async def test_resource_template_from_function_with_icons(self):\n        \"\"\"Test creating a resource template from a function with icons.\"\"\"\n        icons = [Icon(src=\"https://example.com/icon.png\")]\n\n        def my_function(id: str) -> str:\n            \"\"\"A function.\"\"\"\n            return f\"content-{id}\"\n\n        template = ResourceTemplate.from_function(\n            my_function,\n            uri_template=\"test://resource/{id}\",\n            icons=icons,\n        )\n        assert template.icons == icons\n\n        # Verify it converts to MCP template correctly\n        mcp_template = template.to_mcp_template()\n        assert mcp_template.icons == icons\n\n    async def test_resource_template_without_icons(self):\n        \"\"\"Test that resource templates work without icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.resource(\"test://resource/{id}\")\n        def my_template(id: str) -> str:\n            \"\"\"A resource template without an icon.\"\"\"\n            return f\"Resource {id}\"\n\n        async with Client(mcp) as client:\n            templates = await client.list_resource_templates()\n            assert len(templates) == 1\n            template = templates[0]\n            assert template.icons is None\n\n\nclass TestPromptIcons:\n    \"\"\"Test icon support for prompts.\"\"\"\n\n    async def test_prompt_with_icons(self):\n        \"\"\"Test that prompts can have icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        icons = [Icon(src=\"https://example.com/prompt-icon.png\")]\n\n        @mcp.prompt(icons=icons)\n        def my_prompt(name: str):\n            \"\"\"A prompt with an icon.\"\"\"\n            return Message(f\"Hello, {name}!\")\n\n        async with Client(mcp) as client:\n            prompts = await client.list_prompts()\n            assert len(prompts) == 1\n            prompt = prompts[0]\n            assert prompt.icons == icons\n\n    async def test_prompt_from_function_with_icons(self):\n        \"\"\"Test creating a prompt from a function with icons.\"\"\"\n        icons = [Icon(src=\"https://example.com/icon.png\")]\n\n        def my_function(topic: str):\n            \"\"\"A function.\"\"\"\n            return Message(f\"Tell me about {topic}\")\n\n        prompt = Prompt.from_function(my_function, icons=icons)\n        assert prompt.icons == icons\n\n        # Verify it converts to MCP prompt correctly\n        mcp_prompt = prompt.to_mcp_prompt()\n        assert mcp_prompt.icons == icons\n\n    async def test_prompt_without_icons(self):\n        \"\"\"Test that prompts work without icons.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.prompt\n        def my_prompt(name: str):\n            \"\"\"A prompt without an icon.\"\"\"\n            return Message(f\"Hello, {name}!\")\n\n        async with Client(mcp) as client:\n            prompts = await client.list_prompts()\n            assert len(prompts) == 1\n            prompt = prompts[0]\n            assert prompt.icons is None\n\n\nclass TestIconTypes:\n    \"\"\"Test different types of icon data.\"\"\"\n\n    async def test_multiple_icon_sizes(self):\n        \"\"\"Test that multiple icon sizes can be specified.\"\"\"\n        icons = [\n            Icon(\n                src=\"https://example.com/icon-48.png\",\n                mimeType=\"image/png\",\n                sizes=[\"48x48\"],\n            ),\n            Icon(\n                src=\"https://example.com/icon-96.png\",\n                mimeType=\"image/png\",\n                sizes=[\"96x96\"],\n            ),\n            Icon(\n                src=\"https://example.com/icon.svg\",\n                mimeType=\"image/svg+xml\",\n                sizes=[\"any\"],\n            ),\n        ]\n\n        mcp = FastMCP(\"TestServer\", icons=icons)\n\n        async with Client(mcp) as client:\n            server_info = client.initialize_result.serverInfo\n            assert len(server_info.icons) == 3\n            assert server_info.icons == icons\n\n    async def test_data_uri_icon(self):\n        \"\"\"Test using data URIs for icons.\"\"\"\n        # Simple SVG data URI\n        data_uri = \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+\"\n\n        icons = [Icon(src=data_uri, mimeType=\"image/svg+xml\")]\n\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool(icons=icons)\n        def my_tool() -> str:\n            \"\"\"A tool with a data URI icon.\"\"\"\n            return \"result\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert tools[0].icons[0].src == data_uri\n\n    async def test_icon_without_optional_fields(self):\n        \"\"\"Test that icons work with only the src field.\"\"\"\n        icons = [Icon(src=\"https://example.com/icon.png\")]\n\n        mcp = FastMCP(\"TestServer\", icons=icons)\n\n        async with Client(mcp) as client:\n            server_info = client.initialize_result.serverInfo\n            assert server_info.icons[0].src == \"https://example.com/icon.png\"\n            assert server_info.icons[0].mimeType is None\n            assert server_info.icons[0].sizes is None\n\n\nclass TestIconImport:\n    \"\"\"Test that Icon must be imported from mcp.types.\"\"\"\n\n    def test_icon_import(self):\n        \"\"\"Test that Icon must be imported from mcp.types, not fastmcp.\"\"\"\n        # Icon should NOT be available from fastmcp\n        import fastmcp\n\n        assert not hasattr(fastmcp, \"Icon\")\n\n        # Icon should be imported from mcp.types\n        from mcp.types import Icon as MCPIcon\n\n        icon = MCPIcon(src=\"https://example.com/icon.png\")\n        assert icon.src == \"https://example.com/icon.png\"\n"
  },
  {
    "path": "tests/server/test_input_validation.py",
    "content": "\"\"\"\nTests for input validation behavior with strict_input_validation setting.\n\nThis module tests the difference between strict JSON schema validation (when\nstrict_input_validation=True) and Pydantic-based coercion (when\nstrict_input_validation=False, the default).\n\"\"\"\n\nimport json\n\nimport pytest\nfrom mcp.types import TextContent\nfrom pydantic import BaseModel\n\nfrom fastmcp import Client, FastMCP\n\n\nclass UserProfile(BaseModel):\n    \"\"\"A test model for validating Pydantic model arguments.\"\"\"\n\n    name: str\n    age: int\n    email: str\n\n\nclass TestStringToIntegerCoercion:\n    \"\"\"Test string-to-integer coercion behavior.\"\"\"\n\n    async def test_string_integer_with_strict_validation(self):\n        \"\"\"With strict validation, string integers should raise an error.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=True)\n\n        @mcp.tool\n        def add_numbers(a: int, b: int) -> int:\n            \"\"\"Add two numbers together.\"\"\"\n            return a + b\n\n        async with Client(mcp) as client:\n            # String integers should fail with strict validation\n            with pytest.raises(Exception) as exc_info:\n                await client.call_tool(\"add_numbers\", {\"a\": \"10\", \"b\": \"20\"})\n\n            # Verify it's a validation error\n            error_msg = str(exc_info.value).lower()\n            assert (\n                \"validation\" in error_msg\n                or \"invalid\" in error_msg\n                or \"type\" in error_msg\n            )\n\n    async def test_string_integer_without_strict_validation(self):\n        \"\"\"Without strict validation, string integers should be coerced.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def add_numbers(a: int, b: int) -> int:\n            \"\"\"Add two numbers together.\"\"\"\n            return a + b\n\n        async with Client(mcp) as client:\n            # String integers should be coerced to integers\n            result = await client.call_tool(\"add_numbers\", {\"a\": \"10\", \"b\": \"20\"})\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"30\"\n\n    async def test_default_is_not_strict(self):\n        \"\"\"By default, strict_input_validation should be False.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def multiply(x: int, y: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return x * y\n\n        async with Client(mcp) as client:\n            # Should work with string integers by default\n            result = await client.call_tool(\"multiply\", {\"x\": \"5\", \"y\": \"3\"})\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"15\"\n\n    async def test_string_float_coercion(self):\n        \"\"\"Test that string floats are also coerced.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def calculate_area(length: float, width: float) -> float:\n            \"\"\"Calculate rectangle area.\"\"\"\n            return length * width\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\n                \"calculate_area\", {\"length\": \"10.5\", \"width\": \"20.0\"}\n            )\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"210.0\"\n\n    async def test_invalid_coercion_still_fails(self):\n        \"\"\"Even without strict validation, truly invalid inputs should fail.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def square(n: int) -> int:\n            \"\"\"Square a number.\"\"\"\n            return n * n\n\n        async with Client(mcp) as client:\n            # Non-numeric strings should still fail\n            with pytest.raises(Exception):\n                await client.call_tool(\"square\", {\"n\": \"not-a-number\"})\n\n\nclass TestPydanticModelArguments:\n    \"\"\"Test validation of Pydantic model arguments.\"\"\"\n\n    async def test_pydantic_model_with_dict_no_strict(self):\n        \"\"\"Pydantic models should accept dict arguments without strict validation.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def create_user(profile: UserProfile) -> str:\n            \"\"\"Create a user from a profile.\"\"\"\n            return f\"Created user {profile.name}, age {profile.age}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\n                \"create_user\",\n                {\"profile\": {\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}},\n            )\n            assert isinstance(result.content[0], TextContent)\n            assert \"Alice\" in result.content[0].text\n            assert \"30\" in result.content[0].text\n\n    async def test_pydantic_model_with_stringified_json_no_strict(self):\n        \"\"\"Test if stringified JSON is accepted for Pydantic models without strict validation.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def create_user(profile: UserProfile) -> str:\n            \"\"\"Create a user from a profile.\"\"\"\n            return f\"Created user {profile.name}, age {profile.age}\"\n\n        async with Client(mcp) as client:\n            # Some LLM clients send stringified JSON instead of actual JSON\n            stringified = json.dumps(\n                {\"name\": \"Bob\", \"age\": 25, \"email\": \"bob@example.com\"}\n            )\n\n            # This test verifies whether we handle stringified JSON\n            try:\n                result = await client.call_tool(\"create_user\", {\"profile\": stringified})\n                # If this succeeds, we're handling stringified JSON\n                assert isinstance(result.content[0], TextContent)\n                assert \"Bob\" in result.content[0].text\n                stringified_json_works = True\n            except Exception as e:\n                # If this fails, we're not handling stringified JSON\n                stringified_json_works = False\n                error_msg = str(e)\n\n            # Document the behavior - we want to know if this works or not\n            if stringified_json_works:\n                # This is the desired behavior\n                pass\n            else:\n                # This means stringified JSON doesn't work - document it\n                assert (\n                    \"validation\" in error_msg.lower() or \"invalid\" in error_msg.lower()\n                )\n\n    async def test_pydantic_model_with_coercion(self):\n        \"\"\"Pydantic models should benefit from coercion without strict validation.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def create_user(profile: UserProfile) -> str:\n            \"\"\"Create a user from a profile.\"\"\"\n            return f\"Created user {profile.name}, age {profile.age}\"\n\n        async with Client(mcp) as client:\n            # Age as string should be coerced\n            result = await client.call_tool(\n                \"create_user\",\n                {\n                    \"profile\": {\n                        \"name\": \"Charlie\",\n                        \"age\": \"35\",  # String instead of int\n                        \"email\": \"charlie@example.com\",\n                    }\n                },\n            )\n            assert isinstance(result.content[0], TextContent)\n            assert \"Charlie\" in result.content[0].text\n            assert \"35\" in result.content[0].text\n\n    async def test_pydantic_model_strict_validation(self):\n        \"\"\"With strict validation, Pydantic models should enforce exact types.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=True)\n\n        @mcp.tool\n        def create_user(profile: UserProfile) -> str:\n            \"\"\"Create a user from a profile.\"\"\"\n            return f\"Created user {profile.name}, age {profile.age}\"\n\n        async with Client(mcp) as client:\n            # Age as string should fail with strict validation\n            with pytest.raises(Exception):\n                await client.call_tool(\n                    \"create_user\",\n                    {\n                        \"profile\": {\n                            \"name\": \"Dave\",\n                            \"age\": \"40\",  # String instead of int\n                            \"email\": \"dave@example.com\",\n                        }\n                    },\n                )\n\n\nclass TestValidationErrorMessages:\n    \"\"\"Test the quality of validation error messages.\"\"\"\n\n    async def test_error_message_quality_strict(self):\n        \"\"\"Capture error message with strict validation.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=True)\n\n        @mcp.tool\n        def process_data(count: int, name: str) -> str:\n            \"\"\"Process some data.\"\"\"\n            return f\"Processed {count} items for {name}\"\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception) as exc_info:\n                await client.call_tool(\n                    \"process_data\", {\"count\": \"not-a-number\", \"name\": \"test\"}\n                )\n\n            error_msg = str(exc_info.value)\n            # Strict validation error message\n            # Should mention validation or type error\n            assert (\n                \"validation\" in error_msg.lower()\n                or \"invalid\" in error_msg.lower()\n                or \"type\" in error_msg.lower()\n            )\n\n    async def test_error_message_quality_pydantic(self):\n        \"\"\"Capture error message with Pydantic validation.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def process_data(count: int, name: str) -> str:\n            \"\"\"Process some data.\"\"\"\n            return f\"Processed {count} items for {name}\"\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception) as exc_info:\n                await client.call_tool(\n                    \"process_data\", {\"count\": \"not-a-number\", \"name\": \"test\"}\n                )\n\n            error_msg = str(exc_info.value)\n            # Pydantic validation error message\n            # Should be more detailed and mention validation\n            assert \"validation\" in error_msg.lower() or \"invalid\" in error_msg.lower()\n\n    async def test_missing_required_field_error(self):\n        \"\"\"Test error message for missing required fields.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def greet(name: str, age: int) -> str:\n            \"\"\"Greet a person.\"\"\"\n            return f\"Hello {name}, you are {age} years old\"\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception) as exc_info:\n                # Missing 'age' parameter\n                await client.call_tool(\"greet\", {\"name\": \"Alice\"})\n\n            error_msg = str(exc_info.value)\n            # Should mention the missing field\n            assert \"age\" in error_msg.lower() or \"required\" in error_msg.lower()\n\n\nclass TestEdgeCases:\n    \"\"\"Test edge cases and boundary conditions.\"\"\"\n\n    async def test_optional_parameters_with_coercion(self):\n        \"\"\"Optional parameters should work with coercion.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def format_message(text: str, repeat: int = 1) -> str:\n            \"\"\"Format a message with optional repetition.\"\"\"\n            return text * repeat\n\n        async with Client(mcp) as client:\n            # String for optional int parameter\n            result = await client.call_tool(\n                \"format_message\", {\"text\": \"hi\", \"repeat\": \"3\"}\n            )\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"hihihi\"\n\n    async def test_none_values(self):\n        \"\"\"Test handling of None values.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def process_optional(value: int | None) -> str:\n            \"\"\"Process an optional value.\"\"\"\n            return f\"Value: {value}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"process_optional\", {\"value\": None})\n            assert isinstance(result.content[0], TextContent)\n            assert \"None\" in result.content[0].text\n\n    async def test_empty_string_to_int(self):\n        \"\"\"Empty strings should fail conversion to int.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def square(n: int) -> int:\n            \"\"\"Square a number.\"\"\"\n            return n * n\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception):\n                await client.call_tool(\"square\", {\"n\": \"\"})\n\n    async def test_boolean_coercion(self):\n        \"\"\"Test boolean value coercion.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def toggle(enabled: bool) -> str:\n            \"\"\"Toggle a feature.\"\"\"\n            return f\"Feature is {'enabled' if enabled else 'disabled'}\"\n\n        async with Client(mcp) as client:\n            # String \"true\" should be coerced to boolean\n            result = await client.call_tool(\"toggle\", {\"enabled\": \"true\"})\n            assert isinstance(result.content[0], TextContent)\n            assert \"enabled\" in result.content[0].text.lower()\n\n            # String \"false\" should be coerced to boolean\n            result = await client.call_tool(\"toggle\", {\"enabled\": \"false\"})\n            assert isinstance(result.content[0], TextContent)\n            assert \"disabled\" in result.content[0].text.lower()\n\n    async def test_list_of_integers_with_string_elements(self):\n        \"\"\"Test lists containing string representations of integers.\"\"\"\n        mcp = FastMCP(\"TestServer\", strict_input_validation=False)\n\n        @mcp.tool\n        def sum_numbers(numbers: list[int]) -> int:\n            \"\"\"Sum a list of numbers.\"\"\"\n            return sum(numbers)\n\n        async with Client(mcp) as client:\n            # List with string integers\n            result = await client.call_tool(\"sum_numbers\", {\"numbers\": [\"1\", \"2\", \"3\"]})\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"6\"\n"
  },
  {
    "path": "tests/server/test_log_level.py",
    "content": "\"\"\"Test log_level parameter support in FastMCP server.\"\"\"\n\nimport asyncio\nfrom unittest.mock import AsyncMock, patch\n\nfrom fastmcp import FastMCP\n\n\nclass TestLogLevelParameter:\n    \"\"\"Test that log_level parameter is properly accepted by run methods.\"\"\"\n\n    async def test_run_stdio_accepts_log_level(self):\n        \"\"\"Test that run_stdio_async accepts log_level parameter.\"\"\"\n        server = FastMCP(\"TestServer\")\n\n        # Mock the stdio_server to avoid actual stdio operations\n        with patch(\"fastmcp.server.mixins.transport.stdio_server\") as mock_stdio:\n            mock_stdio.return_value.__aenter__ = AsyncMock(\n                return_value=(AsyncMock(), AsyncMock())\n            )\n            mock_stdio.return_value.__aexit__ = AsyncMock()\n\n            # Mock the underlying MCP server run method\n            with patch.object(server._mcp_server, \"run\", new_callable=AsyncMock):\n                try:\n                    # This should accept the log_level parameter without error\n                    await asyncio.wait_for(\n                        server.run_stdio_async(log_level=\"DEBUG\", show_banner=False),\n                        timeout=0.1,\n                    )\n                except asyncio.TimeoutError:\n                    pass  # Expected since we're mocking\n\n    async def test_run_http_accepts_log_level(self):\n        \"\"\"Test that run_http_async accepts log_level parameter.\"\"\"\n        server = FastMCP(\"TestServer\")\n\n        # Mock uvicorn to avoid actual server start\n        with patch(\n            \"fastmcp.server.mixins.transport.uvicorn.Server\"\n        ) as mock_server_class:\n            mock_instance = mock_server_class.return_value\n            mock_instance.serve = AsyncMock()\n\n            # This should accept the log_level parameter without error\n            await server.run_http_async(\n                log_level=\"INFO\", show_banner=False, host=\"127.0.0.1\", port=8000\n            )\n\n            # Verify serve was called\n            mock_instance.serve.assert_called_once()\n\n    async def test_run_async_passes_log_level(self):\n        \"\"\"Test that run_async passes log_level to transport methods.\"\"\"\n        server = FastMCP(\"TestServer\")\n\n        # Test stdio transport\n        with patch.object(\n            server, \"run_stdio_async\", new_callable=AsyncMock\n        ) as mock_stdio:\n            await server.run_async(transport=\"stdio\", log_level=\"WARNING\")\n            mock_stdio.assert_called_once_with(show_banner=True, log_level=\"WARNING\")\n\n        # Test http transport\n        with patch.object(\n            server, \"run_http_async\", new_callable=AsyncMock\n        ) as mock_http:\n            await server.run_async(transport=\"http\", log_level=\"ERROR\")\n            mock_http.assert_called_once_with(\n                transport=\"http\", show_banner=True, log_level=\"ERROR\"\n            )\n\n    def test_sync_run_accepts_log_level(self):\n        \"\"\"Test that the synchronous run method accepts log_level.\"\"\"\n        server = FastMCP(\"TestServer\")\n\n        with patch.object(server, \"run_async\", new_callable=AsyncMock):\n            # Mock anyio.run to avoid actual async execution\n            with patch(\"anyio.run\") as mock_anyio_run:\n                server.run(transport=\"stdio\", log_level=\"CRITICAL\")\n\n                # Verify anyio.run was called\n                mock_anyio_run.assert_called_once()\n\n                # Get the function that was passed to anyio.run\n                called_func = mock_anyio_run.call_args[0][0]\n\n                # The function should be a partial that includes log_level\n                assert hasattr(called_func, \"keywords\")\n                assert called_func.keywords.get(\"log_level\") == \"CRITICAL\"\n"
  },
  {
    "path": "tests/server/test_logging.py",
    "content": "import asyncio\nimport logging\nfrom unittest.mock import AsyncMock, Mock, patch\n\nimport anyio\nimport pytest\n\nfrom fastmcp.server.server import FastMCP\n\n\nclass CustomLogFormatterForTest(logging.Formatter):\n    def format(self, record: logging.LogRecord) -> str:\n        return f\"TEST_FORMAT::{record.levelname}::{record.name}::{record.getMessage()}\"\n\n\n@pytest.fixture\ndef mcp_server() -> FastMCP:\n    return FastMCP(name=\"TestLogServer\")\n\n\n@patch(\"fastmcp.server.mixins.transport.uvicorn.Server\")\n@patch(\"fastmcp.server.mixins.transport.uvicorn.Config\")\nasync def test_uvicorn_logging_default_level(\n    mock_uvicorn_config_constructor: Mock,\n    mock_uvicorn_server_constructor: Mock,\n    mcp_server: FastMCP,\n):\n    \"\"\"Tests that FastMCP passes log_level to uvicorn.Config if no log_config is given.\"\"\"\n    mock_server_instance = AsyncMock()\n    mock_uvicorn_server_constructor.return_value = mock_server_instance\n    serve_finished_event = anyio.Event()\n    mock_server_instance.serve.side_effect = serve_finished_event.wait\n\n    test_log_level = \"warning\"\n\n    server_task = asyncio.create_task(\n        mcp_server.run_http_async(log_level=test_log_level, port=8003)\n    )\n    await mcp_server._started.wait()\n\n    mock_uvicorn_config_constructor.assert_called_once()\n    _, kwargs_config = mock_uvicorn_config_constructor.call_args\n\n    assert kwargs_config.get(\"log_level\") == test_log_level.lower()\n    assert \"log_config\" not in kwargs_config\n\n    mock_uvicorn_server_constructor.assert_called_once_with(\n        mock_uvicorn_config_constructor.return_value\n    )\n    mock_server_instance.serve.assert_awaited_once()\n\n    # Signal the mock to finish and cancel with timeout\n    # Required for uvicorn 0.39+ due to context isolation\n    serve_finished_event.set()\n    server_task.cancel()\n    try:\n        await asyncio.wait_for(server_task, timeout=2.0)\n    except (asyncio.CancelledError, asyncio.TimeoutError):\n        pass\n\n\n@patch(\"fastmcp.server.mixins.transport.uvicorn.Server\")\n@patch(\"fastmcp.server.mixins.transport.uvicorn.Config\")\nasync def test_uvicorn_logging_with_custom_log_config(\n    mock_uvicorn_config_constructor: Mock,\n    mock_uvicorn_server_constructor: Mock,\n    mcp_server: FastMCP,\n):\n    \"\"\"Tests that FastMCP passes log_config to uvicorn.Config and not log_level.\"\"\"\n    mock_server_instance = AsyncMock()\n    mock_uvicorn_server_constructor.return_value = mock_server_instance\n    serve_finished_event = anyio.Event()\n    mock_server_instance.serve.side_effect = serve_finished_event.wait\n\n    sample_log_config = {\n        \"version\": 1,\n        \"disable_existing_loggers\": False,\n        \"formatters\": {\n            \"test_formatter\": {\n                \"()\": \"tests.server.test_logging.CustomLogFormatterForTest\"\n            }\n        },\n        \"handlers\": {\n            \"test_handler\": {\n                \"formatter\": \"test_formatter\",\n                \"class\": \"logging.StreamHandler\",\n                \"stream\": \"ext://sys.stdout\",\n            }\n        },\n        \"loggers\": {\n            \"uvicorn.error\": {\n                \"handlers\": [\"test_handler\"],\n                \"level\": \"INFO\",\n                \"propagate\": False,\n            }\n        },\n    }\n\n    server_task = asyncio.create_task(\n        mcp_server.run_http_async(\n            uvicorn_config={\"log_config\": sample_log_config}, port=8004\n        )\n    )\n    await mcp_server._started.wait()\n\n    mock_uvicorn_config_constructor.assert_called_once()\n    _, kwargs_config = mock_uvicorn_config_constructor.call_args\n\n    assert kwargs_config.get(\"log_config\") == sample_log_config\n    assert \"log_level\" not in kwargs_config\n\n    mock_uvicorn_server_constructor.assert_called_once_with(\n        mock_uvicorn_config_constructor.return_value\n    )\n    mock_server_instance.serve.assert_awaited_once()\n\n    # Signal the mock to finish and cancel with timeout\n    # Required for uvicorn 0.39+ due to context isolation\n    serve_finished_event.set()\n    server_task.cancel()\n    try:\n        await asyncio.wait_for(server_task, timeout=2.0)\n    except (asyncio.CancelledError, asyncio.TimeoutError):\n        pass\n\n\n@patch(\"fastmcp.server.mixins.transport.uvicorn.Server\")\n@patch(\"fastmcp.server.mixins.transport.uvicorn.Config\")\nasync def test_uvicorn_logging_custom_log_config_overrides_log_level_param(\n    mock_uvicorn_config_constructor: Mock,\n    mock_uvicorn_server_constructor: Mock,\n    mcp_server: FastMCP,\n):\n    \"\"\"Tests log_config precedence if log_level is also passed to run_http_async.\"\"\"\n    mock_server_instance = AsyncMock()\n    mock_uvicorn_server_constructor.return_value = mock_server_instance\n    serve_finished_event = anyio.Event()\n    mock_server_instance.serve.side_effect = serve_finished_event.wait\n\n    sample_log_config = {\n        \"version\": 1,\n        \"disable_existing_loggers\": False,\n        \"formatters\": {\n            \"test_formatter\": {\n                \"()\": \"tests.server.test_logging.CustomLogFormatterForTest\"\n            }\n        },\n        \"handlers\": {\n            \"test_handler\": {\n                \"formatter\": \"test_formatter\",\n                \"class\": \"logging.StreamHandler\",\n                \"stream\": \"ext://sys.stdout\",\n            }\n        },\n        \"loggers\": {\n            \"uvicorn.error\": {\n                \"handlers\": [\"test_handler\"],\n                \"level\": \"INFO\",\n                \"propagate\": False,\n            }\n        },\n    }\n    explicit_log_level = \"debug\"\n\n    server_task = asyncio.create_task(\n        mcp_server.run_http_async(\n            log_level=explicit_log_level,\n            uvicorn_config={\"log_config\": sample_log_config},\n            port=8005,\n        )\n    )\n    await mcp_server._started.wait()\n\n    mock_uvicorn_config_constructor.assert_called_once()\n    _, kwargs_config = mock_uvicorn_config_constructor.call_args\n\n    assert kwargs_config.get(\"log_config\") == sample_log_config\n    assert \"log_level\" not in kwargs_config\n\n    mock_uvicorn_server_constructor.assert_called_once_with(\n        mock_uvicorn_config_constructor.return_value\n    )\n    mock_server_instance.serve.assert_awaited_once()\n\n    # Signal the mock to finish and cancel with timeout\n    # Required for uvicorn 0.39+ due to context isolation\n    serve_finished_event.set()\n    server_task.cancel()\n    try:\n        await asyncio.wait_for(server_task, timeout=2.0)\n    except (asyncio.CancelledError, asyncio.TimeoutError):\n        pass\n"
  },
  {
    "path": "tests/server/test_pagination.py",
    "content": "\"\"\"Tests for MCP pagination support.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import patch\n\nimport mcp.types\nimport pytest\nfrom mcp.shared.exceptions import McpError\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.utilities.pagination import CursorState, paginate_sequence\n\n\nclass TestCursorEncoding:\n    \"\"\"Tests for cursor encoding/decoding.\"\"\"\n\n    def test_encode_decode_roundtrip(self) -> None:\n        \"\"\"Cursor should survive encode/decode roundtrip.\"\"\"\n        state = CursorState(offset=100)\n        encoded = state.encode()\n        decoded = CursorState.decode(encoded)\n        assert decoded.offset == 100\n\n    def test_encode_produces_string(self) -> None:\n        \"\"\"Encoded cursor should be a string.\"\"\"\n        state = CursorState(offset=50)\n        encoded = state.encode()\n        assert isinstance(encoded, str)\n        assert len(encoded) > 0\n\n    def test_decode_invalid_base64_raises(self) -> None:\n        \"\"\"Invalid base64 should raise ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Invalid cursor\"):\n            CursorState.decode(\"not-valid-base64!!!\")\n\n    def test_decode_invalid_json_raises(self) -> None:\n        \"\"\"Valid base64 but invalid JSON should raise ValueError.\"\"\"\n        import base64\n\n        invalid = base64.urlsafe_b64encode(b\"not json\").decode()\n        with pytest.raises(ValueError, match=\"Invalid cursor\"):\n            CursorState.decode(invalid)\n\n    def test_decode_missing_offset_raises(self) -> None:\n        \"\"\"JSON missing the offset key should raise ValueError.\"\"\"\n        import base64\n        import json\n\n        invalid = base64.urlsafe_b64encode(json.dumps({\"x\": 1}).encode()).decode()\n        with pytest.raises(ValueError, match=\"Invalid cursor\"):\n            CursorState.decode(invalid)\n\n\nclass TestPaginateSequence:\n    \"\"\"Tests for the paginate_sequence helper.\"\"\"\n\n    def test_first_page_no_cursor(self) -> None:\n        \"\"\"First page should start from beginning.\"\"\"\n        items = list(range(25))\n        page, cursor = paginate_sequence(items, None, 10)\n        assert page == list(range(10))\n        assert cursor is not None\n\n    def test_second_page_with_cursor(self) -> None:\n        \"\"\"Second page should continue from cursor.\"\"\"\n        items = list(range(25))\n        _, cursor = paginate_sequence(items, None, 10)\n        page, next_cursor = paginate_sequence(items, cursor, 10)\n        assert page == list(range(10, 20))\n        assert next_cursor is not None\n\n    def test_last_page_returns_none_cursor(self) -> None:\n        \"\"\"Last page should return None cursor.\"\"\"\n        items = list(range(25))\n        _, c1 = paginate_sequence(items, None, 10)\n        _, c2 = paginate_sequence(items, c1, 10)\n        page, next_cursor = paginate_sequence(items, c2, 10)\n        assert page == list(range(20, 25))\n        assert next_cursor is None\n\n    def test_empty_list(self) -> None:\n        \"\"\"Empty list should return empty page and no cursor.\"\"\"\n        page, cursor = paginate_sequence([], None, 10)\n        assert page == []\n        assert cursor is None\n\n    def test_exact_page_size(self) -> None:\n        \"\"\"List exactly matching page size should return no cursor.\"\"\"\n        items = list(range(10))\n        page, cursor = paginate_sequence(items, None, 10)\n        assert page == items\n        assert cursor is None\n\n    def test_smaller_than_page_size(self) -> None:\n        \"\"\"List smaller than page size should return all items.\"\"\"\n        items = list(range(5))\n        page, cursor = paginate_sequence(items, None, 10)\n        assert page == items\n        assert cursor is None\n\n    def test_invalid_cursor_raises(self) -> None:\n        \"\"\"Invalid cursor should raise ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Invalid cursor\"):\n            paginate_sequence([1, 2, 3], \"invalid!\", 10)\n\n\nclass TestServerPagination:\n    \"\"\"Integration tests for server pagination.\"\"\"\n\n    async def test_tools_pagination_returns_all_tools(self) -> None:\n        \"\"\"Client should receive all tools across paginated requests.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        for i in range(25):\n\n            @server.tool(name=f\"tool_{i}\")\n            def make_tool() -> str:\n                return \"ok\"\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 25\n            tool_names = {t.name for t in tools}\n            assert tool_names == {f\"tool_{i}\" for i in range(25)}\n\n    async def test_resources_pagination_returns_all_resources(self) -> None:\n        \"\"\"Client should receive all resources across paginated requests.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        for i in range(25):\n\n            @server.resource(f\"test://resource_{i}\")\n            def make_resource() -> str:\n                return \"data\"\n\n        async with Client(server) as client:\n            resources = await client.list_resources()\n            assert len(resources) == 25\n\n    async def test_prompts_pagination_returns_all_prompts(self) -> None:\n        \"\"\"Client should receive all prompts across paginated requests.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        for i in range(25):\n\n            @server.prompt(name=f\"prompt_{i}\")\n            def make_prompt() -> str:\n                return \"text\"\n\n        async with Client(server) as client:\n            prompts = await client.list_prompts()\n            assert len(prompts) == 25\n\n    async def test_manual_pagination(self) -> None:\n        \"\"\"Client can manually paginate using cursor.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        for i in range(25):\n\n            @server.tool(name=f\"tool_{i}\")\n            def make_tool() -> str:\n                return \"ok\"\n\n        async with Client(server) as client:\n            # First page\n            result = await client.list_tools_mcp()\n            assert len(result.tools) == 10\n            assert result.nextCursor is not None\n\n            # Second page\n            result2 = await client.list_tools_mcp(cursor=result.nextCursor)\n            assert len(result2.tools) == 10\n            assert result2.nextCursor is not None\n\n            # Third (last) page\n            result3 = await client.list_tools_mcp(cursor=result2.nextCursor)\n            assert len(result3.tools) == 5\n            assert result3.nextCursor is None\n\n    async def test_invalid_cursor_returns_error(self) -> None:\n        \"\"\"Server should return MCP error for invalid cursor.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        @server.tool\n        def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(server) as client:\n            with pytest.raises(McpError) as exc:\n                await client.list_tools_mcp(cursor=\"invalid!\")\n            assert exc.value.error.code == -32602\n\n    async def test_no_pagination_when_disabled(self) -> None:\n        \"\"\"Without list_page_size, all items returned at once.\"\"\"\n        server = FastMCP()  # No pagination\n\n        for i in range(25):\n\n            @server.tool(name=f\"tool_{i}\")\n            def make_tool() -> str:\n                return \"ok\"\n\n        async with Client(server) as client:\n            result = await client.list_tools_mcp()\n            assert len(result.tools) == 25\n            assert result.nextCursor is None\n\n    async def test_pagination_exact_page_boundary(self) -> None:\n        \"\"\"Test pagination at exact page boundaries.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        for i in range(20):  # Exactly 2 pages\n\n            @server.tool(name=f\"tool_{i}\")\n            def make_tool() -> str:\n                return \"ok\"\n\n        async with Client(server) as client:\n            # First page\n            result = await client.list_tools_mcp()\n            assert len(result.tools) == 10\n            assert result.nextCursor is not None\n\n            # Second (last) page\n            result2 = await client.list_tools_mcp(cursor=result.nextCursor)\n            assert len(result2.tools) == 10\n            assert result2.nextCursor is None\n\n\nclass TestPageSizeValidation:\n    \"\"\"Tests for list_page_size validation.\"\"\"\n\n    def test_zero_page_size_raises(self) -> None:\n        \"\"\"Zero page size should raise ValueError.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"list_page_size must be a positive integer\"\n        ):\n            FastMCP(list_page_size=0)\n\n    def test_negative_page_size_raises(self) -> None:\n        \"\"\"Negative page size should raise ValueError.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"list_page_size must be a positive integer\"\n        ):\n            FastMCP(list_page_size=-1)\n\n\nclass TestPaginationCycleDetection:\n    \"\"\"Tests that auto-pagination terminates when the server returns cycling cursors.\"\"\"\n\n    async def test_tools_constant_cursor_terminates(self) -> None:\n        \"\"\"list_tools should stop if the server always returns the same cursor.\"\"\"\n        server = FastMCP()\n\n        @server.tool\n        def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(server) as client:\n            original = client.list_tools_mcp\n\n            async def returning_constant_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListToolsResult:\n                result = await original(cursor=cursor)\n                result.nextCursor = \"stuck\"\n                return result\n\n            with patch.object(\n                client, \"list_tools_mcp\", side_effect=returning_constant_cursor\n            ):\n                tools = await client.list_tools()\n\n            # Should get tools from first page + one duplicate (the retry before\n            # detecting the cycle), then stop.\n            assert len(tools) == 2\n            assert all(t.name == \"my_tool\" for t in tools)\n\n    async def test_prompts_constant_cursor_terminates(self) -> None:\n        \"\"\"list_prompts should stop if the server always returns the same cursor.\"\"\"\n        server = FastMCP()\n\n        @server.prompt\n        def my_prompt() -> str:\n            return \"text\"\n\n        async with Client(server) as client:\n            original = client.list_prompts_mcp\n\n            async def returning_constant_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListPromptsResult:\n                result = await original(cursor=cursor)\n                result.nextCursor = \"stuck\"\n                return result\n\n            with patch.object(\n                client, \"list_prompts_mcp\", side_effect=returning_constant_cursor\n            ):\n                prompts = await client.list_prompts()\n\n            assert len(prompts) == 2\n            assert all(p.name == \"my_prompt\" for p in prompts)\n\n    async def test_resources_constant_cursor_terminates(self) -> None:\n        \"\"\"list_resources should stop if the server always returns the same cursor.\"\"\"\n        server = FastMCP()\n\n        @server.resource(\"test://r\")\n        def my_resource() -> str:\n            return \"data\"\n\n        async with Client(server) as client:\n            original = client.list_resources_mcp\n\n            async def returning_constant_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListResourcesResult:\n                result = await original(cursor=cursor)\n                result.nextCursor = \"stuck\"\n                return result\n\n            with patch.object(\n                client, \"list_resources_mcp\", side_effect=returning_constant_cursor\n            ):\n                resources = await client.list_resources()\n\n            assert len(resources) == 2\n            assert all(r.name == \"my_resource\" for r in resources)\n\n    async def test_resource_templates_constant_cursor_terminates(self) -> None:\n        \"\"\"list_resource_templates should stop if the server always returns the same cursor.\"\"\"\n        server = FastMCP()\n\n        @server.resource(\"test://items/{item_id}\")\n        def my_template(item_id: str) -> str:\n            return item_id\n\n        async with Client(server) as client:\n            original = client.list_resource_templates_mcp\n\n            async def returning_constant_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListResourceTemplatesResult:\n                result = await original(cursor=cursor)\n                result.nextCursor = \"stuck\"\n                return result\n\n            with patch.object(\n                client,\n                \"list_resource_templates_mcp\",\n                side_effect=returning_constant_cursor,\n            ):\n                templates = await client.list_resource_templates()\n\n            assert len(templates) == 2\n\n    async def test_cycling_cursors_terminates(self) -> None:\n        \"\"\"list_tools should stop if the server cycles through a set of cursors.\"\"\"\n        server = FastMCP()\n\n        @server.tool\n        def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(server) as client:\n            call_count = 0\n            original = client.list_tools_mcp\n\n            async def returning_cycling_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListToolsResult:\n                nonlocal call_count\n                result = await original(cursor=cursor)\n                # Cycle through A -> B -> C -> A\n                cursors = [\"A\", \"B\", \"C\"]\n                result.nextCursor = cursors[call_count % 3]\n                call_count += 1\n                return result\n\n            with patch.object(\n                client, \"list_tools_mcp\", side_effect=returning_cycling_cursor\n            ):\n                tools = await client.list_tools()\n\n            # A, B, C seen, then A is a duplicate → 4 calls total\n            assert call_count == 4\n            assert len(tools) == 4\n\n    async def test_empty_string_cursor_terminates(self) -> None:\n        \"\"\"list_tools should stop if the server returns an empty string cursor.\"\"\"\n        server = FastMCP()\n\n        @server.tool\n        def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(server) as client:\n            original = client.list_tools_mcp\n\n            async def returning_empty_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListToolsResult:\n                result = await original(cursor=cursor)\n                result.nextCursor = \"\"\n                return result\n\n            with patch.object(\n                client, \"list_tools_mcp\", side_effect=returning_empty_cursor\n            ):\n                tools = await client.list_tools()\n\n            assert len(tools) == 1\n            assert tools[0].name == \"my_tool\"\n\n    async def test_tools_raises_on_auto_pagination_limit(self) -> None:\n        \"\"\"list_tools should raise RuntimeError after exceeding max_pages.\"\"\"\n        server = FastMCP()\n\n        @server.tool\n        def my_tool() -> str:\n            return \"ok\"\n\n        async with Client(server) as client:\n            original = client.list_tools_mcp\n            call_count = 0\n\n            async def returning_unique_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListToolsResult:\n                nonlocal call_count\n                result = await original(cursor=cursor)\n                call_count += 1\n                result.nextCursor = f\"cursor-{call_count}\"\n                return result\n\n            with (\n                patch.object(\n                    client, \"list_tools_mcp\", side_effect=returning_unique_cursor\n                ),\n                pytest.raises(RuntimeError, match=\"auto-pagination limit\"),\n            ):\n                await client.list_tools(max_pages=5)\n\n    async def test_resources_raises_on_auto_pagination_limit(self) -> None:\n        \"\"\"list_resources should raise RuntimeError after exceeding max_pages.\"\"\"\n        server = FastMCP()\n\n        @server.resource(\"test://r\")\n        def my_resource() -> str:\n            return \"data\"\n\n        async with Client(server) as client:\n            original = client.list_resources_mcp\n            call_count = 0\n\n            async def returning_unique_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListResourcesResult:\n                nonlocal call_count\n                result = await original(cursor=cursor)\n                call_count += 1\n                result.nextCursor = f\"cursor-{call_count}\"\n                return result\n\n            with (\n                patch.object(\n                    client, \"list_resources_mcp\", side_effect=returning_unique_cursor\n                ),\n                pytest.raises(RuntimeError, match=\"auto-pagination limit\"),\n            ):\n                await client.list_resources(max_pages=5)\n\n    async def test_prompts_raises_on_auto_pagination_limit(self) -> None:\n        \"\"\"list_prompts should raise RuntimeError after exceeding max_pages.\"\"\"\n        server = FastMCP()\n\n        @server.prompt\n        def my_prompt() -> str:\n            return \"text\"\n\n        async with Client(server) as client:\n            original = client.list_prompts_mcp\n            call_count = 0\n\n            async def returning_unique_cursor(\n                *,\n                cursor: str | None = None,\n            ) -> mcp.types.ListPromptsResult:\n                nonlocal call_count\n                result = await original(cursor=cursor)\n                call_count += 1\n                result.nextCursor = f\"cursor-{call_count}\"\n                return result\n\n            with (\n                patch.object(\n                    client, \"list_prompts_mcp\", side_effect=returning_unique_cursor\n                ),\n                pytest.raises(RuntimeError, match=\"auto-pagination limit\"),\n            ):\n                await client.list_prompts(max_pages=5)\n\n    async def test_normal_pagination_unaffected(self) -> None:\n        \"\"\"Cycle detection should not interfere with normal pagination.\"\"\"\n        server = FastMCP(list_page_size=10)\n\n        for i in range(25):\n\n            @server.tool(name=f\"tool_{i}\")\n            def make_tool() -> str:\n                return \"ok\"\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 25\n            assert len({t.name for t in tools}) == 25\n"
  },
  {
    "path": "tests/server/test_providers.py",
    "content": "\"\"\"Tests for providers.\"\"\"\n\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport pytest\nfrom mcp.types import AnyUrl, TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.prompts.function_prompt import FunctionPrompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.function_resource import FunctionResource\nfrom fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate\nfrom fastmcp.server.providers import Provider\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.versions import VersionSpec\n\n\nclass SimpleTool(Tool):\n    \"\"\"A simple tool for testing that performs a configured operation.\"\"\"\n\n    operation: str\n    value: int = 0\n\n    async def run(self, arguments: dict[str, Any]) -> ToolResult:\n        a = arguments.get(\"a\", 0)\n        b = arguments.get(\"b\", 0)\n\n        if self.operation == \"add\":\n            result = a + b + self.value\n        elif self.operation == \"multiply\":\n            result = a * b + self.value\n        else:\n            result = a + b\n\n        return ToolResult(\n            structured_content={\"result\": result, \"operation\": self.operation}\n        )\n\n\nclass SimpleToolProvider(Provider):\n    \"\"\"A simple provider that returns a configurable list of tools.\"\"\"\n\n    def __init__(self, tools: Sequence[Tool] | None = None):\n        super().__init__()\n        self._tools = list(tools) if tools else []\n        self.list_tools_call_count = 0\n        self.get_tool_call_count = 0\n\n    async def _list_tools(self) -> list[Tool]:\n        self.list_tools_call_count += 1\n        return self._tools\n\n    async def _get_tool(\n        self, name: str, version: VersionSpec | None = None\n    ) -> Tool | None:\n        self.get_tool_call_count += 1\n        matching = [t for t in self._tools if t.name == name]\n        if not matching:\n            return None\n        if version is None:\n            return matching[0]  # Return first (for testing simplicity)\n        matching = [t for t in matching if version.matches(t.version)]\n        return matching[0] if matching else None\n\n\nclass ListOnlyProvider(Provider):\n    \"\"\"A provider that only implements list_tools (uses default get_tool).\"\"\"\n\n    def __init__(self, tools: Sequence[Tool]):\n        super().__init__()\n        self._tools = list(tools)\n        self.list_tools_call_count = 0\n\n    async def _list_tools(self) -> list[Tool]:\n        self.list_tools_call_count += 1\n        return self._tools\n\n\nclass TestProvider:\n    \"\"\"Tests for Provider.\"\"\"\n\n    @pytest.fixture\n    def base_server(self):\n        \"\"\"Create a base FastMCP server with static tools.\"\"\"\n        mcp = FastMCP(\"BaseServer\")\n\n        @mcp.tool\n        def static_add(a: int, b: int) -> int:\n            \"\"\"Add two numbers (static tool).\"\"\"\n            return a + b\n\n        @mcp.tool\n        def static_subtract(a: int, b: int) -> int:\n            \"\"\"Subtract two numbers (static tool).\"\"\"\n            return a - b\n\n        return mcp\n\n    @pytest.fixture\n    def dynamic_tools(self) -> list[Tool]:\n        \"\"\"Create dynamic tools for testing.\"\"\"\n        return [\n            SimpleTool(\n                name=\"dynamic_multiply\",\n                description=\"Multiply two numbers\",\n                parameters={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"a\": {\"type\": \"integer\"},\n                        \"b\": {\"type\": \"integer\"},\n                    },\n                },\n                operation=\"multiply\",\n            ),\n            SimpleTool(\n                name=\"dynamic_add\",\n                description=\"Add two numbers with offset\",\n                parameters={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"a\": {\"type\": \"integer\"},\n                        \"b\": {\"type\": \"integer\"},\n                    },\n                },\n                operation=\"add\",\n                value=100,\n            ),\n        ]\n\n    async def test_list_tools_includes_dynamic_tools(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that list_tools returns both static and dynamic tools.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        tools = await base_server.list_tools()\n\n        # Should have all tools: 2 static + 2 dynamic\n        assert len(tools) == 4\n        tool_names = [tool.name for tool in tools]\n        assert \"static_add\" in tool_names\n        assert \"static_subtract\" in tool_names\n        assert \"dynamic_multiply\" in tool_names\n        assert \"dynamic_add\" in tool_names\n\n    async def test_list_tools_calls_provider_each_time(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that provider.list_tools() is called on every list_tools request.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        # Call get_tools multiple times\n        await base_server.list_tools()\n        await base_server.list_tools()\n        await base_server.list_tools()\n\n        # Provider should have been called 3 times (once per get_tools call)\n        assert provider.list_tools_call_count == 3\n\n    async def test_call_dynamic_tool(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that dynamic tools can be called successfully.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        result = await base_server.call_tool(\n            name=\"dynamic_multiply\", arguments={\"a\": 7, \"b\": 6}\n        )\n\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 42\n        assert result.structured_content[\"operation\"] == \"multiply\"\n\n    async def test_call_dynamic_tool_with_config(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that dynamic tool config (like value offset) is used.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        result = await base_server.call_tool(\n            name=\"dynamic_add\", arguments={\"a\": 5, \"b\": 3}\n        )\n\n        assert result.structured_content is not None\n        # 5 + 3 + 100 (value offset) = 108\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 108\n\n    async def test_call_static_tool_still_works(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that static tools still work after adding dynamic tools.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        result = await base_server.call_tool(\n            name=\"static_add\", arguments={\"a\": 10, \"b\": 5}\n        )\n\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 15\n\n    async def test_call_tool_uses_get_tool_for_efficient_lookup(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that call_tool uses get_tool() for efficient single-tool lookup.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        await base_server.call_tool(name=\"dynamic_multiply\", arguments={\"a\": 2, \"b\": 3})\n\n        # get_tool is called once for efficient lookup:\n        # call_tool() calls provider.get_tool() to get the tool and execute it\n        # Key point: list_tools is NOT called during tool execution (efficient lookup)\n        assert provider.get_tool_call_count == 1\n\n    async def test_default_get_tool_falls_back_to_list(self, base_server: FastMCP):\n        \"\"\"Test that BaseToolProvider's default get_tool calls list_tools.\"\"\"\n        tools = [\n            SimpleTool(\n                name=\"test_tool\",\n                description=\"A test tool\",\n                parameters={\"type\": \"object\", \"properties\": {}},\n                operation=\"add\",\n            ),\n        ]\n        provider = ListOnlyProvider(tools=tools)\n        base_server.add_provider(provider)\n\n        result = await base_server.call_tool(\n            name=\"test_tool\", arguments={\"a\": 1, \"b\": 2}\n        )\n\n        assert result.structured_content is not None\n        # Default get_tool should have called list_tools\n        assert provider.list_tools_call_count >= 1\n\n    async def test_local_tools_come_first(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that local tools (from LocalProvider) appear before other provider tools.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        tools = await base_server.list_tools()\n\n        tool_names = [tool.name for tool in tools]\n        # Local tools should come first (LocalProvider is first in _providers)\n        assert tool_names[:2] == [\"static_add\", \"static_subtract\"]\n\n    async def test_empty_provider(self, base_server: FastMCP):\n        \"\"\"Test that empty provider doesn't affect behavior.\"\"\"\n        provider = SimpleToolProvider(tools=[])\n        base_server.add_provider(provider)\n\n        tools = await base_server.list_tools()\n\n        # Should only have static tools\n        assert len(tools) == 2\n\n    async def test_tool_not_found_falls_through_to_static(\n        self, base_server: FastMCP, dynamic_tools: list[Tool]\n    ):\n        \"\"\"Test that unknown tool name falls through to static tools.\"\"\"\n        provider = SimpleToolProvider(tools=dynamic_tools)\n        base_server.add_provider(provider)\n\n        # This tool is static, not in the dynamic provider\n        result = await base_server.call_tool(\n            name=\"static_subtract\", arguments={\"a\": 10, \"b\": 3}\n        )\n\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 7\n\n\nclass TestProviderClass:\n    \"\"\"Tests for the Provider class.\"\"\"\n\n    async def test_subclass_is_instance(self):\n        \"\"\"Test that subclasses are instances of Provider.\"\"\"\n        provider = SimpleToolProvider(tools=[])\n        assert isinstance(provider, Provider)\n\n    async def test_default_get_tool_works(self):\n        \"\"\"Test that the default get_tool implementation works.\"\"\"\n        tool = SimpleTool(\n            name=\"test\",\n            description=\"Test\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n            operation=\"add\",\n        )\n        provider = ListOnlyProvider(tools=[tool])\n\n        # Default get_tool should find by name\n        found = await provider.get_tool(\"test\")\n        assert found is not None\n        assert found.name == \"test\"\n\n        # Should return None for unknown names\n        not_found = await provider.get_tool(\"unknown\")\n        assert not_found is None\n\n\nclass TestDynamicToolUpdates:\n    \"\"\"Tests demonstrating dynamic tool updates without restart.\"\"\"\n\n    async def test_tools_update_without_restart(self):\n        \"\"\"Test that tools can be updated dynamically.\"\"\"\n        mcp = FastMCP(\"DynamicServer\")\n\n        # Start with one tool\n        initial_tools = [\n            SimpleTool(\n                name=\"tool_v1\",\n                description=\"Version 1\",\n                parameters={\"type\": \"object\", \"properties\": {}},\n                operation=\"add\",\n            ),\n        ]\n        provider = SimpleToolProvider(tools=initial_tools)\n        mcp.add_provider(provider)\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"tool_v1\"\n\n        # Update the provider's tools (simulating DB update)\n        provider._tools = [\n            SimpleTool(\n                name=\"tool_v2\",\n                description=\"Version 2\",\n                parameters={\"type\": \"object\", \"properties\": {}},\n                operation=\"multiply\",\n            ),\n            SimpleTool(\n                name=\"tool_v3\",\n                description=\"Version 3\",\n                parameters={\"type\": \"object\", \"properties\": {}},\n                operation=\"add\",\n            ),\n        ]\n\n        # List tools again - should see new tools\n        tools = await mcp.list_tools()\n        assert len(tools) == 2\n        tool_names = [t.name for t in tools]\n        assert \"tool_v1\" not in tool_names\n        assert \"tool_v2\" in tool_names\n        assert \"tool_v3\" in tool_names\n\n\nclass TestProviderExecutionMethods:\n    \"\"\"Tests for Provider execution methods (call_tool, read_resource, render_prompt).\"\"\"\n\n    async def test_call_tool_default_implementation(self):\n        \"\"\"Test that default call_tool uses get_tool and runs the tool.\"\"\"\n        tool = SimpleTool(\n            name=\"test_tool\",\n            description=\"Test\",\n            parameters={\"type\": \"object\", \"properties\": {\"a\": {}, \"b\": {}}},\n            operation=\"add\",\n        )\n        provider = SimpleToolProvider(tools=[tool])\n        mcp = FastMCP(\"TestServer\")\n        mcp.add_provider(provider)\n\n        result = await mcp.call_tool(\"test_tool\", {\"a\": 1, \"b\": 2})\n\n        assert result.structured_content is not None\n        assert isinstance(result.structured_content, dict)\n        assert result.structured_content[\"result\"] == 3\n\n    async def test_read_resource_default_implementation(self):\n        \"\"\"Test that default read_resource uses get_resource and reads it.\"\"\"\n\n        class ResourceProvider(Provider):\n            async def _list_resources(self) -> Sequence[Resource]:\n                return [\n                    FunctionResource(\n                        uri=AnyUrl(\"test://data\"),\n                        name=\"Test Data\",\n                        fn=lambda: \"hello world\",\n                    )\n                ]\n\n        provider = ResourceProvider()\n        mcp = FastMCP(\"TestServer\")\n        mcp.add_provider(provider)\n\n        result = await mcp.read_resource(\"test://data\")\n\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"hello world\"\n\n    async def test_read_resource_template_default(self):\n        \"\"\"Test that read_resource_template handles template-based resources.\"\"\"\n\n        class TemplateProvider(Provider):\n            async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:\n                return [\n                    FunctionResourceTemplate.from_function(\n                        fn=lambda name: f\"content of {name}\",\n                        uri_template=\"data://files/{name}\",\n                        name=\"Data Template\",\n                    )\n                ]\n\n        provider = TemplateProvider()\n        mcp = FastMCP(\"TestServer\")\n        mcp.add_provider(provider)\n\n        result = await mcp.read_resource(\"data://files/test.txt\")\n\n        assert len(result.contents) == 1\n        assert result.contents[0].content == \"content of test.txt\"\n\n    async def test_render_prompt_default_implementation(self):\n        \"\"\"Test that default render_prompt uses get_prompt and renders it.\"\"\"\n\n        class PromptProvider(Provider):\n            async def _list_prompts(self) -> Sequence[Prompt]:\n                return [\n                    FunctionPrompt.from_function(\n                        fn=lambda name: f\"Hello, {name}!\",\n                        name=\"greeting\",\n                        description=\"Greet someone\",\n                    )\n                ]\n\n        provider = PromptProvider()\n        mcp = FastMCP(\"TestServer\")\n        mcp.add_provider(provider)\n\n        result = await mcp.render_prompt(\"greeting\", {\"name\": \"World\"})\n\n        assert len(result.messages) == 1\n        assert isinstance(result.messages[0].content, TextContent)\n        assert result.messages[0].content.text == \"Hello, World!\"\n"
  },
  {
    "path": "tests/server/test_run_server.py",
    "content": "# from pathlib import Path\n# from typing import TYPE_CHECKING, Any\n\n# import pytest\n\n# import fastmcp\n# from fastmcp import FastMCP\n\n# if TYPE_CHECKING:\n#     pass\n\n# USERS = [\n#     {\"id\": \"1\", \"name\": \"Alice\", \"active\": True},\n#     {\"id\": \"2\", \"name\": \"Bob\", \"active\": True},\n#     {\"id\": \"3\", \"name\": \"Charlie\", \"active\": False},\n# ]\n\n\n# @pytest.fixture\n# def fastmcp_server():\n#     server = FastMCP(\"TestServer\")\n\n#     # --- Tools ---\n\n#     @server.tool\n#     def greet(name: str) -> str:\n#         \"\"\"Greet someone by name.\"\"\"\n#         return f\"Hello, {name}!\"\n\n#     @server.tool\n#     def add(a: int, b: int) -> int:\n#         \"\"\"Add two numbers together.\"\"\"\n#         return a + b\n\n#     @server.tool\n#     def error_tool():\n#         \"\"\"This tool always raises an error.\"\"\"\n#         raise ValueError(\"This is a test error\")\n\n#     # --- Resources ---\n\n#     @server.resource(uri=\"resource://wave\")\n#     def wave() -> str:\n#         return \"👋\"\n\n#     @server.resource(uri=\"data://users\")\n#     async def get_users() -> list[dict[str, Any]]:\n#         return USERS\n\n#     @server.resource(uri=\"data://user/{user_id}\")\n#     async def get_user(user_id: str) -> dict[str, Any] | None:\n#         return next((user for user in USERS if user[\"id\"] == user_id), None)\n\n#     # --- Prompts ---\n\n#     @server.prompt\n#     def welcome(name: str) -> str:\n#         return f\"Welcome to FastMCP, {name}!\"\n\n#     return server\n\n\n# @pytest.fixture\n# async def stdio_client():\n#     # Find the stdio.py script path\n#     base_dir = Path(__file__).parent\n#     stdio_script = base_dir / \"test_servers\" / \"stdio.py\"\n\n#     if not stdio_script.exists():\n#         raise FileNotFoundError(f\"Could not find stdio.py script at {stdio_script}\")\n\n#     client = fastmcp.Client(\n#         transport=fastmcp.client.transports.StdioTransport(\n#             command=\"python\",\n#             args=[str(stdio_script)],\n#         )\n#     )\n\n#     async with client:\n#         print(\"READY\")\n#         yield client\n#         print(\"DONE\")\n\n\n# class TestRunServerStdio:\n#     async def test_run_server_stdio(\n#         self, fastmcp_server: FastMCP, stdio_client: fastmcp.Client\n#     ):\n#         print(\"TEST\")\n#         tools = await stdio_client.list_tools()\n#         print(\"TEST 2\")\n#         assert tools == 1\n\n\n# class TestRunServerSSE:\n#\n#     async def test_run_server_sse(self, fastmcp_server: FastMCP):\n#         pass\n"
  },
  {
    "path": "tests/server/test_server.py",
    "content": "import os\nimport warnings\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom textwrap import dedent\nfrom unittest import mock\n\nfrom mcp.types import TextContent, TextResourceContents\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.providers import LocalProvider\nfrom fastmcp.tools import FunctionTool\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.tests import temporary_settings\n\n\nclass TestCreateServer:\n    async def test_create_server(self):\n        mcp = FastMCP(instructions=\"Server instructions\")\n        assert mcp.name.startswith(\"FastMCP-\")\n        assert mcp.instructions == \"Server instructions\"\n\n    async def test_change_instruction(self):\n        mcp = FastMCP(instructions=\"Server instructions\")\n        assert mcp.instructions == \"Server instructions\"\n        mcp.instructions = \"New instructions\"\n        assert mcp.instructions == \"New instructions\"\n\n    async def test_non_ascii_description(self):\n        \"\"\"Test that FastMCP handles non-ASCII characters in descriptions correctly\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(\n            description=(\n                \"🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉\"\n            )\n        )\n        def hello_world(name: str = \"世界\") -> str:\n            return f\"¡Hola, {name}! 👋\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            tool = tools[0]\n            assert tool.description is not None\n            assert \"🌟\" in tool.description\n            assert \"漢字\" in tool.description\n            assert \"🎉\" in tool.description\n\n            result = await client.call_tool(\"hello_world\", {})\n            assert result.data == \"¡Hola, 世界! 👋\"\n\n\nclass TestServerDelegation:\n    \"\"\"Test that FastMCP properly delegates to LocalProvider.\"\"\"\n\n    async def test_tool_decorator_delegates_to_local_provider(self):\n        \"\"\"Test that @mcp.tool registers with the local provider.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        # Verify the tool is in the local provider\n        tool = await mcp._local_provider.get_tool(\"my_tool\")\n        assert tool is not None\n        assert tool.name == \"my_tool\"\n\n    async def test_resource_decorator_delegates_to_local_provider(self):\n        \"\"\"Test that @mcp.resource registers with the local provider.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://test\")\n        def my_resource() -> str:\n            return \"content\"\n\n        # Verify the resource is in the local provider\n        resource = await mcp._local_provider.get_resource(\"resource://test\")\n        assert resource is not None\n\n    async def test_prompt_decorator_delegates_to_local_provider(self):\n        \"\"\"Test that @mcp.prompt registers with the local provider.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def my_prompt() -> str:\n            return \"prompt content\"\n\n        # Verify the prompt is in the local provider\n        prompt = await mcp._local_provider.get_prompt(\"my_prompt\")\n        assert prompt is not None\n        assert prompt.name == \"my_prompt\"\n\n    async def test_add_tool_delegates_to_local_provider(self):\n        \"\"\"Test that mcp.add_tool() registers with the local provider.\"\"\"\n        mcp = FastMCP()\n\n        def standalone_tool() -> str:\n            return \"result\"\n\n        mcp.add_tool(FunctionTool.from_function(standalone_tool))\n\n        # Verify the tool is in the local provider\n        tool = await mcp._local_provider.get_tool(\"standalone_tool\")\n        assert tool is not None\n        assert tool.name == \"standalone_tool\"\n\n    async def test_get_tools_includes_local_provider_tools(self):\n        \"\"\"Test that get_tools() returns tools from local provider.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def local_tool() -> str:\n            return \"local\"\n\n        tools = await mcp.list_tools()\n        assert any(t.name == \"local_tool\" for t in tools)\n\n\nclass TestLocalProviderProperty:\n    \"\"\"Test the public local_provider property.\"\"\"\n\n    async def test_local_provider_returns_local_provider(self):\n        mcp = FastMCP()\n        assert isinstance(mcp.local_provider, LocalProvider)\n        assert mcp.local_provider is mcp._local_provider\n\n    async def test_remove_tool_via_local_provider(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        assert await mcp.local_provider.get_tool(\"my_tool\") is not None\n        mcp.local_provider.remove_tool(\"my_tool\")\n        tools = await mcp.list_tools()\n        assert not any(t.name == \"my_tool\" for t in tools)\n\n    async def test_remove_resource_via_local_provider(self):\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://test\")\n        def my_resource() -> str:\n            return \"data\"\n\n        mcp.local_provider.remove_resource(\"resource://test\")\n        resources = await mcp.list_resources()\n        assert not any(r.uri == \"resource://test\" for r in resources)\n\n    async def test_remove_prompt_via_local_provider(self):\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def my_prompt() -> str:\n            return \"hello\"\n\n        mcp.local_provider.remove_prompt(\"my_prompt\")\n        prompts = await mcp.list_prompts()\n        assert not any(p.name == \"my_prompt\" for p in prompts)\n\n\nclass TestRemoveToolDeprecation:\n    async def test_remove_tool_emits_deprecation_warning(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            mcp.remove_tool(\"my_tool\")\n\n            assert len(w) == 1\n            assert issubclass(w[0].category, DeprecationWarning)\n            assert \"local_provider\" in str(w[0].message)\n\n    async def test_remove_tool_still_works(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", DeprecationWarning)\n            mcp.remove_tool(\"my_tool\")\n\n        tools = await mcp.list_tools()\n        assert not any(t.name == \"my_tool\" for t in tools)\n\n\nclass TestResourcePrefixMounting:\n    \"\"\"Test resource prefixing in mounted servers.\"\"\"\n\n    async def test_mounted_server_resource_prefixing(self):\n        \"\"\"Test that resources in mounted servers use the correct prefix format.\"\"\"\n        # Create a server with resources\n        server = FastMCP(name=\"ResourceServer\")\n\n        @server.resource(\"resource://test-resource\")\n        def get_resource():\n            return \"Resource content\"\n\n        @server.resource(\"resource:///absolute/path\")\n        def get_absolute_resource():\n            return \"Absolute resource content\"\n\n        @server.resource(\"resource://{param}/template\")\n        def get_template_resource(param: str):\n            return f\"Template resource with {param}\"\n\n        # Create a main server and mount the resource server\n        main_server = FastMCP(name=\"MainServer\")\n        main_server.mount(server, \"prefix\")\n\n        # Check that the resources are mounted with the correct prefixes\n        resources = await main_server.list_resources()\n        templates = await main_server.list_resource_templates()\n\n        assert any(str(r.uri) == \"resource://prefix/test-resource\" for r in resources)\n        assert any(str(r.uri) == \"resource://prefix//absolute/path\" for r in resources)\n        assert any(\n            t.uri_template == \"resource://prefix/{param}/template\" for t in templates\n        )\n\n        # Test that prefixed resources can be accessed\n        async with Client(main_server) as client:\n            # Regular resource\n            result = await client.read_resource(\"resource://prefix/test-resource\")\n            assert isinstance(result[0], TextResourceContents)\n            assert result[0].text == \"Resource content\"\n\n            # Absolute path resource\n            result = await client.read_resource(\"resource://prefix//absolute/path\")\n            assert isinstance(result[0], TextResourceContents)\n            assert result[0].text == \"Absolute resource content\"\n\n            # Template resource\n            result = await client.read_resource(\n                \"resource://prefix/param-value/template\"\n            )\n            assert isinstance(result[0], TextResourceContents)\n            assert result[0].text == \"Template resource with param-value\"\n\n\nclass TestSettingsFromEnvironment:\n    async def test_server_starts_without_auth(self):\n        \"\"\"Test that server starts without auth configured.\"\"\"\n        from fastmcp.client.transports import PythonStdioTransport\n\n        script = dedent(\"\"\"\n        import fastmcp\n        \n        mcp = fastmcp.FastMCP(\"TestServer\")\n\n        mcp.run()\n        \"\"\")\n\n        with TemporaryDirectory() as temp_dir:\n            server_file = Path(temp_dir) / \"server.py\"\n            server_file.write_text(script)\n\n            transport: PythonStdioTransport = PythonStdioTransport(\n                script_path=server_file\n            )\n\n            async with Client[PythonStdioTransport](transport=transport) as client:\n                tools = await client.list_tools()\n\n                assert tools == []\n\n\nclass TestAbstractCollectionTypes:\n    \"\"\"Test that FastMCP accepts abstract collection types from collections.abc.\"\"\"\n\n    async def test_fastmcp_init_with_tuples(self):\n        \"\"\"Test FastMCP accepts tuples for sequence parameters.\"\"\"\n\n        def dummy_tool() -> str:\n            return \"test\"\n\n        # Test with tuples and other abstract types\n        mcp = FastMCP(\n            \"test\",\n            middleware=(),  # Empty tuple\n            tools=(Tool.from_function(dummy_tool),),  # Tuple of tools\n        )\n        mcp.enable(tags={\"tag1\", \"tag2\"}, only=True)\n        mcp.disable(tags={\"tag3\"})\n        assert mcp is not None\n        assert mcp.name == \"test\"\n        assert isinstance(mcp.middleware, list)  # Should be converted to list\n\n    async def test_fastmcp_works_with_abstract_types(self):\n        \"\"\"Test that abstract types work end-to-end with a client.\"\"\"\n\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        # Create server with tuple of tools\n        mcp = FastMCP(\"test\", tools=(Tool.from_function(greet),))\n\n        # Verify it works with a client\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Hello, World!\"\n\n\nclass TestMeta:\n    \"\"\"Test that fastmcp key is always present in meta.\"\"\"\n\n    async def test_tool_tags_in_meta(self):\n        \"\"\"Test that tool tags appear in meta under fastmcp key.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(tags={\"tool-example\", \"test-tool-tag\"})\n        def sample_tool(x: int) -> int:\n            \"\"\"A sample tool.\"\"\"\n            return x * 2\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = next(t for t in tools if t.name == \"sample_tool\")\n            assert tool.meta is not None\n            assert set(tool.meta[\"fastmcp\"][\"tags\"]) == {\n                \"tool-example\",\n                \"test-tool-tag\",\n            }\n\n    async def test_resource_tags_in_meta(self):\n        \"\"\"Test that resource tags appear in meta under fastmcp key.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\n            uri=\"test://resource\", tags={\"resource-example\", \"test-resource-tag\"}\n        )\n        def sample_resource() -> str:\n            \"\"\"A sample resource.\"\"\"\n            return \"resource content\"\n\n        async with Client(mcp) as client:\n            resources = await client.list_resources()\n            resource = next(r for r in resources if str(r.uri) == \"test://resource\")\n            assert resource.meta is not None\n            assert set(resource.meta[\"fastmcp\"][\"tags\"]) == {\n                \"resource-example\",\n                \"test-resource-tag\",\n            }\n\n    async def test_resource_template_tags_in_meta(self):\n        \"\"\"Test that resource template tags appear in meta under fastmcp key.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\n            \"test://template/{id}\", tags={\"template-example\", \"test-template-tag\"}\n        )\n        def sample_template(id: str) -> str:\n            \"\"\"A sample resource template.\"\"\"\n            return f\"template content for {id}\"\n\n        async with Client(mcp) as client:\n            templates = await client.list_resource_templates()\n            template = next(\n                t for t in templates if t.uriTemplate == \"test://template/{id}\"\n            )\n            assert template.meta is not None\n            assert set(template.meta[\"fastmcp\"][\"tags\"]) == {\n                \"template-example\",\n                \"test-template-tag\",\n            }\n\n    async def test_prompt_tags_in_meta(self):\n        \"\"\"Test that prompt tags appear in meta under fastmcp key.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(tags={\"example\", \"test-tag\"})\n        def sample_prompt() -> str:\n            return \"Hello, world!\"\n\n        async with Client(mcp) as client:\n            prompts = await client.list_prompts()\n            prompt = next(p for p in prompts if p.name == \"sample_prompt\")\n            assert prompt.meta is not None\n            assert set(prompt.meta[\"fastmcp\"][\"tags\"]) == {\"example\", \"test-tag\"}\n\n\nclass TestShowServerBannerSetting:\n    \"\"\"Test that show_server_banner setting controls banner display.\"\"\"\n\n    async def test_show_banner_defaults_to_setting_true(self):\n        \"\"\"Test that show_banner=None uses the setting (default True).\"\"\"\n        mcp = FastMCP()\n\n        with mock.patch.object(mcp, \"run_stdio_async\") as mock_run:\n            mock_run.return_value = None\n            await mcp.run_async(transport=\"stdio\")\n            mock_run.assert_called_once()\n            assert mock_run.call_args.kwargs[\"show_banner\"] is True\n\n    async def test_show_banner_respects_setting_false(self):\n        \"\"\"Test that show_banner=None uses the setting when False.\"\"\"\n        mcp = FastMCP()\n\n        with mock.patch.dict(os.environ, {\"FASTMCP_SHOW_SERVER_BANNER\": \"false\"}):\n            with temporary_settings(show_server_banner=False):\n                with mock.patch.object(mcp, \"run_stdio_async\") as mock_run:\n                    mock_run.return_value = None\n                    await mcp.run_async(transport=\"stdio\")\n                    mock_run.assert_called_once()\n                    assert mock_run.call_args.kwargs[\"show_banner\"] is False\n\n    async def test_show_banner_explicit_true_overrides_setting(self):\n        \"\"\"Test that explicit show_banner=True overrides False setting.\"\"\"\n        mcp = FastMCP()\n\n        with temporary_settings(show_server_banner=False):\n            with mock.patch.object(mcp, \"run_stdio_async\") as mock_run:\n                mock_run.return_value = None\n                await mcp.run_async(transport=\"stdio\", show_banner=True)\n                mock_run.assert_called_once()\n                assert mock_run.call_args.kwargs[\"show_banner\"] is True\n\n    async def test_show_banner_explicit_false_overrides_setting(self):\n        \"\"\"Test that explicit show_banner=False overrides True setting.\"\"\"\n        mcp = FastMCP()\n\n        with temporary_settings(show_server_banner=True):\n            with mock.patch.object(mcp, \"run_stdio_async\") as mock_run:\n                mock_run.return_value = None\n                await mcp.run_async(transport=\"stdio\", show_banner=False)\n                mock_run.assert_called_once()\n                assert mock_run.call_args.kwargs[\"show_banner\"] is False\n"
  },
  {
    "path": "tests/server/test_server_docket.py",
    "content": "\"\"\"Tests for Docket integration in FastMCP.\"\"\"\n\nimport asyncio\nfrom contextlib import asynccontextmanager\n\nfrom docket import Docket\nfrom docket.worker import Worker\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.dependencies import CurrentDocket, CurrentWorker\nfrom fastmcp.server.dependencies import get_context\n\nHUZZAH = \"huzzah!\"\n\n\nasync def test_docket_not_initialized_without_task_components():\n    \"\"\"Docket is only initialized when task-enabled components exist.\"\"\"\n    mcp = FastMCP(\"test-server\")\n\n    @mcp.tool()\n    def regular_tool() -> str:\n        return \"no docket needed\"\n\n    async with Client(mcp) as client:\n        # Docket should not be initialized\n        assert mcp._docket is None\n\n        # Regular tools still work\n        result = await client.call_tool(\"regular_tool\", {})\n        assert result.data == \"no docket needed\"\n\n\nasync def test_current_docket():\n    \"\"\"CurrentDocket dependency provides access to Docket instance.\"\"\"\n    mcp = FastMCP(\"test-server\")\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.tool()\n    def check_docket(docket: Docket = CurrentDocket()) -> str:\n        assert isinstance(docket, Docket)\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"check_docket\", {})\n        assert HUZZAH in str(result)\n\n\nasync def test_current_worker():\n    \"\"\"CurrentWorker dependency provides access to Worker instance.\"\"\"\n    mcp = FastMCP(\"test-server\")\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.tool()\n    def check_worker(\n        worker: Worker = CurrentWorker(),\n        docket: Docket = CurrentDocket(),\n    ) -> str:\n        assert isinstance(worker, Worker)\n        assert worker.docket is docket\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"check_worker\", {})\n        assert HUZZAH in str(result)\n\n\nasync def test_worker_executes_background_tasks():\n    \"\"\"Verify that the Docket Worker is running and executes tasks.\"\"\"\n    task_completed = asyncio.Event()\n    mcp = FastMCP(\"test-server\")\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.tool()\n    async def schedule_work(\n        task_name: str,\n        docket: Docket = CurrentDocket(),\n    ) -> str:\n        \"\"\"Schedule a background task.\"\"\"\n\n        async def background_task(name: str):\n            \"\"\"Simple background task that signals completion.\"\"\"\n            task_completed.set()\n\n        # Schedule the task (Worker running in background will execute it)\n        await docket.add(background_task)(task_name)\n\n        return f\"Scheduled {task_name}\"\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"schedule_work\", {\"task_name\": \"test-task\"})\n        assert \"Scheduled test-task\" in str(result)\n\n        # Wait for background task to execute (max 2 seconds)\n        await asyncio.wait_for(task_completed.wait(), timeout=2.0)\n\n\nasync def test_current_docket_in_resource():\n    \"\"\"CurrentDocket works in resources.\"\"\"\n    mcp = FastMCP(\"test-server\")\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.resource(\"docket://info\")\n    def get_docket_info(docket: Docket = CurrentDocket()) -> str:\n        assert isinstance(docket, Docket)\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        result = await client.read_resource(\"docket://info\")\n        assert HUZZAH in str(result)\n\n\nasync def test_current_docket_in_prompt():\n    \"\"\"CurrentDocket works in prompts.\"\"\"\n    mcp = FastMCP(\"test-server\")\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.prompt()\n    def task_prompt(task_type: str, docket: Docket = CurrentDocket()) -> str:\n        assert isinstance(docket, Docket)\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        result = await client.get_prompt(\"task_prompt\", {\"task_type\": \"background\"})\n        assert HUZZAH in str(result)\n\n\nasync def test_current_docket_in_resource_template():\n    \"\"\"CurrentDocket works in resource templates.\"\"\"\n    mcp = FastMCP(\"test-server\")\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.resource(\"docket://tasks/{task_id}\")\n    def get_task_status(task_id: str, docket: Docket = CurrentDocket()) -> str:\n        assert isinstance(docket, Docket)\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        result = await client.read_resource(\"docket://tasks/123\")\n        assert HUZZAH in str(result)\n\n\nasync def test_concurrent_calls_maintain_isolation():\n    \"\"\"Multiple concurrent calls each get the same Docket instance.\"\"\"\n    mcp = FastMCP(\"test-server\")\n    docket_ids = []\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.tool()\n    def capture_docket_id(call_num: int, docket: Docket = CurrentDocket()) -> str:\n        docket_ids.append((call_num, id(docket)))\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        results = await asyncio.gather(\n            client.call_tool(\"capture_docket_id\", {\"call_num\": 1}),\n            client.call_tool(\"capture_docket_id\", {\"call_num\": 2}),\n            client.call_tool(\"capture_docket_id\", {\"call_num\": 3}),\n        )\n\n        for result in results:\n            assert HUZZAH in str(result)\n\n        # All calls should see the same Docket instance\n        assert len(docket_ids) == 3\n        first_id = docket_ids[0][1]\n        assert all(docket_id == first_id for _, docket_id in docket_ids)\n\n\nasync def test_user_lifespan_still_works_with_docket():\n    \"\"\"User-provided lifespan works correctly alongside Docket.\"\"\"\n    lifespan_entered = False\n\n    @asynccontextmanager\n    async def custom_lifespan(server: FastMCP):\n        nonlocal lifespan_entered\n        lifespan_entered = True\n        yield {\"custom_data\": \"test_value\"}\n\n    mcp = FastMCP(\"test-server\", lifespan=custom_lifespan)\n\n    # Need a task-enabled component to trigger Docket initialization\n    @mcp.tool(task=True)\n    async def _trigger_docket() -> str:\n        return \"trigger\"\n\n    @mcp.tool()\n    def check_both(docket: Docket = CurrentDocket()) -> str:\n        assert isinstance(docket, Docket)\n        ctx = get_context()\n        assert ctx.request_context is not None\n        lifespan_data = ctx.request_context.lifespan_context\n        assert lifespan_data.get(\"custom_data\") == \"test_value\"\n        return HUZZAH\n\n    async with Client(mcp) as client:\n        assert lifespan_entered\n        result = await client.call_tool(\"check_both\", {})\n        assert HUZZAH in str(result)\n"
  },
  {
    "path": "tests/server/test_server_lifespan.py",
    "content": "\"\"\"Tests for server_lifespan and session_lifespan behavior.\"\"\"\n\nimport asyncio\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom typing import Any\n\nimport anyio\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.lifespan import ContextManagerLifespan, lifespan\nfrom fastmcp.server.providers import Provider\nfrom fastmcp.utilities.lifespan import combine_lifespans\n\n\nclass TestServerLifespan:\n    \"\"\"Test server_lifespan functionality.\"\"\"\n\n    async def test_server_lifespan_basic(self):\n        \"\"\"Test that server_lifespan is entered once and persists across sessions.\"\"\"\n        lifespan_events: list[str] = []\n\n        @asynccontextmanager\n        async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            lifespan_events.append(\"enter\")\n            try:\n                yield {\"initialized\": True}\n            finally:\n                lifespan_events.append(\"exit\")\n\n        mcp = FastMCP(\"TestServer\", lifespan=server_lifespan)\n\n        @mcp.tool\n        def get_value() -> str:\n            return \"test\"\n\n        # Server lifespan should be entered when run_async starts\n        assert lifespan_events == []\n\n        # Connect first client session\n        async with Client(mcp) as client1:\n            result1 = await client1.call_tool(\"get_value\", {})\n            assert result1.data == \"test\"\n            # Server lifespan should have been entered once\n            assert lifespan_events == [\"enter\"]\n\n            # Connect second client session while first is still active\n            async with Client(mcp) as client2:\n                result2 = await client2.call_tool(\"get_value\", {})\n                assert result2.data == \"test\"\n                # Server lifespan should still only have been entered once\n                assert lifespan_events == [\"enter\"]\n\n        # Because we're using a fastmcptransport, the server lifespan should be exited\n        # when the client session closes\n        assert lifespan_events == [\"enter\", \"exit\"]\n\n    async def test_server_lifespan_overlapping_sessions(self):\n        \"\"\"Test that overlapping sessions keep lifespan active until all sessions close.\"\"\"\n        lifespan_events: list[str] = []\n\n        resource_state = \"missing\"\n\n        @asynccontextmanager\n        async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            nonlocal resource_state\n            lifespan_events.append(\"enter\")\n            resource_state = \"open\"\n            try:\n                yield {\"initialized\": True}\n            finally:\n                resource_state = \"closed\"\n                lifespan_events.append(\"exit\")\n\n        mcp = FastMCP(\"TestServer\", lifespan=server_lifespan)\n\n        @mcp.tool\n        def get_resource_state() -> str:\n            return resource_state\n\n        async with Client(mcp) as client1:\n            result1 = await client1.call_tool(\"get_resource_state\", {})\n            assert result1.data == \"open\"\n\n            async with Client(mcp) as client2:\n                result2 = await client2.call_tool(\"get_resource_state\", {})\n                assert result2.data == \"open\"\n\n            # client2 exited while client1 is still active; lifespan should remain open\n            result3 = await client1.call_tool(\"get_resource_state\", {})\n            assert result3.data == \"open\"\n            assert lifespan_events == [\"enter\"]\n\n        assert lifespan_events == [\"enter\", \"exit\"]\n\n    async def test_server_lifespan_context_available(self):\n        \"\"\"Test that server_lifespan context is available to tools.\"\"\"\n\n        @asynccontextmanager\n        async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict]:\n            yield {\"db_connection\": \"mock_db\"}\n\n        mcp = FastMCP(\"TestServer\", lifespan=server_lifespan)\n\n        @mcp.tool\n        def get_db_info(ctx: Context) -> str:\n            # Access the server lifespan context\n            assert ctx.request_context is not None  # type narrowing for type checker\n            lifespan_context = ctx.request_context.lifespan_context\n            return lifespan_context.get(\"db_connection\", \"no_db\")\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_db_info\", {})\n            assert result.data == \"mock_db\"\n\n\nclass TestComposableLifespans:\n    \"\"\"Test composable lifespan functionality.\"\"\"\n\n    async def test_lifespan_decorator_basic(self):\n        \"\"\"Test that the @lifespan decorator works like @asynccontextmanager.\"\"\"\n        events: list[str] = []\n\n        @lifespan\n        async def my_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"enter\")\n            try:\n                yield {\"key\": \"value\"}\n            finally:\n                events.append(\"exit\")\n\n        mcp = FastMCP(\"TestServer\", lifespan=my_lifespan)\n\n        @mcp.tool\n        def get_info(ctx: Context) -> str:\n            assert ctx.request_context is not None\n            lifespan_context = ctx.request_context.lifespan_context\n            return lifespan_context.get(\"key\", \"missing\")\n\n        assert events == []\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_info\", {})\n            assert result.data == \"value\"\n            assert events == [\"enter\"]\n\n        assert events == [\"enter\", \"exit\"]\n\n    async def test_lifespan_composition_two(self):\n        \"\"\"Test composing two lifespans with |.\"\"\"\n        events: list[str] = []\n\n        @lifespan\n        async def first_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"first_enter\")\n            try:\n                yield {\"first\": \"a\"}\n            finally:\n                events.append(\"first_exit\")\n\n        @lifespan\n        async def second_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"second_enter\")\n            try:\n                yield {\"second\": \"b\"}\n            finally:\n                events.append(\"second_exit\")\n\n        composed = first_lifespan | second_lifespan\n        mcp = FastMCP(\"TestServer\", lifespan=composed)\n\n        @mcp.tool\n        def get_both(ctx: Context) -> dict:\n            assert ctx.request_context is not None\n            return dict(ctx.request_context.lifespan_context)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_both\", {})\n            # Results should be merged\n            assert result.data == {\"first\": \"a\", \"second\": \"b\"}\n            # Should enter in order\n            assert events == [\"first_enter\", \"second_enter\"]\n\n        # Should exit in reverse order (LIFO)\n        assert events == [\"first_enter\", \"second_enter\", \"second_exit\", \"first_exit\"]\n\n    async def test_lifespan_composition_three(self):\n        \"\"\"Test composing three lifespans with |.\"\"\"\n        events: list[str] = []\n\n        @lifespan\n        async def ls_a(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"a_enter\")\n            try:\n                yield {\"a\": 1}\n            finally:\n                events.append(\"a_exit\")\n\n        @lifespan\n        async def ls_b(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"b_enter\")\n            try:\n                yield {\"b\": 2}\n            finally:\n                events.append(\"b_exit\")\n\n        @lifespan\n        async def ls_c(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"c_enter\")\n            try:\n                yield {\"c\": 3}\n            finally:\n                events.append(\"c_exit\")\n\n        composed = ls_a | ls_b | ls_c\n        mcp = FastMCP(\"TestServer\", lifespan=composed)\n\n        @mcp.tool\n        def get_all(ctx: Context) -> dict:\n            assert ctx.request_context is not None\n            return dict(ctx.request_context.lifespan_context)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_all\", {})\n            assert result.data == {\"a\": 1, \"b\": 2, \"c\": 3}\n            assert events == [\"a_enter\", \"b_enter\", \"c_enter\"]\n\n        assert events == [\n            \"a_enter\",\n            \"b_enter\",\n            \"c_enter\",\n            \"c_exit\",\n            \"b_exit\",\n            \"a_exit\",\n        ]\n\n    async def test_lifespan_result_merge_later_wins(self):\n        \"\"\"Test that later lifespans overwrite earlier ones on key conflict.\"\"\"\n\n        @lifespan\n        async def first(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            yield {\"key\": \"first\", \"only_first\": \"yes\"}\n\n        @lifespan\n        async def second(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            yield {\"key\": \"second\", \"only_second\": \"yes\"}\n\n        composed = first | second\n        mcp = FastMCP(\"TestServer\", lifespan=composed)\n\n        @mcp.tool\n        def get_context(ctx: Context) -> dict:\n            assert ctx.request_context is not None\n            return dict(ctx.request_context.lifespan_context)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_context\", {})\n            # \"key\" should be overwritten by second\n            assert result.data == {\n                \"key\": \"second\",\n                \"only_first\": \"yes\",\n                \"only_second\": \"yes\",\n            }\n\n    async def test_lifespan_composition_with_context_manager_lifespan(self):\n        \"\"\"Test composing with ContextManagerLifespan for @asynccontextmanager functions.\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def legacy_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"legacy_enter\")\n            try:\n                yield {\"legacy\": True}\n            finally:\n                events.append(\"legacy_exit\")\n\n        @lifespan\n        async def new_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"new_enter\")\n            try:\n                yield {\"new\": True}\n            finally:\n                events.append(\"new_exit\")\n\n        # Wrap the @asynccontextmanager function explicitly\n        composed = ContextManagerLifespan(legacy_lifespan) | new_lifespan\n        mcp = FastMCP(\"TestServer\", lifespan=composed)\n\n        @mcp.tool\n        def get_context(ctx: Context) -> dict:\n            assert ctx.request_context is not None\n            return dict(ctx.request_context.lifespan_context)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_context\", {})\n            assert result.data == {\"legacy\": True, \"new\": True}\n\n        assert events == [\n            \"legacy_enter\",\n            \"new_enter\",\n            \"new_exit\",\n            \"legacy_exit\",\n        ]\n\n    async def test_backwards_compatibility_asynccontextmanager(self):\n        \"\"\"Test that existing @asynccontextmanager lifespans still work.\"\"\"\n\n        @asynccontextmanager\n        async def old_style_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            yield {\"old_style\": True}\n\n        mcp = FastMCP(\"TestServer\", lifespan=old_style_lifespan)\n\n        @mcp.tool\n        def get_context(ctx: Context) -> dict:\n            assert ctx.request_context is not None\n            return dict(ctx.request_context.lifespan_context)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_context\", {})\n            assert result.data == {\"old_style\": True}\n\n    async def test_lifespan_or_requires_lifespan_instance(self):\n        \"\"\"Test that | operator requires Lifespan instances and gives helpful error.\"\"\"\n\n        @lifespan\n        async def my_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            yield {\"key\": \"value\"}\n\n        @asynccontextmanager\n        async def regular_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            yield {\"regular\": True}\n\n        # Composing with non-Lifespan should raise TypeError with helpful message\n        with pytest.raises(TypeError) as exc_info:\n            my_lifespan | regular_lifespan  # type: ignore[operator]\n\n        assert \"ContextManagerLifespan\" in str(exc_info.value)\n\n\nclass TestCombineLifespans:\n    \"\"\"Test combine_lifespans utility function.\"\"\"\n\n    async def test_combine_lifespans_fastapi_style(self):\n        \"\"\"Test combining lifespans that yield None (FastAPI-style).\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def first_lifespan(app: Any) -> AsyncIterator[None]:\n            events.append(\"first_enter\")\n            try:\n                yield\n            finally:\n                events.append(\"first_exit\")\n\n        @asynccontextmanager\n        async def second_lifespan(app: Any) -> AsyncIterator[None]:\n            events.append(\"second_enter\")\n            try:\n                yield\n            finally:\n                events.append(\"second_exit\")\n\n        combined = combine_lifespans(first_lifespan, second_lifespan)\n\n        async with combined(\"mock_app\") as result:\n            assert result == {}  # Empty dict when lifespans yield None\n            assert events == [\"first_enter\", \"second_enter\"]\n\n        # LIFO exit order\n        assert events == [\"first_enter\", \"second_enter\", \"second_exit\", \"first_exit\"]\n\n    async def test_combine_lifespans_fastmcp_style(self):\n        \"\"\"Test combining lifespans that yield dicts (FastMCP-style).\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def db_lifespan(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"db_enter\")\n            try:\n                yield {\"db\": \"connected\"}\n            finally:\n                events.append(\"db_exit\")\n\n        @asynccontextmanager\n        async def cache_lifespan(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"cache_enter\")\n            try:\n                yield {\"cache\": \"ready\"}\n            finally:\n                events.append(\"cache_exit\")\n\n        combined = combine_lifespans(db_lifespan, cache_lifespan)\n\n        async with combined(\"mock_app\") as result:\n            assert result == {\"db\": \"connected\", \"cache\": \"ready\"}\n            assert events == [\"db_enter\", \"cache_enter\"]\n\n        assert events == [\"db_enter\", \"cache_enter\", \"cache_exit\", \"db_exit\"]\n\n    async def test_combine_lifespans_mixed_styles(self):\n        \"\"\"Test combining FastAPI-style (yield None) and FastMCP-style (yield dict).\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def fastapi_lifespan(app: Any) -> AsyncIterator[None]:\n            events.append(\"fastapi_enter\")\n            try:\n                yield  # FastAPI-style: yield None\n            finally:\n                events.append(\"fastapi_exit\")\n\n        @asynccontextmanager\n        async def fastmcp_lifespan(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"fastmcp_enter\")\n            try:\n                yield {\"mcp\": \"initialized\"}  # FastMCP-style: yield dict\n            finally:\n                events.append(\"fastmcp_exit\")\n\n        combined = combine_lifespans(fastapi_lifespan, fastmcp_lifespan)\n\n        async with combined(\"mock_app\") as result:\n            # Only the dict from fastmcp_lifespan should be present\n            assert result == {\"mcp\": \"initialized\"}\n            assert events == [\"fastapi_enter\", \"fastmcp_enter\"]\n\n        assert events == [\n            \"fastapi_enter\",\n            \"fastmcp_enter\",\n            \"fastmcp_exit\",\n            \"fastapi_exit\",\n        ]\n\n    async def test_combine_lifespans_result_merge_later_wins(self):\n        \"\"\"Test that later lifespans overwrite earlier ones on key conflict.\"\"\"\n\n        @asynccontextmanager\n        async def first(app: Any) -> AsyncIterator[dict[str, Any]]:\n            yield {\"key\": \"first\", \"only_first\": \"yes\"}\n\n        @asynccontextmanager\n        async def second(app: Any) -> AsyncIterator[dict[str, Any]]:\n            yield {\"key\": \"second\", \"only_second\": \"yes\"}\n\n        combined = combine_lifespans(first, second)\n\n        async with combined(\"mock_app\") as result:\n            assert result == {\n                \"key\": \"second\",  # Overwritten by later lifespan\n                \"only_first\": \"yes\",\n                \"only_second\": \"yes\",\n            }\n\n    async def test_combine_lifespans_three(self):\n        \"\"\"Test combining three lifespans.\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def ls_a(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"a_enter\")\n            try:\n                yield {\"a\": 1}\n            finally:\n                events.append(\"a_exit\")\n\n        @asynccontextmanager\n        async def ls_b(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"b_enter\")\n            try:\n                yield {\"b\": 2}\n            finally:\n                events.append(\"b_exit\")\n\n        @asynccontextmanager\n        async def ls_c(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"c_enter\")\n            try:\n                yield {\"c\": 3}\n            finally:\n                events.append(\"c_exit\")\n\n        combined = combine_lifespans(ls_a, ls_b, ls_c)\n\n        async with combined(\"mock_app\") as result:\n            assert result == {\"a\": 1, \"b\": 2, \"c\": 3}\n            assert events == [\"a_enter\", \"b_enter\", \"c_enter\"]\n\n        assert events == [\n            \"a_enter\",\n            \"b_enter\",\n            \"c_enter\",\n            \"c_exit\",\n            \"b_exit\",\n            \"a_exit\",\n        ]\n\n    async def test_combine_lifespans_empty(self):\n        \"\"\"Test combining zero lifespans.\"\"\"\n        combined = combine_lifespans()\n\n        async with combined(\"mock_app\") as result:\n            assert result == {}\n\n    async def test_combine_lifespans_with_mapping_return_type(self):\n        \"\"\"Test combining lifespans that return Mapping (like Starlette's Lifespan).\n\n        This verifies that combine_lifespans accepts lifespans returning Mapping[str, Any],\n        which is the type that Starlette's Lifespan uses, not just dict[str, Any].\n        \"\"\"\n        from collections.abc import Mapping\n\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def mapping_lifespan(app: Any) -> AsyncIterator[Mapping[str, Any]]:\n            \"\"\"Simulates a Starlette-style lifespan that yields a Mapping.\"\"\"\n            events.append(\"mapping_enter\")\n            try:\n                yield {\"starlette_state\": \"initialized\"}\n            finally:\n                events.append(\"mapping_exit\")\n\n        @asynccontextmanager\n        async def dict_lifespan(app: Any) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"dict_enter\")\n            try:\n                yield {\"fastmcp_state\": \"ready\"}\n            finally:\n                events.append(\"dict_exit\")\n\n        combined = combine_lifespans(mapping_lifespan, dict_lifespan)\n\n        async with combined(\"mock_app\") as result:\n            assert result == {\n                \"starlette_state\": \"initialized\",\n                \"fastmcp_state\": \"ready\",\n            }\n            assert events == [\"mapping_enter\", \"dict_enter\"]\n\n        assert events == [\n            \"mapping_enter\",\n            \"dict_enter\",\n            \"dict_exit\",\n            \"mapping_exit\",\n        ]\n\n\nclass TestLifespanTeardownShielding:\n    \"\"\"Test that async operations in lifespan teardown complete under cancellation.\n\n    When a server shuts down (e.g. Ctrl-C), the cancel scope becomes active.\n    Lifespan teardown must be shielded from cancellation so that async cleanup\n    (closing DB connections, flushing buffers, etc.) can actually run.\n    \"\"\"\n\n    async def test_server_lifespan_async_teardown_under_cancellation(self):\n        \"\"\"Async operations in server lifespan finally block complete even when cancelled.\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"setup\")\n            try:\n                yield {}\n            finally:\n                events.append(\"teardown_start\")\n                await asyncio.sleep(0)\n                events.append(\"teardown_complete\")\n\n        mcp = FastMCP(\"TestServer\", lifespan=server_lifespan)\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_and_cancel() -> None:\n                async with mcp._lifespan_manager():\n                    tg.cancel_scope.cancel()\n\n            tg.start_soon(run_and_cancel)\n\n        assert \"setup\" in events\n        assert \"teardown_start\" in events\n        assert \"teardown_complete\" in events\n\n    async def test_provider_lifespan_async_teardown_under_cancellation(self):\n        \"\"\"Async operations in provider lifespan finally block complete when cancelled.\"\"\"\n        events: list[str] = []\n\n        class TestProvider(Provider):\n            @asynccontextmanager\n            async def lifespan(self) -> AsyncIterator[None]:\n                events.append(\"provider_setup\")\n                try:\n                    yield\n                finally:\n                    events.append(\"provider_teardown_start\")\n                    await asyncio.sleep(0)\n                    events.append(\"provider_teardown_complete\")\n\n        mcp = FastMCP(\"TestServer\", providers=[TestProvider()])\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_and_cancel() -> None:\n                async with mcp._lifespan_manager():\n                    tg.cancel_scope.cancel()\n\n            tg.start_soon(run_and_cancel)\n\n        assert \"provider_setup\" in events\n        assert \"provider_teardown_start\" in events\n        assert \"provider_teardown_complete\" in events\n\n    async def test_composed_lifespans_async_teardown_under_cancellation(self):\n        \"\"\"Both server and provider async teardown completes under cancellation.\"\"\"\n        events: list[str] = []\n\n        @asynccontextmanager\n        async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict[str, Any]]:\n            events.append(\"server_setup\")\n            try:\n                yield {}\n            finally:\n                events.append(\"server_teardown_start\")\n                await asyncio.sleep(0)\n                events.append(\"server_teardown_complete\")\n\n        class TestProvider(Provider):\n            @asynccontextmanager\n            async def lifespan(self) -> AsyncIterator[None]:\n                events.append(\"provider_setup\")\n                try:\n                    yield\n                finally:\n                    events.append(\"provider_teardown_start\")\n                    await asyncio.sleep(0)\n                    events.append(\"provider_teardown_complete\")\n\n        mcp = FastMCP(\n            \"TestServer\",\n            lifespan=server_lifespan,\n            providers=[TestProvider()],\n        )\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_and_cancel() -> None:\n                async with mcp._lifespan_manager():\n                    tg.cancel_scope.cancel()\n\n            tg.start_soon(run_and_cancel)\n\n        assert events == [\n            \"server_setup\",\n            \"provider_setup\",\n            \"provider_teardown_start\",\n            \"provider_teardown_complete\",\n            \"server_teardown_start\",\n            \"server_teardown_complete\",\n        ]\n"
  },
  {
    "path": "tests/server/test_session_visibility.py",
    "content": "\"\"\"Tests for session-specific visibility control via Context.\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\n\nimport anyio\nimport mcp.types\n\nfrom fastmcp.client.messages import MessageHandler\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.server import FastMCP\n\n\n@dataclass\nclass NotificationRecording:\n    \"\"\"Record of a notification that was received.\"\"\"\n\n    method: str\n    notification: mcp.types.ServerNotification\n    timestamp: datetime = field(default_factory=datetime.now)\n\n\nclass RecordingMessageHandler(MessageHandler):\n    \"\"\"A message handler that records all notifications.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.notifications: list[NotificationRecording] = []\n\n    async def on_notification(self, message: mcp.types.ServerNotification) -> None:\n        \"\"\"Record all notifications with timestamp.\"\"\"\n        self.notifications.append(\n            NotificationRecording(method=message.root.method, notification=message)\n        )\n\n    def get_notifications(\n        self, method: str | None = None\n    ) -> list[NotificationRecording]:\n        \"\"\"Get all recorded notifications, optionally filtered by method.\"\"\"\n        if method is None:\n            return self.notifications\n        return [n for n in self.notifications if n.method == method]\n\n    def reset(self):\n        \"\"\"Clear all recorded notifications.\"\"\"\n        self.notifications.clear()\n\n\nclass TestSessionVisibility:\n    \"\"\"Test session-specific visibility control via Context.\"\"\"\n\n    async def test_enable_components_stores_rule_dict(self):\n        \"\"\"Test that enable_components stores a rule dict in session state.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            # Check that the rule was stored\n            rules = await ctx._get_visibility_rules()\n            assert len(rules) == 1\n            assert rules[0][\"enabled\"] is True\n            assert rules[0][\"tags\"] == [\"finance\"]\n            return \"activated\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"activate_finance\", {})\n            assert result.data == \"activated\"\n\n    async def test_disable_components_stores_rule_dict(self):\n        \"\"\"Test that disable_components stores a rule dict in session state.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"internal\"})\n        def internal_tool() -> str:\n            return \"internal\"\n\n        @mcp.tool\n        async def deactivate_internal(ctx: Context) -> str:\n            await ctx.disable_components(tags={\"internal\"})\n            # Check that the rule was stored\n            rules = await ctx._get_visibility_rules()\n            assert len(rules) == 1\n            assert rules[0][\"enabled\"] is False\n            assert rules[0][\"tags\"] == [\"internal\"]\n            return \"deactivated\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"deactivate_internal\", {})\n            assert result.data == \"deactivated\"\n\n    async def test_session_rules_override_global_disables(self):\n        \"\"\"Test that session enable rules override global disable transforms.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        # Globally disable finance tools\n        mcp.disable(tags={\"finance\"})\n\n        async with Client(mcp) as client:\n            # Before activation, finance tool should not be visible\n            tools_before = await client.list_tools()\n            assert not any(t.name == \"finance_tool\" for t in tools_before)\n\n            # Activate finance for this session\n            await client.call_tool(\"activate_finance\", {})\n\n            # After activation, finance tool should be visible in this session\n            tools_after = await client.list_tools()\n            assert any(t.name == \"finance_tool\" for t in tools_after)\n\n    async def test_rules_persist_across_requests(self):\n        \"\"\"Test that session rules persist across multiple requests.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        @mcp.tool\n        async def check_rules(ctx: Context) -> int:\n            rules = await ctx._get_visibility_rules()\n            return len(rules)\n\n        # Globally disable finance tools\n        mcp.disable(tags={\"finance\"})\n\n        async with Client(mcp) as client:\n            # Activate finance\n            await client.call_tool(\"activate_finance\", {})\n\n            # In a subsequent request, rules should still be there\n            result = await client.call_tool(\"check_rules\", {})\n            assert result.data == 1\n\n            # And finance tool should still be visible\n            tools = await client.list_tools()\n            assert any(t.name == \"finance_tool\" for t in tools)\n\n    async def test_rules_isolated_between_sessions(self):\n        \"\"\"Test that session rules are isolated between different sessions.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        # Globally disable finance tools\n        mcp.disable(tags={\"finance\"})\n\n        # Session A activates finance\n        async with Client(mcp) as client_a:\n            await client_a.call_tool(\"activate_finance\", {})\n            tools_a = await client_a.list_tools()\n            assert any(t.name == \"finance_tool\" for t in tools_a)\n\n        # Session B should not see finance tool (different session)\n        async with Client(mcp) as client_b:\n            tools_b = await client_b.list_tools()\n            assert not any(t.name == \"finance_tool\" for t in tools_b)\n\n    async def test_version_spec_serialization(self):\n        \"\"\"Test that VersionSpec is serialized/deserialized correctly.\"\"\"\n        from fastmcp import Client\n        from fastmcp.utilities.versions import VersionSpec\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(version=\"1.0.0\")\n        def old_tool() -> str:\n            return \"old\"\n\n        @mcp.tool(version=\"2.0.0\")\n        def new_tool() -> str:\n            return \"new\"\n\n        @mcp.tool\n        async def enable_v2_only(ctx: Context) -> str:\n            await ctx.enable_components(version=VersionSpec(gte=\"2.0.0\"))\n            # Check serialization - version is stored as a dict\n            rules = await ctx._get_visibility_rules()\n            assert rules[0][\"version\"][\"gte\"] == \"2.0.0\"\n            assert rules[0][\"version\"][\"lt\"] is None\n            assert rules[0][\"version\"][\"eq\"] is None\n            return \"enabled\"\n\n        # Globally disable all versioned tools\n        mcp.disable(names={\"old_tool\", \"new_tool\"})\n\n        async with Client(mcp) as client:\n            # Enable v2 tools\n            await client.call_tool(\"enable_v2_only\", {})\n\n            # Should see new_tool (v2.0.0) but not old_tool (v1.0.0)\n            tools = await client.list_tools()\n            assert any(t.name == \"new_tool\" for t in tools)\n            assert not any(t.name == \"old_tool\" for t in tools)\n\n    async def test_clear_visibility_rules(self):\n        \"\"\"Test that reset_visibility removes all session rules.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        @mcp.tool\n        async def clear_rules(ctx: Context) -> str:\n            await ctx.reset_visibility()\n            rules = await ctx._get_visibility_rules()\n            assert len(rules) == 0\n            return \"cleared\"\n\n        # Globally disable finance tools\n        mcp.disable(tags={\"finance\"})\n\n        async with Client(mcp) as client:\n            # Activate finance\n            await client.call_tool(\"activate_finance\", {})\n            tools_after_activate = await client.list_tools()\n            assert any(t.name == \"finance_tool\" for t in tools_after_activate)\n\n            # Clear rules\n            await client.call_tool(\"clear_rules\", {})\n\n            # Finance tool should no longer be visible (back to global disable)\n            tools_after_clear = await client.list_tools()\n            assert not any(t.name == \"finance_tool\" for t in tools_after_clear)\n\n    async def test_multiple_rules_accumulate(self):\n        \"\"\"Test that multiple enable/disable calls accumulate rules.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool(tags={\"admin\"})\n        def admin_tool() -> str:\n            return \"admin\"\n\n        @mcp.tool\n        async def activate_multiple(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            await ctx.enable_components(tags={\"admin\"})\n            rules = await ctx._get_visibility_rules()\n            assert len(rules) == 2\n            return \"activated\"\n\n        # Globally disable finance and admin tools\n        mcp.disable(tags={\"finance\", \"admin\"})\n\n        async with Client(mcp) as client:\n            # Activate both\n            await client.call_tool(\"activate_multiple\", {})\n\n            # Both should be visible\n            tools = await client.list_tools()\n            assert any(t.name == \"finance_tool\" for t in tools)\n            assert any(t.name == \"admin_tool\" for t in tools)\n\n    async def test_later_rules_override_earlier_rules(self):\n        \"\"\"Test that later session rules override earlier ones (mark semantics).\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"test\"})\n        def test_tool() -> str:\n            return \"test\"\n\n        @mcp.tool\n        async def toggle_test(ctx: Context) -> str:\n            # First enable, then disable\n            await ctx.enable_components(tags={\"test\"})\n            await ctx.disable_components(tags={\"test\"})\n            return \"toggled\"\n\n        async with Client(mcp) as client:\n            # Toggle (enable then disable)\n            await client.call_tool(\"toggle_test\", {})\n\n            # The disable should win (later mark overrides earlier)\n            tools = await client.list_tools()\n            assert not any(t.name == \"test_tool\" for t in tools)\n\n    async def test_session_transforms_apply_to_resources(self):\n        \"\"\"Test that session transforms apply to resources too.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.resource(\"resource://finance\", tags={\"finance\"})\n        def finance_resource() -> str:\n            return \"finance data\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        # Globally disable finance resources\n        mcp.disable(tags={\"finance\"})\n\n        async with Client(mcp) as client:\n            # Before activation, finance resource should not be visible\n            resources_before = await client.list_resources()\n            assert not any(str(r.uri) == \"resource://finance\" for r in resources_before)\n\n            # Activate finance for this session\n            await client.call_tool(\"activate_finance\", {})\n\n            # After activation, finance resource should be visible\n            resources_after = await client.list_resources()\n            assert any(str(r.uri) == \"resource://finance\" for r in resources_after)\n\n    async def test_session_transforms_apply_to_prompts(self):\n        \"\"\"Test that session transforms apply to prompts too.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.prompt(tags={\"finance\"})\n        def finance_prompt() -> str:\n            return \"finance prompt\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        # Globally disable finance prompts\n        mcp.disable(tags={\"finance\"})\n\n        async with Client(mcp) as client:\n            # Before activation, finance prompt should not be visible\n            prompts_before = await client.list_prompts()\n            assert not any(p.name == \"finance_prompt\" for p in prompts_before)\n\n            # Activate finance for this session\n            await client.call_tool(\"activate_finance\", {})\n\n            # After activation, finance prompt should be visible\n            prompts_after = await client.list_prompts()\n            assert any(p.name == \"finance_prompt\" for p in prompts_after)\n\n\nclass TestSessionVisibilityNotifications:\n    \"\"\"Test that notifications are sent when session visibility changes.\"\"\"\n\n    async def test_enable_components_sends_notifications(self):\n        \"\"\"Test that enable_components sends all three notification types.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        async def activate(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        handler = RecordingMessageHandler()\n        async with Client(mcp, message_handler=handler) as client:\n            handler.reset()\n            await client.call_tool(\"activate\", {})\n\n            # Should receive all three notifications\n            tool_notifications = handler.get_notifications(\n                \"notifications/tools/list_changed\"\n            )\n            resource_notifications = handler.get_notifications(\n                \"notifications/resources/list_changed\"\n            )\n            prompt_notifications = handler.get_notifications(\n                \"notifications/prompts/list_changed\"\n            )\n            assert len(tool_notifications) == 1\n            assert len(resource_notifications) == 1\n            assert len(prompt_notifications) == 1\n\n    async def test_disable_components_sends_notifications(self):\n        \"\"\"Test that disable_components sends all three notification types.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        async def deactivate(ctx: Context) -> str:\n            await ctx.disable_components(tags={\"finance\"})\n            return \"deactivated\"\n\n        handler = RecordingMessageHandler()\n        async with Client(mcp, message_handler=handler) as client:\n            handler.reset()\n            await client.call_tool(\"deactivate\", {})\n\n            # Should receive all three notifications\n            assert (\n                len(handler.get_notifications(\"notifications/tools/list_changed\")) == 1\n            )\n            assert (\n                len(handler.get_notifications(\"notifications/resources/list_changed\"))\n                == 1\n            )\n            assert (\n                len(handler.get_notifications(\"notifications/prompts/list_changed\"))\n                == 1\n            )\n\n    async def test_clear_visibility_rules_sends_notifications(self):\n        \"\"\"Test that reset_visibility sends notifications.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        async def clear(ctx: Context) -> str:\n            await ctx.reset_visibility()\n            return \"cleared\"\n\n        handler = RecordingMessageHandler()\n        async with Client(mcp, message_handler=handler) as client:\n            handler.reset()\n            await client.call_tool(\"clear\", {})\n\n            # Should receive all three notifications\n            assert (\n                len(handler.get_notifications(\"notifications/tools/list_changed\")) == 1\n            )\n            assert (\n                len(handler.get_notifications(\"notifications/resources/list_changed\"))\n                == 1\n            )\n            assert (\n                len(handler.get_notifications(\"notifications/prompts/list_changed\"))\n                == 1\n            )\n\n    async def test_components_hint_limits_notifications(self):\n        \"\"\"Test that the components hint limits which notifications are sent.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        async def activate_tools_only(ctx: Context) -> str:\n            # Only specify tool components - should only send tool notification\n            await ctx.enable_components(tags={\"finance\"}, components={\"tool\"})\n            return \"activated\"\n\n        handler = RecordingMessageHandler()\n        async with Client(mcp, message_handler=handler) as client:\n            handler.reset()\n            await client.call_tool(\"activate_tools_only\", {})\n\n            # Should only receive tool notification\n            assert (\n                len(handler.get_notifications(\"notifications/tools/list_changed\")) == 1\n            )\n            assert (\n                len(handler.get_notifications(\"notifications/resources/list_changed\"))\n                == 0\n            )\n            assert (\n                len(handler.get_notifications(\"notifications/prompts/list_changed\"))\n                == 0\n            )\n\n\nclass TestConcurrentSessionIsolation:\n    \"\"\"Test that concurrent sessions don't leak visibility transforms.\"\"\"\n\n    async def test_concurrent_sessions_isolated(self):\n        \"\"\"Test that two concurrent clients don't leak session transforms.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"finance\"})\n        def finance_tool() -> str:\n            return \"finance\"\n\n        @mcp.tool\n        async def activate_finance(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"finance\"})\n            return \"activated\"\n\n        # Globally disable finance tools\n        mcp.disable(tags={\"finance\"})\n\n        # Track what each session sees\n        session_a_sees_finance = False\n        session_b_sees_finance = False\n        ready_event = anyio.Event()\n\n        async def session_a():\n            nonlocal session_a_sees_finance\n            async with Client(mcp) as client:\n                # Activate finance for this session\n                await client.call_tool(\"activate_finance\", {})\n\n                # Signal that session A has activated\n                ready_event.set()\n\n                # Check that session A sees finance tool\n                tools = await client.list_tools()\n                session_a_sees_finance = any(t.name == \"finance_tool\" for t in tools)\n\n                # Keep session A alive while session B checks\n                await anyio.sleep(0.2)\n\n        async def session_b():\n            nonlocal session_b_sees_finance\n            # Wait for session A to activate\n            await ready_event.wait()\n\n            async with Client(mcp) as client:\n                # Session B should NOT see finance tool\n                tools = await client.list_tools()\n                session_b_sees_finance = any(t.name == \"finance_tool\" for t in tools)\n\n        async with anyio.create_task_group() as tg:\n            tg.start_soon(session_a)\n            tg.start_soon(session_b)\n\n        # Session A should see finance, session B should not\n        assert session_a_sees_finance is True, \"Session A should see finance tool\"\n        assert session_b_sees_finance is False, \"Session B should NOT see finance tool\"\n\n    async def test_many_concurrent_sessions_isolated(self):\n        \"\"\"Test that many concurrent sessions remain properly isolated.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"premium\"})\n        def premium_tool() -> str:\n            return \"premium\"\n\n        @mcp.tool\n        async def activate_premium(ctx: Context) -> str:\n            await ctx.enable_components(tags={\"premium\"})\n            return \"activated\"\n\n        # Globally disable premium tools\n        mcp.disable(tags={\"premium\"})\n\n        results: dict[str, bool] = {}\n\n        async def activated_session(session_id: str):\n            async with Client(mcp) as client:\n                await client.call_tool(\"activate_premium\", {})\n                tools = await client.list_tools()\n                results[session_id] = any(t.name == \"premium_tool\" for t in tools)\n\n        async def non_activated_session(session_id: str):\n            async with Client(mcp) as client:\n                tools = await client.list_tools()\n                results[session_id] = any(t.name == \"premium_tool\" for t in tools)\n\n        async with anyio.create_task_group() as tg:\n            # Start 5 activated sessions\n            for i in range(5):\n                tg.start_soon(activated_session, f\"activated_{i}\")\n            # Start 5 non-activated sessions\n            for i in range(5):\n                tg.start_soon(non_activated_session, f\"non_activated_{i}\")\n\n        # All activated sessions should see premium tool\n        for i in range(5):\n            assert results[f\"activated_{i}\"] is True, (\n                f\"Activated session {i} should see premium tool\"\n            )\n\n        # All non-activated sessions should NOT see premium tool\n        for i in range(5):\n            assert results[f\"non_activated_{i}\"] is False, (\n                f\"Non-activated session {i} should NOT see premium tool\"\n            )\n\n\nclass TestSessionVisibilityResetBug:\n    \"\"\"Regression tests for #3034: visibility marks leak via shared component mutation.\"\"\"\n\n    async def test_disable_then_reset_restores_tools(self):\n        \"\"\"After disable + reset within the same session, tools should reappear.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"system\"})\n        def my_tool() -> str:\n            return \"hello\"\n\n        @mcp.tool(tags={\"env\"})\n        async def enter_env(ctx: Context) -> str:\n            await ctx.disable_components(tags={\"system\"})\n            return \"entered\"\n\n        @mcp.tool(tags={\"env\"})\n        async def exit_env(ctx: Context) -> str:\n            await ctx.reset_visibility()\n            return \"exited\"\n\n        async with Client(mcp) as client:\n            # Tool visible initially\n            tools = await client.list_tools()\n            assert any(t.name == \"my_tool\" for t in tools)\n\n            # Disable it\n            await client.call_tool(\"enter_env\", {})\n            tools = await client.list_tools()\n            assert not any(t.name == \"my_tool\" for t in tools)\n\n            # Reset — tool should come back\n            await client.call_tool(\"exit_env\", {})\n            tools = await client.list_tools()\n            assert any(t.name == \"my_tool\" for t in tools), (\n                \"Tool should be visible again after reset_visibility\"\n            )\n\n    async def test_disable_reset_loop(self):\n        \"\"\"Repeated disable/reset cycles should work every time (the exact bug from #3034).\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"system\"})\n        def create_project() -> str:\n            return \"created\"\n\n        @mcp.tool(tags={\"env\"})\n        async def enter_env(ctx: Context) -> str:\n            await ctx.disable_components(tags={\"system\"})\n            return \"entered\"\n\n        @mcp.tool(tags={\"env\"})\n        async def exit_env(ctx: Context) -> str:\n            await ctx.reset_visibility()\n            return \"exited\"\n\n        async with Client(mcp) as client:\n            for i in range(3):\n                # create_project should be visible\n                tools = await client.list_tools()\n                assert any(t.name == \"create_project\" for t in tools), (\n                    f\"Iteration {i}: create_project should be visible before enter_env\"\n                )\n\n                # Enter env — disables system tools\n                await client.call_tool(\"enter_env\", {})\n                tools = await client.list_tools()\n                assert not any(t.name == \"create_project\" for t in tools), (\n                    f\"Iteration {i}: create_project should be hidden after enter_env\"\n                )\n\n                # Exit env — reset\n                await client.call_tool(\"exit_env\", {})\n\n    async def test_session_disable_does_not_leak_to_concurrent_session(self):\n        \"\"\"Disabling tools in one session must not affect a concurrent session.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"system\"})\n        def shared_tool() -> str:\n            return \"shared\"\n\n        @mcp.tool\n        async def disable_system(ctx: Context) -> str:\n            await ctx.disable_components(tags={\"system\"})\n            return \"disabled\"\n\n        session_b_sees_tool = False\n        ready = anyio.Event()\n        check_done = anyio.Event()\n\n        async def session_a():\n            async with Client(mcp) as client:\n                await client.call_tool(\"disable_system\", {})\n                ready.set()\n                await check_done.wait()\n\n        async def session_b():\n            nonlocal session_b_sees_tool\n            await ready.wait()\n            async with Client(mcp) as client:\n                tools = await client.list_tools()\n                session_b_sees_tool = any(t.name == \"shared_tool\" for t in tools)\n                check_done.set()\n\n        async with anyio.create_task_group() as tg:\n            tg.start_soon(session_a)\n            tg.start_soon(session_b)\n\n        assert session_b_sees_tool is True, (\n            \"Session B should still see shared_tool despite Session A disabling it\"\n        )\n\n    async def test_session_disable_does_not_leak_to_sequential_session(self):\n        \"\"\"Disabling tools in one session must not affect a later session.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(tags={\"system\"})\n        def shared_tool() -> str:\n            return \"shared\"\n\n        @mcp.tool\n        async def disable_system(ctx: Context) -> str:\n            await ctx.disable_components(tags={\"system\"})\n            return \"disabled\"\n\n        # Session A disables the tool (no reset)\n        async with Client(mcp) as client_a:\n            await client_a.call_tool(\"disable_system\", {})\n            tools = await client_a.list_tools()\n            assert not any(t.name == \"shared_tool\" for t in tools)\n\n        # Session B should see it fresh\n        async with Client(mcp) as client_b:\n            tools = await client_b.list_tools()\n            assert any(t.name == \"shared_tool\" for t in tools), (\n                \"New session should see shared_tool regardless of previous session\"\n            )\n"
  },
  {
    "path": "tests/server/test_streamable_http_no_redirect.py",
    "content": "\"\"\"Test that streamable HTTP routes avoid 307 redirects.\"\"\"\n\nimport httpx\nimport pytest\nfrom starlette.routing import Route\n\nfrom fastmcp import FastMCP\n\n\n@pytest.mark.parametrize(\n    \"server_path\",\n    [\"/mcp\", \"/mcp/\"],\n)\ndef test_streamable_http_route_structure(server_path: str):\n    \"\"\"Test that streamable HTTP routes use Route objects with correct paths.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    # Create HTTP app with specific path\n    app = mcp.http_app(transport=\"http\", path=server_path)\n\n    # Find the streamable HTTP route\n    streamable_routes = [\n        r\n        for r in app.routes\n        if isinstance(r, Route) and hasattr(r, \"path\") and r.path == server_path\n    ]\n\n    # Verify route exists and uses Route (not Mount)\n    assert len(streamable_routes) == 1, (\n        f\"Should have one streamable route for path {server_path}\"\n    )\n    assert isinstance(streamable_routes[0], Route), \"Should use Route, not Mount\"\n    assert streamable_routes[0].path == server_path, (\n        f\"Route path should match {server_path}\"\n    )\n\n\nasync def test_streamable_http_redirect_behavior():\n    \"\"\"Test that non-matching paths get redirected correctly.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n\n    @mcp.tool\n    def greet(name: str) -> str:\n        return f\"Hello, {name}!\"\n\n    # Create HTTP app with /mcp path (no trailing slash)\n    app = mcp.http_app(transport=\"http\", path=\"/mcp\")\n\n    # Test that /mcp/ gets redirected to /mcp\n    async with httpx.AsyncClient(\n        transport=httpx.ASGITransport(app=app), base_url=\"http://test\"\n    ) as client:\n        response = await client.get(\"/mcp/\", follow_redirects=False)\n        assert response.status_code == 307\n        assert response.headers[\"location\"] == \"http://test/mcp\"\n\n\nasync def test_streamable_http_no_mount_routes():\n    \"\"\"Test that streamable HTTP app creates Route objects, not Mount objects.\"\"\"\n    mcp = FastMCP(\"TestServer\")\n    app = mcp.http_app(transport=\"http\")\n\n    # Should not find any Mount routes for the streamable HTTP path\n    from starlette.routing import Mount\n\n    mount_routes = [\n        r\n        for r in app.routes\n        if isinstance(r, Mount) and hasattr(r, \"path\") and r.path == \"/mcp\"\n    ]\n\n    assert len(mount_routes) == 0, \"Should not have Mount routes for streamable HTTP\"\n\n    # Should find Route objects instead\n    route_routes = [\n        r\n        for r in app.routes\n        if isinstance(r, Route) and hasattr(r, \"path\") and r.path == \"/mcp\"\n    ]\n\n    assert len(route_routes) == 1, \"Should have exactly one Route for streamable HTTP\"\n"
  },
  {
    "path": "tests/server/test_tool_annotations.py",
    "content": "from typing import Any\n\nimport mcp.types as mcp_types\nfrom mcp.types import Tool as MCPTool\nfrom mcp.types import ToolAnnotations, ToolExecution\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.tools.base import Tool\n\n\nasync def test_tool_annotations_in_tool_manager():\n    \"\"\"Test that tool annotations are correctly stored in the tool manager.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(\n        annotations=ToolAnnotations(\n            title=\"Echo Tool\",\n            readOnlyHint=True,\n            openWorldHint=False,\n        )\n    )\n    def echo(message: str) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        return message\n\n    # Check internal tool objects directly\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert tools[0].annotations is not None\n    assert tools[0].annotations.title == \"Echo Tool\"\n    assert tools[0].annotations.readOnlyHint is True\n    assert tools[0].annotations.openWorldHint is False\n\n\nasync def test_tool_annotations_in_mcp_protocol():\n    \"\"\"Test that tool annotations are correctly propagated to MCP tools list.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(\n        annotations=ToolAnnotations(\n            title=\"Echo Tool\",\n            readOnlyHint=True,\n            openWorldHint=False,\n        )\n    )\n    def echo(message: str) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        return message\n\n    # Check via MCP protocol\n    result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())\n    assert len(result.tools) == 1\n    assert result.tools[0].annotations is not None\n    assert result.tools[0].annotations.title == \"Echo Tool\"\n    assert result.tools[0].annotations.readOnlyHint is True\n    assert result.tools[0].annotations.openWorldHint is False\n\n\nasync def test_tool_annotations_in_client_api():\n    \"\"\"Test that tool annotations are correctly accessible via client API.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(\n        annotations=ToolAnnotations(\n            title=\"Echo Tool\",\n            readOnlyHint=True,\n            openWorldHint=False,\n        )\n    )\n    def echo(message: str) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        return message\n\n    # Check via client API\n    async with Client(mcp) as client:\n        tools_result = await client.list_tools()\n        assert len(tools_result) == 1\n        assert tools_result[0].name == \"echo\"\n        assert tools_result[0].annotations is not None\n        assert tools_result[0].annotations.title == \"Echo Tool\"\n        assert tools_result[0].annotations.readOnlyHint is True\n        assert tools_result[0].annotations.openWorldHint is False\n\n\nasync def test_provide_tool_annotations_as_dict_to_decorator():\n    \"\"\"Test that tool annotations are correctly accessible via client API.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(\n        annotations={\n            \"title\": \"Echo Tool\",\n            \"readOnlyHint\": True,\n            \"openWorldHint\": False,\n        }\n    )\n    def echo(message: str) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        return message\n\n    # Check via client API\n    async with Client(mcp) as client:\n        tools_result = await client.list_tools()\n        assert len(tools_result) == 1\n        assert tools_result[0].name == \"echo\"\n        assert tools_result[0].annotations is not None\n        assert tools_result[0].annotations.title == \"Echo Tool\"\n        assert tools_result[0].annotations.readOnlyHint is True\n        assert tools_result[0].annotations.openWorldHint is False\n\n\nasync def test_direct_tool_annotations_in_tool_manager():\n    \"\"\"Test direct ToolAnnotations object is correctly stored in tool manager.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    annotations = ToolAnnotations(\n        title=\"Direct Tool\",\n        readOnlyHint=False,\n        destructiveHint=True,\n        idempotentHint=False,\n        openWorldHint=True,\n    )\n\n    @mcp.tool(annotations=annotations)\n    def modify(data: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Modify the data provided.\"\"\"\n        return {\"modified\": True, **data}\n\n    # Check internal tool objects directly\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert tools[0].annotations is not None\n    assert tools[0].annotations.title == \"Direct Tool\"\n    assert tools[0].annotations.readOnlyHint is False\n    assert tools[0].annotations.destructiveHint is True\n    assert tools[0].annotations.idempotentHint is False\n    assert tools[0].annotations.openWorldHint is True\n\n\nasync def test_direct_tool_annotations_in_client_api():\n    \"\"\"Test direct ToolAnnotations object is correctly accessible via client API.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    annotations = ToolAnnotations(\n        title=\"Direct Tool\",\n        readOnlyHint=False,\n        destructiveHint=True,\n        idempotentHint=False,\n        openWorldHint=True,\n    )\n\n    @mcp.tool(annotations=annotations)\n    def modify(data: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Modify the data provided.\"\"\"\n        return {\"modified\": True, **data}\n\n    # Check via client API\n    async with Client(mcp) as client:\n        tools_result = await client.list_tools()\n        assert len(tools_result) == 1\n        assert tools_result[0].name == \"modify\"\n        assert tools_result[0].annotations is not None\n        assert tools_result[0].annotations.title == \"Direct Tool\"\n        assert tools_result[0].annotations.readOnlyHint is False\n        assert tools_result[0].annotations.destructiveHint is True\n\n\nasync def test_add_tool_method_annotations():\n    \"\"\"Test that tool annotations work with add_tool method.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    def create_item(name: str, value: int) -> dict[str, Any]:\n        \"\"\"Create a new item.\"\"\"\n        return {\"name\": name, \"value\": value}\n\n    tool = Tool.from_function(\n        create_item,\n        name=\"create_item\",\n        annotations=ToolAnnotations(\n            title=\"Create Item\",\n            readOnlyHint=False,\n            destructiveHint=False,\n        ),\n    )\n\n    mcp.add_tool(tool)\n\n    # Check internal tool objects directly\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert tools[0].annotations is not None\n    assert tools[0].annotations.title == \"Create Item\"\n    assert tools[0].annotations.readOnlyHint is False\n    assert tools[0].annotations.destructiveHint is False\n\n\nasync def test_tool_functionality_with_annotations():\n    \"\"\"Test that tool functionality is preserved when using annotations.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    def create_item(name: str, value: int) -> dict[str, Any]:\n        \"\"\"Create a new item.\"\"\"\n        return {\"name\": name, \"value\": value}\n\n    tool = Tool.from_function(\n        create_item,\n        name=\"create_item\",\n        annotations=ToolAnnotations(\n            title=\"Create Item\",\n            readOnlyHint=False,\n            destructiveHint=False,\n        ),\n    )\n    mcp.add_tool(tool)\n\n    # Use the tool to verify functionality is preserved\n    async with Client(mcp) as client:\n        result = await client.call_tool(\n            \"create_item\", {\"name\": \"test_item\", \"value\": 42}\n        )\n        assert result.data == {\"name\": \"test_item\", \"value\": 42}\n\n\nasync def test_task_execution_auto_populated_for_task_enabled_tool():\n    \"\"\"Test that execution.taskSupport is automatically set when tool has task=True.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(task=True)\n    async def background_tool(data: str) -> str:\n        \"\"\"A tool that runs in background.\"\"\"\n        return f\"Processed: {data}\"\n\n    async with Client(mcp) as client:\n        tools_result = await client.list_tools()\n        assert len(tools_result) == 1\n        assert tools_result[0].name == \"background_tool\"\n        assert isinstance(tools_result[0], MCPTool)\n        assert isinstance(tools_result[0].execution, ToolExecution)\n        assert tools_result[0].execution.taskSupport == \"optional\"\n\n\nasync def test_task_execution_omitted_for_task_disabled_tool():\n    \"\"\"Test that execution is not set when tool has task=False or default.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool(task=False)\n    def sync_tool(data: str) -> str:\n        \"\"\"A synchronous tool.\"\"\"\n        return f\"Processed: {data}\"\n\n    async with Client(mcp) as client:\n        tools_result = await client.list_tools()\n        assert len(tools_result) == 1\n        assert tools_result[0].name == \"sync_tool\"\n        # execution should be None for non-task tools (default is False, omitted)\n        assert tools_result[0].execution is None\n"
  },
  {
    "path": "tests/server/test_tool_transformation.py",
    "content": "import httpx\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.transforms import ToolTransform\nfrom fastmcp.tools.tool_transform import (\n    ArgTransformConfig,\n    ToolTransformConfig,\n)\n\n\nasync def test_tool_transformation_via_layer():\n    \"\"\"Test that tool transformations work via add_transform(ToolTransform(...)).\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def echo(message: str) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        return message\n\n    mcp.add_transform(\n        ToolTransform({\"echo\": ToolTransformConfig(name=\"echo_transformed\")})\n    )\n\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert any(t.name == \"echo_transformed\" for t in tools)\n    tool = next(t for t in tools if t.name == \"echo_transformed\")\n    assert tool.name == \"echo_transformed\"\n\n\nasync def test_transformed_tool_filtering():\n    \"\"\"Test that tool transformations add tags that affect filtering.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def echo(message: str) -> str:\n        \"\"\"Echo back the message provided.\"\"\"\n        return message\n\n    # Add transformation that adds tags\n    mcp.add_transform(\n        ToolTransform(\n            {\n                \"echo\": ToolTransformConfig(\n                    name=\"echo_transformed\", tags={\"enabled_tools\"}\n                )\n            }\n        )\n    )\n    # Enable only tools with the enabled_tools tag\n    mcp.enable(tags={\"enabled_tools\"}, only=True)\n\n    tools = await mcp.list_tools()\n    # With transformation applied, the tool now has the enabled_tools tag\n    assert len(tools) == 1\n\n\nasync def test_transformed_tool_structured_output_without_annotation():\n    \"\"\"Test that transformed tools generate structured output when original tool has no return annotation.\n\n    Ref: https://github.com/PrefectHQ/fastmcp/issues/1369\n    \"\"\"\n    from fastmcp.client import Client\n\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def tool_without_annotation(message: str):  # No return annotation\n        \"\"\"A tool without return type annotation.\"\"\"\n        return {\"result\": \"processed\", \"input\": message}\n\n    mcp.add_transform(\n        ToolTransform(\n            {\"tool_without_annotation\": ToolTransformConfig(name=\"transformed_tool\")}\n        )\n    )\n\n    # Test with client to verify structured output is populated\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"transformed_tool\", {\"message\": \"test\"})\n\n        # Structured output should be populated even without return annotation\n        assert result.data is not None\n        assert result.data == {\"result\": \"processed\", \"input\": \"test\"}\n\n\nasync def test_layer_based_transforms():\n    \"\"\"Test that ToolTransform layer works after tool registration.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def my_tool() -> str:\n        return \"hello\"\n\n    # Add transform after tool registration\n    mcp.add_transform(\n        ToolTransform({\"my_tool\": ToolTransformConfig(name=\"renamed_tool\")})\n    )\n\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    assert tools[0].name == \"renamed_tool\"\n\n\nasync def test_server_level_transforms_apply_to_mounted_servers():\n    \"\"\"Test that server-level transforms apply to tools from mounted servers.\"\"\"\n    main = FastMCP(\"Main\")\n    sub = FastMCP(\"Sub\")\n\n    @sub.tool()\n    def sub_tool() -> str:\n        return \"hello from sub\"\n\n    main.mount(sub)\n\n    # Add transform for the mounted tool at server level\n    main.add_transform(\n        ToolTransform({\"sub_tool\": ToolTransformConfig(name=\"renamed_sub_tool\")})\n    )\n\n    tools = await main.list_tools()\n    tool_names = [t.name for t in tools]\n\n    assert \"renamed_sub_tool\" in tool_names\n    assert \"sub_tool\" not in tool_names\n\n\nasync def test_tool_transform_config_enabled_false_hides_tool():\n    \"\"\"Test that ToolTransformConfig with enabled=False hides the tool from list_tools.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def visible_tool() -> str:\n        return \"visible\"\n\n    @mcp.tool()\n    def hidden_tool() -> str:\n        return \"hidden\"\n\n    # Disable one tool via transformation\n    mcp.add_transform(\n        ToolTransform({\"hidden_tool\": ToolTransformConfig(enabled=False)})\n    )\n\n    tools = await mcp.list_tools()\n    tool_names = [t.name for t in tools]\n\n    assert \"visible_tool\" in tool_names\n    assert \"hidden_tool\" not in tool_names\n\n\nasync def test_tool_transform_config_enabled_false_with_rename():\n    \"\"\"Test that enabled=False works together with other transformations like rename.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def my_tool() -> str:\n        return \"result\"\n\n    # Rename AND disable\n    mcp.add_transform(\n        ToolTransform(\n            {\"my_tool\": ToolTransformConfig(name=\"renamed_and_disabled\", enabled=False)}\n        )\n    )\n\n    tools = await mcp.list_tools()\n    tool_names = [t.name for t in tools]\n\n    # Tool should be hidden regardless of rename\n    assert \"my_tool\" not in tool_names\n    assert \"renamed_and_disabled\" not in tool_names\n\n\nasync def test_tool_transform_config_enabled_true_keeps_tool_visible():\n    \"\"\"Test that ToolTransformConfig with enabled=True (explicit) keeps the tool visible.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def my_tool() -> str:\n        return \"result\"\n\n    # Explicitly set enabled=True (should be same as default)\n    mcp.add_transform(ToolTransform({\"my_tool\": ToolTransformConfig(enabled=True)}))\n\n    tools = await mcp.list_tools()\n    tool_names = [t.name for t in tools]\n\n    assert \"my_tool\" in tool_names\n\n\nasync def test_tool_transform_config_enabled_true_overrides_earlier_disable():\n    \"\"\"Test that ToolTransformConfig with enabled=True can re-enable a previously disabled tool.\"\"\"\n    mcp = FastMCP(\"Test Server\")\n\n    @mcp.tool()\n    def my_tool() -> str:\n        return \"result\"\n\n    # Disable the tool first\n    mcp.disable(names={\"my_tool\"})\n\n    # Verify tool is initially hidden\n    tools = await mcp.list_tools()\n    assert \"my_tool\" not in [t.name for t in tools]\n\n    # Re-enable via transformation (later transforms win)\n    mcp.add_transform(ToolTransform({\"my_tool\": ToolTransformConfig(enabled=True)}))\n\n    tools = await mcp.list_tools()\n    tool_names = [t.name for t in tools]\n\n    # Tool should now be visible\n    assert \"my_tool\" in tool_names\n\n\nasync def test_openapi_path_params_not_duplicated_in_description():\n    \"\"\"Path parameter details should live in inputSchema, not the description.\n\n    Regression test for https://github.com/PrefectHQ/fastmcp/issues/3130 — hiding\n    a path param via ToolTransform left stale references in the description\n    because the description was generated before transforms ran. The fix is to\n    keep parameter docs in inputSchema only, where transforms can control them.\n    \"\"\"\n    spec = {\n        \"openapi\": \"3.1.0\",\n        \"info\": {\"title\": \"Test\", \"version\": \"0.1.0\"},\n        \"paths\": {\n            \"/api/{version}/users/{user_id}\": {\n                \"get\": {\n                    \"operationId\": \"my_endpoint\",\n                    \"summary\": \"My endpoint\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"version\",\n                            \"in\": \"path\",\n                            \"required\": True,\n                            \"description\": \"API version\",\n                            \"schema\": {\"type\": \"string\"},\n                        },\n                        {\n                            \"name\": \"user_id\",\n                            \"in\": \"path\",\n                            \"required\": True,\n                            \"description\": \"The user ID\",\n                            \"schema\": {\"type\": \"string\"},\n                        },\n                    ],\n                    \"responses\": {\"200\": {\"description\": \"OK\"}},\n                },\n            },\n        },\n    }\n\n    async with httpx.AsyncClient(base_url=\"http://localhost\") as http_client:\n        mcp = FastMCP.from_openapi(openapi_spec=spec, client=http_client)\n\n        # Hide one of the two path params\n        mcp.add_transform(\n            ToolTransform(\n                {\n                    \"my_endpoint\": ToolTransformConfig(\n                        arguments={\n                            \"version\": ArgTransformConfig(hide=True, default=\"v1\"),\n                        }\n                    )\n                }\n            )\n        )\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = tools[0]\n\n            # Description should be the summary only — no parameter details\n            assert tool.description == \"My endpoint\"\n\n            # Hidden param gone from schema, visible param still present\n            assert \"version\" not in tool.inputSchema.get(\"properties\", {})\n            assert \"user_id\" in tool.inputSchema[\"properties\"]\n            assert (\n                tool.inputSchema[\"properties\"][\"user_id\"][\"description\"]\n                == \"The user ID\"\n            )\n"
  },
  {
    "path": "tests/server/transforms/test_catalog.py",
    "content": "\"\"\"Tests for CatalogTransform base class.\"\"\"\n\nfrom __future__ import annotations\n\nimport ast\nfrom collections.abc import Sequence\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.transforms import GetToolNext\nfrom fastmcp.server.transforms.catalog import CatalogTransform\nfrom fastmcp.server.transforms.version_filter import VersionFilter\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.versions import VersionSpec\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _make_versioned_tool(name: str, version: str) -> Tool:\n    \"\"\"Create a tool with a specific version for testing.\"\"\"\n\n    async def _fn() -> str:\n        return f\"{name}@{version}\"\n\n    return Tool.from_function(fn=_fn, name=name, version=version)\n\n\nclass CatalogReader(CatalogTransform):\n    \"\"\"Minimal CatalogTransform that exposes get_tool_catalog via a tool.\n\n    After calling the ``read_catalog`` tool, the full catalog is available\n    on ``self.last_catalog`` for assertions beyond tool names.\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__()\n        self.last_catalog: Sequence[Tool] = []\n\n    async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        return [self._make_reader_tool()]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        if name == \"read_catalog\":\n            return self._make_reader_tool()\n        return await call_next(name, version=version)\n\n    def _make_reader_tool(self) -> Tool:\n        transform = self\n\n        async def read_catalog(ctx: Context = None) -> list[str]:  # type: ignore[assignment]\n            \"\"\"Return names of tools visible in the catalog.\"\"\"\n            transform.last_catalog = await transform.get_tool_catalog(ctx)\n            return [t.name for t in transform.last_catalog]\n\n        return Tool.from_function(fn=read_catalog, name=\"read_catalog\")\n\n\nclass ReplacingTransform(CatalogTransform):\n    \"\"\"Minimal subclass that replaces tools with a synthetic tool.\n\n    Uses ``get_tool_catalog()`` to read the real catalog inside the\n    synthetic tool's handler, verifying that the bypass mechanism works.\n    \"\"\"\n\n    async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:\n        return [self._make_synthetic_tool()]\n\n    async def get_tool(\n        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None\n    ) -> Tool | None:\n        if name == \"count_tools\":\n            return self._make_synthetic_tool()\n        return await call_next(name, version=version)\n\n    def _make_synthetic_tool(self) -> Tool:\n        transform = self\n\n        async def count_tools(ctx: Context = None) -> int:  # type: ignore[assignment]\n            \"\"\"Return the number of real tools in the catalog.\"\"\"\n            catalog = await transform.get_tool_catalog(ctx)\n            return len(catalog)\n\n        return Tool.from_function(fn=count_tools, name=\"count_tools\")\n\n\nclass TestCatalogTransformBypass:\n    async def test_list_tools_replaced_by_subclass(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.tool\n        def multiply(x: float, y: float) -> float:\n            return x * y\n\n        mcp.add_transform(ReplacingTransform())\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert names == {\"count_tools\"}\n\n    async def test_get_tool_catalog_returns_real_tools(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.tool\n        def multiply(x: float, y: float) -> float:\n            return x * y\n\n        mcp.add_transform(ReplacingTransform())\n        result = await mcp.call_tool(\"count_tools\", {})\n        assert any(\"2\" in c.text for c in result.content if isinstance(c, TextContent))\n\n    async def test_multiple_instances_have_independent_bypass(self):\n        \"\"\"Each CatalogTransform instance has its own bypass ContextVar.\"\"\"\n        t1 = ReplacingTransform()\n        t2 = ReplacingTransform()\n        assert t1._instance_id != t2._instance_id\n        assert t1._bypass is not t2._bypass\n\n\nclass TestCatalogDeduplication:\n    \"\"\"get_tool_catalog() deduplicates versioned tools, keeping only the highest.\"\"\"\n\n    async def test_returns_highest_version_only(self):\n        mcp = FastMCP(\"test\")\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"1\"))\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"2\"))\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"3\"))\n\n        reader = CatalogReader()\n        mcp.add_transform(reader)\n\n        result = await mcp.call_tool(\"read_catalog\", {})\n        names = _extract_result(result)\n        assert names == [\"greet\"]\n\n    async def test_version_metadata_injected(self):\n        \"\"\"Highest-version tool has meta.fastmcp.versions listing all available.\"\"\"\n        mcp = FastMCP(\"test\")\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"1\"))\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"3\"))\n\n        reader = CatalogReader()\n        mcp.add_transform(reader)\n\n        await mcp.call_tool(\"read_catalog\", {})\n        assert len(reader.last_catalog) == 1\n        tool = reader.last_catalog[0]\n        assert tool.name == \"greet\"\n        assert tool.version == \"3\"\n        assert tool.meta is not None\n        versions = tool.meta[\"fastmcp\"][\"versions\"]\n        assert versions == [\"3\", \"1\"]\n\n    async def test_mixed_versioned_and_unversioned(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def standalone() -> str:\n            return \"hi\"\n\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"1\"))\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"2\"))\n\n        reader = CatalogReader()\n        mcp.add_transform(reader)\n\n        result = await mcp.call_tool(\"read_catalog\", {})\n        names = sorted(_extract_result(result))\n        assert names == [\"greet\", \"standalone\"]\n\n    async def test_version_filter_applied_before_catalog(self):\n        \"\"\"A VersionFilter added before the CatalogTransform restricts what the catalog sees.\"\"\"\n        mcp = FastMCP(\"test\")\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"1\"))\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"2\"))\n        mcp.add_tool(_make_versioned_tool(\"greet\", \"3\"))\n\n        # Filter keeps only versions < 3 — so v1 and v2 survive, v3 is excluded.\n        mcp.add_transform(VersionFilter(version_lt=\"3\"))\n\n        reader = CatalogReader()\n        mcp.add_transform(reader)\n\n        await mcp.call_tool(\"read_catalog\", {})\n        assert len(reader.last_catalog) == 1\n        tool = reader.last_catalog[0]\n        assert tool.name == \"greet\"\n        assert tool.version == \"2\"\n\n\nclass TestCatalogVisibility:\n    \"\"\"get_tool_catalog() respects visibility (disabled tools are excluded).\"\"\"\n\n    async def test_disabled_tool_excluded(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def public() -> str:\n            return \"visible\"\n\n        @mcp.tool\n        def secret() -> str:\n            return \"hidden\"\n\n        mcp.disable(names={\"secret\"}, components={\"tool\"})\n        reader = CatalogReader()\n        mcp.add_transform(reader)\n\n        result = await mcp.call_tool(\"read_catalog\", {})\n        names = _extract_result(result)\n        assert names == [\"public\"]\n\n\nclass TestCatalogAuth:\n    \"\"\"get_tool_catalog() respects tool-level auth filtering.\"\"\"\n\n    async def test_auth_rejected_tool_excluded(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def public() -> str:\n            return \"visible\"\n\n        @mcp.tool(auth=lambda _ctx: False)\n        def protected() -> str:\n            return \"hidden\"\n\n        reader = CatalogReader()\n        mcp.add_transform(reader)\n\n        result = await mcp.call_tool(\"read_catalog\", {})\n        names = _extract_result(result)\n        assert names == [\"public\"]\n\n\n# ---------------------------------------------------------------------------\n# Test helpers\n# ---------------------------------------------------------------------------\n\n\ndef _extract_result(result: object) -> list[str]:\n    \"\"\"Extract the list of names from a call_tool ToolResult.\"\"\"\n    for c in result.content:  # type: ignore[union-attr]\n        if isinstance(c, TextContent):\n            return ast.literal_eval(c.text)\n    raise AssertionError(\"No text content found\")\n"
  },
  {
    "path": "tests/server/transforms/test_prompts_as_tools.py",
    "content": "\"\"\"Tests for PromptsAsTools transform.\"\"\"\n\nimport json\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.server.transforms import PromptsAsTools\n\n\nclass TestPromptsAsToolsBasic:\n    \"\"\"Test basic PromptsAsTools functionality.\"\"\"\n\n    async def test_adds_list_prompts_tool(self):\n        \"\"\"Transform adds list_prompts tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = {t.name for t in tools}\n            assert \"list_prompts\" in tool_names\n\n    async def test_adds_get_prompt_tool(self):\n        \"\"\"Transform adds get_prompt tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = {t.name for t in tools}\n            assert \"get_prompt\" in tool_names\n\n    async def test_preserves_existing_tools(self):\n        \"\"\"Transform preserves existing tools.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = {t.name for t in tools}\n            assert \"my_tool\" in tool_names\n            assert \"list_prompts\" in tool_names\n            assert \"get_prompt\" in tool_names\n\n\nclass TestListPromptsTool:\n    \"\"\"Test the list_prompts tool.\"\"\"\n\n    async def test_lists_prompts(self):\n        \"\"\"list_prompts returns prompt metadata.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.prompt\n        def analyze_code() -> str:\n            \"\"\"Analyze code for issues.\"\"\"\n            return \"Analyze this code\"\n\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_prompts\", {})\n            prompts = json.loads(result.data)\n\n            assert len(prompts) == 1\n            assert prompts[0][\"name\"] == \"analyze_code\"\n            assert prompts[0][\"description\"] == \"Analyze code for issues.\"\n\n    async def test_lists_prompt_with_arguments(self):\n        \"\"\"list_prompts includes argument metadata.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.prompt\n        def analyze_code(code: str, language: str = \"python\") -> str:\n            \"\"\"Analyze code for issues.\"\"\"\n            return f\"Analyze this {language} code:\\n{code}\"\n\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_prompts\", {})\n            prompts = json.loads(result.data)\n\n            assert len(prompts) == 1\n            args = prompts[0][\"arguments\"]\n            assert len(args) == 2\n\n            # Check required arg\n            code_arg = next(a for a in args if a[\"name\"] == \"code\")\n            assert code_arg[\"required\"] is True\n\n            # Check optional arg\n            lang_arg = next(a for a in args if a[\"name\"] == \"language\")\n            assert lang_arg[\"required\"] is False\n\n    async def test_empty_when_no_prompts(self):\n        \"\"\"list_prompts returns empty list when no prompts exist.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_prompts\", {})\n            assert json.loads(result.data) == []\n\n\nclass TestGetPromptTool:\n    \"\"\"Test the get_prompt tool.\"\"\"\n\n    async def test_gets_prompt_without_arguments(self):\n        \"\"\"get_prompt gets a prompt with no arguments.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.prompt\n        def simple_prompt() -> str:\n            \"\"\"A simple prompt.\"\"\"\n            return \"Hello, world!\"\n\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_prompt\", {\"name\": \"simple_prompt\"})\n            response = json.loads(result.data)\n\n            assert \"messages\" in response\n            assert len(response[\"messages\"]) == 1\n            assert response[\"messages\"][0][\"role\"] == \"user\"\n            assert \"Hello, world!\" in response[\"messages\"][0][\"content\"]\n\n    async def test_gets_prompt_with_arguments(self):\n        \"\"\"get_prompt gets a prompt with arguments.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.prompt\n        def analyze_code(code: str, language: str = \"python\") -> str:\n            \"\"\"Analyze code.\"\"\"\n            return f\"Analyze this {language} code:\\n{code}\"\n\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\n                \"get_prompt\",\n                {\n                    \"name\": \"analyze_code\",\n                    \"arguments\": {\"code\": \"x = 1\", \"language\": \"python\"},\n                },\n            )\n            response = json.loads(result.data)\n\n            assert \"messages\" in response\n            content = response[\"messages\"][0][\"content\"]\n            assert \"python\" in content\n            assert \"x = 1\" in content\n\n    async def test_error_on_unknown_prompt(self):\n        \"\"\"get_prompt raises error for unknown prompt name.\"\"\"\n        from fastmcp.exceptions import ToolError\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(PromptsAsTools(mcp))\n\n        async with Client(mcp) as client:\n            with pytest.raises(ToolError, match=\"Unknown prompt\"):\n                await client.call_tool(\"get_prompt\", {\"name\": \"unknown_prompt\"})\n\n\nclass TestPromptsAsToolsWithNamespace:\n    \"\"\"Test PromptsAsTools combined with other transforms.\"\"\"\n\n    async def test_works_with_namespace_on_provider(self):\n        \"\"\"PromptsAsTools works when provider has Namespace transform.\"\"\"\n        from fastmcp.server.providers import FastMCPProvider\n        from fastmcp.server.transforms import Namespace\n\n        sub = FastMCP(\"Sub\")\n\n        @sub.prompt\n        def my_prompt() -> str:\n            \"\"\"A prompt.\"\"\"\n            return \"Hello\"\n\n        main = FastMCP(\"Main\")\n        provider = FastMCPProvider(sub)\n        provider.add_transform(Namespace(\"sub\"))\n        main.add_provider(provider)\n        main.add_transform(PromptsAsTools(main))\n\n        async with Client(main) as client:\n            result = await client.call_tool(\"list_prompts\", {})\n            prompts = json.loads(result.data)\n\n            # Prompt should have namespaced name\n            assert len(prompts) == 1\n            assert prompts[0][\"name\"] == \"sub_my_prompt\"\n\n\nclass TestPromptsAsToolsRepr:\n    \"\"\"Test PromptsAsTools repr.\"\"\"\n\n    def test_repr(self):\n        \"\"\"Transform has useful repr.\"\"\"\n        mcp = FastMCP(\"Test\")\n        transform = PromptsAsTools(mcp)\n        assert \"PromptsAsTools\" in repr(transform)\n"
  },
  {
    "path": "tests/server/transforms/test_resources_as_tools.py",
    "content": "\"\"\"Tests for ResourcesAsTools transform.\"\"\"\n\nimport base64\nimport json\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.server.auth import AuthContext\nfrom fastmcp.server.transforms import ResourcesAsTools\n\n\nclass TestResourcesAsToolsBasic:\n    \"\"\"Test basic ResourcesAsTools functionality.\"\"\"\n\n    async def test_adds_list_resources_tool(self):\n        \"\"\"Transform adds list_resources tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = {t.name for t in tools}\n            assert \"list_resources\" in tool_names\n\n    async def test_adds_read_resource_tool(self):\n        \"\"\"Transform adds read_resource tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = {t.name for t in tools}\n            assert \"read_resource\" in tool_names\n\n    async def test_preserves_existing_tools(self):\n        \"\"\"Transform preserves existing tools.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"result\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool_names = {t.name for t in tools}\n            assert \"my_tool\" in tool_names\n            assert \"list_resources\" in tool_names\n            assert \"read_resource\" in tool_names\n\n\nclass TestListResourcesTool:\n    \"\"\"Test the list_resources tool.\"\"\"\n\n    async def test_lists_static_resources(self):\n        \"\"\"list_resources returns static resources with uri.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"config://app\")\n        def app_config() -> str:\n            return \"config data\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            resources = json.loads(result.data)\n\n            assert len(resources) == 1\n            assert resources[0][\"uri\"] == \"config://app\"\n            assert resources[0][\"name\"] == \"app_config\"\n\n    async def test_lists_resource_templates(self):\n        \"\"\"list_resources returns templates with uri_template.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"file://{path}\")\n        def read_file(path: str) -> str:\n            return f\"content of {path}\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            resources = json.loads(result.data)\n\n            assert len(resources) == 1\n            assert resources[0][\"uri_template\"] == \"file://{path}\"\n            assert \"uri\" not in resources[0]\n\n    async def test_lists_both_resources_and_templates(self):\n        \"\"\"list_resources returns both static and templated resources.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"config://app\")\n        def app_config() -> str:\n            return \"config\"\n\n        @mcp.resource(\"file://{path}\")\n        def read_file(path: str) -> str:\n            return f\"content of {path}\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            resources = json.loads(result.data)\n\n            assert len(resources) == 2\n            # One has uri, one has uri_template\n            uris = [r.get(\"uri\") for r in resources if r.get(\"uri\")]\n            templates = [\n                r.get(\"uri_template\") for r in resources if r.get(\"uri_template\")\n            ]\n            assert uris == [\"config://app\"]\n            assert templates == [\"file://{path}\"]\n\n    async def test_empty_when_no_resources(self):\n        \"\"\"list_resources returns empty list when no resources exist.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            assert json.loads(result.data) == []\n\n\nclass TestReadResourceTool:\n    \"\"\"Test the read_resource tool.\"\"\"\n\n    async def test_reads_static_resource(self):\n        \"\"\"read_resource reads a static resource by URI.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"config://app\")\n        def app_config() -> str:\n            return \"my config data\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"read_resource\", {\"uri\": \"config://app\"})\n            assert result.data == \"my config data\"\n\n    async def test_reads_templated_resource(self):\n        \"\"\"read_resource reads a templated resource with parameters.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"user://{user_id}/profile\")\n        def user_profile(user_id: str) -> str:\n            return f\"Profile for user {user_id}\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\n                \"read_resource\", {\"uri\": \"user://123/profile\"}\n            )\n            assert result.data == \"Profile for user 123\"\n\n    async def test_error_on_unknown_resource(self):\n        \"\"\"read_resource raises error for unknown URI.\"\"\"\n        from fastmcp.exceptions import ToolError\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            with pytest.raises(ToolError, match=\"Unknown resource\"):\n                await client.call_tool(\"read_resource\", {\"uri\": \"unknown://resource\"})\n\n    async def test_reads_binary_as_base64(self):\n        \"\"\"read_resource returns binary content as base64.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"data://binary\", mime_type=\"application/octet-stream\")\n        def binary_data() -> bytes:\n            return b\"\\x00\\x01\\x02\\x03\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"read_resource\", {\"uri\": \"data://binary\"})\n            # Should be base64 encoded\n            decoded = base64.b64decode(result.data)\n            assert decoded == b\"\\x00\\x01\\x02\\x03\"\n\n\nclass TestResourcesAsToolsWithNamespace:\n    \"\"\"Test ResourcesAsTools combined with other transforms.\"\"\"\n\n    async def test_works_with_namespace_on_provider(self):\n        \"\"\"ResourcesAsTools works when provider has Namespace transform.\"\"\"\n        from fastmcp.server.providers import FastMCPProvider\n        from fastmcp.server.transforms import Namespace\n\n        sub = FastMCP(\"Sub\")\n\n        @sub.resource(\"config://app\")\n        def app_config() -> str:\n            return \"sub config\"\n\n        main = FastMCP(\"Main\")\n        provider = FastMCPProvider(sub)\n        provider.add_transform(Namespace(\"sub\"))\n        main.add_provider(provider)\n        main.add_transform(ResourcesAsTools(main))\n\n        async with Client(main) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            resources = json.loads(result.data)\n\n            # Resource should have namespaced URI\n            assert len(resources) == 1\n            assert resources[0][\"uri\"] == \"config://sub/app\"\n\n\nclass TestResourcesAsToolsRepr:\n    \"\"\"Test ResourcesAsTools repr.\"\"\"\n\n    def test_repr(self):\n        \"\"\"Transform has useful repr.\"\"\"\n        mcp = FastMCP(\"Test\")\n        transform = ResourcesAsTools(mcp)\n        assert \"ResourcesAsTools\" in repr(transform)\n\n\nclass TestResourcesAsToolsAnnotations:\n    \"\"\"Test ToolAnnotations on generated resource tools.\"\"\"\n\n    async def test_list_resources_is_read_only(self):\n        \"\"\"list_resources is annotated as read-only by default.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = next(t for t in tools if t.name == \"list_resources\")\n            assert tool.annotations is not None\n            assert tool.annotations.readOnlyHint is True\n\n    async def test_read_resource_is_read_only(self):\n        \"\"\"read_resource is annotated as read-only by default.\"\"\"\n        mcp = FastMCP(\"Test\")\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            tool = next(t for t in tools if t.name == \"read_resource\")\n            assert tool.annotations is not None\n            assert tool.annotations.readOnlyHint is True\n\n\ndef _deny_all(ctx: AuthContext) -> bool:\n    \"\"\"Auth check that always denies access.\"\"\"\n    return False\n\n\nclass TestResourcesAsToolsAuthOnServer:\n    \"\"\"Auth checks work when using ResourcesAsTools on a FastMCP server.\"\"\"\n\n    async def test_auth_protected_resources_hidden_from_list(self):\n        \"\"\"Auth-protected resources are filtered from list_resources tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"test://open\")\n        def open_resource() -> str:\n            return \"open content\"\n\n        @mcp.resource(\"test://protected\", auth=_deny_all)\n        def protected_resource() -> str:\n            return \"protected content\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            items = json.loads(result.data)\n            uris = [r.get(\"uri\") for r in items if r.get(\"uri\")]\n            assert \"test://open\" in uris\n            assert \"test://protected\" not in uris\n\n    async def test_auth_protected_resource_cannot_be_read(self):\n        \"\"\"Auth-protected resources cannot be read via read_resource tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"test://protected\", auth=_deny_all)\n        def protected_resource() -> str:\n            return \"protected content\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            with pytest.raises(ToolError):\n                await client.call_tool(\"read_resource\", {\"uri\": \"test://protected\"})\n\n    async def test_open_resource_still_accessible(self):\n        \"\"\"Non-auth-protected resources can still be read.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"test://open\")\n        def open_resource() -> str:\n            return \"open content\"\n\n        @mcp.resource(\"test://protected\", auth=_deny_all)\n        def protected_resource() -> str:\n            return \"protected content\"\n\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"read_resource\", {\"uri\": \"test://open\"})\n            assert result.data == \"open content\"\n\n\nclass TestResourcesAsToolsVisibilityOnServer:\n    \"\"\"Visibility filtering works when using ResourcesAsTools on a server.\"\"\"\n\n    async def test_disabled_resources_hidden_from_list(self):\n        \"\"\"Disabled resources are not listed via list_resources tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"test://public\")\n        def public_resource() -> str:\n            return \"public content\"\n\n        @mcp.resource(\"test://secret\")\n        def secret_resource() -> str:\n            return \"secret content\"\n\n        mcp.disable(names={\"test://secret\"})\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"list_resources\", {})\n            items = json.loads(result.data)\n            uris = [r.get(\"uri\") for r in items if r.get(\"uri\")]\n            assert \"test://public\" in uris\n            assert \"test://secret\" not in uris\n\n    async def test_disabled_resource_cannot_be_read(self):\n        \"\"\"Disabled resources cannot be read via read_resource tool.\"\"\"\n        mcp = FastMCP(\"Test\")\n\n        @mcp.resource(\"test://secret\")\n        def secret_resource() -> str:\n            return \"secret content\"\n\n        mcp.disable(names={\"test://secret\"})\n        mcp.add_transform(ResourcesAsTools(mcp))\n\n        async with Client(mcp) as client:\n            with pytest.raises(ToolError):\n                await client.call_tool(\"read_resource\", {\"uri\": \"test://secret\"})\n"
  },
  {
    "path": "tests/server/transforms/test_search.py",
    "content": "\"\"\"Tests for search transforms.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom typing import Any\nfrom unittest.mock import MagicMock\n\nimport mcp.types as mcp_types\nimport pytest\nfrom mcp.types import TextContent\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.context import Context\nfrom fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext\nfrom fastmcp.server.transforms import Visibility\nfrom fastmcp.server.transforms.search.bm25 import (\n    BM25SearchTransform,\n    _BM25Index,\n    _catalog_hash,\n)\nfrom fastmcp.server.transforms.search.regex import RegexSearchTransform\nfrom fastmcp.tools.base import Tool, ToolResult\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _parse_tool_result(result: ToolResult) -> list[dict[str, Any]]:\n    \"\"\"Extract tool list from a ToolResult's structured content.\"\"\"\n    assert result.structured_content is not None\n    return result.structured_content[\"result\"]\n\n\ndef _make_server_with_tools() -> FastMCP:\n    mcp = FastMCP(\"test\")\n\n    @mcp.tool\n    def add(a: int, b: int) -> int:\n        \"\"\"Add two numbers together.\"\"\"\n        return a + b\n\n    @mcp.tool\n    def multiply(x: float, y: float) -> float:\n        \"\"\"Multiply two numbers.\"\"\"\n        return x * y\n\n    @mcp.tool\n    def search_database(query: str, limit: int = 10) -> str:\n        \"\"\"Search the database for records matching the query.\"\"\"\n        return f\"results for {query}\"\n\n    @mcp.tool\n    def delete_record(record_id: str) -> str:\n        \"\"\"Delete a record from the database by its ID.\"\"\"\n        return f\"deleted {record_id}\"\n\n    @mcp.tool\n    def send_email(to: str, subject: str, body: str) -> str:\n        \"\"\"Send an email to the given recipient.\"\"\"\n        return \"sent\"\n\n    return mcp\n\n\n# ---------------------------------------------------------------------------\n# Shared behavior tests (parameterized across both transforms)\n# ---------------------------------------------------------------------------\n\n\nclass TestBaseTransformBehavior:\n    \"\"\"Tests for behavior shared by all search transforms.\"\"\"\n\n    async def test_list_tools_hides_tools_regex(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert names == {\"search_tools\", \"call_tool\"}\n\n    async def test_list_tools_hides_tools_bm25(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert names == {\"search_tools\", \"call_tool\"}\n\n    async def test_always_visible_pins_tools(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform(always_visible=[\"add\"]))\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert \"add\" in names\n        assert \"search_tools\" in names\n        assert \"call_tool\" in names\n        assert \"multiply\" not in names\n\n    async def test_get_tool_returns_synthetic(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        search = await mcp.get_tool(\"search_tools\")\n        assert search is not None\n        assert search.name == \"search_tools\"\n        call = await mcp.get_tool(\"call_tool\")\n        assert call is not None\n        assert call.name == \"call_tool\"\n\n    async def test_get_tool_passes_through_hidden(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        tool = await mcp.get_tool(\"add\")\n        assert tool is not None\n        assert tool.name == \"add\"\n\n    async def test_call_tool_proxy_executes(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        # Need to call list_tools to populate catalog\n        await mcp.list_tools()\n        result = await mcp.call_tool(\n            \"call_tool\", {\"name\": \"add\", \"arguments\": {\"a\": 2, \"b\": 3}}\n        )\n        assert any(\"5\" in c.text for c in result.content if isinstance(c, TextContent))\n\n    async def test_custom_tool_names(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(\n            RegexSearchTransform(\n                search_tool_name=\"find_tools\",\n                call_tool_name=\"run_tool\",\n            )\n        )\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert names == {\"find_tools\", \"run_tool\"}\n        assert await mcp.get_tool(\"find_tools\") is not None\n        assert await mcp.get_tool(\"run_tool\") is not None\n\n    async def test_search_respects_visibility_filtering(self):\n        \"\"\"Tools disabled via Visibility transform should not appear in search.\"\"\"\n        mcp = _make_server_with_tools()\n        mcp.add_transform(Visibility(False, names={\"delete_record\"}))\n        mcp.add_transform(RegexSearchTransform())\n\n        tools = await mcp.list_tools()\n        names = {t.name for t in tools}\n        assert \"delete_record\" not in names\n\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"delete\"})\n        found = _parse_tool_result(result)\n        assert not any(t[\"name\"] == \"delete_record\" for t in found)\n\n    async def test_search_respects_auth_middleware(self):\n        \"\"\"Tools filtered by auth middleware should not appear in search.\"\"\"\n\n        class BlockAdminTools(Middleware):\n            async def on_list_tools(\n                self,\n                context: MiddlewareContext[mcp_types.ListToolsRequest],\n                call_next: CallNext[mcp_types.ListToolsRequest, Sequence[Tool]],\n            ) -> Sequence[Tool]:\n                tools = await call_next(context)\n                return [t for t in tools if t.name != \"delete_record\"]\n\n        mcp = _make_server_with_tools()\n        mcp.add_middleware(BlockAdminTools())\n        mcp.add_transform(RegexSearchTransform())\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            names = {t.name for t in tools}\n            assert \"delete_record\" not in names\n            assert \"search_tools\" in names\n\n            result = await client.call_tool(\"search_tools\", {\"pattern\": \"delete\"})\n            found = _parse_tool_result(result)\n            assert not any(t[\"name\"] == \"delete_record\" for t in found)\n\n    async def test_search_respects_session_visibility(self):\n        \"\"\"Tools disabled via session visibility should not appear in search.\"\"\"\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n\n        @mcp.tool\n        async def disable_delete(ctx: Context) -> str:\n            \"\"\"Helper tool to disable delete_record for this session.\"\"\"\n            await ctx.disable_components(names={\"delete_record\"})\n            return \"disabled\"\n\n        async with Client(mcp) as client:\n            # Before disabling, search should find delete_record\n            result = await client.call_tool(\"search_tools\", {\"pattern\": \"delete\"})\n            found = _parse_tool_result(result)\n            assert any(t[\"name\"] == \"delete_record\" for t in found)\n\n            # Disable via session visibility\n            await client.call_tool(\"disable_delete\", {})\n\n            # After disabling, search should NOT find it\n            result = await client.call_tool(\"search_tools\", {\"pattern\": \"delete\"})\n            found = _parse_tool_result(result)\n            assert not any(t[\"name\"] == \"delete_record\" for t in found)\n\n\n# ---------------------------------------------------------------------------\n# Regex-specific tests\n# ---------------------------------------------------------------------------\n\n\nclass TestRegexSearch:\n    async def test_search_by_name(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"add\"})\n        tools = _parse_tool_result(result)\n        assert any(t[\"name\"] == \"add\" for t in tools)\n\n    async def test_search_by_description(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"email\"})\n        tools = _parse_tool_result(result)\n        assert any(t[\"name\"] == \"send_email\" for t in tools)\n\n    async def test_search_by_param_name(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"record_id\"})\n        tools = _parse_tool_result(result)\n        assert any(t[\"name\"] == \"delete_record\" for t in tools)\n\n    async def test_search_by_param_description(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"recipient\"})\n        tools = _parse_tool_result(result)\n        assert any(t[\"name\"] == \"send_email\" for t in tools)\n\n    async def test_search_or_pattern(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform(max_results=10))\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"add|multiply\"})\n        tools = _parse_tool_result(result)\n        names = {t[\"name\"] for t in tools}\n        assert \"add\" in names\n        assert \"multiply\" in names\n\n    async def test_search_case_insensitive(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"ADD\"})\n        tools = _parse_tool_result(result)\n        assert any(t[\"name\"] == \"add\" for t in tools)\n\n    async def test_search_invalid_pattern(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"[invalid\"})\n        tools = _parse_tool_result(result)\n        assert tools == []\n\n    async def test_search_max_results(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform(max_results=2))\n        await mcp.list_tools()\n        # Match everything\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \".*\"})\n        tools = _parse_tool_result(result)\n        assert len(tools) == 2\n\n    async def test_search_no_matches(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"zzz_nonexistent\"})\n        tools = _parse_tool_result(result)\n        assert tools == []\n\n    async def test_search_returns_full_schema(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"pattern\": \"add\"})\n        tools = _parse_tool_result(result)\n        add_tool = next(t for t in tools if t[\"name\"] == \"add\")\n        assert \"inputSchema\" in add_tool\n        assert \"properties\" in add_tool[\"inputSchema\"]\n\n\n# ---------------------------------------------------------------------------\n# BM25-specific tests\n# ---------------------------------------------------------------------------\n\n\nclass TestBM25Search:\n    async def test_search_relevance(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"database\"})\n        tools = _parse_tool_result(result)\n        # Database tools should rank highest\n        assert len(tools) > 0\n        names = {t[\"name\"] for t in tools}\n        assert \"search_database\" in names\n\n    async def test_search_database_tools(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\n            \"search_tools\", {\"query\": \"delete records from database\"}\n        )\n        tools = _parse_tool_result(result)\n        assert len(tools) > 0\n        # delete_record should be highly relevant\n        assert tools[0][\"name\"] == \"delete_record\"\n\n    async def test_search_max_results(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform(max_results=2))\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"number\"})\n        tools = _parse_tool_result(result)\n        assert len(tools) <= 2\n\n    async def test_search_no_matches(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"zzz_nonexistent_xyz\"})\n        tools = _parse_tool_result(result)\n        assert tools == []\n\n    async def test_index_rebuilds_on_catalog_change(self):\n        \"\"\"When a new tool is added, the next list_tools + search sees it.\"\"\"\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        await mcp.list_tools()\n\n        # Search before adding tool\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"weather forecast\"})\n        tools = _parse_tool_result(result)\n        assert not any(t[\"name\"] == \"get_weather\" for t in tools)\n\n        # Add a new tool\n        @mcp.tool\n        def get_weather(city: str) -> str:\n            \"\"\"Get the weather forecast for a city.\"\"\"\n            return f\"sunny in {city}\"\n\n        # Must call list_tools to refresh the catalog cache\n        await mcp.list_tools()\n\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"weather forecast\"})\n        tools = _parse_tool_result(result)\n        assert any(t[\"name\"] == \"get_weather\" for t in tools)\n\n    async def test_search_empty_query(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"\"})\n        tools = _parse_tool_result(result)\n        assert tools == []\n\n    async def test_search_returns_full_schema(self):\n        mcp = _make_server_with_tools()\n        mcp.add_transform(BM25SearchTransform())\n        await mcp.list_tools()\n        result = await mcp.call_tool(\"search_tools\", {\"query\": \"add numbers\"})\n        tools = _parse_tool_result(result)\n        add_tool = next(t for t in tools if t[\"name\"] == \"add\")\n        assert \"inputSchema\" in add_tool\n        assert \"properties\" in add_tool[\"inputSchema\"]\n\n\n# ---------------------------------------------------------------------------\n# BM25 index unit tests\n# ---------------------------------------------------------------------------\n\n\nclass TestBM25Index:\n    def test_basic_ranking(self):\n        index = _BM25Index()\n        index.build(\n            [\n                \"search database query records\",\n                \"add two numbers together\",\n                \"send email recipient subject\",\n            ]\n        )\n        results = index.query(\"database records\", 3)\n        assert results[0] == 0  # Database doc should rank first\n\n    def test_empty_corpus(self):\n        index = _BM25Index()\n        index.build([])\n        assert index.query(\"anything\", 5) == []\n\n    def test_no_matching_tokens(self):\n        index = _BM25Index()\n        index.build([\"alpha beta gamma\"])\n        assert index.query(\"zzz\", 5) == []\n\n\n# ---------------------------------------------------------------------------\n# call_tool self-reference guard\n# ---------------------------------------------------------------------------\n\n\nclass TestCallToolGuard:\n    async def test_call_tool_proxy_rejects_itself(self):\n        \"\"\"Calling call_tool(name='call_tool') must not recurse infinitely.\"\"\"\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception):\n                await client.call_tool(\n                    \"call_tool\", {\"name\": \"call_tool\", \"arguments\": {}}\n                )\n\n    async def test_call_tool_proxy_rejects_search_tool(self):\n        \"\"\"Calling call_tool(name='search_tools') must be rejected.\"\"\"\n        mcp = _make_server_with_tools()\n        mcp.add_transform(RegexSearchTransform())\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception):\n                await client.call_tool(\n                    \"call_tool\",\n                    {\"name\": \"search_tools\", \"arguments\": {\"pattern\": \"add\"}},\n                )\n\n    async def test_call_tool_proxy_rejects_custom_names(self):\n        \"\"\"Guard works when synthetic tools have custom names.\"\"\"\n        mcp = _make_server_with_tools()\n        mcp.add_transform(\n            RegexSearchTransform(\n                search_tool_name=\"find_tools\", call_tool_name=\"run_tool\"\n            )\n        )\n\n        async with Client(mcp) as client:\n            with pytest.raises(Exception):\n                await client.call_tool(\n                    \"run_tool\", {\"name\": \"run_tool\", \"arguments\": {}}\n                )\n            with pytest.raises(Exception):\n                await client.call_tool(\n                    \"run_tool\", {\"name\": \"find_tools\", \"arguments\": {\"pattern\": \"add\"}}\n                )\n\n\n# ---------------------------------------------------------------------------\n# catalog hash staleness\n# ---------------------------------------------------------------------------\n\n\nclass TestCatalogHash:\n    def test_hash_differs_for_same_name_different_description(self):\n        \"\"\"Hash must change when a tool's description changes, not just its name.\"\"\"\n        tool_a = MagicMock()\n        tool_a.name = \"search\"\n        tool_a.description = \"find records in the database\"\n        tool_a.parameters = {}\n\n        tool_b = MagicMock()\n        tool_b.name = \"search\"\n        tool_b.description = \"send an email to a recipient\"\n        tool_b.parameters = {}\n\n        assert _catalog_hash([tool_a]) != _catalog_hash([tool_b])\n"
  },
  {
    "path": "tests/server/transforms/test_visibility.py",
    "content": "\"\"\"Tests for Visibility transform.\"\"\"\n\nimport pytest\n\nfrom fastmcp.server.transforms.visibility import Visibility, is_enabled\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.versions import VersionSpec\n\n\nclass TestMatching:\n    \"\"\"Test component matching logic.\"\"\"\n\n    def test_empty_criteria_matches_nothing(self):\n        \"\"\"Empty criteria is a safe default - matches nothing.\"\"\"\n        t = Visibility(False)\n        assert t._matches(Tool(name=\"anything\", parameters={})) is False\n\n    def test_match_all_matches_everything(self):\n        \"\"\"match_all=True matches all components.\"\"\"\n        t = Visibility(False, match_all=True)\n        assert t._matches(Tool(name=\"anything\", parameters={})) is True\n\n    def test_match_by_name(self):\n        \"\"\"Matches component by name.\"\"\"\n        t = Visibility(False, names={\"foo\"})\n        assert t._matches(Tool(name=\"foo\", parameters={})) is True\n        assert t._matches(Tool(name=\"bar\", parameters={})) is False\n\n    def test_match_by_version(self):\n        \"\"\"Matches component by version.\"\"\"\n        t = Visibility(False, version=VersionSpec(eq=\"v1\"))\n        assert t._matches(Tool(name=\"foo\", version=\"v1\", parameters={})) is True\n        assert t._matches(Tool(name=\"foo\", version=\"v2\", parameters={})) is False\n\n    def test_match_by_version_spec_exact(self):\n        \"\"\"VersionSpec(eq=\"v1\") matches v1 only.\"\"\"\n        t = Visibility(False, version=VersionSpec(eq=\"v1\"))\n        assert t._matches(Tool(name=\"foo\", version=\"v1\", parameters={})) is True\n        assert t._matches(Tool(name=\"foo\", version=\"v2\", parameters={})) is False\n        assert t._matches(Tool(name=\"foo\", version=\"v0\", parameters={})) is False\n\n    def test_match_by_version_spec_gte(self):\n        \"\"\"VersionSpec(gte=\"v2\") matches v2, v3, but not v1.\"\"\"\n        t = Visibility(False, version=VersionSpec(gte=\"v2\"))\n        assert t._matches(Tool(name=\"foo\", version=\"v1\", parameters={})) is False\n        assert t._matches(Tool(name=\"foo\", version=\"v2\", parameters={})) is True\n        assert t._matches(Tool(name=\"foo\", version=\"v3\", parameters={})) is True\n\n    def test_match_by_version_spec_range(self):\n        \"\"\"VersionSpec(gte=\"v1\", lt=\"v3\") matches v1, v2, but not v3.\"\"\"\n        t = Visibility(False, version=VersionSpec(gte=\"v1\", lt=\"v3\"))\n        assert t._matches(Tool(name=\"foo\", version=\"v0\", parameters={})) is False\n        assert t._matches(Tool(name=\"foo\", version=\"v1\", parameters={})) is True\n        assert t._matches(Tool(name=\"foo\", version=\"v2\", parameters={})) is True\n        assert t._matches(Tool(name=\"foo\", version=\"v3\", parameters={})) is False\n        assert t._matches(Tool(name=\"foo\", version=\"v4\", parameters={})) is False\n\n    def test_unversioned_does_not_match_version_spec(self):\n        \"\"\"Unversioned components (version=None) don't match a VersionSpec.\"\"\"\n        t = Visibility(False, version=VersionSpec(eq=\"v1\"))\n        assert t._matches(Tool(name=\"foo\", parameters={})) is False\n\n        t2 = Visibility(False, version=VersionSpec(gte=\"v1\"))\n        assert t2._matches(Tool(name=\"foo\", parameters={})) is False\n\n    def test_match_by_tag(self):\n        \"\"\"Matches if component has any of the specified tags.\"\"\"\n        t = Visibility(False, tags=set({\"internal\", \"deprecated\"}))\n        assert t._matches(Tool(name=\"foo\", parameters={}, tags={\"internal\"})) is True\n        assert t._matches(Tool(name=\"foo\", parameters={}, tags={\"public\"})) is False\n\n    def test_match_by_component_type(self):\n        \"\"\"Only matches specified component types.\"\"\"\n        t = Visibility(False, names={\"foo\"}, components={\"prompt\"})\n        # Tool has key \"tool:foo@\", not \"prompt:foo@\"\n        assert t._matches(Tool(name=\"foo\", parameters={})) is False\n\n    def test_all_criteria_must_match(self):\n        \"\"\"Multiple criteria use AND logic - all must match.\"\"\"\n        t = Visibility(\n            False,\n            names={\"foo\"},\n            version=VersionSpec(eq=\"v1\"),\n            tags=set({\"internal\"}),\n        )\n        # All match\n        assert (\n            t._matches(Tool(name=\"foo\", version=\"v1\", parameters={}, tags={\"internal\"}))\n            is True\n        )\n        # Version doesn't match\n        assert (\n            t._matches(Tool(name=\"foo\", version=\"v2\", parameters={}, tags={\"internal\"}))\n            is False\n        )\n\n\nclass TestMarking:\n    \"\"\"Test visibility state marking.\"\"\"\n\n    def test_disable_marks_as_disabled(self):\n        \"\"\"Visibility(False, ...) marks matching components as disabled.\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        marked = Visibility(False, names={\"foo\"})._mark_component(tool)\n        assert is_enabled(marked) is False\n\n    def test_enable_marks_as_enabled(self):\n        \"\"\"Visibility(True, ...) marks matching components as enabled.\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        marked = Visibility(True, names={\"foo\"})._mark_component(tool)\n        assert is_enabled(marked) is True\n        assert marked.meta is not None\n        assert marked.meta[\"fastmcp\"][\"_internal\"][\"visibility\"] is True\n\n    def test_non_matching_unchanged(self):\n        \"\"\"Non-matching components are not modified.\"\"\"\n        tool = Tool(name=\"bar\", parameters={})\n        result = Visibility(False, names={\"foo\"})._mark_component(tool)\n        # No _internal key added\n        assert result.meta is None or \"_internal\" not in result.meta.get(\"fastmcp\", {})\n        assert is_enabled(result) is True\n\n    def test_returns_copy_for_matching(self):\n        \"\"\"Marking returns a copy to avoid mutating shared provider objects.\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        result = Visibility(False, names={\"foo\"})._mark_component(tool)\n        assert result is not tool\n        assert is_enabled(result) is False\n        # Original is untouched\n        assert is_enabled(tool) is True\n\n    def test_disable_all(self):\n        \"\"\"match_all=True disables all components.\"\"\"\n        tool = Tool(name=\"anything\", parameters={})\n        marked = Visibility(False, match_all=True)._mark_component(tool)\n        assert is_enabled(marked) is False\n\n\nclass TestOverride:\n    \"\"\"Test that later marks override earlier ones.\"\"\"\n\n    def test_enable_overrides_disable(self):\n        \"\"\"An enable after disable results in enabled.\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        marked = Visibility(False, names={\"foo\"})._mark_component(tool)\n        assert is_enabled(marked) is False\n\n        marked = Visibility(True, names={\"foo\"})._mark_component(marked)\n        assert is_enabled(marked) is True\n\n    def test_disable_overrides_enable(self):\n        \"\"\"A disable after enable results in disabled.\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        marked = Visibility(True, names={\"foo\"})._mark_component(tool)\n        assert is_enabled(marked) is True\n\n        marked = Visibility(False, names={\"foo\"})._mark_component(marked)\n        assert is_enabled(marked) is False\n\n\nclass TestHelperFunctions:\n    \"\"\"Test is_enabled helper.\"\"\"\n\n    def test_unmarked_is_enabled(self):\n        \"\"\"Components without marks are enabled by default.\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        assert is_enabled(tool) is True\n\n    def test_filtering_pattern(self):\n        \"\"\"Common pattern: filter list with is_enabled.\"\"\"\n        tools = [\n            Tool(name=\"enabled\", parameters={}),\n            Tool(name=\"disabled\", parameters={}),\n        ]\n        vis = Visibility(False, names={\"disabled\"})\n        marked_tools = [vis._mark_component(t) for t in tools]\n\n        visible = [t for t in marked_tools if is_enabled(t)]\n        assert [t.name for t in visible] == [\"enabled\"]\n\n\nclass TestMetadata:\n    \"\"\"Test metadata handling.\"\"\"\n\n    def test_internal_metadata_stripped_by_get_meta(self):\n        \"\"\"Internal metadata is stripped when calling get_meta().\"\"\"\n        tool = Tool(name=\"foo\", parameters={})\n        marked = Visibility(True, names={\"foo\"})._mark_component(tool)\n\n        # Raw meta has _internal\n        assert marked.meta is not None\n        assert \"_internal\" in marked.meta.get(\"fastmcp\", {})\n\n        # get_meta() strips it\n        output = marked.get_meta()\n        assert \"_internal\" not in output.get(\"fastmcp\", {})\n\n    def test_user_metadata_preserved(self):\n        \"\"\"User-provided metadata is not affected.\"\"\"\n        tool = Tool(name=\"foo\", parameters={}, meta={\"custom\": \"value\"})\n        marked = Visibility(False, names={\"foo\"})._mark_component(tool)\n\n        assert marked.meta is not None\n        assert marked.meta[\"custom\"] == \"value\"\n\n\nclass TestRepr:\n    \"\"\"Test string representation.\"\"\"\n\n    def test_repr_disable(self):\n        \"\"\"Repr shows disable action and criteria.\"\"\"\n        t = Visibility(False, names={\"foo\"})\n        r = repr(t)\n        assert \"disable\" in r\n        assert \"foo\" in r\n\n    def test_repr_enable(self):\n        \"\"\"Repr shows enable action.\"\"\"\n        t = Visibility(True, names={\"foo\"})\n        assert \"enable\" in repr(t)\n\n    def test_repr_match_all(self):\n        \"\"\"Repr shows match_all.\"\"\"\n        t = Visibility(False, match_all=True)\n        assert \"match_all=True\" in repr(t)\n\n\nclass TestTransformChain:\n    \"\"\"Test Visibility in async transform chains.\"\"\"\n\n    @pytest.fixture\n    def tools(self):\n        return [\n            Tool(name=\"public\", parameters={}, tags={\"public\"}),\n            Tool(name=\"internal\", parameters={}, tags={\"internal\"}),\n            Tool(name=\"safe_internal\", parameters={}, tags={\"internal\", \"safe\"}),\n        ]\n\n    async def test_list_tools_marks_matching(self, tools):\n        \"\"\"list_tools applies marks to matching components.\"\"\"\n        disable_internal = Visibility(False, tags=set({\"internal\"}))\n\n        result = await disable_internal.list_tools(tools)\n\n        assert len(result) == 3\n        assert is_enabled(result[0])  # public\n        assert not is_enabled(result[1])  # internal\n        assert not is_enabled(result[2])  # safe_internal\n\n    async def test_later_transform_overrides(self, tools):\n        \"\"\"Later transforms in chain override earlier ones.\"\"\"\n        disable_internal = Visibility(False, tags=set({\"internal\"}))\n        enable_safe = Visibility(True, tags=set({\"safe\"}))\n\n        # Apply transforms sequentially\n        after_disable = await disable_internal.list_tools(tools)\n        result = await enable_safe.list_tools(after_disable)\n        enabled = [t for t in result if is_enabled(t)]\n\n        # public: never disabled\n        # internal: disabled, stays disabled\n        # safe_internal: disabled then re-enabled\n        assert {t.name for t in enabled} == {\"public\", \"safe_internal\"}\n\n    async def test_allowlist_pattern(self, tools):\n        \"\"\"Disable all, then enable specific = allowlist.\"\"\"\n        disable_all = Visibility(False, match_all=True)\n        enable_public = Visibility(True, tags=set({\"public\"}))\n\n        # Apply transforms sequentially\n        after_disable = await disable_all.list_tools(tools)\n        result = await enable_public.list_tools(after_disable)\n        enabled = [t for t in result if is_enabled(t)]\n\n        assert [t.name for t in enabled] == [\"public\"]\n"
  },
  {
    "path": "tests/server/versioning/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/versioning/test_calls.py",
    "content": "\"\"\"Tests for versioned calls and client version selection.\"\"\"\n# ruff: noqa: F811  # Intentional function redefinition for version testing\n\nfrom __future__ import annotations\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.versions import (\n    VersionSpec,\n)\n\n\nclass TestVersionMixingValidation:\n    \"\"\"Tests for versioned/unversioned mixing prevention.\"\"\"\n\n    async def test_resource_mixing_rejected(self):\n        \"\"\"Cannot mix versioned and unversioned resources with the same URI.\"\"\"\n        import pytest\n\n        mcp = FastMCP()\n\n        @mcp.resource(\"file:///config\", version=\"1.0\")\n        def config_v1() -> str:\n            return \"v1\"\n\n        with pytest.raises(ValueError, match=\"unversioned.*versioned\"):\n\n            @mcp.resource(\"file:///config\")\n            def config_unversioned() -> str:\n                return \"unversioned\"\n\n    async def test_prompt_mixing_rejected(self):\n        \"\"\"Cannot mix versioned and unversioned prompts with the same name.\"\"\"\n        import pytest\n\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        with pytest.raises(ValueError, match=\"versioned.*unversioned\"):\n\n            @mcp.prompt(version=\"1.0\")\n            def greet(name: str) -> str:\n                return f\"Hi, {name}!\"\n\n    async def test_multiple_versions_allowed(self):\n        \"\"\"Multiple versioned components with same name are allowed.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        @mcp.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        # All versioned - list_tools returns all\n        tools = await mcp.list_tools()\n        assert len(tools) == 3\n        versions = {t.version for t in tools}\n        assert versions == {\"1.0\", \"2.0\", \"3.0\"}\n\n        # get_tool returns highest\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"3.0\"\n\n\nclass TestVersionValidation:\n    \"\"\"Tests for version string validation.\"\"\"\n\n    async def test_version_with_at_symbol_rejected(self):\n        \"\"\"Version strings containing '@' should be rejected.\"\"\"\n        import pytest\n        from pydantic import ValidationError\n\n        mcp = FastMCP()\n\n        with pytest.raises(ValidationError, match=\"cannot contain '@'\"):\n\n            @mcp.tool(version=\"1.0@beta\")\n            def my_tool() -> str:\n                return \"test\"\n\n\nclass TestVersionMetadata:\n    \"\"\"Tests for version metadata exposure in list operations.\"\"\"\n\n    async def test_tool_versions_in_meta(self):\n        \"\"\"Each version has its own version in metadata.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def add(x: int, y: int) -> int:  # noqa: F811\n            return x + y\n\n        @mcp.tool(version=\"2.0\")\n        def add(x: int, y: int) -> int:  # noqa: F811\n            return x + y\n\n        # list_tools returns all versions\n        tools = await mcp.list_tools()\n        assert len(tools) == 2\n\n        # Each version has its own version in metadata\n        by_version = {t.version: t for t in tools}\n        assert by_version[\"1.0\"].get_meta()[\"fastmcp\"][\"version\"] == \"1.0\"\n        assert by_version[\"2.0\"].get_meta()[\"fastmcp\"][\"version\"] == \"2.0\"\n\n    async def test_resource_versions_in_meta(self):\n        \"\"\"Each version has its own version in metadata.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://config\", version=\"1.0\")\n        def config_v1() -> str:  # noqa: F811\n            return \"v1\"\n\n        @mcp.resource(\"data://config\", version=\"2.0\")\n        def config_v2() -> str:  # noqa: F811\n            return \"v2\"\n\n        # list_resources returns all versions\n        resources = await mcp.list_resources()\n        assert len(resources) == 2\n\n        # Each version has its own version in metadata\n        by_version = {r.version: r for r in resources}\n        assert by_version[\"1.0\"].get_meta()[\"fastmcp\"][\"version\"] == \"1.0\"\n        assert by_version[\"2.0\"].get_meta()[\"fastmcp\"][\"version\"] == \"2.0\"\n\n    async def test_prompt_versions_in_meta(self):\n        \"\"\"Each version has its own version in metadata.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet() -> str:  # noqa: F811\n            return \"Hello v1\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet() -> str:  # noqa: F811\n            return \"Hello v2\"\n\n        # list_prompts returns all versions\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 2\n\n        # Each version has its own version in metadata\n        by_version = {p.version: p for p in prompts}\n        assert by_version[\"1.0\"].get_meta()[\"fastmcp\"][\"version\"] == \"1.0\"\n        assert by_version[\"2.0\"].get_meta()[\"fastmcp\"][\"version\"] == \"2.0\"\n\n    async def test_unversioned_no_versions_list(self):\n        \"\"\"Unversioned components should not have versions list in meta.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def simple() -> str:\n            return \"simple\"\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n\n        tool = tools[0]\n        meta = tool.get_meta()\n        assert \"versions\" not in meta.get(\"fastmcp\", {})\n\n\nclass TestVersionedCalls:\n    \"\"\"Tests for calling specific component versions.\"\"\"\n\n    async def test_call_tool_with_version(self):\n        \"\"\"call_tool should use specified version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calculate(x: int, y: int) -> int:  # noqa: F811\n            return x + y\n\n        @mcp.tool(version=\"2.0\")\n        def calculate(x: int, y: int) -> int:  # noqa: F811\n            return x * y\n\n        # Default: highest version (2.0, multiplication)\n        result = await mcp.call_tool(\"calculate\", {\"x\": 3, \"y\": 4})\n        assert result.structured_content is not None\n        assert result.structured_content[\"result\"] == 12\n\n        # Explicit v1.0 (addition)\n        result = await mcp.call_tool(\n            \"calculate\", {\"x\": 3, \"y\": 4}, version=VersionSpec(eq=\"1.0\")\n        )\n        assert result.structured_content is not None\n        assert result.structured_content[\"result\"] == 7\n\n        # Explicit v2.0 (multiplication)\n        result = await mcp.call_tool(\n            \"calculate\", {\"x\": 3, \"y\": 4}, version=VersionSpec(eq=\"2.0\")\n        )\n        assert result.structured_content is not None\n        assert result.structured_content[\"result\"] == 12\n\n    async def test_read_resource_with_version(self):\n        \"\"\"read_resource should use specified version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://config\", version=\"1.0\")\n        def config() -> str:  # noqa: F811\n            return \"config v1\"\n\n        @mcp.resource(\"data://config\", version=\"2.0\")\n        def config() -> str:  # noqa: F811\n            return \"config v2\"\n\n        # Default: highest version\n        result = await mcp.read_resource(\"data://config\")\n        assert result.contents[0].content == \"config v2\"\n\n        # Explicit v1.0\n        result = await mcp.read_resource(\"data://config\", version=VersionSpec(eq=\"1.0\"))\n        assert result.contents[0].content == \"config v1\"\n\n    async def test_render_prompt_with_version(self):\n        \"\"\"render_prompt should use specified version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet() -> str:  # noqa: F811\n            return \"Hello from v1\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet() -> str:  # noqa: F811\n            return \"Hello from v2\"\n\n        # Default: highest version\n        result = await mcp.render_prompt(\"greet\")\n        content = result.messages[0].content\n        assert isinstance(content, TextContent) and content.text == \"Hello from v2\"\n\n        # Explicit v1.0\n        result = await mcp.render_prompt(\"greet\", version=VersionSpec(eq=\"1.0\"))\n        content = result.messages[0].content\n        assert isinstance(content, TextContent) and content.text == \"Hello from v1\"\n\n    async def test_call_tool_invalid_version_not_found(self):\n        \"\"\"Calling with non-existent version should raise NotFoundError.\"\"\"\n        import pytest\n\n        from fastmcp.exceptions import NotFoundError\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def mytool() -> str:\n            return \"v1\"\n\n        with pytest.raises(NotFoundError):\n            await mcp.call_tool(\"mytool\", {}, version=VersionSpec(eq=\"999.0\"))\n\n\nclass TestClientVersionSelection:\n    \"\"\"Tests for client-side version selection via the version parameter.\n\n    Version selection flows through request-level _meta, not arguments.\n    \"\"\"\n\n    import pytest\n\n    @pytest.mark.parametrize(\n        \"version,expected\",\n        [\n            (None, 10),  # Default: highest version (2.0) -> 5 * 2\n            (\"1.0\", 6),  # v1.0 -> 5 + 1\n            (\"2.0\", 10),  # v2.0 -> 5 * 2\n        ],\n    )\n    async def test_call_tool_version_selection(\n        self, version: str | None, expected: int\n    ):\n        \"\"\"Client.call_tool routes to correct version via request meta.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc(x: int) -> int:  # noqa: F811\n            return x + 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc(x: int) -> int:  # noqa: F811\n            return x * 2\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"calc\", {\"x\": 5}, version=version)\n            assert result.data == expected\n\n    @pytest.mark.parametrize(\n        \"version,expected\",\n        [\n            (None, \"Hello world from v2\"),  # Default: highest version\n            (\"1.0\", \"Hello world from v1\"),\n            (\"2.0\", \"Hello world from v2\"),\n        ],\n    )\n    async def test_get_prompt_version_selection(\n        self, version: str | None, expected: str\n    ):\n        \"\"\"Client.get_prompt routes to correct version via request meta.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet(name: str) -> str:  # noqa: F811\n            return f\"Hello {name} from v1\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet(name: str) -> str:  # noqa: F811\n            return f\"Hello {name} from v2\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\n                \"greet\", {\"name\": \"world\"}, version=version\n            )\n            content = result.messages[0].content\n            assert isinstance(content, TextContent) and content.text == expected\n\n    @pytest.mark.parametrize(\n        \"version,expected\",\n        [\n            (None, \"v2 data\"),  # Default: highest version\n            (\"1.0\", \"v1 data\"),\n            (\"2.0\", \"v2 data\"),\n        ],\n    )\n    async def test_read_resource_version_selection(\n        self, version: str | None, expected: str\n    ):\n        \"\"\"Client.read_resource routes to correct version via request meta.\"\"\"\n        from fastmcp import Client\n\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://info\", version=\"1.0\")\n        def info_v1() -> str:  # noqa: F811\n            return \"v1 data\"\n\n        @mcp.resource(\"data://info\", version=\"2.0\")\n        def info_v2() -> str:  # noqa: F811\n            return \"v2 data\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"data://info\", version=version)\n            assert result[0].text == expected\n"
  },
  {
    "path": "tests/server/versioning/test_filtering.py",
    "content": "\"\"\"Tests for version filtering functionality.\"\"\"\n# ruff: noqa: F811  # Intentional function redefinition for version testing\n\nfrom __future__ import annotations\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.transforms import VersionFilter\nfrom fastmcp.utilities.versions import (\n    VersionSpec,\n)\n\n\nclass TestVersionFilter:\n    \"\"\"Tests for VersionFilter transform.\"\"\"\n\n    async def test_version_lt_filters_high_versions(self):\n        \"\"\"VersionFilter(version_lt='3.0') hides v3+, shows v1 and v2.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        @mcp.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        # Without filter, list_tools returns all versions\n        tools = await mcp.list_tools()\n        versions = {t.version for t in tools}\n        assert versions == {\"1.0\", \"2.0\", \"3.0\"}\n\n        # With filter, only v1 and v2 are visible\n        mcp.add_transform(VersionFilter(version_lt=\"3.0\"))\n        tools = await mcp.list_tools()\n        versions = {t.version for t in tools}\n        assert versions == {\"1.0\", \"2.0\"}\n\n        # get_tool returns highest matching version\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"2.0\"\n\n    async def test_version_gte_filters_low_versions(self):\n        \"\"\"VersionFilter(version_gte='2.0') hides v1, shows v2 and v3.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def add(x: int) -> int:\n            return x + 1\n\n        @mcp.tool(version=\"2.0\")\n        def add(x: int) -> int:\n            return x + 2\n\n        @mcp.tool(version=\"3.0\")\n        def add(x: int) -> int:\n            return x + 3\n\n        mcp.add_transform(VersionFilter(version_gte=\"2.0\"))\n\n        # list_tools shows all matching versions (v2 and v3)\n        tools = await mcp.list_tools()\n        versions = {t.version for t in tools}\n        assert versions == {\"2.0\", \"3.0\"}\n\n        # get_tool returns highest matching version\n        tool = await mcp.get_tool(\"add\")\n        assert tool is not None\n        assert tool.version == \"3.0\"\n\n        # Can request specific versions in range\n        tool_v2 = await mcp.get_tool(\"add\", VersionSpec(eq=\"2.0\"))\n        assert tool_v2 is not None\n        assert tool_v2.version == \"2.0\"\n\n        # Cannot request version outside range - returns None\n        assert await mcp.get_tool(\"add\", VersionSpec(eq=\"1.0\")) is None\n\n    async def test_version_range(self):\n        \"\"\"VersionFilter(version_gte='2.0', version_lt='3.0') shows only v2.x.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        @mcp.tool(version=\"2.5\")\n        def calc() -> int:\n            return 25\n\n        @mcp.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        mcp.add_transform(VersionFilter(version_gte=\"2.0\", version_lt=\"3.0\"))\n\n        # list_tools shows all versions in range\n        tools = await mcp.list_tools()\n        versions = {t.version for t in tools}\n        assert versions == {\"2.0\", \"2.5\"}\n\n        # get_tool returns highest in range\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"2.5\"\n\n        # Can request specific versions in range\n        tool_v2 = await mcp.get_tool(\"calc\", VersionSpec(eq=\"2.0\"))\n        assert tool_v2 is not None\n        assert tool_v2.version == \"2.0\"\n\n        # Versions outside range are not accessible - return None\n        assert await mcp.get_tool(\"calc\", VersionSpec(eq=\"1.0\")) is None\n        assert await mcp.get_tool(\"calc\", VersionSpec(eq=\"3.0\")) is None\n\n    async def test_unversioned_always_passes(self):\n        \"\"\"Unversioned components pass through any filter.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def unversioned_tool() -> str:\n            return \"unversioned\"\n\n        @mcp.tool(version=\"5.0\")\n        def versioned_tool() -> str:\n            return \"v5\"\n\n        # Filter that would exclude v5.0\n        mcp.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n        tools = await mcp.list_tools()\n        names = [t.name for t in tools]\n        assert \"unversioned_tool\" in names\n        assert \"versioned_tool\" not in names\n\n    async def test_include_unversioned_false_excludes_unversioned_tools(self):\n        \"\"\"Setting include_unversioned=False hides unversioned tools.\"\"\"\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def unversioned_tool() -> str:\n            return \"unversioned\"\n\n        @mcp.tool(version=\"2.0\")\n        def included_versioned_tool() -> str:\n            return \"v2\"\n\n        @mcp.tool(version=\"5.0\")\n        def excluded_versioned_tool() -> str:\n            return \"v5\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"3.0\", include_unversioned=False))\n\n        tools = await mcp.list_tools()\n        assert [tool.name for tool in tools] == [\"included_versioned_tool\"]\n\n    async def test_date_versions(self):\n        \"\"\"Works with date-based versions like '2025-01-15'.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"2025-01-01\")\n        def report() -> str:\n            return \"jan\"\n\n        @mcp.tool(version=\"2025-06-01\")\n        def report() -> str:\n            return \"jun\"\n\n        @mcp.tool(version=\"2025-12-01\")\n        def report() -> str:\n            return \"dec\"\n\n        # Q1 API: before April\n        mcp.add_transform(VersionFilter(version_lt=\"2025-04-01\"))\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].version == \"2025-01-01\"\n\n    async def test_get_tool_respects_filter(self):\n        \"\"\"get_tool() returns None if highest version is filtered out.\"\"\"\n\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"5.0\")\n        def only_v5() -> str:\n            return \"v5\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n        # Tool exists but is filtered out - returns None (use get_tool to apply transforms)\n        assert await mcp.get_tool(\"only_v5\") is None\n\n    async def test_must_specify_at_least_one(self):\n        \"\"\"VersionFilter() with no args raises ValueError.\"\"\"\n        import pytest\n\n        from fastmcp.server.transforms import VersionFilter\n\n        with pytest.raises(ValueError, match=\"At least one of\"):\n            VersionFilter()\n\n    async def test_resources_filtered(self):\n        \"\"\"Resources are filtered by version.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.resource(\"file:///config\", version=\"1.0\")\n        def config_v1() -> str:\n            return \"v1\"\n\n        @mcp.resource(\"file:///config\", version=\"2.0\")\n        def config_v2() -> str:\n            return \"v2\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"2.0\"))\n\n        resources = await mcp.list_resources()\n        assert len(resources) == 1\n        assert resources[0].version == \"1.0\"\n\n    async def test_include_unversioned_false_excludes_unversioned_resources(self):\n        \"\"\"Setting include_unversioned=False hides unversioned resources.\"\"\"\n\n        mcp = FastMCP()\n\n        @mcp.resource(\"file:///unversioned\")\n        def unversioned_resource() -> str:\n            return \"unversioned\"\n\n        @mcp.resource(\"file:///included_versioned\", version=\"1.0\")\n        def included_versioned_resource() -> str:\n            return \"v1\"\n\n        @mcp.resource(\"file:///excluded_versioned\", version=\"5.0\")\n        def excluded_versioned_resource() -> str:\n            return \"v5\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"2.0\", include_unversioned=False))\n\n        resources = await mcp.list_resources()\n        assert [str(resource.uri) for resource in resources] == [\n            \"file:///included_versioned\"\n        ]\n\n    async def test_include_unversioned_false_excludes_unversioned_resource_templates(\n        self,\n    ):\n        \"\"\"Setting include_unversioned=False hides unversioned resource templates.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"resource://unversioned/{name}\")\n        def unversioned_template(name: str) -> str:\n            return f\"unversioned:{name}\"\n\n        @mcp.resource(\"resource://included_versioned/{name}\", version=\"1.0\")\n        def included_versioned_template(name: str) -> str:\n            return f\"versioned:{name}\"\n\n        @mcp.resource(\"resource://excluded_versioned/{name}\", version=\"5.0\")\n        def excluded_versioned_template(name: str) -> str:\n            return f\"excluded:{name}\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"2.0\", include_unversioned=False))\n\n        templates = await mcp.list_resource_templates()\n        assert [template.uri_template for template in templates] == [\n            \"resource://included_versioned/{name}\"\n        ]\n\n    async def test_prompts_filtered(self):\n        \"\"\"Prompts are filtered by version.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet(name: str) -> str:\n            return f\"Hi {name}\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet(name: str) -> str:\n            return f\"Hello {name}\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"2.0\"))\n\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 1\n        assert prompts[0].version == \"1.0\"\n\n    async def test_include_unversioned_false_excludes_unversioned_prompts(self):\n        \"\"\"Setting include_unversioned=False hides unversioned prompts.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt\n        def unversioned_prompt(name: str) -> str:\n            return f\"Unversioned: {name}\"\n\n        @mcp.prompt(version=\"1.0\")\n        def included_versioned_prompt(name: str) -> str:\n            return f\"Versioned: {name}\"\n\n        @mcp.prompt(version=\"5.0\")\n        def excluded_versioned_prompt(name: str) -> str:\n            return f\"Excluded: {name}\"\n\n        mcp.add_transform(VersionFilter(version_lt=\"2.0\", include_unversioned=False))\n\n        prompts = await mcp.list_prompts()\n        assert [prompt.name for prompt in prompts] == [\"included_versioned_prompt\"]\n\n    async def test_repr(self):\n        \"\"\"Test VersionFilter string representation.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        f1 = VersionFilter(version_lt=\"3.0\")\n        assert repr(f1) == \"VersionFilter(version_lt='3.0')\"\n\n        f2 = VersionFilter(version_gte=\"2.0\", version_lt=\"3.0\")\n        assert repr(f2) == \"VersionFilter(version_gte='2.0', version_lt='3.0')\"\n\n        f3 = VersionFilter(version_gte=\"1.0\")\n        assert repr(f3) == \"VersionFilter(version_gte='1.0')\"\n\n        f4 = VersionFilter(version_lt=\"3.0\", include_unversioned=False)\n        assert repr(f4) == \"VersionFilter(version_lt='3.0', include_unversioned=False)\"\n\n\nclass TestMountedVersionFiltering:\n    \"\"\"Tests for version filtering with mounted servers (FastMCPProvider).\n\n    Note: For mounted servers, list_* methods show what the child exposes (already\n    deduplicated to highest version). get_* methods support range filtering via\n    VersionSpec propagation to FastMCPProvider.\n    \"\"\"\n\n    async def test_mounted_get_tool_with_range_filter(self):\n        \"\"\"FastMCPProvider.get_tool applies range filtering from VersionSpec.\"\"\"\n        from fastmcp.server.providers.fastmcp_provider import FastMCPProvider\n        from fastmcp.utilities.versions import VersionSpec\n\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        provider = FastMCPProvider(child)\n\n        # Without range spec, should return the tool\n        tool = await provider.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"2.0\"\n\n        # With range spec that excludes v2.0, should return None\n        tool = await provider.get_tool(\"calc\", version=VersionSpec(lt=\"2.0\"))\n        assert tool is None\n\n        # With range spec that includes v2.0, should return the tool\n        tool = await provider.get_tool(\"calc\", version=VersionSpec(gte=\"2.0\"))\n        assert tool is not None\n        assert tool.version == \"2.0\"\n\n    async def test_mounted_get_resource_with_range_filter(self):\n        \"\"\"FastMCPProvider.get_resource applies range filtering from VersionSpec.\"\"\"\n        from fastmcp.server.providers.fastmcp_provider import FastMCPProvider\n        from fastmcp.utilities.versions import VersionSpec\n\n        child = FastMCP(\"Child\")\n\n        @child.resource(\"file://data/\", version=\"2.0\")\n        def data() -> str:\n            return \"data\"\n\n        provider = FastMCPProvider(child)\n\n        # Without range spec, should return the resource\n        resource = await provider.get_resource(\"file://data/\")\n        assert resource is not None\n        assert resource.version == \"2.0\"\n\n        # With range spec that excludes v2.0, should return None\n        resource = await provider.get_resource(\n            \"file://data/\", version=VersionSpec(lt=\"2.0\")\n        )\n        assert resource is None\n\n    async def test_mounted_get_prompt_with_range_filter(self):\n        \"\"\"FastMCPProvider.get_prompt applies range filtering from VersionSpec.\"\"\"\n        from fastmcp.server.providers.fastmcp_provider import FastMCPProvider\n        from fastmcp.utilities.versions import VersionSpec\n\n        child = FastMCP(\"Child\")\n\n        @child.prompt(version=\"2.0\")\n        def greet(name: str) -> str:\n            return f\"Hello {name}\"\n\n        provider = FastMCPProvider(child)\n\n        # Without range spec, should return the prompt\n        prompt = await provider.get_prompt(\"greet\")\n        assert prompt is not None\n        assert prompt.version == \"2.0\"\n\n        # With range spec that excludes v2.0, should return None\n        prompt = await provider.get_prompt(\"greet\", version=VersionSpec(lt=\"2.0\"))\n        assert prompt is None\n\n    async def test_mounted_unversioned_passes_version_filter(self):\n        \"\"\"Unversioned components in mounted servers pass through version filters.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        child = FastMCP(\"Child\")\n\n        @child.tool\n        def unversioned_tool() -> str:\n            return \"unversioned\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n        parent.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n        # Unversioned should pass through\n        tools = await parent.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"child_unversioned_tool\"\n        assert tools[0].version is None\n\n    async def test_mounted_include_unversioned_false_excludes_unversioned_tools(self):\n        \"\"\"Mounted unversioned tools can be excluded with include_unversioned=False.\"\"\"\n\n        child = FastMCP(\"Child\")\n\n        @child.tool\n        def unversioned_tool() -> str:\n            return \"unversioned\"\n\n        @child.tool(version=\"2.0\")\n        def included_versioned_tool() -> str:\n            return \"versioned\"\n\n        @child.tool(version=\"0.5\")\n        def excluded_versioned_tool() -> str:\n            return \"excluded-versioned\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n        parent.add_transform(\n            VersionFilter(version_gte=\"1.0\", include_unversioned=False)\n        )\n\n        tools = await parent.list_tools()\n        assert [tool.name for tool in tools] == [\"child_included_versioned_tool\"]\n\n    async def test_version_filter_filters_out_high_mounted_version(self):\n        \"\"\"VersionFilter hides mounted components outside the range.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"5.0\")\n        def high_version_tool() -> int:\n            return 5\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n        parent.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n        # v5.0 is outside the filter range, so it should be hidden\n        tools = await parent.list_tools()\n        assert len(tools) == 0\n\n        # get_tool should also return None (respects filter, applies transforms)\n        assert await parent.get_tool(\"child_high_version_tool\") is None\n\n\nclass TestMountedRangeFiltering:\n    \"\"\"Tests for version range filtering with mounted servers.\"\"\"\n\n    async def test_mounted_lower_version_selected_by_filter(self):\n        \"\"\"When parent has filter <2.0 and child has v1.0+v3.0, should get v1.0.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @child.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n        parent.add_transform(VersionFilter(version_lt=\"2.0\"))\n\n        # Should return v1.0 (the highest version that matches <2.0)\n        # Use get_tool to apply transforms\n        tool = await parent.get_tool(\"child_calc\")\n        assert tool is not None\n        assert tool.version == \"1.0\"\n\n    async def test_explicit_version_honored_within_filter_range(self):\n        \"\"\"Explicit version=\"1.0\" request should work within filter range.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @child.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        @child.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n        parent.add_transform(VersionFilter(version_gte=\"1.0\", version_lt=\"3.0\"))\n\n        # Request specific version within range (use get_tool to apply transforms)\n        tool = await parent.get_tool(\"child_calc\", VersionSpec(eq=\"1.0\"))\n        assert tool is not None\n        assert tool.version == \"1.0\"\n\n        # Request version outside range should return None\n        result = await parent.get_tool(\"child_calc\", VersionSpec(eq=\"3.0\"))\n        assert result is None\n\n\nclass TestUnversionedExemption:\n    \"\"\"Tests confirming unversioned components bypass version filters.\"\"\"\n\n    async def test_unversioned_bypasses_version_filter(self):\n        \"\"\"Unversioned components pass through any VersionFilter - by design.\"\"\"\n        from fastmcp.server.transforms import VersionFilter\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def unversioned_tool() -> str:\n            return \"unversioned\"\n\n        @mcp.tool(version=\"5.0\")\n        def versioned_tool() -> str:\n            return \"v5\"\n\n        # Filter that would exclude v5.0\n        mcp.add_transform(VersionFilter(version_lt=\"3.0\"))\n\n        tools = await mcp.list_tools()\n        names = [t.name for t in tools]\n\n        # Unversioned passes through (exempt from filtering)\n        assert \"unversioned_tool\" in names\n        # Versioned is filtered out\n        assert \"versioned_tool\" not in names\n\n    async def test_unversioned_returned_for_exact_version_request(self):\n        \"\"\"Requesting exact version of unversioned tool returns the tool.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"unversioned\"\n\n        # Even with explicit version request, unversioned tool is returned\n        # (it's the only version that exists, and unversioned matches any spec)\n        tool = await mcp.get_tool(\"my_tool\", VersionSpec(eq=\"1.0\"))\n        assert tool is not None\n        assert tool.version is None\n\n    async def test_unversioned_matches_any_version_spec(self):\n        \"\"\"VersionSpec.matches(None) returns True for any spec.\"\"\"\n        from fastmcp.utilities.versions import VersionSpec\n\n        # Unversioned matches exact version specs\n        assert VersionSpec(eq=\"1.0\").matches(None) is True\n\n        # Unversioned matches range specs\n        assert VersionSpec(gte=\"1.0\", lt=\"3.0\").matches(None) is True\n\n        # Unversioned matches open specs\n        assert VersionSpec(lt=\"5.0\").matches(None) is True\n        assert VersionSpec(gte=\"1.0\").matches(None) is True\n"
  },
  {
    "path": "tests/server/versioning/test_mounting.py",
    "content": "\"\"\"Tests for versioning in mounted servers.\"\"\"\n# ruff: noqa: F811  # Intentional function redefinition for version testing\n\nfrom __future__ import annotations\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.versions import (\n    VersionSpec,\n)\n\n\nclass TestVersionSorting:\n    \"\"\"Tests for version sorting behavior.\"\"\"\n\n    async def test_semantic_version_sorting(self):\n        \"\"\"Versions should sort semantically, not lexicographically.\"\"\"\n        mcp = FastMCP()\n\n        # Add versions out of order\n        @mcp.tool(version=\"1\")\n        def count() -> int:\n            return 1\n\n        @mcp.tool(version=\"10\")\n        def count() -> int:\n            return 10\n\n        @mcp.tool(version=\"2\")\n        def count() -> int:\n            return 2\n\n        # list_tools returns all versions\n        tools = await mcp.list_tools()\n        assert len(tools) == 3\n        versions = {t.version for t in tools}\n        assert versions == {\"1\", \"2\", \"10\"}\n\n        # get_tool returns highest (semantic: 10 > 2 > 1)\n        tool = await mcp.get_tool(\"count\")\n        assert tool is not None\n        assert tool.version == \"10\"\n\n        # call_tool uses highest version\n        result = await mcp.call_tool(\"count\", {})\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"10\"\n\n    async def test_semver_sorting(self):\n        \"\"\"Full semver versions should sort correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.2.3\")\n        def info() -> str:\n            return \"1.2.3\"\n\n        @mcp.tool(version=\"1.2.10\")\n        def info() -> str:\n            return \"1.2.10\"\n\n        @mcp.tool(version=\"1.10.1\")\n        def info() -> str:\n            return \"1.10.1\"\n\n        # list_tools returns all versions\n        tools = await mcp.list_tools()\n        assert len(tools) == 3\n        versions = {t.version for t in tools}\n        assert versions == {\"1.2.3\", \"1.2.10\", \"1.10.1\"}\n\n        # get_tool returns highest: 1.10.1 > 1.2.10 > 1.2.3 (semantic)\n        tool = await mcp.get_tool(\"info\")\n        assert tool is not None\n        assert tool.version == \"1.10.1\"\n\n    async def test_v_prefix_normalized(self):\n        \"\"\"Versions with 'v' prefix should compare correctly.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"v1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"v2.0\")\n        def calc() -> int:\n            return 2\n\n        # list_tools returns all versions\n        tools = await mcp.list_tools()\n        assert len(tools) == 2\n        versions = {t.version for t in tools}\n        assert versions == {\"v1.0\", \"v2.0\"}\n\n        # get_tool returns highest\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"v2.0\"\n\n\nclass TestMountedServerVersioning:\n    \"\"\"Tests for versioning in mounted servers (FastMCPProvider).\"\"\"\n\n    async def test_mounted_tool_preserves_version(self):\n        \"\"\"Mounted tools should preserve their version info.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"2.0\")\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        tools = await parent.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"child_add\"\n        assert tools[0].version == \"2.0\"\n\n    async def test_mounted_resource_preserves_version(self):\n        \"\"\"Mounted resources should preserve their version info.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.resource(\"file:///config\", version=\"1.5\")\n        def config() -> str:\n            return \"config data\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        resources = await parent.list_resources()\n        assert len(resources) == 1\n        assert resources[0].version == \"1.5\"\n\n    async def test_mounted_prompt_preserves_version(self):\n        \"\"\"Mounted prompts should preserve their version info.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.prompt(version=\"3.0\")\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        prompts = await parent.list_prompts()\n        assert len(prompts) == 1\n        assert prompts[0].name == \"child_greet\"\n        assert prompts[0].version == \"3.0\"\n\n    async def test_mounted_get_tool_with_version(self):\n        \"\"\"Should be able to get specific version from mounted server.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @child.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        # Get highest version (default)\n        tool = await parent.get_tool(\"child_calc\")\n        assert tool is not None\n        assert tool.version == \"2.0\"\n\n        # Get specific version\n        tool_v1 = await parent.get_tool(\"child_calc\", VersionSpec(eq=\"1.0\"))\n        assert tool_v1 is not None\n        assert tool_v1.version == \"1.0\"\n\n    async def test_mounted_multiple_versions_all_returned(self):\n        \"\"\"Mounted server with multiple versions should show all versions.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"1.0\")\n        def my_tool() -> str:\n            return \"v1\"\n\n        @child.tool(version=\"3.0\")\n        def my_tool() -> str:\n            return \"v3\"\n\n        @child.tool(version=\"2.0\")\n        def my_tool() -> str:\n            return \"v2\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        # list_tools returns all versions\n        tools = await parent.list_tools()\n        assert len(tools) == 3\n        versions = {t.version for t in tools}\n        assert versions == {\"1.0\", \"2.0\", \"3.0\"}\n\n        # get_tool returns highest\n        tool = await parent.get_tool(\"child_my_tool\")\n        assert tool is not None\n        assert tool.version == \"3.0\"\n\n    async def test_mounted_call_tool_uses_highest_version(self):\n        \"\"\"Calling mounted tool should use highest version.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"1.0\")\n        def double(x: int) -> int:\n            return x * 2\n\n        @child.tool(version=\"2.0\")\n        def double(x: int) -> int:\n            return x * 2 + 100  # Different behavior\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        result = await parent.call_tool(\"child_double\", {\"x\": 5})\n        # Should use v2.0 which adds 100\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"110\"\n\n    async def test_mounted_tool_wrapper_executes_correct_version(self):\n        \"\"\"Calling a specific versioned tool wrapper should execute that version.\"\"\"\n        child = FastMCP(\"Child\")\n\n        @child.tool(version=\"1.0\")\n        def calc(x: int) -> int:\n            return x * 10  # v1.0 multiplies by 10\n\n        @child.tool(version=\"2.0\")\n        def calc(x: int) -> int:\n            return x * 100  # v2.0 multiplies by 100\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        # Get the v1.0 wrapper specifically\n        tools = await parent.list_tools()\n        v1_tool = next(\n            t for t in tools if t.name == \"child_calc\" and t.version == \"1.0\"\n        )\n\n        # Calling the v1.0 wrapper should execute v1.0's logic\n        result = await v1_tool.run({\"x\": 5})\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"50\"  # 5 * 10, not 5 * 100\n\n    async def test_mounted_resource_wrapper_reads_correct_version(self):\n        \"\"\"Reading a specific versioned resource should read that version.\"\"\"\n        from fastmcp.utilities.versions import VersionSpec\n\n        child = FastMCP(\"Child\")\n\n        @child.resource(\"data:///config\", version=\"1.0\")\n        def config_v1() -> str:\n            return \"config-v1-content\"\n\n        @child.resource(\"data:///config\", version=\"2.0\")\n        def config_v2() -> str:\n            return \"config-v2-content\"\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        # Reading with version=1.0 should read v1.0's content\n        result = await parent.read_resource(\n            \"data://child//config\", version=VersionSpec(eq=\"1.0\")\n        )\n        assert result.contents[0].content == \"config-v1-content\"\n\n        # Reading with version=2.0 should read v2.0's content\n        result = await parent.read_resource(\n            \"data://child//config\", version=VersionSpec(eq=\"2.0\")\n        )\n        assert result.contents[0].content == \"config-v2-content\"\n\n    async def test_mounted_prompt_wrapper_renders_correct_version(self):\n        \"\"\"Rendering a specific versioned prompt should render that version.\"\"\"\n        from fastmcp.utilities.versions import VersionSpec\n\n        child = FastMCP(\"Child\")\n\n        @child.prompt(version=\"1.0\")\n        def greeting(name: str) -> str:\n            return f\"Hello, {name}!\"  # v1.0 says Hello\n\n        @child.prompt(version=\"2.0\")\n        def greeting(name: str) -> str:\n            return f\"Greetings, {name}!\"  # v2.0 says Greetings\n\n        parent = FastMCP(\"Parent\")\n        parent.mount(child, \"child\")\n\n        # Rendering with version=1.0 should render v1.0's content\n        result = await parent.render_prompt(\n            \"child_greeting\", {\"name\": \"World\"}, version=VersionSpec(eq=\"1.0\")\n        )\n        content = result.messages[0].content\n        assert isinstance(content, TextContent) and \"Hello, World!\" in content.text\n\n        # Rendering with version=2.0 should render v2.0's content\n        result = await parent.render_prompt(\n            \"child_greeting\", {\"name\": \"World\"}, version=VersionSpec(eq=\"2.0\")\n        )\n        content = result.messages[0].content\n        assert isinstance(content, TextContent) and \"Greetings, World!\" in content.text\n\n    async def test_deeply_nested_version_forwarding(self):\n        \"\"\"Verify version is correctly forwarded through multiple mount levels.\"\"\"\n        level3 = FastMCP(\"Level3\")\n\n        @level3.tool(version=\"1.0\")\n        def calc(x: int) -> int:\n            return x * 10  # v1.0 multiplies by 10\n\n        @level3.tool(version=\"2.0\")\n        def calc(x: int) -> int:\n            return x * 100  # v2.0 multiplies by 100\n\n        level2 = FastMCP(\"Level2\")\n        level2.mount(level3, \"l3\")\n\n        level1 = FastMCP(\"Level1\")\n        level1.mount(level2, \"l2\")\n\n        # All versions should be visible through two levels of mounting\n        tools = await level1.list_tools()\n        calc_tools = [t for t in tools if \"calc\" in t.name]\n        assert len(calc_tools) == 2\n        versions = {t.version for t in calc_tools}\n        assert versions == {\"1.0\", \"2.0\"}\n\n        # Get v1.0 wrapper through two levels of mounting\n        v1_tool = next(t for t in tools if \"calc\" in t.name and t.version == \"1.0\")\n\n        # Should execute v1.0 logic, not v2.0\n        result = await v1_tool.run({\"x\": 5})\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"50\"  # 5 * 10, not 5 * 100\n"
  },
  {
    "path": "tests/server/versioning/test_versioning.py",
    "content": "\"\"\"Core versioning functionality: VersionKey, utilities, and components.\"\"\"\n# ruff: noqa: F811  # Intentional function redefinition for version testing\n\nfrom __future__ import annotations\n\nfrom typing import cast\n\nimport pytest\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.tools import Tool\nfrom fastmcp.utilities.versions import (\n    VersionKey,\n    compare_versions,\n    is_version_greater,\n)\n\n\nclass TestVersionKey:\n    \"\"\"Tests for VersionKey comparison class.\"\"\"\n\n    def test_none_sorts_lowest(self):\n        \"\"\"None (unversioned) should sort lower than any version.\"\"\"\n        assert VersionKey(None) < VersionKey(\"1.0\")\n        assert VersionKey(None) < VersionKey(\"0.1\")\n        assert VersionKey(None) < VersionKey(\"anything\")\n\n    def test_none_equals_none(self):\n        \"\"\"Two None versions should be equal.\"\"\"\n        assert VersionKey(None) == VersionKey(None)\n        assert not (VersionKey(None) < VersionKey(None))\n        assert not (VersionKey(None) > VersionKey(None))\n\n    def test_pep440_versions_compared_semantically(self):\n        \"\"\"Valid PEP 440 versions should compare semantically.\"\"\"\n        assert VersionKey(\"1.0\") < VersionKey(\"2.0\")\n        assert VersionKey(\"1.0\") < VersionKey(\"1.1\")\n        assert VersionKey(\"1.9\") < VersionKey(\"1.10\")  # Semantic, not string\n        assert VersionKey(\"2\") < VersionKey(\"10\")  # Semantic, not string\n\n    def test_v_prefix_stripped(self):\n        \"\"\"Versions with 'v' prefix should be handled correctly.\"\"\"\n        assert VersionKey(\"v1.0\") == VersionKey(\"1.0\")\n        assert VersionKey(\"v2.0\") > VersionKey(\"v1.0\")\n\n    def test_string_fallback_for_invalid_versions(self):\n        \"\"\"Invalid PEP 440 versions should fall back to string comparison.\"\"\"\n        # Dates are not valid PEP 440\n        assert VersionKey(\"2024-01-01\") < VersionKey(\"2025-01-01\")\n        # String comparison (lexicographic)\n        assert VersionKey(\"alpha\") < VersionKey(\"beta\")\n\n    def test_pep440_sorts_before_strings(self):\n        \"\"\"PEP 440 versions sort before invalid string versions.\"\"\"\n        # \"1.0\" is valid PEP 440, \"not-semver\" is not\n        assert VersionKey(\"1.0\") < VersionKey(\"not-semver\")\n        assert VersionKey(\"999.0\") < VersionKey(\"aaa\")  # PEP 440 < string\n\n    def test_repr(self):\n        \"\"\"Test string representation.\"\"\"\n        assert repr(VersionKey(\"1.0\")) == \"VersionKey('1.0')\"\n        assert repr(VersionKey(None)) == \"VersionKey(None)\"\n\n\nclass TestVersionFunctions:\n    \"\"\"Tests for version comparison functions.\"\"\"\n\n    def test_compare_versions(self):\n        \"\"\"Test compare_versions function.\"\"\"\n        assert compare_versions(\"1.0\", \"2.0\") == -1\n        assert compare_versions(\"2.0\", \"1.0\") == 1\n        assert compare_versions(\"1.0\", \"1.0\") == 0\n        assert compare_versions(None, \"1.0\") == -1\n        assert compare_versions(\"1.0\", None) == 1\n        assert compare_versions(None, None) == 0\n\n    def test_is_version_greater(self):\n        \"\"\"Test is_version_greater function.\"\"\"\n        assert is_version_greater(\"2.0\", \"1.0\")\n        assert not is_version_greater(\"1.0\", \"2.0\")\n        assert not is_version_greater(\"1.0\", \"1.0\")\n        assert is_version_greater(\"1.0\", None)\n        assert not is_version_greater(None, \"1.0\")\n\n\nclass TestComponentVersioning:\n    \"\"\"Tests for versioning in FastMCP components.\"\"\"\n\n    async def test_tool_with_version(self):\n        \"\"\"Tool version should be reflected in key.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"2.0\")\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"my_tool\"\n        assert tools[0].version == \"2.0\"\n        assert tools[0].key == \"tool:my_tool@2.0\"\n\n    async def test_tool_without_version(self):\n        \"\"\"Tool without version should have @ sentinel in key but empty version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].version is None\n        # Keys always have @ sentinel for unambiguous parsing\n        assert tools[0].key == \"tool:my_tool@\"\n\n    async def test_tool_version_as_int(self):\n        \"\"\"Tool version as int should be coerced to string.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=2)\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].version == \"2\"\n        assert tools[0].key == \"tool:my_tool@2\"\n\n    async def test_tool_version_zero_is_truthy(self):\n        \"\"\"Version 0 should become \"0\" (truthy string), not empty.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=0)\n        def my_tool(x: int) -> int:\n            return x * 2\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].version == \"0\"\n        assert tools[0].key == \"tool:my_tool@0\"  # Not \"tool:my_tool@\"\n\n    async def test_multiple_tool_versions_all_returned(self):\n        \"\"\"list_tools returns all versions; get_tool returns highest.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        @mcp.tool(version=\"2.0\")\n        def add(x: int, y: int, z: int = 0) -> int:\n            return x + y + z\n\n        # list_tools returns all versions\n        tools = await mcp.list_tools()\n        assert len(tools) == 2\n        versions = {t.version for t in tools}\n        assert versions == {\"1.0\", \"2.0\"}\n\n        # get_tool returns highest version\n        tool = await mcp.get_tool(\"add\")\n        assert tool is not None\n        assert tool.version == \"2.0\"\n\n    async def test_call_tool_invokes_highest_version(self):\n        \"\"\"Calling a tool by name should invoke the highest version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def add(x: int, y: int) -> int:\n            return x + y\n\n        @mcp.tool(version=\"2.0\")\n        def add(x: int, y: int) -> int:\n            return (x + y) * 10  # Different behavior to distinguish\n\n        result = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n        # Should invoke v2.0 which multiplies by 10\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"30\"\n\n    async def test_mixing_versioned_and_unversioned_rejected(self):\n        \"\"\"Cannot mix versioned and unversioned tools with the same name.\"\"\"\n        import pytest\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"unversioned\"\n\n        # Adding versioned tool when unversioned exists should fail\n        with pytest.raises(ValueError, match=\"versioned.*unversioned\"):\n\n            @mcp.tool(version=\"1.0\")\n            def my_tool() -> str:\n                return \"v1.0\"\n\n    async def test_mixing_unversioned_after_versioned_rejected(self):\n        \"\"\"Cannot add unversioned tool when versioned exists.\"\"\"\n        import pytest\n\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def my_tool() -> str:\n            return \"v1.0\"\n\n        # Adding unversioned tool when versioned exists should fail\n        with pytest.raises(ValueError, match=\"unversioned.*versioned\"):\n\n            @mcp.tool\n            def my_tool() -> str:\n                return \"unversioned\"\n\n    async def test_resource_with_version(self):\n        \"\"\"Resource version should work like tool version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"file:///config\", version=\"1.0\")\n        def config_v1() -> str:\n            return \"config v1\"\n\n        @mcp.resource(\"file:///config\", version=\"2.0\")\n        def config_v2() -> str:\n            return \"config v2\"\n\n        # list_resources returns all versions\n        resources = await mcp.list_resources()\n        assert len(resources) == 2\n        versions = {r.version for r in resources}\n        assert versions == {\"1.0\", \"2.0\"}\n\n        # get_resource returns highest version\n        resource = await mcp.get_resource(\"file:///config\")\n        assert resource is not None\n        assert resource.version == \"2.0\"\n\n    async def test_prompt_with_version(self):\n        \"\"\"Prompt version should work like tool version.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet(name: str) -> str:\n            return f\"Greetings, {name}!\"\n\n        # list_prompts returns all versions\n        prompts = await mcp.list_prompts()\n        assert len(prompts) == 2\n        versions = {p.version for p in prompts}\n        assert versions == {\"1.0\", \"2.0\"}\n\n        # get_prompt returns highest version\n        prompt = await mcp.get_prompt(\"greet\")\n        assert prompt is not None\n        assert prompt.version == \"2.0\"\n\n\nclass TestVersionValidation:\n    \"\"\"Tests for version type validation in components and server.\"\"\"\n\n    async def test_fastmcp_version_int_coerced(self):\n        \"\"\"FastMCP(version=42) should coerce to string '42'.\"\"\"\n        mcp = FastMCP(version=42)\n        assert mcp._mcp_server.version == \"42\"\n\n    async def test_fastmcp_version_float_coerced(self):\n        \"\"\"FastMCP(version=1.5) should coerce to string.\"\"\"\n        mcp = FastMCP(version=1.5)\n        assert mcp._mcp_server.version == \"1.5\"\n\n    async def test_tool_version_list_rejected(self):\n        \"\"\"Tool with version=[1, 2] should raise TypeError.\"\"\"\n        with pytest.raises(TypeError, match=\"Version must be a string\"):\n            Tool(\n                name=\"t\",\n                version=cast(str, [1, 2]),\n                parameters={\"type\": \"object\"},\n            )\n\n    async def test_tool_version_dict_rejected(self):\n        \"\"\"Tool with version={'major': 1} should raise TypeError.\"\"\"\n        with pytest.raises(TypeError, match=\"Version must be a string\"):\n            Tool(\n                name=\"t\",\n                version=cast(str, {\"major\": 1}),\n                parameters={\"type\": \"object\"},\n            )\n\n    async def test_fastmcp_version_list_rejected(self):\n        \"\"\"FastMCP(version=[1, 2]) should raise TypeError.\"\"\"\n        with pytest.raises(TypeError, match=\"Version must be a string\"):\n            FastMCP(version=cast(str, [1, 2]))\n\n    async def test_fastmcp_version_dict_rejected(self):\n        \"\"\"FastMCP(version={'v': 1}) should raise TypeError.\"\"\"\n        with pytest.raises(TypeError, match=\"Version must be a string\"):\n            FastMCP(version=cast(str, {\"v\": 1}))\n\n    async def test_fastmcp_version_true_rejected(self):\n        \"\"\"FastMCP(version=True) should raise TypeError, not coerce to 'True'.\"\"\"\n        with pytest.raises(TypeError, match=\"got bool\"):\n            FastMCP(version=cast(str, True))\n\n    async def test_fastmcp_version_false_rejected(self):\n        \"\"\"FastMCP(version=False) should raise TypeError, not coerce to 'False'.\"\"\"\n        with pytest.raises(TypeError, match=\"got bool\"):\n            FastMCP(version=cast(str, False))\n"
  },
  {
    "path": "tests/server/versioning/test_visibility_version_fallback.py",
    "content": "\"\"\"Tests for version fallback when the highest version is disabled via visibility.\n\nRegression tests for https://github.com/jlowin/fastmcp/issues/3421:\nWhen the latest version of a component is disabled, get_* methods should\nfall back to the next-highest enabled version instead of returning None.\n\"\"\"\n# ruff: noqa: F811  # Intentional function redefinition for version testing\n\nfrom __future__ import annotations\n\nfrom mcp.server.auth.middleware.auth_context import auth_context_var\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.auth import AccessToken, require_scopes\nfrom fastmcp.utilities.versions import VersionSpec\n\n\ndef _make_token(scopes: list[str] | None = None) -> AccessToken:\n    \"\"\"Create a test access token.\"\"\"\n    return AccessToken(\n        token=\"test-token\",\n        client_id=\"test-client\",\n        scopes=scopes or [],\n        expires_at=None,\n        claims={},\n    )\n\n\ndef _set_token(token: AccessToken | None):\n    \"\"\"Set the access token in the auth context var.\"\"\"\n    if token is None:\n        return auth_context_var.set(None)\n    return auth_context_var.set(AuthenticatedUser(token))\n\n\nclass TestToolVersionFallback:\n    \"\"\"Test that disabling the latest tool version falls back correctly.\"\"\"\n\n    async def test_list_tools_shows_v1_when_v2_disabled(self):\n        \"\"\"list_tools should show v1 when v2 is disabled.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        tools = await mcp.list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"calc\"\n        assert tools[0].version == \"1.0\"\n\n    async def test_get_tool_returns_v1_when_v2_disabled(self):\n        \"\"\"get_tool should return v1 when v2 is disabled (core bug).\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"1.0\"\n\n    async def test_call_tool_uses_v1_when_v2_disabled(self):\n        \"\"\"call_tool should invoke v1 when v2 is disabled.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        result = await mcp.call_tool(\"calc\", {})\n        first = result.content[0]\n        assert isinstance(first, TextContent)\n        assert first.text == \"1\"\n\n    async def test_get_tool_explicit_disabled_version_returns_none(self):\n        \"\"\"Requesting a specific disabled version should return None.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        tool = await mcp.get_tool(\"calc\", VersionSpec(eq=\"2.0\"))\n        assert tool is None\n\n    async def test_get_tool_all_versions_disabled_returns_none(self):\n        \"\"\"When all versions are disabled, get_tool returns None.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(names={\"calc\"})\n\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is None\n\n    async def test_get_tool_middle_version_fallback(self):\n        \"\"\"Disabling v3 should fall back to v2, not v1.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        @mcp.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        mcp.disable(version=VersionSpec(eq=\"3.0\"))\n\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"2.0\"\n\n\nclass TestResourceVersionFallback:\n    \"\"\"Test that disabling the latest resource version falls back correctly.\"\"\"\n\n    async def test_get_resource_returns_v1_when_v2_disabled(self):\n        \"\"\"get_resource should return v1 when v2 is disabled.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://info\", version=\"1.0\")\n        def info() -> str:\n            return \"v1\"\n\n        @mcp.resource(\"data://info\", version=\"2.0\")\n        def info() -> str:\n            return \"v2\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        resource = await mcp.get_resource(\"data://info\")\n        assert resource is not None\n        assert resource.version == \"1.0\"\n\n    async def test_get_resource_explicit_disabled_version_returns_none(self):\n        \"\"\"Requesting a specific disabled resource version should return None.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://info\", version=\"1.0\")\n        def info() -> str:\n            return \"v1\"\n\n        @mcp.resource(\"data://info\", version=\"2.0\")\n        def info() -> str:\n            return \"v2\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        resource = await mcp.get_resource(\"data://info\", VersionSpec(eq=\"2.0\"))\n        assert resource is None\n\n\nclass TestResourceTemplateVersionFallback:\n    \"\"\"Test that disabling the latest template version falls back correctly.\"\"\"\n\n    async def test_get_resource_template_returns_v1_when_v2_disabled(self):\n        \"\"\"get_resource_template should return v1 when v2 is disabled.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://items/{id}\", version=\"1.0\")\n        def item(id: str) -> str:\n            return f\"v1-{id}\"\n\n        @mcp.resource(\"data://items/{id}\", version=\"2.0\")\n        def item(id: str) -> str:\n            return f\"v2-{id}\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        template = await mcp.get_resource_template(\"data://items/{id}\")\n        assert template is not None\n        assert template.version == \"1.0\"\n\n\nclass TestPromptVersionFallback:\n    \"\"\"Test that disabling the latest prompt version falls back correctly.\"\"\"\n\n    async def test_get_prompt_returns_v1_when_v2_disabled(self):\n        \"\"\"get_prompt should return v1 when v2 is disabled.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet() -> str:\n            return \"hello v1\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet() -> str:\n            return \"hello v2\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        prompt = await mcp.get_prompt(\"greet\")\n        assert prompt is not None\n        assert prompt.version == \"1.0\"\n\n    async def test_get_prompt_explicit_disabled_version_returns_none(self):\n        \"\"\"Requesting a specific disabled prompt version should return None.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\")\n        def greet() -> str:\n            return \"hello v1\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet() -> str:\n            return \"hello v2\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        prompt = await mcp.get_prompt(\"greet\", VersionSpec(eq=\"2.0\"))\n        assert prompt is None\n\n\nclass TestFallbackRespectsAuth:\n    \"\"\"Fallback to older versions must enforce auth checks.\n\n    When the highest version is disabled and the code falls back to older\n    versions, those candidates must go through auth filtering. Otherwise\n    a protected v1 could be exposed to unauthenticated users when a\n    public v2 is disabled.\n    \"\"\"\n\n    async def test_tool_fallback_respects_auth(self):\n        \"\"\"Disabling v2 should not expose auth-protected v1 to unauthorized users.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\", auth=require_scopes(\"admin\"))\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        # Without an admin token, v1 should NOT be returned\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is None\n\n    async def test_tool_fallback_allows_authorized_user(self):\n        \"\"\"Fallback should return auth-protected v1 to authorized users.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\", auth=require_scopes(\"admin\"))\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\")\n        def calc() -> int:\n            return 2\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        token = _make_token(scopes=[\"admin\"])\n        tok = _set_token(token)\n        try:\n            tool = await mcp.get_tool(\"calc\")\n            assert tool is not None\n            assert tool.version == \"1.0\"\n        finally:\n            auth_context_var.reset(tok)\n\n    async def test_resource_fallback_respects_auth(self):\n        \"\"\"Disabling v2 should not expose auth-protected v1 resource.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://info\", version=\"1.0\", auth=require_scopes(\"admin\"))\n        def info() -> str:\n            return \"v1\"\n\n        @mcp.resource(\"data://info\", version=\"2.0\")\n        def info() -> str:\n            return \"v2\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        resource = await mcp.get_resource(\"data://info\")\n        assert resource is None\n\n    async def test_resource_template_fallback_respects_auth(self):\n        \"\"\"Disabling v2 should not expose auth-protected v1 template.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.resource(\"data://items/{id}\", version=\"1.0\", auth=require_scopes(\"admin\"))\n        def item(id: str) -> str:\n            return f\"v1-{id}\"\n\n        @mcp.resource(\"data://items/{id}\", version=\"2.0\")\n        def item(id: str) -> str:\n            return f\"v2-{id}\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        template = await mcp.get_resource_template(\"data://items/{id}\")\n        assert template is None\n\n    async def test_prompt_fallback_respects_auth(self):\n        \"\"\"Disabling v2 should not expose auth-protected v1 prompt.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.prompt(version=\"1.0\", auth=require_scopes(\"admin\"))\n        def greet() -> str:\n            return \"hello v1\"\n\n        @mcp.prompt(version=\"2.0\")\n        def greet() -> str:\n            return \"hello v2\"\n\n        mcp.disable(version=VersionSpec(eq=\"2.0\"))\n\n        prompt = await mcp.get_prompt(\"greet\")\n        assert prompt is None\n\n    async def test_fallback_skips_unauthorized_picks_next(self):\n        \"\"\"When multiple fallback candidates exist, skip unauthorized ones.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(version=\"1.0\")\n        def calc() -> int:\n            return 1\n\n        @mcp.tool(version=\"2.0\", auth=require_scopes(\"admin\"))\n        def calc() -> int:\n            return 2\n\n        @mcp.tool(version=\"3.0\")\n        def calc() -> int:\n            return 3\n\n        mcp.disable(version=VersionSpec(eq=\"3.0\"))\n\n        # v2 requires admin, so unauthorized user should get v1\n        tool = await mcp.get_tool(\"calc\")\n        assert tool is not None\n        assert tool.version == \"1.0\"\n"
  },
  {
    "path": "tests/telemetry/__init__.py",
    "content": "\"\"\"Tests for FastMCP telemetry module.\"\"\"\n"
  },
  {
    "path": "tests/telemetry/test_module.py",
    "content": "\"\"\"Tests for the core telemetry module.\"\"\"\n\nfrom __future__ import annotations\n\nfrom opentelemetry import trace\nfrom opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter\n\nfrom fastmcp.server.telemetry import get_auth_span_attributes\nfrom fastmcp.telemetry import (\n    INSTRUMENTATION_NAME,\n    TRACE_PARENT_KEY,\n    extract_trace_context,\n    get_tracer,\n    inject_trace_context,\n)\n\n\nclass TestGetTracer:\n    def test_tracer_uses_instrumentation_name(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        tracer = get_tracer()\n        with tracer.start_as_current_span(\"test-span\"):\n            pass\n\n        spans = trace_exporter.get_finished_spans()\n        assert len(spans) == 1\n        scope = spans[0].instrumentation_scope\n        assert scope is not None\n        assert scope.name == INSTRUMENTATION_NAME\n\n\nclass TestGetAuthSpanAttributes:\n    def test_returns_empty_dict_when_no_context(self):\n        attrs = get_auth_span_attributes()\n        assert attrs == {}\n\n\nVALID_TRACEPARENT = \"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01\"\nVALID_TRACESTATE = \"congo=t61rcWkgMzE\"\n\n\nclass TestInjectTraceContext:\n    def test_injects_bare_keys(self, trace_exporter: InMemorySpanExporter):\n        tracer = get_tracer()\n        with tracer.start_as_current_span(\"test\"):\n            meta = inject_trace_context()\n\n        assert meta is not None\n        assert TRACE_PARENT_KEY in meta\n        assert meta[TRACE_PARENT_KEY].startswith(\"00-\")\n\n\nclass TestExtractTraceContext:\n    def test_bare_traceparent(self, trace_exporter: InMemorySpanExporter):\n        ctx = extract_trace_context({\"traceparent\": VALID_TRACEPARENT})\n        span_ctx = trace.get_current_span(ctx).get_span_context()\n        assert span_ctx.is_valid\n        assert format(span_ctx.trace_id, \"032x\") == \"0af7651916cd43dd8448eb211c80319c\"\n\n    def test_bare_tracestate(self, trace_exporter: InMemorySpanExporter):\n        ctx = extract_trace_context(\n            {\n                \"traceparent\": VALID_TRACEPARENT,\n                \"tracestate\": VALID_TRACESTATE,\n            }\n        )\n        span_ctx = trace.get_current_span(ctx).get_span_context()\n        assert span_ctx.is_valid\n        assert span_ctx.trace_state.get(\"congo\") == \"t61rcWkgMzE\"\n\n    def test_none_meta_returns_current_context(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        ctx = extract_trace_context(None)\n        assert ctx is not None\n\n    def test_empty_meta_returns_current_context(\n        self, trace_exporter: InMemorySpanExporter\n    ):\n        ctx = extract_trace_context({})\n        span_ctx = trace.get_current_span(ctx).get_span_context()\n        assert not span_ctx.is_valid\n"
  },
  {
    "path": "tests/test_apps.py",
    "content": "\"\"\"Tests for MCP Apps Phase 1 — SDK compatibility.\n\nCovers app config models, tool/resource registration with ``app=``,\nextension negotiation, and the ``Context.client_supports_extension`` method.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.apps import (\n    UI_EXTENSION_ID,\n    UI_MIME_TYPE,\n    AppConfig,\n    ResourceCSP,\n    ResourcePermissions,\n    app_config_to_meta_dict,\n)\nfrom fastmcp.server.context import Context\n\n# ---------------------------------------------------------------------------\n# Model serialization\n# ---------------------------------------------------------------------------\n\n\nclass TestAppConfig:\n    def test_serializes_with_aliases(self):\n        cfg = AppConfig(resource_uri=\"ui://my-app/view.html\", visibility=[\"app\"])\n        d = cfg.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\"resourceUri\": \"ui://my-app/view.html\", \"visibility\": [\"app\"]}\n\n    def test_excludes_none_fields(self):\n        cfg = AppConfig(resource_uri=\"ui://foo\")\n        d = cfg.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\"resourceUri\": \"ui://foo\"}\n\n    def test_all_fields(self):\n        cfg = AppConfig(\n            resource_uri=\"ui://app\",\n            visibility=[\"app\", \"model\"],\n            csp=ResourceCSP(resource_domains=[\"https://cdn.example.com\"]),\n            permissions=ResourcePermissions(camera={}, clipboard_write={}),\n            domain=\"example.com\",\n            prefers_border=True,\n        )\n        d = cfg.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\n            \"resourceUri\": \"ui://app\",\n            \"visibility\": [\"app\", \"model\"],\n            \"csp\": {\"resourceDomains\": [\"https://cdn.example.com\"]},\n            \"permissions\": {\"camera\": {}, \"clipboardWrite\": {}},\n            \"domain\": \"example.com\",\n            \"prefersBorder\": True,\n        }\n\n    def test_populate_by_name(self):\n        cfg = AppConfig(resource_uri=\"ui://app\")\n        assert cfg.resource_uri == \"ui://app\"\n\n\nclass TestResourceCSP:\n    def test_serializes_with_aliases(self):\n        csp = ResourceCSP(\n            connect_domains=[\"https://api.example.com\"],\n            resource_domains=[\"https://cdn.example.com\"],\n        )\n        d = csp.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\n            \"connectDomains\": [\"https://api.example.com\"],\n            \"resourceDomains\": [\"https://cdn.example.com\"],\n        }\n\n    def test_excludes_none_fields(self):\n        csp = ResourceCSP(resource_domains=[\"https://unpkg.com\"])\n        d = csp.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\"resourceDomains\": [\"https://unpkg.com\"]}\n\n    def test_all_fields(self):\n        csp = ResourceCSP(\n            connect_domains=[\"https://api.example.com\"],\n            resource_domains=[\"https://cdn.example.com\"],\n            frame_domains=[\"https://embed.example.com\"],\n            base_uri_domains=[\"https://base.example.com\"],\n        )\n        d = csp.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\n            \"connectDomains\": [\"https://api.example.com\"],\n            \"resourceDomains\": [\"https://cdn.example.com\"],\n            \"frameDomains\": [\"https://embed.example.com\"],\n            \"baseUriDomains\": [\"https://base.example.com\"],\n        }\n\n    def test_populate_by_name(self):\n        csp = ResourceCSP(connect_domains=[\"https://api.example.com\"])\n        assert csp.connect_domains == [\"https://api.example.com\"]\n\n    def test_empty(self):\n        csp = ResourceCSP()\n        d = csp.model_dump(by_alias=True, exclude_none=True)\n        assert d == {}\n\n    def test_extra_fields_preserved(self):\n        \"\"\"Unknown CSP directives from future spec versions pass through.\"\"\"\n        csp = ResourceCSP(\n            resource_domains=[\"https://cdn.example.com\"],\n            **{\"workerDomains\": [\"https://worker.example.com\"]},\n        )\n        d = csp.model_dump(by_alias=True, exclude_none=True)\n        assert d[\"resourceDomains\"] == [\"https://cdn.example.com\"]\n        assert d[\"workerDomains\"] == [\"https://worker.example.com\"]\n\n\nclass TestResourcePermissions:\n    def test_serializes_with_aliases(self):\n        perms = ResourcePermissions(microphone={}, clipboard_write={})\n        d = perms.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\"microphone\": {}, \"clipboardWrite\": {}}\n\n    def test_excludes_none_fields(self):\n        perms = ResourcePermissions(camera={})\n        d = perms.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\"camera\": {}}\n\n    def test_all_fields(self):\n        perms = ResourcePermissions(\n            camera={}, microphone={}, geolocation={}, clipboard_write={}\n        )\n        d = perms.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\n            \"camera\": {},\n            \"microphone\": {},\n            \"geolocation\": {},\n            \"clipboardWrite\": {},\n        }\n\n    def test_populate_by_name(self):\n        perms = ResourcePermissions(clipboard_write={})\n        assert perms.clipboard_write == {}\n\n    def test_extra_fields_preserved(self):\n        \"\"\"Unknown permissions from future spec versions pass through.\"\"\"\n        perms = ResourcePermissions(camera={}, **{\"midi\": {}})\n        d = perms.model_dump(by_alias=True, exclude_none=True)\n        assert d[\"camera\"] == {}\n        assert d[\"midi\"] == {}\n\n    def test_empty(self):\n        perms = ResourcePermissions()\n        d = perms.model_dump(by_alias=True, exclude_none=True)\n        assert d == {}\n\n\nclass TestAppConfigForResources:\n    \"\"\"AppConfig without resource_uri/visibility — for use on resources.\"\"\"\n\n    def test_serializes_with_aliases(self):\n        cfg = AppConfig(\n            prefers_border=True,\n            csp=ResourceCSP(resource_domains=[\"https://cdn.example.com\"]),\n        )\n        d = cfg.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\n            \"prefersBorder\": True,\n            \"csp\": {\"resourceDomains\": [\"https://cdn.example.com\"]},\n        }\n\n    def test_excludes_none_fields(self):\n        cfg = AppConfig()\n        d = cfg.model_dump(by_alias=True, exclude_none=True)\n        assert d == {}\n\n    def test_with_permissions(self):\n        cfg = AppConfig(\n            permissions=ResourcePermissions(microphone={}, clipboard_write={}),\n        )\n        d = cfg.model_dump(by_alias=True, exclude_none=True)\n        assert d == {\n            \"permissions\": {\"microphone\": {}, \"clipboardWrite\": {}},\n        }\n\n\nclass TestAppConfigToMetaDict:\n    def test_from_app_config_with_tool_fields(self):\n        cfg = AppConfig(resource_uri=\"ui://app\", visibility=[\"app\"])\n        result = app_config_to_meta_dict(cfg)\n        assert result[\"resourceUri\"] == \"ui://app\"\n        assert result[\"visibility\"] == [\"app\"]\n\n    def test_from_app_config_resource_fields_only(self):\n        cfg = AppConfig(prefers_border=False)\n        result = app_config_to_meta_dict(cfg)\n        assert result == {\"prefersBorder\": False}\n\n    def test_passthrough_for_dict(self):\n        raw: dict[str, Any] = {\"resourceUri\": \"ui://app\", \"custom\": \"value\"}\n        result = app_config_to_meta_dict(raw)\n        assert result is raw\n\n\n# ---------------------------------------------------------------------------\n# Tool registration with app=\n# ---------------------------------------------------------------------------\n\n\nclass TestToolRegistrationWithApp:\n    async def test_app_config_model(self):\n        server = FastMCP(\"test\")\n\n        @server.tool(app=AppConfig(resource_uri=\"ui://my-app/view.html\"))\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = list(await server.list_tools())\n        assert len(tools) == 1\n        assert tools[0].meta is not None\n        assert tools[0].meta[\"ui\"][\"resourceUri\"] == \"ui://my-app/view.html\"\n\n    async def test_app_dict(self):\n        server = FastMCP(\"test\")\n\n        @server.tool(app={\"resourceUri\": \"ui://foo\", \"visibility\": [\"app\"]})\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = list(await server.list_tools())\n        assert tools[0].meta is not None\n        assert tools[0].meta[\"ui\"][\"resourceUri\"] == \"ui://foo\"\n        assert tools[0].meta[\"ui\"][\"visibility\"] == [\"app\"]\n\n    async def test_app_merges_with_existing_meta(self):\n        server = FastMCP(\"test\")\n\n        @server.tool(meta={\"custom\": \"data\"}, app=AppConfig(resource_uri=\"ui://app\"))\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = list(await server.list_tools())\n        meta = tools[0].meta\n        assert meta is not None\n        assert meta[\"custom\"] == \"data\"\n        assert meta[\"ui\"][\"resourceUri\"] == \"ui://app\"\n\n    async def test_app_in_mcp_wire_format(self):\n        server = FastMCP(\"test\")\n\n        @server.tool(app=AppConfig(resource_uri=\"ui://app\", visibility=[\"app\"]))\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = list(await server.list_tools())\n        mcp_tool = tools[0].to_mcp_tool()\n        assert mcp_tool.meta is not None\n        assert mcp_tool.meta[\"ui\"][\"resourceUri\"] == \"ui://app\"\n        assert mcp_tool.meta[\"ui\"][\"visibility\"] == [\"app\"]\n\n    async def test_tool_without_app_has_no_ui_meta(self):\n        server = FastMCP(\"test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = list(await server.list_tools())\n        meta = tools[0].meta\n        assert meta is None or \"ui\" not in meta\n\n\n# ---------------------------------------------------------------------------\n# Resource registration with ui:// and app=\n# ---------------------------------------------------------------------------\n\n\nclass TestResourceWithApp:\n    async def test_ui_scheme_defaults_mime_type(self):\n        server = FastMCP(\"test\")\n\n        @server.resource(\"ui://my-app/view.html\")\n        def app_html() -> str:\n            return \"<html>hello</html>\"\n\n        resources = list(await server.list_resources())\n        assert len(resources) == 1\n        assert resources[0].mime_type == UI_MIME_TYPE\n\n    async def test_explicit_mime_type_overrides_ui_default(self):\n        server = FastMCP(\"test\")\n\n        @server.resource(\"ui://my-app/view.html\", mime_type=\"text/html\")\n        def app_html() -> str:\n            return \"<html>hello</html>\"\n\n        resources = list(await server.list_resources())\n        assert resources[0].mime_type == \"text/html\"\n\n    async def test_resource_app_metadata(self):\n        server = FastMCP(\"test\")\n\n        @server.resource(\n            \"ui://my-app/view.html\",\n            app=AppConfig(prefers_border=True),\n        )\n        def app_html() -> str:\n            return \"<html>hello</html>\"\n\n        resources = list(await server.list_resources())\n        assert resources[0].meta is not None\n        assert resources[0].meta[\"ui\"][\"prefersBorder\"] is True\n\n    async def test_non_ui_scheme_no_mime_default(self):\n        server = FastMCP(\"test\")\n\n        @server.resource(\"resource://data\")\n        def data() -> str:\n            return \"data\"\n\n        resources = list(await server.list_resources())\n        assert resources[0].mime_type != UI_MIME_TYPE\n\n    async def test_standalone_decorator_ui_scheme_defaults_mime_type(self):\n        \"\"\"The standalone @resource decorator also applies ui:// MIME default.\"\"\"\n        from fastmcp.resources import resource\n\n        @resource(\"ui://standalone-app/view.html\")\n        def standalone_app() -> str:\n            return \"<html>standalone</html>\"\n\n        server = FastMCP(\"test\")\n        server.add_resource(standalone_app)\n\n        resources = list(await server.list_resources())\n        assert len(resources) == 1\n        assert resources[0].mime_type == UI_MIME_TYPE\n\n    async def test_resource_template_ui_scheme_defaults_mime_type(self):\n        \"\"\"Resource templates also apply ui:// MIME default.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"ui://template-app/{view}\")\n        def template_app(view: str) -> str:\n            return f\"<html>{view}</html>\"\n\n        templates = list(await server.list_resource_templates())\n        assert len(templates) == 1\n        assert templates[0].mime_type == UI_MIME_TYPE\n\n    async def test_resource_rejects_resource_uri(self):\n        \"\"\"AppConfig with resource_uri raises ValueError on resources.\"\"\"\n        server = FastMCP(\"test\")\n        with pytest.raises(ValueError, match=\"resource_uri cannot be set on resources\"):\n\n            @server.resource(\n                \"ui://my-app/view.html\",\n                app=AppConfig(resource_uri=\"ui://other\"),\n            )\n            def app_html() -> str:\n                return \"<html>hello</html>\"\n\n    async def test_resource_rejects_visibility(self):\n        \"\"\"AppConfig with visibility raises ValueError on resources.\"\"\"\n        server = FastMCP(\"test\")\n        with pytest.raises(ValueError, match=\"visibility cannot be set on resources\"):\n\n            @server.resource(\n                \"ui://my-app/view.html\",\n                app=AppConfig(visibility=[\"app\"]),\n            )\n            def app_html() -> str:\n                return \"<html>hello</html>\"\n\n\n# ---------------------------------------------------------------------------\n# Extension advertisement\n# ---------------------------------------------------------------------------\n\n\nclass TestExtensionAdvertisement:\n    async def test_capabilities_include_ui_extension(self):\n        server = FastMCP(\"test\")\n\n        @server.tool\n        def my_tool() -> str:\n            return \"hello\"\n\n        async with Client(server) as client:\n            init_result = client.initialize_result\n            extras = init_result.capabilities.model_extra or {}\n            extensions = extras.get(\"extensions\", {})\n            assert UI_EXTENSION_ID in extensions\n\n\n# ---------------------------------------------------------------------------\n# Context.client_supports_extension\n# ---------------------------------------------------------------------------\n\n\nclass TestContextClientSupportsExtension:\n    async def test_returns_false_when_no_session(self):\n        server = FastMCP(\"test\")\n        async with Context(fastmcp=server) as ctx:\n            assert ctx.client_supports_extension(UI_EXTENSION_ID) is False\n\n\n# ---------------------------------------------------------------------------\n# Integration — full client↔server round-trip\n# ---------------------------------------------------------------------------\n\n\nclass TestIntegration:\n    async def test_tool_with_app_roundtrip(self):\n        \"\"\"App metadata flows through to clients — no server-side stripping.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(\n            app=AppConfig(resource_uri=\"ui://app/view.html\", visibility=[\"app\"])\n        )\n        async def my_tool() -> dict[str, str]:\n            return {\"result\": \"ok\"}\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            # _meta.ui is preserved — the host decides what to do with it\n            meta = tools[0].meta\n            assert meta is not None\n            assert meta[\"ui\"][\"resourceUri\"] == \"ui://app/view.html\"\n            assert meta[\"ui\"][\"visibility\"] == [\"app\"]\n\n    async def test_resource_with_ui_scheme_roundtrip(self):\n        server = FastMCP(\"test\")\n\n        @server.resource(\"ui://my-app/view.html\")\n        def app_html() -> str:\n            return \"<html><body>Hello</body></html>\"\n\n        async with Client(server) as client:\n            resources = await client.list_resources()\n            assert len(resources) == 1\n            assert str(resources[0].uri) == \"ui://my-app/view.html\"\n            assert resources[0].mimeType == UI_MIME_TYPE\n\n    async def test_ui_resource_read_preserves_mime_type(self):\n        \"\"\"Reading a ui:// resource returns content with the correct MIME type.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\"ui://my-app/view.html\")\n        def app_html() -> str:\n            return \"<html><body>Hello</body></html>\"\n\n        async with Client(server) as client:\n            result = await client.read_resource_mcp(\"ui://my-app/view.html\")\n            assert len(result.contents) == 1\n            assert result.contents[0].mimeType == UI_MIME_TYPE\n\n    async def test_app_tool_callable(self):\n        \"\"\"A tool registered with app= is still callable normally.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(app=AppConfig(resource_uri=\"ui://app\"))\n        async def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        async with Client(server) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"Alice\"})\n            assert any(\"Hello, Alice!\" in str(c) for c in result.content)\n\n    async def test_extension_and_tool_together(self):\n        \"\"\"Server advertises extension AND tool has app meta.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.tool(app=AppConfig(resource_uri=\"ui://dashboard\", visibility=[\"app\"]))\n        def dashboard() -> str:\n            return \"data\"\n\n        tools = list(await server.list_tools())\n        assert tools[0].meta is not None\n        assert tools[0].meta[\"ui\"][\"resourceUri\"] == \"ui://dashboard\"\n\n        async with Client(server) as client:\n            extras = client.initialize_result.capabilities.model_extra or {}\n            assert UI_EXTENSION_ID in extras.get(\"extensions\", {})\n\n    async def test_csp_and_permissions_roundtrip(self):\n        \"\"\"CSP and permissions metadata flows through to clients correctly.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\n            \"ui://secure-app/view.html\",\n            app=AppConfig(\n                csp=ResourceCSP(\n                    resource_domains=[\"https://unpkg.com\"],\n                    connect_domains=[\"https://api.example.com\"],\n                ),\n                permissions=ResourcePermissions(microphone={}, clipboard_write={}),\n            ),\n        )\n        def secure_app() -> str:\n            return \"<html>secure</html>\"\n\n        @server.tool(\n            app=AppConfig(\n                resource_uri=\"ui://secure-app/view.html\",\n                csp=ResourceCSP(resource_domains=[\"https://cdn.example.com\"]),\n                permissions=ResourcePermissions(camera={}),\n            )\n        )\n        def secure_tool() -> str:\n            return \"result\"\n\n        async with Client(server) as client:\n            # Verify resource metadata\n            resources = await client.list_resources()\n            assert len(resources) == 1\n            meta = resources[0].meta\n            assert meta is not None\n            assert meta[\"ui\"][\"csp\"][\"resourceDomains\"] == [\"https://unpkg.com\"]\n            assert meta[\"ui\"][\"csp\"][\"connectDomains\"] == [\"https://api.example.com\"]\n            assert meta[\"ui\"][\"permissions\"][\"microphone\"] == {}\n            assert meta[\"ui\"][\"permissions\"][\"clipboardWrite\"] == {}\n\n            # Verify tool metadata\n            tools = await client.list_tools()\n            assert len(tools) == 1\n            tool_meta = tools[0].meta\n            assert tool_meta is not None\n            assert tool_meta[\"ui\"][\"csp\"][\"resourceDomains\"] == [\n                \"https://cdn.example.com\"\n            ]\n            assert tool_meta[\"ui\"][\"permissions\"][\"camera\"] == {}\n\n    async def test_resource_read_propagates_meta_to_content_items(self):\n        \"\"\"resources/read must include _meta on content items so hosts can read CSP.\"\"\"\n        server = FastMCP(\"test\")\n\n        @server.resource(\n            \"ui://csp-app/view.html\",\n            app=AppConfig(\n                csp=ResourceCSP(resource_domains=[\"https://unpkg.com\"]),\n            ),\n        )\n        def app_view() -> str:\n            return \"<html>app</html>\"\n\n        async with Client(server) as client:\n            read_result = await client.read_resource_mcp(\"ui://csp-app/view.html\")\n            content_item = read_result.contents[0]\n            assert content_item.meta is not None\n            assert content_item.meta[\"ui\"][\"csp\"][\"resourceDomains\"] == [\n                \"https://unpkg.com\"\n            ]\n"
  },
  {
    "path": "tests/test_apps_prefab.py",
    "content": "\"\"\"Tests for MCP Apps Phase 2 — Prefab integration.\n\nCovers ``convert_result`` for PrefabApp/Component, ``app=True`` auto-wiring,\nreturn-type inference, output-schema suppression, and end-to-end round trips.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Annotated\n\nfrom mcp.types import TextContent\nfrom prefab_ui.app import PrefabApp\nfrom prefab_ui.components import Column, Heading, Text\nfrom prefab_ui.components.base import Component\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.resources.types import TextResource\nfrom fastmcp.server.apps import UI_MIME_TYPE, AppConfig\nfrom fastmcp.server.providers.local_provider.decorators.tools import (\n    PREFAB_RENDERER_URI,\n)\nfrom fastmcp.tools.base import Tool, ToolResult\n\n# ---------------------------------------------------------------------------\n# convert_result\n# ---------------------------------------------------------------------------\n\n\nclass TestConvertResult:\n    def test_prefab_app(self):\n        with Column() as view:\n            Heading(\"Hello\")\n        app = PrefabApp(view=view, state={\"name\": \"Alice\"})\n\n        tool = Tool(name=\"t\", parameters={})\n        result = tool.convert_result(app)\n\n        assert isinstance(result, ToolResult)\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"[Rendered Prefab UI]\"\n        assert result.structured_content is not None\n        assert result.structured_content[\"version\"] == \"0.2\"\n        assert result.structured_content[\"state\"] == {\"name\": \"Alice\"}\n        assert result.structured_content[\"view\"][\"type\"] == \"Column\"\n\n    def test_bare_component(self):\n        heading = Heading(\"World\")\n\n        tool = Tool(name=\"t\", parameters={})\n        result = tool.convert_result(heading)\n\n        assert isinstance(result, ToolResult)\n        assert result.structured_content is not None\n        assert result.structured_content[\"version\"] == \"0.2\"\n        assert result.structured_content[\"view\"][\"type\"] == \"Heading\"\n\n    def test_tool_result_with_prefab_structured_content(self):\n        \"\"\"ToolResult with PrefabApp as structured_content preserves custom text.\"\"\"\n        app = PrefabApp(view=Heading(\"Hello\"), state={\"x\": 1})\n\n        tool = Tool(name=\"t\", parameters={})\n        result = tool.convert_result(\n            ToolResult(content=\"Custom fallback text\", structured_content=app)\n        )\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Custom fallback text\"\n        assert result.structured_content is not None\n        assert result.structured_content[\"version\"] == \"0.2\"\n        assert result.structured_content[\"view\"][\"type\"] == \"Heading\"\n\n    def test_tool_result_with_component_structured_content(self):\n        \"\"\"ToolResult with bare Component as structured_content.\"\"\"\n        tool = Tool(name=\"t\", parameters={})\n        result = tool.convert_result(\n            ToolResult(content=\"My text\", structured_content=Heading(\"Hi\"))\n        )\n\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"My text\"\n        assert result.structured_content is not None\n        assert result.structured_content[\"version\"] == \"0.2\"\n        assert result.structured_content[\"view\"][\"type\"] == \"Heading\"\n\n    def test_tool_result_passthrough(self):\n        \"\"\"ToolResult without prefab structured_content passes through unchanged.\"\"\"\n        original = ToolResult(content=\"hello\")\n        tool = Tool(name=\"t\", parameters={})\n        assert tool.convert_result(original) is original\n\n\n# ---------------------------------------------------------------------------\n# app=True auto-wiring\n# ---------------------------------------------------------------------------\n\n\nclass TestAppTrue:\n    def test_app_true_sets_meta(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert \"ui\" in tool.meta\n        assert tool.meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n    def test_app_true_registers_renderer_resource(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def my_tool() -> str:\n            return \"hello\"\n\n        renderer_key = f\"resource:{PREFAB_RENDERER_URI}@\"\n        assert renderer_key in mcp._local_provider._components\n\n    def test_renderer_resource_has_correct_mime_type(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def my_tool() -> str:\n            return \"hello\"\n\n        renderer_key = f\"resource:{PREFAB_RENDERER_URI}@\"\n        resource = mcp._local_provider._components[renderer_key]\n        assert isinstance(resource, TextResource)\n        assert resource.mime_type == UI_MIME_TYPE\n\n    def test_renderer_resource_has_csp(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def my_tool() -> str:\n            return \"hello\"\n\n        renderer_key = f\"resource:{PREFAB_RENDERER_URI}@\"\n        resource = mcp._local_provider._components[renderer_key]\n        assert resource.meta is not None\n        assert \"ui\" in resource.meta\n        assert \"csp\" in resource.meta[\"ui\"]\n\n    def test_multiple_tools_share_renderer(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def tool_a() -> str:\n            return \"a\"\n\n        @mcp.tool(app=True)\n        def tool_b() -> str:\n            return \"b\"\n\n        renderer_keys = [\n            k for k in mcp._local_provider._components if k.startswith(\"resource:ui://\")\n        ]\n        assert len(renderer_keys) == 1\n\n    def test_explicit_app_config_not_overridden(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=AppConfig(resource_uri=\"ui://custom/app.html\"))\n        def my_tool() -> PrefabApp:\n            return PrefabApp(view=Heading(\"hi\"))\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"resourceUri\"] == \"ui://custom/app.html\"\n\n\n# ---------------------------------------------------------------------------\n# Return type inference\n# ---------------------------------------------------------------------------\n\n\nclass TestInference:\n    def test_prefab_app_annotation_inferred(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> PrefabApp:\n            return PrefabApp(view=Heading(\"hi\"))\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n    def test_component_annotation_inferred(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> Component:\n            return Heading(\"hi\")\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n    def test_no_annotation_no_inference(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool():\n            return \"hello\"\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is None or \"ui\" not in (tool.meta or {})\n\n    def test_non_prefab_annotation_no_inference(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> str:\n            return \"hello\"\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is None or \"ui\" not in (tool.meta or {})\n\n    def test_optional_prefab_app_inferred(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> PrefabApp | None:\n            return None\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n    def test_annotated_prefab_app_inferred(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> Annotated[PrefabApp | None, \"some metadata\"]:\n            return None\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n    def test_component_subclass_union_inferred(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> Column | None:\n            return None\n\n        tools = mcp._local_provider._components\n        tool = next(\n            v\n            for v in tools.values()\n            if hasattr(v, \"parameters\") and v.name == \"my_tool\"\n        )\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n\n# ---------------------------------------------------------------------------\n# Output schema suppression\n# ---------------------------------------------------------------------------\n\n\nclass TestOutputSchema:\n    def test_prefab_app_return_no_output_schema(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> PrefabApp:\n            return PrefabApp(view=Heading(\"hi\"))\n\n        tools = mcp._local_provider._components\n        tool: Tool = next(\n            v for v in tools.values() if isinstance(v, Tool) and v.name == \"my_tool\"\n        )\n        assert tool.output_schema is None\n\n    def test_component_return_no_output_schema(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> Column:\n            with Column() as view:\n                Heading(\"hi\")\n            return view\n\n        tools = mcp._local_provider._components\n        tool: Tool = next(\n            v for v in tools.values() if isinstance(v, Tool) and v.name == \"my_tool\"\n        )\n        assert tool.output_schema is None\n\n    def test_optional_component_no_output_schema(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> Column | None:\n            return None\n\n        tools = mcp._local_provider._components\n        tool: Tool = next(\n            v for v in tools.values() if isinstance(v, Tool) and v.name == \"my_tool\"\n        )\n        assert tool.output_schema is None\n\n    def test_annotated_prefab_app_no_output_schema(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def my_tool() -> Annotated[PrefabApp | None, \"metadata\"]:\n            return None\n\n        tools = mcp._local_provider._components\n        tool: Tool = next(\n            v for v in tools.values() if isinstance(v, Tool) and v.name == \"my_tool\"\n        )\n        assert tool.output_schema is None\n\n\n# ---------------------------------------------------------------------------\n# Integration — client-server round trip\n# ---------------------------------------------------------------------------\n\n\nclass TestIntegration:\n    async def test_tool_call_returns_prefab_structured_content(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def greet(name: str) -> PrefabApp:\n            with Column() as view:\n                Heading(\"Hello\")\n                Text(f\"Welcome, {name}!\")\n            return PrefabApp(view=view, state={\"name\": name})\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"Alice\"})\n\n        assert result.structured_content is not None\n        assert result.structured_content[\"version\"] == \"0.2\"\n        assert result.structured_content[\"state\"] == {\"name\": \"Alice\"}\n\n    async def test_tool_call_with_custom_text(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def greet(name: str) -> ToolResult:\n            app = PrefabApp(view=Heading(f\"Hello {name}\"))\n            return ToolResult(\n                content=f\"Greeting for {name}\",\n                structured_content=app,\n            )\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"Alice\"})\n\n        assert any(\n            \"Greeting for Alice\" in c.text for c in result.content if hasattr(c, \"text\")\n        )\n        assert result.structured_content is not None\n        assert result.structured_content[\"version\"] == \"0.2\"\n\n    async def test_tools_list_includes_app_meta(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def my_tool() -> PrefabApp:\n            return PrefabApp(view=Heading(\"hi\"))\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n\n        tool = next(t for t in tools if t.name == \"my_tool\")\n        meta = tool.meta or {}\n        assert \"ui\" in meta\n        assert meta[\"ui\"][\"resourceUri\"] == PREFAB_RENDERER_URI\n\n    async def test_renderer_resource_readable(self):\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool(app=True)\n        def my_tool() -> str:\n            return \"hello\"\n\n        async with Client(mcp) as client:\n            contents = await client.read_resource(PREFAB_RENDERER_URI)\n\n        assert len(contents) > 0\n        text = contents[0].text if hasattr(contents[0], \"text\") else \"\"\n        assert \"<html\" in text.lower() or \"<!doctype\" in text.lower()\n"
  },
  {
    "path": "tests/test_fastmcp_app.py",
    "content": "\"\"\"Tests for FastMCPApp — the composable application provider.\n\nCovers:\n- @app.tool() decorator (global keys, visibility, calling patterns)\n- @app.ui() decorator (model visibility, CSP auto-wiring)\n- Global key registry and call_tool routing\n- Callable resolver (_resolve_tool_ref)\n- Composition with namespaced servers\n- Provider interface delegation\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom unittest.mock import AsyncMock\n\nimport pytest\nfrom prefab_ui.app import ResolvedTool\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.app import (\n    _APP_TOOL_REGISTRY,\n    _FN_TO_GLOBAL_KEY,\n    FastMCPApp,\n    _make_global_key,\n    _resolve_tool_ref,\n)\nfrom fastmcp.tools.base import Tool\n\n# ---------------------------------------------------------------------------\n# Fixtures / helpers\n# ---------------------------------------------------------------------------\n\nGLOBAL_KEY_PATTERN = re.compile(r\"^.+-[0-9a-f]{8}$\")\n\n\ndef _clear_registries() -> None:\n    \"\"\"Clear process-level registries between tests.\"\"\"\n    _APP_TOOL_REGISTRY.clear()\n    _FN_TO_GLOBAL_KEY.clear()\n\n\n# ---------------------------------------------------------------------------\n# Global key generation\n# ---------------------------------------------------------------------------\n\n\nclass TestGlobalKeyGeneration:\n    def test_make_global_key_format(self):\n        key = _make_global_key(\"save_contact\")\n        assert GLOBAL_KEY_PATTERN.match(key)\n        assert key.startswith(\"save_contact-\")\n\n    def test_make_global_key_uniqueness(self):\n        keys = {_make_global_key(\"my_tool\") for _ in range(100)}\n        assert len(keys) == 100\n\n\n# ---------------------------------------------------------------------------\n# @app.tool() decorator\n# ---------------------------------------------------------------------------\n\n\nclass TestAppTool:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    def test_tool_bare_decorator(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool\n        def save(name: str) -> str:\n            return name\n\n        # Function is returned unchanged\n        assert save(\"alice\") == \"alice\"\n\n    def test_tool_empty_parens(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        assert save(\"alice\") == \"alice\"\n\n    def test_tool_custom_name(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool(\"custom_save\")\n        def save(name: str) -> str:\n            return name\n\n        assert save(\"alice\") == \"alice\"\n\n    def test_tool_name_kwarg(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool(name=\"my_tool\")\n        def save(name: str) -> str:\n            return name\n\n        assert save(\"alice\") == \"alice\"\n\n    def test_tool_name_conflict_raises(self):\n        app = FastMCPApp(\"test\")\n\n        with pytest.raises(TypeError):\n\n            @app.tool(\"x\", name=\"y\")\n            def save() -> str:\n                return \"\"\n\n    async def test_tool_registers_in_provider(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        tools = await app._list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"save\"\n\n    async def test_tool_custom_name_in_provider(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool(\"custom_save\")\n        def save(name: str) -> str:\n            return name\n\n        tools = await app._list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"custom_save\"\n\n    def test_tool_gets_global_key(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        # Check the function is registered in the global key registry\n        assert id(save) in _FN_TO_GLOBAL_KEY\n        global_key = _FN_TO_GLOBAL_KEY[id(save)]\n        assert GLOBAL_KEY_PATTERN.match(global_key)\n        assert global_key.startswith(\"save-\")\n        assert global_key in _APP_TOOL_REGISTRY\n\n    async def test_tool_global_key_in_meta(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        tools = await app._list_tools()\n        tool = tools[0]\n        assert tool.meta is not None\n        assert \"ui\" in tool.meta\n        assert \"globalKey\" in tool.meta[\"ui\"]\n        assert GLOBAL_KEY_PATTERN.match(tool.meta[\"ui\"][\"globalKey\"])\n\n    async def test_tool_default_visibility_app_only(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        tools = await app._list_tools()\n        meta = tools[0].meta\n        assert meta is not None\n        assert meta[\"ui\"][\"visibility\"] == [\"app\"]\n\n    async def test_tool_model_visibility(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool(model=True)\n        def query(search: str) -> list:\n            return []\n\n        tools = await app._list_tools()\n        meta = tools[0].meta\n        assert meta is not None\n        assert meta[\"ui\"][\"visibility\"] == [\"app\", \"model\"]\n\n    def test_tool_with_description(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool(description=\"Save a contact\")\n        def save(name: str) -> str:\n            return name\n\n    def test_tool_with_auth(self):\n        app = FastMCPApp(\"test\")\n        check = AsyncMock(return_value=True)\n\n        @app.tool(auth=check)\n        def save(name: str) -> str:\n            return name\n\n    def test_tool_with_timeout(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool(timeout=30.0)\n        def slow_save(name: str) -> str:\n            return name\n\n\n# ---------------------------------------------------------------------------\n# @app.ui() decorator\n# ---------------------------------------------------------------------------\n\n\nclass TestAppUI:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    def test_ui_bare_decorator(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui\n        def dashboard() -> str:\n            return \"hi\"\n\n        assert dashboard() == \"hi\"\n\n    def test_ui_empty_parens(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"hi\"\n\n        assert dashboard() == \"hi\"\n\n    def test_ui_custom_name(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui(\"my_dashboard\")\n        def dashboard() -> str:\n            return \"hi\"\n\n        assert dashboard() == \"hi\"\n\n    async def test_ui_registers_in_provider(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        tools = await app._list_tools()\n        assert len(tools) == 1\n        assert tools[0].name == \"dashboard\"\n\n    async def test_ui_visibility_model_only(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        tools = await app._list_tools()\n        meta = tools[0].meta\n        assert meta is not None\n        assert meta[\"ui\"][\"visibility\"] == [\"model\"]\n\n    def test_ui_no_global_key(self):\n        \"\"\"UI entry points should NOT get global keys.\"\"\"\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        assert id(dashboard) not in _FN_TO_GLOBAL_KEY\n\n    async def test_ui_has_resource_uri(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        tools = await app._list_tools()\n        meta = tools[0].meta\n        assert meta is not None\n        assert meta[\"ui\"][\"resourceUri\"] == \"ui://prefab/renderer.html\"\n\n    async def test_ui_has_csp(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        tools = await app._list_tools()\n        meta = tools[0].meta\n        assert meta is not None\n        csp = meta[\"ui\"].get(\"csp\")\n        assert csp is not None\n\n    async def test_ui_with_title_and_description(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui(title=\"My Dashboard\", description=\"Shows data\")\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        tools = await app._list_tools()\n        assert tools[0].title == \"My Dashboard\"\n        assert tools[0].description == \"Shows data\"\n\n    async def test_ui_with_tags(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui(tags={\"dashboard\", \"main\"})\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        tools = await app._list_tools()\n        assert tools[0].tags == {\"dashboard\", \"main\"}\n\n\n# ---------------------------------------------------------------------------\n# Callable resolver\n# ---------------------------------------------------------------------------\n\n\nclass TestResolveToolRef:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    def test_resolve_global_key(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        result = _resolve_tool_ref(save)\n        # str return → wrapped tool → ResolvedTool with unwrap_result\n        assert isinstance(result, ResolvedTool)\n        assert GLOBAL_KEY_PATTERN.match(result.name)\n        assert result.name.startswith(\"save-\")\n        assert result.unwrap_result is True\n\n    def test_resolve_global_key_object_return(self):\n        \"\"\"Tools returning dicts don't need unwrapping.\"\"\"\n\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> dict:\n            return {\"name\": name}\n\n        result = _resolve_tool_ref(save)\n        assert isinstance(result, ResolvedTool)\n        assert GLOBAL_KEY_PATTERN.match(result.name)\n        assert result.name.startswith(\"save-\")\n        assert result.unwrap_result is False\n\n    def test_resolve_fastmcp_metadata(self):\n        \"\"\"Functions with __fastmcp__ metadata but no global key.\"\"\"\n\n        from fastmcp.tools.function_tool import ToolMeta\n\n        def my_tool():\n            pass\n\n        my_tool.__fastmcp__ = ToolMeta(name=\"custom_name\")  # type: ignore[attr-defined]\n\n        result = _resolve_tool_ref(my_tool)\n        assert isinstance(result, ResolvedTool)\n        assert result.name == \"custom_name\"\n\n    def test_resolve_bare_function(self):\n        def my_tool():\n            pass\n\n        result = _resolve_tool_ref(my_tool)\n        assert isinstance(result, ResolvedTool)\n        assert result.name == \"my_tool\"\n\n    def test_resolve_unresolvable_raises(self):\n        with pytest.raises(ValueError):\n            _resolve_tool_ref(42)\n\n\n# ---------------------------------------------------------------------------\n# Provider interface\n# ---------------------------------------------------------------------------\n\n\nclass TestProviderInterface:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    async def test_list_tools_empty(self):\n        app = FastMCPApp(\"test\")\n        assert await app._list_tools() == []\n\n    async def test_list_resources_empty(self):\n        app = FastMCPApp(\"test\")\n        assert list(await app._list_resources()) == []\n\n    async def test_get_tool_by_name(self):\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        tool = await app._get_tool(\"save\")\n        assert tool is not None\n        assert tool.name == \"save\"\n\n    async def test_get_tool_missing_returns_none(self):\n        app = FastMCPApp(\"test\")\n        assert await app._get_tool(\"missing\") is None\n\n\n# ---------------------------------------------------------------------------\n# call_tool with global key routing\n# ---------------------------------------------------------------------------\n\n\nclass TestCallToolGlobalKeyRouting:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    async def test_call_tool_by_global_key(self):\n        \"\"\"Server.call_tool can find a FastMCPApp tool by its global key.\"\"\"\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return f\"saved {name}\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app)\n\n        global_key = _FN_TO_GLOBAL_KEY[id(save)]\n\n        result = await server.call_tool(global_key, {\"name\": \"alice\"})\n        assert result.content[0].text == \"saved alice\"  # type: ignore[union-attr]\n\n    async def test_call_tool_by_name(self):\n        \"\"\"Regular name-based resolution still works.\"\"\"\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return f\"saved {name}\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app)\n\n        result = await server.call_tool(\"save\", {\"name\": \"bob\"})\n        assert result.content[0].text == \"saved bob\"  # type: ignore[union-attr]\n\n    async def test_global_key_survives_namespace(self):\n        \"\"\"Global key works even when the app is mounted under a namespace.\"\"\"\n        app = FastMCPApp(\"crm\")\n\n        @app.tool()\n        def save_contact(name: str) -> str:\n            return f\"saved {name}\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app, namespace=\"crm\")\n\n        global_key = _FN_TO_GLOBAL_KEY[id(save_contact)]\n\n        # Global key should still work\n        result = await server.call_tool(global_key, {\"name\": \"alice\"})\n        assert result.content[0].text == \"saved alice\"  # type: ignore[union-attr]\n\n    async def test_namespaced_name_also_works(self):\n        \"\"\"Namespaced tool name works through normal resolution.\"\"\"\n        app = FastMCPApp(\"crm\")\n\n        @app.tool(model=True)\n        def save_contact(name: str) -> str:\n            return f\"saved {name}\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app, namespace=\"crm\")\n\n        result = await server.call_tool(\"crm_save_contact\", {\"name\": \"bob\"})\n        assert result.content[0].text == \"saved bob\"  # type: ignore[union-attr]\n\n    async def test_global_key_auth_blocks_unauthorized(self):\n        \"\"\"Auth checks run even when resolving via global key.\"\"\"\n        from fastmcp.exceptions import NotFoundError\n        from fastmcp.server.context import _current_transport\n\n        app = FastMCPApp(\"test\")\n        deny_all = AsyncMock(return_value=False)\n\n        @app.tool(auth=deny_all)\n        def secret() -> str:\n            return \"classified\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app)\n\n        global_key = _FN_TO_GLOBAL_KEY[id(secret)]\n\n        # Simulate non-stdio transport so auth is not skipped\n        token = _current_transport.set(\"streamable-http\")\n        try:\n            with pytest.raises(NotFoundError):\n                await server.call_tool(global_key, {})\n        finally:\n            _current_transport.reset(token)\n\n\n# ---------------------------------------------------------------------------\n# End-to-end via Client\n# ---------------------------------------------------------------------------\n\n\nclass TestEndToEnd:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    async def test_ui_tool_visible_to_client(self):\n        \"\"\"UI entry-point tools show up in list_tools via Client.\"\"\"\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"dashboard\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app)\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n            names = [t.name for t in tools]\n            assert \"dashboard\" in names\n\n    async def test_app_tool_model_true_visible(self):\n        \"\"\"App tools with model=True are visible via Client.\"\"\"\n        app = FastMCPApp(\"test\")\n\n        @app.tool(model=True)\n        def query(search: str) -> list:\n            return [search]\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app)\n\n        async with Client(server) as client:\n            tools = await client.list_tools()\n            names = [t.name for t in tools]\n            assert \"query\" in names\n\n    async def test_call_tool_via_global_key_through_client(self):\n        \"\"\"Client can call a tool using its global key.\"\"\"\n        app = FastMCPApp(\"test\")\n\n        @app.tool()\n        def save(name: str) -> str:\n            return f\"saved {name}\"\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(app)\n\n        global_key = _FN_TO_GLOBAL_KEY[id(save)]\n\n        async with Client(server) as client:\n            result = await client.call_tool(global_key, {\"name\": \"test\"})\n            assert \"saved test\" in result.content[0].text\n\n\n# ---------------------------------------------------------------------------\n# .run() convenience\n# ---------------------------------------------------------------------------\n\n\nclass TestRun:\n    def test_repr(self):\n        app = FastMCPApp(\"Dashboard\")\n        assert repr(app) == \"FastMCPApp('Dashboard')\"\n\n\n# ---------------------------------------------------------------------------\n# add_tool programmatic\n# ---------------------------------------------------------------------------\n\n\nclass TestAddTool:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    async def test_add_tool_from_function(self):\n        app = FastMCPApp(\"test\")\n\n        def save(name: str) -> str:\n            return name\n\n        tool = app.add_tool(save)\n        assert tool.name == \"save\"\n\n        tools = await app._list_tools()\n        assert len(tools) == 1\n\n    async def test_add_tool_gets_global_key(self):\n        app = FastMCPApp(\"test\")\n\n        def save(name: str) -> str:\n            return name\n\n        tool = app.add_tool(save)\n        assert tool.meta is not None\n        assert \"globalKey\" in tool.meta.get(\"ui\", {})\n\n    async def test_add_tool_object(self):\n        app = FastMCPApp(\"test\")\n        tool = Tool.from_function(lambda x: x, name=\"my_tool\")\n        added = app.add_tool(tool)\n        assert added.name == \"my_tool\"\n\n        tools = await app._list_tools()\n        assert len(tools) == 1\n\n\n# ---------------------------------------------------------------------------\n# Composition\n# ---------------------------------------------------------------------------\n\n\nclass TestComposition:\n    def setup_method(self) -> None:\n        _clear_registries()\n\n    async def test_multiple_apps_on_one_server(self):\n        crm = FastMCPApp(\"CRM\")\n        billing = FastMCPApp(\"Billing\")\n\n        @crm.tool()\n        def save_contact(name: str) -> str:\n            return name\n\n        @billing.tool()\n        def create_invoice(amount: int) -> int:\n            return amount\n\n        server = FastMCP(\"Platform\")\n        server.add_provider(crm, namespace=\"crm\")\n        server.add_provider(billing, namespace=\"billing\")\n\n        # Both tools reachable by global key\n        crm_key = _FN_TO_GLOBAL_KEY[id(save_contact)]\n        billing_key = _FN_TO_GLOBAL_KEY[id(create_invoice)]\n\n        r1 = await server.call_tool(crm_key, {\"name\": \"alice\"})\n        r2 = await server.call_tool(billing_key, {\"amount\": 100})\n\n        assert r1.content[0].text == \"alice\"  # type: ignore[union-attr]\n        assert r2.content[0].text == \"100\"  # type: ignore[union-attr]\n\n    async def test_ui_and_tool_on_same_app(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"ui\"\n\n        @app.tool()\n        def save(name: str) -> str:\n            return name\n\n        tools = await app._list_tools()\n        assert len(tools) == 2\n        names = {t.name for t in tools}\n        assert names == {\"dashboard\", \"save\"}\n\n    async def test_ui_registers_prefab_renderer_resource(self):\n        app = FastMCPApp(\"test\")\n\n        @app.ui()\n        def dashboard() -> str:\n            return \"ui\"\n\n        resources = await app._list_resources()\n        uris = [str(r.uri) for r in resources]\n        assert any(\"ui://prefab/renderer.html\" in uri for uri in uris)\n"
  },
  {
    "path": "tests/test_json_schema_generation.py",
    "content": "\"\"\"Tests for JSON schema generation from FastMCP BaseModel classes.\n\nValidates that callable fields are properly excluded from generated schemas\nusing SkipJsonSchema annotations.\n\"\"\"\n\nfrom fastmcp.prompts.function_prompt import FunctionPrompt\nfrom fastmcp.resources.function_resource import FunctionResource\nfrom fastmcp.resources.template import FunctionResourceTemplate\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import TransformedTool\n\n\nclass TestToolJsonSchema:\n    \"\"\"Test JSON schema generation for Tool classes.\"\"\"\n\n    def test_tool_json_schema_generation(self):\n        \"\"\"Verify Tool.model_json_schema() works without errors.\"\"\"\n        # This should not raise an error\n        schema = Tool.model_json_schema()\n\n        # Verify schema is valid\n        assert schema[\"type\"] == \"object\"\n        assert \"properties\" in schema\n\n        # Verify callable fields are excluded from schema\n        assert \"serializer\" not in schema[\"properties\"]\n        # auth already uses exclude=True, so it shouldn't be in schema\n        assert \"auth\" not in schema[\"properties\"]\n\n    def test_function_tool_json_schema_generation(self):\n        \"\"\"Verify FunctionTool.model_json_schema() works without errors.\"\"\"\n\n        def sample_tool(x: int, y: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return x + y\n\n        tool = FunctionTool.from_function(sample_tool)\n\n        # This should not raise an error\n        schema = tool.model_json_schema()\n\n        # Verify schema is valid\n        assert schema[\"type\"] == \"object\"\n        assert \"properties\" in schema\n\n        # Verify callable field 'fn' is excluded from schema\n        assert \"fn\" not in schema[\"properties\"]\n\n    def test_transformed_tool_json_schema_generation(self):\n        \"\"\"Verify TransformedTool.model_json_schema() works without errors.\"\"\"\n\n        def parent_fn(x: int) -> int:\n            return x * 2\n\n        parent_tool = FunctionTool.from_function(parent_fn)\n        transformed_tool = TransformedTool.from_tool(parent_tool, name=\"doubled\")\n\n        # This should not raise an error\n        schema = transformed_tool.model_json_schema()\n\n        # Verify schema is valid\n        assert schema[\"type\"] == \"object\"\n        assert \"properties\" in schema\n\n        # Verify callable fields are excluded from schema\n        assert \"fn\" not in schema[\"properties\"]\n        assert \"forwarding_fn\" not in schema[\"properties\"]\n        assert \"parent_tool\" not in schema[\"properties\"]\n\n\nclass TestResourceJsonSchema:\n    \"\"\"Test JSON schema generation for Resource classes.\"\"\"\n\n    def test_function_resource_json_schema_generation(self):\n        \"\"\"Verify FunctionResource.model_json_schema() works without errors.\"\"\"\n\n        def sample_resource() -> str:\n            \"\"\"Return sample data.\"\"\"\n            return \"Hello, world!\"\n\n        resource = FunctionResource.from_function(\n            sample_resource, uri=\"test://resource\"\n        )\n\n        # This should not raise an error\n        schema = resource.model_json_schema()\n\n        # Verify schema is valid\n        assert schema[\"type\"] == \"object\"\n        assert \"properties\" in schema\n\n        # Verify callable field 'fn' is excluded from schema\n        assert \"fn\" not in schema[\"properties\"]\n        # auth already uses exclude=True\n        assert \"auth\" not in schema[\"properties\"]\n\n    def test_function_resource_template_json_schema_generation(self):\n        \"\"\"Verify FunctionResourceTemplate.model_json_schema() works without errors.\"\"\"\n\n        def sample_template(name: str) -> str:\n            \"\"\"Return greeting for name.\"\"\"\n            return f\"Hello, {name}!\"\n\n        template = FunctionResourceTemplate.from_function(\n            sample_template, uri_template=\"greeting://{name}\"\n        )\n\n        # This should not raise an error\n        schema = template.model_json_schema()\n\n        # Verify schema is valid\n        assert schema[\"type\"] == \"object\"\n        assert \"properties\" in schema\n\n        # Verify callable field 'fn' is excluded from schema\n        assert \"fn\" not in schema[\"properties\"]\n\n\nclass TestPromptJsonSchema:\n    \"\"\"Test JSON schema generation for Prompt classes.\"\"\"\n\n    def test_function_prompt_json_schema_generation(self):\n        \"\"\"Verify FunctionPrompt.model_json_schema() works without errors.\"\"\"\n\n        def sample_prompt(topic: str) -> str:\n            \"\"\"Generate prompt about topic.\"\"\"\n            return f\"Tell me about {topic}\"\n\n        prompt = FunctionPrompt.from_function(sample_prompt)\n\n        # This should not raise an error\n        schema = prompt.model_json_schema()\n\n        # Verify schema is valid\n        assert schema[\"type\"] == \"object\"\n        assert \"properties\" in schema\n\n        # Verify callable field 'fn' is excluded from schema\n        assert \"fn\" not in schema[\"properties\"]\n        # auth already uses exclude=True\n        assert \"auth\" not in schema[\"properties\"]\n\n\nclass TestJsonSchemaIntegration:\n    \"\"\"Integration tests for JSON schema generation across all classes.\"\"\"\n\n    def test_all_classes_generate_valid_schemas(self):\n        \"\"\"Verify all affected classes can generate valid JSON schemas.\"\"\"\n\n        # Create instances of all affected classes\n        def tool_fn(x: int) -> int:\n            return x\n\n        def resource_fn() -> str:\n            return \"data\"\n\n        def template_fn(id: str) -> str:\n            return f\"data-{id}\"\n\n        def prompt_fn(input: str) -> str:\n            return f\"Prompt: {input}\"\n\n        tool = FunctionTool.from_function(tool_fn)\n        transformed_tool = TransformedTool.from_tool(tool)\n        resource = FunctionResource.from_function(resource_fn, uri=\"test://resource\")\n        template = FunctionResourceTemplate.from_function(\n            template_fn, uri_template=\"test://{id}\"\n        )\n        prompt = FunctionPrompt.from_function(prompt_fn)\n\n        # All of these should succeed without errors\n        schemas = [\n            Tool.model_json_schema(),\n            tool.model_json_schema(),\n            transformed_tool.model_json_schema(),\n            resource.model_json_schema(),\n            template.model_json_schema(),\n            prompt.model_json_schema(),\n        ]\n\n        # Verify all schemas are valid\n        for schema in schemas:\n            assert isinstance(schema, dict)\n            assert schema[\"type\"] == \"object\"\n            assert \"properties\" in schema\n\n    def test_callable_fields_not_in_any_schema(self):\n        \"\"\"Verify no callable fields appear in any generated schema.\"\"\"\n\n        # Define test functions\n        def tool_fn(x: int) -> int:\n            return x\n\n        def resource_fn() -> str:\n            return \"data\"\n\n        def template_fn(id: str) -> str:\n            return f\"data-{id}\"\n\n        def prompt_fn(input: str) -> str:\n            return f\"Prompt: {input}\"\n\n        # Create instances\n        tool = FunctionTool.from_function(tool_fn)\n        transformed_tool = TransformedTool.from_tool(tool)\n        resource = FunctionResource.from_function(resource_fn, uri=\"test://resource\")\n        template = FunctionResourceTemplate.from_function(\n            template_fn, uri_template=\"test://{id}\"\n        )\n        prompt = FunctionPrompt.from_function(prompt_fn)\n\n        # List of (instance, callable_field_names) tuples\n        test_cases = [\n            (tool, [\"fn\", \"serializer\"]),\n            (transformed_tool, [\"fn\", \"forwarding_fn\", \"parent_tool\", \"serializer\"]),\n            (resource, [\"fn\"]),\n            (template, [\"fn\"]),\n            (prompt, [\"fn\"]),\n        ]\n\n        for instance, callable_fields in test_cases:\n            schema = instance.model_json_schema()\n            properties = schema.get(\"properties\", {})\n\n            # Verify none of the callable fields are in the schema\n            for field in callable_fields:\n                assert field not in properties, (\n                    f\"Callable field '{field}' found in schema for {type(instance).__name__}\"\n                )\n"
  },
  {
    "path": "tests/test_mcp_config.py",
    "content": "import asyncio\nimport gc\nimport inspect\nimport logging\nimport os\nimport sys\nimport tempfile\nimport time\nfrom collections.abc import AsyncGenerator\nfrom datetime import timedelta\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import AsyncMock, patch\n\nimport psutil\nimport pytest\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client.auth.bearer import BearerAuth\nfrom fastmcp.client.auth.oauth import OAuthClientProvider\nfrom fastmcp.client.client import Client\nfrom fastmcp.client.logging import LogMessage\nfrom fastmcp.client.transports import (\n    MCPConfigTransport,\n    SSETransport,\n    StdioTransport,\n    StreamableHttpTransport,\n)\nfrom fastmcp.mcp_config import (\n    CanonicalMCPConfig,\n    CanonicalMCPServerTypes,\n    MCPConfig,\n    MCPServerTypes,\n    RemoteMCPServer,\n    StdioMCPServer,\n    TransformingStdioMCPServer,\n)\nfrom fastmcp.tools.base import Tool as FastMCPTool\n\n# These tests spawn subprocess servers via stdio which can be slow under\n# parallel CI load. Give them more headroom than the 5s default, and skip\n# entirely on Windows due to process lifecycle issues.\npytestmark = [\n    pytest.mark.timeout(15),\n    pytest.mark.skipif(\n        sys.platform.startswith(\"win32\"),\n        reason=\"Windows has process lifecycle issues with stdio subprocesses\",\n    ),\n]\n\n\ndef running_under_debugger():\n    return os.environ.get(\"DEBUGPY_RUNNING\") == \"true\"\n\n\ndef gc_collect_harder():\n    gc.collect()\n    gc.collect()\n    gc.collect()\n    gc.collect()\n    gc.collect()\n    gc.collect()\n\n\ndef test_parse_single_stdio_config():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"command\": \"echo\",\n                \"args\": [\"hello\"],\n            }\n        }\n    }\n    mcp_config = MCPConfig.from_dict(config)\n    transport = mcp_config.mcpServers[\"test_server\"].to_transport()\n    assert isinstance(transport, StdioTransport)\n    assert transport.command == \"echo\"\n    assert transport.args == [\"hello\"]\n\n\ndef test_stdio_config_keep_alive_passthrough():\n    \"\"\"Test that keep_alive parameter is passed through from StdioMCPServer to StdioTransport.\"\"\"\n    # Test with keep_alive=False\n    server = StdioMCPServer(command=\"test\", keep_alive=False)\n    assert server.keep_alive is False\n    transport = server.to_transport()\n    assert isinstance(transport, StdioTransport)\n    assert transport.keep_alive is False\n\n    # Test with keep_alive=True\n    server = StdioMCPServer(command=\"test\", keep_alive=True)\n    assert server.keep_alive is True\n    transport = server.to_transport()\n    assert isinstance(transport, StdioTransport)\n    assert transport.keep_alive is True\n\n    # Test with keep_alive=None (should default to True in StdioTransport)\n    server = StdioMCPServer(command=\"test\", keep_alive=None)\n    assert server.keep_alive is None\n    transport = server.to_transport()\n    assert isinstance(transport, StdioTransport)\n    assert transport.keep_alive is True  # StdioTransport defaults to True\n\n    # Test with keep_alive not specified (should default to None, then True in StdioTransport)\n    server = StdioMCPServer(command=\"test\")\n    assert server.keep_alive is None\n    transport = server.to_transport()\n    assert isinstance(transport, StdioTransport)\n    assert transport.keep_alive is True  # StdioTransport defaults to True\n\n\ndef test_parse_extra_keys():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"command\": \"echo\",\n                \"args\": [\"hello\"],\n                \"leaf_extra\": \"leaf_extra\",\n            }\n        },\n        \"root_extra\": \"root_extra\",\n    }\n    mcp_config = MCPConfig.from_dict(config)\n\n    serialized_mcp_config = mcp_config.to_dict()\n    assert serialized_mcp_config[\"root_extra\"] == \"root_extra\"\n    assert (\n        serialized_mcp_config[\"mcpServers\"][\"test_server\"][\"leaf_extra\"] == \"leaf_extra\"\n    )\n\n\ndef test_parse_mcpservers_at_root():\n    config = {\n        \"test_server\": {\n            \"command\": \"echo\",\n            \"args\": [\"hello\"],\n        }\n    }\n\n    mcp_config = MCPConfig.from_dict(config)\n\n    serialized_mcp_config = mcp_config.model_dump()\n    assert serialized_mcp_config[\"mcpServers\"][\"test_server\"][\"command\"] == \"echo\"\n    assert serialized_mcp_config[\"mcpServers\"][\"test_server\"][\"args\"] == [\"hello\"]\n\n\ndef test_parse_mcpservers_discriminator():\n    \"\"\"Test that the MCPConfig discriminator produces StdioMCPServer for a non-transforming server\n    and TransformingStdioMCPServer for a transforming server.\"\"\"\n\n    config = {\n        \"test_server\": {\n            \"command\": \"echo\",\n            \"args\": [\"hello\"],\n        },\n        \"test_server_two\": {\"command\": \"echo\", \"args\": [\"hello\"], \"tools\": {}},\n        \"test_server_three\": {\n            \"command\": \"echo\",\n            \"args\": [\"hello\"],\n            \"include_tags\": [\"my_tag\"],\n        },\n    }\n\n    mcp_config = MCPConfig.from_dict(config)\n\n    test_server: MCPServerTypes = mcp_config.mcpServers[\"test_server\"]\n    assert isinstance(test_server, StdioMCPServer)\n\n    # Empty tools dict with no tags is not a meaningful transform\n    test_server_two: MCPServerTypes = mcp_config.mcpServers[\"test_server_two\"]\n    assert isinstance(test_server_two, StdioMCPServer)\n\n    # include_tags alone triggers transforming type\n    test_server_three: MCPServerTypes = mcp_config.mcpServers[\"test_server_three\"]\n    assert isinstance(test_server_three, TransformingStdioMCPServer)\n\n    canonical_mcp_config = CanonicalMCPConfig.from_dict(config)\n\n    canonical_test_server: CanonicalMCPServerTypes = canonical_mcp_config.mcpServers[\n        \"test_server\"\n    ]\n    assert isinstance(canonical_test_server, StdioMCPServer)\n\n    canonical_test_server_two: CanonicalMCPServerTypes = (\n        canonical_mcp_config.mcpServers[\"test_server_two\"]\n    )\n    assert isinstance(canonical_test_server_two, StdioMCPServer)\n\n\ndef test_parse_single_remote_config():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000\",\n            }\n        }\n    }\n    mcp_config = MCPConfig.from_dict(config)\n    transport = mcp_config.mcpServers[\"test_server\"].to_transport()\n    assert isinstance(transport, StreamableHttpTransport)\n    assert transport.url == \"http://localhost:8000\"\n\n\ndef test_parse_remote_config_with_transport():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000\",\n                \"transport\": \"sse\",\n            }\n        }\n    }\n    mcp_config = MCPConfig.from_dict(config)\n    transport = mcp_config.mcpServers[\"test_server\"].to_transport()\n    assert isinstance(transport, SSETransport)\n    assert transport.url == \"http://localhost:8000\"\n\n\ndef test_parse_remote_config_with_url_inference():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000/sse/\",\n            }\n        }\n    }\n    mcp_config = MCPConfig.from_dict(config)\n    transport = mcp_config.mcpServers[\"test_server\"].to_transport()\n    assert isinstance(transport, SSETransport)\n    assert transport.url == \"http://localhost:8000/sse/\"\n\n\ndef test_parse_multiple_servers():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000/sse/\",\n            },\n            \"test_server_2\": {\n                \"command\": \"echo\",\n                \"args\": [\"hello\"],\n                \"env\": {\"TEST\": \"test\"},\n            },\n        }\n    }\n    mcp_config = MCPConfig.from_dict(config)\n    assert len(mcp_config.mcpServers) == 2\n    assert isinstance(mcp_config.mcpServers[\"test_server\"], RemoteMCPServer)\n    assert isinstance(mcp_config.mcpServers[\"test_server\"].to_transport(), SSETransport)\n\n    assert isinstance(mcp_config.mcpServers[\"test_server_2\"], StdioMCPServer)\n    assert isinstance(\n        mcp_config.mcpServers[\"test_server_2\"].to_transport(), StdioTransport\n    )\n    assert mcp_config.mcpServers[\"test_server_2\"].command == \"echo\"\n    assert mcp_config.mcpServers[\"test_server_2\"].args == [\"hello\"]\n    assert mcp_config.mcpServers[\"test_server_2\"].env == {\"TEST\": \"test\"}\n\n\nasync def test_multi_client(tmp_path: Path):\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"test_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    client = Client(config)\n\n    async with client:\n        tools = await client.list_tools()\n        assert len(tools) == 2\n\n        result_1 = await client.call_tool(\"test_1_add\", {\"a\": 1, \"b\": 2})\n        result_2 = await client.call_tool(\"test_2_add\", {\"a\": 1, \"b\": 2})\n        assert result_1.data == 3\n        assert result_2.data == 3\n\n\nasync def test_multi_client_parallel_calls(tmp_path: Path):\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"test_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    client = Client(config)\n\n    async with client:\n        _ = await client.list_tools()\n\n        tasks = [client.list_tools() for _ in range(40)]\n\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        exceptions = [result for result in results if isinstance(result, Exception)]\n        assert len(exceptions) == 0\n        assert len(results) == 40\n        assert all(len(result) == 2 for result in results)  # type: ignore[arg-type]\n\n\nasync def _wait_for_process_exit(pid: int, timeout: float = 3.0) -> None:\n    \"\"\"Poll until a process has exited, raising if it's still alive after timeout.\"\"\"\n\n    deadline = time.monotonic() + timeout\n    while time.monotonic() < deadline:\n        try:\n            psutil.Process(pid)\n        except psutil.NoSuchProcess:\n            return\n        await asyncio.sleep(0.05)\n    # Final check — if still alive, let the NoSuchProcess propagation fail the test clearly\n    psutil.Process(pid)\n    pytest.fail(f\"Process {pid} still alive after {timeout}s\")\n\n\n@pytest.mark.skipif(\n    running_under_debugger(),\n    reason=\"Debugger holds a reference to the transport\",\n)\n@pytest.mark.timeout(15)\nasync def test_multi_client_lifespan(tmp_path: Path):\n    pid_1: int | None = None\n    pid_2: int | None = None\n\n    async def test_server():\n        server_script = inspect.cleandoc(\"\"\"\n            from fastmcp import FastMCP\n            import os\n\n            mcp = FastMCP()\n\n            @mcp.tool\n            def pid() -> int:\n                return os.getpid()\n\n            if __name__ == '__main__':\n                mcp.run()\n            \"\"\")\n\n        script_path = tmp_path / \"test.py\"\n        script_path.write_text(server_script)\n\n        config = {\n            \"mcpServers\": {\n                \"test_1\": {\n                    \"command\": \"python\",\n                    \"args\": [str(script_path)],\n                },\n                \"test_2\": {\n                    \"command\": \"python\",\n                    \"args\": [str(script_path)],\n                },\n            }\n        }\n        transport = MCPConfigTransport(config)\n        client = Client(transport)\n\n        async with client:\n            nonlocal pid_1\n            pid_1 = (await client.call_tool(\"test_1_pid\")).data\n\n            nonlocal pid_2\n            pid_2 = (await client.call_tool(\"test_2_pid\")).data\n\n    await test_server()\n\n    gc_collect_harder()\n\n    # This test will fail while debugging because the debugger holds a reference to the underlying transport\n    assert pid_1 is not None\n    assert pid_2 is not None\n    await _wait_for_process_exit(pid_1)\n    await _wait_for_process_exit(pid_2)\n\n\n@pytest.mark.timeout(15)\nasync def test_multi_client_force_close(tmp_path: Path):\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n        import os\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def pid() -> int:\n            return os.getpid()\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"test_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n    transport = MCPConfigTransport(config)\n    client = Client(transport)\n\n    async with client:\n        pid_1 = (await client.call_tool(\"test_1_pid\")).data\n        pid_2 = (await client.call_tool(\"test_2_pid\")).data\n\n    await client.close()\n\n    gc_collect_harder()\n\n    await _wait_for_process_exit(pid_1)\n    await _wait_for_process_exit(pid_2)\n\n\nasync def test_remote_config_default_no_auth():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000\",\n            }\n        }\n    }\n    client = Client(config)\n    assert isinstance(client.transport.transport, StreamableHttpTransport)\n    assert client.transport.transport.auth is None\n\n\nasync def test_remote_config_with_auth_token():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000\",\n                \"auth\": \"test_token\",\n            }\n        }\n    }\n    client = Client(config)\n    assert isinstance(client.transport.transport, StreamableHttpTransport)\n    assert isinstance(client.transport.transport.auth, BearerAuth)\n    assert client.transport.transport.auth.token.get_secret_value() == \"test_token\"\n\n\nasync def test_remote_config_sse_with_auth_token():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000/sse/\",\n                \"auth\": \"test_token\",\n            }\n        }\n    }\n    client = Client(config)\n    assert isinstance(client.transport.transport, SSETransport)\n    assert isinstance(client.transport.transport.auth, BearerAuth)\n    assert client.transport.transport.auth.token.get_secret_value() == \"test_token\"\n\n\nasync def test_remote_config_with_oauth_literal():\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"url\": \"http://localhost:8000\",\n                \"auth\": \"oauth\",\n            }\n        }\n    }\n    client = Client(config)\n    assert isinstance(client.transport.transport, StreamableHttpTransport)\n    assert isinstance(client.transport.transport.auth, OAuthClientProvider)\n\n\nasync def test_multi_client_with_logging(tmp_path: Path, caplog):\n    \"\"\"\n    Tests that logging is properly forwarded to the ultimate client.\n    \"\"\"\n    caplog.set_level(logging.INFO, logger=__name__)\n\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP, Context\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        async def log_test(message: str, ctx: Context) -> int:\n            await ctx.log(message)\n            return 42\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"test_server_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    MESSAGES = []\n\n    logger = logging.getLogger(__name__)\n    # Backwards-compatible way to get the log level mapping\n    if hasattr(logging, \"getLevelNamesMapping\"):\n        # For Python 3.11+\n        LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()  # pyright: ignore [reportAttributeAccessIssue]\n    else:\n        # For older Python versions\n        LOGGING_LEVEL_MAP = logging._nameToLevel\n\n    async def log_handler(message: LogMessage):\n        MESSAGES.append(message)\n\n        level = LOGGING_LEVEL_MAP[message.level.upper()]\n        msg = message.data.get(\"msg\")\n        extra = message.data.get(\"extra\")\n        logger.log(level, msg, extra=extra)\n\n    async with Client(config, log_handler=log_handler) as client:\n        result = await client.call_tool(\"test_server_log_test\", {\"message\": \"test 42\"})\n        assert result.data == 42\n        assert len(MESSAGES) == 1\n        assert MESSAGES[0].data[\"msg\"] == \"test 42\"\n\n        # Filter to only our test logger (exclude OpenTelemetry internal logs)\n        test_records = [r for r in caplog.records if r.name == __name__]\n        assert len(test_records) == 1\n        assert test_records[0].msg == \"test 42\"\n\n\nasync def test_multi_client_with_transforms(tmp_path: Path):\n    \"\"\"\n    Tests that transforms are properly applied to the tools.\n    \"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n                \"tools\": {\n                    \"add\": {\n                        \"name\": \"transformed_add\",\n                        \"arguments\": {\n                            \"a\": {\"name\": \"transformed_a\"},\n                            \"b\": {\"name\": \"transformed_b\"},\n                        },\n                    }\n                },\n            },\n            \"test_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    client = Client[MCPConfigTransport](config)\n\n    async with client:\n        tools = await client.list_tools()\n        tools_by_name = {tool.name: tool for tool in tools}\n        assert len(tools) == 2\n        assert \"test_1_transformed_add\" in tools_by_name\n\n        result = await client.call_tool(\n            \"test_1_transformed_add\", {\"transformed_a\": 1, \"transformed_b\": 2}\n        )\n        assert result.data == 3\n\n\nasync def test_canonical_multi_client_with_transforms(tmp_path: Path):\n    \"\"\"Test that transforms are not applied to servers in a canonical MCPConfig.\"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = CanonicalMCPConfig(\n        mcpServers={\n            \"test_1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n                \"tools\": {  # <--- Will be ignored as it's not valid for a canonical MCPConfig\n                    \"add\": {\n                        \"name\": \"transformed_add\",\n                        \"arguments\": {\n                            \"a\": {\"name\": \"transformed_a\"},\n                            \"b\": {\"name\": \"transformed_b\"},\n                        },\n                    }\n                },\n            },\n            \"test_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }  # type: ignore[reportUnknownArgumentType]\n    )\n\n    client = Client(config)\n\n    async with client:\n        tools = await client.list_tools()\n        tools_by_name = {tool.name: tool for tool in tools}\n        assert len(tools) == 2\n        assert \"test_1_transformed_add\" not in tools_by_name\n\n\n@pytest.mark.flaky(retries=3)\nasync def test_multi_client_transform_with_filtering(tmp_path: Path):\n    \"\"\"\n    Tests that tag-based filtering works when using a transforming MCPConfig.\n    \"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.tool\n        def subtract(a: int, b: int) -> int:\n            return a - b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n                \"tools\": {\n                    \"add\": {\n                        \"name\": \"transformed_add\",\n                        \"tags\": [\"keep\"],\n                        \"arguments\": {\n                            \"a\": {\"name\": \"transformed_a\"},\n                            \"b\": {\"name\": \"transformed_b\"},\n                        },\n                    },\n                },\n                \"include_tags\": [\"keep\"],\n            },\n            \"test_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    client = Client[MCPConfigTransport](config)\n\n    async with client:\n        tools = await client.list_tools()\n        tools_by_name = {tool.name: tool for tool in tools}\n        assert len(tools) == 3\n        assert \"test_1_transformed_add\" in tools_by_name\n        assert \"test_1_add\" not in tools_by_name\n        assert \"test_1_subtract\" not in tools_by_name\n        assert \"test_2_add\" in tools_by_name\n        assert \"test_2_subtract\" in tools_by_name\n\n\n@pytest.mark.flaky(retries=3)\nasync def test_single_server_config_include_tags_filtering(tmp_path: Path):\n    \"\"\"include_tags should filter tools even with a single server in the config.\"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool(tags={\"keep\"})\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.tool\n        def subtract(a: int, b: int) -> int:\n            return a - b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n                \"include_tags\": [\"keep\"],\n            },\n        }\n    }\n\n    client = Client(config)\n\n    async with client:\n        tools = await client.list_tools()\n        tool_names = {tool.name for tool in tools}\n        assert \"add\" in tool_names\n        assert \"subtract\" not in tool_names\n\n\nasync def test_multi_client_with_elicitation(tmp_path: Path):\n    \"\"\"\n    Tests that elicitation is properly forwarded to the ultimate client.\n    \"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP, Context\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        async def elicit_test(ctx: Context) -> int:\n            result = await ctx.elicit('Pick a number', response_type=int)\n            return result.data\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"test_server\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"test_server_2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    async def elicitation_handler(message, response_type, params, ctx):\n        return response_type(value=42)\n\n    async with Client(config, elicitation_handler=elicitation_handler) as client:\n        result = await client.call_tool(\"test_server_elicit_test\", {})\n        assert result.data == 42\n\n\nasync def test_multi_server_config_transport(tmp_path: Path):\n    \"\"\"\n    Tests that MCPConfigTransport properly handles multi-server configurations.\n\n    Related to https://github.com/PrefectHQ/fastmcp/issues/2802 - verifies the\n    refactored architecture creates composite servers correctly.\n    \"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"greet_server.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"server1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"server2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    # Create client with multiple servers\n    client = Client(config)\n    assert isinstance(client.transport, MCPConfigTransport)\n\n    # Verify both servers are accessible via prefixed tool names\n    async with client:\n        tools = await client.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"server1_greet\" in tool_names\n        assert \"server2_greet\" in tool_names\n\n        # Call tools on both servers\n        result1 = await client.call_tool(\"server1_greet\", {\"name\": \"World\"})\n        assert isinstance(result1.content[0], TextContent)\n        assert \"Hello, World!\" in result1.content[0].text\n\n        result2 = await client.call_tool(\"server2_greet\", {\"name\": \"FastMCP\"})\n        assert isinstance(result2.content[0], TextContent)\n        assert \"Hello, FastMCP!\" in result2.content[0].text\n\n\nasync def test_multi_server_timeout_propagation():\n    \"\"\"Test that timeout is correctly propagated to proxy clients in multi-server configs.\"\"\"\n    # Create a config with multiple servers\n    config = MCPConfig(\n        mcpServers={\n            \"server1\": StdioMCPServer(command=\"echo\", args=[\"test\"]),\n            \"server2\": StdioMCPServer(command=\"echo\", args=[\"test\"]),\n        }\n    )\n\n    transport = MCPConfigTransport(config)\n    timeout = timedelta(seconds=42)\n\n    # Mock _create_proxy to avoid real stdio connections and verify timeout\n    mock_create_proxy = AsyncMock(\n        return_value=(AsyncMock(), AsyncMock(), FastMCP(name=\"MockProxy\"))\n    )\n\n    with (\n        patch.object(transport, \"_create_proxy\", mock_create_proxy),\n        patch(\n            \"fastmcp.client.transports.FastMCPTransport.connect_session\"\n        ) as mock_connect,\n    ):\n        mock_session = AsyncMock()\n        mock_connect.return_value.__aenter__ = AsyncMock(return_value=mock_session)\n        mock_connect.return_value.__aexit__ = AsyncMock(return_value=None)\n\n        async with transport.connect_session(read_timeout_seconds=timeout):\n            pass\n\n    # Verify _create_proxy was called with the timeout for each server\n    assert mock_create_proxy.call_count == 2\n    for call in mock_create_proxy.call_args_list:\n        # Third positional arg is timeout\n        call_timeout = call[0][2] if len(call[0]) > 2 else call.kwargs.get(\"timeout\")\n        assert call_timeout == timeout, (\n            f\"Expected timeout {timeout}, got {call_timeout}\"\n        )\n\n\nasync def test_multi_server_session_persistence(tmp_path: Path):\n    \"\"\"Test that session IDs persist across tool calls in multi-server mode.\n\n    Regression test for https://github.com/PrefectHQ/fastmcp/issues/2790 —\n    MCPConfigTransport was not connecting ProxyClients before mounting, so\n    each tool call opened a new session with the backend server.\n    \"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP, Context\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def get_session(ctx: Context) -> str:\n            return ctx.session_id\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"session_server.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"server1\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"server2\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n        }\n    }\n\n    client = Client(config)\n    async with client:\n        result1 = await client.call_tool(\"server1_get_session\", {})\n        assert isinstance(result1.content[0], TextContent)\n        session_id_1 = result1.content[0].text\n\n        result2 = await client.call_tool(\"server1_get_session\", {})\n        assert isinstance(result2.content[0], TextContent)\n        session_id_2 = result2.content[0].text\n\n        assert session_id_1 == session_id_2, (\n            f\"Session ID changed between calls: {session_id_1} != {session_id_2}\"\n        )\n\n\nasync def test_single_server_config_transport():\n    \"\"\"Test that single-server configs delegate directly without creating a composite.\"\"\"\n    config = MCPConfig(\n        mcpServers={\n            \"only_server\": StdioMCPServer(command=\"echo\", args=[\"test\"]),\n        }\n    )\n\n    transport = MCPConfigTransport(config)\n\n    # Single server should have transport created eagerly (not at connect time)\n    assert hasattr(transport, \"transport\")\n    assert isinstance(transport.transport, StdioTransport)\n\n    # _transports should already contain the single transport\n    assert len(transport._transports) == 1\n\n\n@pytest.mark.parametrize(\n    \"server_order\",\n    [\n        {\"good_server\": True, \"bad_server\": False},\n        {\"bad_server\": False, \"good_server\": True},\n    ],\n    ids=[\"good_first\", \"bad_first\"],\n)\nasync def test_multi_server_partial_failure(tmp_path: Path, server_order: dict):\n    \"\"\"When one server fails to connect, the others should still work.\"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    servers = {}\n    for name, is_good in server_order.items():\n        if is_good:\n            servers[name] = {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            }\n        else:\n            servers[name] = {\n                \"command\": \"this-command-does-not-exist-anywhere\",\n                \"args\": [],\n            }\n\n    client = Client({\"mcpServers\": servers})\n    async with client:\n        tools = await client.list_tools()\n        tool_names = [t.name for t in tools]\n        assert \"good_server_add\" in tool_names\n        assert len(tools) == 1\n\n\nasync def test_multi_server_partial_failure_logs_warning(tmp_path: Path, caplog):\n    \"\"\"A warning should be logged when a server fails to connect.\"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"good_server\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"bad_server\": {\n                \"command\": \"this-command-does-not-exist-anywhere\",\n                \"args\": [],\n            },\n        }\n    }\n\n    with caplog.at_level(logging.WARNING):\n        async with Client(config):\n            pass\n\n    warning_records = [\n        r\n        for r in caplog.records\n        if r.levelno == logging.WARNING and \"bad_server\" in r.message\n    ]\n    assert len(warning_records) == 1\n\n\nasync def test_multi_server_all_fail():\n    \"\"\"When all servers fail to connect, a ConnectionError should be raised.\"\"\"\n    config = MCPConfig(\n        mcpServers={\n            \"bad_1\": StdioMCPServer(\n                command=\"this-command-does-not-exist-anywhere\",\n                args=[],\n            ),\n            \"bad_2\": StdioMCPServer(\n                command=\"this-other-command-does-not-exist-either\",\n                args=[],\n            ),\n        }\n    )\n\n    transport = MCPConfigTransport(config)\n    with pytest.raises(ConnectionError, match=\"All MCP servers failed to connect\"):\n        async with transport.connect_session():\n            pass\n\n\nasync def test_multi_server_partial_failure_cleanup(tmp_path: Path):\n    \"\"\"Transports for failed servers should not leak into _transports.\"\"\"\n    server_script = inspect.cleandoc(\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def ping() -> str:\n            return \"pong\"\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n    script_path = tmp_path / \"test.py\"\n    script_path.write_text(server_script)\n\n    config = {\n        \"mcpServers\": {\n            \"working\": {\n                \"command\": \"python\",\n                \"args\": [str(script_path)],\n            },\n            \"broken\": {\n                \"command\": \"this-command-does-not-exist-anywhere\",\n                \"args\": [],\n            },\n        }\n    }\n\n    transport = MCPConfigTransport(config)\n    async with transport.connect_session():\n        assert len(transport._transports) == 1\n\n\ndef sample_tool_fn(arg1: int, arg2: str) -> str:\n    return f\"Hello, world! {arg1} {arg2}\"\n\n\n@pytest.fixture\ndef sample_tool() -> FastMCPTool:\n    return FastMCPTool.from_function(sample_tool_fn, name=\"sample_tool\")\n\n\n@pytest.fixture\nasync def test_script(tmp_path: Path) -> AsyncGenerator[Path, Any]:\n    with tempfile.NamedTemporaryFile() as f:\n        f.write(b\"\"\"\n        from fastmcp import FastMCP\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def fetch(url: str) -> str:\n\n            return f\"Hello, world! {url}\"\n\n        if __name__ == '__main__':\n            mcp.run()\n        \"\"\")\n\n        yield Path(f.name)\n\n    pass\n"
  },
  {
    "path": "tests/tools/__init__.py",
    "content": ""
  },
  {
    "path": "tests/tools/test_standalone_decorator.py",
    "content": "\"\"\"Tests for the standalone @tool decorator.\n\nThe @tool decorator attaches metadata to functions without registering them\nto a server. Functions can be added explicitly via server.add_tool() or\ndiscovered by FileSystemProvider.\n\"\"\"\n\nfrom typing import cast\n\nimport pytest\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.tools import tool\nfrom fastmcp.tools.function_tool import DecoratedTool, ToolMeta\n\n\nclass TestToolDecorator:\n    \"\"\"Tests for the @tool decorator.\"\"\"\n\n    def test_tool_without_parens(self):\n        \"\"\"@tool without parentheses should attach metadata.\"\"\"\n\n        @tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        decorated = cast(DecoratedTool, greet)\n        assert callable(greet)\n        assert hasattr(greet, \"__fastmcp__\")\n        assert isinstance(decorated.__fastmcp__, ToolMeta)\n        assert decorated.__fastmcp__.name is None  # Uses function name by default\n\n    def test_tool_with_empty_parens(self):\n        \"\"\"@tool() with empty parentheses should attach metadata.\"\"\"\n\n        @tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        decorated = cast(DecoratedTool, greet)\n        assert callable(greet)\n        assert hasattr(greet, \"__fastmcp__\")\n        assert isinstance(decorated.__fastmcp__, ToolMeta)\n\n    def test_tool_with_name_arg(self):\n        \"\"\"@tool(\"name\") with name as first arg should work.\"\"\"\n\n        @tool(\"custom-greet\")\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        decorated = cast(DecoratedTool, greet)\n        assert callable(greet)\n        assert hasattr(greet, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"custom-greet\"\n\n    def test_tool_with_name_kwarg(self):\n        \"\"\"@tool(name=\"name\") with keyword arg should work.\"\"\"\n\n        @tool(name=\"custom-greet\")\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        decorated = cast(DecoratedTool, greet)\n        assert callable(greet)\n        assert hasattr(greet, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"custom-greet\"\n\n    def test_tool_with_all_metadata(self):\n        \"\"\"@tool with all metadata should store it all.\"\"\"\n\n        @tool(\n            name=\"custom-greet\",\n            title=\"Greeting Tool\",\n            description=\"Greets people\",\n            tags={\"greeting\", \"demo\"},\n            meta={\"custom\": \"value\"},\n        )\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        decorated = cast(DecoratedTool, greet)\n        assert callable(greet)\n        assert hasattr(greet, \"__fastmcp__\")\n        assert decorated.__fastmcp__.name == \"custom-greet\"\n        assert decorated.__fastmcp__.title == \"Greeting Tool\"\n        assert decorated.__fastmcp__.description == \"Greets people\"\n        assert decorated.__fastmcp__.tags == {\"greeting\", \"demo\"}\n        assert decorated.__fastmcp__.meta == {\"custom\": \"value\"}\n\n    async def test_tool_function_still_callable(self):\n        \"\"\"Decorated function should still be directly callable.\"\"\"\n\n        @tool\n        def greet(name: str) -> str:\n            \"\"\"Greet someone.\"\"\"\n            return f\"Hello, {name}!\"\n\n        # The function is still callable even though it has metadata\n        result = cast(DecoratedTool, greet)(\"World\")\n        assert result == \"Hello, World!\"\n\n    def test_tool_rejects_classmethod_decorator(self):\n        \"\"\"@tool should reject classmethod-decorated functions.\"\"\"\n        with pytest.raises(TypeError, match=\"classmethod\"):\n\n            class MyClass:\n                @tool\n                @classmethod\n                def my_method(cls) -> str:\n                    return \"hello\"\n\n    def test_tool_with_both_name_args_raises(self):\n        \"\"\"@tool should raise if both positional and keyword name are given.\"\"\"\n        with pytest.raises(TypeError, match=\"Cannot specify.*both.*argument.*keyword\"):\n\n            @tool(\"name1\", name=\"name2\")  # type: ignore[call-overload]\n            def my_tool() -> str:\n                return \"hello\"\n\n    async def test_tool_added_to_server(self):\n        \"\"\"Tool created by @tool should work when added to a server.\"\"\"\n\n        @tool\n        def greet(name: str) -> str:\n            \"\"\"Greet someone.\"\"\"\n            return f\"Hello, {name}!\"\n\n        mcp = FastMCP(\"Test\")\n        mcp.add_tool(greet)\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert any(t.name == \"greet\" for t in tools)\n\n            result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n            assert result.data == \"Hello, World!\"\n"
  },
  {
    "path": "tests/tools/test_tool_future_annotations.py",
    "content": "from __future__ import annotations\n\nfrom typing import Annotated, Any, Literal, cast\n\nimport mcp.types\nfrom pydantic import Field\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.client import Client\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.utilities.types import Image\n\nfastmcp_server = FastMCP()\n\n\n@fastmcp_server.tool\ndef simple_with_context(ctx: Context) -> str:\n    \"\"\"Simple tool with context parameter.\"\"\"\n    return f\"Request ID: {ctx.request_id}\"\n\n\n@fastmcp_server.tool\ndef complex_types(\n    data: dict[str, Any], items: list[int], ctx: Context\n) -> dict[str, str | int]:\n    \"\"\"Tool with complex type annotations.\"\"\"\n    return {\"count\": len(items), \"request_id\": ctx.request_id}\n\n\n@fastmcp_server.tool\ndef optional_context(name: str, ctx: Context | None = None) -> str:\n    \"\"\"Tool with optional context.\"\"\"\n    if ctx:\n        return f\"Hello {name} from request {ctx.request_id}\"\n    return f\"Hello {name}\"\n\n\n@fastmcp_server.tool\ndef union_with_context(value: int | str, ctx: Context) -> ToolResult:\n    \"\"\"Tool returning ToolResult with context.\"\"\"\n    return ToolResult(content=f\"Value: {value}, Request: {ctx.request_id}\")\n\n\n@fastmcp_server.tool\ndef returns_image(ctx: Context) -> Image:\n    \"\"\"Tool that returns an Image.\"\"\"\n    # Create a simple 1x1 white pixel PNG\n    png_data = b\"\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x08\\x02\\x00\\x00\\x00\\x90wS\\xde\\x00\\x00\\x00\\x0cIDATx\\x9cc\\xf8\\x0f\\x00\\x00\\x01\\x01\\x00\\x05\\x18\\xd4c\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\"\n    return Image(data=png_data, format=\"png\")\n\n\n@fastmcp_server.tool\nasync def async_with_context(ctx: Context) -> str:\n    \"\"\"Async tool with context.\"\"\"\n    return f\"Async request: {ctx.request_id}\"\n\n\n@fastmcp_server.tool\ndef annotated_with_context(\n    query: Annotated[str, Field(description=\"Search query\")], ctx: Context\n) -> str:\n    \"\"\"Tool using Annotated + Field with context.\"\"\"\n    return f\"Result for: {query}\"\n\n\n@fastmcp_server.tool\ndef literal_with_context(mode: Literal[\"fast\", \"slow\"], ctx: Context) -> str:\n    \"\"\"Tool using Literal with context.\"\"\"\n    return f\"Mode: {mode}\"\n\n\nclass TestFutureAnnotations:\n    async def test_simple_with_context(self):\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\"simple_with_context\", {})\n            assert \"Request ID:\" in cast(mcp.types.TextContent, result.content[0]).text\n\n    async def test_complex_types(self):\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\n                \"complex_types\", {\"data\": {\"key\": \"value\"}, \"items\": [1, 2, 3]}\n            )\n            # Check the result is valid JSON with expected values\n            import json\n\n            data = json.loads(cast(mcp.types.TextContent, result.content[0]).text)\n            assert data[\"count\"] == 3\n            assert \"request_id\" in data\n\n    async def test_optional_context(self):\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\"optional_context\", {\"name\": \"World\"})\n            assert (\n                \"Hello World from request\"\n                in cast(mcp.types.TextContent, result.content[0]).text\n            )\n\n    async def test_union_with_context(self):\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\"union_with_context\", {\"value\": 42})\n            assert (\n                \"Value: 42, Request:\"\n                in cast(mcp.types.TextContent, result.content[0]).text\n            )\n\n    async def test_returns_image(self):\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\"returns_image\", {})\n            assert result.content[0].type == \"image\"\n            assert result.content[0].mimeType == \"image/png\"\n\n    async def test_async_with_context(self):\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\"async_with_context\", {})\n            assert (\n                \"Async request:\" in cast(mcp.types.TextContent, result.content[0]).text\n            )\n\n    async def test_annotated_with_context(self):\n        \"\"\"Test Annotated[str, Field(...)] works with Context and future annotations.\"\"\"\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\n                \"annotated_with_context\", {\"query\": \"hello\"}\n            )\n            assert (\n                \"Result for: hello\"\n                in cast(mcp.types.TextContent, result.content[0]).text\n            )\n\n    async def test_literal_with_context(self):\n        \"\"\"Test Literal types work with Context and future annotations.\"\"\"\n        async with Client(fastmcp_server) as client:\n            result = await client.call_tool(\"literal_with_context\", {\"mode\": \"fast\"})\n            assert \"Mode: fast\" in cast(mcp.types.TextContent, result.content[0]).text\n\n    async def test_modern_union_syntax_works(self):\n        \"\"\"Test that modern | union syntax works with future annotations.\"\"\"\n        # This demonstrates that our solution works with | syntax when types\n        # are available in module globals\n\n        # Define a tool with modern union syntax\n        @fastmcp_server.tool\n        def modern_union_tool(value: str | int | None) -> str | None:\n            \"\"\"Tool using modern | union syntax throughout.\"\"\"\n            if value is None:\n                return None\n            return f\"processed: {value}\"\n\n        async with Client(fastmcp_server) as client:\n            # Test with string\n            result = await client.call_tool(\"modern_union_tool\", {\"value\": \"hello\"})\n            assert (\n                \"processed: hello\"\n                in cast(mcp.types.TextContent, result.content[0]).text\n            )\n\n            # Test with int\n            result = await client.call_tool(\"modern_union_tool\", {\"value\": 42})\n            assert (\n                \"processed: 42\" in cast(mcp.types.TextContent, result.content[0]).text\n            )\n\n            # Test with None\n            result = await client.call_tool(\"modern_union_tool\", {\"value\": None})\n            # When function returns None, FastMCP returns empty content\n            assert (\n                len(result.content) == 0\n                or cast(mcp.types.TextContent, result.content[0]).text == \"null\"\n            )\n\n\ndef test_closure_scoped_types_with_builtins():\n    \"\"\"Closure-scoped tools work when annotations only reference builtins.\"\"\"\n\n    def create_closure():\n        mcp = FastMCP()\n\n        @mcp.tool\n        def closure_tool(value: str | None) -> str:\n            return str(value)\n\n        return mcp\n\n    create_closure()\n"
  },
  {
    "path": "tests/tools/test_tool_timeout.py",
    "content": "\"\"\"Tests for tool timeout functionality.\"\"\"\n\nimport time\n\nimport anyio\nimport pytest\nfrom mcp.shared.exceptions import McpError\nfrom mcp.types import TextContent\n\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import ToolError\n\n\nclass TestToolTimeout:\n    \"\"\"Test tool timeout behavior.\"\"\"\n\n    async def test_no_timeout_completes_normally_async(self):\n        \"\"\"Tool without timeout completes normally (async).\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        async def quick_async_tool() -> str:\n            await anyio.sleep(0.01)\n            return \"completed\"\n\n        result = await mcp.call_tool(\"quick_async_tool\")\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"completed\"\n\n    async def test_no_timeout_completes_normally_sync(self):\n        \"\"\"Tool without timeout completes normally (sync).\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def quick_sync_tool() -> str:\n            time.sleep(0.01)\n            return \"completed\"\n\n        result = await mcp.call_tool(\"quick_sync_tool\")\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"completed\"\n\n    async def test_timeout_not_reached_async(self):\n        \"\"\"Async tool with timeout completes before timeout.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=5.0)\n        async def fast_async_tool() -> str:\n            await anyio.sleep(0.1)\n            return \"completed\"\n\n        result = await mcp.call_tool(\"fast_async_tool\")\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"completed\"\n\n    async def test_timeout_not_reached_sync(self):\n        \"\"\"Sync tool with timeout completes before timeout.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=5.0)\n        def fast_sync_tool() -> str:\n            time.sleep(0.1)\n            return \"completed\"\n\n        result = await mcp.call_tool(\"fast_sync_tool\")\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"completed\"\n\n    async def test_async_timeout_exceeded(self):\n        \"\"\"Async tool exceeds timeout and raises TimeoutError.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=0.2)\n        async def slow_async_tool() -> str:\n            await anyio.sleep(2.0)\n            return \"should not reach\"\n\n        # TimeoutError is caught and converted to ToolError by FastMCP\n        with pytest.raises(ToolError) as exc_info:\n            await mcp.call_tool(\"slow_async_tool\")\n\n        # Verify the tool raised an error (error message may be masked)\n        assert exc_info.value is not None\n\n    async def test_sync_timeout_exceeded(self):\n        \"\"\"Sync tool timeout works with CPU-bound operations.\"\"\"\n        pytest.skip(\n            \"Sync timeouts require thread pool execution (coming in future commit)\"\n        )\n        # Note: time.sleep() blocks the event loop and cannot be interrupted\n        # by anyio.fail_after(). This will work once sync functions run in\n        # thread pools (commit 8c471a49).\n\n    async def test_timeout_error_raises_tool_error(self):\n        \"\"\"Timeout error is converted to ToolError and logs warning.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=0.1)\n        async def slow_tool() -> str:\n            await anyio.sleep(1.0)\n            return \"never\"\n\n        # Verify that ToolError is raised (timeout warning is logged to stderr)\n        with pytest.raises(ToolError):\n            await mcp.call_tool(\"slow_tool\")\n\n    async def test_timeout_from_tool_from_function(self):\n        \"\"\"Timeout works when using Tool.from_function().\"\"\"\n        from fastmcp.tools import Tool\n\n        async def my_slow_tool() -> str:\n            await anyio.sleep(1.0)\n            return \"never\"\n\n        tool = Tool.from_function(my_slow_tool, timeout=0.1)\n        mcp = FastMCP()\n        mcp.add_tool(tool)\n\n        with pytest.raises(ToolError):\n            await mcp.call_tool(\"my_slow_tool\")\n\n    async def test_timeout_zero_times_out_immediately(self):\n        \"\"\"Timeout of 0 times out immediately.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=0.0)\n        async def instant_timeout() -> str:\n            await anyio.sleep(0)  # Give the event loop a chance to check timeout\n            return \"never\"\n\n        with pytest.raises(ToolError):\n            await mcp.call_tool(\"instant_timeout\")\n\n    async def test_timeout_with_task_mode(self):\n        \"\"\"Tool with timeout and task mode can be configured together.\"\"\"\n        mcp = FastMCP(tasks=True)\n\n        @mcp.tool(task=True, timeout=1.0)\n        async def task_with_timeout() -> str:\n            await anyio.sleep(0.1)\n            return \"completed\"\n\n        # Tool should be registered successfully\n        tools = await mcp.list_tools()\n        tool = next((t for t in tools if t.name == \"task_with_timeout\"), None)\n        assert tool is not None\n        assert tool.timeout == 1.0\n        assert tool.task_config.supports_tasks()\n\n    async def test_multiple_tools_with_different_timeouts(self):\n        \"\"\"Multiple tools can have different timeout values.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=1.0)\n        async def short_timeout() -> str:\n            await anyio.sleep(0.1)\n            return \"short\"\n\n        @mcp.tool(timeout=5.0)\n        async def long_timeout() -> str:\n            await anyio.sleep(0.1)\n            return \"long\"\n\n        @mcp.tool\n        async def no_timeout() -> str:\n            await anyio.sleep(0.1)\n            return \"none\"\n\n        # All should complete successfully\n        result1 = await mcp.call_tool(\"short_timeout\")\n        result2 = await mcp.call_tool(\"long_timeout\")\n        result3 = await mcp.call_tool(\"no_timeout\")\n\n        assert isinstance(result1.content[0], TextContent)\n        assert isinstance(result2.content[0], TextContent)\n        assert isinstance(result3.content[0], TextContent)\n        assert result1.content[0].text == \"short\"\n        assert result2.content[0].text == \"long\"\n        assert result3.content[0].text == \"none\"\n\n    async def test_timeout_error_converted_to_tool_error(self):\n        \"\"\"Timeout errors are converted to ToolError by FastMCP.\"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool(timeout=0.1)\n        async def times_out() -> str:\n            await anyio.sleep(1.0)\n            return \"never\"\n\n        # TimeoutError should be caught and converted to ToolError\n        with pytest.raises((ToolError, McpError)):\n            await mcp.call_tool(\"times_out\")\n"
  },
  {
    "path": "tests/tools/tool/__init__.py",
    "content": ""
  },
  {
    "path": "tests/tools/tool/test_callable.py",
    "content": "import asyncio\nimport threading\n\nfrom mcp.types import TextContent\n\nfrom fastmcp import Context, FastMCP\nfrom fastmcp.tools.base import Tool\n\n\nclass TestToolCallable:\n    \"\"\"Test tools with callable objects.\"\"\"\n\n    async def test_callable_object_sync(self):\n        \"\"\"Test that callable objects with sync __call__ work.\"\"\"\n\n        class MyTool:\n            def __init__(self, multiplier: int):\n                self.multiplier = multiplier\n\n            def __call__(self, x: int) -> int:\n                return x * self.multiplier\n\n        tool = Tool.from_function(MyTool(3))\n        result = await tool.run({\"x\": 5})\n        assert result.content == [TextContent(type=\"text\", text=\"15\")]\n\n    async def test_callable_object_async(self):\n        \"\"\"Test that callable objects with async __call__ work.\"\"\"\n\n        class AsyncTool:\n            def __init__(self, multiplier: int):\n                self.multiplier = multiplier\n\n            async def __call__(self, x: int) -> int:\n                return x * self.multiplier\n\n        tool = Tool.from_function(AsyncTool(4))\n        result = await tool.run({\"x\": 5})\n        assert result.content == [TextContent(type=\"text\", text=\"20\")]\n\n\nclass TestSyncToolConcurrency:\n    \"\"\"Tests for concurrent execution of sync tools without blocking the event loop.\"\"\"\n\n    async def test_sync_tools_run_concurrently(self):\n        \"\"\"Test that sync tools run in threadpool and don't block each other.\n\n        Uses a threading barrier to prove concurrent execution: all calls must\n        reach the barrier simultaneously for any to proceed. If they ran\n        sequentially, only one would reach the barrier and it would time out.\n        \"\"\"\n        num_calls = 3\n        # Barrier requires all threads to arrive before any proceed\n        # Short timeout since concurrent threads should arrive within milliseconds\n        barrier = threading.Barrier(num_calls, timeout=0.5)\n\n        def concurrent_tool(x: int) -> int:\n            \"\"\"Tool that proves concurrency via barrier synchronization.\"\"\"\n            # If calls run sequentially, only 1 thread reaches barrier and times out\n            # If calls run concurrently, all 3 reach barrier and proceed\n            barrier.wait()\n            return x * 2\n\n        tool = Tool.from_function(concurrent_tool)\n\n        # Run concurrent calls - will raise BrokenBarrierError if not concurrent\n        results = await asyncio.gather(\n            tool.run({\"x\": 1}),\n            tool.run({\"x\": 2}),\n            tool.run({\"x\": 3}),\n        )\n\n        # Verify results\n        assert [r.content for r in results] == [\n            [TextContent(type=\"text\", text=\"2\")],\n            [TextContent(type=\"text\", text=\"4\")],\n            [TextContent(type=\"text\", text=\"6\")],\n        ]\n\n    async def test_sync_tool_with_context_runs_concurrently(self):\n        \"\"\"Test that sync tools with Context dependency also run concurrently.\"\"\"\n        num_calls = 3\n        barrier = threading.Barrier(num_calls, timeout=0.5)\n\n        mcp = FastMCP(\"test\")\n\n        @mcp.tool\n        def ctx_tool(x: int, ctx: Context) -> str:\n            \"\"\"A sync tool with context that uses barrier to prove concurrency.\"\"\"\n            barrier.wait()\n            return f\"{ctx.fastmcp.name}:{x}\"\n\n        # Run concurrent calls through the server interface (which sets up Context)\n        results = await asyncio.gather(\n            mcp.call_tool(\"ctx_tool\", {\"x\": 1}),\n            mcp.call_tool(\"ctx_tool\", {\"x\": 2}),\n            mcp.call_tool(\"ctx_tool\", {\"x\": 3}),\n        )\n\n        # Verify results\n        for i, result in enumerate(results, 1):\n            assert result.content == [TextContent(type=\"text\", text=f\"test:{i}\")]\n"
  },
  {
    "path": "tests/tools/tool/test_content.py",
    "content": "from dataclasses import dataclass\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp.types import (\n    AudioContent,\n    BlobResourceContents,\n    EmbeddedResource,\n    ImageContent,\n    ResourceLink,\n    TextContent,\n    TextResourceContents,\n)\nfrom pydantic import AnyUrl, BaseModel\n\nfrom fastmcp.tools.base import Tool, _convert_to_content\nfrom fastmcp.utilities.types import Audio, File, Image\n\n\nclass SampleModel(BaseModel):\n    x: int\n    y: str\n\n\nclass TestConvertResultToContent:\n    \"\"\"Tests for the _convert_to_content helper function.\"\"\"\n\n    @pytest.mark.parametrize(\n        argnames=(\"result\", \"expected\"),\n        argvalues=[\n            (True, \"true\"),\n            (\"hello\", \"hello\"),\n            (123, \"123\"),\n            (123.45, \"123.45\"),\n            ({\"key\": \"value\"}, '{\"key\":\"value\"}'),\n            (\n                SampleModel(x=1, y=\"hello\"),\n                '{\"x\":1,\"y\":\"hello\"}',\n            ),\n        ],\n        ids=[\n            \"boolean\",\n            \"string\",\n            \"integer\",\n            \"float\",\n            \"object\",\n            \"basemodel\",\n        ],\n    )\n    def test_convert_singular(self, result, expected):\n        \"\"\"Test that a single item is converted to a TextContent.\"\"\"\n        converted = _convert_to_content(result)\n        assert converted == [TextContent(type=\"text\", text=expected)]\n\n    @pytest.mark.parametrize(\n        argnames=(\"result\", \"expected_text\"),\n        argvalues=[\n            ([None], \"[null]\"),\n            ([None, None], \"[null,null]\"),\n            ([True], \"[true]\"),\n            ([True, False], \"[true,false]\"),\n            ([\"hello\"], '[\"hello\"]'),\n            ([\"hello\", \"world\"], '[\"hello\",\"world\"]'),\n            ([123], \"[123]\"),\n            ([123, 456], \"[123,456]\"),\n            ([123.45], \"[123.45]\"),\n            ([123.45, 456.78], \"[123.45,456.78]\"),\n            ([{\"key\": \"value\"}], '[{\"key\":\"value\"}]'),\n            (\n                [{\"key\": \"value\"}, {\"key2\": \"value2\"}],\n                '[{\"key\":\"value\"},{\"key2\":\"value2\"}]',\n            ),\n            ([SampleModel(x=1, y=\"hello\")], '[{\"x\":1,\"y\":\"hello\"}]'),\n            (\n                [SampleModel(x=1, y=\"hello\"), SampleModel(x=2, y=\"world\")],\n                '[{\"x\":1,\"y\":\"hello\"},{\"x\":2,\"y\":\"world\"}]',\n            ),\n            ([1, \"two\", None, {\"c\": 3}, False], '[1,\"two\",null,{\"c\":3},false]'),\n        ],\n        ids=[\n            \"none\",\n            \"none_many\",\n            \"boolean\",\n            \"boolean_many\",\n            \"string\",\n            \"string_many\",\n            \"integer\",\n            \"integer_many\",\n            \"float\",\n            \"float_many\",\n            \"object\",\n            \"object_many\",\n            \"basemodel\",\n            \"basemodel_many\",\n            \"mixed\",\n        ],\n    )\n    def test_convert_list(self, result, expected_text):\n        \"\"\"Test that a list is converted to a TextContent.\"\"\"\n        converted = _convert_to_content(result)\n        assert converted == [TextContent(type=\"text\", text=expected_text)]\n\n    @pytest.mark.parametrize(\n        argnames=\"content_block\",\n        argvalues=[\n            (TextContent(type=\"text\", text=\"hello\")),\n            (ImageContent(type=\"image\", data=\"fakeimagedata\", mimeType=\"image/png\")),\n            (AudioContent(type=\"audio\", data=\"fakeaudiodata\", mimeType=\"audio/mpeg\")),\n            (\n                ResourceLink(\n                    type=\"resource_link\",\n                    name=\"test resource\",\n                    uri=AnyUrl(\"resource://test\"),\n                )\n            ),\n            (\n                EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=AnyUrl(\"resource://test\"),\n                        mimeType=\"text/plain\",\n                        text=\"resource content\",\n                    ),\n                )\n            ),\n        ],\n        ids=[\"text\", \"image\", \"audio\", \"resource link\", \"embedded resource\"],\n    )\n    def test_convert_content_block(self, content_block):\n        converted = _convert_to_content(content_block)\n        assert converted == [content_block]\n\n        converted = _convert_to_content([content_block, content_block])\n        assert converted == [content_block, content_block]\n\n    @pytest.mark.parametrize(\n        argnames=(\"result\", \"expected\"),\n        argvalues=[\n            (\n                Image(data=b\"fakeimagedata\"),\n                [\n                    ImageContent(\n                        type=\"image\", data=\"ZmFrZWltYWdlZGF0YQ==\", mimeType=\"image/png\"\n                    )\n                ],\n            ),\n            (\n                Audio(data=b\"fakeaudiodata\"),\n                [\n                    AudioContent(\n                        type=\"audio\", data=\"ZmFrZWF1ZGlvZGF0YQ==\", mimeType=\"audio/wav\"\n                    )\n                ],\n            ),\n            (\n                File(data=b\"filedata\", format=\"octet-stream\"),\n                [\n                    EmbeddedResource(\n                        type=\"resource\",\n                        resource=BlobResourceContents(\n                            uri=AnyUrl(\"file:///resource.octet-stream\"),\n                            blob=\"ZmlsZWRhdGE=\",\n                            mimeType=\"application/octet-stream\",\n                        ),\n                    )\n                ],\n            ),\n        ],\n        ids=[\"image\", \"audio\", \"file\"],\n    )\n    def test_convert_helpers(self, result, expected):\n        converted = _convert_to_content(result)\n        assert converted == expected\n\n        converted = _convert_to_content([result, result])\n        assert converted == expected * 2\n\n    def test_convert_mixed_content(self):\n        result = [\n            \"hello\",\n            123,\n            123.45,\n            {\"key\": \"value\"},\n            SampleModel(x=1, y=\"hello\"),\n            Image(data=b\"fakeimagedata\"),\n            Audio(data=b\"fakeaudiodata\"),\n            ResourceLink(\n                type=\"resource_link\",\n                name=\"test resource\",\n                uri=AnyUrl(\"resource://test\"),\n            ),\n            EmbeddedResource(\n                type=\"resource\",\n                resource=TextResourceContents(\n                    uri=AnyUrl(\"resource://test\"),\n                    mimeType=\"text/plain\",\n                    text=\"resource content\",\n                ),\n            ),\n        ]\n\n        converted = _convert_to_content(result)\n\n        assert converted == snapshot(\n            [\n                TextContent(type=\"text\", text=\"hello\"),\n                TextContent(type=\"text\", text=\"123\"),\n                TextContent(type=\"text\", text=\"123.45\"),\n                TextContent(type=\"text\", text='{\"key\":\"value\"}'),\n                TextContent(type=\"text\", text='{\"x\":1,\"y\":\"hello\"}'),\n                ImageContent(\n                    type=\"image\", data=\"ZmFrZWltYWdlZGF0YQ==\", mimeType=\"image/png\"\n                ),\n                AudioContent(\n                    type=\"audio\", data=\"ZmFrZWF1ZGlvZGF0YQ==\", mimeType=\"audio/wav\"\n                ),\n                ResourceLink(\n                    name=\"test resource\",\n                    uri=AnyUrl(\"resource://test\"),\n                    type=\"resource_link\",\n                ),\n                EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=AnyUrl(\"resource://test\"),\n                        mimeType=\"text/plain\",\n                        text=\"resource content\",\n                    ),\n                ),\n            ]\n        )\n\n    def test_empty_list(self):\n        \"\"\"Test that an empty list results in an empty list.\"\"\"\n        result = _convert_to_content([])\n        assert isinstance(result, list)\n        assert len(result) == 0\n\n    def test_empty_dict(self):\n        \"\"\"Test that an empty dictionary is converted to TextContent.\"\"\"\n        result = _convert_to_content({})\n        assert isinstance(result, list)\n        assert len(result) == 1\n        assert isinstance(result[0], TextContent)\n        assert result[0].text == \"{}\"\n\n\nclass TestAutomaticStructuredContent:\n    \"\"\"Tests for automatic structured content generation based on return types.\"\"\"\n\n    async def test_dict_return_creates_structured_content_without_schema(self):\n        \"\"\"Test that dict returns automatically create structured content even without output schema.\"\"\"\n\n        def get_user_data(user_id: str) -> dict:\n            return {\"name\": \"Alice\", \"age\": 30, \"active\": True}\n\n        # No explicit output schema provided\n        tool = Tool.from_function(get_user_data)\n\n        result = await tool.run({\"user_id\": \"123\"})\n\n        # Should have both content and structured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.structured_content == {\"name\": \"Alice\", \"age\": 30, \"active\": True}\n\n    async def test_dataclass_return_creates_structured_content_without_schema(self):\n        \"\"\"Test that dataclass returns automatically create structured content even without output schema.\"\"\"\n\n        @dataclass\n        class UserProfile:\n            name: str\n            age: int\n            email: str\n\n        def get_profile(user_id: str) -> UserProfile:\n            return UserProfile(name=\"Bob\", age=25, email=\"bob@example.com\")\n\n        # No explicit output schema, but dataclass should still create structured content\n        tool = Tool.from_function(get_profile, output_schema=None)\n\n        result = await tool.run({\"user_id\": \"456\"})\n\n        # Should have both content and structured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        # Dataclass should serialize to dict\n        assert result.structured_content == {\n            \"name\": \"Bob\",\n            \"age\": 25,\n            \"email\": \"bob@example.com\",\n        }\n\n    async def test_pydantic_model_return_creates_structured_content_without_schema(\n        self,\n    ):\n        \"\"\"Test that Pydantic model returns automatically create structured content even without output schema.\"\"\"\n\n        class UserData(BaseModel):\n            username: str\n            score: int\n            verified: bool\n\n        def get_user_stats(user_id: str) -> UserData:\n            return UserData(username=\"charlie\", score=100, verified=True)\n\n        # Explicitly set output schema to None to test automatic structured content\n        tool = Tool.from_function(get_user_stats, output_schema=None)\n\n        result = await tool.run({\"user_id\": \"789\"})\n\n        # Should have both content and structured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        # Pydantic model should serialize to dict\n        assert result.structured_content == {\n            \"username\": \"charlie\",\n            \"score\": 100,\n            \"verified\": True,\n        }\n\n    async def test_self_referencing_dataclass_not_wrapped(self):\n        \"\"\"Test that self-referencing dataclasses are not wrapped in result field.\"\"\"\n\n        @dataclass\n        class ReturnThing:\n            value: int\n            stuff: list[\"ReturnThing\"]\n\n        def return_things() -> ReturnThing:\n            return ReturnThing(value=123, stuff=[ReturnThing(value=456, stuff=[])])\n\n        tool = Tool.from_function(return_things)\n\n        result = await tool.run({})\n\n        # Should have structured content without wrapping\n        assert result.structured_content is not None\n        # Should NOT be wrapped in \"result\" field\n        assert \"result\" not in result.structured_content\n        # Should have the actual data directly\n        assert result.structured_content == {\n            \"value\": 123,\n            \"stuff\": [{\"value\": 456, \"stuff\": []}],\n        }\n\n    async def test_self_referencing_pydantic_model_has_type_object_at_root(self):\n        \"\"\"Test that self-referencing Pydantic models have type: object at root.\n\n        MCP spec requires outputSchema to have \"type\": \"object\" at the root level.\n        Pydantic generates schemas with $ref at root for self-referential models,\n        which violates this requirement. FastMCP should resolve the $ref.\n\n        Regression test for issue #2455.\n        \"\"\"\n\n        class Issue(BaseModel):\n            id: str\n            title: str\n            dependencies: list[\"Issue\"] = []\n            dependents: list[\"Issue\"] = []\n\n        def get_issue(issue_id: str) -> Issue:\n            return Issue(id=issue_id, title=\"Test\")\n\n        tool = Tool.from_function(get_issue)\n\n        # The output schema should have \"type\": \"object\" at root, not $ref\n        assert tool.output_schema is not None\n        assert tool.output_schema.get(\"type\") == \"object\"\n        assert \"properties\" in tool.output_schema\n        # Should still have $defs for nested references\n        assert \"$defs\" in tool.output_schema\n        # Should NOT have $ref at root level\n        assert \"$ref\" not in tool.output_schema\n\n    async def test_self_referencing_model_outputschema_mcp_compliant(self):\n        \"\"\"Test that self-referencing model schemas are MCP spec compliant.\n\n        The MCP spec requires:\n        - type: \"object\" at root level\n        - properties field\n        - required field (optional)\n\n        This ensures clients can properly validate the schema.\n\n        Regression test for issue #2455.\n        \"\"\"\n\n        class Node(BaseModel):\n            id: str\n            children: list[\"Node\"] = []\n\n        def get_node() -> Node:\n            return Node(id=\"1\")\n\n        tool = Tool.from_function(get_node)\n\n        # Schema should be MCP-compliant\n        assert tool.output_schema is not None\n        assert tool.output_schema.get(\"type\") == \"object\", (\n            \"MCP spec requires 'type': 'object' at root\"\n        )\n        assert \"properties\" in tool.output_schema\n        assert \"id\" in tool.output_schema[\"properties\"]\n        assert \"children\" in tool.output_schema[\"properties\"]\n        # Required should include 'id'\n        assert \"id\" in tool.output_schema.get(\"required\", [])\n\n    async def test_int_return_no_structured_content_without_schema(self):\n        \"\"\"Test that int returns don't create structured content without output schema.\"\"\"\n\n        def calculate_sum(a: int, b: int):\n            \"\"\"No return annotation.\"\"\"\n            return a + b\n\n        # No output schema\n        tool = Tool.from_function(calculate_sum)\n\n        result = await tool.run({\"a\": 5, \"b\": 3})\n\n        # Should only have content, no structured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"8\"\n        assert result.structured_content is None\n\n    async def test_str_return_no_structured_content_without_schema(self):\n        \"\"\"Test that str returns don't create structured content without output schema.\"\"\"\n\n        def get_greeting(name: str):\n            \"\"\"No return annotation.\"\"\"\n            return f\"Hello, {name}!\"\n\n        # No output schema\n        tool = Tool.from_function(get_greeting)\n\n        result = await tool.run({\"name\": \"World\"})\n\n        # Should only have content, no structured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Hello, World!\"\n        assert result.structured_content is None\n\n    async def test_list_return_no_structured_content_without_schema(self):\n        \"\"\"Test that list returns don't create structured content without output schema.\"\"\"\n\n        def get_numbers():\n            \"\"\"No return annotation.\"\"\"\n            return [1, 2, 3, 4, 5]\n\n        # No output schema\n        tool = Tool.from_function(get_numbers)\n\n        result = await tool.run({})\n\n        assert result.structured_content is None\n        assert result.content == snapshot(\n            [TextContent(type=\"text\", text=\"[1,2,3,4,5]\")]\n        )\n\n    async def test_audio_return_creates_no_structured_content(self):\n        \"\"\"Test that audio returns don't create structured content.\"\"\"\n\n        def get_audio() -> AudioContent:\n            \"\"\"No return annotation.\"\"\"\n            return Audio(data=b\"fakeaudiodata\").to_audio_content()\n\n        # No output schema\n        tool = Tool.from_function(get_audio)\n\n        result = await tool.run({})\n\n        assert result.content == snapshot(\n            [\n                AudioContent(\n                    type=\"audio\", data=\"ZmFrZWF1ZGlvZGF0YQ==\", mimeType=\"audio/wav\"\n                )\n            ]\n        )\n        assert result.structured_content is None\n\n    async def test_int_return_with_schema_creates_structured_content(self):\n        \"\"\"Test that int returns DO create structured content when there's an output schema.\"\"\"\n\n        def calculate_sum(a: int, b: int) -> int:\n            \"\"\"With return annotation.\"\"\"\n            return a + b\n\n        # Output schema should be auto-generated from annotation\n        tool = Tool.from_function(calculate_sum)\n        assert tool.output_schema is not None\n\n        result = await tool.run({\"a\": 5, \"b\": 3})\n\n        # Should have both content and structured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"8\"\n        assert result.structured_content == {\"result\": 8}\n\n    async def test_client_automatic_deserialization_with_dict_result(self):\n        \"\"\"Test that clients automatically deserialize dict results from structured content.\"\"\"\n        from fastmcp import FastMCP\n        from fastmcp.client import Client\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def get_user_info(user_id: str) -> dict:\n            return {\"name\": \"Alice\", \"age\": 30, \"active\": True}\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_user_info\", {\"user_id\": \"123\"})\n\n            # Client should provide the deserialized data\n            assert result.data == {\"name\": \"Alice\", \"age\": 30, \"active\": True}\n            assert result.structured_content == {\n                \"name\": \"Alice\",\n                \"age\": 30,\n                \"active\": True,\n            }\n            assert len(result.content) == 1\n\n    async def test_client_automatic_deserialization_with_dataclass_result(self):\n        \"\"\"Test that clients automatically deserialize dataclass results from structured content.\"\"\"\n        from fastmcp import FastMCP\n        from fastmcp.client import Client\n\n        mcp = FastMCP()\n\n        @dataclass\n        class UserProfile:\n            name: str\n            age: int\n            verified: bool\n\n        @mcp.tool\n        def get_profile(user_id: str) -> UserProfile:\n            return UserProfile(name=\"Bob\", age=25, verified=True)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_profile\", {\"user_id\": \"456\"})\n\n            # Client should deserialize back to a dataclass (but type name is lost with title pruning)\n            assert result.data.__class__.__name__ == \"Root\"\n            assert result.data.name == \"Bob\"\n            assert result.data.age == 25\n            assert result.data.verified is True\n"
  },
  {
    "path": "tests/tools/tool/test_output_schema.py",
    "content": "from dataclasses import dataclass\nfrom typing import Annotated, Any\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp.types import AudioContent, EmbeddedResource, ImageContent, TextContent\nfrom pydantic import AnyUrl, BaseModel, Field, TypeAdapter\nfrom typing_extensions import TypedDict\n\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.types import Audio, File, Image\n\n\nclass TestToolFromFunctionOutputSchema:\n    async def test_no_return_annotation(self):\n        def func():\n            pass\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    @pytest.mark.parametrize(\n        \"annotation\",\n        [\n            int,\n            float,\n            bool,\n            str,\n            int | float,\n            list,\n            list[int],\n            list[int | float],\n            dict,\n            dict[str, Any],\n            dict[str, int | None],\n            tuple[int, str],\n            set[int],\n            list[tuple[int, str]],\n        ],\n    )\n    async def test_simple_return_annotation(self, annotation):\n        def func() -> annotation:\n            return 1\n\n        tool = Tool.from_function(func)\n\n        base_schema = TypeAdapter(annotation).json_schema()\n\n        # Non-object types get wrapped\n        schema_type = base_schema.get(\"type\")\n        is_object_type = schema_type == \"object\"\n\n        if not is_object_type:\n            # Non-object types get wrapped\n            expected_schema = {\n                \"type\": \"object\",\n                \"properties\": {\"result\": base_schema},\n                \"required\": [\"result\"],\n                \"x-fastmcp-wrap-result\": True,\n            }\n            assert tool.output_schema == expected_schema\n            # # Note: Parameterized test - keeping original assertion for multiple parameter values\n        else:\n            # Object types remain unwrapped\n            assert tool.output_schema == base_schema\n\n    @pytest.mark.parametrize(\n        \"annotation\",\n        [\n            AnyUrl,\n            Annotated[int, Field(ge=1)],\n            Annotated[int, Field(ge=1)],\n        ],\n    )\n    async def test_complex_return_annotation(self, annotation):\n        def func() -> annotation:\n            return 1\n\n        tool = Tool.from_function(func)\n\n        base_schema = TypeAdapter(annotation).json_schema()\n        expected_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"result\": base_schema},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n        assert tool.output_schema == expected_schema\n\n    async def test_none_return_annotation(self):\n        def func() -> None:\n            pass\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    async def test_any_return_annotation(self):\n        from typing import Any\n\n        def func() -> Any:\n            return 1\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    @pytest.mark.parametrize(\n        \"annotation, expected\",\n        [\n            (Image, ImageContent),\n            (Audio, AudioContent),\n            (File, EmbeddedResource),\n            (Image | int, ImageContent | int),\n            (Image | Audio, ImageContent | AudioContent),\n            (list[Image | Audio], list[ImageContent | AudioContent]),\n        ],\n    )\n    async def test_converted_return_annotation(self, annotation, expected):\n        def func() -> annotation:\n            return 1\n\n        tool = Tool.from_function(func)\n        # Image, Audio, File types don't generate output schemas since they're converted to content directly\n        assert tool.output_schema is None\n\n    async def test_tool_result_return_annotation_no_output_schema(self):\n        def func() -> ToolResult:\n            return ToolResult(content=\"hello\")\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    async def test_tool_result_subclass_return_annotation_no_output_schema(self):\n        class MyToolResult(ToolResult):\n            def __init__(self, data: str):\n                super().__init__(structured_content={\"content\": data})\n\n        def func() -> MyToolResult:\n            return MyToolResult(\"hello\")\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    async def test_optional_tool_result_subclass_no_output_schema(self):\n        class MyToolResult(ToolResult):\n            pass\n\n        def func() -> MyToolResult | None:\n            return None\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    async def test_dataclass_return_annotation(self):\n        @dataclass\n        class Person:\n            name: str\n            age: int\n\n        def func() -> Person:\n            return Person(name=\"John\", age=30)\n\n        tool = Tool.from_function(func)\n        expected_schema = compress_schema(\n            TypeAdapter(Person).json_schema(), prune_titles=True\n        )\n        assert tool.output_schema == expected_schema\n\n    async def test_base_model_return_annotation(self):\n        class Person(BaseModel):\n            name: str\n            age: int\n\n        def func() -> Person:\n            return Person(name=\"John\", age=30)\n\n        tool = Tool.from_function(func)\n\n        assert tool.output_schema == snapshot(\n            {\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"age\": {\"type\": \"integer\"},\n                },\n                \"required\": [\"name\", \"age\"],\n                \"type\": \"object\",\n            }\n        )\n\n    async def test_typeddict_return_annotation(self):\n        class Person(TypedDict):\n            name: str\n            age: int\n\n        def func() -> Person:\n            return Person(name=\"John\", age=30)\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema == snapshot(\n            {\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"age\": {\"type\": \"integer\"},\n                },\n                \"required\": [\"name\", \"age\"],\n                \"type\": \"object\",\n            }\n        )\n\n    async def test_unserializable_return_annotation(self):\n        class Unserializable:\n            def __init__(self, data: Any):\n                self.data = data\n\n        def func() -> Unserializable:\n            return Unserializable(data=\"test\")\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    async def test_mixed_unserializable_return_annotation(self):\n        class Unserializable:\n            def __init__(self, data: Any):\n                self.data = data\n\n        def func() -> Unserializable | int:\n            return Unserializable(data=\"test\")\n\n        tool = Tool.from_function(func)\n        assert tool.output_schema is None\n\n    async def test_provided_output_schema_takes_precedence_over_json_compatible_annotation(\n        self,\n    ):\n        \"\"\"Test that provided output_schema takes precedence over inferred schema from JSON-compatible annotation.\"\"\"\n\n        def func() -> dict[str, int]:\n            return {\"a\": 1, \"b\": 2}\n\n        # Provide a custom output schema that differs from the inferred one\n        custom_schema = {\"type\": \"object\", \"description\": \"Custom schema\"}\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_provided_output_schema_takes_precedence_over_complex_annotation(\n        self,\n    ):\n        \"\"\"Test that provided output_schema takes precedence over inferred schema from complex annotation.\"\"\"\n\n        def func() -> list[dict[str, int | float]]:\n            return [{\"a\": 1, \"b\": 2.5}]\n\n        # Provide a custom output schema that differs from the inferred one\n        custom_schema = {\"type\": \"object\", \"properties\": {\"custom\": {\"type\": \"string\"}}}\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_provided_output_schema_takes_precedence_over_unserializable_annotation(\n        self,\n    ):\n        \"\"\"Test that provided output_schema takes precedence over None schema from unserializable annotation.\"\"\"\n\n        class Unserializable:\n            def __init__(self, data: Any):\n                self.data = data\n\n        def func() -> Unserializable:\n            return Unserializable(data=\"test\")\n\n        # Provide a custom output schema even though the annotation is unserializable\n        custom_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"items\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}},\n        }\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_provided_output_schema_takes_precedence_over_no_annotation(self):\n        \"\"\"Test that provided output_schema takes precedence over None schema from no annotation.\"\"\"\n\n        def func():\n            return \"hello\"\n\n        # Provide a custom output schema even though there's no return annotation\n        custom_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"value\": {\"type\": \"number\", \"minimum\": 0}},\n        }\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_provided_output_schema_takes_precedence_over_converted_annotation(\n        self,\n    ):\n        \"\"\"Test that provided output_schema takes precedence over converted schema from Image/Audio/File annotations.\"\"\"\n\n        def func() -> Image:\n            return Image(data=b\"test\")\n\n        # Provide a custom output schema that differs from the converted ImageContent schema\n        custom_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"custom_image\": {\"type\": \"string\"}},\n        }\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_provided_output_schema_takes_precedence_over_union_annotation(self):\n        \"\"\"Test that provided output_schema takes precedence over inferred schema from union annotation.\"\"\"\n\n        def func() -> str | int | None:\n            return \"hello\"\n\n        # Provide a custom output schema that differs from the inferred union schema\n        custom_schema = {\"type\": \"object\", \"properties\": {\"flag\": {\"type\": \"boolean\"}}}\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_provided_output_schema_takes_precedence_over_pydantic_annotation(\n        self,\n    ):\n        \"\"\"Test that provided output_schema takes precedence over inferred schema from Pydantic model annotation.\"\"\"\n\n        class Person(BaseModel):\n            name: str\n            age: int\n\n        def func() -> Person:\n            return Person(name=\"John\", age=30)\n\n        # Provide a custom output schema that differs from the inferred Person schema\n        custom_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"numbers\": {\"type\": \"array\", \"items\": {\"type\": \"number\"}}},\n        }\n\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n    async def test_output_schema_false_allows_automatic_structured_content(self):\n        \"\"\"Test that output_schema=False still allows automatic structured content for dict-like objects.\"\"\"\n\n        def func() -> dict[str, str]:\n            return {\"message\": \"Hello, world!\"}\n\n        tool = Tool.from_function(func, output_schema=None)\n        assert tool.output_schema is None\n\n        result = await tool.run({})\n        # Dict objects automatically become structured content even without schema\n        assert result.structured_content == {\"message\": \"Hello, world!\"}\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == '{\"message\":\"Hello, world!\"}'\n\n    async def test_output_schema_none_disables_structured_content(self):\n        \"\"\"Test that output_schema=None explicitly disables structured content.\"\"\"\n\n        def func() -> int:\n            return 42\n\n        tool = Tool.from_function(func, output_schema=None)\n        assert tool.output_schema is None\n\n        result = await tool.run({})\n        assert result.structured_content is None\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"42\"\n\n    async def test_output_schema_inferred_when_not_specified(self):\n        \"\"\"Test that output schema is inferred when not explicitly specified.\"\"\"\n\n        def func() -> int:\n            return 42\n\n        # Don't specify output_schema - should infer and wrap\n        tool = Tool.from_function(func)\n        assert tool.output_schema == snapshot(\n            {\n                \"properties\": {\"result\": {\"type\": \"integer\"}},\n                \"required\": [\"result\"],\n                \"type\": \"object\",\n                \"x-fastmcp-wrap-result\": True,\n            }\n        )\n\n        result = await tool.run({})\n        assert result.structured_content == {\"result\": 42}\n\n    async def test_explicit_object_schema_with_dict_return(self):\n        \"\"\"Test that explicit object schemas work when function returns a dict.\"\"\"\n\n        def func() -> dict[str, int]:\n            return {\"value\": 42}\n\n        # Provide explicit object schema\n        explicit_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"value\": {\"type\": \"integer\", \"minimum\": 0}},\n        }\n        tool = Tool.from_function(func, output_schema=explicit_schema)\n        assert tool.output_schema == explicit_schema  # Schema not wrapped\n        assert tool.output_schema and \"x-fastmcp-wrap-result\" not in tool.output_schema\n\n        result = await tool.run({})\n        # Dict result with object schema is used directly\n        assert result.structured_content == {\"value\": 42}\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == '{\"value\":42}'\n\n    async def test_explicit_object_schema_with_non_dict_return_fails(self):\n        \"\"\"Test that explicit object schemas fail when function returns non-dict.\"\"\"\n\n        def func() -> int:\n            return 42\n\n        # Provide explicit object schema but return non-dict\n        explicit_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"value\": {\"type\": \"integer\"}},\n        }\n        tool = Tool.from_function(func, output_schema=explicit_schema)\n\n        # Should fail because int is not dict-compatible with object schema\n        with pytest.raises(ValueError, match=\"structured_content must be a dict\"):\n            await tool.run({})\n\n    async def test_object_output_schema_not_wrapped(self):\n        \"\"\"Test that object-type output schemas are never wrapped.\"\"\"\n\n        def func() -> dict[str, int]:\n            return {\"value\": 42}\n\n        # Object schemas should never be wrapped, even when inferred\n        tool = Tool.from_function(func)\n        expected_schema = TypeAdapter(dict[str, int]).json_schema()\n        assert tool.output_schema == expected_schema  # Not wrapped\n        assert tool.output_schema and \"x-fastmcp-wrap-result\" not in tool.output_schema\n\n        result = await tool.run({})\n        assert result.structured_content == {\"value\": 42}  # Direct value\n\n    async def test_structured_content_interaction_with_wrapping(self):\n        \"\"\"Test that structured content works correctly with schema wrapping.\"\"\"\n\n        def func() -> str:\n            return \"hello\"\n\n        # Inferred schema should wrap string type\n        tool = Tool.from_function(func)\n        assert tool.output_schema == snapshot(\n            {\n                \"properties\": {\"result\": {\"type\": \"string\"}},\n                \"required\": [\"result\"],\n                \"type\": \"object\",\n                \"x-fastmcp-wrap-result\": True,\n            }\n        )\n\n        result = await tool.run({})\n        # Unstructured content\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"hello\"\n        # Structured content should be wrapped\n        assert result.structured_content == {\"result\": \"hello\"}\n\n    async def test_structured_content_with_explicit_object_schema(self):\n        \"\"\"Test structured content with explicit object schema.\"\"\"\n\n        def func() -> dict[str, str]:\n            return {\"greeting\": \"hello\"}\n\n        # Provide explicit object schema\n        explicit_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"greeting\": {\"type\": \"string\"}},\n            \"required\": [\"greeting\"],\n        }\n        tool = Tool.from_function(func, output_schema=explicit_schema)\n        assert tool.output_schema == explicit_schema\n\n        result = await tool.run({})\n        # Should use direct value since explicit schema doesn't have wrap marker\n        assert result.structured_content == {\"greeting\": \"hello\"}\n\n    async def test_structured_content_with_custom_wrapper_schema(self):\n        \"\"\"Test structured content with custom schema that includes wrap marker.\"\"\"\n\n        def func() -> str:\n            return \"world\"\n\n        # Custom schema with wrap marker\n        custom_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"message\": {\"type\": \"string\"}},\n            \"x-fastmcp-wrap-result\": True,\n        }\n        tool = Tool.from_function(func, output_schema=custom_schema)\n        assert tool.output_schema == custom_schema\n\n        result = await tool.run({})\n        # Should wrap with \"result\" key due to wrap marker\n        assert result.structured_content == {\"result\": \"world\"}\n\n    async def test_none_vs_false_output_schema_behavior(self):\n        \"\"\"Test the difference between None and False for output_schema.\"\"\"\n\n        def func() -> int:\n            return 123\n\n        # None should disable\n        tool_none = Tool.from_function(func, output_schema=None)\n        assert tool_none.output_schema is None\n\n        # Default (NotSet) should infer from return type\n        tool_default = Tool.from_function(func)\n        assert (\n            tool_default.output_schema is not None\n        )  # Should infer schema from dict return type\n\n        # Different behavior: None vs inferred\n        result_none = await tool_none.run({})\n        result_default = await tool_default.run({})\n\n        # None should still try fallback generation but fail for non-dict\n        assert result_none.structured_content is None  # Fallback fails for int\n        # Default should use proper schema and wrap the result\n        assert result_default.structured_content == {\n            \"result\": 123\n        }  # Schema-based generation with wrapping\n        assert isinstance(result_none.content[0], TextContent)\n        assert isinstance(result_default.content[0], TextContent)\n        assert result_none.content[0].text == result_default.content[0].text == \"123\"\n\n    async def test_non_object_output_schema_raises_error(self):\n        \"\"\"Test that providing a non-object output schema raises a ValueError.\"\"\"\n\n        def func() -> int:\n            return 42\n\n        # Test various non-object schemas that should raise errors\n        non_object_schemas = [\n            {\"type\": \"string\"},\n            {\"type\": \"integer\", \"minimum\": 0},\n            {\"type\": \"number\"},\n            {\"type\": \"boolean\"},\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n        ]\n\n        for schema in non_object_schemas:\n            with pytest.raises(\n                ValueError, match=\"Output schemas must represent object types\"\n            ):\n                Tool.from_function(func, output_schema=schema)\n\n\nclass TestWrapResultMeta:\n    async def test_list_return_includes_wrap_result_meta(self):\n        \"\"\"A tool returning list[dict] should set wrap_result in meta.\"\"\"\n\n        def func() -> list[dict]:\n            return [{\"a\": 1}, {\"b\": 2}]\n\n        tool = Tool.from_function(func)\n        result = await tool.run({})\n        assert result.structured_content == {\"result\": [{\"a\": 1}, {\"b\": 2}]}\n        assert result.meta == {\"fastmcp\": {\"wrap_result\": True}}\n\n    async def test_int_return_includes_wrap_result_meta(self):\n        \"\"\"A tool returning int should set wrap_result in meta.\"\"\"\n\n        def func() -> int:\n            return 42\n\n        tool = Tool.from_function(func)\n        result = await tool.run({})\n        assert result.structured_content == {\"result\": 42}\n        assert result.meta == {\"fastmcp\": {\"wrap_result\": True}}\n\n    async def test_dict_return_does_not_include_wrap_result_meta(self):\n        \"\"\"A tool returning dict should NOT set wrap_result in meta.\"\"\"\n\n        def func() -> dict[str, int]:\n            return {\"value\": 42}\n\n        tool = Tool.from_function(func)\n        result = await tool.run({})\n        assert result.structured_content == {\"value\": 42}\n        assert result.meta is None\n\n    async def test_no_schema_dict_return_no_meta(self):\n        \"\"\"A tool without output schema returning dict should not set meta.\"\"\"\n\n        def func():\n            return {\"key\": \"val\"}\n\n        tool = Tool.from_function(func)\n        result = await tool.run({})\n        assert result.structured_content == {\"key\": \"val\"}\n        assert result.meta is None\n"
  },
  {
    "path": "tests/tools/tool/test_results.py",
    "content": "from dataclasses import dataclass\nfrom typing import Any\n\nimport pytest\n\nfrom fastmcp.tools.base import Tool, ToolResult\n\n\nclass TestToolResultCasting:\n    @pytest.fixture\n    async def client(self):\n        from fastmcp import FastMCP\n        from fastmcp.client import Client\n\n        mcp = FastMCP()\n\n        @mcp.tool\n        def test_tool(\n            unstructured: str | None = None,\n            structured: dict[str, Any] | None = None,\n            meta: dict[str, Any] | None = None,\n        ):\n            return ToolResult(\n                content=unstructured,\n                structured_content=structured,\n                meta=meta,\n            )\n\n        async with Client(mcp) as client:\n            yield client\n\n    async def test_only_unstructured_content(self, client):\n        result = await client.call_tool(\"test_tool\", {\"unstructured\": \"test data\"})\n\n        assert result.content[0].type == \"text\"\n        assert result.content[0].text == \"test data\"\n        assert result.structured_content is None\n        assert result.meta is None\n\n    async def test_neither_unstructured_or_structured_content(self, client):\n        from fastmcp.exceptions import ToolError\n\n        with pytest.raises(ToolError):\n            await client.call_tool(\"test_tool\", {})\n\n    async def test_structured_and_unstructured_content(self, client):\n        result = await client.call_tool(\n            \"test_tool\",\n            {\"unstructured\": \"test data\", \"structured\": {\"data_type\": \"test\"}},\n        )\n\n        assert result.content[0].type == \"text\"\n        assert result.content[0].text == \"test data\"\n        assert result.structured_content == {\"data_type\": \"test\"}\n        assert result.meta is None\n\n    async def test_structured_unstructured_and_meta_content(self, client):\n        result = await client.call_tool(\n            \"test_tool\",\n            {\n                \"unstructured\": \"test data\",\n                \"structured\": {\"data_type\": \"test\"},\n                \"meta\": {\"some\": \"metadata\"},\n            },\n        )\n\n        assert result.content[0].type == \"text\"\n        assert result.content[0].text == \"test data\"\n        assert result.structured_content == {\"data_type\": \"test\"}\n        assert result.meta == {\"some\": \"metadata\"}\n\n\nclass TestUnionReturnTypes:\n    \"\"\"Tests for tools with union return types.\"\"\"\n\n    async def test_dataclass_union_string_works(self):\n        \"\"\"Test that union of dataclass and string works correctly.\"\"\"\n\n        @dataclass\n        class Data:\n            value: int\n\n        def get_data(return_error: bool) -> Data | str:\n            if return_error:\n                return \"error occurred\"\n            return Data(value=42)\n\n        tool = Tool.from_function(get_data)\n\n        # Test returning dataclass\n        result1 = await tool.run({\"return_error\": False})\n        assert result1.structured_content == {\"result\": {\"value\": 42}}\n\n        # Test returning string\n        result2 = await tool.run({\"return_error\": True})\n        assert result2.structured_content == {\"result\": \"error occurred\"}\n\n\nclass TestSerializationAlias:\n    \"\"\"Tests for Pydantic field serialization alias support in tool output schemas.\"\"\"\n\n    def test_output_schema_respects_serialization_alias(self):\n        \"\"\"Test that Tool.from_function generates output schema using serialization alias.\"\"\"\n        from typing import Annotated\n\n        from pydantic import AliasChoices, BaseModel, Field\n\n        class Component(BaseModel):\n            \"\"\"Model with multiple validation aliases but specific serialization alias.\"\"\"\n\n            component_id: str = Field(\n                validation_alias=AliasChoices(\"id\", \"componentId\"),\n                serialization_alias=\"componentId\",\n                description=\"The ID of the component\",\n            )\n\n        async def get_component(\n            component_id: str,\n        ) -> Annotated[Component, Field(description=\"The component.\")]:\n            # API returns data with 'id' field\n            api_data = {\"id\": component_id}\n            return Component.model_validate(api_data)\n\n        tool = Tool.from_function(get_component, name=\"get-component\")\n\n        # The output schema should use the serialization alias 'componentId'\n        # not the first validation alias 'id'\n        assert tool.output_schema is not None\n\n        # Object schemas have properties directly at root (MCP spec compliance)\n        # Root-level $refs are resolved to ensure type: object at root\n        assert \"properties\" in tool.output_schema\n        assert tool.output_schema.get(\"type\") == \"object\"\n\n        # Should have 'componentId' not 'id' in properties\n        assert \"componentId\" in tool.output_schema[\"properties\"]\n        assert \"id\" not in tool.output_schema[\"properties\"]\n\n        # Should require 'componentId' not 'id'\n        assert \"componentId\" in tool.output_schema.get(\"required\", [])\n        assert \"id\" not in tool.output_schema.get(\"required\", [])\n\n    async def test_tool_execution_with_serialization_alias(self):\n        \"\"\"Test that tool execution works correctly with serialization aliases.\"\"\"\n        from typing import Annotated\n\n        from pydantic import AliasChoices, BaseModel, Field\n\n        from fastmcp import Client, FastMCP\n\n        class Component(BaseModel):\n            \"\"\"Model with multiple validation aliases but specific serialization alias.\"\"\"\n\n            component_id: str = Field(\n                validation_alias=AliasChoices(\"id\", \"componentId\"),\n                serialization_alias=\"componentId\",\n                description=\"The ID of the component\",\n            )\n\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        async def get_component(\n            component_id: str,\n        ) -> Annotated[Component, Field(description=\"The component.\")]:\n            # API returns data with 'id' field\n            api_data = {\"id\": component_id}\n            return Component.model_validate(api_data)\n\n        async with Client(mcp) as client:\n            # Execute the tool - this should work without validation errors\n            result = await client.call_tool(\n                \"get_component\", {\"component_id\": \"test123\"}\n            )\n\n            # The result should contain the serialized form with 'componentId'\n            assert result.structured_content is not None\n            # Object types may be wrapped in \"result\" or not, depending on schema structure\n            if \"result\" in result.structured_content:\n                component_data = result.structured_content[\"result\"]\n            else:\n                component_data = result.structured_content\n            assert component_data[\"componentId\"] == \"test123\"\n            assert \"id\" not in component_data\n"
  },
  {
    "path": "tests/tools/tool/test_title.py",
    "content": "from fastmcp.tools.base import Tool\n\n\nclass TestToolTitle:\n    \"\"\"Tests for tool title functionality.\"\"\"\n\n    def test_tool_with_title(self):\n        \"\"\"Test that tools can have titles and they appear in MCP conversion.\"\"\"\n\n        def calculate(x: int, y: int) -> int:\n            \"\"\"Calculate the sum of two numbers.\"\"\"\n            return x + y\n\n        tool = Tool.from_function(\n            calculate,\n            name=\"calc\",\n            title=\"Advanced Calculator Tool\",\n            description=\"Custom description\",\n        )\n\n        assert tool.name == \"calc\"\n        assert tool.title == \"Advanced Calculator Tool\"\n        assert tool.description == \"Custom description\"\n\n        # Test MCP conversion includes title\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.name == \"calc\"\n        assert (\n            hasattr(mcp_tool, \"title\") and mcp_tool.title == \"Advanced Calculator Tool\"\n        )\n\n    def test_tool_without_title(self):\n        \"\"\"Test that tools without titles use name as display name.\"\"\"\n\n        def multiply(a: int, b: int) -> int:\n            return a * b\n\n        tool = Tool.from_function(multiply)\n\n        assert tool.name == \"multiply\"\n        assert tool.title is None\n\n        # Test MCP conversion doesn't include title when None\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.name == \"multiply\"\n        assert not hasattr(mcp_tool, \"title\") or mcp_tool.title is None\n\n    def test_tool_title_priority(self):\n        \"\"\"Test that explicit title takes priority over annotations.title.\"\"\"\n        from mcp.types import ToolAnnotations\n\n        def divide(x: int, y: int) -> float:\n            \"\"\"Divide two numbers.\"\"\"\n            return x / y\n\n        # Test with both explicit title and annotations.title\n        annotations = ToolAnnotations(title=\"Annotation Title\")\n        tool = Tool.from_function(\n            divide,\n            name=\"div\",\n            title=\"Explicit Title\",\n            annotations=annotations,\n        )\n\n        assert tool.title == \"Explicit Title\"\n        assert tool.annotations is not None\n        assert tool.annotations.title == \"Annotation Title\"\n\n        # Explicit title should take priority\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.title == \"Explicit Title\"\n\n    def test_tool_annotations_title_fallback(self):\n        \"\"\"Test that annotations.title is used when no explicit title is provided.\"\"\"\n        from mcp.types import ToolAnnotations\n\n        def modulo(x: int, y: int) -> int:\n            \"\"\"Get modulo of two numbers.\"\"\"\n            return x % y\n\n        # Test with only annotations.title (no explicit title)\n        annotations = ToolAnnotations(title=\"Annotation Title\")\n        tool = Tool.from_function(\n            modulo,\n            name=\"mod\",\n            annotations=annotations,\n        )\n\n        assert tool.title is None\n        assert tool.annotations is not None\n        assert tool.annotations.title == \"Annotation Title\"\n\n        # Should fall back to annotations.title\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.title == \"Annotation Title\"\n"
  },
  {
    "path": "tests/tools/tool/test_tool.py",
    "content": "from datetime import timedelta\n\nimport pytest\nfrom dirty_equals import HasName\nfrom inline_snapshot import snapshot\nfrom mcp.types import (\n    AudioContent,\n    ImageContent,\n    ToolExecution,\n)\nfrom pydantic import BaseModel\n\nfrom fastmcp.tools.base import Tool, ToolResult\nfrom fastmcp.utilities.types import Audio, File, Image\n\n\nclass TestToolFromFunction:\n    def test_basic_function(self):\n        \"\"\"Test registering and running a basic function.\"\"\"\n\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        tool = Tool.from_function(add)\n\n        assert tool.model_dump(exclude_none=True) == snapshot(\n            {\n                \"name\": \"add\",\n                \"description\": \"Add two numbers.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"a\": {\"type\": \"integer\"},\n                        \"b\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"a\", \"b\"],\n                    \"type\": \"object\",\n                },\n                \"output_schema\": {\n                    \"properties\": {\"result\": {\"type\": \"integer\"}},\n                    \"required\": [\"result\"],\n                    \"type\": \"object\",\n                    \"x-fastmcp-wrap-result\": True,\n                },\n                \"fn\": HasName(\"add\"),\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    def test_meta_parameter(self):\n        \"\"\"Test that meta parameter is properly handled.\"\"\"\n\n        def multiply(a: int, b: int) -> int:\n            \"\"\"Multiply two numbers.\"\"\"\n            return a * b\n\n        meta_data = {\"version\": \"1.0\", \"author\": \"test\"}\n        tool = Tool.from_function(multiply, meta=meta_data)\n\n        assert tool.meta == meta_data\n        mcp_tool = tool.to_mcp_tool()\n\n        # MCP tool includes fastmcp meta, so check that our meta is included\n        assert mcp_tool.meta is not None\n        assert meta_data.items() <= mcp_tool.meta.items()\n\n    async def test_async_function(self):\n        \"\"\"Test registering and running an async function.\"\"\"\n\n        async def fetch_data(url: str) -> str:\n            \"\"\"Fetch data from URL.\"\"\"\n            return f\"Data from {url}\"\n\n        tool = Tool.from_function(fetch_data)\n\n        assert tool.model_dump(exclude_none=True) == snapshot(\n            {\n                \"name\": \"fetch_data\",\n                \"description\": \"Fetch data from URL.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\"url\": {\"type\": \"string\"}},\n                    \"required\": [\"url\"],\n                    \"type\": \"object\",\n                },\n                \"output_schema\": {\n                    \"properties\": {\"result\": {\"type\": \"string\"}},\n                    \"required\": [\"result\"],\n                    \"type\": \"object\",\n                    \"x-fastmcp-wrap-result\": True,\n                },\n                \"fn\": HasName(\"fetch_data\"),\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    def test_callable_object(self):\n        class Adder:\n            \"\"\"Adds two numbers.\"\"\"\n\n            def __call__(self, x: int, y: int) -> int:\n                \"\"\"ignore this\"\"\"\n                return x + y\n\n        tool = Tool.from_function(Adder())\n\n        assert tool.model_dump(exclude_none=True, exclude={\"fn\"}) == snapshot(\n            {\n                \"name\": \"Adder\",\n                \"description\": \"Adds two numbers.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"x\": {\"type\": \"integer\"},\n                        \"y\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"x\", \"y\"],\n                    \"type\": \"object\",\n                },\n                \"output_schema\": {\n                    \"properties\": {\"result\": {\"type\": \"integer\"}},\n                    \"required\": [\"result\"],\n                    \"type\": \"object\",\n                    \"x-fastmcp-wrap-result\": True,\n                },\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    def test_async_callable_object(self):\n        class Adder:\n            \"\"\"Adds two numbers.\"\"\"\n\n            async def __call__(self, x: int, y: int) -> int:\n                \"\"\"ignore this\"\"\"\n                return x + y\n\n        tool = Tool.from_function(Adder())\n\n        assert tool.model_dump(exclude_none=True, exclude={\"fn\"}) == snapshot(\n            {\n                \"name\": \"Adder\",\n                \"description\": \"Adds two numbers.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"x\": {\"type\": \"integer\"},\n                        \"y\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"x\", \"y\"],\n                    \"type\": \"object\",\n                },\n                \"output_schema\": {\n                    \"properties\": {\"result\": {\"type\": \"integer\"}},\n                    \"required\": [\"result\"],\n                    \"type\": \"object\",\n                    \"x-fastmcp-wrap-result\": True,\n                },\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    def test_pydantic_model_function(self):\n        \"\"\"Test registering a function that takes a Pydantic model.\"\"\"\n\n        class UserInput(BaseModel):\n            name: str\n            age: int\n\n        def create_user(user: UserInput, flag: bool) -> dict:\n            \"\"\"Create a new user.\"\"\"\n            return {\"id\": 1, **user.model_dump()}\n\n        tool = Tool.from_function(create_user)\n\n        assert tool.model_dump(exclude_none=True) == snapshot(\n            {\n                \"name\": \"create_user\",\n                \"description\": \"Create a new user.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"$defs\": {\n                        \"UserInput\": {\n                            \"properties\": {\n                                \"name\": {\"type\": \"string\"},\n                                \"age\": {\"type\": \"integer\"},\n                            },\n                            \"required\": [\"name\", \"age\"],\n                            \"type\": \"object\",\n                        },\n                    },\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"user\": {\"$ref\": \"#/$defs/UserInput\"},\n                        \"flag\": {\"type\": \"boolean\"},\n                    },\n                    \"required\": [\"user\", \"flag\"],\n                    \"type\": \"object\",\n                },\n                \"output_schema\": {\"additionalProperties\": True, \"type\": \"object\"},\n                \"fn\": HasName(\"create_user\"),\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    async def test_tool_with_image_return(self):\n        def image_tool(data: bytes) -> Image:\n            return Image(data=data)\n\n        tool = Tool.from_function(image_tool)\n        assert tool.parameters[\"properties\"][\"data\"][\"type\"] == \"string\"\n        assert tool.output_schema is None\n\n        result = await tool.run({\"data\": \"test.png\"})\n        assert isinstance(result.content[0], ImageContent)\n\n    async def test_tool_with_audio_return(self):\n        def audio_tool(data: bytes) -> Audio:\n            return Audio(data=data)\n\n        tool = Tool.from_function(audio_tool)\n        assert tool.parameters[\"properties\"][\"data\"][\"type\"] == \"string\"\n        assert tool.output_schema is None\n\n        result = await tool.run({\"data\": \"test.wav\"})\n        assert isinstance(result.content[0], AudioContent)\n\n    async def test_tool_with_file_return(self):\n        from pydantic import AnyUrl\n\n        def file_tool(data: bytes) -> File:\n            return File(data=data, format=\"octet-stream\")\n\n        tool = Tool.from_function(file_tool)\n        assert tool.parameters[\"properties\"][\"data\"][\"type\"] == \"string\"\n        assert tool.output_schema is None\n\n        result: ToolResult = await tool.run({\"data\": \"test.bin\"})\n        assert result.content[0].model_dump(exclude_none=True) == snapshot(\n            {\n                \"type\": \"resource\",\n                \"resource\": {\n                    \"uri\": AnyUrl(\"file:///resource.octet-stream\"),\n                    \"mimeType\": \"application/octet-stream\",\n                    \"blob\": \"dGVzdC5iaW4=\",\n                },\n            }\n        )\n\n    def test_non_callable_fn(self):\n        with pytest.raises(TypeError, match=\"not a callable object\"):\n            Tool.from_function(1)  # type: ignore\n\n    def test_lambda(self):\n        tool = Tool.from_function(lambda x: x, name=\"my_tool\")\n        assert tool.model_dump(exclude_none=True, exclude={\"fn\"}) == snapshot(\n            {\n                \"name\": \"my_tool\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\"x\": {\"title\": \"X\"}},\n                    \"required\": [\"x\"],\n                    \"type\": \"object\",\n                },\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    def test_lambda_with_no_name(self):\n        with pytest.raises(\n            ValueError, match=\"You must provide a name for lambda functions\"\n        ):\n            Tool.from_function(lambda x: x)\n\n    def test_private_arguments(self):\n        def add(_a: int, _b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return _a + _b\n\n        tool = Tool.from_function(add)\n\n        assert tool.model_dump(\n            exclude_none=True, exclude={\"output_schema\", \"fn\"}\n        ) == snapshot(\n            {\n                \"name\": \"add\",\n                \"description\": \"Add two numbers.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"_a\": {\"type\": \"integer\"},\n                        \"_b\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"_a\", \"_b\"],\n                    \"type\": \"object\",\n                },\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    def test_tool_with_varargs_not_allowed(self):\n        def func(a: int, b: int, *args: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        with pytest.raises(\n            ValueError, match=r\"Functions with \\*args are not supported as tools\"\n        ):\n            Tool.from_function(func)\n\n    def test_tool_with_varkwargs_not_allowed(self):\n        def func(a: int, b: int, **kwargs: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        with pytest.raises(\n            ValueError, match=r\"Functions with \\*\\*kwargs are not supported as tools\"\n        ):\n            Tool.from_function(func)\n\n    async def test_instance_method(self):\n        class MyClass:\n            def add(self, x: int, y: int) -> int:\n                \"\"\"Add two numbers.\"\"\"\n                return x + y\n\n        obj = MyClass()\n\n        tool = Tool.from_function(obj.add)\n        assert \"self\" not in tool.parameters[\"properties\"]\n\n        assert tool.model_dump(exclude_none=True, exclude={\"fn\"}) == snapshot(\n            {\n                \"name\": \"add\",\n                \"description\": \"Add two numbers.\",\n                \"tags\": set(),\n                \"parameters\": {\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"x\": {\"type\": \"integer\"},\n                        \"y\": {\"type\": \"integer\"},\n                    },\n                    \"required\": [\"x\", \"y\"],\n                    \"type\": \"object\",\n                },\n                \"output_schema\": {\n                    \"properties\": {\"result\": {\"type\": \"integer\"}},\n                    \"required\": [\"result\"],\n                    \"type\": \"object\",\n                    \"x-fastmcp-wrap-result\": True,\n                },\n                \"task_config\": {\n                    \"mode\": \"forbidden\",\n                    \"poll_interval\": timedelta(seconds=5),\n                },\n            }\n        )\n\n    async def test_instance_method_with_varargs_not_allowed(self):\n        class MyClass:\n            def add(self, x: int, y: int, *args: int) -> int:\n                \"\"\"Add two numbers.\"\"\"\n                return x + y\n\n        obj = MyClass()\n\n        with pytest.raises(\n            ValueError, match=r\"Functions with \\*args are not supported as tools\"\n        ):\n            Tool.from_function(obj.add)\n\n    async def test_instance_method_with_varkwargs_not_allowed(self):\n        class MyClass:\n            def add(self, x: int, y: int, **kwargs: int) -> int:\n                \"\"\"Add two numbers.\"\"\"\n                return x + y\n\n        obj = MyClass()\n\n        with pytest.raises(\n            ValueError, match=r\"Functions with \\*\\*kwargs are not supported as tools\"\n        ):\n            Tool.from_function(obj.add)\n\n    async def test_classmethod(self):\n        class MyClass:\n            x: int = 10\n\n            @classmethod\n            def call(cls, x: int, y: int) -> int:\n                \"\"\"Add two numbers.\"\"\"\n                return x + y\n\n        tool = Tool.from_function(MyClass.call)\n        assert tool.name == \"call\"\n        assert tool.description == \"Add two numbers.\"\n        assert \"x\" in tool.parameters[\"properties\"]\n        assert \"y\" in tool.parameters[\"properties\"]\n\n\nclass TestToolNameValidation:\n    \"\"\"Tests for tool name validation per MCP specification (SEP-986).\"\"\"\n\n    @pytest.fixture\n    def caplog_for_mcp_validation(self, caplog):\n        \"\"\"Capture logs from the MCP SDK's tool name validation logger.\"\"\"\n        import logging\n\n        caplog.set_level(logging.WARNING)\n        logger = logging.getLogger(\"mcp.shared.tool_name_validation\")\n        original_level = logger.level\n        logger.setLevel(logging.WARNING)\n        logger.addHandler(caplog.handler)\n        try:\n            yield caplog\n        finally:\n            logger.removeHandler(caplog.handler)\n            logger.setLevel(original_level)\n\n    @pytest.mark.parametrize(\n        \"name\",\n        [\n            \"valid_tool\",\n            \"valid-tool\",\n            \"valid.tool\",\n            \"ValidTool\",\n            \"tool123\",\n            \"a\",\n            \"a\" * 128,\n        ],\n    )\n    def test_valid_tool_names_no_warnings(self, name, caplog_for_mcp_validation):\n        \"\"\"Valid tool names should not produce warnings.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        tool = Tool.from_function(fn, name=name)\n        assert tool.name == name\n        assert \"Tool name validation warning\" not in caplog_for_mcp_validation.text\n\n    def test_tool_name_with_spaces_warns(self, caplog_for_mcp_validation):\n        \"\"\"Tool names with spaces should produce a warning.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        tool = Tool.from_function(fn, name=\"my tool\")\n        assert tool.name == \"my tool\"\n        assert \"Tool name validation warning\" in caplog_for_mcp_validation.text\n        assert \"contains spaces\" in caplog_for_mcp_validation.text\n\n    def test_tool_name_with_invalid_chars_warns(self, caplog_for_mcp_validation):\n        \"\"\"Tool names with invalid characters should produce a warning.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        tool = Tool.from_function(fn, name=\"tool@name!\")\n        assert tool.name == \"tool@name!\"\n        assert \"Tool name validation warning\" in caplog_for_mcp_validation.text\n        assert \"invalid characters\" in caplog_for_mcp_validation.text\n\n    def test_tool_name_too_long_warns(self, caplog_for_mcp_validation):\n        \"\"\"Tool names exceeding 128 characters should produce a warning.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        long_name = \"a\" * 129\n        tool = Tool.from_function(fn, name=long_name)\n        assert tool.name == long_name\n        assert \"Tool name validation warning\" in caplog_for_mcp_validation.text\n        assert \"exceeds maximum length\" in caplog_for_mcp_validation.text\n\n    def test_tool_name_with_leading_dash_warns(self, caplog_for_mcp_validation):\n        \"\"\"Tool names starting with dash should produce a warning.\"\"\"\n\n        def fn() -> str:\n            return \"test\"\n\n        tool = Tool.from_function(fn, name=\"-tool\")\n        assert tool.name == \"-tool\"\n        assert \"Tool name validation warning\" in caplog_for_mcp_validation.text\n        assert \"starts or ends with a dash\" in caplog_for_mcp_validation.text\n\n    def test_tool_still_created_despite_warnings(self, caplog_for_mcp_validation):\n        \"\"\"Tools with invalid names should still be created (SHOULD not MUST).\"\"\"\n\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        tool = Tool.from_function(add, name=\"invalid tool name!\")\n        assert tool.name == \"invalid tool name!\"\n        assert tool.parameters is not None\n        assert \"a\" in tool.parameters[\"properties\"]\n        assert \"b\" in tool.parameters[\"properties\"]\n\n\nclass TestToolExecutionField:\n    \"\"\"Tests for the execution field on the base Tool class.\"\"\"\n\n    def test_tool_with_execution_field(self):\n        \"\"\"Test that Tool can store and return execution metadata.\"\"\"\n        tool = Tool(\n            name=\"my_tool\",\n            description=\"A tool with execution\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n            execution=ToolExecution(taskSupport=\"optional\"),\n        )\n\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.execution is not None\n        assert mcp_tool.execution.taskSupport == \"optional\"\n\n    def test_tool_without_execution_field(self):\n        \"\"\"Test that Tool without execution returns None.\"\"\"\n        tool = Tool(\n            name=\"my_tool\",\n            description=\"A tool without execution\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n        )\n\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.execution is None\n\n    def test_execution_override_takes_precedence(self):\n        \"\"\"Test that explicit override takes precedence over field value.\"\"\"\n        tool = Tool(\n            name=\"my_tool\",\n            description=\"A tool\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n            execution=ToolExecution(taskSupport=\"optional\"),\n        )\n\n        override_execution = ToolExecution(taskSupport=\"required\")\n        mcp_tool = tool.to_mcp_tool(execution=override_execution)\n        assert mcp_tool.execution is not None\n        assert mcp_tool.execution.taskSupport == \"required\"\n\n    async def test_function_tool_task_config_still_works(self):\n        \"\"\"FunctionTool should still derive execution from task_config.\"\"\"\n\n        async def my_fn() -> str:\n            return \"hello\"\n\n        tool = Tool.from_function(my_fn, task=True)\n        mcp_tool = tool.to_mcp_tool()\n\n        # FunctionTool sets execution from task_config\n        assert mcp_tool.execution is not None\n        assert mcp_tool.execution.taskSupport == \"optional\"\n\n    def test_tool_execution_required_mode(self):\n        \"\"\"Test that Tool can store required execution mode.\"\"\"\n        tool = Tool(\n            name=\"my_tool\",\n            description=\"A tool with required execution\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n            execution=ToolExecution(taskSupport=\"required\"),\n        )\n\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.execution is not None\n        assert mcp_tool.execution.taskSupport == \"required\"\n\n    def test_tool_execution_forbidden_mode(self):\n        \"\"\"Test that Tool can store forbidden execution mode.\"\"\"\n        tool = Tool(\n            name=\"my_tool\",\n            description=\"A tool with forbidden execution\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n            execution=ToolExecution(taskSupport=\"forbidden\"),\n        )\n\n        mcp_tool = tool.to_mcp_tool()\n        assert mcp_tool.execution is not None\n        assert mcp_tool.execution.taskSupport == \"forbidden\"\n"
  },
  {
    "path": "tests/tools/tool_transform/__init__.py",
    "content": ""
  },
  {
    "path": "tests/tools/tool_transform/test_args.py",
    "content": "\"\"\"Tests for argument transformation in tool transforms.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Annotated, Any\n\nimport pytest\nfrom mcp.types import TextContent\nfrom pydantic import BaseModel, Field\nfrom typing_extensions import TypedDict\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client.client import Client\nfrom fastmcp.exceptions import ToolError\nfrom fastmcp.tools import Tool, forward, forward_raw\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import (\n    ArgTransform,\n)\n\n\ndef get_property(tool: Tool, name: str) -> dict[str, Any]:\n    return tool.parameters[\"properties\"][name]\n\n\n@pytest.fixture\ndef add_tool() -> FunctionTool:\n    def add(\n        old_x: Annotated[int, Field(description=\"old_x description\")], old_y: int = 10\n    ) -> int:\n        print(\"running!\")\n        return old_x + old_y\n\n    return Tool.from_function(add)\n\n\nasync def test_tool_transform_chaining(add_tool):\n    \"\"\"Test that transformed tools can be transformed again.\"\"\"\n    # First transformation: a -> x\n    tool1 = Tool.from_tool(add_tool, transform_args={\"old_x\": ArgTransform(name=\"x\")})\n\n    # Second transformation: x -> final_x, using tool1\n    tool2 = Tool.from_tool(tool1, transform_args={\"x\": ArgTransform(name=\"final_x\")})\n\n    result = await tool2.run(arguments={\"final_x\": 5})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"15\"\n\n    # Transform tool1 with custom function that handles all parameters\n    async def custom(final_x: int, **kwargs) -> str:\n        result = await forward(final_x=final_x, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return f\"custom {result.content[0].text}\"  # Extract text from content\n\n    tool3 = Tool.from_tool(\n        tool1, transform_fn=custom, transform_args={\"x\": ArgTransform(name=\"final_x\")}\n    )\n    result = await tool3.run(arguments={\"final_x\": 3, \"old_y\": 5})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"custom 8\"\n\n\nclass MyModel(BaseModel):\n    x: int\n    y: str\n\n\n@dataclass\nclass MyDataclass:\n    x: int\n    y: str\n\n\nclass MyTypedDict(TypedDict):\n    x: int\n    y: str\n\n\n@pytest.mark.parametrize(\n    \"py_type, json_type\",\n    [\n        (int, \"integer\"),\n        (str, \"string\"),\n        (float, \"number\"),\n        (bool, \"boolean\"),\n        (MyModel, \"object\"),\n        (MyDataclass, \"object\"),\n        (MyTypedDict, \"object\"),\n    ],\n)\ndef test_arg_transform_type_handling(add_tool, py_type, json_type):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_x\": ArgTransform(type=py_type)}\n    )\n    prop = get_property(new_tool, \"old_x\")\n    assert prop[\"type\"] == json_type\n\n\ndef test_arg_transform_annotated_types(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_args={\n            \"old_x\": ArgTransform(\n                type=Annotated[int, Field(ge=0, le=100)], description=\"A number 0-100\"\n            )\n        },\n    )\n    prop = get_property(new_tool, \"old_x\")\n    assert prop[\"type\"] == \"integer\"\n    assert prop[\"description\"] == \"A number 0-100\"\n    assert prop[\"minimum\"] == 0\n    assert prop[\"maximum\"] == 100\n\n\ndef test_arg_transform_precedence_over_function_without_kwargs():\n    def base(x: int) -> int:\n        return x\n\n    tool = Tool.from_function(base)\n    new_tool = Tool.from_tool(\n        tool, transform_args={\"x\": ArgTransform(type=str, description=\"String input\")}\n    )\n\n    prop = get_property(new_tool, \"x\")\n    assert prop[\"type\"] == \"string\"\n    assert prop[\"description\"] == \"String input\"\n\n\nasync def test_arg_transform_precedence_over_function_with_kwargs():\n    \"\"\"Test that ArgTransform attributes take precedence over function signature (with **kwargs).\"\"\"\n\n    @Tool.from_function\n    def base(x: int, y: str = \"base_default\") -> str:\n        return f\"{x}: {y}\"\n\n    # Function signature has different types/defaults than ArgTransform\n    async def custom_fn(x: str = \"function_default\", **kwargs) -> str:\n        result = await forward(x=x, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return f\"custom: {result.content[0].text}\"\n\n    tool = Tool.from_tool(\n        base,\n        transform_fn=custom_fn,\n        transform_args={\n            \"x\": ArgTransform(type=int, default=42),  # Different type and default\n            \"y\": ArgTransform(description=\"ArgTransform description\"),\n        },\n    )\n\n    # ArgTransform should take precedence\n    x_prop = get_property(tool, \"x\")\n    y_prop = get_property(tool, \"y\")\n\n    assert x_prop[\"type\"] == \"integer\"  # ArgTransform type wins over function's str\n    assert x_prop[\"default\"] == 42  # ArgTransform default wins over function's default\n    assert (\n        y_prop[\"description\"] == \"ArgTransform description\"\n    )  # ArgTransform description\n\n    # x should not be required due to ArgTransform default\n    assert \"x\" not in tool.parameters[\"required\"]\n\n    # Test it works at runtime\n    result = await tool.run(arguments={\"y\": \"test\"})\n    # Should use ArgTransform default of 42\n    assert isinstance(result.content[0], TextContent)\n    assert \"42: test\" in result.content[0].text\n\n\ndef test_arg_transform_combined_attributes(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_args={\n            \"old_x\": ArgTransform(\n                name=\"new_x\",\n                description=\"New description\",\n                type=str,\n            )\n        },\n    )\n\n    prop = get_property(new_tool, \"new_x\")\n    assert prop[\"type\"] == \"string\"\n    assert prop[\"description\"] == \"New description\"\n    assert \"old_x\" not in new_tool.parameters[\"properties\"]\n\n\nasync def test_arg_transform_type_precedence_runtime():\n    \"\"\"Test that ArgTransform type changes work correctly at runtime.\"\"\"\n\n    @Tool.from_function\n    def base(x: int, y: int = 10) -> int:\n        return x + y\n\n    # Transform x to string type but keep same logic\n    async def custom_fn(x: str, y: int = 10) -> str:\n        # Convert string back to int for the original function\n        result = await forward_raw(x=int(x), y=y)\n        # Extract the text from the result\n        assert isinstance(result.content[0], TextContent)\n        result_text = result.content[0].text\n        return f\"String input '{x}' converted to result: {result_text}\"\n\n    tool = Tool.from_tool(\n        base, transform_fn=custom_fn, transform_args={\"x\": ArgTransform(type=str)}\n    )\n\n    # Verify schema shows string type\n    assert get_property(tool, \"x\")[\"type\"] == \"string\"\n\n    # Test it works with string input\n    result = await tool.run(arguments={\"x\": \"5\", \"y\": 3})\n    assert isinstance(result.content[0], TextContent)\n    assert \"String input '5'\" in result.content[0].text\n    assert \"result: 8\" in result.content[0].text\n\n\nasync def test_arg_transform_default_factory():\n    \"\"\"Test ArgTransform with default_factory for hidden parameters.\"\"\"\n    import asyncio\n    import time\n\n    @Tool.from_function\n    def base_tool(x: int, timestamp: float) -> str:\n        return f\"{x}_{timestamp}\"\n\n    new_tool = Tool.from_tool(\n        base_tool,\n        transform_args={\n            \"timestamp\": ArgTransform(hide=True, default_factory=time.time)\n        },\n    )\n\n    result1 = await new_tool.run(arguments={\"x\": 1})\n    await asyncio.sleep(0.01)\n    result2 = await new_tool.run(arguments={\"x\": 2})\n\n    # Each call should get a different timestamp\n    assert isinstance(result1.content[0], TextContent)\n    assert isinstance(result2.content[0], TextContent)\n    assert result1.content[0].text != result2.content[0].text\n    assert \"1_\" in result1.content[0].text\n    assert \"2_\" in result2.content[0].text\n\n\nasync def test_arg_transform_default_factory_called_each_time():\n    \"\"\"Test that default_factory is called for each tool execution.\"\"\"\n\n    call_count = {\"count\": 0}\n\n    def get_counter():\n        call_count[\"count\"] += 1\n        return call_count[\"count\"]\n\n    @Tool.from_function\n    def base_tool(x: int, counter: int) -> str:\n        return f\"{x}_{counter}\"\n\n    new_tool = Tool.from_tool(\n        base_tool,\n        transform_args={\n            \"counter\": ArgTransform(hide=True, default_factory=get_counter)\n        },\n    )\n\n    result1 = await new_tool.run(arguments={\"x\": 1})\n    result2 = await new_tool.run(arguments={\"x\": 2})\n    result3 = await new_tool.run(arguments={\"x\": 3})\n\n    # Each call should increment the counter\n    assert isinstance(result1.content[0], TextContent)\n    assert isinstance(result2.content[0], TextContent)\n    assert isinstance(result3.content[0], TextContent)\n    assert \"1_1\" in result1.content[0].text\n    assert \"2_2\" in result2.content[0].text\n    assert \"3_3\" in result3.content[0].text\n\n\nasync def test_arg_transform_hidden_with_default_factory():\n    \"\"\"Test that hidden parameters with default_factory work correctly.\"\"\"\n\n    @Tool.from_function\n    def base_tool(x: int, session_id: str) -> str:\n        return f\"{x}_{session_id}\"\n\n    import uuid\n\n    new_tool = Tool.from_tool(\n        base_tool,\n        transform_args={\n            \"session_id\": ArgTransform(\n                hide=True, default_factory=lambda: str(uuid.uuid4())\n            )\n        },\n    )\n\n    result = await new_tool.run(arguments={\"x\": 1})\n    # Should have a UUID in the result\n    assert isinstance(result.content[0], TextContent)\n    assert \"1_\" in result.content[0].text\n    assert len(result.content[0].text.split(\"_\")[1]) > 10\n\n\nasync def test_arg_transform_default_and_factory_raises_error():\n    \"\"\"Test that providing both default and default_factory raises an error.\"\"\"\n    with pytest.raises(\n        ValueError, match=\"Cannot specify both 'default' and 'default_factory'\"\n    ):\n        ArgTransform(default=10, default_factory=lambda: 20)\n\n\nasync def test_arg_transform_default_factory_requires_hide():\n    \"\"\"Test that default_factory requires hide=True.\"\"\"\n    with pytest.raises(\n        ValueError, match=\"default_factory can only be used with hide=True\"\n    ):\n        ArgTransform(default_factory=lambda: 10)\n\n\nasync def test_arg_transform_required_true(add_tool):\n    \"\"\"Test ArgTransform with required=True.\"\"\"\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_args={\"old_y\": ArgTransform(required=True)},\n    )\n\n    # old_y should now be required (even though it had a default)\n    assert \"old_y\" in new_tool.parameters[\"required\"]\n\n\nasync def test_arg_transform_required_false():\n    \"\"\"Test ArgTransform with required=False by setting a default.\"\"\"\n\n    def func(x: int, y: int) -> int:\n        return x + y\n\n    tool = Tool.from_function(func)\n    # Setting a default makes it not required\n    new_tool = Tool.from_tool(tool, transform_args={\"y\": ArgTransform(default=0)})\n\n    # y should not be required since it has a default\n    assert \"y\" not in new_tool.parameters.get(\"required\", [])\n\n\nasync def test_arg_transform_required_with_rename(add_tool):\n    \"\"\"Test ArgTransform with required and rename.\"\"\"\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_args={\"old_y\": ArgTransform(name=\"new_y\", required=True)},\n    )\n\n    # new_y should be required\n    assert \"new_y\" in new_tool.parameters[\"required\"]\n    assert \"old_y\" not in new_tool.parameters[\"properties\"]\n\n\nasync def test_arg_transform_required_true_with_default_raises_error():\n    \"\"\"Test that required=True with default raises an error.\"\"\"\n    with pytest.raises(\n        ValueError, match=\"Cannot specify 'required=True' with 'default'\"\n    ):\n        ArgTransform(required=True, default=42)\n\n\nasync def test_arg_transform_required_true_with_factory_raises_error():\n    \"\"\"Test that required=True with default_factory raises an error.\"\"\"\n    with pytest.raises(\n        ValueError, match=\"default_factory can only be used with hide=True\"\n    ):\n        ArgTransform(required=True, default_factory=lambda: 42)\n\n\nasync def test_arg_transform_required_no_change():\n    \"\"\"Test that not specifying required doesn't change existing required status.\"\"\"\n\n    def func(x: int, y: int) -> int:\n        return x + y\n\n    tool = Tool.from_function(func)\n    # Both x and y are required in original\n    assert \"x\" in tool.parameters[\"required\"]\n    assert \"y\" in tool.parameters[\"required\"]\n\n    # Not specifying required should keep x required\n    new_tool = Tool.from_tool(\n        tool, transform_args={\"x\": ArgTransform(description=\"Updated x\")}\n    )\n\n    # x should still be required, and y should still be\n    assert \"x\" in new_tool.parameters.get(\"required\", [])\n    assert \"y\" in new_tool.parameters[\"required\"]\n\n\nasync def test_arg_transform_hide_and_required_raises_error():\n    \"\"\"Test that hide=True and required=True together raises an error.\"\"\"\n    with pytest.raises(\n        ValueError, match=\"Cannot specify both 'hide=True' and 'required=True'\"\n    ):\n        ArgTransform(hide=True, required=True)\n\n\nclass TestEnableDisable:\n    async def test_transform_disabled_tool(self):\n        \"\"\"\n        Tests that a transformed tool can run even if the parent tool is disabled via server.\n        \"\"\"\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(x: int, y: int = 10) -> int:\n            return x + y\n\n        # Get the registered Tool object from the server\n        add_tool = await mcp._local_provider.get_tool(\"add\")\n        assert isinstance(add_tool, Tool)\n        new_add = Tool.from_tool(add_tool, name=\"new_add\")\n        mcp.add_tool(new_add)\n\n        # Disable original tool, but new_add should still work\n        mcp.disable(names={\"add\"}, components={\"tool\"})\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert {tool.name for tool in tools} == {\"new_add\"}\n\n            result = await client.call_tool(\"new_add\", {\"x\": 1, \"y\": 2})\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"3\"\n\n            with pytest.raises(ToolError):\n                await client.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n\n    async def test_disable_transformed_tool(self):\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(x: int, y: int = 10) -> int:\n            return x + y\n\n        # Get the registered Tool object from the server\n        add_tool = await mcp._local_provider.get_tool(\"add\")\n        assert isinstance(add_tool, Tool)\n        new_add = Tool.from_tool(add_tool, name=\"new_add\")\n        mcp.add_tool(new_add)\n\n        # Disable both tools via server\n        mcp.disable(names={\"add\"}, components={\"tool\"}).disable(\n            names={\"new_add\"}, components={\"tool\"}\n        )\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools) == 0\n\n            with pytest.raises(ToolError):\n                await client.call_tool(\"new_add\", {\"x\": 1, \"y\": 2})\n"
  },
  {
    "path": "tests/tools/tool_transform/test_metadata.py",
    "content": "from typing import Annotated, Any\n\nimport pytest\nfrom pydantic import Field\n\nfrom fastmcp.tools import Tool\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import (\n    ToolTransformConfig,\n)\n\n\ndef get_property(tool: Tool, name: str) -> dict[str, Any]:\n    return tool.parameters[\"properties\"][name]\n\n\n@pytest.fixture\ndef add_tool() -> FunctionTool:\n    def add(\n        old_x: Annotated[int, Field(description=\"old_x description\")], old_y: int = 10\n    ) -> int:\n        print(\"running!\")\n        return old_x + old_y\n\n    return Tool.from_function(add)\n\n\n@pytest.fixture\ndef sample_tool():\n    \"\"\"Sample tool for testing transformations.\"\"\"\n\n    def sample_func(x: int) -> str:\n        return f\"Result: {x}\"\n\n    return Tool.from_function(\n        sample_func,\n        name=\"sample_tool\",\n        title=\"Original Tool Title\",\n        description=\"Original description\",\n    )\n\n\n@pytest.fixture\ndef sample_tool_no_title():\n    \"\"\"Sample tool without title for testing.\"\"\"\n\n    def sample_func(x: int) -> str:\n        return f\"Result: {x}\"\n\n    return Tool.from_function(sample_func, name=\"no_title_tool\")\n\n\ndef test_transform_inherits_title(sample_tool):\n    \"\"\"Test that transformed tools inherit title when none specified.\"\"\"\n    transformed = Tool.from_tool(sample_tool)\n    assert transformed.title == \"Original Tool Title\"\n\n\ndef test_transform_overrides_title(sample_tool):\n    \"\"\"Test that transformed tools can override title.\"\"\"\n    transformed = Tool.from_tool(sample_tool, title=\"New Tool Title\")\n    assert transformed.title == \"New Tool Title\"\n\n\ndef test_transform_sets_title_to_none(sample_tool):\n    \"\"\"Test that transformed tools can explicitly set title to None.\"\"\"\n    transformed = Tool.from_tool(sample_tool, title=None)\n    assert transformed.title is None\n\n\ndef test_transform_inherits_none_title(sample_tool_no_title):\n    \"\"\"Test that transformed tools inherit None title.\"\"\"\n    transformed = Tool.from_tool(sample_tool_no_title)\n    assert transformed.title is None\n\n\ndef test_transform_adds_title_to_none(sample_tool_no_title):\n    \"\"\"Test that transformed tools can add title when parent has None.\"\"\"\n    transformed = Tool.from_tool(sample_tool_no_title, title=\"Added Title\")\n    assert transformed.title == \"Added Title\"\n\n\ndef test_transform_inherits_description(sample_tool):\n    \"\"\"Test that transformed tools inherit description when none specified.\"\"\"\n    transformed = Tool.from_tool(sample_tool)\n    assert transformed.description == \"Original description\"\n\n\ndef test_transform_overrides_description(sample_tool):\n    \"\"\"Test that transformed tools can override description.\"\"\"\n    transformed = Tool.from_tool(sample_tool, description=\"New description\")\n    assert transformed.description == \"New description\"\n\n\ndef test_transform_sets_description_to_none(sample_tool):\n    \"\"\"Test that transformed tools can explicitly set description to None.\"\"\"\n    transformed = Tool.from_tool(sample_tool, description=None)\n    assert transformed.description is None\n\n\ndef test_transform_inherits_none_description(sample_tool_no_title):\n    \"\"\"Test that transformed tools inherit None description.\"\"\"\n    transformed = Tool.from_tool(sample_tool_no_title)\n    assert transformed.description is None\n\n\ndef test_transform_adds_description_to_none(sample_tool_no_title):\n    \"\"\"Test that transformed tools can add description when parent has None.\"\"\"\n    transformed = Tool.from_tool(sample_tool_no_title, description=\"Added description\")\n    assert transformed.description == \"Added description\"\n\n\n# Meta transformation tests\ndef test_transform_inherits_meta(sample_tool):\n    \"\"\"Test that transformed tools inherit meta when none specified.\"\"\"\n    sample_tool.meta = {\"original\": True, \"version\": \"1.0\"}\n    transformed = Tool.from_tool(sample_tool)\n    assert transformed.meta == {\"original\": True, \"version\": \"1.0\"}\n\n\ndef test_transform_overrides_meta(sample_tool):\n    \"\"\"Test that transformed tools can override meta.\"\"\"\n    sample_tool.meta = {\"original\": True, \"version\": \"1.0\"}\n    transformed = Tool.from_tool(sample_tool, meta={\"custom\": True, \"priority\": \"high\"})\n    assert transformed.meta == {\"custom\": True, \"priority\": \"high\"}\n\n\ndef test_transform_sets_meta_to_none(sample_tool):\n    \"\"\"Test that transformed tools can explicitly set meta to None.\"\"\"\n    sample_tool.meta = {\"original\": True, \"version\": \"1.0\"}\n    transformed = Tool.from_tool(sample_tool, meta=None)\n    assert transformed.meta is None\n\n\ndef test_transform_inherits_none_meta(sample_tool_no_title):\n    \"\"\"Test that transformed tools inherit None meta.\"\"\"\n    sample_tool_no_title.meta = None\n    transformed = Tool.from_tool(sample_tool_no_title)\n    assert transformed.meta is None\n\n\ndef test_transform_adds_meta_to_none(sample_tool_no_title):\n    \"\"\"Test that transformed tools can add meta when parent has None.\"\"\"\n    sample_tool_no_title.meta = None\n    transformed = Tool.from_tool(sample_tool_no_title, meta={\"added\": True})\n    assert transformed.meta == {\"added\": True}\n\n\ndef test_tool_transform_config_inherits_meta(sample_tool):\n    \"\"\"Test that ToolTransformConfig inherits meta when unset.\"\"\"\n    sample_tool.meta = {\"original\": True, \"version\": \"1.0\"}\n    config = ToolTransformConfig(name=\"config_tool\")\n    transformed = config.apply(sample_tool)\n    assert transformed.meta == {\"original\": True, \"version\": \"1.0\"}\n\n\ndef test_tool_transform_config_overrides_meta(sample_tool):\n    \"\"\"Test that ToolTransformConfig can override meta.\"\"\"\n    sample_tool.meta = {\"original\": True, \"version\": \"1.0\"}\n    config = ToolTransformConfig(\n        name=\"config_tool\", meta={\"config\": True, \"priority\": \"high\"}\n    )\n    transformed = config.apply(sample_tool)\n    assert transformed.meta == {\"config\": True, \"priority\": \"high\"}\n\n\ndef test_tool_transform_config_removes_meta(sample_tool):\n    \"\"\"Test that ToolTransformConfig can remove meta with None.\"\"\"\n    sample_tool.meta = {\"original\": True, \"version\": \"1.0\"}\n    config = ToolTransformConfig(name=\"config_tool\", meta=None)\n    transformed = config.apply(sample_tool)\n    assert transformed.meta is None\n\n\n# Enabled field tests\ndef test_tool_transform_config_enabled_defaults_to_true(sample_tool):\n    \"\"\"Test that enabled defaults to True and no visibility metadata is set.\"\"\"\n    config = ToolTransformConfig(name=\"enabled_tool\")\n    transformed = config.apply(sample_tool)\n\n    # No visibility metadata should be set when enabled=True (default)\n    meta = transformed.meta or {}\n    internal = meta.get(\"fastmcp\", {}).get(\"_internal\", {})\n    assert \"visibility\" not in internal\n\n\ndef test_tool_transform_config_enabled_false_sets_visibility_metadata(sample_tool):\n    \"\"\"Test that enabled=False sets visibility metadata to hide the tool.\"\"\"\n    from fastmcp.server.transforms.visibility import is_enabled\n\n    config = ToolTransformConfig(name=\"disabled_tool\", enabled=False)\n    transformed = config.apply(sample_tool)\n\n    # The is_enabled helper should return False\n    assert is_enabled(transformed) is False\n\n    # Check the raw metadata structure\n    assert transformed.meta is not None\n    assert transformed.meta[\"fastmcp\"][\"_internal\"][\"visibility\"] is False\n\n\ndef test_tool_transform_config_enabled_true_explicit_sets_visibility(sample_tool):\n    \"\"\"Test that enabled=True explicitly sets visibility metadata to allow overriding earlier disables.\"\"\"\n    from fastmcp.server.transforms.visibility import is_enabled\n\n    config = ToolTransformConfig(name=\"explicit_enabled\", enabled=True)\n    transformed = config.apply(sample_tool)\n\n    # Visibility metadata should be set to True when enabled=True is explicit\n    # This allows later transforms to override earlier disables\n    assert is_enabled(transformed) is True\n    assert transformed.meta is not None\n    assert transformed.meta[\"fastmcp\"][\"_internal\"][\"visibility\"] is True\n\n\ndef test_tool_transform_config_enabled_false_preserves_existing_meta(sample_tool):\n    \"\"\"Test that enabled=False preserves existing meta while adding visibility.\"\"\"\n    from fastmcp.server.transforms.visibility import is_enabled\n\n    sample_tool.meta = {\"custom_key\": \"custom_value\"}\n    config = ToolTransformConfig(enabled=False)\n    transformed = config.apply(sample_tool)\n\n    # Original meta should be preserved\n    assert transformed.meta is not None\n    assert transformed.meta[\"custom_key\"] == \"custom_value\"\n    # Visibility should be set\n    assert is_enabled(transformed) is False\n\n\ndef test_tool_transform_config_enabled_false_merges_with_config_meta(sample_tool):\n    \"\"\"Test that enabled=False works with explicit meta override.\"\"\"\n    from fastmcp.server.transforms.visibility import is_enabled\n\n    sample_tool.meta = {\"original\": True}\n    config = ToolTransformConfig(meta={\"overridden\": True}, enabled=False)\n    transformed = config.apply(sample_tool)\n\n    # Config meta should override original\n    assert transformed.meta is not None\n    assert \"original\" not in transformed.meta\n    assert transformed.meta[\"overridden\"] is True\n    # But visibility should still be set\n    assert is_enabled(transformed) is False\n"
  },
  {
    "path": "tests/tools/tool_transform/test_schemas.py",
    "content": "from typing import Annotated, Any\n\nimport pytest\nfrom dirty_equals import IsList\nfrom inline_snapshot import snapshot\nfrom mcp.types import TextContent\nfrom pydantic import BaseModel, Field, TypeAdapter\n\nfrom fastmcp.tools import Tool, forward\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import (\n    ArgTransform,\n    TransformedTool,\n)\n\n\ndef get_property(tool: Tool, name: str) -> dict[str, Any]:\n    return tool.parameters[\"properties\"][name]\n\n\n@pytest.fixture\ndef add_tool() -> FunctionTool:\n    def add(\n        old_x: Annotated[int, Field(description=\"old_x description\")], old_y: int = 10\n    ) -> int:\n        print(\"running!\")\n        return old_x + old_y\n\n    return Tool.from_function(add)\n\n\nclass TestTransformToolOutputSchema:\n    \"\"\"Test output schema handling in transformed tools.\"\"\"\n\n    @pytest.fixture\n    def base_string_tool(self) -> FunctionTool:\n        \"\"\"Tool that returns a string (gets wrapped).\"\"\"\n\n        def string_tool(x: int) -> str:\n            return f\"Result: {x}\"\n\n        return Tool.from_function(string_tool)\n\n    @pytest.fixture\n    def base_dict_tool(self) -> FunctionTool:\n        \"\"\"Tool that returns a dict (object type, not wrapped).\"\"\"\n\n        def dict_tool(x: int) -> dict[str, int]:\n            return {\"value\": x}\n\n        return Tool.from_function(dict_tool)\n\n    def test_transform_inherits_parent_output_schema(self, base_string_tool):\n        \"\"\"Test that transformed tool inherits parent's output schema by default.\"\"\"\n        new_tool = Tool.from_tool(base_string_tool)\n\n        # Should inherit parent's wrapped string schema\n        expected_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"result\": {\"type\": \"string\"}},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n        assert new_tool.output_schema == expected_schema\n        assert new_tool.output_schema == base_string_tool.output_schema\n\n    def test_transform_with_explicit_output_schema_none(self, base_string_tool):\n        \"\"\"Test that output_schema=None sets output schema to None.\"\"\"\n        new_tool = Tool.from_tool(base_string_tool, output_schema=None)\n\n        assert new_tool.output_schema is None\n\n    async def test_transform_output_schema_none_runtime(self, base_string_tool):\n        \"\"\"Test runtime behavior with output_schema=None.\"\"\"\n        new_tool = Tool.from_tool(base_string_tool, output_schema=None)\n\n        # Debug: check that output_schema is actually None\n        assert new_tool.output_schema is None, (\n            f\"Expected None, got {new_tool.output_schema}\"\n        )\n\n        result = await new_tool.run({\"x\": 5})\n        # Even with output_schema=None, structured content should be generated via fallback logic\n        assert result.structured_content == {\"result\": \"Result: 5\"}\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Result: 5\"\n\n    def test_transform_with_explicit_output_schema_dict(self, base_string_tool):\n        \"\"\"Test that explicit output schema overrides parent.\"\"\"\n        custom_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"message\": {\"type\": \"string\"}},\n        }\n        new_tool = Tool.from_tool(base_string_tool, output_schema=custom_schema)\n\n        assert new_tool.output_schema == custom_schema\n        assert new_tool.output_schema != base_string_tool.output_schema\n\n    async def test_transform_explicit_schema_runtime(self, base_string_tool):\n        \"\"\"Test runtime behavior with explicit output schema.\"\"\"\n        custom_schema = {\"type\": \"string\", \"minLength\": 1}\n        new_tool = Tool.from_tool(base_string_tool, output_schema=custom_schema)\n\n        result = await new_tool.run({\"x\": 10})\n        # Non-object explicit schemas disable structured content\n        assert result.structured_content is None\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Result: 10\"\n\n    def test_transform_with_custom_function_inferred_schema(self, base_dict_tool):\n        \"\"\"Test that custom function's output schema is inferred.\"\"\"\n\n        async def custom_fn(x: int) -> str:\n            result = await forward(x=x)\n            assert isinstance(result.content[0], TextContent)\n            return f\"Custom: {result.content[0].text}\"\n\n        new_tool = Tool.from_tool(base_dict_tool, transform_fn=custom_fn)\n\n        # Should infer string schema from custom function and wrap it\n        expected_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"result\": {\"type\": \"string\"}},\n            \"required\": [\"result\"],\n            \"x-fastmcp-wrap-result\": True,\n        }\n        assert new_tool.output_schema == expected_schema\n\n    async def test_transform_custom_function_runtime(self, base_dict_tool):\n        \"\"\"Test runtime behavior with custom function that has inferred schema.\"\"\"\n\n        async def custom_fn(x: int) -> str:\n            result = await forward(x=x)\n            assert isinstance(result.content[0], TextContent)\n            return f\"Custom: {result.content[0].text}\"\n\n        new_tool = Tool.from_tool(base_dict_tool, transform_fn=custom_fn)\n\n        result = await new_tool.run({\"x\": 3})\n        # Should wrap string result\n        assert result.structured_content == {\"result\": 'Custom: {\"value\":3}'}\n\n    def test_transform_custom_function_fallback_to_parent(self, base_string_tool):\n        \"\"\"Test that custom function without output annotation falls back to parent.\"\"\"\n\n        async def custom_fn(x: int):\n            # No return annotation - should fallback to parent schema\n            result = await forward(x=x)\n            return result\n\n        new_tool = Tool.from_tool(base_string_tool, transform_fn=custom_fn)\n\n        # Should use parent's schema since custom function has no annotation\n        assert new_tool.output_schema == base_string_tool.output_schema\n\n    def test_transform_custom_function_explicit_overrides(self, base_string_tool):\n        \"\"\"Test that explicit output_schema overrides both custom function and parent.\"\"\"\n\n        async def custom_fn(x: int) -> dict[str, str]:\n            return {\"custom\": \"value\"}\n\n        explicit_schema = {\"type\": \"array\", \"items\": {\"type\": \"number\"}}\n        new_tool = Tool.from_tool(\n            base_string_tool, transform_fn=custom_fn, output_schema=explicit_schema\n        )\n\n        # Explicit schema should win\n        assert new_tool.output_schema == explicit_schema\n\n    async def test_transform_custom_function_object_return(self, base_string_tool):\n        \"\"\"Test custom function returning object type.\"\"\"\n\n        async def custom_fn(x: int) -> dict[str, int]:\n            await forward(x=x)\n            return {\"original\": x, \"transformed\": x * 2}\n\n        new_tool = Tool.from_tool(base_string_tool, transform_fn=custom_fn)\n\n        # Object types should not be wrapped\n        expected_schema = TypeAdapter(dict[str, int]).json_schema()\n        assert new_tool.output_schema == expected_schema\n        assert isinstance(new_tool.output_schema, dict)\n        assert \"x-fastmcp-wrap-result\" not in new_tool.output_schema\n\n        result = await new_tool.run({\"x\": 4})\n        # Direct value, not wrapped\n        assert result.structured_content == {\"original\": 4, \"transformed\": 8}\n\n    async def test_transform_preserves_wrap_marker_behavior(self, base_string_tool):\n        \"\"\"Test that wrap marker behavior is preserved through transformation.\"\"\"\n        new_tool = Tool.from_tool(base_string_tool)\n\n        result = await new_tool.run({\"x\": 7})\n        # Should wrap because parent schema has wrap marker\n        assert result.structured_content == {\"result\": \"Result: 7\"}\n        assert isinstance(new_tool.output_schema, dict)\n        assert \"x-fastmcp-wrap-result\" in new_tool.output_schema\n\n    def test_transform_chained_output_schema_inheritance(self, base_string_tool):\n        \"\"\"Test output schema inheritance through multiple transformations.\"\"\"\n        # First transformation keeps parent schema\n        tool1 = Tool.from_tool(base_string_tool)\n        assert tool1.output_schema == base_string_tool.output_schema\n\n        # Second transformation also inherits\n        tool2 = Tool.from_tool(tool1)\n        assert (\n            tool2.output_schema == tool1.output_schema == base_string_tool.output_schema\n        )\n\n        # Third transformation with explicit override\n        custom_schema = {\"type\": \"number\"}\n        tool3 = Tool.from_tool(tool2, output_schema=custom_schema)\n        assert tool3.output_schema == custom_schema\n        assert tool3.output_schema != tool2.output_schema\n\n    async def test_transform_mixed_structured_unstructured_content(\n        self, base_string_tool\n    ):\n        \"\"\"Test transformation handling of mixed content types.\"\"\"\n\n        async def custom_fn(x: int):\n            # Return mixed content including ToolResult\n            if x == 1:\n                return [\"text\", {\"data\": x}]\n            else:\n                # Return ToolResult directly\n                return ToolResult(\n                    content=[TextContent(type=\"text\", text=f\"Custom: {x}\")],\n                    structured_content={\"custom_value\": x},\n                )\n\n        new_tool = Tool.from_tool(base_string_tool, transform_fn=custom_fn)\n\n        # Test mixed content return\n        result1 = await new_tool.run({\"x\": 1})\n        assert result1.structured_content == {\"result\": [\"text\", {\"data\": 1}]}\n\n        # Test ToolResult return\n        result2 = await new_tool.run({\"x\": 2})\n        assert result2.structured_content == {\"custom_value\": 2}\n        assert isinstance(result2.content[0], TextContent)\n        assert result2.content[0].text == \"Custom: 2\"\n\n    def test_transform_output_schema_with_arg_transforms(self, base_string_tool):\n        \"\"\"Test that output schema works correctly with argument transformations.\"\"\"\n\n        async def custom_fn(new_x: int) -> dict[str, str]:\n            result = await forward(new_x=new_x)\n            assert isinstance(result.content[0], TextContent)\n            return {\"transformed\": result.content[0].text}\n\n        new_tool = Tool.from_tool(\n            base_string_tool,\n            transform_fn=custom_fn,\n            transform_args={\"x\": ArgTransform(name=\"new_x\")},\n        )\n\n        # Should infer object schema from custom function\n        expected_schema = TypeAdapter(dict[str, str]).json_schema()\n        assert new_tool.output_schema == expected_schema\n\n    async def test_transform_output_schema_default_vs_none(self, base_string_tool):\n        \"\"\"Test default (NotSet) vs explicit None behavior for output_schema in transforms.\"\"\"\n        # Default (NotSet) should use smart fallback (inherit from parent)\n        tool_default = Tool.from_tool(base_string_tool)  # default output_schema=NotSet\n        assert tool_default.output_schema == base_string_tool.output_schema  # Inherits\n\n        # None should explicitly set output_schema to None but still generate structured content via fallback\n        tool_explicit_none = Tool.from_tool(base_string_tool, output_schema=None)\n        assert tool_explicit_none.output_schema is None\n\n        # Both should generate structured content now (via different paths)\n        result_default = await tool_default.run({\"x\": 5})\n        result_explicit_none = await tool_explicit_none.run({\"x\": 5})\n\n        assert result_default.structured_content == {\n            \"result\": \"Result: 5\"\n        }  # Inherits wrapping\n        assert result_explicit_none.structured_content == {\n            \"result\": \"Result: 5\"\n        }  # Generated via fallback logic\n        assert isinstance(result_default.content[0], TextContent)\n        assert isinstance(result_explicit_none.content[0], TextContent)\n        assert result_default.content[0].text == result_explicit_none.content[0].text\n\n    async def test_transform_output_schema_with_tool_result_return(\n        self, base_string_tool\n    ):\n        \"\"\"Test transform when custom function returns ToolResult directly.\"\"\"\n\n        async def custom_fn(x: int) -> ToolResult:\n            # Custom function returns ToolResult - should bypass schema handling\n            return ToolResult(\n                content=[TextContent(type=\"text\", text=f\"Direct: {x}\")],\n                structured_content={\"direct_value\": x, \"doubled\": x * 2},\n            )\n\n        new_tool = Tool.from_tool(base_string_tool, transform_fn=custom_fn)\n\n        # ToolResult return type should result in None output schema\n        assert new_tool.output_schema is None\n\n        result = await new_tool.run({\"x\": 6})\n        # Should use ToolResult content directly\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Direct: 6\"\n        assert result.structured_content == {\"direct_value\": 6, \"doubled\": 12}\n\n\nclass TestInputSchema:\n    \"\"\"Test schema definition handling and reference finding.\"\"\"\n\n    def test_arg_transform_examples_in_schema(self, add_tool: Tool):\n        # Simple example\n        new_tool = Tool.from_tool(\n            add_tool,\n            transform_args={\n                \"old_x\": ArgTransform(examples=[1, 2, 3]),\n            },\n        )\n        prop = get_property(new_tool, \"old_x\")\n        assert prop[\"examples\"] == [1, 2, 3]\n\n        # Nested example (e.g., for array type)\n        new_tool2 = Tool.from_tool(\n            add_tool,\n            transform_args={\n                \"old_x\": ArgTransform(examples=[[\"a\", \"b\"], [\"c\", \"d\"]]),\n            },\n        )\n        prop2 = get_property(new_tool2, \"old_x\")\n        assert prop2[\"examples\"] == [[\"a\", \"b\"], [\"c\", \"d\"]]\n\n        # If not set, should not be present\n        new_tool3 = Tool.from_tool(\n            add_tool,\n            transform_args={\n                \"old_x\": ArgTransform(),\n            },\n        )\n        prop3 = get_property(new_tool3, \"old_x\")\n        assert \"examples\" not in prop3\n\n    def test_merge_schema_with_defs_precedence(self):\n        \"\"\"Test _merge_schema_with_precedence merges $defs correctly.\n\n        Note: compress_schema no longer dereferences $ref by default.\n        Used definitions are kept in $defs; unused definitions are pruned.\n        \"\"\"\n        base_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"field1\": {\"$ref\": \"#/$defs/BaseType\"}},\n            \"$defs\": {\n                \"BaseType\": {\"type\": \"string\", \"description\": \"base\"},\n                \"SharedType\": {\"type\": \"integer\", \"minimum\": 0},\n            },\n        }\n\n        override_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"field2\": {\"$ref\": \"#/$defs/OverrideType\"}},\n            \"$defs\": {\n                \"OverrideType\": {\"type\": \"boolean\"},\n                \"SharedType\": {\"type\": \"integer\", \"minimum\": 10},  # Override\n            },\n        }\n\n        transformed_tool_schema = TransformedTool._merge_schema_with_precedence(\n            base_schema, override_schema\n        )\n\n        # SharedType should no longer be present on the schema (unused)\n        assert \"SharedType\" not in transformed_tool_schema.get(\"$defs\", {})\n\n        # $ref and $defs are preserved for used definitions\n        assert transformed_tool_schema == snapshot(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"field1\": {\"$ref\": \"#/$defs/BaseType\"},\n                    \"field2\": {\"$ref\": \"#/$defs/OverrideType\"},\n                },\n                \"$defs\": {\n                    \"BaseType\": {\"type\": \"string\", \"description\": \"base\"},\n                    \"OverrideType\": {\"type\": \"boolean\"},\n                },\n                \"required\": [],\n                \"additionalProperties\": False,\n            }\n        )\n\n    def test_transform_tool_with_complex_defs_pruning(self):\n        \"\"\"Test that tool transformation properly handles hidden params.\n\n        Unused type definitions are pruned from $defs when their\n        corresponding parameters are hidden. Used types remain as $ref.\n        \"\"\"\n\n        class UsedType(BaseModel):\n            value: str\n\n        class UnusedType(BaseModel):\n            other: int\n\n        @Tool.from_function\n        def complex_tool(\n            used_param: UsedType, unused_param: UnusedType | None = None\n        ) -> str:\n            return used_param.value\n\n        # Transform to hide unused_param\n        transformed_tool: TransformedTool = Tool.from_tool(\n            complex_tool, transform_args={\"unused_param\": ArgTransform(hide=True)}\n        )\n\n        # UnusedType should be pruned from $defs, but UsedType remains\n        assert \"UnusedType\" not in transformed_tool.parameters.get(\"$defs\", {})\n\n        assert transformed_tool.parameters == snapshot(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"used_param\": {\"$ref\": \"#/$defs/UsedType\"},\n                },\n                \"$defs\": {\n                    \"UsedType\": {\n                        \"properties\": {\"value\": {\"type\": \"string\"}},\n                        \"required\": [\"value\"],\n                        \"type\": \"object\",\n                    },\n                },\n                \"required\": [\"used_param\"],\n                \"additionalProperties\": False,\n            }\n        )\n\n    def test_transform_with_custom_function_preserves_needed_types(self):\n        \"\"\"Test that custom transform functions preserve necessary type definitions.\"\"\"\n\n        class InputType(BaseModel):\n            data: str\n\n        class OutputType(BaseModel):\n            result: str\n\n        @Tool.from_function\n        def base_tool(input_data: InputType) -> OutputType:\n            return OutputType(result=input_data.data.upper())\n\n        async def transform_function(renamed_input: InputType):\n            return await forward(renamed_input=renamed_input)\n\n        # Transform with custom function and argument rename\n        transformed = Tool.from_tool(\n            base_tool,\n            transform_fn=transform_function,\n            transform_args={\"input_data\": ArgTransform(name=\"renamed_input\")},\n        )\n\n        # Used type definitions are preserved as $ref/$defs\n        assert transformed.parameters == snapshot(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"renamed_input\": {\"$ref\": \"#/$defs/InputType\"},\n                },\n                \"$defs\": {\n                    \"InputType\": {\n                        \"properties\": {\"data\": {\"type\": \"string\"}},\n                        \"required\": [\"data\"],\n                        \"type\": \"object\",\n                    },\n                },\n                \"required\": [\"renamed_input\"],\n                \"additionalProperties\": False,\n            }\n        )\n\n    def test_chained_transforms_inline_types(self):\n        \"\"\"Test that chained transformations produce correct schemas with $ref/$defs.\"\"\"\n\n        class TypeA(BaseModel):\n            a: str\n\n        class TypeB(BaseModel):\n            b: int\n\n        class TypeC(BaseModel):\n            c: bool\n\n        @Tool.from_function\n        def base_tool(param_a: TypeA, param_b: TypeB, param_c: TypeC) -> str:\n            return f\"{param_a.a}-{param_b.b}-{param_c.c}\"\n\n        # First transform: hide param_c\n        transform1 = Tool.from_tool(\n            base_tool,\n            transform_args={\"param_c\": ArgTransform(hide=True, default=TypeC(c=True))},\n        )\n\n        # TypeC should be pruned from $defs, TypeA and TypeB remain\n        assert \"TypeC\" not in transform1.parameters.get(\"$defs\", {})\n\n        assert transform1.parameters == snapshot(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"param_a\": {\"$ref\": \"#/$defs/TypeA\"},\n                    \"param_b\": {\"$ref\": \"#/$defs/TypeB\"},\n                },\n                \"$defs\": {\n                    \"TypeA\": {\n                        \"properties\": {\"a\": {\"type\": \"string\"}},\n                        \"required\": [\"a\"],\n                        \"type\": \"object\",\n                    },\n                    \"TypeB\": {\n                        \"properties\": {\"b\": {\"type\": \"integer\"}},\n                        \"required\": [\"b\"],\n                        \"type\": \"object\",\n                    },\n                },\n                \"required\": IsList(\"param_b\", \"param_a\", check_order=False),\n                \"additionalProperties\": False,\n            }\n        )\n\n        # Second transform: hide param_b\n        transform2 = Tool.from_tool(\n            transform1,\n            transform_args={\"param_b\": ArgTransform(hide=True, default=TypeB(b=42))},\n        )\n\n        # TypeB should be pruned from $defs, only TypeA remains\n        assert \"TypeB\" not in transform2.parameters.get(\"$defs\", {})\n\n        assert transform2.parameters == snapshot(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"param_a\": {\"$ref\": \"#/$defs/TypeA\"},\n                },\n                \"$defs\": {\n                    \"TypeA\": {\n                        \"properties\": {\"a\": {\"type\": \"string\"}},\n                        \"required\": [\"a\"],\n                        \"type\": \"object\",\n                    },\n                },\n                \"required\": [\"param_a\"],\n                \"additionalProperties\": False,\n            }\n        )\n"
  },
  {
    "path": "tests/tools/tool_transform/test_tool_transform.py",
    "content": "\"\"\"Core tool transform functionality.\"\"\"\n\nimport re\nfrom typing import Annotated, Any\n\nimport pytest\nfrom mcp.types import TextContent\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp import FastMCP\nfrom fastmcp.client.client import Client\nfrom fastmcp.tools import Tool, forward, forward_raw, tool\nfrom fastmcp.tools.base import ToolResult\nfrom fastmcp.tools.function_tool import FunctionTool\nfrom fastmcp.tools.tool_transform import (\n    ArgTransform,\n    TransformedTool,\n)\n\n\ndef get_property(tool: Tool, name: str) -> dict[str, Any]:\n    return tool.parameters[\"properties\"][name]\n\n\n@pytest.fixture\ndef add_tool() -> FunctionTool:\n    def add(\n        old_x: Annotated[int, Field(description=\"old_x description\")], old_y: int = 10\n    ) -> int:\n        print(\"running!\")\n        return old_x + old_y\n\n    return Tool.from_function(add)\n\n\ndef test_tool_from_tool_no_change(add_tool):\n    new_tool = Tool.from_tool(add_tool)\n    assert isinstance(new_tool, TransformedTool)\n    assert new_tool.parameters == add_tool.parameters\n    assert new_tool.name == add_tool.name\n    assert new_tool.description == add_tool.description\n\n\ndef test_from_tool_accepts_decorated_function():\n    @tool\n    def search(q: str, limit: int = 10) -> list[str]:\n        \"\"\"Search for items.\"\"\"\n        return [f\"Result {i} for {q}\" for i in range(limit)]\n\n    transformed = Tool.from_tool(\n        search,\n        name=\"find_items\",\n        transform_args={\"q\": ArgTransform(name=\"query\")},\n    )\n    assert isinstance(transformed, TransformedTool)\n    assert transformed.name == \"find_items\"\n    assert \"query\" in transformed.parameters[\"properties\"]\n    assert \"q\" not in transformed.parameters[\"properties\"]\n\n\ndef test_from_tool_accepts_plain_function():\n    def search(q: str, limit: int = 10) -> list[str]:\n        return [f\"Result {i} for {q}\" for i in range(limit)]\n\n    transformed = Tool.from_tool(\n        search,\n        name=\"find_items\",\n        transform_args={\"q\": ArgTransform(name=\"query\")},\n    )\n    assert isinstance(transformed, TransformedTool)\n    assert transformed.name == \"find_items\"\n    assert \"query\" in transformed.parameters[\"properties\"]\n\n\ndef test_from_tool_decorated_function_preserves_metadata():\n    @tool(description=\"Custom description\")\n    def search(q: str) -> list[str]:\n        \"\"\"Original description.\"\"\"\n        return []\n\n    transformed = Tool.from_tool(search)\n    assert transformed.parent_tool.description == \"Custom description\"\n\n\nasync def test_from_tool_decorated_function_runs(add_tool):\n    @tool\n    def add(x: int, y: int = 10) -> int:\n        return x + y\n\n    transformed = Tool.from_tool(\n        add,\n        transform_args={\"x\": ArgTransform(name=\"a\")},\n    )\n    result = await transformed.run(arguments={\"a\": 3, \"y\": 5})\n    assert result.structured_content == {\"result\": 8}\n\n\nasync def test_renamed_arg_description_is_maintained(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_x\": ArgTransform(name=\"new_x\")}\n    )\n    assert (\n        new_tool.parameters[\"properties\"][\"new_x\"][\"description\"] == \"old_x description\"\n    )\n\n\nasync def test_tool_defaults_are_maintained_on_unmapped_args(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_x\": ArgTransform(name=\"new_x\")}\n    )\n    result = await new_tool.run(arguments={\"new_x\": 1})\n    # The parent tool returns int which gets wrapped as structured output\n    assert result.structured_content == {\"result\": 11}\n\n\nasync def test_tool_defaults_are_maintained_on_mapped_args(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_y\": ArgTransform(name=\"new_y\")}\n    )\n    result = await new_tool.run(arguments={\"old_x\": 1})\n    # The parent tool returns int which gets wrapped as structured output\n    assert result.structured_content == {\"result\": 11}\n\n\ndef test_tool_change_arg_name(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_x\": ArgTransform(name=\"new_x\")}\n    )\n\n    assert sorted(new_tool.parameters[\"properties\"]) == [\"new_x\", \"old_y\"]\n    assert get_property(new_tool, \"new_x\") == get_property(add_tool, \"old_x\")\n    assert get_property(new_tool, \"old_y\") == get_property(add_tool, \"old_y\")\n    assert new_tool.parameters[\"required\"] == [\"new_x\"]\n\n\ndef test_tool_change_arg_description(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_x\": ArgTransform(description=\"new description\")}\n    )\n    assert get_property(new_tool, \"old_x\")[\"description\"] == \"new description\"\n\n\nasync def test_tool_drop_arg(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_y\": ArgTransform(hide=True)}\n    )\n    assert sorted(new_tool.parameters[\"properties\"]) == [\"old_x\"]\n    result = await new_tool.run(arguments={\"old_x\": 1})\n    assert result.structured_content == {\"result\": 11}\n\n\nasync def test_dropped_args_error_if_provided(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_y\": ArgTransform(hide=True)}\n    )\n    with pytest.raises(\n        TypeError, match=\"Got unexpected keyword argument\\\\(s\\\\): old_y\"\n    ):\n        await new_tool.run(arguments={\"old_x\": 1, \"old_y\": 2})\n\n\nasync def test_hidden_arg_with_constant_default(add_tool):\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_y\": ArgTransform(hide=True)}\n    )\n    result = await new_tool.run(arguments={\"old_x\": 1})\n    # old_y should use its default value of 10\n    assert result.structured_content == {\"result\": 11}\n\n\nasync def test_hidden_arg_without_default_uses_parent_default(add_tool):\n    \"\"\"Test that hidden argument without default uses parent's default.\"\"\"\n    new_tool = Tool.from_tool(\n        add_tool, transform_args={\"old_y\": ArgTransform(hide=True)}\n    )\n    # Only old_x should be exposed\n    assert sorted(new_tool.parameters[\"properties\"]) == [\"old_x\"]\n    # Should pass old_x=3 and let parent use its default old_y=10\n    result = await new_tool.run(arguments={\"old_x\": 3})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"13\"\n    assert result.structured_content == {\"result\": 13}\n\n\nasync def test_mixed_hidden_args_with_custom_function(add_tool):\n    async def custom_fn(new_x: int, **kwargs) -> str:\n        result = await forward(new_x=new_x, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return f\"Custom: {result.content[0].text}\"\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\n            \"old_x\": ArgTransform(name=\"new_x\"),\n            \"old_y\": ArgTransform(hide=True),\n        },\n    )\n\n    result = await new_tool.run(arguments={\"new_x\": 5})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"Custom: 15\"\n\n\nasync def test_hide_required_param_without_default_raises_error():\n    \"\"\"Test that hiding a required parameter without providing default raises error.\"\"\"\n\n    @Tool.from_function\n    def tool_with_required_param(required_param: int, optional_param: int = 10) -> int:\n        return required_param + optional_param\n\n    # This should raise an error because required_param has no default and we're not providing one\n    with pytest.raises(\n        ValueError,\n        match=r\"Hidden parameter 'required_param' has no default value in parent tool\",\n    ):\n        Tool.from_tool(\n            tool_with_required_param,\n            transform_args={\"required_param\": ArgTransform(hide=True)},\n        )\n\n\nasync def test_hide_required_param_with_user_default_works():\n    \"\"\"Test that hiding a required parameter works when user provides a default.\"\"\"\n\n    @Tool.from_function\n    def tool_with_required_param(required_param: int, optional_param: int = 10) -> int:\n        return required_param + optional_param\n\n    # This should work because we're providing a default for the hidden required param\n    new_tool = Tool.from_tool(\n        tool_with_required_param,\n        transform_args={\"required_param\": ArgTransform(hide=True, default=5)},\n    )\n\n    # Only optional_param should be exposed\n    assert sorted(new_tool.parameters[\"properties\"]) == [\"optional_param\"]\n    # Should pass required_param=5 and optional_param=20 to parent\n    result = await new_tool.run(arguments={\"optional_param\": 20})\n    assert result.structured_content == {\"result\": 25}\n\n\nasync def test_hidden_param_prunes_defs():\n    class VisibleType(BaseModel):\n        x: int\n\n    class HiddenType(BaseModel):\n        y: int\n\n    @Tool.from_function\n    def tool_with_refs(a: VisibleType, b: HiddenType | None = None) -> int:\n        return a.x + (b.y if b else 0)\n\n    # Hide parameter 'b'\n    new_tool = Tool.from_tool(\n        tool_with_refs, transform_args={\"b\": ArgTransform(hide=True)}\n    )\n\n    schema = new_tool.parameters\n    # Only 'a' should be visible\n    assert list(schema[\"properties\"].keys()) == [\"a\"]\n    # HiddenType should be pruned from $defs\n    assert \"HiddenType\" not in schema.get(\"$defs\", {})\n    # VisibleType should remain in $defs and be referenced via $ref\n    assert schema[\"properties\"][\"a\"] == {\"$ref\": \"#/$defs/VisibleType\"}\n    assert schema[\"$defs\"][\"VisibleType\"] == {\n        \"properties\": {\"x\": {\"type\": \"integer\"}},\n        \"required\": [\"x\"],\n        \"type\": \"object\",\n    }\n\n\nasync def test_forward_with_argument_mapping(add_tool):\n    async def custom_fn(new_x: int, **kwargs) -> str:\n        result = await forward(new_x=new_x, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return f\"Mapped: {result.content[0].text}\"\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\"old_x\": ArgTransform(name=\"new_x\")},\n    )\n\n    result = await new_tool.run(arguments={\"new_x\": 3, \"old_y\": 7})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"Mapped: 10\"\n\n\nasync def test_forward_with_incorrect_args_raises_error(add_tool):\n    async def custom_fn(new_x: int, new_y: int = 5) -> ToolResult:\n        # the forward should use the new args, not the old ones\n        return await forward(old_x=new_x, old_y=new_y)\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\n            \"old_x\": ArgTransform(name=\"new_x\"),\n            \"old_y\": ArgTransform(name=\"new_y\"),\n        },\n    )\n    with pytest.raises(\n        TypeError, match=re.escape(\"Got unexpected keyword argument(s): old_x, old_y\")\n    ):\n        await new_tool.run(arguments={\"new_x\": 2, \"new_y\": 3})\n\n\nasync def test_forward_raw_without_argument_mapping(add_tool):\n    async def custom_fn(**kwargs) -> str:\n        # forward_raw passes through kwargs as-is\n        result = await forward_raw(**kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return f\"Raw: {result.content[0].text}\"\n\n    new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)\n\n    result = await new_tool.run(arguments={\"old_x\": 2, \"old_y\": 8})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"Raw: 10\"\n\n\nasync def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):\n    async def custom_fn(**kwargs) -> str:\n        result = await forward(**kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return f\"Custom: {result.content[0].text}\"\n\n    new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)\n\n    result = await new_tool.run(arguments={\"old_x\": 4, \"old_y\": 6})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"Custom: 10\"\n\n\nasync def test_fn_with_kwargs_passes_through_original_args(add_tool):\n    async def custom_fn(**kwargs) -> str:\n        # Should receive original arg names\n        assert \"old_x\" in kwargs\n        assert \"old_y\" in kwargs\n        result = await forward(**kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return result.content[0].text\n\n    new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)\n\n    result = await new_tool.run(arguments={\"old_x\": 1, \"old_y\": 2})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"3\"\n\n\nasync def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):\n    \"\"\"Test that **kwargs receives arguments with their transformed names from transform_args.\"\"\"\n\n    async def custom_fn(new_x: int, **kwargs) -> ToolResult:\n        # kwargs should contain 'old_y': 3 (transformed name), not 'old_y': 3 (original name)\n        assert kwargs == {\"old_y\": 3}\n        result = await forward(new_x=new_x, **kwargs)\n        return result\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\"old_x\": ArgTransform(name=\"new_x\")},\n    )\n    result = await new_tool.run(arguments={\"new_x\": 2, \"old_y\": 3})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"5\"\n    assert result.structured_content == {\"result\": 5}\n\n\nasync def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):\n    async def custom_fn(new_x: int, **kwargs) -> str:\n        result = await forward(new_x=new_x, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return result.content[0].text\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\"old_x\": ArgTransform(name=\"new_x\")},\n    )\n\n    # Only provide new_x, old_y should use default\n    result = await new_tool.run(arguments={\"new_x\": 7})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"17\"  # 7 + 10 (default)\n\n\nasync def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):\n    async def custom_fn(new_x: int, old_y: int, **kwargs) -> str:\n        result = await forward(new_x=new_x, old_y=old_y, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return result.content[0].text\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\"old_x\": ArgTransform(name=\"new_x\")},\n    )\n\n    result = await new_tool.run(arguments={\"new_x\": 2, \"old_y\": 8})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"10\"\n\n\nasync def test_fn_with_kwargs_dropped_args_not_in_kwargs(add_tool):\n    async def custom_fn(new_x: int, **kwargs) -> str:\n        # old_y is dropped, so it shouldn't be in kwargs\n        assert \"old_y\" not in kwargs\n        result = await forward(new_x=new_x, **kwargs)\n        assert isinstance(result.content[0], TextContent)\n        return result.content[0].text\n\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=custom_fn,\n        transform_args={\n            \"old_x\": ArgTransform(name=\"new_x\"),\n            \"old_y\": ArgTransform(hide=True),\n        },\n    )\n\n    result = await new_tool.run(arguments={\"new_x\": 3})\n    assert isinstance(result.content[0], TextContent)\n    assert result.content[0].text == \"13\"  # 3 + 10 (default for hidden old_y)\n\n\nasync def test_forward_outside_context_raises_error():\n    \"\"\"Test that forward() raises error when called outside transform context.\"\"\"\n    with pytest.raises(RuntimeError, match=r\"forward\\(\\) can only be called\"):\n        await forward(x=1)\n\n\nasync def test_forward_raw_outside_context_raises_error():\n    \"\"\"Test that forward_raw() raises error when called outside transform context.\"\"\"\n    with pytest.raises(RuntimeError, match=r\"forward_raw\\(\\) can only be called\"):\n        await forward_raw(x=1)\n\n\ndef test_transform_args_with_parent_defaults():\n    \"\"\"Test that transform_args with parent defaults works.\"\"\"\n\n    class CoolModel(BaseModel):\n        x: int = 10\n\n    def parent_tool(cool_model: CoolModel) -> int:\n        return cool_model.x\n\n    tool = Tool.from_function(parent_tool)\n\n    new_tool = Tool.from_tool(tool)\n\n    # Both tools should have the same schema (with $ref/$defs preserved)\n    assert new_tool.parameters == tool.parameters\n\n\ndef test_transform_args_validation_unknown_arg(add_tool):\n    \"\"\"Test that transform_args with unknown arguments raises ValueError.\"\"\"\n    with pytest.raises(\n        ValueError, match=\"Unknown arguments in transform_args: unknown_param\"\n    ) as exc_info:\n        Tool.from_tool(\n            add_tool, transform_args={\"unknown_param\": ArgTransform(name=\"new_name\")}\n        )\n\n    assert \"`add`\" in str(exc_info.value)\n\n\ndef test_transform_args_creates_duplicate_names(add_tool):\n    \"\"\"Test that transform_args creating duplicate parameter names raises ValueError.\"\"\"\n    with pytest.raises(\n        ValueError,\n        match=\"Multiple arguments would be mapped to the same names: same_name\",\n    ):\n        Tool.from_tool(\n            add_tool,\n            transform_args={\n                \"old_x\": ArgTransform(name=\"same_name\"),\n                \"old_y\": ArgTransform(name=\"same_name\"),\n            },\n        )\n\n\ndef test_transform_args_collision_with_passthrough_name(add_tool):\n    \"\"\"Test that renaming to a passthrough parameter name raises ValueError.\"\"\"\n    with pytest.raises(\n        ValueError,\n        match=\"Multiple arguments would be mapped to the same names: old_y\",\n    ):\n        Tool.from_tool(\n            add_tool,\n            transform_args={\n                \"old_x\": ArgTransform(name=\"old_y\"),\n            },\n        )\n\n\ndef test_function_without_kwargs_missing_params(add_tool):\n    \"\"\"Test that function missing required transformed parameters raises ValueError.\"\"\"\n\n    def invalid_fn(new_x: int, non_existent: str) -> str:\n        return f\"{new_x}_{non_existent}\"\n\n    with pytest.raises(\n        ValueError,\n        match=\"Function missing parameters required after transformation: new_y\",\n    ):\n        Tool.from_tool(\n            add_tool,\n            transform_fn=invalid_fn,\n            transform_args={\n                \"old_x\": ArgTransform(name=\"new_x\"),\n                \"old_y\": ArgTransform(name=\"new_y\"),\n            },\n        )\n\n\ndef test_function_without_kwargs_can_have_extra_params(add_tool):\n    \"\"\"Test that function can have extra parameters not in parent tool.\"\"\"\n\n    def valid_fn(new_x: int, new_y: int, extra_param: str = \"default\") -> str:\n        return f\"{new_x}_{new_y}_{extra_param}\"\n\n    # Should work - extra_param is fine as long as it has a default\n    new_tool = Tool.from_tool(\n        add_tool,\n        transform_fn=valid_fn,\n        transform_args={\n            \"old_x\": ArgTransform(name=\"new_x\"),\n            \"old_y\": ArgTransform(name=\"new_y\"),\n        },\n    )\n\n    # The final schema should include all function parameters\n    assert \"new_x\" in new_tool.parameters[\"properties\"]\n    assert \"new_y\" in new_tool.parameters[\"properties\"]\n    assert \"extra_param\" in new_tool.parameters[\"properties\"]\n\n\ndef test_function_with_kwargs_can_add_params(add_tool):\n    \"\"\"Test that function with **kwargs can add new parameters.\"\"\"\n\n    async def valid_fn(extra_param: str, **kwargs) -> str:\n        result = await forward(**kwargs)\n        return f\"{extra_param}: {result}\"\n\n    # This should work fine - kwargs allows access to all transformed params\n    tool = Tool.from_tool(\n        add_tool,\n        transform_fn=valid_fn,\n        transform_args={\n            \"old_x\": ArgTransform(name=\"new_x\"),\n            \"old_y\": ArgTransform(name=\"new_y\"),\n        },\n    )\n\n    # extra_param is added, new_x and new_y are available\n    assert \"extra_param\" in tool.parameters[\"properties\"]\n    assert \"new_x\" in tool.parameters[\"properties\"]\n\n\nasync def test_from_tool_decorated_function_via_client():\n    @tool\n    def search(q: str, limit: int = 10) -> list[str]:\n        \"\"\"Search for items.\"\"\"\n        return [f\"Result {i} for {q}\" for i in range(limit)]\n\n    better_search = Tool.from_tool(\n        search,\n        name=\"find_items\",\n        transform_args={\n            \"q\": ArgTransform(name=\"query\", description=\"The search terms\"),\n        },\n    )\n\n    mcp = FastMCP(\"Server\")\n    mcp.add_tool(better_search)\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"find_items\", {\"query\": \"hello\", \"limit\": 3})\n        assert isinstance(result.content[0], TextContent)\n        assert \"Result 0 for hello\" in result.content[0].text\n\n\nclass TestProxy:\n    @pytest.fixture\n    def mcp_server(self) -> FastMCP:\n        mcp = FastMCP()\n\n        @mcp.tool\n        def add(old_x: int, old_y: int = 10) -> int:\n            return old_x + old_y\n\n        return mcp\n\n    @pytest.fixture\n    def proxy_server(self, mcp_server: FastMCP) -> FastMCP:\n        from fastmcp.client.transports import FastMCPTransport\n\n        proxy = FastMCP.as_proxy(FastMCPTransport(mcp_server))\n        return proxy\n\n    async def test_transform_proxy(self, proxy_server: FastMCP):\n        # when adding transformed tools to proxy servers. Needs separate investigation.\n\n        add_tool = await proxy_server.get_tool(\"add\")\n        assert add_tool is not None\n        new_add_tool = Tool.from_tool(\n            add_tool,\n            name=\"add_transformed\",\n            transform_args={\"old_x\": ArgTransform(name=\"new_x\")},\n        )\n        proxy_server.add_tool(new_add_tool)\n\n        async with Client(proxy_server) as client:\n            # The tool should be registered with its transformed name\n            result = await client.call_tool(\"add_transformed\", {\"new_x\": 1, \"old_y\": 2})\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"3\"\n"
  },
  {
    "path": "tests/utilities/__init__.py",
    "content": "\"\"\"Tests for utilities in the fastmcp package.\"\"\"\n"
  },
  {
    "path": "tests/utilities/json_schema_type/__init__.py",
    "content": ""
  },
  {
    "path": "tests/utilities/json_schema_type/test_advanced.py",
    "content": "\"\"\"Advanced JSON schema type conversion features.\"\"\"\n\nfrom dataclasses import Field\nfrom typing import Union\n\nimport pytest\nfrom pydantic import BaseModel, TypeAdapter, ValidationError\n\nfrom fastmcp.utilities.json_schema_type import (\n    _hash_schema,\n    _merge_defaults,\n    json_schema_to_type,\n)\n\n\ndef get_dataclass_field(type: type, field_name: str) -> Field:\n    return type.__dataclass_fields__[field_name]  # ty: ignore[unresolved-attribute]\n\n\nclass TestDefaultValues:\n    \"\"\"Test suite for default value handling.\"\"\"\n\n    @pytest.fixture\n    def simple_defaults(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\", \"default\": \"anonymous\"},\n                    \"age\": {\"type\": \"integer\", \"default\": 0},\n                },\n            }\n        )\n\n    @pytest.fixture\n    def nested_defaults(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"user\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\", \"default\": \"anonymous\"},\n                            \"settings\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"theme\": {\"type\": \"string\", \"default\": \"light\"}\n                                },\n                                \"default\": {\"theme\": \"dark\"},\n                            },\n                        },\n                        \"default\": {\"name\": \"guest\", \"settings\": {\"theme\": \"system\"}},\n                    }\n                },\n            }\n        )\n\n    def test_simple_defaults_empty_object(self, simple_defaults):\n        validator = TypeAdapter(simple_defaults)\n        result = validator.validate_python({})\n        assert result.name == \"anonymous\"\n        assert result.age == 0\n\n    def test_simple_defaults_partial_override(self, simple_defaults):\n        validator = TypeAdapter(simple_defaults)\n        result = validator.validate_python({\"name\": \"test\"})\n        assert result.name == \"test\"\n        assert result.age == 0\n\n    def test_nested_defaults_empty_object(self, nested_defaults):\n        validator = TypeAdapter(nested_defaults)\n        result = validator.validate_python({})\n        assert result.user.name == \"guest\"\n        assert result.user.settings.theme == \"system\"\n\n    def test_nested_defaults_partial_override(self, nested_defaults):\n        validator = TypeAdapter(nested_defaults)\n        result = validator.validate_python({\"user\": {\"name\": \"test\"}})\n        assert result.user.name == \"test\"\n        assert result.user.settings.theme == \"system\"\n\n\nclass TestCircularReferences:\n    \"\"\"Test suite for circular reference handling.\"\"\"\n\n    @pytest.fixture\n    def self_referential(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\"name\": {\"type\": \"string\"}, \"child\": {\"$ref\": \"#\"}},\n            }\n        )\n\n    @pytest.fixture\n    def mutually_recursive(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"definitions\": {\n                    \"Person\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\"},\n                            \"friend\": {\"$ref\": \"#/definitions/Pet\"},\n                        },\n                    },\n                    \"Pet\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\"},\n                            \"owner\": {\"$ref\": \"#/definitions/Person\"},\n                        },\n                    },\n                },\n                \"properties\": {\"person\": {\"$ref\": \"#/definitions/Person\"}},\n            }\n        )\n\n    def test_self_ref_single_level(self, self_referential):\n        validator = TypeAdapter(self_referential)\n        result = validator.validate_python(\n            {\"name\": \"parent\", \"child\": {\"name\": \"child\"}}\n        )\n        assert result.name == \"parent\"\n        assert result.child.name == \"child\"\n        assert result.child.child is None\n\n    def test_self_ref_multiple_levels(self, self_referential):\n        validator = TypeAdapter(self_referential)\n        result = validator.validate_python(\n            {\n                \"name\": \"grandparent\",\n                \"child\": {\"name\": \"parent\", \"child\": {\"name\": \"child\"}},\n            }\n        )\n        assert result.name == \"grandparent\"\n        assert result.child.name == \"parent\"\n        assert result.child.child.name == \"child\"\n\n    def test_mutual_recursion_single_level(self, mutually_recursive):\n        validator = TypeAdapter(mutually_recursive)\n        result = validator.validate_python(\n            {\"person\": {\"name\": \"Alice\", \"friend\": {\"name\": \"Spot\"}}}\n        )\n        assert result.person.name == \"Alice\"\n        assert result.person.friend.name == \"Spot\"\n        assert result.person.friend.owner is None\n\n    def test_mutual_recursion_multiple_levels(self, mutually_recursive):\n        validator = TypeAdapter(mutually_recursive)\n        result = validator.validate_python(\n            {\n                \"person\": {\n                    \"name\": \"Alice\",\n                    \"friend\": {\"name\": \"Spot\", \"owner\": {\"name\": \"Bob\"}},\n                }\n            }\n        )\n        assert result.person.name == \"Alice\"\n        assert result.person.friend.name == \"Spot\"\n        assert result.person.friend.owner.name == \"Bob\"\n\n\nclass TestIdentifierNormalization:\n    \"\"\"Test suite for handling non-standard property names.\"\"\"\n\n    @pytest.fixture\n    def special_chars(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"@type\": {\"type\": \"string\"},\n                    \"first-name\": {\"type\": \"string\"},\n                    \"last.name\": {\"type\": \"string\"},\n                    \"2nd_address\": {\"type\": \"string\"},\n                    \"$ref\": {\"type\": \"string\"},\n                },\n            }\n        )\n\n    def test_normalizes_special_chars(self, special_chars):\n        validator = TypeAdapter(special_chars)\n        result = validator.validate_python(\n            {\n                \"@type\": \"person\",\n                \"first-name\": \"Alice\",\n                \"last.name\": \"Smith\",\n                \"2nd_address\": \"456 Oak St\",\n                \"$ref\": \"12345\",\n            }\n        )\n        assert result.field_type == \"person\"  # @type -> field_type\n        assert result.first_name == \"Alice\"  # first-name -> first_name\n        assert result.last_name == \"Smith\"  # last.name -> last_name\n        assert (\n            result.field_2nd_address == \"456 Oak St\"\n        )  # 2nd_address -> field_2nd_address\n        assert result.field_ref == \"12345\"  # $ref -> field_ref\n\n\nclass TestConstantValues:\n    \"\"\"Test suite for constant value validation.\"\"\"\n\n    @pytest.fixture\n    def string_const(self):\n        return json_schema_to_type({\"type\": \"string\", \"const\": \"production\"})\n\n    @pytest.fixture\n    def number_const(self):\n        return json_schema_to_type({\"type\": \"number\", \"const\": 42.5})\n\n    @pytest.fixture\n    def boolean_const(self):\n        return json_schema_to_type({\"type\": \"boolean\", \"const\": True})\n\n    @pytest.fixture\n    def null_const(self):\n        return json_schema_to_type({\"type\": \"null\", \"const\": None})\n\n    @pytest.fixture\n    def object_with_consts(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"env\": {\"const\": \"production\"},\n                    \"version\": {\"const\": 1},\n                    \"enabled\": {\"const\": True},\n                },\n            }\n        )\n\n    def test_string_const_valid(self, string_const):\n        validator = TypeAdapter(string_const)\n        assert validator.validate_python(\"production\") == \"production\"\n\n    def test_string_const_invalid(self, string_const):\n        validator = TypeAdapter(string_const)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"development\")\n\n    def test_number_const_valid(self, number_const):\n        validator = TypeAdapter(number_const)\n        assert validator.validate_python(42.5) == 42.5\n\n    def test_number_const_invalid(self, number_const):\n        validator = TypeAdapter(number_const)\n        with pytest.raises(ValidationError):\n            validator.validate_python(42)\n\n    def test_boolean_const_valid(self, boolean_const):\n        validator = TypeAdapter(boolean_const)\n        assert validator.validate_python(True) is True\n\n    def test_boolean_const_invalid(self, boolean_const):\n        validator = TypeAdapter(boolean_const)\n        with pytest.raises(ValidationError):\n            validator.validate_python(False)\n\n    def test_null_const_valid(self, null_const):\n        validator = TypeAdapter(null_const)\n        assert validator.validate_python(None) is None\n\n    def test_null_const_invalid(self, null_const):\n        validator = TypeAdapter(null_const)\n        with pytest.raises(ValidationError):\n            validator.validate_python(False)\n\n    def test_object_consts_valid(self, object_with_consts):\n        validator = TypeAdapter(object_with_consts)\n        result = validator.validate_python(\n            {\"env\": \"production\", \"version\": 1, \"enabled\": True}\n        )\n        assert result.env == \"production\"\n        assert result.version == 1\n        assert result.enabled is True\n\n    def test_object_consts_invalid(self, object_with_consts):\n        validator = TypeAdapter(object_with_consts)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\n                {\n                    \"env\": \"production\",\n                    \"version\": 2,  # Wrong constant\n                    \"enabled\": True,\n                }\n            )\n\n\nclass TestSchemaCaching:\n    \"\"\"Test suite for schema caching behavior.\"\"\"\n\n    def test_identical_schemas_reuse_class(self):\n        schema = {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n\n        class1 = json_schema_to_type(schema)\n        class2 = json_schema_to_type(schema)\n        assert class1 is class2\n\n    def test_different_names_different_classes(self):\n        schema = {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n\n        class1 = json_schema_to_type(schema, name=\"Class1\")\n        class2 = json_schema_to_type(schema, name=\"Class2\")\n        assert class1 is not class2\n        assert class1.__name__ == \"Class1\"\n        assert class2.__name__ == \"Class2\"\n\n    def test_nested_schema_caching(self):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"nested\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n            },\n        }\n\n        class1 = json_schema_to_type(schema)\n        class2 = json_schema_to_type(schema)\n\n        # Both main classes and their nested classes should be identical\n        assert class1 is class2\n        assert (\n            get_dataclass_field(class1, \"nested\").type\n            is get_dataclass_field(class2, \"nested\").type\n        )\n\n\nclass TestSchemaHashing:\n    \"\"\"Test suite for schema hashing utility.\"\"\"\n\n    def test_deterministic_hash(self):\n        schema = {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n        hash1 = _hash_schema(schema)\n        hash2 = _hash_schema(schema)\n        assert hash1 == hash2\n        assert isinstance(hash1, str)\n        assert len(hash1) == 64  # SHA-256 hash length\n\n    def test_different_schemas_different_hashes(self):\n        schema1 = {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n        schema2 = {\"type\": \"object\", \"properties\": {\"age\": {\"type\": \"integer\"}}}\n        assert _hash_schema(schema1) != _hash_schema(schema2)\n\n    def test_order_independent_hash(self):\n        schema1 = {\"properties\": {\"name\": {\"type\": \"string\"}}, \"type\": \"object\"}\n        schema2 = {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n        assert _hash_schema(schema1) == _hash_schema(schema2)\n\n    def test_nested_schema_hash(self):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"nested\": {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n            },\n        }\n        hash1 = _hash_schema(schema)\n        assert isinstance(hash1, str)\n        assert len(hash1) == 64\n\n\nclass TestDefaultMerging:\n    \"\"\"Test suite for default value merging behavior.\"\"\"\n\n    def test_simple_merge(self):\n        defaults = {\"name\": \"anonymous\", \"age\": 0}\n        data = {\"name\": \"test\"}\n        result = _merge_defaults(data, {\"properties\": {}}, defaults)\n        assert result[\"name\"] == \"test\"\n        assert result[\"age\"] == 0\n\n    def test_nested_merge(self):\n        defaults = {\"user\": {\"name\": \"anonymous\", \"settings\": {\"theme\": \"light\"}}}\n        data = {\"user\": {\"name\": \"test\"}}\n        result = _merge_defaults(data, {\"properties\": {}}, defaults)\n        assert result[\"user\"][\"name\"] == \"test\"\n        assert result[\"user\"][\"settings\"][\"theme\"] == \"light\"\n\n    def test_array_merge(self):\n        defaults = {\n            \"items\": [\n                {\"name\": \"item1\", \"done\": False},\n                {\"name\": \"item2\", \"done\": False},\n            ]\n        }\n        data = {\"items\": [{\"name\": \"custom\", \"done\": True}]}\n        result = _merge_defaults(data, {\"properties\": {}}, defaults)\n        assert len(result[\"items\"]) == 1\n        assert result[\"items\"][0][\"name\"] == \"custom\"\n        assert result[\"items\"][0][\"done\"] is True\n\n    def test_empty_data_uses_defaults(self):\n        schema = {\n            \"properties\": {\n                \"user\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\", \"default\": \"anonymous\"},\n                        \"settings\": {\"type\": \"object\", \"default\": {\"theme\": \"light\"}},\n                    },\n                    \"default\": {\"name\": \"guest\", \"settings\": {\"theme\": \"dark\"}},\n                }\n            }\n        }\n        result = _merge_defaults({}, schema)\n        assert result[\"user\"][\"name\"] == \"guest\"\n        assert result[\"user\"][\"settings\"][\"theme\"] == \"dark\"\n\n    def test_property_level_defaults(self):\n        schema = {\n            \"properties\": {\n                \"name\": {\"type\": \"string\", \"default\": \"anonymous\"},\n                \"age\": {\"type\": \"integer\", \"default\": 0},\n            }\n        }\n        result = _merge_defaults({}, schema)\n        assert result[\"name\"] == \"anonymous\"\n        assert result[\"age\"] == 0\n\n    def test_nested_property_defaults(self):\n        schema = {\n            \"properties\": {\n                \"user\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\", \"default\": \"anonymous\"},\n                        \"settings\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"theme\": {\"type\": \"string\", \"default\": \"light\"}\n                            },\n                        },\n                    },\n                }\n            }\n        }\n        result = _merge_defaults({\"user\": {\"settings\": {}}}, schema)\n        assert result[\"user\"][\"name\"] == \"anonymous\"\n        assert result[\"user\"][\"settings\"][\"theme\"] == \"light\"\n\n    def test_default_priority(self):\n        schema = {\n            \"properties\": {\n                \"settings\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"theme\": {\"type\": \"string\", \"default\": \"light\"}},\n                    \"default\": {\"theme\": \"dark\"},\n                }\n            },\n            \"default\": {\"settings\": {\"theme\": \"system\"}},\n        }\n\n        # Test priority: data > parent default > object default > property default\n        result1 = _merge_defaults({}, schema)  # Uses schema default\n        assert result1[\"settings\"][\"theme\"] == \"system\"\n\n        result2 = _merge_defaults({\"settings\": {}}, schema)  # Uses object default\n        assert result2[\"settings\"][\"theme\"] == \"dark\"\n\n        result3 = _merge_defaults(\n            {\"settings\": {\"theme\": \"custom\"}}, schema\n        )  # Uses provided data\n        assert result3[\"settings\"][\"theme\"] == \"custom\"\n\n\nclass TestEdgeCases:\n    \"\"\"Test suite for edge cases and corner scenarios.\"\"\"\n\n    def test_empty_schema(self):\n        schema = {}\n        result = json_schema_to_type(schema)\n        assert result is object\n\n    def test_schema_without_type(self):\n        schema = {\"properties\": {\"name\": {\"type\": \"string\"}}}\n        Type = json_schema_to_type(schema)\n        validator = TypeAdapter(Type)\n        result = validator.validate_python({\"name\": \"test\"})\n        assert result.name == \"test\"  # type: ignore[attr-defined]\n\n    def test_recursive_defaults(self):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"node\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"value\": {\"type\": \"string\"}, \"next\": {\"$ref\": \"#\"}},\n                    \"default\": {\"value\": \"default\", \"next\": None},\n                }\n            },\n        }\n        Type = json_schema_to_type(schema)\n        validator = TypeAdapter(Type)\n        result = validator.validate_python({})\n        assert result.node.value == \"default\"  # type: ignore[attr-defined]\n        assert result.node.next is None  # type: ignore[attr-defined]\n\n    def test_mixed_type_array(self):\n        schema = {\n            \"type\": \"array\",\n            \"items\": [{\"type\": \"string\"}, {\"type\": \"number\"}, {\"type\": \"boolean\"}],\n        }\n        Type = json_schema_to_type(schema)\n        validator = TypeAdapter(Type)\n        result = validator.validate_python([\"test\", 123, True])\n        assert result == [\"test\", 123, True]\n\n\nclass TestNameHandling:\n    \"\"\"Test suite for schema name handling.\"\"\"\n\n    def test_name_from_title(self):\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"Person\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n        Type = json_schema_to_type(schema)\n        assert Type.__name__ == \"Person\"\n\n    def test_explicit_name_overrides_title(self):\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"Person\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n        Type = json_schema_to_type(schema, name=\"CustomPerson\")\n        assert Type.__name__ == \"CustomPerson\"\n\n    def test_default_name_without_title(self):\n        schema = {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}\n        Type = json_schema_to_type(schema)\n        assert Type.__name__ == \"Root\"\n\n    def test_name_only_allowed_for_objects(self):\n        schema = {\"type\": \"string\"}\n        with pytest.raises(ValueError, match=\"Can not apply name to non-object schema\"):\n            json_schema_to_type(schema, name=\"StringType\")\n\n    def test_nested_object_names(self):\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"Parent\",\n            \"properties\": {\n                \"child\": {\n                    \"type\": \"object\",\n                    \"title\": \"Child\",\n                    \"properties\": {\"name\": {\"type\": \"string\"}},\n                }\n            },\n        }\n        Type = json_schema_to_type(schema)\n        assert Type.__name__ == \"Parent\"\n        child_field_type = get_dataclass_field(Type, \"child\").type\n        assert child_field_type.__origin__ is Union  # ty: ignore[unresolved-attribute]\n        assert child_field_type.__args__[0].__name__ == \"Child\"  # ty: ignore[unresolved-attribute]\n        assert child_field_type.__args__[1] is type(None)  # ty: ignore[unresolved-attribute]\n\n    def test_recursive_schema_naming(self):\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"Node\",\n            \"properties\": {\"next\": {\"$ref\": \"#\"}},\n        }\n        Type = json_schema_to_type(schema)\n        assert Type.__name__ == \"Node\"\n\n        next_field_type = get_dataclass_field(Type, \"next\").type\n\n        assert next_field_type.__origin__ is Union  # ty: ignore[unresolved-attribute]\n        assert next_field_type.__args__[0].__forward_arg__ == \"Node\"  # ty: ignore[unresolved-attribute]\n        assert next_field_type.__args__[1] is type(None)  # ty: ignore[unresolved-attribute]\n\n    def test_name_caching_with_different_titles(self):\n        \"\"\"Ensure schemas with different titles create different cached classes\"\"\"\n        schema1 = {\n            \"type\": \"object\",\n            \"title\": \"Type1\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n        schema2 = {\n            \"type\": \"object\",\n            \"title\": \"Type2\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n        Type1 = json_schema_to_type(schema1)\n        Type2 = json_schema_to_type(schema2)\n        assert Type1 is not Type2\n        assert Type1.__name__ == \"Type1\"\n        assert Type2.__name__ == \"Type2\"\n\n    def test_recursive_schema_with_invalid_python_name(self):\n        \"\"\"Test that recursive schemas work with titles that aren't valid Python identifiers\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"My Complex Type!\",\n            \"properties\": {\"name\": {\"type\": \"string\"}, \"child\": {\"$ref\": \"#\"}},\n        }\n        Type = json_schema_to_type(schema)\n        # The class should get a sanitized name\n        assert Type.__name__ == \"My_Complex_Type\"\n        # Create an instance to verify the recursive reference works\n        validator = TypeAdapter(Type)\n        result = validator.validate_python(\n            {\"name\": \"parent\", \"child\": {\"name\": \"child\", \"child\": None}}\n        )\n        assert result.name == \"parent\"  # type: ignore[attr-defined]\n        assert result.child.name == \"child\"  # type: ignore[attr-defined]\n        assert result.child.child is None  # type: ignore[attr-defined]\n\n\nclass TestAdditionalProperties:\n    \"\"\"Test suite for additionalProperties handling.\"\"\"\n\n    @pytest.fixture\n    def dict_only_schema(self):\n        \"\"\"Schema with no properties but additionalProperties=True -> dict[str, Any]\"\"\"\n        return json_schema_to_type({\"type\": \"object\", \"additionalProperties\": True})\n\n    @pytest.fixture\n    def properties_with_additional(self):\n        \"\"\"Schema with properties AND additionalProperties=True -> BaseModel\"\"\"\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"age\": {\"type\": \"integer\"},\n                },\n                \"additionalProperties\": True,\n            }\n        )\n\n    @pytest.fixture\n    def properties_without_additional(self):\n        \"\"\"Schema with properties but no additionalProperties -> dataclass\"\"\"\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"age\": {\"type\": \"integer\"},\n                },\n            }\n        )\n\n    @pytest.fixture\n    def required_properties_with_additional(self):\n        \"\"\"Schema with required properties AND additionalProperties=True -> BaseModel\"\"\"\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"age\": {\"type\": \"integer\"},\n                },\n                \"required\": [\"name\"],\n                \"additionalProperties\": True,\n            }\n        )\n\n    def test_dict_only_returns_dict_type(self, dict_only_schema):\n        \"\"\"Test that schema with no properties + additionalProperties=True returns dict[str, Any]\"\"\"\n        import typing\n\n        assert dict_only_schema == dict[str, typing.Any]\n\n    def test_dict_only_accepts_any_data(self, dict_only_schema):\n        \"\"\"Test that pure dict accepts arbitrary key-value pairs\"\"\"\n        validator = TypeAdapter(dict_only_schema)\n        data = {\"anything\": \"works\", \"numbers\": 123, \"nested\": {\"key\": \"value\"}}\n        result = validator.validate_python(data)\n        assert result == data\n        assert isinstance(result, dict)\n\n    def test_properties_with_additional_returns_basemodel(\n        self, properties_with_additional\n    ):\n        \"\"\"Test that schema with properties + additionalProperties=True returns BaseModel\"\"\"\n        assert issubclass(properties_with_additional, BaseModel)\n\n    def test_properties_with_additional_accepts_extra_fields(\n        self, properties_with_additional\n    ):\n        \"\"\"Test that BaseModel with extra='allow' accepts additional properties\"\"\"\n        validator = TypeAdapter(properties_with_additional)\n        data = {\n            \"name\": \"Alice\",\n            \"age\": 30,\n            \"extra\": \"field\",\n            \"another\": {\"nested\": \"data\"},\n        }\n        result = validator.validate_python(data)\n\n        # Check standard properties\n        assert result.name == \"Alice\"\n        assert result.age == 30\n\n        # Check extra properties are preserved with dot access\n        assert hasattr(result, \"extra\")\n        assert result.extra == \"field\"\n        assert hasattr(result, \"another\")\n        assert result.another == {\"nested\": \"data\"}\n\n    def test_properties_with_additional_validates_known_fields(\n        self, properties_with_additional\n    ):\n        \"\"\"Test that BaseModel still validates known fields\"\"\"\n        validator = TypeAdapter(properties_with_additional)\n\n        # Should accept valid data\n        result = validator.validate_python({\"name\": \"Alice\", \"age\": 30, \"extra\": \"ok\"})\n        assert result.name == \"Alice\"\n        assert result.age == 30\n        assert result.extra == \"ok\"\n\n        # Should reject invalid types for known fields\n        with pytest.raises(ValidationError):\n            validator.validate_python({\"name\": \"Alice\", \"age\": \"not_a_number\"})\n\n    def test_properties_without_additional_is_dataclass(\n        self, properties_without_additional\n    ):\n        \"\"\"Test that schema with properties but no additionalProperties returns dataclass\"\"\"\n        assert not issubclass(properties_without_additional, BaseModel)\n        assert hasattr(properties_without_additional, \"__dataclass_fields__\")\n\n    def test_properties_without_additional_ignores_extra_fields(\n        self, properties_without_additional\n    ):\n        \"\"\"Test that dataclass ignores extra properties (current behavior)\"\"\"\n        validator = TypeAdapter(properties_without_additional)\n        data = {\"name\": \"Alice\", \"age\": 30, \"extra\": \"ignored\"}\n        result = validator.validate_python(data)\n\n        # Check standard properties\n        assert result.name == \"Alice\"\n        assert result.age == 30\n\n        # Check extra property is ignored\n        assert not hasattr(result, \"extra\")\n\n    def test_required_properties_with_additional(\n        self, required_properties_with_additional\n    ):\n        \"\"\"Test BaseModel with required fields and additional properties\"\"\"\n        validator = TypeAdapter(required_properties_with_additional)\n\n        # Should accept valid data with required field\n        result = validator.validate_python({\"name\": \"Alice\", \"extra\": \"field\"})\n        assert result.name == \"Alice\"\n        assert result.age is None  # Optional field\n        assert result.extra == \"field\"\n\n        # Should reject missing required field\n        with pytest.raises(ValidationError):\n            validator.validate_python({\"age\": 30, \"extra\": \"field\"})\n\n    def test_nested_additional_properties(self):\n        \"\"\"Test nested objects with additionalProperties\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"user\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"name\": {\"type\": \"string\"}},\n                    \"additionalProperties\": True,\n                },\n                \"settings\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"theme\": {\"type\": \"string\"}},\n                },\n            },\n            \"additionalProperties\": True,\n        }\n\n        Type = json_schema_to_type(schema)\n        validator = TypeAdapter(Type)\n\n        data = {\n            \"user\": {\"name\": \"Alice\", \"extra_user_field\": \"value\"},\n            \"settings\": {\"theme\": \"dark\", \"extra_settings_field\": \"ignored\"},\n            \"top_level_extra\": \"preserved\",\n        }\n\n        result = validator.validate_python(data)\n\n        # Check top-level extra field (BaseModel)\n        assert result.top_level_extra == \"preserved\"  # type: ignore[attr-defined]\n\n        # Check nested user extra field (BaseModel)\n        assert result.user.name == \"Alice\"  # type: ignore[attr-defined]\n        assert result.user.extra_user_field == \"value\"  # type: ignore[attr-defined]\n\n        # Check nested settings - should be dataclass\n        assert result.settings.theme == \"dark\"  # type: ignore[attr-defined]\n        # Note: When nested in BaseModel with extra='allow', Pydantic may preserve extra fields\n        # even on dataclass children. The important thing is that settings is still a dataclass.\n        assert not issubclass(type(result.settings), BaseModel)  # type: ignore[attr-defined]\n\n    def test_additional_properties_false_vs_missing(self):\n        \"\"\"Test difference between additionalProperties: false and missing additionalProperties\"\"\"\n        # Schema with explicit additionalProperties: false\n        schema_false = {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n            \"additionalProperties\": False,\n        }\n\n        # Schema with no additionalProperties key\n        schema_missing = {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n\n        Type_false = json_schema_to_type(schema_false)\n        Type_missing = json_schema_to_type(schema_missing)\n\n        # Both should create dataclasses (not BaseModel)\n        assert not issubclass(Type_false, BaseModel)\n        assert not issubclass(Type_missing, BaseModel)\n        assert hasattr(Type_false, \"__dataclass_fields__\")\n        assert hasattr(Type_missing, \"__dataclass_fields__\")\n\n    def test_additional_properties_with_defaults(self):\n        \"\"\"Test additionalProperties with default values\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\", \"default\": \"anonymous\"},\n                \"age\": {\"type\": \"integer\", \"default\": 0},\n            },\n            \"additionalProperties\": True,\n        }\n\n        Type = json_schema_to_type(schema)\n        validator = TypeAdapter(Type)\n\n        # Test with extra fields and defaults\n        result = validator.validate_python({\"extra\": \"field\"})\n        assert result.name == \"anonymous\"  # type: ignore[attr-defined]\n        assert result.age == 0  # type: ignore[attr-defined]\n        assert result.extra == \"field\"  # type: ignore[attr-defined]\n\n    def test_additional_properties_type_consistency(self):\n        \"\"\"Test that the same schema always returns the same type\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n            \"additionalProperties\": True,\n        }\n\n        Type1 = json_schema_to_type(schema)\n        Type2 = json_schema_to_type(schema)\n\n        # Should be the same cached class\n        assert Type1 is Type2\n        assert issubclass(Type1, BaseModel)\n\n\nclass TestFieldsWithDefaults:\n    \"\"\"Test suite for fields with default values not being made nullable.\"\"\"\n\n    def test_field_with_default_preserves_type(self):\n        \"\"\"Test that fields with defaults preserve their original type.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"flag\": {\"type\": \"boolean\", \"default\": False}},\n        }\n\n        generated_type = json_schema_to_type(schema)\n        regenerated_schema = TypeAdapter(generated_type).json_schema()\n\n        assert regenerated_schema[\"properties\"][\"flag\"][\"type\"] == \"boolean\"\n\n    def test_field_with_default_not_nullable(self):\n        \"\"\"Test that fields with defaults are not made nullable.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"flag\": {\"type\": \"boolean\", \"default\": False}},\n        }\n\n        generated_type = json_schema_to_type(schema)\n        regenerated_schema = TypeAdapter(generated_type).json_schema()\n\n        flag_prop = regenerated_schema[\"properties\"][\"flag\"]\n        assert \"anyOf\" not in flag_prop\n\n    def test_field_with_default_uses_default(self):\n        \"\"\"Test that fields with defaults use their default values.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"flag\": {\"type\": \"boolean\", \"default\": False}},\n        }\n\n        generated_type = json_schema_to_type(schema)\n        validator = TypeAdapter(generated_type)\n        result = validator.validate_python({})\n        assert result.flag is False  # type: ignore[attr-defined]\n\n    def test_field_with_default_accepts_explicit_value(self):\n        \"\"\"Test that fields with defaults accept explicit values.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"flag\": {\"type\": \"boolean\", \"default\": False}},\n        }\n\n        generated_type = json_schema_to_type(schema)\n        validator = TypeAdapter(generated_type)\n        result = validator.validate_python({\"flag\": True})\n        assert result.flag is True  # type: ignore[attr-defined]\n"
  },
  {
    "path": "tests/utilities/json_schema_type/test_constraints.py",
    "content": "\"\"\"Tests for type constraints in JSON schema conversion.\"\"\"\n\nfrom dataclasses import Field\n\nimport pytest\nfrom pydantic import TypeAdapter, ValidationError\n\nfrom fastmcp.utilities.json_schema_type import (\n    json_schema_to_type,\n)\n\n\ndef get_dataclass_field(type: type, field_name: str) -> Field:\n    return type.__dataclass_fields__[field_name]  # ty: ignore[unresolved-attribute]\n\n\nclass TestStringConstraints:\n    \"\"\"Test suite for string constraint validation.\"\"\"\n\n    @pytest.fixture\n    def min_length_string(self):\n        return json_schema_to_type({\"type\": \"string\", \"minLength\": 3})\n\n    @pytest.fixture\n    def max_length_string(self):\n        return json_schema_to_type({\"type\": \"string\", \"maxLength\": 5})\n\n    @pytest.fixture\n    def pattern_string(self):\n        return json_schema_to_type({\"type\": \"string\", \"pattern\": \"^[A-Z][a-z]+$\"})\n\n    @pytest.fixture\n    def email_string(self):\n        return json_schema_to_type({\"type\": \"string\", \"format\": \"email\"})\n\n    def test_min_length_accepts_valid(self, min_length_string):\n        validator = TypeAdapter(min_length_string)\n        assert validator.validate_python(\"test\") == \"test\"\n\n    def test_min_length_rejects_short(self, min_length_string):\n        validator = TypeAdapter(min_length_string)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"ab\")\n\n    def test_max_length_accepts_valid(self, max_length_string):\n        validator = TypeAdapter(max_length_string)\n        assert validator.validate_python(\"test\") == \"test\"\n\n    def test_max_length_rejects_long(self, max_length_string):\n        validator = TypeAdapter(max_length_string)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"toolong\")\n\n    def test_pattern_accepts_valid(self, pattern_string):\n        validator = TypeAdapter(pattern_string)\n        assert validator.validate_python(\"Hello\") == \"Hello\"\n\n    def test_pattern_rejects_invalid(self, pattern_string):\n        validator = TypeAdapter(pattern_string)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"hello\")\n\n    def test_email_accepts_valid(self, email_string):\n        validator = TypeAdapter(email_string)\n        result = validator.validate_python(\"test@example.com\")\n        assert result == \"test@example.com\"\n\n    def test_email_rejects_invalid(self, email_string):\n        validator = TypeAdapter(email_string)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not-an-email\")\n\n\nclass TestNumberConstraints:\n    \"\"\"Test suite for numeric constraint validation.\"\"\"\n\n    @pytest.fixture\n    def multiple_of_number(self):\n        return json_schema_to_type({\"type\": \"number\", \"multipleOf\": 0.5})\n\n    @pytest.fixture\n    def min_number(self):\n        return json_schema_to_type({\"type\": \"number\", \"minimum\": 0})\n\n    @pytest.fixture\n    def exclusive_min_number(self):\n        return json_schema_to_type({\"type\": \"number\", \"exclusiveMinimum\": 0})\n\n    @pytest.fixture\n    def max_number(self):\n        return json_schema_to_type({\"type\": \"number\", \"maximum\": 100})\n\n    @pytest.fixture\n    def exclusive_max_number(self):\n        return json_schema_to_type({\"type\": \"number\", \"exclusiveMaximum\": 100})\n\n    def test_multiple_of_accepts_valid(self, multiple_of_number):\n        validator = TypeAdapter(multiple_of_number)\n        assert validator.validate_python(2.5) == 2.5\n\n    def test_multiple_of_rejects_invalid(self, multiple_of_number):\n        validator = TypeAdapter(multiple_of_number)\n        with pytest.raises(ValidationError):\n            validator.validate_python(2.7)\n\n    def test_minimum_accepts_equal(self, min_number):\n        validator = TypeAdapter(min_number)\n        assert validator.validate_python(0) == 0\n\n    def test_minimum_rejects_less(self, min_number):\n        validator = TypeAdapter(min_number)\n        with pytest.raises(ValidationError):\n            validator.validate_python(-1)\n\n    def test_exclusive_minimum_rejects_equal(self, exclusive_min_number):\n        validator = TypeAdapter(exclusive_min_number)\n        with pytest.raises(ValidationError):\n            validator.validate_python(0)\n\n    def test_maximum_accepts_equal(self, max_number):\n        validator = TypeAdapter(max_number)\n        assert validator.validate_python(100) == 100\n\n    def test_maximum_rejects_greater(self, max_number):\n        validator = TypeAdapter(max_number)\n        with pytest.raises(ValidationError):\n            validator.validate_python(101)\n\n    def test_exclusive_maximum_rejects_equal(self, exclusive_max_number):\n        validator = TypeAdapter(exclusive_max_number)\n        with pytest.raises(ValidationError):\n            validator.validate_python(100)\n"
  },
  {
    "path": "tests/utilities/json_schema_type/test_containers.py",
    "content": "\"\"\"Tests for container types in JSON schema conversion.\"\"\"\n\nfrom dataclasses import Field, dataclass\nfrom typing import Any\n\nimport pytest\nfrom pydantic import TypeAdapter, ValidationError\n\nfrom fastmcp.utilities.json_schema_type import (\n    json_schema_to_type,\n)\n\n\ndef get_dataclass_field(type: type, field_name: str) -> Field:\n    return type.__dataclass_fields__[field_name]  # ty: ignore[unresolved-attribute]\n\n\nclass TestArrayTypes:\n    \"\"\"Test suite for array validation.\"\"\"\n\n    @pytest.fixture\n    def string_array(self):\n        return json_schema_to_type({\"type\": \"array\", \"items\": {\"type\": \"string\"}})\n\n    @pytest.fixture\n    def min_items_array(self):\n        return json_schema_to_type(\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"minItems\": 2}\n        )\n\n    @pytest.fixture\n    def max_items_array(self):\n        return json_schema_to_type(\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"maxItems\": 3}\n        )\n\n    @pytest.fixture\n    def unique_items_array(self):\n        return json_schema_to_type(\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"uniqueItems\": True}\n        )\n\n    def test_array_accepts_valid_items(self, string_array):\n        validator = TypeAdapter(string_array)\n        assert validator.validate_python([\"a\", \"b\"]) == [\"a\", \"b\"]\n\n    def test_array_rejects_invalid_items(self, string_array):\n        validator = TypeAdapter(string_array)\n        with pytest.raises(ValidationError):\n            validator.validate_python([1, \"b\"])\n\n    def test_min_items_accepts_valid(self, min_items_array):\n        validator = TypeAdapter(min_items_array)\n        assert validator.validate_python([\"a\", \"b\"]) == [\"a\", \"b\"]\n\n    def test_min_items_rejects_too_few(self, min_items_array):\n        validator = TypeAdapter(min_items_array)\n        with pytest.raises(ValidationError):\n            validator.validate_python([\"a\"])\n\n    def test_max_items_accepts_valid(self, max_items_array):\n        validator = TypeAdapter(max_items_array)\n        assert validator.validate_python([\"a\", \"b\", \"c\"]) == [\"a\", \"b\", \"c\"]\n\n    def test_max_items_rejects_too_many(self, max_items_array):\n        validator = TypeAdapter(max_items_array)\n        with pytest.raises(ValidationError):\n            validator.validate_python([\"a\", \"b\", \"c\", \"d\"])\n\n    def test_unique_items_accepts_unique(self, unique_items_array):\n        validator = TypeAdapter(unique_items_array)\n        assert isinstance(validator.validate_python([\"a\", \"b\"]), set)\n\n    def test_unique_items_converts_duplicates(self, unique_items_array):\n        validator = TypeAdapter(unique_items_array)\n        result = validator.validate_python([\"a\", \"a\", \"b\"])\n        assert result == {\"a\", \"b\"}\n\n\nclass TestObjectTypes:\n    \"\"\"Test suite for object validation.\"\"\"\n\n    @pytest.fixture\n    def simple_object(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\"name\": {\"type\": \"string\"}, \"age\": {\"type\": \"integer\"}},\n            }\n        )\n\n    @pytest.fixture\n    def required_object(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\"name\": {\"type\": \"string\"}, \"age\": {\"type\": \"integer\"}},\n                \"required\": [\"name\"],\n            }\n        )\n\n    @pytest.fixture\n    def nested_object(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"user\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\"},\n                            \"age\": {\"type\": \"integer\"},\n                        },\n                        \"required\": [\"name\"],\n                    }\n                },\n            }\n        )\n\n    @pytest.mark.parametrize(\n        \"input_type, expected_type\",\n        [\n            # Plain dict becomes dict[str, Any] (JSON Schema accurate)\n            (dict, dict[str, Any]),\n            # dict[str, Any] stays the same\n            (dict[str, Any], dict[str, Any]),\n            # Simple typed dicts work correctly\n            (dict[str, str], dict[str, str]),\n            (dict[str, int], dict[str, int]),\n            # Union value types work\n            (dict[str, str | int], dict[str, str | int]),\n            # Key types are constrained to str in JSON Schema\n            (dict[int, list[str]], dict[str, list[str]]),\n            # Union key types become str (JSON Schema limitation)\n            (dict[str | int, str | None], dict[str, str | None]),\n        ],\n    )\n    def test_dict_types_are_generated_correctly(self, input_type, expected_type):\n        schema = TypeAdapter(input_type).json_schema()\n        generated_type = json_schema_to_type(schema)\n        assert generated_type == expected_type\n\n    def test_object_accepts_valid(self, simple_object):\n        validator = TypeAdapter(simple_object)\n        result = validator.validate_python({\"name\": \"test\", \"age\": 30})\n        assert result.name == \"test\"\n        assert result.age == 30\n\n    def test_object_accepts_extra_properties(self, simple_object):\n        validator = TypeAdapter(simple_object)\n        result = validator.validate_python(\n            {\"name\": \"test\", \"age\": 30, \"extra\": \"field\"}\n        )\n        assert result.name == \"test\"\n        assert result.age == 30\n        assert not hasattr(result, \"extra\")\n\n    def test_required_accepts_valid(self, required_object):\n        validator = TypeAdapter(required_object)\n        result = validator.validate_python({\"name\": \"test\"})\n        assert result.name == \"test\"\n        assert result.age is None\n\n    def test_required_rejects_missing(self, required_object):\n        validator = TypeAdapter(required_object)\n        with pytest.raises(ValidationError):\n            validator.validate_python({})\n\n    def test_nested_accepts_valid(self, nested_object):\n        validator = TypeAdapter(nested_object)\n        result = validator.validate_python({\"user\": {\"name\": \"test\", \"age\": 30}})\n        assert result.user.name == \"test\"\n        assert result.user.age == 30\n\n    def test_nested_rejects_invalid(self, nested_object):\n        validator = TypeAdapter(nested_object)\n        with pytest.raises(ValidationError):\n            validator.validate_python({\"user\": {\"age\": 30}})\n\n    def test_object_with_underscore_names(self):\n        @dataclass\n        class Data:\n            x: int\n            x_: int\n            _x: int\n\n        schema = TypeAdapter(Data).json_schema()\n        assert schema == {\n            \"title\": \"Data\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"x\": {\"type\": \"integer\", \"title\": \"X\"},\n                \"x_\": {\"type\": \"integer\", \"title\": \"X\"},\n                \"_x\": {\"type\": \"integer\", \"title\": \"X\"},\n            },\n            \"required\": [\"x\", \"x_\", \"_x\"],\n        }\n\n        object = json_schema_to_type(schema)\n        object_schema = TypeAdapter(object).json_schema()\n        assert object_schema == schema\n"
  },
  {
    "path": "tests/utilities/json_schema_type/test_formats.py",
    "content": "\"\"\"Tests for format handling in JSON schema conversion.\"\"\"\n\nfrom dataclasses import Field\nfrom datetime import datetime\n\nimport pytest\nfrom pydantic import AnyUrl, TypeAdapter, ValidationError\n\nfrom fastmcp.utilities.json_schema_type import (\n    json_schema_to_type,\n)\n\n\ndef get_dataclass_field(type: type, field_name: str) -> Field:\n    return type.__dataclass_fields__[field_name]  # ty: ignore[unresolved-attribute]\n\n\nclass TestFormatTypes:\n    \"\"\"Test suite for format type validation.\"\"\"\n\n    @pytest.fixture\n    def datetime_format(self):\n        return json_schema_to_type({\"type\": \"string\", \"format\": \"date-time\"})\n\n    @pytest.fixture\n    def email_format(self):\n        return json_schema_to_type({\"type\": \"string\", \"format\": \"email\"})\n\n    @pytest.fixture\n    def uri_format(self):\n        return json_schema_to_type({\"type\": \"string\", \"format\": \"uri\"})\n\n    @pytest.fixture\n    def uri_reference_format(self):\n        return json_schema_to_type({\"type\": \"string\", \"format\": \"uri-reference\"})\n\n    @pytest.fixture\n    def json_format(self):\n        return json_schema_to_type({\"type\": \"string\", \"format\": \"json\"})\n\n    @pytest.fixture\n    def mixed_formats_object(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"full_uri\": {\"type\": \"string\", \"format\": \"uri\"},\n                    \"ref_uri\": {\"type\": \"string\", \"format\": \"uri-reference\"},\n                },\n            }\n        )\n\n    def test_datetime_valid(self, datetime_format):\n        validator = TypeAdapter(datetime_format)\n        result = validator.validate_python(\"2024-01-17T12:34:56Z\")\n        assert isinstance(result, datetime)\n\n    def test_datetime_invalid(self, datetime_format):\n        validator = TypeAdapter(datetime_format)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not-a-date\")\n\n    def test_email_valid(self, email_format):\n        validator = TypeAdapter(email_format)\n        result = validator.validate_python(\"test@example.com\")\n        assert isinstance(result, str)\n\n    def test_email_invalid(self, email_format):\n        validator = TypeAdapter(email_format)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not-an-email\")\n\n    def test_uri_valid(self, uri_format):\n        validator = TypeAdapter(uri_format)\n        result = validator.validate_python(\"https://example.com\")\n        assert isinstance(result, AnyUrl)\n\n    def test_uri_invalid(self, uri_format):\n        validator = TypeAdapter(uri_format)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not-a-uri\")\n\n    def test_uri_reference_valid(self, uri_reference_format):\n        validator = TypeAdapter(uri_reference_format)\n        result = validator.validate_python(\"https://example.com\")\n        assert isinstance(result, str)\n\n    def test_uri_reference_relative_valid(self, uri_reference_format):\n        validator = TypeAdapter(uri_reference_format)\n        result = validator.validate_python(\"/path/to/resource\")\n        assert isinstance(result, str)\n\n    def test_uri_reference_invalid(self, uri_reference_format):\n        validator = TypeAdapter(uri_reference_format)\n        result = validator.validate_python(\"not a uri\")\n        assert isinstance(result, str)\n\n    def test_json_valid(self, json_format):\n        validator = TypeAdapter(json_format)\n        result = validator.validate_python('{\"key\": \"value\"}')\n        assert isinstance(result, dict)\n\n    def test_json_invalid(self, json_format):\n        validator = TypeAdapter(json_format)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"{invalid json}\")\n\n    def test_mixed_formats_object(self, mixed_formats_object):\n        validator = TypeAdapter(mixed_formats_object)\n        result = validator.validate_python(\n            {\"full_uri\": \"https://example.com\", \"ref_uri\": \"/path/to/resource\"}\n        )\n        assert isinstance(result.full_uri, AnyUrl)\n        assert isinstance(result.ref_uri, str)\n"
  },
  {
    "path": "tests/utilities/json_schema_type/test_json_schema_type.py",
    "content": "\"\"\"Core JSON schema type conversion tests.\"\"\"\n\nfrom dataclasses import Field\nfrom enum import Enum\nfrom typing import Literal\n\nimport pytest\nfrom pydantic import TypeAdapter, ValidationError\n\nfrom fastmcp.utilities.json_schema_type import (\n    json_schema_to_type,\n)\n\n\ndef get_dataclass_field(type: type, field_name: str) -> Field:\n    return type.__dataclass_fields__[field_name]  # ty: ignore[unresolved-attribute]\n\n\nclass TestSimpleTypes:\n    \"\"\"Test suite for basic type validation.\"\"\"\n\n    @pytest.fixture\n    def simple_string(self):\n        return json_schema_to_type({\"type\": \"string\"})\n\n    @pytest.fixture\n    def simple_number(self):\n        return json_schema_to_type({\"type\": \"number\"})\n\n    @pytest.fixture\n    def simple_integer(self):\n        return json_schema_to_type({\"type\": \"integer\"})\n\n    @pytest.fixture\n    def simple_boolean(self):\n        return json_schema_to_type({\"type\": \"boolean\"})\n\n    @pytest.fixture\n    def simple_null(self):\n        return json_schema_to_type({\"type\": \"null\"})\n\n    def test_string_accepts_string(self, simple_string):\n        validator = TypeAdapter(simple_string)\n        assert validator.validate_python(\"test\") == \"test\"\n\n    def test_string_rejects_number(self, simple_string):\n        validator = TypeAdapter(simple_string)\n        with pytest.raises(ValidationError):\n            validator.validate_python(123)\n\n    def test_number_accepts_float(self, simple_number):\n        validator = TypeAdapter(simple_number)\n        assert validator.validate_python(123.45) == 123.45\n\n    def test_number_accepts_integer(self, simple_number):\n        validator = TypeAdapter(simple_number)\n        assert validator.validate_python(123) == 123\n\n    def test_number_accepts_numeric_string(self, simple_number):\n        validator = TypeAdapter(simple_number)\n        assert validator.validate_python(\"123.45\") == 123.45\n        assert validator.validate_python(\"123\") == 123\n\n    def test_number_rejects_invalid_string(self, simple_number):\n        validator = TypeAdapter(simple_number)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not a number\")\n\n    def test_integer_accepts_integer(self, simple_integer):\n        validator = TypeAdapter(simple_integer)\n        assert validator.validate_python(123) == 123\n\n    def test_integer_accepts_integer_string(self, simple_integer):\n        validator = TypeAdapter(simple_integer)\n        assert validator.validate_python(\"123\") == 123\n\n    def test_integer_rejects_float(self, simple_integer):\n        validator = TypeAdapter(simple_integer)\n        with pytest.raises(ValidationError):\n            validator.validate_python(123.45)\n\n    def test_integer_rejects_float_string(self, simple_integer):\n        validator = TypeAdapter(simple_integer)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"123.45\")\n\n    def test_boolean_accepts_boolean(self, simple_boolean):\n        validator = TypeAdapter(simple_boolean)\n        assert validator.validate_python(True) is True\n        assert validator.validate_python(False) is False\n\n    def test_boolean_accepts_boolean_strings(self, simple_boolean):\n        validator = TypeAdapter(simple_boolean)\n        assert validator.validate_python(\"true\") is True\n        assert validator.validate_python(\"True\") is True\n        assert validator.validate_python(\"false\") is False\n        assert validator.validate_python(\"False\") is False\n\n    def test_boolean_rejects_invalid_string(self, simple_boolean):\n        validator = TypeAdapter(simple_boolean)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not a boolean\")\n\n    def test_null_accepts_none(self, simple_null):\n        validator = TypeAdapter(simple_null)\n        assert validator.validate_python(None) is None\n\n    def test_null_rejects_false(self, simple_null):\n        validator = TypeAdapter(simple_null)\n        with pytest.raises(ValidationError):\n            validator.validate_python(False)\n\n\nclass TestConstrainedTypes:\n    def test_constant(self):\n        validator = TypeAdapter(Literal[\"x\"])\n        schema = validator.json_schema()\n        type_ = json_schema_to_type(schema)\n        assert type_ == Literal[\"x\"]\n        assert TypeAdapter(type_).validate_python(\"x\") == \"x\"\n        with pytest.raises(ValidationError):\n            TypeAdapter(type_).validate_python(\"y\")\n\n    def test_union_constants(self):\n        validator = TypeAdapter(Literal[\"x\"] | Literal[\"y\"])\n        schema = validator.json_schema()\n        type_ = json_schema_to_type(schema)\n        assert type_ == Literal[\"x\"] | Literal[\"y\"]\n        assert TypeAdapter(type_).validate_python(\"x\") == \"x\"\n        assert TypeAdapter(type_).validate_python(\"y\") == \"y\"\n        with pytest.raises(ValidationError):\n            TypeAdapter(type_).validate_python(\"z\")\n\n    def test_enum_str(self):\n        class MyEnum(Enum):\n            X = \"x\"\n            Y = \"y\"\n\n        validator = TypeAdapter(MyEnum)\n        schema = validator.json_schema()\n        type_ = json_schema_to_type(schema)\n        assert type_ == Literal[\"x\", \"y\"]\n        assert TypeAdapter(type_).validate_python(\"x\") == \"x\"\n        assert TypeAdapter(type_).validate_python(\"y\") == \"y\"\n        with pytest.raises(ValidationError):\n            TypeAdapter(type_).validate_python(\"z\")\n\n    def test_enum_int(self):\n        class MyEnum(Enum):\n            X = 1\n            Y = 2\n\n        validator = TypeAdapter(MyEnum)\n        schema = validator.json_schema()\n        type_ = json_schema_to_type(schema)\n        assert type_ == Literal[1, 2]\n        assert TypeAdapter(type_).validate_python(1) == 1\n        assert TypeAdapter(type_).validate_python(2) == 2\n        with pytest.raises(ValidationError):\n            TypeAdapter(type_).validate_python(3)\n\n    def test_choice(self):\n        validator = TypeAdapter(Literal[\"x\", \"y\"])\n        schema = validator.json_schema()\n        type_ = json_schema_to_type(schema)\n        assert type_ == Literal[\"x\", \"y\"]\n        assert TypeAdapter(type_).validate_python(\"x\") == \"x\"\n        assert TypeAdapter(type_).validate_python(\"y\") == \"y\"\n        with pytest.raises(ValidationError):\n            TypeAdapter(type_).validate_python(\"z\")\n"
  },
  {
    "path": "tests/utilities/json_schema_type/test_unions.py",
    "content": "\"\"\"Tests for union types in JSON schema conversion.\"\"\"\n\nfrom dataclasses import Field\n\nimport pytest\nfrom pydantic import TypeAdapter, ValidationError\n\nfrom fastmcp.utilities.json_schema_type import (\n    json_schema_to_type,\n)\n\n\ndef get_dataclass_field(type: type, field_name: str) -> Field:\n    return type.__dataclass_fields__[field_name]  # ty: ignore[unresolved-attribute]\n\n\nclass TestUnionTypes:\n    \"\"\"Test suite for testing union type behaviors.\"\"\"\n\n    @pytest.fixture\n    def heterogeneous_union(self):\n        return json_schema_to_type({\"type\": [\"string\", \"number\", \"boolean\", \"null\"]})\n\n    @pytest.fixture\n    def union_with_constraints(self):\n        return json_schema_to_type(\n            {\"type\": [\"string\", \"number\"], \"minLength\": 3, \"minimum\": 0}\n        )\n\n    @pytest.fixture\n    def union_with_formats(self):\n        return json_schema_to_type({\"type\": [\"string\", \"null\"], \"format\": \"email\"})\n\n    @pytest.fixture\n    def nested_union_array(self):\n        return json_schema_to_type(\n            {\"type\": \"array\", \"items\": {\"type\": [\"string\", \"number\"]}}\n        )\n\n    @pytest.fixture\n    def nested_union_object(self):\n        return json_schema_to_type(\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"id\": {\"type\": [\"string\", \"integer\"]},\n                    \"data\": {\n                        \"type\": [\"object\", \"null\"],\n                        \"properties\": {\"value\": {\"type\": \"string\"}},\n                    },\n                },\n            }\n        )\n\n    def test_heterogeneous_accepts_string(self, heterogeneous_union):\n        validator = TypeAdapter(heterogeneous_union)\n        assert validator.validate_python(\"test\") == \"test\"\n\n    def test_heterogeneous_accepts_number(self, heterogeneous_union):\n        validator = TypeAdapter(heterogeneous_union)\n        assert validator.validate_python(123.45) == 123.45\n\n    def test_heterogeneous_accepts_boolean(self, heterogeneous_union):\n        validator = TypeAdapter(heterogeneous_union)\n        assert validator.validate_python(True) is True\n\n    def test_heterogeneous_accepts_null(self, heterogeneous_union):\n        validator = TypeAdapter(heterogeneous_union)\n        assert validator.validate_python(None) is None\n\n    def test_heterogeneous_rejects_array(self, heterogeneous_union):\n        validator = TypeAdapter(heterogeneous_union)\n        with pytest.raises(ValidationError):\n            validator.validate_python([])\n\n    def test_constrained_string_valid(self, union_with_constraints):\n        validator = TypeAdapter(union_with_constraints)\n        assert validator.validate_python(\"test\") == \"test\"\n\n    def test_constrained_string_invalid(self, union_with_constraints):\n        validator = TypeAdapter(union_with_constraints)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"ab\")\n\n    def test_constrained_number_valid(self, union_with_constraints):\n        validator = TypeAdapter(union_with_constraints)\n        assert validator.validate_python(10) == 10\n\n    def test_constrained_number_invalid(self, union_with_constraints):\n        validator = TypeAdapter(union_with_constraints)\n        with pytest.raises(ValidationError):\n            validator.validate_python(-1)\n\n    def test_format_valid_email(self, union_with_formats):\n        validator = TypeAdapter(union_with_formats)\n        result = validator.validate_python(\"test@example.com\")\n        assert isinstance(result, str)\n\n    def test_format_valid_null(self, union_with_formats):\n        validator = TypeAdapter(union_with_formats)\n        assert validator.validate_python(None) is None\n\n    def test_format_invalid_email(self, union_with_formats):\n        validator = TypeAdapter(union_with_formats)\n        with pytest.raises(ValidationError):\n            validator.validate_python(\"not-an-email\")\n\n    def test_nested_array_mixed_types(self, nested_union_array):\n        validator = TypeAdapter(nested_union_array)\n        result = validator.validate_python([\"test\", 123, \"abc\"])\n        assert result == [\"test\", 123, \"abc\"]\n\n    def test_nested_array_rejects_invalid(self, nested_union_array):\n        validator = TypeAdapter(nested_union_array)\n        with pytest.raises(ValidationError):\n            validator.validate_python([\"test\", [\"not\", \"allowed\"], \"abc\"])\n\n    def test_nested_object_string_id(self, nested_union_object):\n        validator = TypeAdapter(nested_union_object)\n        result = validator.validate_python({\"id\": \"abc123\", \"data\": {\"value\": \"test\"}})\n        assert result.id == \"abc123\"\n        assert result.data.value == \"test\"\n\n    def test_nested_object_integer_id(self, nested_union_object):\n        validator = TypeAdapter(nested_union_object)\n        result = validator.validate_python({\"id\": 123, \"data\": None})\n        assert result.id == 123\n        assert result.data is None\n"
  },
  {
    "path": "tests/utilities/openapi/__init__.py",
    "content": "\"\"\"Tests for openapi_new utilities.\"\"\"\n"
  },
  {
    "path": "tests/utilities/openapi/conftest.py",
    "content": "\"\"\"Shared fixtures for openapi_new utilities tests.\"\"\"\n\nimport pytest\n\n\n@pytest.fixture\ndef basic_openapi_30_spec():\n    \"\"\"Basic OpenAPI 3.0 spec for testing.\"\"\"\n    return {\n        \"openapi\": \"3.0.0\",\n        \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n        \"servers\": [{\"url\": \"https://api.example.com\"}],\n        \"paths\": {\n            \"/users/{id}\": {\n                \"get\": {\n                    \"operationId\": \"get_user\",\n                    \"summary\": \"Get user by ID\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"in\": \"path\",\n                            \"required\": True,\n                            \"schema\": {\"type\": \"integer\"},\n                        }\n                    ],\n                    \"responses\": {\n                        \"200\": {\n                            \"description\": \"User retrieved successfully\",\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"id\": {\"type\": \"integer\"},\n                                            \"name\": {\"type\": \"string\"},\n                                        },\n                                    }\n                                }\n                            },\n                        }\n                    },\n                }\n            }\n        },\n    }\n\n\n@pytest.fixture\ndef basic_openapi_31_spec():\n    \"\"\"Basic OpenAPI 3.1 spec for testing.\"\"\"\n    return {\n        \"openapi\": \"3.1.0\",\n        \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n        \"servers\": [{\"url\": \"https://api.example.com\"}],\n        \"paths\": {\n            \"/users/{id}\": {\n                \"get\": {\n                    \"operationId\": \"get_user\",\n                    \"summary\": \"Get user by ID\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"in\": \"path\",\n                            \"required\": True,\n                            \"schema\": {\"type\": \"integer\"},\n                        }\n                    ],\n                    \"responses\": {\n                        \"200\": {\n                            \"description\": \"User retrieved successfully\",\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"id\": {\"type\": \"integer\"},\n                                            \"name\": {\"type\": \"string\"},\n                                        },\n                                    }\n                                }\n                            },\n                        }\n                    },\n                }\n            }\n        },\n    }\n\n\n@pytest.fixture\ndef collision_spec():\n    \"\"\"OpenAPI spec with parameter name collisions.\"\"\"\n    return {\n        \"openapi\": \"3.0.0\",\n        \"info\": {\"title\": \"Collision Test API\", \"version\": \"1.0.0\"},\n        \"paths\": {\n            \"/users/{id}\": {\n                \"put\": {\n                    \"operationId\": \"update_user\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"in\": \"path\",\n                            \"required\": True,\n                            \"schema\": {\"type\": \"integer\"},\n                        }\n                    ],\n                    \"requestBody\": {\n                        \"required\": True,\n                        \"content\": {\n                            \"application/json\": {\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"id\": {\"type\": \"integer\"},\n                                        \"name\": {\"type\": \"string\"},\n                                    },\n                                    \"required\": [\"name\"],\n                                }\n                            }\n                        },\n                    },\n                    \"responses\": {\"200\": {\"description\": \"Updated\"}},\n                }\n            }\n        },\n    }\n\n\n@pytest.fixture\ndef deepobject_spec():\n    \"\"\"OpenAPI spec with deepObject parameter style.\"\"\"\n    return {\n        \"openapi\": \"3.0.0\",\n        \"info\": {\"title\": \"DeepObject Test API\", \"version\": \"1.0.0\"},\n        \"paths\": {\n            \"/search\": {\n                \"get\": {\n                    \"operationId\": \"search\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"filter\",\n                            \"in\": \"query\",\n                            \"required\": False,\n                            \"style\": \"deepObject\",\n                            \"explode\": True,\n                            \"schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"category\": {\"type\": \"string\"},\n                                    \"price\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"min\": {\"type\": \"number\"},\n                                            \"max\": {\"type\": \"number\"},\n                                        },\n                                    },\n                                },\n                            },\n                        }\n                    ],\n                    \"responses\": {\"200\": {\"description\": \"Search results\"}},\n                }\n            }\n        },\n    }\n\n\n@pytest.fixture\ndef complex_spec():\n    \"\"\"Complex OpenAPI spec with multiple parameter types.\"\"\"\n    return {\n        \"openapi\": \"3.0.0\",\n        \"info\": {\"title\": \"Complex API\", \"version\": \"1.0.0\"},\n        \"paths\": {\n            \"/items/{id}\": {\n                \"patch\": {\n                    \"operationId\": \"update_item\",\n                    \"parameters\": [\n                        {\n                            \"name\": \"id\",\n                            \"in\": \"path\",\n                            \"required\": True,\n                            \"schema\": {\"type\": \"string\"},\n                        },\n                        {\n                            \"name\": \"version\",\n                            \"in\": \"query\",\n                            \"required\": False,\n                            \"schema\": {\"type\": \"integer\", \"default\": 1},\n                        },\n                        {\n                            \"name\": \"X-Client-Version\",\n                            \"in\": \"header\",\n                            \"required\": False,\n                            \"schema\": {\"type\": \"string\"},\n                        },\n                    ],\n                    \"requestBody\": {\n                        \"required\": True,\n                        \"content\": {\n                            \"application/json\": {\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"title\": {\"type\": \"string\"},\n                                        \"description\": {\"type\": \"string\"},\n                                        \"tags\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\"type\": \"string\"},\n                                        },\n                                    },\n                                    \"required\": [\"title\"],\n                                }\n                            }\n                        },\n                    },\n                    \"responses\": {\"200\": {\"description\": \"Item updated\"}},\n                }\n            }\n        },\n    }\n"
  },
  {
    "path": "tests/utilities/openapi/test_allof_requestbody.py",
    "content": "\"\"\"Tests for allOf handling at requestBody top level.\"\"\"\n\nfrom fastmcp.utilities.openapi.models import (\n    HTTPRoute,\n    RequestBodyInfo,\n)\nfrom fastmcp.utilities.openapi.schemas import _combine_schemas\n\n\ndef test_allof_at_requestbody_top_level():\n    \"\"\"Test that allOf schemas at requestBody top level are properly merged.\"\"\"\n\n    # Create a route with allOf at the requestBody top level\n    route = HTTPRoute(\n        path=\"/test\",\n        method=\"POST\",\n        operation_id=\"testOperation\",\n        parameters=[],\n        request_body=RequestBodyInfo(\n            required=True,\n            content_schema={\n                \"application/json\": {\n                    \"allOf\": [\n                        {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"name\": {\"type\": \"string\"},\n                                \"age\": {\"type\": \"integer\"},\n                            },\n                            \"required\": [\"name\"],\n                        },\n                        {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"email\": {\"type\": \"string\"},\n                                \"phone\": {\"type\": \"string\"},\n                            },\n                            \"required\": [\"email\"],\n                        },\n                    ]\n                }\n            },\n        ),\n        responses={},\n    )\n\n    # Combine schemas - this should merge allOf schemas\n    combined = _combine_schemas(route)\n\n    # Check that all properties from both allOf schemas are present\n    properties = combined.get(\"properties\", {})\n    assert \"name\" in properties\n    assert \"age\" in properties\n    assert \"email\" in properties\n    assert \"phone\" in properties\n\n    # Check property types\n    assert properties[\"name\"][\"type\"] == \"string\"\n    assert properties[\"age\"][\"type\"] == \"integer\"\n    assert properties[\"email\"][\"type\"] == \"string\"\n    assert properties[\"phone\"][\"type\"] == \"string\"\n\n    # Check that required fields are merged correctly\n    required = set(combined.get(\"required\", []))\n    assert \"name\" in required\n    assert \"email\" in required\n\n    # allOf should be removed after merging\n    assert \"allOf\" not in combined\n\n\ndef test_allof_with_nested_properties():\n    \"\"\"Test allOf with nested object properties.\"\"\"\n\n    route = HTTPRoute(\n        path=\"/test\",\n        method=\"POST\",\n        operation_id=\"testNested\",\n        parameters=[],\n        request_body=RequestBodyInfo(\n            required=True,\n            content_schema={\n                \"application/json\": {\n                    \"allOf\": [\n                        {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"user\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"id\": {\"type\": \"integer\"},\n                                        \"name\": {\"type\": \"string\"},\n                                    },\n                                }\n                            },\n                            \"required\": [\"user\"],\n                        },\n                        {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"metadata\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"created\": {\"type\": \"string\"},\n                                        \"updated\": {\"type\": \"string\"},\n                                    },\n                                }\n                            },\n                        },\n                    ]\n                }\n            },\n        ),\n        responses={},\n    )\n\n    combined = _combine_schemas(route)\n\n    # Check nested properties are preserved\n    properties = combined.get(\"properties\", {})\n    assert \"user\" in properties\n    assert \"metadata\" in properties\n\n    # Check nested structure\n    assert properties[\"user\"][\"type\"] == \"object\"\n    assert \"id\" in properties[\"user\"][\"properties\"]\n    assert \"name\" in properties[\"user\"][\"properties\"]\n\n    assert properties[\"metadata\"][\"type\"] == \"object\"\n    assert \"created\" in properties[\"metadata\"][\"properties\"]\n    assert \"updated\" in properties[\"metadata\"][\"properties\"]\n\n    # Check required\n    required = set(combined.get(\"required\", []))\n    assert \"user\" in required\n    assert \"metadata\" not in required  # Not in any required array\n\n\ndef test_allof_with_overlapping_properties():\n    \"\"\"Test allOf with overlapping property names (later schemas override).\"\"\"\n\n    route = HTTPRoute(\n        path=\"/test\",\n        method=\"POST\",\n        operation_id=\"testOverlap\",\n        parameters=[],\n        request_body=RequestBodyInfo(\n            required=True,\n            content_schema={\n                \"application/json\": {\n                    \"allOf\": [\n                        {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"name\": {\"type\": \"string\", \"minLength\": 1},\n                                \"age\": {\"type\": \"integer\"},\n                            },\n                            \"required\": [\"name\"],\n                        },\n                        {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"name\": {\"type\": \"string\", \"maxLength\": 50},  # Override\n                                \"email\": {\"type\": \"string\"},\n                            },\n                            \"required\": [\"email\"],\n                        },\n                    ]\n                }\n            },\n        ),\n        responses={},\n    )\n\n    combined = _combine_schemas(route)\n\n    properties = combined.get(\"properties\", {})\n\n    # Later schema should win for overlapping properties\n    assert \"name\" in properties\n    assert properties[\"name\"][\"type\"] == \"string\"\n    assert \"maxLength\" in properties[\"name\"]  # From second schema\n    assert properties[\"name\"][\"maxLength\"] == 50\n\n    # Check other properties\n    assert \"age\" in properties\n    assert \"email\" in properties\n\n    # Both name and email should be required\n    required = set(combined.get(\"required\", []))\n    assert \"name\" in required\n    assert \"email\" in required\n\n\ndef test_no_allof_passthrough():\n    \"\"\"Test that schemas without allOf pass through unchanged.\"\"\"\n\n    route = HTTPRoute(\n        path=\"/test\",\n        method=\"POST\",\n        operation_id=\"testNoAllOf\",\n        parameters=[],\n        request_body=RequestBodyInfo(\n            required=True,\n            content_schema={\n                \"application/json\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"simple\": {\"type\": \"string\"}},\n                    \"required\": [\"simple\"],\n                }\n            },\n        ),\n        responses={},\n    )\n\n    combined = _combine_schemas(route)\n\n    # Should pass through unchanged\n    properties = combined.get(\"properties\", {})\n    assert \"simple\" in properties\n    assert properties[\"simple\"][\"type\"] == \"string\"\n\n    required = set(combined.get(\"required\", []))\n    assert \"simple\" in required\n\n    # No allOf in original or result\n    assert \"allOf\" not in combined\n"
  },
  {
    "path": "tests/utilities/openapi/test_circular_references.py",
    "content": "\"\"\"Tests for circular and self-referential schema serialization (Issues #1016, #1206, #3242).\"\"\"\n\nimport httpx\n\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.openapi.models import (\n    ResponseInfo,\n)\nfrom fastmcp.utilities.openapi.schemas import (\n    _replace_ref_with_defs,\n    extract_output_schema_from_responses,\n)\n\n\nclass TestCircularReferencesSerialization:\n    \"\"\"Tests for circular/self-referential schemas surviving MCP serialization.\n\n    Issues: #1016, #1206, #3242\n\n    The crash occurs when Pydantic's model_dump() encounters the same Python\n    dict object at multiple positions in the serialization tree. This happens\n    because _replace_ref_with_defs mutates shared list objects (anyOf/allOf/oneOf)\n    in place via shallow copy, causing different tools to share internal dict\n    references.\n    \"\"\"\n\n    def test_replace_ref_with_defs_does_not_mutate_input(self):\n        \"\"\"_replace_ref_with_defs must not mutate its input dict's lists.\"\"\"\n        schema = {\n            \"oneOf\": [\n                {\"$ref\": \"#/components/schemas/Cat\"},\n                {\"$ref\": \"#/components/schemas/Dog\"},\n            ]\n        }\n        original_list = schema[\"oneOf\"]\n        original_items = list(original_list)  # snapshot\n\n        _replace_ref_with_defs(schema)\n\n        # The original list object must not have been mutated\n        assert original_list is schema[\"oneOf\"]\n        assert original_list == original_items\n\n    def test_replace_ref_with_defs_produces_independent_results(self):\n        \"\"\"Calling _replace_ref_with_defs twice on the same input must produce\n        independent dict trees with no shared mutable objects.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"pet\": {\n                    \"oneOf\": [\n                        {\"$ref\": \"#/components/schemas/Cat\"},\n                        {\"$ref\": \"#/components/schemas/Dog\"},\n                    ]\n                }\n            },\n        }\n\n        result1 = _replace_ref_with_defs(schema)\n        result2 = _replace_ref_with_defs(schema)\n\n        # The oneOf lists should be different objects\n        list1 = result1[\"properties\"][\"pet\"][\"oneOf\"]\n        list2 = result2[\"properties\"][\"pet\"][\"oneOf\"]\n        assert list1 is not list2\n\n        # Items within the lists should also be independent\n        assert list1[0] is not list2[0]\n\n    def test_circular_output_schema_serialization(self):\n        \"\"\"Output schemas with self-referential types must survive model_dump().\n\n        This is the exact crash from issue #3242: Pydantic raises\n        ValueError('Circular reference detected (id repeated)') when\n        serializing MCP Tool objects whose schemas share Python dict references.\n        \"\"\"\n        responses = {\n            \"200\": ResponseInfo(\n                description=\"A tree node\",\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/Node\"}\n                },\n            )\n        }\n        schema_definitions = {\n            \"Node\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"value\": {\"type\": \"string\"},\n                    \"children\": {\n                        \"type\": \"array\",\n                        \"items\": {\"$ref\": \"#/components/schemas/Node\"},\n                    },\n                },\n            },\n        }\n\n        output_schema = extract_output_schema_from_responses(\n            responses, schema_definitions=schema_definitions, openapi_version=\"3.0.0\"\n        )\n        assert output_schema is not None\n\n        # Build an MCP Tool with this schema and try to serialize it —\n        # this is the exact path that crashes in the reported issue.\n        from mcp.types import Tool as MCPTool\n\n        tool = MCPTool(\n            name=\"get_node\",\n            description=\"Get a node\",\n            inputSchema={\"type\": \"object\", \"properties\": {}},\n            outputSchema=output_schema,\n        )\n        # This must not raise ValueError: Circular reference detected\n        tool.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n\n    def test_mutual_circular_references_serialization(self):\n        \"\"\"Mutually circular schemas (A→B→A) must survive serialization.\"\"\"\n        responses = {\n            \"200\": ResponseInfo(\n                description=\"A pull request\",\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/PullRequest\"}\n                },\n            )\n        }\n        schema_definitions = {\n            \"PullRequest\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"title\": {\"type\": \"string\"},\n                    \"author\": {\"$ref\": \"#/components/schemas/User\"},\n                },\n            },\n            \"User\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"pull_requests\": {\n                        \"type\": \"array\",\n                        \"items\": {\"$ref\": \"#/components/schemas/PullRequest\"},\n                    },\n                },\n            },\n        }\n\n        output_schema = extract_output_schema_from_responses(\n            responses, schema_definitions=schema_definitions, openapi_version=\"3.0.0\"\n        )\n        assert output_schema is not None\n\n        from mcp.types import Tool as MCPTool\n\n        tool = MCPTool(\n            name=\"get_pr\",\n            description=\"Get a pull request\",\n            inputSchema={\"type\": \"object\", \"properties\": {}},\n            outputSchema=output_schema,\n        )\n        tool.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n\n    async def test_multiple_tools_sharing_circular_schemas(self):\n        \"\"\"Multiple tools from the same spec must not share Python dict objects\n        in their schemas, which would cause Pydantic to raise circular reference\n        errors when serializing the list_tools response.\"\"\"\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/nodes\": {\n                    \"get\": {\n                        \"operationId\": \"list_nodes\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"List of nodes\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/components/schemas/Node\"\n                                            },\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    },\n                    \"post\": {\n                        \"operationId\": \"create_node\",\n                        \"requestBody\": {\n                            \"required\": True,\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\"$ref\": \"#/components/schemas/Node\"}\n                                }\n                            },\n                        },\n                        \"responses\": {\n                            \"201\": {\n                                \"description\": \"Created node\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/Node\"}\n                                    }\n                                },\n                            }\n                        },\n                    },\n                },\n                \"/nodes/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_node\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"id\",\n                                \"in\": \"path\",\n                                \"required\": True,\n                                \"schema\": {\"type\": \"string\"},\n                            }\n                        ],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"A node\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/Node\"}\n                                    }\n                                },\n                            }\n                        },\n                    },\n                },\n            },\n            \"components\": {\n                \"schemas\": {\n                    \"Node\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"value\": {\"type\": \"string\"},\n                            \"children\": {\n                                \"type\": \"array\",\n                                \"items\": {\"$ref\": \"#/components/schemas/Node\"},\n                            },\n                        },\n                    }\n                }\n            },\n        }\n\n        server = FastMCP.from_openapi(spec, httpx.AsyncClient())\n        tools = await server.list_tools()\n        assert len(tools) >= 3\n\n        # Simulate what the MCP SDK does: serialize all tools together.\n        # This is the exact crash path — model_dump on a list of tools\n        # whose schemas share Python dict objects.\n\n        mcp_tools = [tool.to_mcp_tool(name=tool.name) for tool in tools]\n        for mcp_tool in mcp_tools:\n            mcp_tool.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n"
  },
  {
    "path": "tests/utilities/openapi/test_direct_array_schemas.py",
    "content": "\"\"\"Test handling of direct array schemas in request bodies (FastAPI list parameters).\"\"\"\n\nfrom fastmcp.utilities.openapi.models import (\n    HTTPRoute,\n    ParameterInfo,\n    RequestBodyInfo,\n)\nfrom fastmcp.utilities.openapi.schemas import (\n    _combine_schemas_and_map_params,\n)\n\n\nclass TestDirectArraySchemas:\n    \"\"\"Test handling of direct array schemas like those generated by FastAPI for list[str] parameters.\"\"\"\n\n    def test_simple_direct_array_schema(self):\n        \"\"\"Test route with direct array request body schema.\"\"\"\n        route = HTTPRoute(\n            path=\"/simple_list\",\n            method=\"POST\",\n            operation_id=\"simple_list_tool\",\n            summary=\"Simple List Tool\",\n            description=\"A simple tool that takes a list of strings.\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"items\": {\"type\": \"string\"},\n                        \"type\": \"array\",\n                        \"title\": \"Values\",\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should create a single parameter from the title\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"values\" in combined_schema[\"properties\"]\n        assert \"values\" in combined_schema[\"required\"]\n\n        # Check the array schema is preserved\n        values_schema = combined_schema[\"properties\"][\"values\"]\n        assert values_schema[\"type\"] == \"array\"\n        assert values_schema[\"items\"][\"type\"] == \"string\"\n        assert values_schema[\"title\"] == \"Values\"\n\n        # Check parameter mapping\n        assert \"values\" in param_map\n        assert param_map[\"values\"][\"location\"] == \"body\"\n        assert param_map[\"values\"][\"openapi_name\"] == \"values\"\n\n    def test_int_array_schema(self):\n        \"\"\"Test route with integer array request body schema.\"\"\"\n        route = HTTPRoute(\n            path=\"/int_list\",\n            method=\"POST\",\n            operation_id=\"int_list_tool\",\n            summary=\"Integer List Tool\",\n            description=\"A simple tool that takes a list of integers.\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"items\": {\"type\": \"integer\"},\n                        \"type\": \"array\",\n                        \"title\": \"Numbers\",\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should create a single parameter from the title\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"numbers\" in combined_schema[\"properties\"]\n        assert \"numbers\" in combined_schema[\"required\"]\n\n        # Check the array schema is preserved\n        numbers_schema = combined_schema[\"properties\"][\"numbers\"]\n        assert numbers_schema[\"type\"] == \"array\"\n        assert numbers_schema[\"items\"][\"type\"] == \"integer\"\n        assert numbers_schema[\"title\"] == \"Numbers\"\n\n    def test_mixed_params_with_direct_array(self):\n        \"\"\"Test route with both URL parameters and direct array request body.\"\"\"\n        route = HTTPRoute(\n            path=\"/mixed_params\",\n            method=\"POST\",\n            operation_id=\"mixed_params_tool\",\n            summary=\"Mixed Params Tool\",\n            description=\"A tool with both list and simple parameters.\",\n            parameters=[\n                ParameterInfo(\n                    name=\"prefix\",\n                    location=\"query\",\n                    required=True,\n                    schema={\"type\": \"string\", \"title\": \"Prefix\"},\n                )\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"array\",\n                        \"items\": {\"type\": \"string\"},\n                        \"title\": \"Values\",\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should have both parameters\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"prefix\" in combined_schema[\"properties\"]  # From query param\n        assert \"values\" in combined_schema[\"properties\"]  # From body array\n        assert \"prefix\" in combined_schema[\"required\"]\n        assert \"values\" in combined_schema[\"required\"]\n\n        # Check query parameter\n        prefix_schema = combined_schema[\"properties\"][\"prefix\"]\n        assert prefix_schema[\"type\"] == \"string\"\n\n        # Check body array parameter\n        values_schema = combined_schema[\"properties\"][\"values\"]\n        assert values_schema[\"type\"] == \"array\"\n        assert values_schema[\"items\"][\"type\"] == \"string\"\n\n        # Check parameter mappings\n        assert param_map[\"prefix\"][\"location\"] == \"query\"\n        assert param_map[\"values\"][\"location\"] == \"body\"\n\n    def test_direct_array_no_title(self):\n        \"\"\"Test direct array schema without title.\"\"\"\n        route = HTTPRoute(\n            path=\"/no_title\",\n            method=\"POST\",\n            operation_id=\"no_title_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"array\",\n                        \"items\": {\"type\": \"string\"},\n                        # No title\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should fall back to \"body\" (default title falls back)\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"body\" in combined_schema[\"properties\"]\n        assert \"body\" in combined_schema[\"required\"]\n\n        # Check parameter mapping\n        assert param_map[\"body\"][\"location\"] == \"body\"\n        assert param_map[\"body\"][\"openapi_name\"] == \"body\"\n\n    def test_direct_primitive_schema(self):\n        \"\"\"Test direct primitive (non-array) request body schema.\"\"\"\n        route = HTTPRoute(\n            path=\"/primitive\",\n            method=\"POST\",\n            operation_id=\"primitive_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"type\": \"string\", \"title\": \"Message\"}\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should create a single parameter from the title\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"message\" in combined_schema[\"properties\"]\n        assert \"message\" in combined_schema[\"required\"]\n\n        # Check the primitive schema is preserved\n        message_schema = combined_schema[\"properties\"][\"message\"]\n        assert message_schema[\"type\"] == \"string\"\n        assert message_schema[\"title\"] == \"Message\"\n\n    def test_direct_array_optional(self):\n        \"\"\"Test direct array schema that's not required.\"\"\"\n        route = HTTPRoute(\n            path=\"/optional_list\",\n            method=\"POST\",\n            operation_id=\"optional_list_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=False,  # Not required\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"array\",\n                        \"items\": {\"type\": \"string\"},\n                        \"title\": \"OptionalValues\",\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should create parameter but not mark as required\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"optionalvalues\" in combined_schema[\"properties\"]\n        assert \"optionalvalues\" not in combined_schema[\"required\"]\n\n    def test_direct_array_title_sanitization(self):\n        \"\"\"Test that titles with special characters are sanitized.\"\"\"\n        route = HTTPRoute(\n            path=\"/special_chars\",\n            method=\"POST\",\n            operation_id=\"special_chars_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"array\",\n                        \"items\": {\"type\": \"string\"},\n                        \"title\": \"Special-Chars & Spaces!\",\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should sanitize the title to a valid parameter name (& becomes multiple underscores)\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"special_chars___spaces_\" in combined_schema[\"properties\"]\n\n    def test_direct_array_title_starting_with_number(self):\n        \"\"\"Test that titles starting with numbers are handled.\"\"\"\n        route = HTTPRoute(\n            path=\"/number_start\",\n            method=\"POST\",\n            operation_id=\"number_start_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"array\",\n                        \"items\": {\"type\": \"string\"},\n                        \"title\": \"123Values\",\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should fall back to \"body_data\" since \"123values\" starts with a number\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"body_data\" in combined_schema[\"properties\"]\n\n    def test_preserve_existing_behavior_object_body(self):\n        \"\"\"Test that existing object-based request bodies still work.\"\"\"\n        route = HTTPRoute(\n            path=\"/object_body\",\n            method=\"POST\",\n            operation_id=\"object_body_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\"},\n                            \"age\": {\"type\": \"integer\"},\n                        },\n                        \"required\": [\"name\"],\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should handle object bodies as before\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"name\" in combined_schema[\"properties\"]\n        assert \"age\" in combined_schema[\"properties\"]\n        assert \"name\" in combined_schema[\"required\"]\n        assert \"age\" not in combined_schema[\"required\"]\n\n    def test_preserve_existing_behavior_ref_body(self):\n        \"\"\"Test that $ref-based request bodies still work.\"\"\"\n        route = HTTPRoute(\n            path=\"/ref_body\",\n            method=\"POST\",\n            operation_id=\"ref_body_tool\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/UserCreate\"}\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should handle $ref bodies as before (refs get converted to $defs)\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"body\" in combined_schema[\"properties\"]\n        assert combined_schema[\"properties\"][\"body\"][\"$ref\"] == \"#/$defs/UserCreate\"\n"
  },
  {
    "path": "tests/utilities/openapi/test_director.py",
    "content": "\"\"\"Unit tests for RequestDirector.\"\"\"\n\nfrom urllib.parse import unquote\n\nimport pytest\nfrom jsonschema_path import SchemaPath\n\nfrom fastmcp.utilities.openapi.director import RequestDirector\nfrom fastmcp.utilities.openapi.models import (\n    HTTPRoute,\n    ParameterInfo,\n    RequestBodyInfo,\n)\nfrom fastmcp.utilities.openapi.parser import parse_openapi_to_http_routes\n\n\nclass TestRequestDirector:\n    \"\"\"Test RequestDirector request building functionality.\"\"\"\n\n    @pytest.fixture\n    def basic_route(self):\n        \"\"\"Create a basic HTTPRoute for testing.\"\"\"\n        return HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"GET\",\n            operation_id=\"get_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                )\n            ],\n            flat_param_schema={\n                \"type\": \"object\",\n                \"properties\": {\"id\": {\"type\": \"integer\"}},\n                \"required\": [\"id\"],\n            },\n            parameter_map={\"id\": {\"location\": \"path\", \"openapi_name\": \"id\"}},\n        )\n\n    @pytest.fixture\n    def complex_route(self):\n        \"\"\"Create a complex HTTPRoute with multiple parameter types.\"\"\"\n        return HTTPRoute(\n            path=\"/items/{id}\",\n            method=\"PATCH\",\n            operation_id=\"update_item\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"string\"},\n                ),\n                ParameterInfo(\n                    name=\"version\",\n                    location=\"query\",\n                    required=False,\n                    schema={\"type\": \"integer\", \"default\": 1},\n                ),\n                ParameterInfo(\n                    name=\"X-Client-Version\",\n                    location=\"header\",\n                    required=False,\n                    schema={\"type\": \"string\"},\n                ),\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"title\": {\"type\": \"string\"},\n                            \"description\": {\"type\": \"string\"},\n                        },\n                        \"required\": [\"title\"],\n                    }\n                },\n            ),\n            flat_param_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"id\": {\"type\": \"string\"},\n                    \"version\": {\"type\": \"integer\", \"default\": 1},\n                    \"X-Client-Version\": {\"type\": \"string\"},\n                    \"title\": {\"type\": \"string\"},\n                    \"description\": {\"type\": \"string\"},\n                },\n                \"required\": [\"id\", \"title\"],\n            },\n            parameter_map={\n                \"id\": {\"location\": \"path\", \"openapi_name\": \"id\"},\n                \"version\": {\"location\": \"query\", \"openapi_name\": \"version\"},\n                \"X-Client-Version\": {\n                    \"location\": \"header\",\n                    \"openapi_name\": \"X-Client-Version\",\n                },\n                \"title\": {\"location\": \"body\", \"openapi_name\": \"title\"},\n                \"description\": {\"location\": \"body\", \"openapi_name\": \"description\"},\n            },\n        )\n\n    @pytest.fixture\n    def collision_route(self):\n        \"\"\"Create a route with parameter name collisions.\"\"\"\n        return HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"PUT\",\n            operation_id=\"update_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                )\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"id\": {\"type\": \"integer\"},\n                            \"name\": {\"type\": \"string\"},\n                        },\n                        \"required\": [\"name\"],\n                    }\n                },\n            ),\n            flat_param_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"id__path\": {\"type\": \"integer\"},\n                    \"id\": {\"type\": \"integer\"},\n                    \"name\": {\"type\": \"string\"},\n                },\n                \"required\": [\"id__path\", \"name\"],\n            },\n            parameter_map={\n                \"id__path\": {\"location\": \"path\", \"openapi_name\": \"id\"},\n                \"id\": {\"location\": \"body\", \"openapi_name\": \"id\"},\n                \"name\": {\"location\": \"body\", \"openapi_name\": \"name\"},\n            },\n        )\n\n    @pytest.fixture\n    def director(self, basic_openapi_30_spec):\n        \"\"\"Create a RequestDirector instance.\"\"\"\n        spec = SchemaPath.from_dict(basic_openapi_30_spec)\n        return RequestDirector(spec)\n\n    def test_director_initialization(self, basic_openapi_30_spec):\n        \"\"\"Test RequestDirector initialization.\"\"\"\n        spec = SchemaPath.from_dict(basic_openapi_30_spec)\n        director = RequestDirector(spec)\n\n        assert director._spec is not None\n        assert director._spec == spec\n\n    def test_build_basic_request(self, director, basic_route):\n        \"\"\"Test building a basic GET request with path parameter.\"\"\"\n        flat_args = {\"id\": 123}\n\n        request = director.build(basic_route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"GET\"\n        assert request.url == \"https://api.example.com/users/123\"\n        assert (\n            request.content == b\"\"\n        )  # httpx.Request sets content to empty bytes for GET\n\n    def test_build_complex_request(self, director, complex_route):\n        \"\"\"Test building a complex request with multiple parameter types.\"\"\"\n        flat_args = {\n            \"id\": \"item123\",\n            \"version\": 2,\n            \"X-Client-Version\": \"1.0.0\",\n            \"title\": \"Updated Title\",\n            \"description\": \"Updated description\",\n        }\n\n        request = director.build(complex_route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"PATCH\"\n        assert \"item123\" in str(request.url)\n        assert \"version=2\" in str(request.url)\n\n        # Check headers\n        headers = dict(request.headers) if request.headers else {}\n        assert (\n            headers.get(\"x-client-version\") == \"1.0.0\"\n        )  # httpx normalizes headers to lowercase\n\n        # Check body\n        import json\n\n        assert request.content is not None\n        body_data = json.loads(request.content)\n        assert body_data[\"title\"] == \"Updated Title\"\n        assert body_data[\"description\"] == \"Updated description\"\n\n    def test_build_request_with_collisions(self, director, collision_route):\n        \"\"\"Test building request with parameter name collisions.\"\"\"\n        flat_args = {\n            \"id__path\": 123,  # Path parameter\n            \"id\": 456,  # Body parameter\n            \"name\": \"John Doe\",\n        }\n\n        request = director.build(collision_route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"PUT\"\n        assert \"123\" in str(request.url)  # Path ID should be 123\n\n        # Check body\n        import json\n\n        body_data = json.loads(request.content)\n        assert body_data[\"id\"] == 456  # Body ID should be 456\n        assert body_data[\"name\"] == \"John Doe\"\n\n    def test_build_request_with_none_values(self, director, complex_route):\n        \"\"\"Test that None values are skipped for optional parameters.\"\"\"\n        flat_args = {\n            \"id\": \"item123\",\n            \"version\": None,  # Optional, should be skipped\n            \"X-Client-Version\": None,  # Optional, should be skipped\n            \"title\": \"Required Title\",\n            \"description\": None,  # Optional body param, should be skipped\n        }\n\n        request = director.build(complex_route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"PATCH\"\n        assert \"item123\" in str(request.url)\n        assert \"version\" not in str(request.url)  # Should not include None version\n\n        headers = dict(request.headers) if request.headers else {}\n        assert \"X-Client-Version\" not in headers\n\n        import json\n\n        body_data = json.loads(request.content)\n        assert body_data[\"title\"] == \"Required Title\"\n        assert \"description\" not in body_data  # Should not include None description\n\n    def test_build_request_fallback_mapping(self, director):\n        \"\"\"Test fallback parameter mapping when parameter_map is not available.\"\"\"\n        # Create route without parameter_map\n        route_without_map = HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"GET\",\n            operation_id=\"get_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                )\n            ],\n            # No parameter_map provided\n        )\n\n        flat_args = {\"id\": 123}\n\n        request = director.build(\n            route_without_map, flat_args, \"https://api.example.com\"\n        )\n\n        assert request.method == \"GET\"\n        assert \"123\" in str(request.url)\n\n    def test_build_request_suffixed_parameters(self, director):\n        \"\"\"Test handling of suffixed parameters in fallback mode.\"\"\"\n        route = HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"POST\",\n            operation_id=\"create_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                )\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"name\": {\"type\": \"string\"}},\n                    }\n                },\n            ),\n        )\n\n        # Use suffixed parameter names\n        flat_args = {\n            \"id__path\": 123,\n            \"name\": \"John Doe\",\n        }\n\n        request = director.build(route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"POST\"\n        assert \"123\" in str(request.url)\n\n        import json\n\n        body_data = json.loads(request.content)\n        assert body_data[\"name\"] == \"John Doe\"\n\n    def test_url_building(self, director, basic_route):\n        \"\"\"Test URL building with different base URLs.\"\"\"\n        flat_args = {\"id\": 123}\n\n        # Test with trailing slash\n        request1 = director.build(basic_route, flat_args, \"https://api.example.com/\")\n        assert request1.url == \"https://api.example.com/users/123\"\n\n        # Test without trailing slash\n        request2 = director.build(basic_route, flat_args, \"https://api.example.com\")\n        assert request2.url == \"https://api.example.com/users/123\"\n\n        # Test with path in base URL\n        request3 = director.build(basic_route, flat_args, \"https://api.example.com/v1\")\n        assert request3.url == \"https://api.example.com/v1/users/123\"\n\n    def test_body_construction_single_value(self, director):\n        \"\"\"Test body construction when body schema is not an object.\"\"\"\n        route = HTTPRoute(\n            path=\"/upload\",\n            method=\"POST\",\n            operation_id=\"upload_file\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\"text/plain\": {\"type\": \"string\"}},\n            ),\n            parameter_map={\n                \"content\": {\"location\": \"body\", \"openapi_name\": \"content\"},\n            },\n        )\n\n        flat_args = {\"content\": \"Hello, World!\"}\n\n        request = director.build(route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"POST\"\n        # For non-JSON content, httpx uses 'content' parameter which becomes bytes\n        assert request.content == b\"Hello, World!\"\n\n    def test_body_construction_multiple_properties_non_object_schema(self, director):\n        \"\"\"Test body construction with multiple properties but non-object schema.\"\"\"\n        route = HTTPRoute(\n            path=\"/complex\",\n            method=\"POST\",\n            operation_id=\"complex_op\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"type\": \"string\"}  # Non-object schema\n                },\n            ),\n            parameter_map={\n                \"prop1\": {\"location\": \"body\", \"openapi_name\": \"prop1\"},\n                \"prop2\": {\"location\": \"body\", \"openapi_name\": \"prop2\"},\n            },\n        )\n\n        flat_args = {\"prop1\": \"value1\", \"prop2\": \"value2\"}\n\n        request = director.build(route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"POST\"\n        # Should wrap in object when multiple properties but schema is not object\n        import json\n\n        body_data = json.loads(request.content)\n        assert body_data == {\"prop1\": \"value1\", \"prop2\": \"value2\"}\n\n\nclass TestRequestDirectorIntegration:\n    \"\"\"Test RequestDirector with real parsed routes.\"\"\"\n\n    def test_with_parsed_routes(self, basic_openapi_30_spec):\n        \"\"\"Test RequestDirector with routes parsed from real spec.\"\"\"\n        routes = parse_openapi_to_http_routes(basic_openapi_30_spec)\n        assert len(routes) == 1\n\n        route = routes[0]\n        spec = SchemaPath.from_dict(basic_openapi_30_spec)\n        director = RequestDirector(spec)\n\n        flat_args = {\"id\": 42}\n        request = director.build(route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"GET\"\n        assert request.url == \"https://api.example.com/users/42\"\n\n    def test_with_collision_spec(self, collision_spec):\n        \"\"\"Test RequestDirector with collision spec.\"\"\"\n        routes = parse_openapi_to_http_routes(collision_spec)\n        assert len(routes) == 1\n\n        route = routes[0]\n        spec = SchemaPath.from_dict(collision_spec)\n        director = RequestDirector(spec)\n\n        # Use the parameter names from the actual parameter map\n        param_map = route.parameter_map\n        path_param_name = None\n        body_param_names = []\n\n        for param_name, mapping in param_map.items():\n            if mapping[\"location\"] == \"path\" and mapping[\"openapi_name\"] == \"id\":\n                path_param_name = param_name\n            elif mapping[\"location\"] == \"body\":\n                body_param_names.append(param_name)\n\n        assert path_param_name is not None\n\n        flat_args = {path_param_name: 123, \"name\": \"John Doe\"}\n        # Add body id if it exists in the parameter map\n        for param_name in body_param_names:\n            if \"id\" in param_name:\n                flat_args[param_name] = 456\n\n        request = director.build(route, flat_args, \"https://api.example.com\")\n\n        assert request.method == \"PUT\"\n        assert \"123\" in str(request.url)\n\n    def test_with_deepobject_spec(self, deepobject_spec):\n        \"\"\"Test RequestDirector with deepObject parameters.\"\"\"\n        routes = parse_openapi_to_http_routes(deepobject_spec)\n        assert len(routes) == 1\n\n        route = routes[0]\n        spec = SchemaPath.from_dict(deepobject_spec)\n        director = RequestDirector(spec)\n\n        # DeepObject parameters should be flattened in the parameter map\n        flat_args = {}\n        for param_name in route.parameter_map.keys():\n            if \"filter\" in param_name:\n                # Set some test values based on parameter name\n                if \"category\" in param_name:\n                    flat_args[param_name] = \"electronics\"\n                elif \"min\" in param_name:\n                    flat_args[param_name] = 10.0\n                elif \"max\" in param_name:\n                    flat_args[param_name] = 100.0\n\n        if flat_args:  # Only test if we have parameters to test with\n            request = director.build(route, flat_args, \"https://api.example.com\")\n\n            assert request.method == \"GET\"\n            assert str(request.url).startswith(\"https://api.example.com/search\")\n\n\nclass TestPathTraversalPrevention:\n    \"\"\"Test that path parameter values are URL-encoded to prevent SSRF/path traversal.\"\"\"\n\n    @pytest.fixture\n    def director(self, basic_openapi_30_spec):\n        spec = SchemaPath.from_dict(basic_openapi_30_spec)\n        return RequestDirector(spec)\n\n    @pytest.fixture\n    def path_route(self):\n        return HTTPRoute(\n            path=\"/api/v1/users/{id}/profile\",\n            method=\"GET\",\n            operation_id=\"get_user_profile\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"string\"},\n                )\n            ],\n            flat_param_schema={\n                \"type\": \"object\",\n                \"properties\": {\"id\": {\"type\": \"string\"}},\n                \"required\": [\"id\"],\n            },\n            parameter_map={\"id\": {\"location\": \"path\", \"openapi_name\": \"id\"}},\n        )\n\n    @pytest.mark.parametrize(\n        \"malicious_id\",\n        [\n            \"../../../admin/delete-all?\",\n            \"../../secret\",\n            \"../../../etc/passwd\",\n            \"foo/../../../admin\",\n            \"..%2F..%2Fadmin\",\n            \"..%2f..%2fadmin\",\n        ],\n    )\n    def test_path_traversal_encoded(self, director, path_route, malicious_id: str):\n        request = director.build(\n            path_route, {\"id\": malicious_id}, \"https://api.example.com\"\n        )\n        url = str(request.url)\n        assert \"/admin\" not in url\n        assert \"/secret\" not in url\n        assert \"/etc/passwd\" not in url\n        assert url.startswith(\"https://api.example.com/api/v1/users/\")\n\n    def test_slash_in_param_is_encoded(self, director, path_route):\n        request = director.build(path_route, {\"id\": \"a/b\"}, \"https://api.example.com\")\n        url = str(request.url)\n        assert \"/a/b/\" not in url\n        assert \"a%2Fb\" in url\n\n    def test_dot_dot_slash_is_encoded(self, director, path_route):\n        request = director.build(\n            path_route, {\"id\": \"../admin\"}, \"https://api.example.com\"\n        )\n        url = str(request.url)\n        assert \"%2E%2E%2Fadmin\" in url or \"%2e%2e%2fadmin\" in url\n        assert url.startswith(\"https://api.example.com/api/v1/users/\")\n\n    def test_question_mark_encoded(self, director, path_route):\n        request = director.build(\n            path_route, {\"id\": \"foo?bar=baz\"}, \"https://api.example.com\"\n        )\n        url = str(request.url)\n        assert \"foo%3Fbar%3Dbaz\" in url or \"foo%3fbar%3dbaz\" in url\n\n    def test_hash_encoded(self, director, path_route):\n        request = director.build(\n            path_route, {\"id\": \"foo#fragment\"}, \"https://api.example.com\"\n        )\n        url = str(request.url)\n        assert \"foo%23fragment\" in url\n\n    def test_normal_values_still_work(self, director, path_route):\n        request = director.build(\n            path_route, {\"id\": \"user-123\"}, \"https://api.example.com\"\n        )\n        assert (\n            str(request.url) == \"https://api.example.com/api/v1/users/user-123/profile\"\n        )\n\n    def test_dotted_values_encode_dots(self, director, path_route):\n        \"\"\"Dots are encoded to prevent path normalization by urljoin.\"\"\"\n        request = director.build(\n            path_route, {\"id\": \"v1.2.3\"}, \"https://api.example.com\"\n        )\n        url = str(request.url)\n        assert \"v1%2E2%2E3\" in url\n        assert url.startswith(\"https://api.example.com/api/v1/users/\")\n\n    def test_numeric_values_still_work(self, director, path_route):\n        request = director.build(path_route, {\"id\": 42}, \"https://api.example.com\")\n        assert str(request.url) == \"https://api.example.com/api/v1/users/42/profile\"\n\n    def test_bare_single_dot_encoded(self, director, path_route):\n        \"\"\"Bare '.' must be encoded so urljoin doesn't normalize it away.\"\"\"\n        request = director.build(path_route, {\"id\": \".\"}, \"https://api.example.com\")\n        url = str(request.url)\n        assert \"%2E\" in url\n        assert url.startswith(\"https://api.example.com/api/v1/users/\")\n\n    def test_bare_dotdot_encoded(self, director, path_route):\n        \"\"\"Bare '..' must be encoded so urljoin doesn't resolve it as traversal.\"\"\"\n        request = director.build(path_route, {\"id\": \"..\"}, \"https://api.example.com\")\n        url = str(request.url)\n        assert \"%2E%2E\" in url or \"%2e%2e\" in url\n        assert url.startswith(\"https://api.example.com/api/v1/users/\")\n\n    def test_double_encoded_traversal(self, director, path_route):\n        request = director.build(\n            path_route,\n            {\"id\": \"..%2F..%2Fadmin\"},\n            \"https://api.example.com\",\n        )\n        url = str(request.url)\n        decoded = unquote(unquote(url))\n        # Verify traversal didn't escape the users/ prefix\n        assert decoded.startswith(\"https://api.example.com/api/v1/users/\")\n        assert url.startswith(\"https://api.example.com/api/v1/users/\")\n"
  },
  {
    "path": "tests/utilities/openapi/test_legacy_compatibility.py",
    "content": "\"\"\"Tests to ensure OpenAPI schema generation works correctly.\"\"\"\n\nimport pytest\n\nfrom fastmcp.utilities.openapi.models import (\n    HTTPRoute,\n    ParameterInfo,\n    RequestBodyInfo,\n)\nfrom fastmcp.utilities.openapi.schemas import (\n    _combine_schemas_and_map_params,\n)\n\n\nclass TestSchemaGeneration:\n    \"\"\"Test that OpenAPI schema generation produces correct schemas.\"\"\"\n\n    def test_optional_parameter_nullable_behavior(self):\n        \"\"\"Test that optional parameters are not made nullable - they can simply be omitted.\"\"\"\n        route = HTTPRoute(\n            method=\"GET\",\n            path=\"/test\",\n            operation_id=\"test_op\",\n            parameters=[\n                ParameterInfo(\n                    name=\"required_param\",\n                    location=\"query\",\n                    required=True,\n                    schema={\"type\": \"string\"},\n                ),\n                ParameterInfo(\n                    name=\"optional_param\",\n                    location=\"query\",\n                    required=False,\n                    schema={\"type\": \"string\"},\n                ),\n            ],\n        )\n\n        schema, _ = _combine_schemas_and_map_params(route)\n\n        # Required parameter should have simple type\n        assert schema[\"properties\"][\"required_param\"][\"type\"] == \"string\"\n        assert \"anyOf\" not in schema[\"properties\"][\"required_param\"]\n\n        # Optional parameters should preserve original schema without making it nullable\n        assert \"anyOf\" not in schema[\"properties\"][\"optional_param\"]\n        assert schema[\"properties\"][\"optional_param\"][\"type\"] == \"string\"\n\n        # Required list should only include required parameters\n        assert \"required_param\" in schema[\"required\"]\n        assert \"optional_param\" not in schema[\"required\"]\n\n    def test_parameter_collision_handling(self):\n        \"\"\"Test that parameter collisions are handled with suffixes.\"\"\"\n        route = HTTPRoute(\n            method=\"PUT\",\n            path=\"/users/{id}\",\n            operation_id=\"update_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                )\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"id\": {\"type\": \"integer\"},\n                            \"name\": {\"type\": \"string\"},\n                        },\n                        \"required\": [\"name\"],\n                    }\n                },\n            ),\n        )\n\n        schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should have path parameter with suffix\n        assert \"id__path\" in schema[\"properties\"]\n\n        # Should have body parameter without suffix\n        assert \"id\" in schema[\"properties\"]\n\n        # Should have name parameter from body\n        assert \"name\" in schema[\"properties\"]\n\n        # Required should include path param (suffixed) and required body params\n        required = set(schema[\"required\"])\n        assert \"id__path\" in required\n        assert \"name\" in required\n\n        # Parameter map should correctly map suffixed parameter\n        assert param_map[\"id__path\"][\"location\"] == \"path\"\n        assert param_map[\"id__path\"][\"openapi_name\"] == \"id\"\n        assert param_map[\"id\"][\"location\"] == \"body\"\n        assert param_map[\"name\"][\"location\"] == \"body\"\n\n    @pytest.mark.parametrize(\n        \"param_type\",\n        [\n            {\"type\": \"integer\"},\n            {\"type\": \"number\"},\n            {\"type\": \"boolean\"},\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n            {\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}},\n        ],\n    )\n    def test_nullable_behavior_different_types(self, param_type):\n        \"\"\"Test nullable behavior works for all parameter types.\"\"\"\n        route = HTTPRoute(\n            method=\"GET\",\n            path=\"/test\",\n            operation_id=\"test_op\",\n            parameters=[\n                ParameterInfo(\n                    name=\"optional_param\",\n                    location=\"query\",\n                    required=False,\n                    schema=param_type,\n                )\n            ],\n        )\n\n        schema, _ = _combine_schemas_and_map_params(route)\n\n        # Should preserve original schema without making it nullable\n        param = schema[\"properties\"][\"optional_param\"]\n        assert \"anyOf\" not in param\n\n        # Should match the original parameter schema\n        for key, value in param_type.items():\n            assert param[key] == value\n\n    def test_no_parameters_no_body(self):\n        \"\"\"Test schema generation when there are no parameters or body.\"\"\"\n        route = HTTPRoute(\n            method=\"GET\",\n            path=\"/health\",\n            operation_id=\"health_check\",\n        )\n\n        schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should have empty object schema\n        assert schema[\"type\"] == \"object\"\n        assert schema[\"properties\"] == {}\n        assert schema[\"required\"] == []\n        assert param_map == {}\n\n    def test_body_only_no_parameters(self):\n        \"\"\"Test schema generation with only request body, no parameters.\"\"\"\n        body_schema = {\n            \"application/json\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"title\": {\"type\": \"string\"},\n                    \"description\": {\"type\": \"string\"},\n                },\n                \"required\": [\"title\"],\n            }\n        }\n\n        route = HTTPRoute(\n            method=\"POST\",\n            path=\"/items\",\n            operation_id=\"create_item\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema=body_schema,\n            ),\n        )\n\n        schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should have body properties\n        assert \"title\" in schema[\"properties\"]\n        assert \"description\" in schema[\"properties\"]\n\n        # Required should match body requirements\n        assert \"title\" in schema[\"required\"]\n        assert \"description\" not in schema[\"required\"]\n\n        # Parameter map should map body properties\n        assert param_map[\"title\"][\"location\"] == \"body\"\n        assert param_map[\"description\"][\"location\"] == \"body\"\n"
  },
  {
    "path": "tests/utilities/openapi/test_models.py",
    "content": "\"\"\"Unit tests for OpenAPI models.\"\"\"\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nfrom fastmcp.utilities.openapi.models import (\n    HttpMethod,\n    HTTPRoute,\n    ParameterInfo,\n    ParameterLocation,\n    RequestBodyInfo,\n    ResponseInfo,\n)\n\n\nclass TestParameterInfo:\n    \"\"\"Test ParameterInfo model.\"\"\"\n\n    def test_basic_parameter_creation(self):\n        \"\"\"Test creating a basic parameter.\"\"\"\n        param = ParameterInfo(\n            name=\"id\",\n            location=\"path\",\n            required=True,\n            schema={\"type\": \"integer\"},\n        )\n\n        assert param.name == \"id\"\n        assert param.location == \"path\"\n        assert param.required is True\n        assert param.schema_ == {\"type\": \"integer\"}\n        assert param.description is None\n        assert param.explode is None\n        assert param.style is None\n\n    def test_parameter_with_all_fields(self):\n        \"\"\"Test creating parameter with all optional fields.\"\"\"\n        param = ParameterInfo(\n            name=\"filter\",\n            location=\"query\",\n            required=False,\n            schema={\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}},\n            description=\"Filter criteria\",\n            explode=True,\n            style=\"deepObject\",\n        )\n\n        assert param.name == \"filter\"\n        assert param.location == \"query\"\n        assert param.required is False\n        assert param.description == \"Filter criteria\"\n        assert param.explode is True\n        assert param.style == \"deepObject\"\n\n    @pytest.mark.parametrize(\"location\", [\"path\", \"query\", \"header\", \"cookie\"])\n    def test_valid_parameter_locations(self, location: ParameterLocation):\n        \"\"\"Test that all valid parameter locations are accepted.\"\"\"\n        param = ParameterInfo(\n            name=\"test\",\n            location=location,\n            required=False,\n            schema={\"type\": \"string\"},\n        )\n        assert param.location == location\n\n    def test_parameter_defaults(self):\n        \"\"\"Test parameter default values.\"\"\"\n        param = ParameterInfo(\n            name=\"test\",\n            location=\"query\",\n            schema={\"type\": \"string\"},\n        )\n\n        # required should default to False for non-path parameters\n        assert param.required is False\n        assert param.description is None\n        assert param.explode is None\n        assert param.style is None\n\n    def test_parameter_with_empty_schema(self):\n        \"\"\"Test parameter with empty schema.\"\"\"\n        param = ParameterInfo(\n            name=\"test\",\n            location=\"query\",\n            schema={},\n        )\n\n        assert param.schema_ == {}\n\n\nclass TestRequestBodyInfo:\n    \"\"\"Test RequestBodyInfo model.\"\"\"\n\n    def test_basic_request_body(self):\n        \"\"\"Test creating a basic request body.\"\"\"\n        request_body = RequestBodyInfo(\n            required=True,\n            description=\"User data\",\n        )\n\n        assert request_body.required is True\n        assert request_body.description == \"User data\"\n        assert request_body.content_schema == {}\n\n    def test_request_body_with_content_schema(self):\n        \"\"\"Test request body with content schema.\"\"\"\n        content_schema = {\n            \"application/json\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"email\": {\"type\": \"string\"},\n                },\n                \"required\": [\"name\"],\n            }\n        }\n\n        request_body = RequestBodyInfo(\n            required=True,\n            content_schema=content_schema,\n        )\n\n        assert request_body.content_schema == content_schema\n\n    def test_request_body_defaults(self):\n        \"\"\"Test request body default values.\"\"\"\n        request_body = RequestBodyInfo()\n\n        assert request_body.required is False\n        assert request_body.description is None\n        assert request_body.content_schema == {}\n\n    def test_request_body_multiple_content_types(self):\n        \"\"\"Test request body with multiple content types.\"\"\"\n        content_schema = {\n            \"application/json\": {\n                \"type\": \"object\",\n                \"properties\": {\"name\": {\"type\": \"string\"}},\n            },\n            \"application/xml\": {\n                \"type\": \"object\",\n                \"properties\": {\"name\": {\"type\": \"string\"}},\n            },\n        }\n\n        request_body = RequestBodyInfo(content_schema=content_schema)\n\n        assert len(request_body.content_schema) == 2\n        assert \"application/json\" in request_body.content_schema\n        assert \"application/xml\" in request_body.content_schema\n\n\nclass TestResponseInfo:\n    \"\"\"Test ResponseInfo model.\"\"\"\n\n    def test_basic_response(self):\n        \"\"\"Test creating a basic response.\"\"\"\n        response = ResponseInfo(description=\"Success response\")\n\n        assert response.description == \"Success response\"\n        assert response.content_schema == {}\n\n    def test_response_with_content_schema(self):\n        \"\"\"Test response with content schema.\"\"\"\n        content_schema = {\n            \"application/json\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"id\": {\"type\": \"integer\"},\n                    \"message\": {\"type\": \"string\"},\n                },\n            }\n        }\n\n        response = ResponseInfo(\n            description=\"User created\",\n            content_schema=content_schema,\n        )\n\n        assert response.description == \"User created\"\n        assert response.content_schema == content_schema\n\n    def test_response_required_description(self):\n        \"\"\"Test that response description is required.\"\"\"\n        # Should not raise an error - description has a default\n        response = ResponseInfo()\n        assert response.description is None\n\n\nclass TestHTTPRoute:\n    \"\"\"Test HTTPRoute model.\"\"\"\n\n    def test_basic_route_creation(self):\n        \"\"\"Test creating a basic HTTP route.\"\"\"\n        route = HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"GET\",\n            operation_id=\"get_user\",\n        )\n\n        assert route.path == \"/users/{id}\"\n        assert route.method == \"GET\"\n        assert route.operation_id == \"get_user\"\n        assert route.summary is None\n        assert route.description is None\n        assert route.tags == []\n        assert route.parameters == []\n        assert route.request_body is None\n        assert route.responses == {}\n\n    def test_route_with_all_fields(self):\n        \"\"\"Test creating route with all fields.\"\"\"\n        parameters = [\n            ParameterInfo(\n                name=\"id\",\n                location=\"path\",\n                required=True,\n                schema={\"type\": \"integer\"},\n            )\n        ]\n\n        request_body = RequestBodyInfo(\n            required=True,\n            content_schema={\n                \"application/json\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"name\": {\"type\": \"string\"}},\n                }\n            },\n        )\n\n        responses = {\n            \"200\": ResponseInfo(\n                description=\"Success\",\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"id\": {\"type\": \"integer\"}},\n                    }\n                },\n            )\n        }\n\n        route = HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"PUT\",\n            operation_id=\"update_user\",\n            summary=\"Update user\",\n            description=\"Update user by ID\",\n            tags=[\"users\"],\n            parameters=parameters,\n            request_body=request_body,\n            responses=responses,\n            request_schemas={\"User\": {\"type\": \"object\"}},\n            extensions={\"x-custom\": \"value\"},\n        )\n\n        assert route.path == \"/users/{id}\"\n        assert route.method == \"PUT\"\n        assert route.operation_id == \"update_user\"\n        assert route.summary == \"Update user\"\n        assert route.description == \"Update user by ID\"\n        assert route.tags == [\"users\"]\n        assert len(route.parameters) == 1\n        assert route.request_body is not None\n        assert \"200\" in route.responses\n        assert \"User\" in route.request_schemas\n        assert route.extensions[\"x-custom\"] == \"value\"\n\n    def test_route_pre_calculated_fields(self):\n        \"\"\"Test route with pre-calculated fields.\"\"\"\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"GET\",\n            operation_id=\"test\",\n            flat_param_schema={\n                \"type\": \"object\",\n                \"properties\": {\"id\": {\"type\": \"integer\"}},\n            },\n            parameter_map={\"id\": {\"location\": \"path\", \"openapi_name\": \"id\"}},\n        )\n\n        assert route.flat_param_schema[\"type\"] == \"object\"\n        assert \"id\" in route.flat_param_schema[\"properties\"]\n        assert \"id\" in route.parameter_map\n        assert route.parameter_map[\"id\"][\"location\"] == \"path\"\n\n    @pytest.mark.parametrize(\n        \"method\", [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\"]\n    )\n    def test_valid_http_methods(self, method: HttpMethod):\n        \"\"\"Test that all valid HTTP methods are accepted.\"\"\"\n        route = HTTPRoute(\n            path=\"/test\",\n            method=method,\n            operation_id=\"test\",\n        )\n        assert route.method == method\n\n    def test_route_with_empty_collections(self):\n        \"\"\"Test route with empty collections.\"\"\"\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"GET\",\n            operation_id=\"test\",\n            tags=[],\n            parameters=[],\n            responses={},\n            request_schemas={},\n            extensions={},\n        )\n\n        assert route.tags == []\n        assert route.parameters == []\n        assert route.responses == {}\n        assert route.request_schemas == {}\n        assert route.response_schemas == {}\n        assert route.extensions == {}\n\n    def test_route_defaults(self):\n        \"\"\"Test route default values.\"\"\"\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"GET\",\n            operation_id=\"test\",\n        )\n\n        assert route.summary is None\n        assert route.description is None\n        assert route.tags == []\n        assert route.parameters == []\n        assert route.request_body is None\n        assert route.responses == {}\n        assert route.request_schemas == {}\n        assert route.response_schemas == {}\n        assert route.extensions == {}\n        assert route.flat_param_schema == {}\n        assert route.parameter_map == {}\n\n\nclass TestModelValidation:\n    \"\"\"Test model validation and error cases.\"\"\"\n\n    def test_parameter_info_validation(self):\n        \"\"\"Test ParameterInfo validation.\"\"\"\n        # Valid parameter\n        param = ParameterInfo(\n            name=\"test\",\n            location=\"query\",\n            schema={\"type\": \"string\"},\n        )\n        assert param.name == \"test\"\n\n    def test_route_validation(self):\n        \"\"\"Test HTTPRoute validation.\"\"\"\n        # Valid route\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"GET\",\n            operation_id=\"test\",\n        )\n        assert route.path == \"/test\"\n\n    def test_nested_model_validation(self):\n        \"\"\"Test validation of nested models.\"\"\"\n        # Create route with nested models\n        param = ParameterInfo(\n            name=\"id\",\n            location=\"path\",\n            required=True,\n            schema={\"type\": \"integer\"},\n        )\n\n        request_body = RequestBodyInfo(required=True)\n\n        route = HTTPRoute(\n            path=\"/test/{id}\",\n            method=\"POST\",\n            operation_id=\"test\",\n            parameters=[param],\n            request_body=request_body,\n        )\n\n        assert len(route.parameters) == 1\n        assert route.parameters[0].name == \"id\"\n        assert route.request_body is not None\n        assert route.request_body.required is True\n\n\nclass TestModelSerialization:\n    \"\"\"Test model serialization and deserialization.\"\"\"\n\n    def test_parameter_info_serialization(self):\n        \"\"\"Test ParameterInfo serialization.\"\"\"\n        param = ParameterInfo(\n            name=\"filter\",\n            location=\"query\",\n            required=False,\n            schema={\"type\": \"object\"},\n            description=\"Filter criteria\",\n            explode=True,\n            style=\"deepObject\",\n        )\n\n        # Test model_dump with alias\n        data = param.model_dump(by_alias=True)\n\n        assert data[\"name\"] == \"filter\"\n        assert data[\"location\"] == \"query\"\n        assert data[\"required\"] is False\n        assert data[\"schema\"] == {\"type\": \"object\"}  # Using alias\n        assert data[\"description\"] == \"Filter criteria\"\n        assert data[\"explode\"] is True\n        assert data[\"style\"] == \"deepObject\"\n\n    def test_route_serialization(self):\n        \"\"\"Test HTTPRoute serialization.\"\"\"\n        param = ParameterInfo(\n            name=\"id\",\n            location=\"path\",\n            required=True,\n            schema={\"type\": \"integer\"},\n        )\n\n        route = HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"GET\",\n            operation_id=\"get_user\",\n            parameters=[param],\n        )\n\n        # Test model_dump\n        data = route.model_dump()\n\n        assert data[\"path\"] == \"/users/{id}\"\n        assert data[\"method\"] == \"GET\"\n        assert data[\"operation_id\"] == \"get_user\"\n        assert len(data[\"parameters\"]) == 1\n        assert data[\"parameters\"][0][\"name\"] == \"id\"\n\n    def test_model_reconstruction(self):\n        \"\"\"Test reconstructing models from serialized data.\"\"\"\n        # Create original parameter\n        original_param = ParameterInfo(\n            name=\"test\",\n            location=\"query\",\n            schema={\"type\": \"string\"},\n            description=\"Test parameter\",\n        )\n\n        # Serialize and reconstruct using by_alias\n        data = original_param.model_dump(by_alias=True)\n\n        data == snapshot(\n            {\n                \"name\": \"test\",\n                \"location\": \"query\",\n                \"required\": False,\n                \"schema\": {\"type\": \"string\"},\n                \"description\": \"Test parameter\",\n                \"explode\": None,\n                \"style\": None,\n            }\n        )\n\n        reconstructed_param = ParameterInfo(\n            name=data[\"name\"],\n            location=data[\"location\"],\n            schema=data[\"schema\"],\n            description=data[\"description\"],\n            explode=data[\"explode\"],\n            style=data[\"style\"],\n        )\n\n        assert reconstructed_param.name == original_param.name\n        assert reconstructed_param.location == original_param.location\n        assert reconstructed_param.schema_ == original_param.schema_\n        assert reconstructed_param.description == original_param.description\n"
  },
  {
    "path": "tests/utilities/openapi/test_nullable_fields.py",
    "content": "\"\"\"Tests for nullable field handling in OpenAPI schemas.\"\"\"\n\nimport pytest\nfrom jsonschema import ValidationError, validate\n\nfrom fastmcp.utilities.openapi.json_schema_converter import (\n    convert_openapi_schema_to_json_schema,\n)\n\n\nclass TestHandleNullableFields:\n    \"\"\"Test conversion of OpenAPI nullable fields to JSON Schema format.\"\"\"\n\n    def test_root_level_nullable_string(self):\n        \"\"\"Test nullable string at root level.\"\"\"\n        input_schema = {\"type\": \"string\", \"nullable\": True}\n        expected = {\"type\": [\"string\", \"null\"]}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_root_level_nullable_integer(self):\n        \"\"\"Test nullable integer at root level.\"\"\"\n        input_schema = {\"type\": \"integer\", \"nullable\": True}\n        expected = {\"type\": [\"integer\", \"null\"]}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_root_level_nullable_boolean(self):\n        \"\"\"Test nullable boolean at root level.\"\"\"\n        input_schema = {\"type\": \"boolean\", \"nullable\": True}\n        expected = {\"type\": [\"boolean\", \"null\"]}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_property_level_nullable_fields(self):\n        \"\"\"Test nullable fields in properties.\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"company\": {\"type\": \"string\", \"nullable\": True},\n                \"age\": {\"type\": \"integer\", \"nullable\": True},\n                \"active\": {\"type\": \"boolean\", \"nullable\": True},\n            },\n        }\n        expected = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"company\": {\"type\": [\"string\", \"null\"]},\n                \"age\": {\"type\": [\"integer\", \"null\"]},\n                \"active\": {\"type\": [\"boolean\", \"null\"]},\n            },\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_mixed_nullable_and_non_nullable(self):\n        \"\"\"Test mix of nullable and non-nullable fields.\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"required_field\": {\"type\": \"string\"},\n                \"optional_nullable\": {\"type\": \"string\", \"nullable\": True},\n                \"optional_non_nullable\": {\"type\": \"string\"},\n            },\n            \"required\": [\"required_field\"],\n        }\n        expected = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"required_field\": {\"type\": \"string\"},\n                \"optional_nullable\": {\"type\": [\"string\", \"null\"]},\n                \"optional_non_nullable\": {\"type\": \"string\"},\n            },\n            \"required\": [\"required_field\"],\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_nullable_false_ignored(self):\n        \"\"\"Test that nullable: false is ignored (removed but no type change).\"\"\"\n        input_schema = {\"type\": \"string\", \"nullable\": False}\n        expected = {\"type\": \"string\"}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_no_nullable_field_unchanged(self):\n        \"\"\"Test that schemas without nullable field are unchanged.\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n        expected = input_schema.copy()\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_nullable_without_type_removes_nullable(self):\n        \"\"\"Test that nullable field is removed even without type.\"\"\"\n        input_schema = {\"nullable\": True, \"description\": \"Some field\"}\n        expected = {\"description\": \"Some field\"}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_preserves_other_fields(self):\n        \"\"\"Test that other fields are preserved during conversion.\"\"\"\n        input_schema = {\n            \"type\": \"string\",\n            \"nullable\": True,\n            \"description\": \"A nullable string\",\n            \"example\": \"test\",\n            \"format\": \"email\",\n        }\n        expected = {\n            \"type\": [\"string\", \"null\"],\n            \"description\": \"A nullable string\",\n            \"example\": \"test\",\n            \"format\": \"email\",\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_non_dict_input_unchanged(self):\n        \"\"\"Test that non-dict inputs are returned unchanged.\"\"\"\n        # These tests intentionally pass invalid types to check edge case handling\n        from typing import Any, cast\n\n        assert (\n            convert_openapi_schema_to_json_schema(cast(Any, \"string\"), \"3.0.0\")\n            == \"string\"\n        )\n        assert convert_openapi_schema_to_json_schema(cast(Any, 123), \"3.0.0\") == 123\n        assert convert_openapi_schema_to_json_schema(cast(Any, None), \"3.0.0\") is None\n        assert convert_openapi_schema_to_json_schema(cast(Any, [1, 2, 3]), \"3.0.0\") == [\n            1,\n            2,\n            3,\n        ]\n\n    def test_performance_optimization_no_copy_when_unchanged(self):\n        \"\"\"Test that schemas without nullable fields return the same object (no copy).\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"name\": {\"type\": \"string\"}},\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        # Should return the exact same object, not a copy\n        assert result is input_schema\n\n    def test_union_types_with_nullable(self):\n        \"\"\"Test nullable handling with existing union types (type as array).\"\"\"\n        input_schema = {\"type\": [\"string\", \"integer\"], \"nullable\": True}\n        expected = {\"type\": [\"string\", \"integer\", \"null\"]}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_already_nullable_union_unchanged(self):\n        \"\"\"Test that union types already containing null are not modified.\"\"\"\n        input_schema = {\"type\": [\"string\", \"null\"], \"nullable\": True}\n        expected = {\"type\": [\"string\", \"null\"]}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_property_level_union_with_nullable(self):\n        \"\"\"Test nullable handling with union types in properties.\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\"value\": {\"type\": [\"string\", \"integer\"], \"nullable\": True}},\n        }\n        expected = {\n            \"type\": \"object\",\n            \"properties\": {\"value\": {\"type\": [\"string\", \"integer\", \"null\"]}},\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_complex_union_nullable_scenarios(self):\n        \"\"\"Test various complex union type scenarios.\"\"\"\n        # Already has null in different position\n        input1 = {\"type\": [\"null\", \"string\", \"integer\"], \"nullable\": True}\n        result1 = convert_openapi_schema_to_json_schema(input1, \"3.0.0\")\n        assert result1 == {\"type\": [\"null\", \"string\", \"integer\"]}\n\n        # Single item array\n        input2 = {\"type\": [\"string\"], \"nullable\": True}\n        result2 = convert_openapi_schema_to_json_schema(input2, \"3.0.0\")\n        assert result2 == {\"type\": [\"string\", \"null\"]}\n\n    def test_oneof_with_nullable(self):\n        \"\"\"Test nullable handling with oneOf constructs.\"\"\"\n        input_schema = {\n            \"oneOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}],\n            \"nullable\": True,\n        }\n        expected = {\n            \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}, {\"type\": \"null\"}]\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_anyof_with_nullable(self):\n        \"\"\"Test nullable handling with anyOf constructs.\"\"\"\n        input_schema = {\n            \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}],\n            \"nullable\": True,\n        }\n        expected = {\n            \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}, {\"type\": \"null\"}]\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_anyof_already_nullable(self):\n        \"\"\"Test anyOf that already contains null type.\"\"\"\n        input_schema = {\n            \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}],\n            \"nullable\": True,\n        }\n        expected = {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}]}\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_allof_with_nullable(self):\n        \"\"\"Test nullable handling with allOf constructs.\"\"\"\n        input_schema = {\n            \"allOf\": [{\"type\": \"string\"}, {\"minLength\": 1}],\n            \"nullable\": True,\n        }\n        expected = {\n            \"anyOf\": [\n                {\"allOf\": [{\"type\": \"string\"}, {\"minLength\": 1}]},\n                {\"type\": \"null\"},\n            ]\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_property_level_oneof_with_nullable(self):\n        \"\"\"Test nullable handling with oneOf in properties.\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"value\": {\n                    \"oneOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}],\n                    \"nullable\": True,\n                }\n            },\n        }\n        expected = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"value\": {\n                    \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}, {\"type\": \"null\"}]\n                }\n            },\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_nullable_enum_field(self):\n        \"\"\"Test nullable enum field - issue #2082.\"\"\"\n        input_schema = {\n            \"type\": \"string\",\n            \"nullable\": True,\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\"],\n        }\n        expected = {\n            \"type\": [\"string\", \"null\"],\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\", None],\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_nullable_enum_already_contains_null(self):\n        \"\"\"Test nullable enum that already contains None.\"\"\"\n        input_schema = {\n            \"type\": \"string\",\n            \"nullable\": True,\n            \"enum\": [\"VALUE1\", \"VALUE2\", None],\n        }\n        expected = {\n            \"type\": [\"string\", \"null\"],\n            \"enum\": [\"VALUE1\", \"VALUE2\", None],\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_nullable_enum_without_type(self):\n        \"\"\"Test nullable enum without explicit type field.\"\"\"\n        input_schema = {\n            \"nullable\": True,\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\"],\n        }\n        expected = {\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\", None],\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_non_nullable_enum_unchanged(self):\n        \"\"\"Test that enum without nullable is unchanged.\"\"\"\n        input_schema = {\n            \"type\": \"string\",\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\"],\n        }\n        expected = {\n            \"type\": \"string\",\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\"],\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_property_level_nullable_enum(self):\n        \"\"\"Test nullable enum in object properties.\"\"\"\n        input_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"status\": {\n                    \"type\": \"string\",\n                    \"nullable\": True,\n                    \"enum\": [\"ACTIVE\", \"INACTIVE\", \"PENDING\"],\n                },\n                \"name\": {\"type\": \"string\"},\n            },\n        }\n        expected = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"status\": {\n                    \"type\": [\"string\", \"null\"],\n                    \"enum\": [\"ACTIVE\", \"INACTIVE\", \"PENDING\", None],\n                },\n                \"name\": {\"type\": \"string\"},\n            },\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n    def test_nullable_integer_enum(self):\n        \"\"\"Test nullable enum with integer values.\"\"\"\n        input_schema = {\n            \"type\": \"integer\",\n            \"nullable\": True,\n            \"enum\": [1, 2, 3],\n        }\n        expected = {\n            \"type\": [\"integer\", \"null\"],\n            \"enum\": [1, 2, 3, None],\n        }\n        result = convert_openapi_schema_to_json_schema(input_schema, \"3.0.0\")\n        assert result == expected\n\n\nclass TestNullableFieldValidation:\n    \"\"\"Test that converted schemas validate correctly with jsonschema.\"\"\"\n\n    def test_nullable_string_validates(self):\n        \"\"\"Test that nullable string validates both null and string values.\"\"\"\n        openapi_schema = {\"type\": \"string\", \"nullable\": True}\n        json_schema = convert_openapi_schema_to_json_schema(openapi_schema, \"3.0.0\")\n\n        # Both null and string should validate\n        validate(instance=None, schema=json_schema)\n        validate(instance=\"test\", schema=json_schema)\n\n        # Other types should fail\n        with pytest.raises(ValidationError):\n            validate(instance=123, schema=json_schema)\n\n    def test_nullable_enum_validates(self):\n        \"\"\"Test that nullable enum validates null, enum values, and rejects invalid values.\"\"\"\n        openapi_schema = {\n            \"type\": \"string\",\n            \"nullable\": True,\n            \"enum\": [\"VALUE1\", \"VALUE2\", \"VALUE3\"],\n        }\n        json_schema = convert_openapi_schema_to_json_schema(openapi_schema, \"3.0.0\")\n\n        # Null and enum values should validate\n        validate(instance=None, schema=json_schema)\n        validate(instance=\"VALUE1\", schema=json_schema)\n\n        # Invalid values should fail\n        with pytest.raises(ValidationError):\n            validate(instance=\"INVALID\", schema=json_schema)\n"
  },
  {
    "path": "tests/utilities/openapi/test_parser.py",
    "content": "\"\"\"Unit tests for OpenAPI parser.\"\"\"\n\nimport pytest\n\nfrom fastmcp.utilities.openapi.parser import parse_openapi_to_http_routes\n\n\nclass TestOpenAPIParser:\n    \"\"\"Test OpenAPI parsing functionality.\"\"\"\n\n    def test_parse_basic_openapi_30(self, basic_openapi_30_spec):\n        \"\"\"Test parsing a basic OpenAPI 3.0 spec.\"\"\"\n        routes = parse_openapi_to_http_routes(basic_openapi_30_spec)\n\n        assert len(routes) == 1\n        route = routes[0]\n\n        assert route.path == \"/users/{id}\"\n        assert route.method == \"GET\"\n        assert route.operation_id == \"get_user\"\n        assert route.summary == \"Get user by ID\"\n\n        # Check parameters\n        assert len(route.parameters) == 1\n        param = route.parameters[0]\n        assert param.name == \"id\"\n        assert param.location == \"path\"\n        assert param.required is True\n        assert param.schema_[\"type\"] == \"integer\"\n\n        # Check pre-calculated fields\n        assert hasattr(route, \"flat_param_schema\")\n        assert hasattr(route, \"parameter_map\")\n        assert route.flat_param_schema is not None\n        assert route.parameter_map is not None\n\n    def test_parse_basic_openapi_31(self, basic_openapi_31_spec):\n        \"\"\"Test parsing a basic OpenAPI 3.1 spec.\"\"\"\n        routes = parse_openapi_to_http_routes(basic_openapi_31_spec)\n\n        assert len(routes) == 1\n        route = routes[0]\n\n        assert route.path == \"/users/{id}\"\n        assert route.method == \"GET\"\n        assert route.operation_id == \"get_user\"\n\n        # Same structure should work for both 3.0 and 3.1\n        assert len(route.parameters) == 1\n        param = route.parameters[0]\n        assert param.name == \"id\"\n        assert param.location == \"path\"\n\n    def test_parse_collision_spec(self, collision_spec):\n        \"\"\"Test parsing spec with parameter collisions.\"\"\"\n        routes = parse_openapi_to_http_routes(collision_spec)\n\n        assert len(routes) == 1\n        route = routes[0]\n\n        assert route.operation_id == \"update_user\"\n\n        # Should have path parameter\n        path_params = [p for p in route.parameters if p.location == \"path\"]\n        assert len(path_params) == 1\n        assert path_params[0].name == \"id\"\n\n        # Should have request body\n        assert route.request_body is not None\n        assert route.request_body.required is True\n\n        # Check that parameter map handles collisions\n        assert route.parameter_map is not None\n        # Should have entries for both path and body parameters\n        assert len(route.parameter_map) >= 2  # At least path id and body fields\n\n    def test_parse_deepobject_spec(self, deepobject_spec):\n        \"\"\"Test parsing spec with deepObject parameters.\"\"\"\n        routes = parse_openapi_to_http_routes(deepobject_spec)\n\n        assert len(routes) == 1\n        route = routes[0]\n\n        assert route.operation_id == \"search\"\n\n        # Should have deepObject parameter\n        assert len(route.parameters) == 1\n        param = route.parameters[0]\n        assert param.name == \"filter\"\n        assert param.location == \"query\"\n        assert param.style == \"deepObject\"\n        assert param.explode is True\n        assert param.schema_[\"type\"] == \"object\"\n\n    def test_parse_complex_spec(self, complex_spec):\n        \"\"\"Test parsing complex spec with multiple parameter types.\"\"\"\n        routes = parse_openapi_to_http_routes(complex_spec)\n\n        assert len(routes) == 1\n        route = routes[0]\n\n        assert route.operation_id == \"update_item\"\n\n        # Should have multiple parameters\n        assert len(route.parameters) == 3\n\n        # Check parameter locations\n        locations = {p.location for p in route.parameters}\n        assert locations == {\"path\", \"query\", \"header\"}\n\n        # Check specific parameters\n        path_param = next(p for p in route.parameters if p.location == \"path\")\n        assert path_param.name == \"id\"\n        assert path_param.required is True\n\n        query_param = next(p for p in route.parameters if p.location == \"query\")\n        assert query_param.name == \"version\"\n        assert query_param.required is False\n        assert query_param.schema_.get(\"default\") == 1\n\n        header_param = next(p for p in route.parameters if p.location == \"header\")\n        assert header_param.name == \"X-Client-Version\"\n        assert header_param.required is False\n\n        # Check request body\n        assert route.request_body is not None\n        assert route.request_body.required is True\n\n    def test_parse_empty_spec(self):\n        \"\"\"Test parsing spec with no paths.\"\"\"\n        empty_spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Empty API\", \"version\": \"1.0.0\"},\n            \"paths\": {},\n        }\n\n        routes = parse_openapi_to_http_routes(empty_spec)\n        assert len(routes) == 0\n\n    def test_parse_invalid_spec(self):\n        \"\"\"Test parsing invalid OpenAPI spec.\"\"\"\n        invalid_spec = {\n            \"openapi\": \"3.0.0\",\n            # Missing required fields\n        }\n\n        with pytest.raises(ValueError, match=\"Invalid OpenAPI schema\"):\n            parse_openapi_to_http_routes(invalid_spec)\n\n    def test_parse_spec_with_refs(self):\n        \"\"\"Test parsing spec with $ref references.\"\"\"\n        spec_with_refs = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Ref Test API\", \"version\": \"1.0.0\"},\n            \"components\": {\n                \"schemas\": {\n                    \"User\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"id\": {\"type\": \"integer\"},\n                            \"name\": {\"type\": \"string\"},\n                        },\n                    }\n                },\n                \"parameters\": {\n                    \"UserId\": {\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": True,\n                        \"schema\": {\"type\": \"integer\"},\n                    }\n                },\n            },\n            \"paths\": {\n                \"/users/{id}\": {\n                    \"get\": {\n                        \"operationId\": \"get_user\",\n                        \"parameters\": [{\"$ref\": \"#/components/parameters/UserId\"}],\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"User\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\"$ref\": \"#/components/schemas/User\"}\n                                    }\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n        }\n\n        routes = parse_openapi_to_http_routes(spec_with_refs)\n\n        assert len(routes) == 1\n        route = routes[0]\n\n        # Parameter should be resolved from $ref\n        assert len(route.parameters) == 1\n        param = route.parameters[0]\n        assert param.name == \"id\"\n        assert param.location == \"path\"\n        assert param.required is True\n\n    def test_parse_simple_transitive_refs(self):\n        \"\"\"Test that A->B->C transitive references are preserved.\n\n        When a request body references schema A, which references B, which references C:\n        - A is expanded inline (expected optimization)\n        - B and C MUST be included in $defs (the bug fix for #1372)\n        \"\"\"\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Test\", \"version\": \"1.0.0\"},\n            \"components\": {\n                \"schemas\": {\n                    \"SchemaA\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"refToB\": {\"$ref\": \"#/components/schemas/SchemaB\"}\n                        },\n                    },\n                    \"SchemaB\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"refToC\": {\"$ref\": \"#/components/schemas/SchemaC\"}\n                        },\n                    },\n                    \"SchemaC\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"value1\", \"value2\"],\n                    },\n                }\n            },\n            \"paths\": {\n                \"/test\": {\n                    \"post\": {\n                        \"operationId\": \"test_op\",\n                        \"requestBody\": {\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\"$ref\": \"#/components/schemas/SchemaA\"}\n                                }\n                            }\n                        },\n                        \"responses\": {\"200\": {\"description\": \"OK\"}},\n                    }\n                }\n            },\n        }\n\n        routes = parse_openapi_to_http_routes(spec)\n        route = routes[0]\n\n        # SchemaA is expanded inline, so it's NOT in request_schemas\n        assert \"SchemaA\" not in route.request_schemas\n\n        # But SchemaB and SchemaC MUST be there (transitive dependencies)\n        assert \"SchemaB\" in route.request_schemas\n        assert \"SchemaC\" in route.request_schemas\n\n        # Same in the flat parameter schema\n        assert \"SchemaB\" in route.flat_param_schema[\"$defs\"]\n        assert \"SchemaC\" in route.flat_param_schema[\"$defs\"]\n\n    def test_parse_tspicer_issue_1372(self):\n        \"\"\"Reproduce the exact bug from issue #1372 (tspicer's report).\n\n        Issue: Profile -> {countryCode, AccountInfo} transitive refs were missing from $defs.\n        \"\"\"\n        spec = {\n            \"openapi\": \"3.0.1\",\n            \"info\": {\"title\": \"Test\", \"version\": \"1.0.0\"},\n            \"components\": {\n                \"schemas\": {\n                    \"Profile\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"profileId\": {\"type\": \"integer\"},\n                            \"countryCode\": {\"$ref\": \"#/components/schemas/countryCode\"},\n                            \"accountInfo\": {\"$ref\": \"#/components/schemas/AccountInfo\"},\n                        },\n                    },\n                    \"countryCode\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"US\", \"UK\", \"CA\", \"AU\"],\n                    },\n                    \"AccountInfo\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"accountId\": {\"type\": \"string\"},\n                            \"accountType\": {\"type\": \"string\"},\n                        },\n                    },\n                }\n            },\n            \"paths\": {\n                \"/profile\": {\n                    \"post\": {\n                        \"operationId\": \"create_profile\",\n                        \"requestBody\": {\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\"$ref\": \"#/components/schemas/Profile\"}\n                                }\n                            },\n                        },\n                        \"responses\": {\"200\": {\"description\": \"OK\"}},\n                    }\n                }\n            },\n        }\n\n        routes = parse_openapi_to_http_routes(spec)\n        route = routes[0]\n\n        # Profile is expanded inline, NOT in schema_defs\n        assert \"Profile\" not in route.request_schemas\n\n        # Bug fix: countryCode and AccountInfo MUST be in schema_defs\n        assert \"countryCode\" in route.request_schemas  # Was missing in #1372\n        assert \"AccountInfo\" in route.request_schemas  # Was missing in #1372\n\n        # Same in flat parameter schema\n        assert \"countryCode\" in route.flat_param_schema[\"$defs\"]\n        assert \"AccountInfo\" in route.flat_param_schema[\"$defs\"]\n\n        # Verify Profile's properties were inlined correctly\n        props = route.flat_param_schema[\"properties\"]\n        assert \"profileId\" in props\n        assert props[\"countryCode\"][\"$ref\"] == \"#/$defs/countryCode\"\n        assert props[\"accountInfo\"][\"$ref\"] == \"#/$defs/AccountInfo\"\n\n    def test_parameter_schema_extraction(self, complex_spec):\n        \"\"\"Test that parameter schemas are properly extracted.\"\"\"\n        routes = parse_openapi_to_http_routes(complex_spec)\n        route = routes[0]\n\n        # Check that flat_param_schema contains all parameters\n        flat_schema = route.flat_param_schema\n        assert flat_schema[\"type\"] == \"object\"\n        assert \"properties\" in flat_schema\n\n        properties = flat_schema[\"properties\"]\n\n        # Should contain path, query, and body parameters\n        assert \"id\" in properties or any(\"id\" in key for key in properties.keys())\n        assert \"title\" in properties  # From request body\n\n        # Check parameter mapping\n        param_map = route.parameter_map\n        assert len(param_map) > 0\n\n        # Each mapped parameter should have location and openapi_name\n        for param_name, mapping in param_map.items():\n            assert \"location\" in mapping\n            assert \"openapi_name\" in mapping\n            assert mapping[\"location\"] in [\"path\", \"query\", \"header\", \"body\"]\n\n\nclass TestParameterLocationHandling:\n    \"\"\"Test parameter location conversion and handling.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"location_str,expected\",\n        [\n            (\"path\", \"path\"),\n            (\"query\", \"query\"),\n            (\"header\", \"header\"),\n            (\"cookie\", \"cookie\"),\n            (\"unknown\", \"query\"),  # Should default to query\n        ],\n    )\n    def test_parameter_location_conversion(self, location_str, expected):\n        \"\"\"Test parameter location string conversion.\"\"\"\n        # Create a simple spec with the parameter location\n        spec = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Location Test\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/test\": {\n                    \"get\": {\n                        \"operationId\": \"test_op\",\n                        \"parameters\": [\n                            {\n                                \"name\": \"test_param\",\n                                \"in\": location_str,\n                                \"schema\": {\"type\": \"string\"},\n                            }\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"OK\"}},\n                    }\n                }\n            },\n        }\n\n        if location_str == \"unknown\":\n            # Should raise validation error for unknown location\n            with pytest.raises(ValueError, match=\"Invalid OpenAPI schema\"):\n                parse_openapi_to_http_routes(spec)\n        else:\n            routes = parse_openapi_to_http_routes(spec)\n            route = routes[0]\n            param = route.parameters[0]\n            assert param.location == expected\n\n\nclass TestErrorHandling:\n    \"\"\"Test error handling in parser.\"\"\"\n\n    def test_external_ref_error(self):\n        \"\"\"Test that external references are handled gracefully.\"\"\"\n        spec_with_external_ref = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"External Ref Test\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/test\": {\n                    \"get\": {\n                        \"operationId\": \"test_op\",\n                        \"parameters\": [\n                            {\n                                \"$ref\": \"external-file.yaml#/components/parameters/ExternalParam\"\n                            }\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"OK\"}},\n                    }\n                }\n            },\n        }\n\n        # Should not crash but skip the invalid parameter\n        routes = parse_openapi_to_http_routes(spec_with_external_ref)\n        assert len(routes) == 1\n        assert (\n            len(routes[0].parameters) == 0\n        )  # External ref parameter should be skipped\n\n    def test_broken_ref_error(self):\n        \"\"\"Test that broken internal references are handled gracefully.\"\"\"\n        spec_with_broken_ref = {\n            \"openapi\": \"3.0.0\",\n            \"info\": {\"title\": \"Broken Ref Test\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/test\": {\n                    \"get\": {\n                        \"operationId\": \"test_op\",\n                        \"parameters\": [\n                            {\"$ref\": \"#/components/parameters/NonExistentParam\"}\n                        ],\n                        \"responses\": {\"200\": {\"description\": \"OK\"}},\n                    }\n                }\n            },\n        }\n\n        # Should handle broken refs gracefully and continue parsing\n        routes = parse_openapi_to_http_routes(spec_with_broken_ref)\n        # May have empty routes or skip the broken operation\n        assert isinstance(routes, list)\n"
  },
  {
    "path": "tests/utilities/openapi/test_propertynames_ref_rewrite.py",
    "content": "from fastmcp.utilities.openapi.schemas import _replace_ref_with_defs\n\n\ndef test_replace_ref_with_defs_rewrites_propertyNames_ref():\n    \"\"\"\n    Regression test for issue #3303.\n\n    When using dict[StrEnum, Model], Pydantic generates:\n\n        {\n            \"type\": \"object\",\n            \"propertyNames\": {\"$ref\": \"#/components/schemas/Category\"},\n            \"additionalProperties\": {\"$ref\": \"#/components/schemas/ItemInfo\"}\n        }\n\n    _replace_ref_with_defs should rewrite BOTH refs to #/$defs/.\n    \"\"\"\n\n    schema = {\n        \"type\": \"object\",\n        \"propertyNames\": {\"$ref\": \"#/components/schemas/Category\"},\n        \"additionalProperties\": {\"$ref\": \"#/components/schemas/ItemInfo\"},\n    }\n\n    result = _replace_ref_with_defs(schema)\n\n    # additionalProperties ref is rewritten\n    assert result[\"additionalProperties\"][\"$ref\"] == \"#/$defs/ItemInfo\"\n\n    # propertyNames ref must also be rewritten (this was the bug)\n    assert result[\"propertyNames\"][\"$ref\"] == \"#/$defs/Category\"\n\n    # Ensure no dangling OpenAPI refs remain\n    assert \"#/components/schemas/\" not in str(result)\n"
  },
  {
    "path": "tests/utilities/openapi/test_schemas.py",
    "content": "\"\"\"Unit tests for schema processing and parameter mapping.\"\"\"\n\nimport pytest\n\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.openapi.models import (\n    HTTPRoute,\n    ParameterInfo,\n    RequestBodyInfo,\n)\nfrom fastmcp.utilities.openapi.schemas import (\n    _combine_schemas,\n    _combine_schemas_and_map_params,\n    _replace_ref_with_defs,\n)\n\n\nclass TestSchemaProcessing:\n    \"\"\"Test schema processing utilities.\"\"\"\n\n    @pytest.fixture\n    def simple_route(self):\n        \"\"\"Create a simple route for testing.\"\"\"\n        return HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"GET\",\n            operation_id=\"get_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                )\n            ],\n        )\n\n    @pytest.fixture\n    def collision_route(self):\n        \"\"\"Create a route with parameter name collisions.\"\"\"\n        return HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"PUT\",\n            operation_id=\"update_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"integer\"},\n                    description=\"User ID in path\",\n                )\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"id\": {\"type\": \"integer\", \"description\": \"User ID in body\"},\n                            \"name\": {\"type\": \"string\"},\n                            \"email\": {\"type\": \"string\"},\n                        },\n                        \"required\": [\"name\"],\n                    }\n                },\n            ),\n        )\n\n    @pytest.fixture\n    def complex_route(self):\n        \"\"\"Create a complex route with multiple parameter types.\"\"\"\n        return HTTPRoute(\n            path=\"/items/{id}\",\n            method=\"PATCH\",\n            operation_id=\"update_item\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\",\n                    location=\"path\",\n                    required=True,\n                    schema={\"type\": \"string\"},\n                ),\n                ParameterInfo(\n                    name=\"version\",\n                    location=\"query\",\n                    required=False,\n                    schema={\"type\": \"integer\", \"default\": 1},\n                ),\n                ParameterInfo(\n                    name=\"X-Client-Version\",\n                    location=\"header\",\n                    required=False,\n                    schema={\"type\": \"string\"},\n                ),\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"title\": {\"type\": \"string\"},\n                            \"description\": {\"type\": \"string\"},\n                            \"tags\": {\n                                \"type\": \"array\",\n                                \"items\": {\"type\": \"string\"},\n                            },\n                        },\n                        \"required\": [\"title\"],\n                    }\n                },\n            ),\n        )\n\n    def test_combine_schemas_simple(self, simple_route):\n        \"\"\"Test combining schemas for a simple route.\"\"\"\n        combined_schema = _combine_schemas(simple_route)\n\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"properties\" in combined_schema\n\n        properties = combined_schema[\"properties\"]\n        assert \"id\" in properties\n        assert properties[\"id\"][\"type\"] == \"integer\"\n\n        required = combined_schema.get(\"required\", [])\n        assert \"id\" in required\n\n    def test_combine_schemas_with_collisions(self, collision_route):\n        \"\"\"Test combining schemas with parameter name collisions.\"\"\"\n        combined_schema = _combine_schemas(collision_route)\n\n        assert combined_schema[\"type\"] == \"object\"\n        properties = combined_schema[\"properties\"]\n\n        # Should handle collision by suffixing\n        id_params = [key for key in properties.keys() if \"id\" in key]\n        assert len(id_params) >= 2  # Should have both path and body id\n\n        # Should have other body parameters\n        assert \"name\" in properties\n        assert \"email\" in properties\n\n    def test_combine_schemas_complex(self, complex_route):\n        \"\"\"Test combining schemas for complex route.\"\"\"\n        combined_schema = _combine_schemas(complex_route)\n\n        properties = combined_schema[\"properties\"]\n\n        # Should have path parameter\n        assert \"id\" in properties\n\n        # Should have query parameter\n        assert \"version\" in properties\n        assert properties[\"version\"].get(\"default\") == 1\n\n        # Should have header parameter\n        assert \"X-Client-Version\" in properties\n\n        # Should have body parameters\n        assert \"title\" in properties\n        assert \"description\" in properties\n        assert \"tags\" in properties\n\n        # Check required fields\n        required = combined_schema.get(\"required\", [])\n        assert \"id\" in required  # Path parameters are required\n        assert \"title\" in required  # Required body parameter\n\n    def test_combine_schemas_and_map_params_simple(self, simple_route):\n        \"\"\"Test combining schemas and creating parameter map.\"\"\"\n        combined_schema, param_map = _combine_schemas_and_map_params(simple_route)\n\n        # Check schema\n        assert combined_schema[\"type\"] == \"object\"\n        assert \"id\" in combined_schema[\"properties\"]\n\n        # Check parameter map\n        assert len(param_map) == 1\n        assert \"id\" in param_map\n        assert param_map[\"id\"][\"location\"] == \"path\"\n        assert param_map[\"id\"][\"openapi_name\"] == \"id\"\n\n    def test_combine_schemas_and_map_params_with_collisions(self, collision_route):\n        \"\"\"Test parameter mapping with collisions.\"\"\"\n        combined_schema, param_map = _combine_schemas_and_map_params(collision_route)\n\n        # Check that we have entries for both conflicting parameters\n        path_id_key = None\n        body_id_key = None\n\n        for key, mapping in param_map.items():\n            if mapping[\"location\"] == \"path\" and mapping[\"openapi_name\"] == \"id\":\n                path_id_key = key\n            elif mapping[\"location\"] == \"body\" and mapping[\"openapi_name\"] == \"id\":\n                body_id_key = key\n\n        assert path_id_key is not None\n        assert body_id_key is not None\n        assert path_id_key != body_id_key  # Should be different keys\n\n        # Both should exist in schema\n        assert path_id_key in combined_schema[\"properties\"]\n        assert body_id_key in combined_schema[\"properties\"]\n\n        # Should also have non-conflicting parameters\n        assert \"name\" in param_map\n        assert \"email\" in param_map\n\n    def test_combine_schemas_and_map_params_complex(self, complex_route):\n        \"\"\"Test parameter mapping for complex route.\"\"\"\n        combined_schema, param_map = _combine_schemas_and_map_params(complex_route)\n\n        # Should have all parameters mapped\n        actual_locations = {mapping[\"location\"] for mapping in param_map.values()}\n\n        # Should have representatives from each location\n        assert \"path\" in actual_locations\n        assert \"body\" in actual_locations\n        # May or may not have query/header depending on whether they're included\n\n        # Check specific mappings\n        id_mapping = param_map[\"id\"]\n        assert id_mapping[\"location\"] == \"path\"\n        assert id_mapping[\"openapi_name\"] == \"id\"\n\n        title_mapping = param_map[\"title\"]\n        assert title_mapping[\"location\"] == \"body\"\n        assert title_mapping[\"openapi_name\"] == \"title\"\n\n    def test_replace_ref_with_defs(self):\n        \"\"\"Test replacing $ref with $defs for JSON Schema compatibility.\"\"\"\n\n        schema_with_ref = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"user\": {\"$ref\": \"#/components/schemas/User\"},\n                \"items\": {\n                    \"type\": \"array\",\n                    \"items\": {\"$ref\": \"#/components/schemas/Item\"},\n                },\n            },\n        }\n\n        # Use our recursive replacement approach\n        result = _replace_ref_with_defs(schema_with_ref)\n\n        assert result[\"properties\"][\"user\"][\"$ref\"] == \"#/$defs/User\"\n        assert result[\"properties\"][\"items\"][\"items\"][\"$ref\"] == \"#/$defs/Item\"\n\n    def test_replace_ref_with_defs_nested(self):\n        \"\"\"Test replacing $ref in deeply nested structures.\"\"\"\n\n        nested_schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"data\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"nested\": {\"$ref\": \"#/components/schemas/Nested\"},\n                    },\n                },\n                \"items\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"ref_prop\": {\"$ref\": \"#/components/schemas/RefProp\"},\n                        },\n                    },\n                },\n            },\n        }\n\n        # Use our recursive replacement approach\n        result = _replace_ref_with_defs(nested_schema)\n\n        # Check nested object property\n        nested_prop = result[\"properties\"][\"data\"][\"properties\"][\"nested\"]\n        assert nested_prop[\"$ref\"] == \"#/$defs/Nested\"\n\n        # Check array item property\n        array_item_prop = result[\"properties\"][\"items\"][\"items\"][\"properties\"][\n            \"ref_prop\"\n        ]\n        assert array_item_prop[\"$ref\"] == \"#/$defs/RefProp\"\n\n    def test_replace_ref_with_defs_in_additional_properties(self):\n        \"\"\"Test replacing $ref deeply in 'additionalProperties'.\"\"\"\n\n        add_props_schema = {\n            \"description\": \"An invoice with a fixed header and a flexible set of line items.\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"invoice_number\": {\n                    \"type\": \"string\",\n                    \"description\": \"The unique identifier for the invoice.\",\n                },\n                \"customer_name\": {\n                    \"type\": \"string\",\n                    \"description\": \"The name of the customer.\",\n                },\n                \"total_amount\": {\n                    \"type\": \"number\",\n                    \"description\": \"The total amount of the invoice.\",\n                },\n            },\n            \"required\": [\"invoice_number\", \"customer_name\", \"total_amount\"],\n            \"additionalProperties\": {\"$ref\": \"#/components/schemas/Link\"},\n        }\n\n        # Use our recursive replacement approach\n        result = _replace_ref_with_defs(add_props_schema)\n\n        # Check additional properties\n        add_props = result[\"additionalProperties\"]\n        assert add_props[\"$ref\"] == \"#/$defs/Link\"\n\n    def test_replace_ref_with_defs_with_bool_additional_properties(self):\n        \"\"\"Test replacing a bool 'additionalProperties'.\"\"\"\n\n        add_props_schema = {\n            \"description\": \"An invoice with a fixed header and a flexible set of line items.\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"invoice_number\": {\n                    \"type\": \"string\",\n                    \"description\": \"The unique identifier for the invoice.\",\n                },\n                \"customer_name\": {\n                    \"type\": \"string\",\n                    \"description\": \"The name of the customer.\",\n                },\n                \"total_amount\": {\n                    \"type\": \"number\",\n                    \"description\": \"The total amount of the invoice.\",\n                },\n            },\n            \"required\": [\"invoice_number\", \"customer_name\", \"total_amount\"],\n            \"additionalProperties\": False,\n        }\n\n        # Use our recursive replacement approach\n        result = _replace_ref_with_defs(add_props_schema)\n\n        # Check additional properties\n        add_props = result[\"additionalProperties\"]\n        assert add_props is False\n\n    def test_replace_ref_with_defs_with_inner_schema_additional_properties(self):\n        \"\"\"Test replacing a inner schema 'additionalProperties'.\"\"\"\n\n        add_props_schema = {\n            \"description\": \"An invoice with a fixed header and a flexible set of line items.\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"invoice_number\": {\n                    \"type\": \"string\",\n                    \"description\": \"The unique identifier for the invoice.\",\n                },\n                \"customer_name\": {\n                    \"type\": \"string\",\n                    \"description\": \"The name of the customer.\",\n                },\n                \"total_amount\": {\n                    \"type\": \"number\",\n                    \"description\": \"The total amount of the invoice.\",\n                },\n            },\n            \"required\": [\"invoice_number\", \"customer_name\", \"total_amount\"],\n            \"additionalProperties\": {\n                \"type\": \"integer\",\n                \"format\": \"int32\",\n                \"description\": \"The total amount of the invoice.\",\n            },\n        }\n\n        # Use our recursive replacement approach\n        result = _replace_ref_with_defs(add_props_schema)\n\n        # Check additional properties\n        add_props = result[\"additionalProperties\"]\n        assert add_props == {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"description\": \"The total amount of the invoice.\",\n        }\n\n    def test_parameter_collision_suffixing_logic(self):\n        \"\"\"Test the specific logic for parameter collision suffixing.\"\"\"\n        # Create a route that would definitely cause collisions\n        route = HTTPRoute(\n            path=\"/test/{id}\",\n            method=\"POST\",\n            operation_id=\"test_collision\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\", location=\"path\", required=True, schema={\"type\": \"string\"}\n                ),\n                ParameterInfo(\n                    name=\"name\",\n                    location=\"query\",\n                    required=False,\n                    schema={\"type\": \"string\"},\n                ),\n                ParameterInfo(\n                    name=\"name\",\n                    location=\"header\",\n                    required=False,\n                    schema={\"type\": \"string\"},\n                ),\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"id\": {\"type\": \"integer\"},\n                            \"name\": {\"type\": \"string\"},\n                            \"description\": {\"type\": \"string\"},\n                        },\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Check that all parameters are included with unique keys\n        param_keys = list(param_map.keys())\n        assert len(param_keys) == len(set(param_keys))  # All keys should be unique\n\n        # Should have some form of id and name parameters\n        id_keys = [key for key in param_keys if \"id\" in key]\n        name_keys = [key for key in param_keys if \"name\" in key]\n\n        assert len(id_keys) >= 2  # Path id and body id\n        assert len(name_keys) >= 3  # Query name, header name, and body name\n\n        # Check that locations are correctly mapped\n        path_params = [\n            key for key, mapping in param_map.items() if mapping[\"location\"] == \"path\"\n        ]\n        query_params = [\n            key for key, mapping in param_map.items() if mapping[\"location\"] == \"query\"\n        ]\n        header_params = [\n            key for key, mapping in param_map.items() if mapping[\"location\"] == \"header\"\n        ]\n        body_params = [\n            key for key, mapping in param_map.items() if mapping[\"location\"] == \"body\"\n        ]\n\n        assert len(path_params) == 1\n        assert len(query_params) == 1\n        assert len(header_params) == 1\n        assert len(body_params) >= 3  # id, name, description from body\n\n\nclass TestEdgeCases:\n    \"\"\"Test edge cases in schema processing.\"\"\"\n\n    def test_empty_route(self):\n        \"\"\"Test schema processing with empty route.\"\"\"\n        empty_route = HTTPRoute(\n            path=\"/empty\",\n            method=\"GET\",\n            operation_id=\"empty_op\",\n            parameters=[],\n        )\n\n        combined_schema = _combine_schemas(empty_route)\n\n        assert combined_schema[\"type\"] == \"object\"\n        assert combined_schema[\"properties\"] == {}\n        assert combined_schema.get(\"required\", []) == []\n\n    def test_route_without_request_body(self):\n        \"\"\"Test route with only parameters, no request body.\"\"\"\n        route = HTTPRoute(\n            path=\"/test/{id}\",\n            method=\"GET\",\n            operation_id=\"test_get\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\", location=\"path\", required=True, schema={\"type\": \"string\"}\n                ),\n                ParameterInfo(\n                    name=\"filter\",\n                    location=\"query\",\n                    required=False,\n                    schema={\"type\": \"string\"},\n                ),\n            ],\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        assert \"id\" in combined_schema[\"properties\"]\n        assert \"filter\" in combined_schema[\"properties\"]\n        assert len(param_map) == 2\n\n    def test_route_with_only_request_body(self):\n        \"\"\"Test route with only request body, no parameters.\"\"\"\n        route = HTTPRoute(\n            path=\"/create\",\n            method=\"POST\",\n            operation_id=\"create_item\",\n            parameters=[],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"name\": {\"type\": \"string\"},\n                            \"description\": {\"type\": \"string\"},\n                        },\n                        \"required\": [\"name\"],\n                    }\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        assert \"name\" in combined_schema[\"properties\"]\n        assert \"description\" in combined_schema[\"properties\"]\n        assert \"name\" in combined_schema[\"required\"]\n        assert len(param_map) == 2\n\n    def test_parameter_without_schema(self):\n        \"\"\"Test handling parameters without schema.\"\"\"\n        # Use minimal schema to avoid validation error\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"GET\",\n            operation_id=\"test_no_schema\",\n            parameters=[\n                ParameterInfo(\n                    name=\"param1\", location=\"query\", required=False, schema={}\n                ),  # Empty schema\n            ],\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should handle gracefully\n        assert combined_schema[\"type\"] == \"object\"\n        assert isinstance(param_map, dict)\n\n    def test_request_body_multiple_content_types(self):\n        \"\"\"Test request body with multiple content types.\"\"\"\n        route = HTTPRoute(\n            path=\"/upload\",\n            method=\"POST\",\n            operation_id=\"upload_file\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"metadata\": {\"type\": \"string\"}},\n                    },\n                    \"multipart/form-data\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"file\": {\"type\": \"string\", \"format\": \"binary\"}},\n                    },\n                },\n            ),\n        )\n\n        combined_schema, param_map = _combine_schemas_and_map_params(route)\n\n        # Should use the first content type found\n        properties = combined_schema[\"properties\"]\n        assert (\n            len(properties) > 0\n        )  # Should have some properties from one of the content types\n\n    def test_oneof_reference_dereferenced(self):\n        \"\"\"Test that schemas referenced in oneOf are preserved and unused defs pruned.\"\"\"\n\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"data\": {\"oneOf\": [{\"$ref\": \"#/$defs/TestSchema\"}]}},\n            \"$defs\": {\n                \"TestSchema\": {\"type\": \"string\"},\n                \"UnusedSchema\": {\"type\": \"number\"},\n            },\n        }\n\n        result = compress_schema(schema)\n\n        # UnusedSchema should be pruned, TestSchema should be kept\n        assert \"UnusedSchema\" not in result.get(\"$defs\", {})\n        assert result[\"$defs\"][\"TestSchema\"] == {\"type\": \"string\"}\n\n        # $ref should be preserved in oneOf\n        assert result[\"properties\"][\"data\"][\"oneOf\"] == [{\"$ref\": \"#/$defs/TestSchema\"}]\n\n    def test_anyof_reference_dereferenced(self):\n        \"\"\"Test that schemas referenced in anyOf are preserved and unused defs pruned.\"\"\"\n\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"data\": {\"anyOf\": [{\"$ref\": \"#/$defs/TestSchema\"}]}},\n            \"$defs\": {\n                \"TestSchema\": {\"type\": \"string\"},\n                \"UnusedSchema\": {\"type\": \"number\"},\n            },\n        }\n\n        result = compress_schema(schema)\n\n        # UnusedSchema should be pruned, TestSchema should be kept\n        assert \"UnusedSchema\" not in result.get(\"$defs\", {})\n        assert result[\"$defs\"][\"TestSchema\"] == {\"type\": \"string\"}\n\n        # $ref should be preserved in anyOf\n        assert result[\"properties\"][\"data\"][\"anyOf\"] == [{\"$ref\": \"#/$defs/TestSchema\"}]\n\n    def test_allof_reference_dereferenced(self):\n        \"\"\"Test that schemas referenced in allOf are preserved and unused defs pruned.\"\"\"\n\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"data\": {\"allOf\": [{\"$ref\": \"#/$defs/TestSchema\"}]}},\n            \"$defs\": {\n                \"TestSchema\": {\"type\": \"string\"},\n                \"UnusedSchema\": {\"type\": \"number\"},\n            },\n        }\n\n        result = compress_schema(schema)\n\n        # UnusedSchema should be pruned, TestSchema should be kept\n        assert \"UnusedSchema\" not in result.get(\"$defs\", {})\n        assert result[\"$defs\"][\"TestSchema\"] == {\"type\": \"string\"}\n\n        # $ref should be preserved in allOf\n        assert result[\"properties\"][\"data\"][\"allOf\"] == [{\"$ref\": \"#/$defs/TestSchema\"}]\n"
  },
  {
    "path": "tests/utilities/openapi/test_transitive_references.py",
    "content": "\"\"\"Comprehensive tests for transitive and nested reference handling (Issue #1372).\"\"\"\n\nfrom fastmcp.utilities.openapi.models import (\n    HTTPRoute,\n    ParameterInfo,\n    RequestBodyInfo,\n    ResponseInfo,\n)\nfrom fastmcp.utilities.openapi.parser import parse_openapi_to_http_routes\nfrom fastmcp.utilities.openapi.schemas import (\n    _combine_schemas_and_map_params,\n    extract_output_schema_from_responses,\n)\n\n\nclass TestTransitiveAndNestedReferences:\n    \"\"\"Comprehensive tests for transitive and nested reference handling (Issue #1372).\"\"\"\n\n    def test_nested_refs_in_schema_definitions_converted(self):\n        \"\"\"$refs inside schema definitions must be converted from OpenAPI to JSON Schema format.\"\"\"\n        route = HTTPRoute(\n            path=\"/users/{id}\",\n            method=\"POST\",\n            operation_id=\"create_user\",\n            parameters=[\n                ParameterInfo(\n                    name=\"id\", location=\"path\", required=True, schema={\"type\": \"string\"}\n                )\n            ],\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"user\": {\"$ref\": \"#/components/schemas/User\"}},\n                    }\n                },\n            ),\n            request_schemas={\n                \"User\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"profile\": {\"$ref\": \"#/components/schemas/Profile\"}},\n                },\n                \"Profile\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"name\": {\"type\": \"string\"}},\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # Root level refs should be converted\n        assert combined_schema[\"properties\"][\"user\"][\"$ref\"] == \"#/$defs/User\"\n\n        # Refs inside schema definitions should also be converted\n        user_def = combined_schema[\"$defs\"][\"User\"]\n        assert user_def[\"properties\"][\"profile\"][\"$ref\"] == \"#/$defs/Profile\"\n\n    def test_transitive_dependencies_in_response_schemas(self):\n        \"\"\"Transitive dependencies (A→B→C) must all be preserved in response schemas.\"\"\"\n        # This mimics the exact structure reported in issue #1372\n        responses = {\n            \"201\": ResponseInfo(\n                description=\"User created\",\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/User\"}\n                },\n            )\n        }\n\n        schema_definitions = {\n            \"User\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"id\": {\"type\": \"string\"},\n                    \"profile\": {\"$ref\": \"#/components/schemas/Profile\"},\n                },\n                \"required\": [\"id\", \"profile\"],\n            },\n            \"Profile\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"address\": {\"$ref\": \"#/components/schemas/Address\"},\n                },\n                \"required\": [\"name\", \"address\"],\n            },\n            \"Address\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"street\": {\"type\": \"string\"},\n                    \"city\": {\"type\": \"string\"},\n                    \"zipcode\": {\"type\": \"string\"},\n                },\n                \"required\": [\"street\", \"city\", \"zipcode\"],\n            },\n        }\n\n        result = extract_output_schema_from_responses(\n            responses, schema_definitions=schema_definitions, openapi_version=\"3.0.3\"\n        )\n\n        # All transitive dependencies must be preserved\n        assert result is not None\n        assert \"$defs\" in result\n        assert \"User\" in result[\"$defs\"], \"User should be preserved\"\n        assert \"Profile\" in result[\"$defs\"], \"Profile should be preserved\"\n        assert \"Address\" in result[\"$defs\"], \"Address must be preserved (main bug)\"\n\n        # All refs should be converted to #/$defs format\n        user_def = result[\"$defs\"][\"User\"]\n        assert user_def[\"properties\"][\"profile\"][\"$ref\"] == \"#/$defs/Profile\"\n\n        profile_def = result[\"$defs\"][\"Profile\"]\n        assert profile_def[\"properties\"][\"address\"][\"$ref\"] == \"#/$defs/Address\"\n\n    def test_elongl_reported_case_xref_with_nullable_function(self):\n        \"\"\"Test the specific case reported by elongl with nullable function reference.\"\"\"\n        responses = {\n            \"200\": ResponseInfo(\n                description=\"Success\",\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"array\",\n                        \"items\": {\"$ref\": \"#/components/schemas/Xref\"},\n                    }\n                },\n            )\n        }\n\n        schema_definitions = {\n            \"Xref\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"address\": {\"type\": \"string\", \"title\": \"Address\"},\n                    \"type\": {\"type\": \"string\", \"title\": \"Type\"},\n                    \"function\": {\n                        \"anyOf\": [\n                            {\"$ref\": \"#/components/schemas/Function\"},\n                            {\"type\": \"null\"},\n                        ]\n                    },\n                },\n                \"required\": [\"address\", \"type\", \"function\"],\n                \"title\": \"Xref\",\n            },\n            \"Function\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\"type\": \"string\"},\n                    \"address\": {\"type\": \"string\"},\n                },\n                \"title\": \"Function\",\n            },\n        }\n\n        result = extract_output_schema_from_responses(\n            responses, schema_definitions=schema_definitions\n        )\n\n        # Function must be included in $defs\n        assert result is not None\n        assert \"$defs\" in result\n        assert \"Xref\" in result[\"$defs\"], \"Xref should be preserved\"\n        assert \"Function\" in result[\"$defs\"], (\n            \"Function must be preserved (reported bug)\"\n        )\n\n        # Refs in anyOf should be converted\n        xref_def = result[\"$defs\"][\"Xref\"]\n        function_prop = xref_def[\"properties\"][\"function\"]\n        assert function_prop[\"anyOf\"][0][\"$ref\"] == \"#/$defs/Function\"\n\n    def test_tspicer_reported_case_profile_with_nested_refs(self):\n        \"\"\"Test the specific case reported by tspicer with Profile->countryCode->AccountInfo.\"\"\"\n        route = HTTPRoute(\n            path=\"/profile\",\n            method=\"POST\",\n            operation_id=\"create_profile\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/Profile\"}\n                },\n            ),\n            request_schemas={\n                \"Profile\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"profileId\": {\"type\": \"integer\"},\n                        \"countryCode\": {\"$ref\": \"#/components/schemas/countryCode\"},\n                        \"accountInfo\": {\"$ref\": \"#/components/schemas/AccountInfo\"},\n                    },\n                },\n                \"countryCode\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"US\", \"UK\", \"CA\", \"AU\"],\n                },\n                \"AccountInfo\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"accountId\": {\"type\": \"string\"},\n                        \"accountType\": {\"type\": \"string\"},\n                    },\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # All referenced schemas must be included in $defs\n        assert \"Profile\" in combined_schema[\"$defs\"], \"Profile should be preserved\"\n        assert \"countryCode\" in combined_schema[\"$defs\"], (\n            \"countryCode must be preserved\"\n        )\n        assert \"AccountInfo\" in combined_schema[\"$defs\"], (\n            \"AccountInfo must be preserved\"\n        )\n\n        # All refs should be converted\n        profile_def = combined_schema[\"$defs\"][\"Profile\"]\n        assert profile_def[\"properties\"][\"countryCode\"][\"$ref\"] == \"#/$defs/countryCode\"\n        assert profile_def[\"properties\"][\"accountInfo\"][\"$ref\"] == \"#/$defs/AccountInfo\"\n\n    def test_transitive_refs_in_request_body_schemas(self):\n        \"\"\"Transitive $refs in request body schemas must be preserved and converted.\"\"\"\n        route = HTTPRoute(\n            path=\"/users\",\n            method=\"POST\",\n            operation_id=\"create_user\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/User\"}\n                },\n            ),\n            request_schemas={\n                \"User\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"id\": {\"type\": \"string\"},\n                        \"profile\": {\"$ref\": \"#/components/schemas/Profile\"},\n                    },\n                    \"required\": [\"id\", \"profile\"],\n                },\n                \"Profile\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\"},\n                        \"address\": {\"$ref\": \"#/components/schemas/Address\"},\n                    },\n                    \"required\": [\"name\", \"address\"],\n                },\n                \"Address\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"street\": {\"type\": \"string\"},\n                        \"city\": {\"type\": \"string\"},\n                        \"zipcode\": {\"type\": \"string\"},\n                    },\n                    \"required\": [\"street\", \"city\", \"zipcode\"],\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # All transitive dependencies should be preserved\n        assert \"User\" in combined_schema[\"$defs\"]\n        assert \"Profile\" in combined_schema[\"$defs\"]\n        assert \"Address\" in combined_schema[\"$defs\"]\n\n        # All internal refs should be converted to #/$defs format\n        user_def = combined_schema[\"$defs\"][\"User\"]\n        assert user_def[\"properties\"][\"profile\"][\"$ref\"] == \"#/$defs/Profile\"\n\n        profile_def = combined_schema[\"$defs\"][\"Profile\"]\n        assert profile_def[\"properties\"][\"address\"][\"$ref\"] == \"#/$defs/Address\"\n\n    def test_refs_in_array_items_converted(self):\n        \"\"\"$refs inside array items must be converted from OpenAPI to JSON Schema format.\"\"\"\n        route = HTTPRoute(\n            path=\"/users\",\n            method=\"POST\",\n            operation_id=\"create_users\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"users\": {\n                                \"type\": \"array\",\n                                \"items\": {\"$ref\": \"#/components/schemas/User\"},\n                            }\n                        },\n                    }\n                },\n            ),\n            request_schemas={\n                \"User\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"profile\": {\"$ref\": \"#/components/schemas/Profile\"}},\n                },\n                \"Profile\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"name\": {\"type\": \"string\"}},\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # Array item refs should be converted\n        assert combined_schema[\"properties\"][\"users\"][\"items\"][\"$ref\"] == \"#/$defs/User\"\n\n        # Nested refs should be converted\n        user_def = combined_schema[\"$defs\"][\"User\"]\n        assert user_def[\"properties\"][\"profile\"][\"$ref\"] == \"#/$defs/Profile\"\n\n    def test_refs_in_composition_keywords_converted(self):\n        \"\"\"$refs inside oneOf/anyOf/allOf must be converted from OpenAPI to JSON Schema format.\"\"\"\n        route = HTTPRoute(\n            path=\"/data\",\n            method=\"POST\",\n            operation_id=\"create_data\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"data\": {\n                                \"oneOf\": [\n                                    {\"$ref\": \"#/components/schemas/TypeA\"},\n                                    {\"$ref\": \"#/components/schemas/TypeB\"},\n                                ]\n                            },\n                            \"alternate\": {\n                                \"anyOf\": [\n                                    {\"$ref\": \"#/components/schemas/TypeC\"},\n                                    {\"$ref\": \"#/components/schemas/TypeD\"},\n                                ]\n                            },\n                            \"combined\": {\n                                \"allOf\": [\n                                    {\"$ref\": \"#/components/schemas/BaseType\"},\n                                    {\"properties\": {\"extra\": {\"type\": \"string\"}}},\n                                ]\n                            },\n                        },\n                    }\n                },\n            ),\n            request_schemas={\n                \"TypeA\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"nested\": {\"$ref\": \"#/components/schemas/Nested\"}},\n                },\n                \"TypeB\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"value\": {\"type\": \"string\"}},\n                },\n                \"TypeC\": {\"type\": \"string\"},\n                \"TypeD\": {\"type\": \"number\"},\n                \"BaseType\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"base\": {\"type\": \"string\"}},\n                },\n                \"Nested\": {\"type\": \"string\"},\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # oneOf refs should be converted\n        oneof_refs = combined_schema[\"properties\"][\"data\"][\"oneOf\"]\n        assert oneof_refs[0][\"$ref\"] == \"#/$defs/TypeA\"\n        assert oneof_refs[1][\"$ref\"] == \"#/$defs/TypeB\"\n\n        # anyOf refs should be converted\n        anyof_refs = combined_schema[\"properties\"][\"alternate\"][\"anyOf\"]\n        assert anyof_refs[0][\"$ref\"] == \"#/$defs/TypeC\"\n        assert anyof_refs[1][\"$ref\"] == \"#/$defs/TypeD\"\n\n        # allOf refs should be converted\n        allof_refs = combined_schema[\"properties\"][\"combined\"][\"allOf\"]\n        assert allof_refs[0][\"$ref\"] == \"#/$defs/BaseType\"\n\n        # Transitive refs should be converted\n        type_a_def = combined_schema[\"$defs\"][\"TypeA\"]\n        assert type_a_def[\"properties\"][\"nested\"][\"$ref\"] == \"#/$defs/Nested\"\n\n    def test_deeply_nested_transitive_refs_preserved(self):\n        \"\"\"Deeply nested transitive refs (A→B→C→D→E) must all be preserved.\"\"\"\n        route = HTTPRoute(\n            path=\"/deep\",\n            method=\"POST\",\n            operation_id=\"create_deep\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/Level1\"}\n                },\n            ),\n            request_schemas={\n                \"Level1\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"level2\": {\"$ref\": \"#/components/schemas/Level2\"}},\n                },\n                \"Level2\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"level3\": {\"$ref\": \"#/components/schemas/Level3\"}},\n                },\n                \"Level3\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"level4\": {\"$ref\": \"#/components/schemas/Level4\"}},\n                },\n                \"Level4\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"level5\": {\"$ref\": \"#/components/schemas/Level5\"}},\n                },\n                \"Level5\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"value\": {\"type\": \"string\"}},\n                },\n                \"UnusedSchema\": {\"type\": \"number\"},\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # All levels should be preserved\n        assert \"Level1\" in combined_schema[\"$defs\"]\n        assert \"Level2\" in combined_schema[\"$defs\"]\n        assert \"Level3\" in combined_schema[\"$defs\"]\n        assert \"Level4\" in combined_schema[\"$defs\"]\n        assert \"Level5\" in combined_schema[\"$defs\"]\n\n        # Unused should be removed (pruning is allowed for unused schemas)\n        assert \"UnusedSchema\" not in combined_schema[\"$defs\"]\n\n        # All refs should be converted\n        assert (\n            combined_schema[\"$defs\"][\"Level1\"][\"properties\"][\"level2\"][\"$ref\"]\n            == \"#/$defs/Level2\"\n        )\n        assert (\n            combined_schema[\"$defs\"][\"Level2\"][\"properties\"][\"level3\"][\"$ref\"]\n            == \"#/$defs/Level3\"\n        )\n        assert (\n            combined_schema[\"$defs\"][\"Level3\"][\"properties\"][\"level4\"][\"$ref\"]\n            == \"#/$defs/Level4\"\n        )\n        assert (\n            combined_schema[\"$defs\"][\"Level4\"][\"properties\"][\"level5\"][\"$ref\"]\n            == \"#/$defs/Level5\"\n        )\n\n    def test_circular_references_handled(self):\n        \"\"\"Circular references (A→B→A) must be handled without infinite loops.\"\"\"\n        route = HTTPRoute(\n            path=\"/circular\",\n            method=\"POST\",\n            operation_id=\"circular_test\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/Node\"}\n                },\n            ),\n            request_schemas={\n                \"Node\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"value\": {\"type\": \"string\"},\n                        \"children\": {\n                            \"type\": \"array\",\n                            \"items\": {\"$ref\": \"#/components/schemas/Node\"},\n                        },\n                    },\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # Node should be preserved\n        assert \"Node\" in combined_schema[\"$defs\"]\n\n        # Self-reference should be converted\n        node_def = combined_schema[\"$defs\"][\"Node\"]\n        assert node_def[\"properties\"][\"children\"][\"items\"][\"$ref\"] == \"#/$defs/Node\"\n\n    def test_multiple_reference_paths_to_same_schema(self):\n        \"\"\"Multiple paths to the same schema (diamond pattern) must preserve the schema.\"\"\"\n        route = HTTPRoute(\n            path=\"/diamond\",\n            method=\"POST\",\n            operation_id=\"diamond_test\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"left\": {\"$ref\": \"#/components/schemas/Left\"},\n                            \"right\": {\"$ref\": \"#/components/schemas/Right\"},\n                        },\n                    }\n                },\n            ),\n            request_schemas={\n                \"Left\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"shared\": {\"$ref\": \"#/components/schemas/Shared\"}},\n                },\n                \"Right\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"shared\": {\"$ref\": \"#/components/schemas/Shared\"}},\n                },\n                \"Shared\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"value\": {\"type\": \"string\"}},\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # All schemas should be preserved\n        assert \"Left\" in combined_schema[\"$defs\"]\n        assert \"Right\" in combined_schema[\"$defs\"]\n        assert \"Shared\" in combined_schema[\"$defs\"]\n\n        # All refs should be converted\n        assert combined_schema[\"properties\"][\"left\"][\"$ref\"] == \"#/$defs/Left\"\n        assert combined_schema[\"properties\"][\"right\"][\"$ref\"] == \"#/$defs/Right\"\n        assert (\n            combined_schema[\"$defs\"][\"Left\"][\"properties\"][\"shared\"][\"$ref\"]\n            == \"#/$defs/Shared\"\n        )\n        assert (\n            combined_schema[\"$defs\"][\"Right\"][\"properties\"][\"shared\"][\"$ref\"]\n            == \"#/$defs/Shared\"\n        )\n\n    def test_refs_in_nested_content_schemas(self):\n        \"\"\"$refs in nested content schemas (the original bug location) must be converted.\"\"\"\n        route = HTTPRoute(\n            path=\"/content\",\n            method=\"POST\",\n            operation_id=\"content_test\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\"$ref\": \"#/components/schemas/Content\"}\n                },\n            ),\n            request_schemas={\n                \"Content\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"media\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"application/json\": {\n                                    \"$ref\": \"#/components/schemas/JsonContent\"\n                                }\n                            },\n                        }\n                    },\n                },\n                \"JsonContent\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"data\": {\"type\": \"string\"}},\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # Both schemas should be preserved\n        assert \"Content\" in combined_schema[\"$defs\"]\n        assert \"JsonContent\" in combined_schema[\"$defs\"]\n\n        # Nested ref should be converted\n        content_def = combined_schema[\"$defs\"][\"Content\"]\n        nested_ref = content_def[\"properties\"][\"media\"][\"properties\"][\n            \"application/json\"\n        ]\n        assert nested_ref[\"$ref\"] == \"#/$defs/JsonContent\"\n\n    def test_unnecessary_defs_preserved_when_referenced(self):\n        \"\"\"Even seemingly unnecessary $defs must be preserved if they're referenced.\"\"\"\n        route = HTTPRoute(\n            path=\"/test\",\n            method=\"POST\",\n            operation_id=\"test_unnecessary\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    \"application/json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            # Reference to a simple type schema\n                            \"simple\": {\"$ref\": \"#/components/schemas/SimpleString\"},\n                            # Reference to an empty object schema\n                            \"empty\": {\"$ref\": \"#/components/schemas/EmptyObject\"},\n                        },\n                    }\n                },\n            ),\n            request_schemas={\n                \"SimpleString\": {\"type\": \"string\"},\n                \"EmptyObject\": {\"type\": \"object\"},\n                \"UnreferencedSchema\": {\"type\": \"number\"},\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # Referenced schemas should be preserved even if simple\n        assert \"SimpleString\" in combined_schema[\"$defs\"]\n        assert \"EmptyObject\" in combined_schema[\"$defs\"]\n\n        # Unreferenced should be removed\n        assert \"UnreferencedSchema\" not in combined_schema[\"$defs\"]\n\n        # Refs should be converted\n        assert combined_schema[\"properties\"][\"simple\"][\"$ref\"] == \"#/$defs/SimpleString\"\n        assert combined_schema[\"properties\"][\"empty\"][\"$ref\"] == \"#/$defs/EmptyObject\"\n\n    def test_ref_only_request_body_handled(self):\n        \"\"\"Request bodies that are just a $ref (not an object with properties) must work.\"\"\"\n        route = HTTPRoute(\n            path=\"/direct-ref\",\n            method=\"POST\",\n            operation_id=\"direct_ref_test\",\n            request_body=RequestBodyInfo(\n                required=True,\n                content_schema={\n                    # Direct $ref, not wrapped in an object\n                    \"application/json\": {\"$ref\": \"#/components/schemas/DirectBody\"}\n                },\n            ),\n            request_schemas={\n                \"DirectBody\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"field1\": {\"type\": \"string\"},\n                        \"nested\": {\"$ref\": \"#/components/schemas/NestedBody\"},\n                    },\n                },\n                \"NestedBody\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"field2\": {\"type\": \"number\"}},\n                },\n            },\n        )\n\n        combined_schema, _ = _combine_schemas_and_map_params(route)\n\n        # Should handle the direct ref properly\n        assert \"body\" in combined_schema[\"properties\"]\n        assert combined_schema[\"properties\"][\"body\"][\"$ref\"] == \"#/$defs/DirectBody\"\n\n        # Both schemas should be preserved\n        assert \"DirectBody\" in combined_schema[\"$defs\"]\n        assert \"NestedBody\" in combined_schema[\"$defs\"]\n\n        # Nested ref should be converted\n        assert (\n            combined_schema[\"$defs\"][\"DirectBody\"][\"properties\"][\"nested\"][\"$ref\"]\n            == \"#/$defs/NestedBody\"\n        )\n\n    def test_separate_input_output_schemas(self):\n        \"\"\"Test that input and output schemas contain different schema\n        definitions and don't overlap in the ultimate schema definitions.\"\"\"\n        # OpenAPI spec with transitive dependencies to force schema inclusion\n        openapi_spec = {\n            \"openapi\": \"3.0.1\",\n            \"info\": {\"title\": \"Test API\", \"version\": \"1.0.0\"},\n            \"paths\": {\n                \"/test\": {\n                    \"post\": {\n                        \"summary\": \"Test endpoint\",\n                        \"requestBody\": {\n                            \"content\": {\n                                \"application/json\": {\n                                    \"schema\": {\n                                        \"$ref\": \"#/components/schemas/InputContainer\"\n                                    }\n                                }\n                            }\n                        },\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Success\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"$ref\": \"#/components/schemas/OutputContainer\"\n                                        }\n                                    }\n                                },\n                            }\n                        },\n                    }\n                }\n            },\n            \"components\": {\n                \"schemas\": {\n                    \"InputContainer\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"data\": {\"$ref\": \"#/components/schemas/InputData\"}\n                        },\n                    },\n                    \"InputData\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"input_field\": {\"type\": \"string\"}},\n                    },\n                    \"OutputContainer\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"result\": {\"$ref\": \"#/components/schemas/OutputData\"}\n                        },\n                    },\n                    \"OutputData\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"output_field\": {\"type\": \"string\"}},\n                    },\n                    \"UnusedSchema\": {\n                        \"type\": \"object\",\n                        \"properties\": {\"unused_field\": {\"type\": \"string\"}},\n                    },\n                }\n            },\n        }\n\n        routes = parse_openapi_to_http_routes(openapi_spec)\n        assert len(routes) == 1\n\n        route = routes[0]\n\n        # Check that schemas are properly separated\n        input_schema_names = set(route.request_schemas.keys())\n        output_schema_names = set(route.response_schemas.keys())\n\n        # Input should contain transitive dependencies from InputContainer\n        assert \"InputData\" in input_schema_names, (\n            f\"Expected InputData in request schemas: {input_schema_names}\"\n        )\n        assert \"OutputContainer\" not in input_schema_names, (\n            \"OutputContainer should not be in request schemas\"\n        )\n        assert \"OutputData\" not in input_schema_names, (\n            \"OutputData should not be in request schemas\"\n        )\n\n        # Output should contain transitive dependencies from OutputContainer\n        assert \"OutputData\" in output_schema_names, (\n            f\"Expected OutputData in response schemas: {output_schema_names}\"\n        )\n        assert \"InputContainer\" not in output_schema_names, (\n            \"InputContainer should not be in response schemas\"\n        )\n        assert \"InputData\" not in output_schema_names, (\n            \"InputData should not be in response schemas\"\n        )\n\n        # Neither should contain unused schema\n        assert \"UnusedSchema\" not in input_schema_names, (\n            \"UnusedSchema should not be in request schemas\"\n        )\n        assert \"UnusedSchema\" not in output_schema_names, (\n            \"UnusedSchema should not be in response schemas\"\n        )\n\n        # Verify no overlap\n        overlap = input_schema_names & output_schema_names\n        assert len(overlap) == 0, (\n            f\"Found overlapping schemas between input and output: {overlap}\"\n        )\n\n    def test_issue_2087_top_level_response_ref_includes_all_nested_schemas(self):\n        \"\"\"Issue #2087: Top-level response $ref must include itself in response_schemas.\"\"\"\n        spec = {\n            \"openapi\": \"3.0.1\",\n            \"info\": {\"title\": \"Test\", \"version\": \"1.0\"},\n            \"paths\": {\n                \"/persons\": {\n                    \"get\": {\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"OK\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"$ref\": \"#/components/schemas/PersonList\"\n                                        }\n                                    }\n                                },\n                            }\n                        }\n                    }\n                }\n            },\n            \"components\": {\n                \"schemas\": {\n                    \"PersonList\": {\n                        \"properties\": {\n                            \"Items\": {\n                                \"type\": \"array\",\n                                \"items\": {\"$ref\": \"#/components/schemas/Person\"},\n                            }\n                        }\n                    },\n                    \"Person\": {\n                        \"properties\": {\n                            \"Name\": {\"$ref\": \"#/components/schemas/Name\"},\n                            \"Job\": {\"$ref\": \"#/components/schemas/Job\"},\n                        }\n                    },\n                    \"Name\": {\"properties\": {\"First\": {\"type\": \"string\"}}},\n                    \"Job\": {\"properties\": {\"Company\": {\"type\": \"string\"}}},\n                }\n            },\n        }\n\n        routes = parse_openapi_to_http_routes(spec)\n        route = routes[0]\n\n        # Bug was: PersonList missing from response_schemas despite being top-level ref\n        assert \"PersonList\" in route.response_schemas\n        assert \"Person\" in route.response_schemas\n        assert \"Name\" in route.response_schemas\n        assert \"Job\" in route.response_schemas\n"
  },
  {
    "path": "tests/utilities/test_async_utils.py",
    "content": "\"\"\"Tests for fastmcp.utilities.async_utils.\"\"\"\n\nimport functools\n\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.prompts import prompt\nfrom fastmcp.resources import resource\nfrom fastmcp.tools import tool\nfrom fastmcp.utilities.async_utils import is_coroutine_function\n\n\nasync def _async_fn(x: int) -> int:\n    return x\n\n\ndef _sync_fn(x: int) -> int:\n    return x\n\n\nclass TestIsCoroutineFunction:\n    def test_plain_async(self) -> None:\n        assert is_coroutine_function(_async_fn) is True\n\n    def test_plain_sync(self) -> None:\n        assert is_coroutine_function(_sync_fn) is False\n\n    def test_partial_async(self) -> None:\n        p = functools.partial(_async_fn, x=1)\n        assert is_coroutine_function(p) is True\n\n    def test_partial_sync(self) -> None:\n        p = functools.partial(_sync_fn, x=1)\n        assert is_coroutine_function(p) is False\n\n    def test_nested_partial_async(self) -> None:\n        p = functools.partial(functools.partial(_async_fn, x=1))\n        assert is_coroutine_function(p) is True\n\n    def test_nested_partial_sync(self) -> None:\n        p = functools.partial(functools.partial(_sync_fn, x=1))\n        assert is_coroutine_function(p) is False\n\n    def test_lambda(self) -> None:\n        assert is_coroutine_function(lambda: None) is False\n\n    def test_non_callable(self) -> None:\n        assert is_coroutine_function(42) is False\n\n\nclass TestAsyncPartialIntegration:\n    async def test_async_partial_tool_runs(self) -> None:\n        async def greet(greeting: str, name: str) -> str:\n            return f\"{greeting}, {name}!\"\n\n        greet_tool = tool(name=\"greet\")(functools.partial(greet, \"Hello\"))\n\n        mcp = FastMCP()\n        mcp.add_tool(greet_tool)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"greet\", {\"name\": \"world\"})\n            assert result.content[0].text == \"Hello, world!\"\n\n    async def test_async_partial_resource_reads(self) -> None:\n        async def make_greeting(greeting: str) -> str:\n            return f\"{greeting}, resource!\"\n\n        greet_resource = resource(\"test://greet\")(\n            functools.partial(make_greeting, \"Hi\")\n        )\n\n        mcp = FastMCP()\n        mcp.add_resource(greet_resource)\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"test://greet\")\n            assert result[0].text == \"Hi, resource!\"\n\n    async def test_async_partial_prompt_renders(self) -> None:\n        async def make_prompt(prefix: str) -> str:\n            return f\"{prefix}: prompt content\"\n\n        note_prompt = prompt(name=\"note\")(functools.partial(make_prompt, \"Note\"))\n\n        mcp = FastMCP()\n        mcp.add_prompt(note_prompt)\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"note\")\n            assert \"Note: prompt content\" in result.messages[0].content.text\n\n    async def test_async_partial_with_task_true_does_not_raise(self) -> None:\n        async def slow_task(prefix: str, x: int) -> str:\n            return f\"{prefix}-{x}\"\n\n        slow_tool = tool(name=\"slow\", task=True)(functools.partial(slow_task, \"ok\"))\n\n        mcp = FastMCP()\n        mcp.add_tool(slow_tool)\n\n    async def test_sync_partial_with_task_true_raises(self) -> None:\n        def sync_task(prefix: str, x: int) -> str:\n            return f\"{prefix}-{x}\"\n\n        mcp = FastMCP()\n        with pytest.raises(ValueError, match=\"sync function\"):\n            decorated = tool(name=\"slow\", task=True)(functools.partial(sync_task, \"ok\"))\n            mcp.add_tool(decorated)\n"
  },
  {
    "path": "tests/utilities/test_auth.py",
    "content": "\"\"\"Tests for authentication utility helpers.\"\"\"\n\nimport base64\nimport json\n\nimport pytest\n\nfrom fastmcp.utilities.auth import (\n    decode_jwt_header,\n    decode_jwt_payload,\n    parse_scopes,\n)\n\n\ndef create_jwt(header: dict, payload: dict, signature: bytes = b\"fake-sig\") -> str:\n    \"\"\"Create a test JWT token.\"\"\"\n    header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b\"=\")\n    payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b\"=\")\n    sig_b64 = base64.urlsafe_b64encode(signature).rstrip(b\"=\")\n    return f\"{header_b64.decode()}.{payload_b64.decode()}.{sig_b64.decode()}\"\n\n\nclass TestDecodeJwtHeader:\n    \"\"\"Tests for decode_jwt_header utility.\"\"\"\n\n    def test_decode_basic_header(self):\n        \"\"\"Test decoding a basic JWT header.\"\"\"\n        header = {\"alg\": \"RS256\", \"typ\": \"JWT\"}\n        payload = {\"sub\": \"user-123\"}\n        token = create_jwt(header, payload)\n\n        result = decode_jwt_header(token)\n\n        assert result == header\n        assert result[\"alg\"] == \"RS256\"\n        assert result[\"typ\"] == \"JWT\"\n\n    def test_decode_header_with_kid(self):\n        \"\"\"Test decoding header with key ID for JWKS lookup.\"\"\"\n        header = {\"alg\": \"RS256\", \"typ\": \"JWT\", \"kid\": \"key-id-123\"}\n        payload = {\"sub\": \"user-123\"}\n        token = create_jwt(header, payload)\n\n        result = decode_jwt_header(token)\n\n        assert result[\"kid\"] == \"key-id-123\"\n\n    def test_invalid_jwt_format_two_parts(self):\n        \"\"\"Test that two-part token raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Invalid JWT format\"):\n            decode_jwt_header(\"header.payload\")\n\n    def test_invalid_jwt_format_one_part(self):\n        \"\"\"Test that single-part token raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Invalid JWT format\"):\n            decode_jwt_header(\"not-a-jwt\")\n\n    def test_invalid_jwt_format_four_parts(self):\n        \"\"\"Test that four-part token raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Invalid JWT format\"):\n            decode_jwt_header(\"a.b.c.d\")\n\n    def test_decode_header_length_divisible_by_4(self):\n        \"\"\"Test decoding when base64 length is divisible by 4 (no padding needed).\n\n        This tests the edge case where len(part) % 4 == 0.\n        The padding calculation (-len % 4) correctly yields 0 in this case.\n        \"\"\"\n        # Create a header that encodes to exactly 12 chars (divisible by 4)\n        header = {\"x\": \"\"}  # eyJ4IjogIiJ9 = 12 chars\n        payload = {\"sub\": \"user\"}\n        token = create_jwt(header, payload)\n\n        result = decode_jwt_header(token)\n        assert result == header\n\n\nclass TestDecodeJwtPayload:\n    \"\"\"Tests for decode_jwt_payload utility.\"\"\"\n\n    def test_decode_basic_payload(self):\n        \"\"\"Test decoding a basic JWT payload.\"\"\"\n        header = {\"alg\": \"RS256\", \"typ\": \"JWT\"}\n        payload = {\"sub\": \"user-123\", \"name\": \"Test User\"}\n        token = create_jwt(header, payload)\n\n        result = decode_jwt_payload(token)\n\n        assert result == payload\n        assert result[\"sub\"] == \"user-123\"\n        assert result[\"name\"] == \"Test User\"\n\n    def test_decode_payload_with_claims(self):\n        \"\"\"Test decoding payload with various claims.\"\"\"\n        header = {\"alg\": \"RS256\"}\n        payload = {\n            \"sub\": \"user-123\",\n            \"oid\": \"object-456\",\n            \"name\": \"Test User\",\n            \"email\": \"test@example.com\",\n            \"roles\": [\"admin\", \"user\"],\n            \"exp\": 1234567890,\n        }\n        token = create_jwt(header, payload)\n\n        result = decode_jwt_payload(token)\n\n        assert result[\"sub\"] == \"user-123\"\n        assert result[\"oid\"] == \"object-456\"\n        assert result[\"name\"] == \"Test User\"\n        assert result[\"email\"] == \"test@example.com\"\n        assert result[\"roles\"] == [\"admin\", \"user\"]\n        assert result[\"exp\"] == 1234567890\n\n    def test_invalid_jwt_format(self):\n        \"\"\"Test that invalid format raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Invalid JWT format\"):\n            decode_jwt_payload(\"not-a-jwt\")\n\n    def test_decode_payload_with_padding_edge_cases(self):\n        \"\"\"Test that base64 padding is handled correctly.\"\"\"\n        # Create payloads of different sizes to test padding\n        for payload_size in [1, 2, 3, 4, 5, 10, 100]:\n            payload = {\"data\": \"x\" * payload_size}\n            token = create_jwt({\"alg\": \"RS256\"}, payload)\n            result = decode_jwt_payload(token)\n            assert result == payload\n\n    def test_decode_payload_length_divisible_by_4(self):\n        \"\"\"Test decoding when base64 length is divisible by 4 (no padding needed).\n\n        This tests the edge case where len(part) % 4 == 0.\n        The padding calculation (-len % 4) correctly yields 0 in this case.\n        \"\"\"\n        # Create a payload that encodes to exactly 12 chars (divisible by 4)\n        header = {\"alg\": \"RS256\"}\n        payload = {\"x\": \"\"}  # eyJ4IjogIiJ9 = 12 chars\n        token = create_jwt(header, payload)\n\n        result = decode_jwt_payload(token)\n        assert result == payload\n\n\nclass TestParseScopes:\n    \"\"\"Tests for parse_scopes utility.\"\"\"\n\n    def test_parse_none(self):\n        \"\"\"Test that None returns None.\"\"\"\n        assert parse_scopes(None) is None\n\n    def test_parse_empty_string(self):\n        \"\"\"Test that empty string returns empty list.\"\"\"\n        assert parse_scopes(\"\") == []\n\n    def test_parse_space_separated(self):\n        \"\"\"Test parsing space-separated scopes.\"\"\"\n        assert parse_scopes(\"read write delete\") == [\"read\", \"write\", \"delete\"]\n\n    def test_parse_comma_separated(self):\n        \"\"\"Test parsing comma-separated scopes.\"\"\"\n        assert parse_scopes(\"read,write,delete\") == [\"read\", \"write\", \"delete\"]\n\n    def test_parse_json_array(self):\n        \"\"\"Test parsing JSON array string.\"\"\"\n        assert parse_scopes('[\"read\", \"write\", \"delete\"]') == [\n            \"read\",\n            \"write\",\n            \"delete\",\n        ]\n\n    def test_parse_list(self):\n        \"\"\"Test that list is returned as-is.\"\"\"\n        assert parse_scopes([\"read\", \"write\"]) == [\"read\", \"write\"]\n\n    def test_parse_strips_whitespace(self):\n        \"\"\"Test that whitespace is stripped.\"\"\"\n        assert parse_scopes(\"  read  ,  write  \") == [\"read\", \"write\"]\n"
  },
  {
    "path": "tests/utilities/test_cli.py",
    "content": "\"\"\"Tests for CLI utility functions.\"\"\"\n\nfrom pathlib import Path\n\nfrom fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment\n\n\nclass TestEnvironmentBuildUVRunCommand:\n    \"\"\"Test the Environment.build_uv_run_command() method.\"\"\"\n\n    def test_build_uv_run_command_basic(self):\n        \"\"\"Test building basic uv command with no environment config.\"\"\"\n        env = UVEnvironment()\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        # With no config, the command should be returned unchanged\n        expected = [\"fastmcp\", \"run\", \"server.py\"]\n        assert cmd == expected\n\n    def test_build_uv_run_command_with_editable(self):\n        \"\"\"Test building uv command with editable package.\"\"\"\n        editable_path = Path(\"/path/to/package\")\n        env = UVEnvironment(editable=[editable_path])\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--with-editable\",\n            str(editable_path.resolve()),\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n    def test_build_uv_run_command_with_packages(self):\n        \"\"\"Test building uv command with additional packages.\"\"\"\n        env = UVEnvironment(dependencies=[\"pkg1\", \"pkg2\"])\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--with\",\n            \"pkg1\",\n            \"--with\",\n            \"pkg2\",\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n    def test_build_uv_run_command_with_python_version(self):\n        \"\"\"Test building uv command with Python version.\"\"\"\n        env = UVEnvironment(python=\"3.10\")\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--python\",\n            \"3.10\",\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n    def test_build_uv_run_command_with_requirements(self):\n        \"\"\"Test building uv command with requirements file.\"\"\"\n        requirements_path = Path(\"/path/to/requirements.txt\")\n        env = UVEnvironment(requirements=requirements_path)\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--with-requirements\",\n            str(requirements_path.resolve()),\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n    def test_build_uv_run_command_with_project(self):\n        \"\"\"Test building uv command with project directory.\"\"\"\n        project_path = Path(\"/path/to/project\")\n        env = UVEnvironment(project=project_path)\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--project\",\n            str(project_path.resolve()),\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n    def test_build_uv_run_command_with_everything(self):\n        \"\"\"Test building uv command with all options.\"\"\"\n        requirements_path = Path(\"/path/to/requirements.txt\")\n        editable_path = Path(\"/local/pkg\")\n        env = UVEnvironment(\n            python=\"3.10\",\n            dependencies=[\"pandas\", \"numpy\"],\n            requirements=requirements_path,\n            editable=[editable_path],\n        )\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--python\",\n            \"3.10\",\n            \"--with\",\n            \"numpy\",\n            \"--with\",\n            \"pandas\",\n            \"--with-requirements\",\n            str(requirements_path.resolve()),\n            \"--with-editable\",\n            str(editable_path.resolve()),\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n    # Note: These tests are removed because build_uv_run_command now requires a command\n    # and only accepts a list, not optional or string commands\n\n    def test_build_uv_run_command_project_with_extras(self):\n        \"\"\"Test that project flag works with additional dependencies.\"\"\"\n        project_path = Path(\"/path/to/project\")\n        editable_path = Path(\"/pkg\")\n        env = UVEnvironment(\n            project=project_path,\n            python=\"3.10\",  # Should be ignored with project\n            dependencies=[\"pandas\"],  # Should be added on top of project\n            editable=[editable_path],  # Should be added on top of project\n        )\n        cmd = env.build_command([\"fastmcp\", \"run\", \"server.py\"])\n        expected = [\n            \"uv\",\n            \"run\",\n            \"--project\",\n            str(project_path.resolve()),\n            \"--with\",\n            \"pandas\",\n            \"--with-editable\",\n            str(editable_path.resolve()),\n            \"fastmcp\",\n            \"run\",\n            \"server.py\",\n        ]\n        assert cmd == expected\n\n\nclass TestEnvironmentNeedsUV:\n    \"\"\"Test the Environment.needs_uv() method.\"\"\"\n\n    def test_needs_uv_with_python(self):\n        \"\"\"Test that needs_uv returns True with Python version.\"\"\"\n        env = UVEnvironment(python=\"3.10\")\n        assert env._must_run_with_uv() is True\n\n    def test_needs_uv_with_dependencies(self):\n        \"\"\"Test that needs_uv returns True with dependencies.\"\"\"\n        env = UVEnvironment(dependencies=[\"pandas\"])\n        assert env._must_run_with_uv() is True\n\n    def test_needs_uv_with_requirements(self):\n        \"\"\"Test that needs_uv returns True with requirements.\"\"\"\n        env = UVEnvironment(requirements=Path(\"/path/to/requirements.txt\"))\n        assert env._must_run_with_uv() is True\n\n    def test_needs_uv_with_project(self):\n        \"\"\"Test that needs_uv returns True with project.\"\"\"\n        env = UVEnvironment(project=Path(\"/path/to/project\"))\n        assert env._must_run_with_uv() is True\n\n    def test_needs_uv_with_editable(self):\n        \"\"\"Test that needs_uv returns True with editable.\"\"\"\n        env = UVEnvironment(editable=[Path(\"/pkg\")])\n        assert env._must_run_with_uv() is True\n\n    def test_needs_uv_empty(self):\n        \"\"\"Test that needs_uv returns False with empty config.\"\"\"\n        env = UVEnvironment()\n        assert env._must_run_with_uv() is False\n\n    def test_needs_uv_with_empty_lists(self):\n        \"\"\"Test that needs_uv returns False with empty lists.\"\"\"\n        env = UVEnvironment(dependencies=None, editable=None)\n        assert env._must_run_with_uv() is False\n"
  },
  {
    "path": "tests/utilities/test_components.py",
    "content": "\"\"\"Tests for fastmcp.utilities.components module.\"\"\"\n\nimport warnings\n\nimport pytest\nfrom pydantic import ValidationError\n\nfrom fastmcp.prompts.base import Prompt\nfrom fastmcp.resources.base import Resource\nfrom fastmcp.resources.template import ResourceTemplate\nfrom fastmcp.tools.base import Tool\nfrom fastmcp.utilities.components import (\n    FastMCPComponent,\n    FastMCPMeta,\n    _convert_set_default_none,\n    get_fastmcp_metadata,\n)\n\n\nclass TestConvertSetDefaultNone:\n    \"\"\"Tests for the _convert_set_default_none helper function.\"\"\"\n\n    def test_none_returns_empty_set(self):\n        \"\"\"Test that None returns an empty set.\"\"\"\n        result = _convert_set_default_none(None)\n        assert result == set()\n\n    def test_set_returns_same_set(self):\n        \"\"\"Test that a set returns the same set.\"\"\"\n        test_set = {\"tag1\", \"tag2\"}\n        result = _convert_set_default_none(test_set)\n        assert result == test_set\n\n    def test_list_converts_to_set(self):\n        \"\"\"Test that a list converts to a set.\"\"\"\n        test_list = [\"tag1\", \"tag2\", \"tag1\"]  # Duplicate to test deduplication\n        result = _convert_set_default_none(test_list)\n        assert result == {\"tag1\", \"tag2\"}\n\n    def test_tuple_converts_to_set(self):\n        \"\"\"Test that a tuple converts to a set.\"\"\"\n        test_tuple = (\"tag1\", \"tag2\")\n        result = _convert_set_default_none(test_tuple)\n        assert result == {\"tag1\", \"tag2\"}\n\n\nclass TestFastMCPComponent:\n    \"\"\"Tests for the FastMCPComponent class.\"\"\"\n\n    @pytest.fixture\n    def basic_component(self):\n        \"\"\"Create a basic component for testing.\"\"\"\n        return FastMCPComponent(\n            name=\"test_component\",\n            title=\"Test Component\",\n            description=\"A test component\",\n            tags={\"test\", \"component\"},\n        )\n\n    def test_initialization_with_minimal_params(self):\n        \"\"\"Test component initialization with minimal parameters.\"\"\"\n        component = FastMCPComponent(name=\"minimal\")\n        assert component.name == \"minimal\"\n        assert component.title is None\n        assert component.description is None\n        assert component.tags == set()\n        assert component.meta is None\n\n    def test_initialization_with_all_params(self):\n        \"\"\"Test component initialization with all parameters.\"\"\"\n        meta = {\"custom\": \"value\"}\n        component = FastMCPComponent(\n            name=\"full\",\n            title=\"Full Component\",\n            description=\"A fully configured component\",\n            tags={\"tag1\", \"tag2\"},\n            meta=meta,\n        )\n        assert component.name == \"full\"\n        assert component.title == \"Full Component\"\n        assert component.description == \"A fully configured component\"\n        assert component.tags == {\"tag1\", \"tag2\"}\n        assert component.meta == meta\n\n    def test_key_property_without_custom_key(self, basic_component):\n        \"\"\"Test that key property returns name@version when no custom key is set.\"\"\"\n        # Base component has no KEY_PREFIX, so key is just \"name@version\" (or \"name@\" for unversioned)\n        assert basic_component.key == \"test_component@\"\n\n    def test_get_meta_with_fastmcp_meta(self, basic_component):\n        \"\"\"Test get_meta always includes fastmcp meta.\"\"\"\n        basic_component.meta = {\"custom\": \"data\"}\n        basic_component.tags = {\"tag2\", \"tag1\"}  # Unordered to test sorting\n        result = basic_component.get_meta()\n        assert result[\"custom\"] == \"data\"\n        assert \"fastmcp\" in result\n        assert result[\"fastmcp\"][\"tags\"] == [\"tag1\", \"tag2\"]  # Should be sorted\n\n    def test_get_meta_preserves_existing_fastmcp_meta(self):\n        \"\"\"Test that get_meta preserves existing fastmcp meta.\"\"\"\n        component = FastMCPComponent(\n            name=\"test\",\n            meta={\"fastmcp\": {\"existing\": \"value\"}},\n            tags={\"new_tag\"},\n        )\n        result = component.get_meta()\n        assert result is not None\n        assert result[\"fastmcp\"][\"existing\"] == \"value\"\n        assert result[\"fastmcp\"][\"tags\"] == [\"new_tag\"]\n\n    def test_get_meta_returns_dict_with_fastmcp_when_empty(self):\n        \"\"\"Test that get_meta returns dict with fastmcp meta even when no custom meta.\"\"\"\n        component = FastMCPComponent(name=\"test\")\n        result = component.get_meta()\n        assert result is not None\n        assert \"fastmcp\" in result\n        assert result[\"fastmcp\"][\"tags\"] == []\n\n    def test_get_meta_includes_version(self):\n        \"\"\"Test that get_meta includes version when component has a version.\"\"\"\n        component = FastMCPComponent(name=\"test\", version=\"v1.0.0\", tags={\"tag1\"})\n        result = component.get_meta()\n        assert result is not None\n        assert result[\"fastmcp\"][\"version\"] == \"v1.0.0\"\n        assert result[\"fastmcp\"][\"tags\"] == [\"tag1\"]\n\n    def test_get_meta_excludes_version_when_none(self):\n        \"\"\"Test that get_meta excludes version when component has no version.\"\"\"\n        component = FastMCPComponent(name=\"test\", tags={\"tag1\"})\n        result = component.get_meta()\n        assert result is not None\n        assert \"version\" not in result[\"fastmcp\"]\n        assert result[\"fastmcp\"][\"tags\"] == [\"tag1\"]\n\n    def test_equality_same_components(self):\n        \"\"\"Test that identical components are equal.\"\"\"\n        comp1 = FastMCPComponent(name=\"test\", description=\"desc\")\n        comp2 = FastMCPComponent(name=\"test\", description=\"desc\")\n        assert comp1 == comp2\n\n    def test_equality_different_components(self):\n        \"\"\"Test that different components are not equal.\"\"\"\n        comp1 = FastMCPComponent(name=\"test1\")\n        comp2 = FastMCPComponent(name=\"test2\")\n        assert comp1 != comp2\n\n    def test_equality_different_types(self, basic_component):\n        \"\"\"Test that component is not equal to other types.\"\"\"\n        assert basic_component != \"not a component\"\n        assert basic_component != 123\n        assert basic_component is not None\n\n    def test_repr(self, basic_component):\n        \"\"\"Test string representation of component.\"\"\"\n        repr_str = repr(basic_component)\n        assert \"FastMCPComponent\" in repr_str\n        assert \"name='test_component'\" in repr_str\n        assert \"title='Test Component'\" in repr_str\n        assert \"description='A test component'\" in repr_str\n\n    def test_copy_method(self, basic_component):\n        \"\"\"Test copy method creates an independent copy.\"\"\"\n        copy = basic_component.copy()\n        assert copy == basic_component\n        assert copy is not basic_component\n\n        # Modify copy and ensure original is unchanged\n        copy.name = \"modified\"\n        assert basic_component.name == \"test_component\"\n\n    def test_tags_deduplication(self):\n        \"\"\"Test that tags are deduplicated when passed as a sequence.\"\"\"\n        component = FastMCPComponent(\n            name=\"test\",\n            tags=[\"tag1\", \"tag2\", \"tag1\", \"tag2\"],  # type: ignore[arg-type]\n        )\n        assert component.tags == {\"tag1\", \"tag2\"}\n\n    def test_validation_error_for_invalid_data(self):\n        \"\"\"Test that validation errors are raised for invalid data.\"\"\"\n        with pytest.raises(ValidationError):\n            FastMCPComponent()  # type: ignore[call-arg]\n\n    def test_extra_fields_forbidden(self):\n        \"\"\"Test that extra fields are not allowed.\"\"\"\n        with pytest.raises(ValidationError) as exc_info:\n            FastMCPComponent(name=\"test\", unknown_field=\"value\")  # type: ignore[call-arg]  # Intentionally passing invalid field for test\n        assert \"Extra inputs are not permitted\" in str(exc_info.value)\n\n\nclass TestKeyPrefix:\n    \"\"\"Tests for KEY_PREFIX and make_key functionality.\"\"\"\n\n    def test_base_class_has_empty_prefix(self):\n        \"\"\"Test that FastMCPComponent has empty KEY_PREFIX.\"\"\"\n        assert FastMCPComponent.KEY_PREFIX == \"\"\n\n    def test_make_key_without_prefix(self):\n        \"\"\"Test make_key returns just identifier when KEY_PREFIX is empty.\"\"\"\n        assert FastMCPComponent.make_key(\"my_name\") == \"my_name\"\n\n    def test_tool_has_tool_prefix(self):\n        \"\"\"Test that Tool has 'tool' KEY_PREFIX.\"\"\"\n        assert Tool.KEY_PREFIX == \"tool\"\n        assert Tool.make_key(\"my_tool\") == \"tool:my_tool\"\n\n    def test_resource_has_resource_prefix(self):\n        \"\"\"Test that Resource has 'resource' KEY_PREFIX.\"\"\"\n        assert Resource.KEY_PREFIX == \"resource\"\n        assert Resource.make_key(\"file://test.txt\") == \"resource:file://test.txt\"\n\n    def test_template_has_template_prefix(self):\n        \"\"\"Test that ResourceTemplate has 'template' KEY_PREFIX.\"\"\"\n        assert ResourceTemplate.KEY_PREFIX == \"template\"\n        assert ResourceTemplate.make_key(\"data://{id}\") == \"template:data://{id}\"\n\n    def test_prompt_has_prompt_prefix(self):\n        \"\"\"Test that Prompt has 'prompt' KEY_PREFIX.\"\"\"\n        assert Prompt.KEY_PREFIX == \"prompt\"\n        assert Prompt.make_key(\"my_prompt\") == \"prompt:my_prompt\"\n\n    def test_tool_key_property(self):\n        \"\"\"Test that Tool.key returns prefixed key with version sentinel.\"\"\"\n        tool = Tool(name=\"greet\", description=\"A greeting tool\", parameters={})\n        assert tool.key == \"tool:greet@\"\n\n    def test_prompt_key_property(self):\n        \"\"\"Test that Prompt.key returns prefixed key with version sentinel.\"\"\"\n        prompt = Prompt(name=\"analyze\", description=\"An analysis prompt\")\n        assert prompt.key == \"prompt:analyze@\"\n\n    def test_warning_for_missing_key_prefix(self):\n        \"\"\"Test that subclassing without KEY_PREFIX emits a warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n\n            class NoPrefix(FastMCPComponent):\n                pass\n\n            key_prefix_warnings = [\n                x for x in w if \"does not define KEY_PREFIX\" in str(x.message)\n            ]\n            assert len(key_prefix_warnings) == 1\n\n    def test_no_warning_when_key_prefix_defined(self):\n        \"\"\"Test that subclassing with KEY_PREFIX does not emit a warning.\"\"\"\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n\n            class WithPrefix(FastMCPComponent):\n                KEY_PREFIX = \"custom\"\n\n            key_prefix_warnings = [\n                x for x in w if \"does not define KEY_PREFIX\" in str(x.message)\n            ]\n            assert len(key_prefix_warnings) == 0\n            assert WithPrefix.make_key(\"test\") == \"custom:test\"\n\n\nclass TestGetFastMCPMetadata:\n    \"\"\"Tests for get_fastmcp_metadata helper.\"\"\"\n\n    def test_returns_fastmcp_namespace_when_dict(self):\n        meta = {\"fastmcp\": {\"tags\": [\"a\"]}, \"_fastmcp\": {\"tags\": [\"b\"]}}\n\n        assert get_fastmcp_metadata(meta) == {\"tags\": [\"a\"]}\n\n    def test_falls_back_to_legacy_namespace_when_dict(self):\n        meta = {\"fastmcp\": \"invalid\", \"_fastmcp\": {\"tags\": [\"legacy\"]}}\n\n        assert get_fastmcp_metadata(meta) == {\"tags\": [\"legacy\"]}\n\n    def test_ignores_non_dict_metadata(self):\n        assert get_fastmcp_metadata({\"fastmcp\": \"invalid\"}) == {}\n        assert get_fastmcp_metadata({\"fastmcp\": [\"invalid\"]}) == {}\n        assert get_fastmcp_metadata({\"_fastmcp\": \"invalid\"}) == {}\n\n\nclass TestComponentEnableDisable:\n    \"\"\"Tests for the enable/disable methods raising NotImplementedError.\"\"\"\n\n    def test_enable_raises_not_implemented_error(self):\n        \"\"\"Test that enable() raises NotImplementedError with migration guidance.\"\"\"\n        component = FastMCPComponent(name=\"test\")\n        with pytest.raises(NotImplementedError) as exc_info:\n            component.enable()\n        assert \"server.enable\" in str(exc_info.value)\n        assert \"test\" in str(exc_info.value)\n\n    def test_disable_raises_not_implemented_error(self):\n        \"\"\"Test that disable() raises NotImplementedError with migration guidance.\"\"\"\n        component = FastMCPComponent(name=\"test\")\n        with pytest.raises(NotImplementedError) as exc_info:\n            component.disable()\n        assert \"server.disable\" in str(exc_info.value)\n        assert \"test\" in str(exc_info.value)\n\n    def test_tool_enable_raises_not_implemented(self):\n        \"\"\"Test that Tool.enable() raises NotImplementedError.\"\"\"\n        tool = Tool(name=\"my_tool\", description=\"A tool\", parameters={})\n        with pytest.raises(NotImplementedError) as exc_info:\n            tool.enable()\n        assert \"tool:my_tool@\" in str(exc_info.value)\n\n    def test_tool_disable_raises_not_implemented(self):\n        \"\"\"Test that Tool.disable() raises NotImplementedError.\"\"\"\n        tool = Tool(name=\"my_tool\", description=\"A tool\", parameters={})\n        with pytest.raises(NotImplementedError) as exc_info:\n            tool.disable()\n        assert \"tool:my_tool@\" in str(exc_info.value)\n\n    def test_prompt_enable_raises_not_implemented(self):\n        \"\"\"Test that Prompt.enable() raises NotImplementedError.\"\"\"\n        prompt = Prompt(name=\"my_prompt\", description=\"A prompt\")\n        with pytest.raises(NotImplementedError) as exc_info:\n            prompt.enable()\n        assert \"prompt:my_prompt@\" in str(exc_info.value)\n\n\nclass TestFastMCPMeta:\n    \"\"\"Tests for the FastMCPMeta TypedDict.\"\"\"\n\n    def test_fastmcp_meta_structure(self):\n        \"\"\"Test that FastMCPMeta has the expected structure.\"\"\"\n        meta: FastMCPMeta = {\"tags\": [\"tag1\", \"tag2\"]}\n        assert meta[\"tags\"] == [\"tag1\", \"tag2\"]\n\n    def test_fastmcp_meta_with_version(self):\n        \"\"\"Test that FastMCPMeta can include version.\"\"\"\n        meta: FastMCPMeta = {\"tags\": [\"tag1\"], \"version\": \"v1.0.0\"}\n        assert meta[\"tags\"] == [\"tag1\"]\n        assert meta[\"version\"] == \"v1.0.0\"\n\n    def test_fastmcp_meta_optional_fields(self):\n        \"\"\"Test that FastMCPMeta fields are optional.\"\"\"\n        meta: FastMCPMeta = {}\n        assert \"tags\" not in meta  # Should be optional\n        assert \"version\" not in meta  # Should be optional\n\n\nclass TestEdgeCasesAndIntegration:\n    \"\"\"Tests for edge cases and integration scenarios.\"\"\"\n\n    def test_empty_tags_conversion(self):\n        \"\"\"Test that empty tags are handled correctly.\"\"\"\n        component = FastMCPComponent(name=\"test\", tags=set())\n        assert component.tags == set()\n\n    def test_tags_with_none_values(self):\n        \"\"\"Test tags behavior with various input types.\"\"\"\n        # Test with None (through validator)\n        component = FastMCPComponent(name=\"test\")\n        assert component.tags == set()\n\n    def test_get_meta_returns_copy(self):\n        \"\"\"Test that get_meta returns a copy, not a reference to the original.\"\"\"\n        component = FastMCPComponent(name=\"test\", meta={\"key\": \"value\"})\n        meta = component.get_meta()\n        assert meta is not None\n        meta[\"key\"] = \"modified\"\n        assert component.meta is not None\n        # get_meta returns a copy - mutating it doesn't affect the original\n        assert component.meta[\"key\"] == \"value\"\n\n    def test_component_with_complex_meta(self):\n        \"\"\"Test component with nested meta structures.\"\"\"\n        complex_meta = {\n            \"nested\": {\"level1\": {\"level2\": \"value\"}},\n            \"list\": [1, 2, 3],\n            \"bool\": True,\n        }\n        component = FastMCPComponent(name=\"test\", meta=complex_meta)\n        assert component.meta == complex_meta\n\n    def test_model_copy_preserves_all_attributes(self):\n        \"\"\"Test that model_copy preserves all component attributes.\"\"\"\n        component = FastMCPComponent(\n            name=\"test\",\n            title=\"Title\",\n            description=\"Description\",\n            tags={\"tag1\", \"tag2\"},\n            meta={\"key\": \"value\"},\n        )\n        new_component = component.model_copy()\n\n        assert new_component.name == component.name\n        assert new_component.title == component.title\n        assert new_component.description == component.description\n        assert new_component.tags == component.tags\n        assert new_component.meta == component.meta\n        assert new_component.key == component.key\n\n    def test_model_copy_with_update(self):\n        \"\"\"Test that model_copy works with update dict.\"\"\"\n        component = FastMCPComponent(\n            name=\"test\",\n            title=\"Original Title\",\n            description=\"Original Description\",\n            tags={\"tag1\"},\n        )\n\n        # Test with update (including name which affects .key)\n        updated_component = component.model_copy(\n            update={\n                \"name\": \"new_name\",\n                \"title\": \"New Title\",\n                \"description\": \"New Description\",\n            },\n        )\n\n        assert updated_component.name == \"new_name\"  # Updated\n        assert updated_component.title == \"New Title\"  # Updated\n        assert updated_component.description == \"New Description\"  # Updated\n        assert updated_component.tags == {\"tag1\"}  # Not in update, unchanged\n        assert (\n            updated_component.key == \"new_name@\"\n        )  # .key is computed from name with @ sentinel\n\n        # Original should be unchanged\n        assert component.name == \"test\"\n        assert component.title == \"Original Title\"\n        assert component.description == \"Original Description\"\n        assert component.key == \"test@\"  # Uses name as key with @ sentinel\n\n    def test_model_copy_deep_parameter(self):\n        \"\"\"Test that model_copy respects the deep parameter.\"\"\"\n        nested_dict = {\"nested\": {\"value\": 1}}\n        component = FastMCPComponent(name=\"test\", meta=nested_dict)\n\n        # Shallow copy (default)\n        shallow_copy = component.model_copy()\n        assert shallow_copy.meta is not None\n        assert component.meta is not None\n        shallow_copy.meta[\"nested\"][\"value\"] = 2\n        assert component.meta[\"nested\"][\"value\"] == 2  # Original affected\n\n        # Deep copy\n        component.meta[\"nested\"][\"value\"] = 1  # Reset\n        deep_copy = component.model_copy(deep=True)\n        assert deep_copy.meta is not None\n        deep_copy.meta[\"nested\"][\"value\"] = 3\n        assert component.meta[\"nested\"][\"value\"] == 1  # Original unaffected\n"
  },
  {
    "path": "tests/utilities/test_inspect.py",
    "content": "\"\"\"Tests for the inspect.py module.\"\"\"\n\nimport importlib.metadata\n\nfrom mcp.server.fastmcp import FastMCP as FastMCP1x\n\nimport fastmcp\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.utilities.inspect import (\n    FastMCPInfo,\n    ToolInfo,\n    inspect_fastmcp,\n    inspect_fastmcp_v1,\n)\n\n\nclass TestFastMCPInfo:\n    \"\"\"Tests for the FastMCPInfo dataclass.\"\"\"\n\n    def test_fastmcp_info_creation(self):\n        \"\"\"Test that FastMCPInfo can be created with all required fields.\"\"\"\n        tool = ToolInfo(\n            key=\"tool1\",\n            name=\"tool1\",\n            description=\"Test tool\",\n            input_schema={},\n            output_schema={\n                \"type\": \"object\",\n                \"properties\": {\"result\": {\"type\": \"string\"}},\n            },\n        )\n        info = FastMCPInfo(\n            name=\"TestServer\",\n            instructions=\"Test instructions\",\n            fastmcp_version=\"1.0.0\",\n            mcp_version=\"1.0.0\",\n            server_generation=2,\n            version=\"1.0.0\",\n            website_url=None,\n            icons=None,\n            tools=[tool],\n            prompts=[],\n            resources=[],\n            templates=[],\n            capabilities={\"tools\": {\"listChanged\": True}},\n        )\n\n        assert info.name == \"TestServer\"\n        assert info.instructions == \"Test instructions\"\n        assert info.fastmcp_version == \"1.0.0\"\n        assert info.mcp_version == \"1.0.0\"\n        assert info.server_generation == 2\n        assert info.version == \"1.0.0\"\n        assert len(info.tools) == 1\n        assert info.tools[0].name == \"tool1\"\n        assert info.capabilities == {\"tools\": {\"listChanged\": True}}\n\n    def test_fastmcp_info_with_none_instructions(self):\n        \"\"\"Test that FastMCPInfo works with None instructions.\"\"\"\n        info = FastMCPInfo(\n            name=\"TestServer\",\n            instructions=None,\n            fastmcp_version=\"1.0.0\",\n            mcp_version=\"1.0.0\",\n            server_generation=2,\n            version=\"1.0.0\",\n            website_url=None,\n            icons=None,\n            tools=[],\n            prompts=[],\n            resources=[],\n            templates=[],\n            capabilities={},\n        )\n\n        assert info.instructions is None\n\n\nclass TestGetFastMCPInfo:\n    \"\"\"Tests for the get_fastmcp_info function.\"\"\"\n\n    async def test_empty_server(self):\n        \"\"\"Test get_fastmcp_info with an empty server.\"\"\"\n        mcp = FastMCP(\"EmptyServer\")\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"EmptyServer\"\n        assert info.instructions is None\n        assert info.fastmcp_version == fastmcp.__version__\n        assert info.mcp_version == importlib.metadata.version(\"mcp\")\n        assert info.server_generation == 2  # v2 server\n        assert info.version == fastmcp.__version__\n        assert info.tools == []\n        assert info.prompts == []\n        assert info.resources == []\n        assert info.templates == []\n        assert \"tools\" in info.capabilities\n        assert \"resources\" in info.capabilities\n        assert \"prompts\" in info.capabilities\n        assert \"logging\" in info.capabilities\n\n    async def test_server_with_instructions(self):\n        \"\"\"Test get_fastmcp_info with a server that has instructions.\"\"\"\n        mcp = FastMCP(\"InstructionsServer\", instructions=\"Test instructions\")\n        info = await inspect_fastmcp(mcp)\n        assert info.instructions == \"Test instructions\"\n\n    async def test_server_with_version(self):\n        \"\"\"Test get_fastmcp_info with a server that has a version.\"\"\"\n        mcp = FastMCP(\"VersionServer\", version=\"1.2.3\")\n        info = await inspect_fastmcp(mcp)\n        assert info.version == \"1.2.3\"\n\n    async def test_server_with_tools(self):\n        \"\"\"Test get_fastmcp_info with a server that has tools.\"\"\"\n        mcp = FastMCP(\"ToolServer\")\n\n        @mcp.tool\n        def add_numbers(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.tool\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"ToolServer\"\n        assert len(info.tools) == 2\n        tool_names = [tool.name for tool in info.tools]\n        assert \"add_numbers\" in tool_names\n        assert \"greet\" in tool_names\n\n    async def test_server_with_resources(self):\n        \"\"\"Test get_fastmcp_info with a server that has resources.\"\"\"\n        mcp = FastMCP(\"ResourceServer\")\n\n        @mcp.resource(\"resource://static\")\n        def get_static_data() -> str:\n            return \"Static data\"\n\n        @mcp.resource(\"resource://dynamic/{param}\")\n        def get_dynamic_data(param: str) -> str:\n            return f\"Dynamic data: {param}\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"ResourceServer\"\n        assert len(info.resources) == 1  # Static resource\n        assert len(info.templates) == 1  # Dynamic resource becomes template\n        resource_uris = [res.uri for res in info.resources]\n        template_uris = [tmpl.uri_template for tmpl in info.templates]\n        assert \"resource://static\" in resource_uris\n        assert \"resource://dynamic/{param}\" in template_uris\n\n    async def test_server_with_prompts(self):\n        \"\"\"Test get_fastmcp_info with a server that has prompts.\"\"\"\n        mcp = FastMCP(\"PromptServer\")\n\n        @mcp.prompt\n        def analyze_data(data: str) -> list:\n            return [{\"role\": \"user\", \"content\": f\"Analyze: {data}\"}]\n\n        @mcp.prompt(\"custom_prompt\")\n        def custom_analysis(text: str) -> list:\n            return [{\"role\": \"user\", \"content\": f\"Custom: {text}\"}]\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"PromptServer\"\n        assert len(info.prompts) == 2\n        prompt_names = [prompt.name for prompt in info.prompts]\n        assert \"analyze_data\" in prompt_names\n        assert \"custom_prompt\" in prompt_names\n\n    async def test_comprehensive_server(self):\n        \"\"\"Test get_fastmcp_info with a server that has all component types.\"\"\"\n        mcp = FastMCP(\"ComprehensiveServer\", instructions=\"A server with everything\")\n\n        # Add a tool\n        @mcp.tool\n        def calculate(x: int, y: int) -> int:\n            return x * y\n\n        # Add a resource\n        @mcp.resource(\"resource://data\")\n        def get_data() -> str:\n            return \"Some data\"\n\n        # Add a template\n        @mcp.resource(\"resource://item/{id}\")\n        def get_item(id: str) -> str:\n            return f\"Item {id}\"\n\n        # Add a prompt\n        @mcp.prompt\n        def analyze(content: str) -> list:\n            return [{\"role\": \"user\", \"content\": content}]\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"ComprehensiveServer\"\n        assert info.instructions == \"A server with everything\"\n        assert info.fastmcp_version == fastmcp.__version__\n\n        # Check all components are present\n        assert len(info.tools) == 1\n        tool_names = [tool.name for tool in info.tools]\n        assert \"calculate\" in tool_names\n\n        assert len(info.resources) == 1\n        resource_uris = [res.uri for res in info.resources]\n        assert \"resource://data\" in resource_uris\n\n        assert len(info.templates) == 1\n        template_uris = [tmpl.uri_template for tmpl in info.templates]\n        assert \"resource://item/{id}\" in template_uris\n\n        assert len(info.prompts) == 1\n        prompt_names = [prompt.name for prompt in info.prompts]\n        assert \"analyze\" in prompt_names\n\n        # Check capabilities\n        assert \"tools\" in info.capabilities\n        assert \"resources\" in info.capabilities\n        assert \"prompts\" in info.capabilities\n        assert \"logging\" in info.capabilities\n\n    async def test_server_no_instructions(self):\n        \"\"\"Test get_fastmcp_info with a server that has no instructions.\"\"\"\n        mcp = FastMCP(\"NoInstructionsServer\")\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"NoInstructionsServer\"\n        assert info.instructions is None\n\n    async def test_server_with_client_integration(self):\n        \"\"\"Test that the extracted info matches what a client would see.\"\"\"\n        mcp = FastMCP(\"IntegrationServer\")\n\n        @mcp.tool\n        def test_tool() -> str:\n            return \"test\"\n\n        @mcp.resource(\"resource://test\")\n        def test_resource() -> str:\n            return \"test resource\"\n\n        @mcp.prompt\n        def test_prompt() -> list:\n            return [{\"role\": \"user\", \"content\": \"test\"}]\n\n        # Get info using our function\n        info = await inspect_fastmcp(mcp)\n\n        # Verify using client\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            resources = await client.list_resources()\n            prompts = await client.list_prompts()\n\n            assert len(info.tools) == len(tools)\n            assert len(info.resources) == len(resources)\n            assert len(info.prompts) == len(prompts)\n\n            assert info.tools[0].name == tools[0].name\n            assert info.resources[0].uri == str(resources[0].uri)\n            assert info.prompts[0].name == prompts[0].name\n\n    async def test_inspect_respects_tag_filtering(self):\n        \"\"\"Test that inspect omits components filtered out by include_tags/exclude_tags.\n\n        Regression test for Issue #2032: inspect command was showing components\n        that were filtered out by tag rules, causing confusion when those\n        components weren't actually available to clients.\n        \"\"\"\n        # Create server with include_tags that will filter out untagged components\n        mcp = FastMCP(\"FilteredServer\")\n        mcp.enable(tags={\"fetch\", \"analyze\", \"create\"}, only=True)\n\n        # Add tools with and without matching tags\n        @mcp.tool(tags={\"fetch\"})\n        def tagged_tool() -> str:\n            \"\"\"Tool with matching tag - should be visible.\"\"\"\n            return \"visible\"\n\n        @mcp.tool\n        def untagged_tool() -> str:\n            \"\"\"Tool without tags - should be filtered out.\"\"\"\n            return \"hidden\"\n\n        # Add resources with and without matching tags\n        @mcp.resource(\"resource://tagged\", tags={\"analyze\"})\n        def tagged_resource() -> str:\n            \"\"\"Resource with matching tag - should be visible.\"\"\"\n            return \"visible resource\"\n\n        @mcp.resource(\"resource://untagged\")\n        def untagged_resource() -> str:\n            \"\"\"Resource without tags - should be filtered out.\"\"\"\n            return \"hidden resource\"\n\n        # Add templates with and without matching tags\n        @mcp.resource(\"resource://tagged/{id}\", tags={\"create\"})\n        def tagged_template(id: str) -> str:\n            \"\"\"Template with matching tag - should be visible.\"\"\"\n            return f\"visible template {id}\"\n\n        @mcp.resource(\"resource://untagged/{id}\")\n        def untagged_template(id: str) -> str:\n            \"\"\"Template without tags - should be filtered out.\"\"\"\n            return f\"hidden template {id}\"\n\n        # Add prompts with and without matching tags\n        @mcp.prompt(tags={\"fetch\"})\n        def tagged_prompt() -> list:\n            \"\"\"Prompt with matching tag - should be visible.\"\"\"\n            return [{\"role\": \"user\", \"content\": \"visible prompt\"}]\n\n        @mcp.prompt\n        def untagged_prompt() -> list:\n            \"\"\"Prompt without tags - should be filtered out.\"\"\"\n            return [{\"role\": \"user\", \"content\": \"hidden prompt\"}]\n\n        # Get inspect info\n        info = await inspect_fastmcp(mcp)\n\n        # Verify only tagged components are visible\n        assert len(info.tools) == 1\n        assert info.tools[0].name == \"tagged_tool\"\n\n        assert len(info.resources) == 1\n        assert info.resources[0].uri == \"resource://tagged\"\n\n        assert len(info.templates) == 1\n        assert info.templates[0].uri_template == \"resource://tagged/{id}\"\n\n        assert len(info.prompts) == 1\n        assert info.prompts[0].name == \"tagged_prompt\"\n\n        # Verify this matches what a client would see\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            resources = await client.list_resources()\n            templates = await client.list_resource_templates()\n            prompts = await client.list_prompts()\n\n            assert len(info.tools) == len(tools)\n            assert len(info.resources) == len(resources)\n            assert len(info.templates) == len(templates)\n            assert len(info.prompts) == len(prompts)\n\n    async def test_inspect_respects_tag_filtering_with_mounted_servers(self):\n        \"\"\"Test that inspect applies tag filtering to mounted servers.\n\n        Verifies that when a parent server has tag filters, those filters\n        are respected when inspecting components from mounted servers.\n        \"\"\"\n        # Create a mounted server with various tagged and untagged components\n        mounted = FastMCP(\"MountedServer\")\n\n        @mounted.tool(tags={\"allowed\"})\n        def allowed_tool() -> str:\n            return \"allowed\"\n\n        @mounted.tool(tags={\"blocked\"})\n        def blocked_tool() -> str:\n            return \"blocked\"\n\n        @mounted.tool\n        def untagged_tool() -> str:\n            return \"untagged\"\n\n        @mounted.resource(\"resource://allowed\", tags={\"allowed\"})\n        def allowed_resource() -> str:\n            return \"allowed resource\"\n\n        @mounted.resource(\"resource://blocked\", tags={\"blocked\"})\n        def blocked_resource() -> str:\n            return \"blocked resource\"\n\n        @mounted.prompt(tags={\"allowed\"})\n        def allowed_prompt() -> list:\n            return [{\"role\": \"user\", \"content\": \"allowed\"}]\n\n        @mounted.prompt(tags={\"blocked\"})\n        def blocked_prompt() -> list:\n            return [{\"role\": \"user\", \"content\": \"blocked\"}]\n\n        # Create parent server with tag filtering\n        parent = FastMCP(\"ParentServer\")\n        parent.enable(tags={\"allowed\"}, only=True)\n        parent.mount(mounted)\n\n        # Get inspect info\n        info = await inspect_fastmcp(parent)\n\n        # Only components with \"allowed\" tag should be visible\n        tool_names = [t.name for t in info.tools]\n        assert \"allowed_tool\" in tool_names\n        assert \"blocked_tool\" not in tool_names\n        assert \"untagged_tool\" not in tool_names\n\n        resource_uris = [r.uri for r in info.resources]\n        assert \"resource://allowed\" in resource_uris\n        assert \"resource://blocked\" not in resource_uris\n\n        prompt_names = [p.name for p in info.prompts]\n        assert \"allowed_prompt\" in prompt_names\n        assert \"blocked_prompt\" not in prompt_names\n\n        # Verify this matches what a client would see\n        async with Client(parent) as client:\n            tools = await client.list_tools()\n            resources = await client.list_resources()\n            prompts = await client.list_prompts()\n\n            assert len(info.tools) == len(tools)\n            assert len(info.resources) == len(resources)\n            assert len(info.prompts) == len(prompts)\n\n    async def test_inspect_parent_filters_override_mounted_server_filters(self):\n        \"\"\"Test that parent server tag filters apply to mounted servers.\n\n        Even if a mounted server has no tag filters of its own,\n        the parent server's filters should still apply.\n        \"\"\"\n        # Create mounted server with NO tag filters (allows everything)\n        mounted = FastMCP(\"MountedServer\")\n\n        @mounted.tool(tags={\"production\"})\n        def production_tool() -> str:\n            return \"production\"\n\n        @mounted.tool(tags={\"development\"})\n        def development_tool() -> str:\n            return \"development\"\n\n        @mounted.tool\n        def untagged_tool() -> str:\n            return \"untagged\"\n\n        # Create parent with exclude_tags - should filter mounted components\n        parent = FastMCP(\"ParentServer\")\n        parent.disable(tags={\"development\"})\n        parent.mount(mounted)\n\n        # Get inspect info\n        info = await inspect_fastmcp(parent)\n\n        # Only production and untagged should be visible\n        tool_names = [t.name for t in info.tools]\n        assert \"production_tool\" in tool_names\n        assert \"untagged_tool\" in tool_names\n        assert \"development_tool\" not in tool_names\n\n        # Verify this matches what a client would see\n        async with Client(parent) as client:\n            tools = await client.list_tools()\n            assert len(info.tools) == len(tools)\n\n\nclass TestFastMCP1xCompatibility:\n    \"\"\"Tests for FastMCP 1.x compatibility.\"\"\"\n\n    async def test_fastmcp1x_empty_server(self):\n        \"\"\"Test get_fastmcp_info_v1 with an empty FastMCP1x server.\"\"\"\n        mcp = FastMCP1x(\"Test1x\")\n\n        info = await inspect_fastmcp_v1(mcp)\n\n        assert info.name == \"Test1x\"\n        assert info.instructions is None\n        assert info.fastmcp_version == fastmcp.__version__  # CLI version\n        assert info.mcp_version == importlib.metadata.version(\"mcp\")\n        assert info.server_generation == 1  # v1 server\n        assert info.version is None\n        assert info.tools == []\n        assert info.prompts == []\n        assert info.resources == []\n        assert info.templates == []  # No templates added in this test\n        assert \"tools\" in info.capabilities\n\n    async def test_fastmcp1x_with_tools(self):\n        \"\"\"Test get_fastmcp_info_v1 with a FastMCP1x server that has tools.\"\"\"\n        mcp = FastMCP1x(\"Test1x\")\n\n        @mcp.tool()\n        def add_numbers(a: int, b: int) -> int:\n            return a + b\n\n        @mcp.tool()\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        info = await inspect_fastmcp_v1(mcp)\n\n        assert info.name == \"Test1x\"\n        assert len(info.tools) == 2\n        tool_names = [tool.name for tool in info.tools]\n        assert \"add_numbers\" in tool_names\n        assert \"greet\" in tool_names\n\n    async def test_fastmcp1x_with_resources(self):\n        \"\"\"Test get_fastmcp_info_v1 with a FastMCP1x server that has resources.\"\"\"\n        mcp = FastMCP1x(\"Test1x\")\n\n        @mcp.resource(\"resource://data\")\n        def get_data() -> str:\n            return \"Some data\"\n\n        info = await inspect_fastmcp_v1(mcp)\n\n        assert info.name == \"Test1x\"\n        assert len(info.resources) == 1\n        resource_uris = [res.uri for res in info.resources]\n        assert \"resource://data\" in resource_uris\n        assert len(info.templates) == 0  # No templates added in this test\n        assert info.server_generation == 1  # v1 server\n\n    async def test_fastmcp1x_with_prompts(self):\n        \"\"\"Test get_fastmcp_info_v1 with a FastMCP1x server that has prompts.\"\"\"\n        mcp = FastMCP1x(\"Test1x\")\n\n        @mcp.prompt(\"analyze\")\n        def analyze_data(data: str) -> list:\n            return [{\"role\": \"user\", \"content\": f\"Analyze: {data}\"}]\n\n        info = await inspect_fastmcp_v1(mcp)\n\n        assert info.name == \"Test1x\"\n        assert len(info.prompts) == 1\n        prompt_names = [prompt.name for prompt in info.prompts]\n        assert \"analyze\" in prompt_names\n\n    async def test_dispatcher_with_fastmcp1x(self):\n        \"\"\"Test that the main get_fastmcp_info function correctly dispatches to v1.\"\"\"\n        mcp = FastMCP1x(\"Test1x\")\n\n        @mcp.tool()\n        def test_tool() -> str:\n            return \"test\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"Test1x\"\n        assert len(info.tools) == 1\n        tool_names = [tool.name for tool in info.tools]\n        assert \"test_tool\" in tool_names\n        assert len(info.templates) == 0  # No templates added in this test\n        assert info.server_generation == 1  # v1 server\n\n    async def test_dispatcher_with_fastmcp2x(self):\n        \"\"\"Test that the main get_fastmcp_info function correctly dispatches to v2.\"\"\"\n        mcp = FastMCP(\"Test2x\")\n\n        @mcp.tool\n        def test_tool() -> str:\n            return \"test\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.name == \"Test2x\"\n        assert len(info.tools) == 1\n        tool_names = [tool.name for tool in info.tools]\n        assert \"test_tool\" in tool_names\n\n    async def test_fastmcp1x_vs_fastmcp2x_comparison(self):\n        \"\"\"Test that both versions can be inspected and compared.\"\"\"\n        mcp1x = FastMCP1x(\"Test1x\")\n        mcp2x = FastMCP(\"Test2x\")\n\n        @mcp1x.tool()\n        def tool1x() -> str:\n            return \"1x\"\n\n        @mcp2x.tool\n        def tool2x() -> str:\n            return \"2x\"\n\n        info1x = await inspect_fastmcp(mcp1x)\n        info2x = await inspect_fastmcp(mcp2x)\n\n        assert info1x.name == \"Test1x\"\n        assert info2x.name == \"Test2x\"\n        assert len(info1x.tools) == 1\n        assert len(info2x.tools) == 1\n\n        tool1x_names = [tool.name for tool in info1x.tools]\n        tool2x_names = [tool.name for tool in info2x.tools]\n        assert \"tool1x\" in tool1x_names\n        assert \"tool2x\" in tool2x_names\n\n        # Check server versions\n        assert info1x.server_generation == 1  # v1\n        assert info2x.server_generation == 2  # v2\n        assert info1x.version is None\n        assert info2x.version == fastmcp.__version__\n\n        # No templates added in these tests\n        assert len(info1x.templates) == 0\n        assert len(info2x.templates) == 0\n"
  },
  {
    "path": "tests/utilities/test_inspect_icons.py",
    "content": "\"\"\"Tests for icon extraction and formatting functions in inspect.py.\"\"\"\n\nimport importlib.metadata\n\nfrom mcp.server.fastmcp import FastMCP as FastMCP1x\n\nimport fastmcp\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.inspect import (\n    InspectFormat,\n    format_fastmcp_info,\n    format_info,\n    format_mcp_info,\n    inspect_fastmcp,\n    inspect_fastmcp_v1,\n)\n\n\nclass TestIconExtraction:\n    \"\"\"Tests for icon extraction in inspect.\"\"\"\n\n    async def test_server_icons_and_website(self):\n        \"\"\"Test that server-level icons and website_url are extracted.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\n            \"IconServer\",\n            website_url=\"https://example.com\",\n            icons=[\n                Icon(\n                    src=\"https://example.com/icon.png\",\n                    mimeType=\"image/png\",\n                    sizes=[\"48x48\"],\n                )\n            ],\n        )\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.website_url == \"https://example.com\"\n        assert info.icons is not None\n        assert len(info.icons) == 1\n        assert info.icons[0][\"src\"] == \"https://example.com/icon.png\"\n        assert info.icons[0][\"mimeType\"] == \"image/png\"\n        assert info.icons[0][\"sizes\"] == [\"48x48\"]\n\n    async def test_server_without_icons(self):\n        \"\"\"Test that servers without icons have None for icons and website_url.\"\"\"\n        mcp = FastMCP(\"NoIconServer\")\n\n        info = await inspect_fastmcp(mcp)\n\n        assert info.website_url is None\n        assert info.icons is None\n\n    async def test_tool_icons(self):\n        \"\"\"Test that tool icons are extracted.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\"ToolIconServer\")\n\n        @mcp.tool(\n            icons=[\n                Icon(\n                    src=\"https://example.com/calculator.png\",\n                    mimeType=\"image/png\",\n                )\n            ]\n        )\n        def calculate(x: int) -> int:\n            \"\"\"Calculate something.\"\"\"\n            return x * 2\n\n        @mcp.tool\n        def no_icon_tool() -> str:\n            \"\"\"Tool without icon.\"\"\"\n            return \"no icon\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert len(info.tools) == 2\n\n        # Find the calculate tool\n        calculate_tool = next(t for t in info.tools if t.name == \"calculate\")\n        assert calculate_tool.icons is not None\n        assert len(calculate_tool.icons) == 1\n        assert calculate_tool.icons[0][\"src\"] == \"https://example.com/calculator.png\"\n\n        # Find the no_icon tool\n        no_icon = next(t for t in info.tools if t.name == \"no_icon_tool\")\n        assert no_icon.icons is None\n\n    async def test_resource_icons(self):\n        \"\"\"Test that resource icons are extracted.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\"ResourceIconServer\")\n\n        @mcp.resource(\n            \"resource://data\",\n            icons=[Icon(src=\"https://example.com/data.png\", mimeType=\"image/png\")],\n        )\n        def get_data() -> str:\n            \"\"\"Get data.\"\"\"\n            return \"data\"\n\n        @mcp.resource(\"resource://no-icon\")\n        def get_no_icon() -> str:\n            \"\"\"Get data without icon.\"\"\"\n            return \"no icon\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert len(info.resources) == 2\n\n        # Find the data resource\n        data_resource = next(r for r in info.resources if r.uri == \"resource://data\")\n        assert data_resource.icons is not None\n        assert len(data_resource.icons) == 1\n        assert data_resource.icons[0][\"src\"] == \"https://example.com/data.png\"\n\n        # Find the no-icon resource\n        no_icon = next(r for r in info.resources if r.uri == \"resource://no-icon\")\n        assert no_icon.icons is None\n\n    async def test_template_icons(self):\n        \"\"\"Test that resource template icons are extracted.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\"TemplateIconServer\")\n\n        @mcp.resource(\n            \"resource://user/{id}\",\n            icons=[Icon(src=\"https://example.com/user.png\", mimeType=\"image/png\")],\n        )\n        def get_user(id: str) -> str:\n            \"\"\"Get user by ID.\"\"\"\n            return f\"user {id}\"\n\n        @mcp.resource(\"resource://item/{id}\")\n        def get_item(id: str) -> str:\n            \"\"\"Get item without icon.\"\"\"\n            return f\"item {id}\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert len(info.templates) == 2\n\n        # Find the user template\n        user_template = next(\n            t for t in info.templates if t.uri_template == \"resource://user/{id}\"\n        )\n        assert user_template.icons is not None\n        assert len(user_template.icons) == 1\n        assert user_template.icons[0][\"src\"] == \"https://example.com/user.png\"\n\n        # Find the no-icon template\n        no_icon = next(\n            t for t in info.templates if t.uri_template == \"resource://item/{id}\"\n        )\n        assert no_icon.icons is None\n\n    async def test_prompt_icons(self):\n        \"\"\"Test that prompt icons are extracted.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\"PromptIconServer\")\n\n        @mcp.prompt(\n            icons=[Icon(src=\"https://example.com/analyze.png\", mimeType=\"image/png\")]\n        )\n        def analyze(data: str) -> list:\n            \"\"\"Analyze data.\"\"\"\n            return [{\"role\": \"user\", \"content\": f\"Analyze: {data}\"}]\n\n        @mcp.prompt\n        def no_icon_prompt(text: str) -> list:\n            \"\"\"Prompt without icon.\"\"\"\n            return [{\"role\": \"user\", \"content\": text}]\n\n        info = await inspect_fastmcp(mcp)\n\n        assert len(info.prompts) == 2\n\n        # Find the analyze prompt\n        analyze_prompt = next(p for p in info.prompts if p.name == \"analyze\")\n        assert analyze_prompt.icons is not None\n        assert len(analyze_prompt.icons) == 1\n        assert analyze_prompt.icons[0][\"src\"] == \"https://example.com/analyze.png\"\n\n        # Find the no-icon prompt\n        no_icon = next(p for p in info.prompts if p.name == \"no_icon_prompt\")\n        assert no_icon.icons is None\n\n    async def test_multiple_icons(self):\n        \"\"\"Test that components with multiple icons extract all of them.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\n            \"MultiIconServer\",\n            icons=[\n                Icon(\n                    src=\"https://example.com/icon-48.png\",\n                    mimeType=\"image/png\",\n                    sizes=[\"48x48\"],\n                ),\n                Icon(\n                    src=\"https://example.com/icon-96.png\",\n                    mimeType=\"image/png\",\n                    sizes=[\"96x96\"],\n                ),\n            ],\n        )\n\n        @mcp.tool(\n            icons=[\n                Icon(src=\"https://example.com/tool-small.png\", sizes=[\"24x24\"]),\n                Icon(src=\"https://example.com/tool-large.png\", sizes=[\"48x48\"]),\n            ]\n        )\n        def multi_icon_tool() -> str:\n            \"\"\"Tool with multiple icons.\"\"\"\n            return \"multi\"\n\n        info = await inspect_fastmcp(mcp)\n\n        # Check server icons\n        assert info.icons is not None\n        assert len(info.icons) == 2\n        assert info.icons[0][\"sizes\"] == [\"48x48\"]\n        assert info.icons[1][\"sizes\"] == [\"96x96\"]\n\n        # Check tool icons\n        assert len(info.tools) == 1\n        assert info.tools[0].icons is not None\n        assert len(info.tools[0].icons) == 2\n        assert info.tools[0].icons[0][\"sizes\"] == [\"24x24\"]\n        assert info.tools[0].icons[1][\"sizes\"] == [\"48x48\"]\n\n    async def test_data_uri_icons(self):\n        \"\"\"Test that data URI icons are extracted correctly.\"\"\"\n        from mcp.types import Icon\n\n        data_uri = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n\n        mcp = FastMCP(\"DataURIServer\")\n\n        @mcp.tool(icons=[Icon(src=data_uri, mimeType=\"image/png\")])\n        def data_uri_tool() -> str:\n            \"\"\"Tool with data URI icon.\"\"\"\n            return \"data\"\n\n        info = await inspect_fastmcp(mcp)\n\n        assert len(info.tools) == 1\n        assert info.tools[0].icons is not None\n        assert info.tools[0].icons[0][\"src\"] == data_uri\n        assert info.tools[0].icons[0][\"mimeType\"] == \"image/png\"\n\n    async def test_icons_in_fastmcp_v1(self):\n        \"\"\"Test that icons are extracted from FastMCP 1.x servers.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP1x(\"Icon1xServer\")\n\n        @mcp.tool(\n            icons=[Icon(src=\"https://example.com/v1-tool.png\", mimeType=\"image/png\")]\n        )\n        def v1_tool() -> str:\n            \"\"\"Tool in v1 server.\"\"\"\n            return \"v1\"\n\n        info = await inspect_fastmcp_v1(mcp)\n\n        assert len(info.tools) == 1\n        # v1 servers should also extract icons if present\n        if info.tools[0].icons is not None:\n            assert info.tools[0].icons[0][\"src\"] == \"https://example.com/v1-tool.png\"\n\n    async def test_icons_in_formatted_output(self):\n        \"\"\"Test that icons appear in formatted JSON output.\"\"\"\n        from mcp.types import Icon\n\n        mcp = FastMCP(\n            \"FormattedIconServer\",\n            website_url=\"https://example.com\",\n            icons=[Icon(src=\"https://example.com/server.png\", mimeType=\"image/png\")],\n        )\n\n        @mcp.tool(\n            icons=[Icon(src=\"https://example.com/tool.png\", mimeType=\"image/png\")]\n        )\n        def icon_tool() -> str:\n            \"\"\"Tool with icon.\"\"\"\n            return \"icon\"\n\n        info = await inspect_fastmcp(mcp)\n        json_bytes = format_fastmcp_info(info)\n\n        import json\n\n        data = json.loads(json_bytes)\n\n        # Check server icons in formatted output\n        assert data[\"server\"][\"website_url\"] == \"https://example.com\"\n        assert data[\"server\"][\"icons\"] is not None\n        assert len(data[\"server\"][\"icons\"]) == 1\n        assert data[\"server\"][\"icons\"][0][\"src\"] == \"https://example.com/server.png\"\n\n        # Check tool icons in formatted output\n        assert len(data[\"tools\"]) == 1\n        assert data[\"tools\"][0][\"icons\"] is not None\n        assert len(data[\"tools\"][0][\"icons\"]) == 1\n        assert data[\"tools\"][0][\"icons\"][0][\"src\"] == \"https://example.com/tool.png\"\n\n    async def test_icons_always_present_in_json(self):\n        \"\"\"Test that icons and website_url fields are always present in JSON, even when None.\"\"\"\n        mcp = FastMCP(\"AlwaysPresentServer\")\n\n        @mcp.tool\n        def no_icon() -> str:\n            \"\"\"Tool without icon.\"\"\"\n            return \"none\"\n\n        info = await inspect_fastmcp(mcp)\n        json_bytes = format_fastmcp_info(info)\n\n        import json\n\n        data = json.loads(json_bytes)\n\n        # Fields should always be present, even when None\n        assert \"website_url\" in data[\"server\"]\n        assert \"icons\" in data[\"server\"]\n        assert data[\"server\"][\"website_url\"] is None\n        assert data[\"server\"][\"icons\"] is None\n\n        assert len(data[\"tools\"]) == 1\n        assert \"icons\" in data[\"tools\"][0]\n        assert data[\"tools\"][0][\"icons\"] is None\n\n\nclass TestFormatFunctions:\n    \"\"\"Tests for the formatting functions.\"\"\"\n\n    async def test_format_fastmcp_info(self):\n        \"\"\"Test formatting as FastMCP-specific JSON.\"\"\"\n        mcp = FastMCP(\"TestServer\", instructions=\"Test instructions\", version=\"1.2.3\")\n\n        @mcp.tool\n        def test_tool(x: int) -> dict:\n            \"\"\"A test tool.\"\"\"\n            return {\"result\": x * 2}\n\n        info = await inspect_fastmcp(mcp)\n        json_bytes = format_fastmcp_info(info)\n\n        # Verify it's valid JSON\n        import json\n\n        data = json.loads(json_bytes)\n\n        # Check FastMCP-specific fields are present\n        assert \"server\" in data\n        assert data[\"server\"][\"name\"] == \"TestServer\"\n        assert data[\"server\"][\"instructions\"] == \"Test instructions\"\n        assert data[\"server\"][\"generation\"] == 2  # v2 server\n        assert data[\"server\"][\"version\"] == \"1.2.3\"\n        assert \"capabilities\" in data[\"server\"]\n\n        # Check environment information\n        assert \"environment\" in data\n        assert data[\"environment\"][\"fastmcp\"] == fastmcp.__version__\n        assert data[\"environment\"][\"mcp\"] == importlib.metadata.version(\"mcp\")\n\n        # Check tools\n        assert len(data[\"tools\"]) == 1\n        assert data[\"tools\"][0][\"name\"] == \"test_tool\"\n        assert \"tags\" in data[\"tools\"][0]\n\n    async def test_format_mcp_info(self):\n        \"\"\"Test formatting as MCP protocol JSON.\"\"\"\n        mcp = FastMCP(\"TestServer\", instructions=\"Test instructions\", version=\"2.0.0\")\n\n        @mcp.tool\n        def add(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        @mcp.prompt\n        def test_prompt(name: str) -> list:\n            \"\"\"Test prompt.\"\"\"\n            return [{\"role\": \"user\", \"content\": f\"Hello {name}\"}]\n\n        json_bytes = await format_mcp_info(mcp)\n\n        # Verify it's valid JSON\n        import json\n\n        data = json.loads(json_bytes)\n\n        # Check MCP protocol structure with camelCase\n        assert \"serverInfo\" in data\n        assert data[\"serverInfo\"][\"name\"] == \"TestServer\"\n\n        # Check server version in MCP format\n        assert data[\"serverInfo\"][\"version\"] == \"2.0.0\"\n\n        # MCP format SHOULD have environment fields\n        assert \"environment\" in data\n        assert data[\"environment\"][\"fastmcp\"] == fastmcp.__version__\n        assert data[\"environment\"][\"mcp\"] == importlib.metadata.version(\"mcp\")\n        assert \"capabilities\" in data\n\n        assert \"tools\" in data\n        assert \"prompts\" in data\n        assert \"resources\" in data\n        assert \"resourceTemplates\" in data\n\n        # Check tools have MCP format (camelCase fields)\n        assert len(data[\"tools\"]) == 1\n        assert data[\"tools\"][0][\"name\"] == \"add\"\n        assert \"inputSchema\" in data[\"tools\"][0]\n\n        # FastMCP-specific fields should not be present\n        assert \"tags\" not in data[\"tools\"][0]\n        assert \"enabled\" not in data[\"tools\"][0]\n\n    async def test_format_info_with_fastmcp_format(self):\n        \"\"\"Test format_info with fastmcp format.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def test() -> str:\n            return \"test\"\n\n        # Test with string format\n        json_bytes = await format_info(mcp, \"fastmcp\")\n        import json\n\n        data = json.loads(json_bytes)\n        assert data[\"server\"][\"name\"] == \"TestServer\"\n        assert \"tags\" in data[\"tools\"][0]  # FastMCP-specific field\n\n        # Test with enum format\n        json_bytes = await format_info(mcp, InspectFormat.FASTMCP)\n        data = json.loads(json_bytes)\n        assert data[\"server\"][\"name\"] == \"TestServer\"\n\n    async def test_format_info_with_mcp_format(self):\n        \"\"\"Test format_info with mcp format.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def test() -> str:\n            return \"test\"\n\n        json_bytes = await format_info(mcp, \"mcp\")\n\n        import json\n\n        data = json.loads(json_bytes)\n        assert \"serverInfo\" in data\n        assert \"tools\" in data\n        assert \"inputSchema\" in data[\"tools\"][0]  # MCP uses camelCase\n\n    async def test_format_info_requires_format(self):\n        \"\"\"Test that format_info requires a format parameter.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool\n        def test() -> str:\n            return \"test\"\n\n        # Should work with valid formats\n        json_bytes = await format_info(mcp, \"fastmcp\")\n        assert json_bytes\n\n        json_bytes = await format_info(mcp, \"mcp\")\n        assert json_bytes\n\n        # Should fail with invalid format\n        import pytest\n\n        with pytest.raises(ValueError, match=\"not a valid InspectFormat\"):\n            await format_info(mcp, \"invalid\")  # type: ignore\n\n    async def test_tool_with_output_schema(self):\n        \"\"\"Test that output_schema is properly extracted and included.\"\"\"\n        mcp = FastMCP(\"TestServer\")\n\n        @mcp.tool(\n            output_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"result\": {\"type\": \"number\"},\n                    \"message\": {\"type\": \"string\"},\n                },\n            }\n        )\n        def compute(x: int) -> dict:\n            \"\"\"Compute something.\"\"\"\n            return {\"result\": x * 2, \"message\": f\"Doubled {x}\"}\n\n        info = await inspect_fastmcp(mcp)\n\n        # Check output_schema is captured\n        assert len(info.tools) == 1\n        assert info.tools[0].output_schema is not None\n        assert info.tools[0].output_schema[\"type\"] == \"object\"\n        assert \"result\" in info.tools[0].output_schema[\"properties\"]\n\n        # Verify it's included in FastMCP format\n        json_bytes = format_fastmcp_info(info)\n        import json\n\n        data = json.loads(json_bytes)\n        # Tools are at the top level, not nested\n        assert data[\"tools\"][0][\"output_schema\"][\"type\"] == \"object\"\n"
  },
  {
    "path": "tests/utilities/test_json_schema.py",
    "content": "from unittest.mock import patch\n\nfrom jsonref import replace_refs\n\nfrom fastmcp.utilities.json_schema import (\n    _prune_param,\n    _strip_remote_refs,\n    compress_schema,\n    dereference_refs,\n    resolve_root_ref,\n)\n\n\nclass TestPruneParam:\n    \"\"\"Tests for the _prune_param function.\"\"\"\n\n    def test_nonexistent(self):\n        \"\"\"Test pruning a parameter that doesn't exist.\"\"\"\n        schema = {\"properties\": {\"foo\": {\"type\": \"string\"}}}\n        result = _prune_param(schema, \"bar\")\n        assert result == schema  # Schema should be unchanged\n\n    def test_exists(self):\n        \"\"\"Test pruning a parameter that exists.\"\"\"\n        schema = {\"properties\": {\"foo\": {\"type\": \"string\"}, \"bar\": {\"type\": \"integer\"}}}\n        result = _prune_param(schema, \"bar\")\n        assert result[\"properties\"] == {\"foo\": {\"type\": \"string\"}}\n\n    def test_last_property(self):\n        \"\"\"Test pruning the only/last parameter, should leave empty properties object.\"\"\"\n        schema = {\"properties\": {\"foo\": {\"type\": \"string\"}}}\n        result = _prune_param(schema, \"foo\")\n        assert \"properties\" in result\n        assert result[\"properties\"] == {}\n\n    def test_from_required(self):\n        \"\"\"Test pruning a parameter that's in the required list.\"\"\"\n        schema = {\n            \"properties\": {\"foo\": {\"type\": \"string\"}, \"bar\": {\"type\": \"integer\"}},\n            \"required\": [\"foo\", \"bar\"],\n        }\n        result = _prune_param(schema, \"bar\")\n        assert result[\"required\"] == [\"foo\"]\n\n    def test_last_required(self):\n        \"\"\"Test pruning the last required parameter, should remove required field.\"\"\"\n        schema = {\n            \"properties\": {\"foo\": {\"type\": \"string\"}, \"bar\": {\"type\": \"integer\"}},\n            \"required\": [\"foo\"],\n        }\n        result = _prune_param(schema, \"foo\")\n        assert \"required\" not in result\n\n\nclass TestDereferenceRefs:\n    \"\"\"Tests for the dereference_refs function.\"\"\"\n\n    def test_dereferences_simple_ref(self):\n        \"\"\"Test that simple $ref is dereferenced.\"\"\"\n        schema = {\n            \"properties\": {\n                \"foo\": {\"$ref\": \"#/$defs/foo_def\"},\n            },\n            \"$defs\": {\n                \"foo_def\": {\"type\": \"string\"},\n            },\n        }\n        result = dereference_refs(schema)\n\n        # $ref should be inlined\n        assert result[\"properties\"][\"foo\"] == {\"type\": \"string\"}\n        # $defs should be removed\n        assert \"$defs\" not in result\n\n    def test_dereferences_nested_refs(self):\n        \"\"\"Test that nested $refs are dereferenced.\"\"\"\n        schema = {\n            \"properties\": {\n                \"foo\": {\"$ref\": \"#/$defs/foo_def\"},\n            },\n            \"$defs\": {\n                \"foo_def\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"nested\": {\"$ref\": \"#/$defs/nested_def\"}},\n                },\n                \"nested_def\": {\"type\": \"string\"},\n            },\n        }\n        result = dereference_refs(schema)\n\n        # All refs should be inlined\n        assert result[\"properties\"][\"foo\"][\"properties\"][\"nested\"] == {\"type\": \"string\"}\n        # $defs should be removed\n        assert \"$defs\" not in result\n\n    def test_falls_back_for_circular_refs(self):\n        \"\"\"Test that circular references fall back to resolve_root_ref.\"\"\"\n        schema = {\n            \"$defs\": {\n                \"Node\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"children\": {\n                            \"type\": \"array\",\n                            \"items\": {\"$ref\": \"#/$defs/Node\"},\n                        }\n                    },\n                }\n            },\n            \"$ref\": \"#/$defs/Node\",\n        }\n        result = dereference_refs(schema)\n\n        # Should fall back to resolve_root_ref behavior\n        # Root should be resolved but nested refs preserved\n        assert result.get(\"type\") == \"object\"\n        assert \"$defs\" in result  # $defs preserved for circular refs\n\n    def test_preserves_sibling_keywords(self):\n        \"\"\"Test that sibling keywords (default, description) are preserved.\n\n        Pydantic places description, default, examples as siblings to $ref.\n        These should not be lost during dereferencing.\n        \"\"\"\n        schema = {\n            \"$defs\": {\n                \"Status\": {\"type\": \"string\", \"enum\": [\"active\", \"inactive\"]},\n            },\n            \"properties\": {\n                \"status\": {\n                    \"$ref\": \"#/$defs/Status\",\n                    \"default\": \"active\",\n                    \"description\": \"The user status\",\n                },\n            },\n            \"type\": \"object\",\n        }\n        result = dereference_refs(schema)\n\n        # $ref should be inlined with siblings preserved\n        status = result[\"properties\"][\"status\"]\n        assert status[\"type\"] == \"string\"\n        assert status[\"enum\"] == [\"active\", \"inactive\"]\n        assert status[\"default\"] == \"active\"\n        assert status[\"description\"] == \"The user status\"\n        # $defs should be removed\n        assert \"$defs\" not in result\n\n    def test_preserves_siblings_in_lists(self):\n        \"\"\"Test that siblings are preserved for $refs inside lists (allOf, anyOf, etc).\"\"\"\n        schema = {\n            \"$defs\": {\n                \"StringType\": {\"type\": \"string\"},\n                \"IntType\": {\"type\": \"integer\"},\n            },\n            \"properties\": {\n                \"field\": {\n                    \"anyOf\": [\n                        {\"$ref\": \"#/$defs/StringType\", \"description\": \"As string\"},\n                        {\"$ref\": \"#/$defs/IntType\", \"description\": \"As integer\"},\n                    ]\n                },\n            },\n        }\n        result = dereference_refs(schema)\n\n        # Both items in anyOf should have their siblings preserved\n        any_of = result[\"properties\"][\"field\"][\"anyOf\"]\n        assert any_of[0][\"type\"] == \"string\"\n        assert any_of[0][\"description\"] == \"As string\"\n        assert any_of[1][\"type\"] == \"integer\"\n        assert any_of[1][\"description\"] == \"As integer\"\n        assert \"$defs\" not in result\n\n    def test_preserves_nested_siblings(self):\n        \"\"\"Test that siblings on nested $refs are preserved.\"\"\"\n        schema = {\n            \"$defs\": {\n                \"Address\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"country\": {\"$ref\": \"#/$defs/Country\", \"default\": \"US\"},\n                    },\n                },\n                \"Country\": {\"type\": \"string\", \"enum\": [\"US\", \"UK\", \"CA\"]},\n            },\n            \"properties\": {\n                \"home_address\": {\"$ref\": \"#/$defs/Address\"},\n            },\n        }\n        result = dereference_refs(schema)\n\n        # The nested $ref's sibling (default) should be preserved\n        country = result[\"properties\"][\"home_address\"][\"properties\"][\"country\"]\n        assert country[\"type\"] == \"string\"\n        assert country[\"enum\"] == [\"US\", \"UK\", \"CA\"]\n        assert country[\"default\"] == \"US\"\n        assert \"$defs\" not in result\n\n\nclass TestCompressSchema:\n    \"\"\"Tests for the compress_schema function.\"\"\"\n\n    def test_preserves_refs_by_default(self):\n        \"\"\"Test that compress_schema preserves $refs by default.\"\"\"\n        schema = {\n            \"properties\": {\n                \"foo\": {\"$ref\": \"#/$defs/foo_def\"},\n            },\n            \"$defs\": {\n                \"foo_def\": {\"type\": \"string\"},\n            },\n        }\n        result = compress_schema(schema)\n\n        # $ref should be preserved (dereferencing is handled by middleware)\n        assert result[\"properties\"][\"foo\"] == {\"$ref\": \"#/$defs/foo_def\"}\n        assert \"$defs\" in result\n\n    def test_prune_params(self):\n        \"\"\"Test pruning parameters with compress_schema.\"\"\"\n        schema = {\n            \"properties\": {\n                \"foo\": {\"type\": \"string\"},\n                \"bar\": {\"type\": \"integer\"},\n                \"baz\": {\"type\": \"boolean\"},\n            },\n            \"required\": [\"foo\", \"bar\"],\n        }\n        result = compress_schema(schema, prune_params=[\"foo\", \"baz\"])\n        assert result[\"properties\"] == {\"bar\": {\"type\": \"integer\"}}\n        assert result[\"required\"] == [\"bar\"]\n\n    def test_pruning_additional_properties(self):\n        \"\"\"Test pruning additionalProperties when explicitly enabled.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"foo\": {\"type\": \"string\"}},\n            \"additionalProperties\": False,\n        }\n        # Must explicitly enable pruning now (default changed for MCP compatibility)\n        result = compress_schema(schema, prune_additional_properties=True)\n        assert \"additionalProperties\" not in result\n\n    def test_disable_pruning_additional_properties(self):\n        \"\"\"Test disabling pruning of additionalProperties.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"foo\": {\"type\": \"string\"}},\n            \"additionalProperties\": False,\n        }\n        result = compress_schema(schema, prune_additional_properties=False)\n        assert \"additionalProperties\" in result\n        assert result[\"additionalProperties\"] is False\n\n    def test_combined_operations(self):\n        \"\"\"Test all pruning operations together.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"keep\": {\"type\": \"string\"},\n                \"remove\": {\"$ref\": \"#/$defs/remove_def\"},\n            },\n            \"required\": [\"keep\", \"remove\"],\n            \"additionalProperties\": False,\n            \"$defs\": {\n                \"remove_def\": {\"type\": \"string\"},\n                \"unused_def\": {\"type\": \"number\"},\n            },\n        }\n        result = compress_schema(\n            schema, prune_params=[\"remove\"], prune_additional_properties=True\n        )\n        # Check that parameter was removed\n        assert \"remove\" not in result[\"properties\"]\n        # Check that required list was updated\n        assert result[\"required\"] == [\"keep\"]\n        # All $defs entries are now unreferenced after pruning \"remove\", so they're cleaned up\n        assert \"$defs\" not in result\n        # Check that additionalProperties was removed\n        assert \"additionalProperties\" not in result\n\n    def test_prune_titles(self):\n        \"\"\"Test pruning title fields.\"\"\"\n        schema = {\n            \"title\": \"Root Schema\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"foo\": {\"title\": \"Foo Property\", \"type\": \"string\"},\n                \"bar\": {\n                    \"title\": \"Bar Property\",\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"nested\": {\"title\": \"Nested Property\", \"type\": \"string\"}\n                    },\n                },\n            },\n        }\n        result = compress_schema(schema, prune_titles=True)\n        assert \"title\" not in result\n        assert \"title\" not in result[\"properties\"][\"foo\"]\n        assert \"title\" not in result[\"properties\"][\"bar\"]\n        assert \"title\" not in result[\"properties\"][\"bar\"][\"properties\"][\"nested\"]\n\n    def test_prune_nested_additional_properties(self):\n        \"\"\"Test pruning additionalProperties: false at all levels when explicitly enabled.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"additionalProperties\": False,\n            \"properties\": {\n                \"foo\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": False,\n                    \"properties\": {\n                        \"nested\": {\n                            \"type\": \"object\",\n                            \"additionalProperties\": False,\n                        }\n                    },\n                },\n            },\n        }\n        result = compress_schema(schema, prune_additional_properties=True)\n        assert \"additionalProperties\" not in result\n        assert \"additionalProperties\" not in result[\"properties\"][\"foo\"]\n        assert (\n            \"additionalProperties\"\n            not in result[\"properties\"][\"foo\"][\"properties\"][\"nested\"]\n        )\n\n    def test_title_pruning_preserves_parameter_named_title(self):\n        \"\"\"Test that a parameter named 'title' is not removed during title pruning.\n\n        This is a critical edge case - we want to remove title metadata but preserve\n        actual parameters that happen to be named 'title'.\n        \"\"\"\n        from typing import Annotated\n\n        from pydantic import Field, TypeAdapter\n\n        def greet(\n            name: Annotated[str, Field(description=\"The name to greet\")],\n            title: Annotated[str, Field(description=\"Optional title\", default=\"\")],\n        ) -> str:\n            \"\"\"A greeting function.\"\"\"\n            return f\"Hello {title} {name}\"\n\n        adapter = TypeAdapter(greet)\n        schema = adapter.json_schema()\n\n        # Compress with title pruning\n        compressed = compress_schema(schema, prune_titles=True)\n\n        # The 'title' parameter should be preserved\n        assert \"title\" in compressed[\"properties\"]\n        assert compressed[\"properties\"][\"title\"][\"description\"] == \"Optional title\"\n        assert compressed[\"properties\"][\"title\"][\"default\"] == \"\"\n\n        # But title metadata should be removed\n        assert \"title\" not in compressed[\"properties\"][\"name\"]\n        assert \"title\" not in compressed[\"properties\"][\"title\"]\n\n    def test_title_pruning_with_nested_properties(self):\n        \"\"\"Test that nested property structures are handled correctly.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"title\": \"OuterObject\",\n            \"properties\": {\n                \"title\": {  # This is a property named \"title\", not metadata\n                    \"type\": \"object\",\n                    \"title\": \"TitleObject\",  # This is metadata\n                    \"properties\": {\n                        \"subtitle\": {\n                            \"type\": \"string\",\n                            \"title\": \"SubTitle\",  # This is metadata\n                        }\n                    },\n                },\n                \"normal_field\": {\n                    \"type\": \"string\",\n                    \"title\": \"NormalField\",  # This is metadata\n                },\n            },\n        }\n\n        compressed = compress_schema(schema, prune_titles=True)\n\n        # Root title should be removed\n        assert \"title\" not in compressed\n\n        # The property named \"title\" should be preserved\n        assert \"title\" in compressed[\"properties\"]\n\n        # But its metadata title should be removed\n        assert \"title\" not in compressed[\"properties\"][\"title\"]\n\n        # Nested metadata titles should be removed\n        assert (\n            \"title\" not in compressed[\"properties\"][\"title\"][\"properties\"][\"subtitle\"]\n        )\n        assert \"title\" not in compressed[\"properties\"][\"normal_field\"]\n\n    def test_mcp_client_compatibility_requires_additional_properties(self):\n        \"\"\"Test that compress_schema preserves additionalProperties: false for MCP clients.\n\n        MCP clients like Claude require strict JSON schemas with additionalProperties: false.\n        When tools use Pydantic models with extra=\"forbid\", this constraint must be preserved.\n\n        Without this, MCP clients return:\n        \"Invalid schema for function 'X': In context=('properties', 'Y'),\n        'additionalProperties' is required to be supplied and to be false\"\n\n        See: https://github.com/PrefectHQ/fastmcp/issues/3008\n        \"\"\"\n        # Schema representing a Pydantic model with extra=\"forbid\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"graph_table\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"name\": {\"type\": \"string\"},\n                        \"columns\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n                    },\n                    \"required\": [\"name\"],\n                    \"additionalProperties\": False,\n                }\n            },\n            \"required\": [\"graph_table\"],\n            \"additionalProperties\": False,\n        }\n\n        # By default, compress_schema should NOT strip additionalProperties: false\n        # This is the new expected behavior for MCP compatibility\n        result = compress_schema(schema)\n\n        # Root level should preserve additionalProperties: false\n        assert result.get(\"additionalProperties\") is False, (\n            \"Root additionalProperties: false was removed, breaking MCP compatibility\"\n        )\n\n        # Nested object should also preserve additionalProperties: false\n        graph_table = result[\"properties\"][\"graph_table\"]\n        assert graph_table.get(\"additionalProperties\") is False, (\n            \"Nested additionalProperties: false was removed, breaking MCP compatibility\"\n        )\n\n\nclass TestCompressSchemaDereference:\n    \"\"\"Tests for the dereference parameter of compress_schema.\"\"\"\n\n    SCHEMA_WITH_REFS = {\n        \"properties\": {\n            \"foo\": {\"$ref\": \"#/$defs/foo_def\"},\n        },\n        \"$defs\": {\n            \"foo_def\": {\"type\": \"string\"},\n        },\n    }\n\n    def test_dereference_true_inlines_refs(self):\n        result = compress_schema(self.SCHEMA_WITH_REFS, dereference=True)\n        assert result[\"properties\"][\"foo\"] == {\"type\": \"string\"}\n        assert \"$defs\" not in result\n\n    def test_dereference_false_preserves_refs(self):\n        result = compress_schema(self.SCHEMA_WITH_REFS, dereference=False)\n        assert result[\"properties\"][\"foo\"] == {\"$ref\": \"#/$defs/foo_def\"}\n        assert \"$defs\" in result\n\n    def test_other_optimizations_still_apply_without_dereference(self):\n        schema = {\n            \"properties\": {\n                \"foo\": {\"$ref\": \"#/$defs/foo_def\"},\n                \"bar\": {\"type\": \"integer\", \"title\": \"Bar\"},\n            },\n            \"$defs\": {\n                \"foo_def\": {\"type\": \"string\"},\n            },\n        }\n        result = compress_schema(\n            schema, dereference=False, prune_params=[\"bar\"], prune_titles=True\n        )\n        assert \"bar\" not in result[\"properties\"]\n        assert \"$ref\" in result[\"properties\"][\"foo\"]\n        assert \"$defs\" in result\n\n\nclass TestResolveRootRef:\n    \"\"\"Tests for the resolve_root_ref function.\n\n    This function resolves $ref at root level to meet MCP spec requirements.\n    MCP specification requires outputSchema to have \"type\": \"object\" at root.\n    \"\"\"\n\n    def test_resolves_simple_root_ref(self):\n        \"\"\"Test that simple $ref at root is resolved.\"\"\"\n        schema = {\n            \"$defs\": {\n                \"Node\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"id\": {\"type\": \"string\"},\n                        \"name\": {\"type\": \"string\"},\n                    },\n                    \"required\": [\"id\"],\n                }\n            },\n            \"$ref\": \"#/$defs/Node\",\n        }\n        result = resolve_root_ref(schema)\n\n        # Should have type: object at root now\n        assert result.get(\"type\") == \"object\"\n        assert \"properties\" in result\n        assert \"id\" in result[\"properties\"]\n        assert \"name\" in result[\"properties\"]\n        # Should still have $defs for nested references\n        assert \"$defs\" in result\n        # Should NOT have $ref at root\n        assert \"$ref\" not in result\n\n    def test_resolves_self_referential_model(self):\n        \"\"\"Test resolving schema for self-referential models like Issue.\"\"\"\n        # This is the exact schema Pydantic generates for self-referential models\n        schema = {\n            \"$defs\": {\n                \"Issue\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"id\": {\"type\": \"string\"},\n                        \"title\": {\"type\": \"string\"},\n                        \"dependencies\": {\n                            \"type\": \"array\",\n                            \"items\": {\"$ref\": \"#/$defs/Issue\"},\n                        },\n                        \"dependents\": {\n                            \"type\": \"array\",\n                            \"items\": {\"$ref\": \"#/$defs/Issue\"},\n                        },\n                    },\n                    \"required\": [\"id\", \"title\"],\n                }\n            },\n            \"$ref\": \"#/$defs/Issue\",\n        }\n        result = resolve_root_ref(schema)\n\n        # Should have type: object at root\n        assert result.get(\"type\") == \"object\"\n        assert \"properties\" in result\n        assert \"id\" in result[\"properties\"]\n        assert \"dependencies\" in result[\"properties\"]\n        # Nested $refs should still point to $defs\n        assert result[\"properties\"][\"dependencies\"][\"items\"][\"$ref\"] == \"#/$defs/Issue\"\n        # Should have $defs preserved for nested references\n        assert \"$defs\" in result\n        assert \"Issue\" in result[\"$defs\"]\n\n    def test_does_not_modify_schema_with_type_at_root(self):\n        \"\"\"Test that schemas already having type at root are not modified.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"id\": {\"type\": \"string\"}},\n            \"$defs\": {\"SomeType\": {\"type\": \"string\"}},\n            \"$ref\": \"#/$defs/SomeType\",  # This would be unusual but possible\n        }\n        result = resolve_root_ref(schema)\n\n        # Schema should be unchanged (returned as-is)\n        assert result is schema\n\n    def test_does_not_modify_schema_without_ref(self):\n        \"\"\"Test that schemas without $ref are not modified.\"\"\"\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\"id\": {\"type\": \"string\"}},\n        }\n        result = resolve_root_ref(schema)\n\n        assert result is schema\n\n    def test_does_not_modify_schema_without_defs(self):\n        \"\"\"Test that schemas with $ref but without $defs are not modified.\"\"\"\n        schema = {\n            \"$ref\": \"#/$defs/Missing\",\n        }\n        result = resolve_root_ref(schema)\n\n        assert result is schema\n\n    def test_does_not_modify_external_ref(self):\n        \"\"\"Test that external $refs (not pointing to $defs) are not resolved.\"\"\"\n        schema = {\n            \"$defs\": {\"Node\": {\"type\": \"object\"}},\n            \"$ref\": \"https://example.com/schema.json#/definitions/Node\",\n        }\n        result = resolve_root_ref(schema)\n\n        assert result is schema\n\n    def test_preserves_all_defs_for_nested_references(self):\n        \"\"\"Test that $defs are preserved even if multiple definitions exist.\"\"\"\n        schema = {\n            \"$defs\": {\n                \"Node\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"child\": {\"$ref\": \"#/$defs/ChildNode\"},\n                    },\n                },\n                \"ChildNode\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"value\": {\"type\": \"string\"}},\n                },\n            },\n            \"$ref\": \"#/$defs/Node\",\n        }\n        result = resolve_root_ref(schema)\n\n        # Both defs should be preserved\n        assert \"$defs\" in result\n        assert \"Node\" in result[\"$defs\"]\n        assert \"ChildNode\" in result[\"$defs\"]\n\n    def test_handles_missing_def_gracefully(self):\n        \"\"\"Test that missing definition in $defs doesn't cause error.\"\"\"\n        schema = {\n            \"$defs\": {\"OtherType\": {\"type\": \"string\"}},\n            \"$ref\": \"#/$defs/Missing\",\n        }\n        result = resolve_root_ref(schema)\n\n        # Should return original schema unchanged\n        assert result is schema\n\n\nclass TestStripRemoteRefs:\n    \"\"\"Tests for _strip_remote_refs which prevents SSRF/LFI via $ref.\"\"\"\n\n    def test_preserves_local_ref(self):\n        schema = {\"$ref\": \"#/$defs/Foo\"}\n        assert _strip_remote_refs(schema) == {\"$ref\": \"#/$defs/Foo\"}\n\n    def test_strips_http_ref(self):\n        schema = {\"$ref\": \"http://evil.com/schema.json\"}\n        assert _strip_remote_refs(schema) == {}\n\n    def test_strips_https_ref(self):\n        schema = {\"$ref\": \"https://evil.com/schema.json\"}\n        assert _strip_remote_refs(schema) == {}\n\n    def test_strips_file_ref(self):\n        schema = {\"$ref\": \"file:///etc/passwd\"}\n        assert _strip_remote_refs(schema) == {}\n\n    def test_preserves_siblings_when_stripping(self):\n        schema = {\n            \"$ref\": \"http://evil.com/schema.json\",\n            \"description\": \"keep me\",\n            \"default\": 42,\n        }\n        result = _strip_remote_refs(schema)\n        assert result == {\"description\": \"keep me\", \"default\": 42}\n\n    def test_strips_nested_remote_refs(self):\n        schema = {\n            \"properties\": {\n                \"safe\": {\"$ref\": \"#/$defs/Safe\"},\n                \"evil\": {\"$ref\": \"http://169.254.169.254/latest/meta-data/\"},\n            }\n        }\n        result = _strip_remote_refs(schema)\n        assert result[\"properties\"][\"safe\"] == {\"$ref\": \"#/$defs/Safe\"}\n        assert \"$ref\" not in result[\"properties\"][\"evil\"]\n\n    def test_strips_remote_refs_in_lists(self):\n        schema = {\n            \"anyOf\": [\n                {\"$ref\": \"#/$defs/Good\"},\n                {\"$ref\": \"file:///etc/credentials.json\"},\n            ]\n        }\n        result = _strip_remote_refs(schema)\n        assert result[\"anyOf\"][0] == {\"$ref\": \"#/$defs/Good\"}\n        assert \"$ref\" not in result[\"anyOf\"][1]\n\n    def test_deep_nesting(self):\n        schema = {\n            \"properties\": {\n                \"a\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"b\": {\"$ref\": \"https://internal-service/secret\"}},\n                }\n            }\n        }\n        result = _strip_remote_refs(schema)\n        assert \"$ref\" not in result[\"properties\"][\"a\"][\"properties\"][\"b\"]\n\n\nclass TestDereferenceRefsRemoteRefSafety:\n    \"\"\"Verify dereference_refs never fetches remote URIs.\"\"\"\n\n    def test_http_ref_not_fetched(self):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"$ref\": \"http://evil.com/schema.json\"},\n            },\n        }\n        with patch(\n            \"fastmcp.utilities.json_schema.replace_refs\", wraps=replace_refs\n        ) as mock:\n            result = dereference_refs(schema)\n            # The remote $ref should have been stripped before replace_refs\n            if mock.called:\n                call_schema = mock.call_args[0][0]\n                assert \"$ref\" not in call_schema.get(\"properties\", {}).get(\"name\", {})\n        # Result should not contain the remote $ref\n        assert \"$ref\" not in result.get(\"properties\", {}).get(\"name\", {})\n\n    def test_file_ref_not_fetched(self):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"secret\": {\"$ref\": \"file:///etc/passwd\"},\n            },\n        }\n        result = dereference_refs(schema)\n        assert \"$ref\" not in result.get(\"properties\", {}).get(\"secret\", {})\n\n    def test_cloud_metadata_ref_not_fetched(self):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"creds\": {\n                    \"$ref\": \"http://169.254.169.254/latest/meta-data/iam/security-credentials/\"\n                },\n            },\n        }\n        result = dereference_refs(schema)\n        assert \"$ref\" not in result.get(\"properties\", {}).get(\"creds\", {})\n\n    def test_local_refs_still_resolved(self):\n        schema = {\n            \"$defs\": {\"Status\": {\"type\": \"string\", \"enum\": [\"a\", \"b\"]}},\n            \"type\": \"object\",\n            \"properties\": {\n                \"status\": {\"$ref\": \"#/$defs/Status\"},\n                \"evil\": {\"$ref\": \"https://evil.com/inject\"},\n            },\n        }\n        result = dereference_refs(schema)\n        # Local ref should be resolved\n        assert result[\"properties\"][\"status\"] == {\"type\": \"string\", \"enum\": [\"a\", \"b\"]}\n        # Remote ref should be stripped\n        assert \"$ref\" not in result[\"properties\"][\"evil\"]\n        assert \"$defs\" not in result\n"
  },
  {
    "path": "tests/utilities/test_logging.py",
    "content": "import logging\n\nimport fastmcp\nfrom fastmcp.utilities.logging import configure_logging, get_logger\n\n\ndef test_logging_doesnt_affect_other_loggers(caplog):\n    # set FastMCP loggers to CRITICAL and ensure other loggers still emit messages\n    original_level = logging.getLogger(\"fastmcp\").getEffectiveLevel()\n\n    try:\n        logging.getLogger(\"fastmcp\").setLevel(logging.CRITICAL)\n\n        root_logger = logging.getLogger()\n        app_logger = logging.getLogger(\"app\")\n        fastmcp_logger = logging.getLogger(\"fastmcp\")\n        fastmcp_server_logger = get_logger(\"server\")\n\n        with caplog.at_level(logging.INFO):\n            root_logger.info(\"--ROOT--\")\n            app_logger.info(\"--APP--\")\n            fastmcp_logger.info(\"--FASTMCP--\")\n            fastmcp_server_logger.info(\"--FASTMCP SERVER--\")\n\n        assert \"--ROOT--\" in caplog.text\n        assert \"--APP--\" in caplog.text\n        assert \"--FASTMCP--\" not in caplog.text\n        assert \"--FASTMCP SERVER--\" not in caplog.text\n\n    finally:\n        logging.getLogger(\"fastmcp\").setLevel(original_level)\n\n\ndef test_configure_logging_with_traceback_kwargs():\n    \"\"\"Test that traceback-related kwargs can be passed without causing duplicate argument errors.\"\"\"\n    # This should not raise TypeError about duplicate keyword arguments\n    configure_logging(enable_rich_tracebacks=True, tracebacks_max_frames=20)\n\n    # Verify the logger was configured\n    logger = logging.getLogger(\"fastmcp\")\n    assert logger.handlers\n    assert len(logger.handlers) == 2  # One for normal logs, one for tracebacks\n\n\ndef test_configure_logging_traceback_defaults_can_be_overridden():\n    \"\"\"Test that default traceback settings can be overridden by kwargs.\"\"\"\n    configure_logging(\n        enable_rich_tracebacks=True,\n        tracebacks_max_frames=20,\n        show_path=True,\n        show_level=True,\n    )\n\n    logger = logging.getLogger(\"fastmcp\")\n    assert logger.handlers\n    # The traceback handler should have been created with custom values\n    # We can't directly inspect RichHandler internals easily, but we verified no error was raised\n\n\ndef test_configure_logging_with_rich_disabled():\n    \"\"\"Test that disabling rich logging uses standard StreamHandler.\"\"\"\n    original_enable_rich = fastmcp.settings.enable_rich_logging\n    try:\n        fastmcp.settings.enable_rich_logging = False\n        configure_logging()\n\n        logger = logging.getLogger(\"fastmcp\")\n        assert logger.handlers\n        # Should only have one handler when rich logging is disabled\n        assert len(logger.handlers) == 1\n        # Should be a StreamHandler, not RichHandler\n        assert isinstance(logger.handlers[0], logging.StreamHandler)\n        assert not hasattr(\n            logger.handlers[0], \"console\"\n        )  # RichHandler has console attribute\n    finally:\n        fastmcp.settings.enable_rich_logging = original_enable_rich\n        # Reconfigure to restore state\n        configure_logging()\n\n\ndef test_configure_logging_with_rich_enabled():\n    \"\"\"Test that enabling rich logging uses RichHandler.\"\"\"\n    original_enable_rich = fastmcp.settings.enable_rich_logging\n    try:\n        fastmcp.settings.enable_rich_logging = True\n        configure_logging()\n\n        logger = logging.getLogger(\"fastmcp\")\n        assert logger.handlers\n        # Should have two handlers when rich logging is enabled (normal + traceback)\n        assert len(logger.handlers) == 2\n        # Both should be RichHandler instances\n        from rich.logging import RichHandler\n\n        assert all(isinstance(h, RichHandler) for h in logger.handlers)\n    finally:\n        fastmcp.settings.enable_rich_logging = original_enable_rich\n        # Reconfigure to restore state\n        configure_logging()\n"
  },
  {
    "path": "tests/utilities/test_skills.py",
    "content": "\"\"\"Tests for skills client utilities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\n\nfrom fastmcp import Client, FastMCP\nfrom fastmcp.server.providers.skills import SkillsDirectoryProvider\nfrom fastmcp.utilities.skills import (\n    SkillFile,\n    SkillManifest,\n    SkillSummary,\n    download_skill,\n    get_skill_manifest,\n    list_skills,\n    sync_skills,\n)\n\n\n@pytest.fixture\ndef skills_dir(tmp_path: Path) -> Path:\n    \"\"\"Create a temporary skills directory with sample skills.\"\"\"\n    skills = tmp_path / \"skills\"\n    skills.mkdir()\n\n    # Create pdf-processing skill\n    pdf_skill = skills / \"pdf-processing\"\n    pdf_skill.mkdir()\n    (pdf_skill / \"SKILL.md\").write_text(\n        \"\"\"---\ndescription: Process PDF documents\n---\n\n# PDF Processing\n\nInstructions for PDF handling.\n\"\"\"\n    )\n    (pdf_skill / \"reference.md\").write_text(\"# Reference\\n\\nSome reference docs.\")\n\n    # Create code-review skill\n    code_skill = skills / \"code-review\"\n    code_skill.mkdir()\n    (code_skill / \"SKILL.md\").write_text(\n        \"\"\"---\ndescription: Review code for quality\n---\n\n# Code Review\n\nInstructions for reviewing code.\n\"\"\"\n    )\n\n    # Create skill with nested files\n    nested_skill = skills / \"nested-skill\"\n    nested_skill.mkdir()\n    (nested_skill / \"SKILL.md\").write_text(\"# Nested\\n\\nHas nested files.\")\n    scripts = nested_skill / \"scripts\"\n    scripts.mkdir()\n    (scripts / \"helper.py\").write_text(\"# Helper script\\nprint('hello')\")\n\n    return skills\n\n\n@pytest.fixture\ndef skills_server(skills_dir: Path) -> FastMCP:\n    \"\"\"Create a FastMCP server with skills provider.\"\"\"\n    mcp = FastMCP(\"Skills Server\")\n    mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir))\n    return mcp\n\n\nclass TestListSkills:\n    async def test_lists_available_skills(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            skills = await list_skills(client)\n\n        assert len(skills) == 3\n        names = {s.name for s in skills}\n        assert names == {\"pdf-processing\", \"code-review\", \"nested-skill\"}\n\n    async def test_returns_skill_summary_objects(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            skills = await list_skills(client)\n\n        for skill in skills:\n            assert isinstance(skill, SkillSummary)\n            assert skill.name\n            assert skill.uri.startswith(\"skill://\")\n            assert skill.uri.endswith(\"/SKILL.md\")\n\n    async def test_includes_descriptions(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            skills = await list_skills(client)\n\n        by_name = {s.name: s for s in skills}\n        assert by_name[\"pdf-processing\"].description == \"Process PDF documents\"\n        assert by_name[\"code-review\"].description == \"Review code for quality\"\n\n    async def test_empty_server_returns_empty_list(self):\n        mcp = FastMCP(\"Empty\")\n        async with Client(mcp) as client:\n            skills = await list_skills(client)\n\n        assert skills == []\n\n\nclass TestGetSkillManifest:\n    async def test_returns_manifest_with_files(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            manifest = await get_skill_manifest(client, \"pdf-processing\")\n\n        assert isinstance(manifest, SkillManifest)\n        assert manifest.name == \"pdf-processing\"\n        assert len(manifest.files) == 2\n\n        paths = {f.path for f in manifest.files}\n        assert paths == {\"SKILL.md\", \"reference.md\"}\n\n    async def test_files_have_size_and_hash(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            manifest = await get_skill_manifest(client, \"pdf-processing\")\n\n        for file in manifest.files:\n            assert isinstance(file, SkillFile)\n            assert file.size > 0\n            assert file.hash.startswith(\"sha256:\")\n\n    async def test_nested_files_use_posix_paths(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            manifest = await get_skill_manifest(client, \"nested-skill\")\n\n        paths = {f.path for f in manifest.files}\n        assert \"scripts/helper.py\" in paths\n\n    async def test_nonexistent_skill_raises(self, skills_server: FastMCP):\n        async with Client(skills_server) as client:\n            with pytest.raises(Exception):\n                await get_skill_manifest(client, \"nonexistent\")\n\n\nclass TestDownloadSkill:\n    async def test_downloads_skill_to_directory(\n        self, skills_server: FastMCP, tmp_path: Path\n    ):\n        target = tmp_path / \"downloaded\"\n        target.mkdir()\n\n        async with Client(skills_server) as client:\n            result = await download_skill(client, \"pdf-processing\", target)\n\n        assert result == target / \"pdf-processing\"\n        assert result.exists()\n        assert (result / \"SKILL.md\").exists()\n        assert (result / \"reference.md\").exists()\n\n    async def test_creates_nested_directories(\n        self, skills_server: FastMCP, tmp_path: Path\n    ):\n        target = tmp_path / \"downloaded\"\n        target.mkdir()\n\n        async with Client(skills_server) as client:\n            result = await download_skill(client, \"nested-skill\", target)\n\n        assert (result / \"scripts\" / \"helper.py\").exists()\n        content = (result / \"scripts\" / \"helper.py\").read_text()\n        assert \"print('hello')\" in content\n\n    async def test_preserves_file_content(\n        self, skills_server: FastMCP, tmp_path: Path, skills_dir: Path\n    ):\n        target = tmp_path / \"downloaded\"\n        target.mkdir()\n\n        async with Client(skills_server) as client:\n            result = await download_skill(client, \"pdf-processing\", target)\n\n        original = (skills_dir / \"pdf-processing\" / \"SKILL.md\").read_text()\n        downloaded = (result / \"SKILL.md\").read_text()\n        assert downloaded == original\n\n    async def test_raises_if_exists_without_overwrite(\n        self, skills_server: FastMCP, tmp_path: Path\n    ):\n        target = tmp_path / \"downloaded\"\n        target.mkdir()\n        (target / \"pdf-processing\").mkdir()\n\n        async with Client(skills_server) as client:\n            with pytest.raises(FileExistsError):\n                await download_skill(client, \"pdf-processing\", target)\n\n    async def test_overwrites_with_flag(self, skills_server: FastMCP, tmp_path: Path):\n        target = tmp_path / \"downloaded\"\n        target.mkdir()\n        existing = target / \"pdf-processing\"\n        existing.mkdir()\n        (existing / \"old-file.txt\").write_text(\"old content\")\n\n        async with Client(skills_server) as client:\n            result = await download_skill(\n                client, \"pdf-processing\", target, overwrite=True\n            )\n\n        assert (result / \"SKILL.md\").exists()\n\n    async def test_expands_user_path(self, skills_server: FastMCP, tmp_path: Path):\n        # This tests that ~ expansion works (though we can't actually test ~)\n        async with Client(skills_server) as client:\n            result = await download_skill(client, \"code-review\", tmp_path)\n\n        assert result.exists()\n\n\nclass TestSyncSkills:\n    async def test_downloads_all_skills(self, skills_server: FastMCP, tmp_path: Path):\n        target = tmp_path / \"synced\"\n        target.mkdir()\n\n        async with Client(skills_server) as client:\n            results = await sync_skills(client, target)\n\n        assert len(results) == 3\n        assert (target / \"pdf-processing\").exists()\n        assert (target / \"code-review\").exists()\n        assert (target / \"nested-skill\").exists()\n\n    async def test_skips_existing_without_overwrite(\n        self, skills_server: FastMCP, tmp_path: Path\n    ):\n        target = tmp_path / \"synced\"\n        target.mkdir()\n        (target / \"pdf-processing\").mkdir()\n\n        async with Client(skills_server) as client:\n            results = await sync_skills(client, target)\n\n        # Should skip pdf-processing, download the other two\n        assert len(results) == 2\n        names = {r.name for r in results}\n        assert \"pdf-processing\" not in names\n\n    async def test_overwrites_with_flag(self, skills_server: FastMCP, tmp_path: Path):\n        target = tmp_path / \"synced\"\n        target.mkdir()\n        (target / \"pdf-processing\").mkdir()\n\n        async with Client(skills_server) as client:\n            results = await sync_skills(client, target, overwrite=True)\n\n        assert len(results) == 3\n\n    async def test_returns_paths_to_downloaded_skills(\n        self, skills_server: FastMCP, tmp_path: Path\n    ):\n        target = tmp_path / \"synced\"\n        target.mkdir()\n\n        async with Client(skills_server) as client:\n            results = await sync_skills(client, target)\n\n        for path in results:\n            assert isinstance(path, Path)\n            assert path.exists()\n            assert (path / \"SKILL.md\").exists()\n\n\nclass TestPathTraversal:\n    @pytest.mark.parametrize(\n        \"malicious_name\",\n        [\n            \"../escape\",\n            \"../../root\",\n            \"../../../etc/passwd\",\n            \"foo/../../escape\",\n        ],\n    )\n    async def test_malicious_skill_name_raises(\n        self, skills_server: FastMCP, tmp_path: Path, malicious_name: str\n    ):\n        target = tmp_path / \"downloaded\"\n        target.mkdir()\n\n        async with Client(skills_server) as client:\n            with pytest.raises(ValueError, match=\"would escape the target directory\"):\n                await download_skill(client, malicious_name, target)\n"
  },
  {
    "path": "tests/utilities/test_tests.py",
    "content": "from unittest.mock import AsyncMock, patch\n\nimport fastmcp\nfrom fastmcp import FastMCP\nfrom fastmcp.utilities.tests import temporary_settings\n\n\nclass TestTemporarySettings:\n    def test_temporary_settings(self):\n        assert fastmcp.settings.log_level == \"DEBUG\"\n        with temporary_settings(log_level=\"ERROR\"):\n            assert fastmcp.settings.log_level == \"ERROR\"\n        assert fastmcp.settings.log_level == \"DEBUG\"\n\n\nclass TestTransportSetting:\n    def test_transport_default_is_stdio(self):\n        assert fastmcp.settings.transport == \"stdio\"\n\n    def test_transport_setting_can_be_changed(self):\n        with temporary_settings(transport=\"http\"):\n            assert fastmcp.settings.transport == \"http\"\n        assert fastmcp.settings.transport == \"stdio\"\n\n    async def test_run_async_uses_transport_setting(self):\n        mcp = FastMCP(\"test\")\n        with temporary_settings(transport=\"http\"):\n            with patch.object(\n                mcp, \"run_http_async\", new_callable=AsyncMock\n            ) as mock_http:\n                await mcp.run_async()\n                mock_http.assert_called_once()\n\n    async def test_run_async_explicit_transport_overrides_setting(self):\n        mcp = FastMCP(\"test\")\n        with temporary_settings(transport=\"http\"):\n            with patch.object(\n                mcp, \"run_stdio_async\", new_callable=AsyncMock\n            ) as mock_stdio:\n                await mcp.run_async(transport=\"stdio\")\n                mock_stdio.assert_called_once()\n"
  },
  {
    "path": "tests/utilities/test_token_cache.py",
    "content": "\"\"\"Tests for the shared TokenCache utility.\"\"\"\n\nimport time\n\nimport pytest\n\nfrom fastmcp.server.auth.auth import AccessToken\nfrom fastmcp.utilities.token_cache import TokenCache\n\n\ndef _make_token(\n    *,\n    token: str = \"tok\",\n    client_id: str = \"client-1\",\n    scopes: list[str] | None = None,\n    expires_at: int | None = None,\n) -> AccessToken:\n    return AccessToken(\n        token=token,\n        client_id=client_id,\n        scopes=scopes or [],\n        expires_at=expires_at,\n    )\n\n\nclass TestTokenCacheDisabled:\n    \"\"\"Verify behaviour when caching is turned off.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"ttl, max_size\",\n        [\n            (None, None),\n            (0, 100),\n            (300, 0),\n        ],\n    )\n    def test_disabled_configurations(self, ttl: int | None, max_size: int | None):\n        cache = TokenCache(ttl_seconds=ttl, max_size=max_size)\n        assert not cache.enabled\n\n    def test_negative_ttl_raises(self):\n        with pytest.raises(ValueError, match=\"cache_ttl_seconds must be non-negative\"):\n            TokenCache(ttl_seconds=-1)\n\n    def test_negative_max_size_raises(self):\n        with pytest.raises(ValueError, match=\"max_cache_size must be non-negative\"):\n            TokenCache(max_size=-1)\n\n    def test_get_returns_miss_when_disabled(self):\n        cache = TokenCache(ttl_seconds=0)\n        cache.set(\"tok\", _make_token())\n        hit, result = cache.get(\"tok\")\n        assert not hit\n        assert result is None\n\n    def test_set_is_noop_when_disabled(self):\n        cache = TokenCache(ttl_seconds=0)\n        cache.set(\"tok\", _make_token())\n        assert len(cache._entries) == 0\n\n\nclass TestTokenCacheEnabled:\n    \"\"\"Core get/set behaviour with caching on.\"\"\"\n\n    @pytest.fixture\n    def cache(self) -> TokenCache:\n        return TokenCache(ttl_seconds=300, max_size=100)\n\n    def test_enabled(self, cache: TokenCache):\n        assert cache.enabled\n\n    def test_set_and_get(self, cache: TokenCache):\n        access = _make_token(client_id=\"user-1\")\n        cache.set(\"tok-1\", access)\n\n        hit, result = cache.get(\"tok-1\")\n        assert hit\n        assert result is not None\n        assert result.client_id == \"user-1\"\n\n    def test_miss_for_unknown_token(self, cache: TokenCache):\n        hit, result = cache.get(\"unknown\")\n        assert not hit\n        assert result is None\n\n    def test_different_tokens_cached_separately(self, cache: TokenCache):\n        cache.set(\"tok-a\", _make_token(client_id=\"a\"))\n        cache.set(\"tok-b\", _make_token(client_id=\"b\"))\n\n        _, a = cache.get(\"tok-a\")\n        _, b = cache.get(\"tok-b\")\n        assert a is not None and a.client_id == \"a\"\n        assert b is not None and b.client_id == \"b\"\n\n\nclass TestTokenCacheDefensiveCopy:\n    \"\"\"Mutating a returned token must not affect the cached value.\"\"\"\n\n    def test_get_returns_deep_copy(self):\n        cache = TokenCache(ttl_seconds=300, max_size=100)\n        access = _make_token(client_id=\"orig\")\n        access.claims = {\"key\": \"original\"}\n        cache.set(\"tok\", access)\n\n        _, first = cache.get(\"tok\")\n        assert first is not None\n        first.claims[\"key\"] = \"mutated\"\n        first.scopes.append(\"admin\")\n\n        _, second = cache.get(\"tok\")\n        assert second is not None\n        assert second.claims[\"key\"] == \"original\"\n        assert \"admin\" not in second.scopes\n\n    def test_mutating_source_does_not_affect_cache(self):\n        cache = TokenCache(ttl_seconds=300, max_size=100)\n        access = _make_token(client_id=\"orig\")\n        access.claims = {\"key\": \"original\"}\n        cache.set(\"tok\", access)\n\n        access.claims[\"key\"] = \"mutated\"\n\n        _, cached = cache.get(\"tok\")\n        assert cached is not None\n        assert cached.claims[\"key\"] == \"original\"\n\n\nclass TestTokenCacheTTL:\n    \"\"\"Expiration and TTL behaviour.\"\"\"\n\n    def test_expired_entry_is_evicted_on_get(self):\n        cache = TokenCache(ttl_seconds=300, max_size=100)\n        cache.set(\"tok\", _make_token())\n\n        key = cache._hash_token(\"tok\")\n        cache._entries[key].expires_at = time.time() - 1\n\n        hit, result = cache.get(\"tok\")\n        assert not hit\n        assert result is None\n        assert key not in cache._entries\n\n    def test_token_expires_at_caps_ttl(self):\n        cache = TokenCache(ttl_seconds=300, max_size=100)\n        short_exp = int(time.time()) + 30\n        cache.set(\"tok\", _make_token(expires_at=short_exp))\n\n        key = cache._hash_token(\"tok\")\n        assert cache._entries[key].expires_at <= short_exp\n\n    def test_ttl_used_when_no_token_expiry(self):\n        cache = TokenCache(ttl_seconds=60, max_size=100)\n        before = time.time()\n        cache.set(\"tok\", _make_token(expires_at=None))\n        after = time.time()\n\n        key = cache._hash_token(\"tok\")\n        entry = cache._entries[key]\n        assert before + 60 <= entry.expires_at <= after + 60\n\n\nclass TestTokenCacheSizeLimit:\n    \"\"\"Eviction and size-limit behaviour.\"\"\"\n\n    def test_evicts_oldest_when_full(self):\n        cache = TokenCache(ttl_seconds=300, max_size=2)\n        cache.set(\"tok-0\", _make_token(client_id=\"0\"))\n        cache.set(\"tok-1\", _make_token(client_id=\"1\"))\n        cache.set(\"tok-2\", _make_token(client_id=\"2\"))\n\n        assert len(cache._entries) == 2\n        hit_0, _ = cache.get(\"tok-0\")\n        assert not hit_0\n\n        hit_1, _ = cache.get(\"tok-1\")\n        hit_2, _ = cache.get(\"tok-2\")\n        assert hit_1\n        assert hit_2\n\n    def test_cleanup_expired_before_eviction(self):\n        cache = TokenCache(ttl_seconds=300, max_size=2)\n        cache.set(\"tok-0\", _make_token(client_id=\"0\"))\n        cache.set(\"tok-1\", _make_token(client_id=\"1\"))\n\n        key_0 = cache._hash_token(\"tok-0\")\n        cache._entries[key_0].expires_at = time.time() - 1\n\n        cache.set(\"tok-2\", _make_token(client_id=\"2\"))\n\n        assert len(cache._entries) == 2\n        hit_1, _ = cache.get(\"tok-1\")\n        hit_2, _ = cache.get(\"tok-2\")\n        assert hit_1\n        assert hit_2\n\n    def test_overwrite_does_not_evict(self):\n        \"\"\"Overwriting an existing key should not evict another entry.\"\"\"\n        cache = TokenCache(ttl_seconds=300, max_size=2)\n        cache.set(\"tok-0\", _make_token(client_id=\"0\"))\n        cache.set(\"tok-1\", _make_token(client_id=\"1\"))\n\n        # Overwrite tok-0 — should NOT evict tok-1\n        cache.set(\"tok-0\", _make_token(client_id=\"0-updated\"))\n\n        assert len(cache._entries) == 2\n        hit_0, result_0 = cache.get(\"tok-0\")\n        hit_1, _ = cache.get(\"tok-1\")\n        assert hit_0\n        assert hit_1\n        assert result_0 is not None\n        assert result_0.client_id == \"0-updated\"\n\n\nclass TestTokenCacheHashing:\n    \"\"\"SHA-256 key hashing.\"\"\"\n\n    def test_consistent_hashing(self):\n        assert TokenCache._hash_token(\"abc\") == TokenCache._hash_token(\"abc\")\n\n    def test_different_tokens_different_hashes(self):\n        assert TokenCache._hash_token(\"abc\") != TokenCache._hash_token(\"xyz\")\n\n    def test_hash_is_64_hex_chars(self):\n        h = TokenCache._hash_token(\"anything\")\n        assert len(h) == 64\n        int(h, 16)  # must be valid hex\n"
  },
  {
    "path": "tests/utilities/test_typeadapter.py",
    "content": "\"\"\"\nThis test file adapts tests from test_func_metadata.py which tested a custom implementation\nthat has been replaced by pydantic TypeAdapters.\n\nThe tests ensure our TypeAdapter-based approach covers all the edge cases the old custom\nimplementation handled. Since we're now using standard pydantic functionality, these tests\nmay be redundant with pydantic's own tests and could potentially be removed in the future.\n\"\"\"\n\nfrom typing import Annotated\n\nimport annotated_types\nimport pytest\nfrom pydantic import BaseModel, Field\n\nfrom fastmcp.utilities.json_schema import compress_schema\nfrom fastmcp.utilities.types import get_cached_typeadapter\n\n\n# Models must be defined at the module level for forward references to work\nclass SomeInputModelA(BaseModel):\n    pass\n\n\nclass SomeInputModelB(BaseModel):\n    class InnerModel(BaseModel):\n        x: int\n\n    how_many_shrimp: Annotated[int, Field(description=\"How many shrimp in the tank???\")]\n    ok: InnerModel\n    y: None\n\n\n# Define additional models needed in tests\nclass SomeComplexModel(BaseModel):\n    x: int\n    y: dict[int, str]\n\n\nclass ClassWithMethods:\n    def do_something(self, x: int) -> int:\n        return x\n\n    def do_something_annotated(\n        self, x: Annotated[int, Field(description=\"A description\")]\n    ) -> int:\n        return x\n\n    def do_something_return_none(self) -> None:\n        return None\n\n\ndef complex_arguments_fn(\n    an_int: int,\n    must_be_none: None,\n    must_be_none_dumb_annotation: Annotated[None, \"blah\"],\n    list_of_ints: list[int],\n    # list[str] | str is an interesting case because if it comes in as JSON like\n    # \"[\\\"a\\\", \\\"b\\\"]\" then it will be naively parsed as a string.\n    list_str_or_str: list[str] | str,\n    an_int_annotated_with_field: Annotated[\n        int, Field(description=\"An int with a field\")\n    ],\n    an_int_annotated_with_field_and_others: Annotated[\n        int,\n        str,  # Should be ignored, really\n        Field(description=\"An int with a field\"),\n        annotated_types.Gt(1),\n    ],\n    an_int_annotated_with_junk: Annotated[\n        int,\n        \"123\",\n        456,\n    ],\n    field_with_default_via_field_annotation_before_nondefault_arg: Annotated[\n        int, Field(default=1)\n    ],\n    unannotated,\n    my_model_a: SomeInputModelA,\n    my_model_a_forward_ref: \"SomeInputModelA\",\n    my_model_b: SomeInputModelB,\n    an_int_annotated_with_field_default: Annotated[\n        int,\n        Field(default=1, description=\"An int with a field\"),\n    ],\n    unannotated_with_default=5,\n    my_model_a_with_default: SomeInputModelA = SomeInputModelA(),  # noqa: B008\n    an_int_with_default: int = 1,\n    must_be_none_with_default: None = None,\n    an_int_with_equals_field: int = Field(1, ge=0),\n    int_annotated_with_default: Annotated[int, Field(description=\"hey\")] = 5,\n) -> str:\n    _ = (\n        an_int,\n        must_be_none,\n        must_be_none_dumb_annotation,\n        list_of_ints,\n        list_str_or_str,\n        an_int_annotated_with_field,\n        an_int_annotated_with_field_and_others,\n        an_int_annotated_with_junk,\n        field_with_default_via_field_annotation_before_nondefault_arg,\n        unannotated,\n        an_int_annotated_with_field_default,\n        unannotated_with_default,\n        my_model_a,\n        my_model_a_forward_ref,\n        my_model_b,\n        my_model_a_with_default,\n        an_int_with_default,\n        must_be_none_with_default,\n        an_int_with_equals_field,\n        int_annotated_with_default,\n    )\n    return \"ok!\"\n\n\ndef get_simple_func_adapter():\n    \"\"\"Get a TypeAdapter for a simple function to avoid forward reference issues\"\"\"\n\n    def simple_func(x: int, y: str = \"default\") -> str:\n        return f\"{x}-{y}\"\n\n    return get_cached_typeadapter(simple_func)\n\n\nasync def test_complex_function_runtime_arg_validation_non_json():\n    \"\"\"Test that basic non-JSON arguments are validated correctly using a simpler function\"\"\"\n    type_adapter = get_simple_func_adapter()\n\n    # Test with minimum required arguments\n    args = {\"x\": 1}\n    result = type_adapter.validate_python(args)\n    assert (\n        result == \"1-default\"\n    )  # Don't call result() as TypeAdapter returns the value directly\n\n    # Test with all arguments\n    args = {\"x\": 1, \"y\": \"hello\"}\n    result = type_adapter.validate_python(args)\n    assert result == \"1-hello\"\n\n    # Test with invalid types\n    with pytest.raises(Exception):\n        type_adapter.validate_python({\"x\": \"not an int\"})\n\n\ndef test_missing_annotation():\n    \"\"\"Test that missing annotations don't cause errors\"\"\"\n\n    def func_no_annotations(x, y):\n        return x + y\n\n    type_adapter = get_cached_typeadapter(func_no_annotations)\n    result = type_adapter.validate_python({\"x\": \"1\", \"y\": \"2\"})\n    assert result == \"12\"  # String concatenation since no type info\n\n\ndef test_convert_str_to_complex_type():\n    \"\"\"Test that string arguments are converted to the complex type when valid\"\"\"\n\n    def func_with_str_types(string: SomeComplexModel):\n        return string\n\n    # Create a valid model instance\n    input_data = {\"x\": 1, \"y\": {1: \"hello\"}}\n\n    # Validate with model directly\n    SomeComplexModel.model_validate(input_data)\n\n    # Now check if type adapter validates correctly\n    type_adapter = get_cached_typeadapter(func_with_str_types)\n    result = type_adapter.validate_python({\"string\": input_data})\n\n    assert isinstance(result, SomeComplexModel)\n    assert result.x == 1\n    assert result.y == {1: \"hello\"}\n\n\ndef test_skip_names():\n    \"\"\"Test that skipped parameters are not included in the schema\"\"\"\n\n    def func_with_many_params(\n        keep_this: int, skip_this: str, also_keep: float, also_skip: bool\n    ):\n        return keep_this, skip_this, also_keep, also_skip\n\n    # Get schema and prune parameters\n    type_adapter = get_cached_typeadapter(func_with_many_params)\n    schema = type_adapter.json_schema()\n    pruned_schema = compress_schema(schema, prune_params=[\"skip_this\", \"also_skip\"])\n\n    # Check that only the desired parameters remain\n    assert \"keep_this\" in pruned_schema[\"properties\"]\n    assert \"also_keep\" in pruned_schema[\"properties\"]\n    assert \"skip_this\" not in pruned_schema[\"properties\"]\n    assert \"also_skip\" not in pruned_schema[\"properties\"]\n\n    # The pruned parameters should also be removed from required\n    if \"required\" in pruned_schema:\n        assert \"skip_this\" not in pruned_schema[\"required\"]\n        assert \"also_skip\" not in pruned_schema[\"required\"]\n\n\nasync def test_lambda_function():\n    \"\"\"Test lambda function schema and validation\"\"\"\n    fn = lambda x, y=5: str(x)  # noqa: E731\n    type_adapter = get_cached_typeadapter(fn)\n\n    # Basic calls - validate_python returns the result directly\n    result = type_adapter.validate_python({\"x\": \"hello\"})\n    assert result == \"hello\"\n\n    result = type_adapter.validate_python({\"x\": \"hello\", \"y\": \"world\"})\n    assert result == \"hello\"\n\n    # Missing required arg\n    with pytest.raises(Exception):\n        type_adapter.validate_python({\"y\": \"world\"})\n\n\ndef test_basic_json_schema():\n    \"\"\"Test JSON schema generation for a simple function\"\"\"\n\n    def simple_func(a: int, b: str = \"default\") -> str:\n        return f\"{a}-{b}\"\n\n    type_adapter = get_cached_typeadapter(simple_func)\n    schema = type_adapter.json_schema()\n\n    # Check basic properties\n    assert \"properties\" in schema\n    assert \"a\" in schema[\"properties\"]\n    assert \"b\" in schema[\"properties\"]\n    assert schema[\"properties\"][\"a\"][\"type\"] == \"integer\"\n    assert schema[\"properties\"][\"b\"][\"type\"] == \"string\"\n    assert \"default\" in schema[\"properties\"][\"b\"]\n    assert schema[\"properties\"][\"b\"][\"default\"] == \"default\"\n\n    # Check required\n    assert \"required\" in schema\n    assert \"a\" in schema[\"required\"]\n    assert \"b\" not in schema[\"required\"]\n\n\ndef test_str_vs_int():\n    \"\"\"\n    Test that string values are kept as strings even when they contain numbers,\n    while numbers are parsed correctly.\n    \"\"\"\n\n    def func_with_str_and_int(a: str, b: int):\n        return a\n\n    type_adapter = get_cached_typeadapter(func_with_str_and_int)\n    result = type_adapter.validate_python({\"a\": \"123\", \"b\": 123})\n    assert result == \"123\"\n\n\ndef test_class_with_methods():\n    \"\"\"Test that class methods are not included in the schema\"\"\"\n    class_with_methods = ClassWithMethods()\n    type_adapter = get_cached_typeadapter(class_with_methods.do_something)\n    schema = type_adapter.json_schema()\n    assert \"self\" not in schema[\"properties\"]\n\n    type_adapter = get_cached_typeadapter(class_with_methods.do_something_annotated)\n    schema = type_adapter.json_schema()\n    assert \"self\" not in schema[\"properties\"]\n\n    type_adapter = get_cached_typeadapter(class_with_methods.do_something_return_none)\n    schema = type_adapter.json_schema()\n    assert \"self\" not in schema[\"properties\"]\n"
  },
  {
    "path": "tests/utilities/test_types.py",
    "content": "import base64\nimport os\nfrom typing import Annotated, Any, cast\n\nimport pytest\nfrom mcp.types import BlobResourceContents, TextResourceContents\nfrom pydantic import Field\n\nfrom fastmcp.utilities.types import (\n    Audio,\n    File,\n    Image,\n    create_function_without_params,\n    get_cached_typeadapter,\n    is_class_member_of_type,\n    issubclass_safe,\n    replace_type,\n)\n\n\nclass BaseClass:\n    pass\n\n\nclass ChildClass(BaseClass):\n    pass\n\n\nclass OtherClass:\n    pass\n\n\nclass TestIsClassMemberOfType:\n    def test_basic_subclass_check(self):\n        \"\"\"Test that a subclass is recognized as a member of the base class.\"\"\"\n        assert is_class_member_of_type(ChildClass, BaseClass)\n\n    def test_self_is_member(self):\n        \"\"\"Test that a class is a member of itself.\"\"\"\n        assert is_class_member_of_type(BaseClass, BaseClass)\n\n    def test_unrelated_class_is_not_member(self):\n        \"\"\"Test that an unrelated class is not a member of the base class.\"\"\"\n        assert not is_class_member_of_type(OtherClass, BaseClass)\n\n    def test_typing_union_with_member_is_member(self):\n        \"\"\"Test that Union type with a member class is detected as a member.\"\"\"\n        union_type1: Any = ChildClass | OtherClass\n        union_type2: Any = OtherClass | ChildClass\n\n        assert is_class_member_of_type(union_type1, BaseClass)\n        assert is_class_member_of_type(union_type2, BaseClass)\n\n    def test_typing_union_without_member_is_not_member(self):\n        \"\"\"Test that Union type without any member class is not a member.\"\"\"\n        union_type: Any = OtherClass | str\n        assert not is_class_member_of_type(union_type, BaseClass)\n\n    def test_pipe_union_with_member_is_member(self):\n        \"\"\"Test that pipe syntax union with a member class is detected as a member.\"\"\"\n        union_pipe1: Any = ChildClass | OtherClass\n        union_pipe2: Any = OtherClass | ChildClass\n\n        assert is_class_member_of_type(union_pipe1, BaseClass)\n        assert is_class_member_of_type(union_pipe2, BaseClass)\n\n    def test_pipe_union_without_member_is_not_member(self):\n        \"\"\"Test that pipe syntax union without any member class is not a member.\"\"\"\n        union_pipe: Any = OtherClass | str\n        assert not is_class_member_of_type(union_pipe, BaseClass)\n\n    def test_annotated_member_is_member(self):\n        \"\"\"Test that Annotated with a member class is detected as a member.\"\"\"\n        annotated1: Any = Annotated[ChildClass, \"metadata\"]\n        annotated2: Any = Annotated[BaseClass, \"metadata\"]\n\n        assert is_class_member_of_type(annotated1, BaseClass)\n        assert is_class_member_of_type(annotated2, BaseClass)\n\n    def test_annotated_non_member_is_not_member(self):\n        \"\"\"Test that Annotated with a non-member class is not a member.\"\"\"\n        annotated: Any = Annotated[OtherClass, \"metadata\"]\n        assert not is_class_member_of_type(annotated, BaseClass)\n\n    def test_annotated_with_union_member_is_member(self):\n        \"\"\"Test that Annotated with a Union containing a member class is a member.\"\"\"\n        # Test with both Union styles\n        annotated1: Any = Annotated[ChildClass | OtherClass, \"metadata\"]\n        annotated2: Any = Annotated[ChildClass | OtherClass, \"metadata\"]\n\n        assert is_class_member_of_type(annotated1, BaseClass)\n        assert is_class_member_of_type(annotated2, BaseClass)\n\n    def test_nested_annotated_with_member_is_member(self):\n        \"\"\"Test that nested Annotated with a member class is a member.\"\"\"\n        annotated: Any = Annotated[Annotated[ChildClass, \"inner\"], \"outer\"]\n        assert is_class_member_of_type(annotated, BaseClass)\n\n    def test_none_is_not_member(self):\n        \"\"\"Test that None is not a member of any class.\"\"\"\n        assert not is_class_member_of_type(None, BaseClass)\n\n    def test_generic_type_is_not_member(self):\n        \"\"\"Test that generic types are not members based on their parameter types.\"\"\"\n        list_type: Any = list[ChildClass]\n        assert not is_class_member_of_type(list_type, BaseClass)\n\n\nclass TestIsSubclassSafe:\n    def test_child_is_subclass_of_parent(self):\n        \"\"\"Test that a child class is recognized as a subclass of its parent.\"\"\"\n        assert issubclass_safe(ChildClass, BaseClass)\n\n    def test_class_is_subclass_of_itself(self):\n        \"\"\"Test that a class is a subclass of itself.\"\"\"\n        assert issubclass_safe(BaseClass, BaseClass)\n\n    def test_unrelated_class_is_not_subclass(self):\n        \"\"\"Test that an unrelated class is not a subclass.\"\"\"\n        assert not issubclass_safe(OtherClass, BaseClass)\n\n    def test_none_type_handled_safely(self):\n        \"\"\"Test that None type is handled safely without raising TypeError.\"\"\"\n        assert not issubclass_safe(cast(Any, None), BaseClass)\n\n\nclass TestImage:\n    def test_image_initialization_with_path(self):\n        \"\"\"Test image initialization with a path.\"\"\"\n        # Mock test - we're not actually going to read a file\n        image = Image(path=\"test.png\")\n        assert image.path is not None\n        assert image.data is None\n        assert image._mime_type == \"image/png\"\n\n    def test_image_path_expansion_with_tilde(self):\n        \"\"\"Test that ~ is expanded to the user's home directory.\"\"\"\n        image = Image(path=\"~/test.png\")\n        assert image.path is not None\n        assert not str(image.path).startswith(\"~\")\n        assert str(image.path).startswith(os.path.expanduser(\"~\"))\n\n    def test_image_path_expansion_with_env_var(self, monkeypatch, tmp_path):\n        \"\"\"Test that environment variables are expanded.\"\"\"\n        test_dir = tmp_path / \"test_path\"\n        test_dir.mkdir()\n        monkeypatch.setenv(\"TEST_PATH\", str(test_dir))\n        image = Image(path=\"$TEST_PATH/test.png\")\n        assert image.path is not None\n        assert not str(image.path).startswith(\"$TEST_PATH\")\n        expected_path = test_dir / \"test.png\"\n        assert image.path == expected_path\n\n    def test_image_initialization_with_data(self):\n        \"\"\"Test image initialization with data.\"\"\"\n        image = Image(data=b\"test\")\n        assert image.path is None\n        assert image.data == b\"test\"\n        assert image._mime_type == \"image/png\"  # Default for raw data\n\n    def test_image_initialization_with_format(self):\n        \"\"\"Test image initialization with a specific format.\"\"\"\n        image = Image(data=b\"test\", format=\"jpeg\")\n        assert image._mime_type == \"image/jpeg\"\n\n    def test_missing_data_and_path_raises_error(self):\n        \"\"\"Test that error is raised when neither path nor data is provided.\"\"\"\n        with pytest.raises(ValueError, match=\"Either path or data must be provided\"):\n            Image()\n\n    def test_both_data_and_path_raises_error(self):\n        \"\"\"Test that error is raised when both path and data are provided.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"Only one of path or data can be provided\"\n        ):\n            Image(path=\"test.png\", data=b\"test\")\n\n    @pytest.mark.parametrize(\n        \"extension,mime_type\",\n        [\n            (\".png\", \"image/png\"),\n            (\".jpg\", \"image/jpeg\"),\n            (\".jpeg\", \"image/jpeg\"),\n            (\".gif\", \"image/gif\"),\n            (\".webp\", \"image/webp\"),\n            (\".unknown\", \"application/octet-stream\"),\n        ],\n    )\n    def test_get_mime_type_from_path(self, tmp_path, extension, mime_type):\n        \"\"\"Test MIME type detection from file extension.\"\"\"\n        path = tmp_path / f\"test{extension}\"\n        path.write_bytes(b\"fake image data\")\n        img = Image(path=path)\n        assert img._mime_type == mime_type\n\n    def test_to_image_content(self, tmp_path, monkeypatch):\n        \"\"\"Test conversion to ImageContent.\"\"\"\n        # Test with path\n        img_path = tmp_path / \"test.png\"\n        test_data = b\"fake image data\"\n        img_path.write_bytes(test_data)\n\n        img = Image(path=img_path)\n        content = img.to_image_content()\n\n        assert content.type == \"image\"\n        assert content.mimeType == \"image/png\"\n        assert content.data == base64.b64encode(test_data).decode()\n\n        # Test with data\n        img = Image(data=test_data, format=\"jpeg\")\n        content = img.to_image_content()\n\n        assert content.type == \"image\"\n        assert content.mimeType == \"image/jpeg\"\n        assert content.data == base64.b64encode(test_data).decode()\n\n    def test_to_image_content_error(self, monkeypatch):\n        \"\"\"Test error case in to_image_content.\"\"\"\n        # Create an Image with neither path nor data (shouldn't happen due to __init__ checks,\n        # but testing the method's own error handling)\n        img = Image(data=b\"test\")\n        monkeypatch.setattr(img, \"path\", None)\n        monkeypatch.setattr(img, \"data\", None)\n\n        with pytest.raises(ValueError, match=\"No image data available\"):\n            img.to_image_content()\n\n    @pytest.mark.parametrize(\n        \"mime_type,fname,expected_mime\",\n        [\n            (None, \"test.png\", \"image/png\"),\n            (\"image/jpeg\", \"test.unknown\", \"image/jpeg\"),\n        ],\n    )\n    def test_to_data_uri(self, tmp_path, mime_type, fname, expected_mime):\n        \"\"\"Test conversion to data URI.\"\"\"\n        img_path = tmp_path / fname\n        test_data = b\"fake image data\"\n        img_path.write_bytes(test_data)\n\n        img = Image(path=img_path)\n        data_uri = img.to_data_uri(mime_type=mime_type)\n\n        expected_data_uri = (\n            f\"data:{expected_mime};base64,{base64.b64encode(test_data).decode()}\"\n        )\n        assert data_uri == expected_data_uri\n\n\nclass TestAudio:\n    def test_audio_initialization_with_path(self):\n        \"\"\"Test audio initialization with a path.\"\"\"\n        # Mock test - we're not actually going to read a file\n        audio = Audio(path=\"test.wav\")\n        assert audio.path is not None\n        assert audio.data is None\n        assert audio._mime_type == \"audio/wav\"\n\n    def test_audio_path_expansion_with_tilde(self):\n        \"\"\"Test that ~ is expanded to the user's home directory.\"\"\"\n        audio = Audio(path=\"~/test.wav\")\n        assert audio.path is not None\n        assert not str(audio.path).startswith(\"~\")\n        assert str(audio.path).startswith(os.path.expanduser(\"~\"))\n\n    def test_audio_path_expansion_with_env_var(self, monkeypatch, tmp_path):\n        \"\"\"Test that environment variables are expanded.\"\"\"\n        test_dir = tmp_path / \"test_audio_path\"\n        test_dir.mkdir()\n        monkeypatch.setenv(\"TEST_AUDIO_PATH\", str(test_dir))\n        audio = Audio(path=\"$TEST_AUDIO_PATH/test.wav\")\n        assert audio.path is not None\n        assert not str(audio.path).startswith(\"$TEST_AUDIO_PATH\")\n        expected_path = test_dir / \"test.wav\"\n        assert audio.path == expected_path\n\n    def test_audio_initialization_with_data(self):\n        \"\"\"Test audio initialization with data.\"\"\"\n        audio = Audio(data=b\"test\")\n        assert audio.path is None\n        assert audio.data == b\"test\"\n        assert audio._mime_type == \"audio/wav\"  # Default for raw data\n\n    def test_audio_initialization_with_format(self):\n        \"\"\"Test audio initialization with a specific format.\"\"\"\n        audio = Audio(data=b\"test\", format=\"mp3\")\n        assert audio._mime_type == \"audio/mp3\"\n\n    def test_missing_data_and_path_raises_error(self):\n        \"\"\"Test that error is raised when neither path nor data is provided.\"\"\"\n        with pytest.raises(ValueError, match=\"Either path or data must be provided\"):\n            Audio()\n\n    def test_both_data_and_path_raises_error(self):\n        \"\"\"Test that error is raised when both path and data are provided.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"Only one of path or data can be provided\"\n        ):\n            Audio(path=\"test.wav\", data=b\"test\")\n\n    def test_get_mime_type_from_path(self, tmp_path):\n        \"\"\"Test MIME type detection from file extension.\"\"\"\n        extensions = {\n            \".wav\": \"audio/wav\",\n            \".mp3\": \"audio/mpeg\",\n            \".ogg\": \"audio/ogg\",\n            \".m4a\": \"audio/mp4\",\n            \".flac\": \"audio/flac\",\n            \".unknown\": \"application/octet-stream\",\n        }\n\n        for ext, mime in extensions.items():\n            path = tmp_path / f\"test{ext}\"\n            path.write_bytes(b\"fake audio data\")\n            audio = Audio(path=path)\n            assert audio._mime_type == mime\n\n    def test_to_audio_content(self, tmp_path, monkeypatch):\n        \"\"\"Test conversion to AudioContent.\"\"\"\n        # Test with path\n        audio_path = tmp_path / \"test.wav\"\n        test_data = b\"fake audio data\"\n        audio_path.write_bytes(test_data)\n\n        audio = Audio(path=audio_path)\n        content = audio.to_audio_content()\n\n        assert content.type == \"audio\"\n        assert content.mimeType == \"audio/wav\"\n        assert content.data == base64.b64encode(test_data).decode()\n\n        # Test with data\n        audio = Audio(data=test_data, format=\"mp3\")\n        content = audio.to_audio_content()\n\n        assert content.type == \"audio\"\n        assert content.mimeType == \"audio/mp3\"\n        assert content.data == base64.b64encode(test_data).decode()\n\n    def test_to_audio_content_error(self, monkeypatch):\n        \"\"\"Test error case in to_audio_content.\"\"\"\n        # Create an Audio with neither path nor data (shouldn't happen due to __init__ checks,\n        # but testing the method's own error handling)\n        audio = Audio(data=b\"test\")\n        monkeypatch.setattr(audio, \"path\", None)\n        monkeypatch.setattr(audio, \"data\", None)\n\n        with pytest.raises(ValueError, match=\"No audio data available\"):\n            audio.to_audio_content()\n\n    def test_to_audio_content_with_override_mime_type(self, tmp_path):\n        \"\"\"Test conversion to AudioContent with override MIME type.\"\"\"\n        audio_path = tmp_path / \"test.wav\"\n        test_data = b\"fake audio data\"\n        audio_path.write_bytes(test_data)\n\n        audio = Audio(path=audio_path)\n        content = audio.to_audio_content(mime_type=\"audio/custom\")\n\n        assert content.type == \"audio\"\n        assert content.mimeType == \"audio/custom\"\n        assert content.data == base64.b64encode(test_data).decode()\n\n\nclass TestFile:\n    def test_file_initialization_with_path(self):\n        \"\"\"Test file initialization with a path.\"\"\"\n        # Mock test - we're not actually going to read a file\n        file = File(path=\"test.txt\")\n        assert file.path is not None\n        assert file.data is None\n        assert file._mime_type == \"text/plain\"\n\n    def test_file_path_expansion_with_tilde(self):\n        \"\"\"Test that ~ is expanded to the user's home directory.\"\"\"\n        file = File(path=\"~/test.txt\")\n        assert file.path is not None\n        assert not str(file.path).startswith(\"~\")\n        assert str(file.path).startswith(os.path.expanduser(\"~\"))\n\n    def test_file_path_expansion_with_env_var(self, monkeypatch, tmp_path):\n        \"\"\"Test that environment variables are expanded.\"\"\"\n        test_dir = tmp_path / \"test_file_path\"\n        test_dir.mkdir()\n        monkeypatch.setenv(\"TEST_FILE_PATH\", str(test_dir))\n        file = File(path=\"$TEST_FILE_PATH/test.txt\")\n        assert file.path is not None\n        assert not str(file.path).startswith(\"$TEST_FILE_PATH\")\n        expected_path = test_dir / \"test.txt\"\n        assert file.path == expected_path\n\n    def test_file_initialization_with_data(self):\n        \"\"\"Test initialization with data and format.\"\"\"\n        test_data = b\"test data\"\n        file = File(data=test_data, format=\"octet-stream\")\n        assert file.data == test_data\n        # The format parameter should set the MIME type\n        assert file._mime_type == \"application/octet-stream\"\n        assert file._name is None\n        assert file.annotations is None\n\n    def test_file_initialization_with_format(self):\n        \"\"\"Test file initialization with a specific format.\"\"\"\n        file = File(data=b\"test\", format=\"pdf\")\n        assert file._mime_type == \"application/pdf\"\n\n    def test_file_initialization_with_name(self):\n        \"\"\"Test file initialization with a custom name.\"\"\"\n        file = File(data=b\"test\", name=\"custom\")\n        assert file._name == \"custom\"\n\n    def test_missing_data_and_path_raises_error(self):\n        \"\"\"Test that error is raised when neither path nor data is provided.\"\"\"\n        with pytest.raises(ValueError, match=\"Either path or data must be provided\"):\n            File()\n\n    def test_both_data_and_path_raises_error(self):\n        \"\"\"Test that error is raised when both path and data are provided.\"\"\"\n        with pytest.raises(\n            ValueError, match=\"Only one of path or data can be provided\"\n        ):\n            File(path=\"test.txt\", data=b\"test\")\n\n    def test_get_mime_type_from_path(self, tmp_path):\n        \"\"\"Test MIME type detection from file extension.\"\"\"\n        file_path = tmp_path / \"test.txt\"\n        file_path.write_text(\n            \"test content\"\n        )  # Need to write content for MIME type detection\n        file = File(path=file_path)\n        # The MIME type should be detected from the .txt extension\n        assert file._mime_type == \"text/plain\"\n\n    def test_to_resource_content_with_path(self, tmp_path):\n        \"\"\"Test conversion to ResourceContent with path.\"\"\"\n        file_path = tmp_path / \"test.txt\"\n        test_data = b\"test file data\"\n        file_path.write_bytes(test_data)\n\n        file = File(path=file_path)\n        resource = file.to_resource_content()\n\n        assert resource.type == \"resource\"\n        assert resource.resource.mimeType == \"text/plain\"\n        # Convert both to strings for comparison\n        assert str(resource.resource.uri) == file_path.resolve().as_uri()\n        if isinstance(resource.resource, BlobResourceContents):\n            assert resource.resource.blob == base64.b64encode(test_data).decode()\n\n    def test_to_resource_content_with_data(self):\n        \"\"\"Test conversion to ResourceContent with data.\"\"\"\n        test_data = b\"test file data\"\n        file = File(data=test_data, format=\"pdf\")\n        resource = file.to_resource_content()\n\n        assert resource.type == \"resource\"\n        assert resource.resource.mimeType == \"application/pdf\"\n        # Convert URI to string for comparison\n        assert str(resource.resource.uri) == \"file:///resource.pdf\"\n        if isinstance(resource.resource, BlobResourceContents):\n            assert resource.resource.blob == base64.b64encode(test_data).decode()\n\n    def test_to_resource_content_with_text_data(self):\n        \"\"\"Test conversion to ResourceContent with text data (TextResourceContents).\"\"\"\n        test_data = b\"hello world\"\n        file = File(data=test_data, format=\"plain\")\n        resource = file.to_resource_content()\n        assert resource.type == \"resource\"\n        # Should be TextResourceContents for text/plain\n        assert isinstance(resource.resource, TextResourceContents)\n        assert resource.resource.mimeType == \"text/plain\"\n        assert resource.resource.text == \"hello world\"\n\n    def test_to_resource_content_error(self, monkeypatch):\n        \"\"\"Test error case in to_resource_content.\"\"\"\n        file = File(data=b\"test\")\n        monkeypatch.setattr(file, \"path\", None)\n        monkeypatch.setattr(file, \"data\", None)\n\n        with pytest.raises(ValueError, match=\"No resource data available\"):\n            file.to_resource_content()\n\n    def test_to_resource_content_with_override_mime_type(self, tmp_path):\n        \"\"\"Test conversion to ResourceContent with override MIME type.\"\"\"\n        file_path = tmp_path / \"test.txt\"\n        test_data = b\"test file data\"\n        file_path.write_bytes(test_data)\n\n        file = File(path=file_path)\n        resource = file.to_resource_content(mime_type=\"application/custom\")\n\n        assert resource.resource.mimeType == \"application/custom\"\n\n\nclass TestReplaceType:\n    @pytest.mark.parametrize(\n        \"input,type_map,expected\",\n        [\n            (int, {}, int),\n            (int, {int: str}, str),\n            (int, {int: int}, int),\n            (int, {int: float, bool: str}, float),\n            (bool, {int: float, bool: str}, str),\n            (int, {int: list[int]}, list[int]),\n            (list[int], {int: str}, list[str]),\n            (list[int], {int: list[str]}, list[list[str]]),\n            (\n                list[int],\n                {int: float, list[int]: bool},\n                bool,\n            ),  # list[int] will match before int\n            (list[int | bool], {int: str}, list[str | bool]),\n            (list[list[int]], {int: str}, list[list[str]]),\n        ],\n    )\n    def test_replace_type(self, input, type_map, expected):\n        \"\"\"Test replacing a type with another type.\"\"\"\n        assert replace_type(input, type_map) == expected\n\n\nclass TestCreateFunctionWithoutParams:\n    \"\"\"Test create_function_without_params properly removes parameters from both annotations and signature.\"\"\"\n\n    def test_removes_params_from_both_annotations_and_signature(self):\n        \"\"\"Test that excluded parameters are removed from __annotations__ AND __signature__.\"\"\"\n        import inspect\n\n        def original_func(ctx: str, query: str, limit: int = 10) -> list[str]:\n            return []\n\n        new_func = create_function_without_params(original_func, [\"ctx\"])\n\n        # Verify removal from annotations\n        assert \"ctx\" not in new_func.__annotations__\n        assert \"query\" in new_func.__annotations__\n        assert \"limit\" in new_func.__annotations__\n\n        # Verify removal from signature (regression test for #2562)\n        sig = inspect.signature(new_func)\n        assert \"ctx\" not in sig.parameters\n        assert \"query\" in sig.parameters\n        assert \"limit\" in sig.parameters\n\n    def test_preserves_return_annotation_in_signature(self):\n        \"\"\"Test that return annotation is preserved in both annotations and signature.\"\"\"\n        import inspect\n\n        def original_func(ctx: str, value: int) -> dict[str, int]:\n            return {}\n\n        new_func = create_function_without_params(original_func, [\"ctx\"])\n\n        sig = inspect.signature(new_func)\n        assert sig.return_annotation == dict[str, int]\n        assert new_func.__annotations__[\"return\"] == dict[str, int]\n\n    def test_pydantic_typeadapter_compatibility(self):\n        \"\"\"Test that modified function works with Pydantic TypeAdapter (regression test for #2562).\"\"\"\n        from pydantic import BaseModel\n\n        class Result(BaseModel):\n            name: str\n\n        def tool_function(ctx: str, search_query: str, limit: int = 10) -> list[Result]:\n            return []\n\n        # Remove context parameter (what FastMCP does internally)\n        new_func = create_function_without_params(tool_function, [\"ctx\"])\n\n        # This raised KeyError: 'ctx' before the fix\n        adapter = get_cached_typeadapter(new_func)\n        schema = adapter.json_schema()\n\n        # Verify schema excludes the removed parameter\n        assert \"properties\" in schema\n        assert \"ctx\" not in schema[\"properties\"]\n        assert \"search_query\" in schema[\"properties\"]\n        assert \"limit\" in schema[\"properties\"]\n\n    def test_multiple_excluded_parameters(self):\n        \"\"\"Test excluding multiple parameters simultaneously.\"\"\"\n        import inspect\n\n        def func(ctx: str, session: int, query: str, limit: int = 5) -> str:\n            return \"\"\n\n        new_func = create_function_without_params(func, [\"ctx\", \"session\"])\n\n        sig = inspect.signature(new_func)\n\n        # Both excluded params should be removed\n        assert \"ctx\" not in sig.parameters\n        assert \"session\" not in sig.parameters\n        assert \"ctx\" not in new_func.__annotations__\n        assert \"session\" not in new_func.__annotations__\n\n        # Non-excluded params should remain\n        assert \"query\" in sig.parameters\n        assert \"limit\" in sig.parameters\n\n\nclass TestAnnotationStringDescriptions:\n    \"\"\"Test the new functionality for string descriptions in Annotated types.\"\"\"\n\n    def test_get_cached_typeadapter_with_string_descriptions(self):\n        \"\"\"Test TypeAdapter creation with string descriptions.\"\"\"\n\n        def func(name: Annotated[str, \"The user's name\"]) -> str:\n            return f\"Hello {name}\"\n\n        adapter = get_cached_typeadapter(func)\n        schema = adapter.json_schema()\n\n        # Should have description in schema\n        assert \"properties\" in schema\n        assert \"name\" in schema[\"properties\"]\n        assert schema[\"properties\"][\"name\"][\"description\"] == \"The user's name\"\n\n    def test_multiple_string_annotations(self):\n        \"\"\"Test function with multiple string-annotated parameters.\"\"\"\n\n        def func(\n            name: Annotated[str, \"User's name\"],\n            email: Annotated[str, \"User's email\"],\n            age: int,\n        ) -> str:\n            return f\"{name} ({email}) is {age}\"\n\n        adapter = get_cached_typeadapter(func)\n        schema = adapter.json_schema()\n\n        # Both annotated parameters should have descriptions\n        assert schema[\"properties\"][\"name\"][\"description\"] == \"User's name\"\n        assert schema[\"properties\"][\"email\"][\"description\"] == \"User's email\"\n        # Non-annotated parameter should not have description\n        assert \"description\" not in schema[\"properties\"][\"age\"]\n\n    def test_annotated_with_more_than_string_unchanged(self):\n        \"\"\"Test that Annotated with more than just a string is unchanged.\"\"\"\n\n        def func(name: Annotated[str, \"desc\", \"extra\"]) -> str:\n            return f\"Hello {name}\"\n\n        adapter = get_cached_typeadapter(func)\n        schema = adapter.json_schema()\n\n        # Should not have description since it's not exactly length 2\n        assert \"description\" not in schema[\"properties\"][\"name\"]\n\n    def test_annotated_with_non_string_unchanged(self):\n        \"\"\"Test that Annotated with non-string second arg is unchanged.\"\"\"\n\n        def func(name: Annotated[str, 42]) -> str:\n            return f\"Hello {name}\"\n\n        adapter = get_cached_typeadapter(func)\n        schema = adapter.json_schema()\n\n        # Should not have description since second arg is not string\n        assert \"description\" not in schema[\"properties\"][\"name\"]\n\n    def test_existing_field_unchanged(self):\n        \"\"\"Test that existing Field annotations are unchanged.\"\"\"\n\n        def func(name: Annotated[str, Field(description=\"Field desc\")]) -> str:\n            return f\"Hello {name}\"\n\n        adapter = get_cached_typeadapter(func)\n        schema = adapter.json_schema()\n\n        # Should keep the Field description\n        assert schema[\"properties\"][\"name\"][\"description\"] == \"Field desc\"\n\n    def test_kwonly_defaults_preserved_when_annotations_are_processed(self):\n        \"\"\"Keyword-only defaults should survive function cloning during annotation processing.\"\"\"\n\n        def func(*, limit: Annotated[int, \"Maximum number of results\"] = 5) -> int:\n            return limit\n\n        adapter = get_cached_typeadapter(func)\n        schema = adapter.json_schema()\n\n        assert \"required\" not in schema\n        assert schema[\"properties\"][\"limit\"][\"default\"] == 5\n        assert (\n            schema[\"properties\"][\"limit\"][\"description\"] == \"Maximum number of results\"\n        )\n\n        validated = adapter.validate_python({})\n        assert validated == 5\n"
  },
  {
    "path": "tests/utilities/test_version_check.py",
    "content": "\"\"\"Tests for version checking utilities.\"\"\"\n\nimport json\nimport time\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nimport httpx\nimport pytest\n\nfrom fastmcp.utilities.version_check import (\n    CACHE_TTL_SECONDS,\n    _fetch_latest_version,\n    _get_cache_path,\n    _read_cache,\n    _write_cache,\n    check_for_newer_version,\n    get_latest_version,\n)\n\n\nclass TestCachePath:\n    def test_cache_path_in_home_directory(self):\n        \"\"\"Cache file should be in fastmcp home directory.\"\"\"\n        cache_path = _get_cache_path()\n        assert cache_path.name == \"version_cache.json\"\n        assert \"fastmcp\" in str(cache_path).lower()\n\n    def test_cache_path_prerelease_suffix(self):\n        \"\"\"Prerelease cache uses different file.\"\"\"\n        cache_path = _get_cache_path(include_prereleases=True)\n        assert cache_path.name == \"version_cache_prerelease.json\"\n\n\nclass TestReadCache:\n    def test_read_cache_no_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Reading non-existent cache returns None.\"\"\"\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: tmp_path / \"nonexistent.json\",\n        )\n        version, timestamp = _read_cache()\n        assert version is None\n        assert timestamp == 0\n\n    def test_read_cache_valid(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Reading valid cache returns version and timestamp.\"\"\"\n        cache_file = tmp_path / \"version_cache.json\"\n        cache_file.write_text(\n            json.dumps({\"latest_version\": \"2.5.0\", \"timestamp\": 1000})\n        )\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: cache_file,\n        )\n\n        version, timestamp = _read_cache()\n        assert version == \"2.5.0\"\n        assert timestamp == 1000\n\n    def test_read_cache_invalid_json(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Reading invalid JSON returns None.\"\"\"\n        cache_file = tmp_path / \"version_cache.json\"\n        cache_file.write_text(\"not valid json\")\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: cache_file,\n        )\n\n        version, timestamp = _read_cache()\n        assert version is None\n        assert timestamp == 0\n\n\nclass TestWriteCache:\n    def test_write_cache_creates_file(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Writing cache creates the cache file.\"\"\"\n        cache_file = tmp_path / \"subdir\" / \"version_cache.json\"\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: cache_file,\n        )\n\n        _write_cache(\"2.6.0\")\n\n        assert cache_file.exists()\n        data = json.loads(cache_file.read_text())\n        assert data[\"latest_version\"] == \"2.6.0\"\n        assert \"timestamp\" in data\n\n\nclass TestFetchLatestVersion:\n    def test_fetch_success(self):\n        \"\"\"Successful fetch returns highest stable version.\"\"\"\n        mock_response = MagicMock()\n        mock_response.json.return_value = {\n            \"releases\": {\n                \"2.5.0\": [],\n                \"2.4.0\": [],\n                \"2.6.0b1\": [],  # prerelease should be skipped\n            }\n        }\n\n        with patch(\"httpx.get\", return_value=mock_response) as mock_get:\n            version = _fetch_latest_version()\n            assert version == \"2.5.0\"\n            mock_get.assert_called_once()\n\n    def test_fetch_network_error(self):\n        \"\"\"Network error returns None.\"\"\"\n        with patch(\"httpx.get\", side_effect=httpx.HTTPError(\"Network error\")):\n            version = _fetch_latest_version()\n            assert version is None\n\n    def test_fetch_invalid_response(self):\n        \"\"\"Invalid response returns None.\"\"\"\n        mock_response = MagicMock()\n        mock_response.json.return_value = {\"unexpected\": \"format\"}\n\n        with patch(\"httpx.get\", return_value=mock_response):\n            version = _fetch_latest_version()\n            assert version is None\n\n    def test_fetch_prereleases(self):\n        \"\"\"Fetching with prereleases returns highest version.\"\"\"\n        mock_response = MagicMock()\n        mock_response.json.return_value = {\n            \"info\": {\"version\": \"2.5.0\"},\n            \"releases\": {\n                \"2.5.0\": [],\n                \"2.6.0b1\": [],\n                \"2.6.0b2\": [],\n                \"2.4.0\": [],\n            },\n        }\n\n        with patch(\"httpx.get\", return_value=mock_response):\n            version = _fetch_latest_version(include_prereleases=True)\n            assert version == \"2.6.0b2\"\n\n    def test_fetch_prereleases_stable_is_highest(self):\n        \"\"\"Prerelease mode still returns stable if it's highest.\"\"\"\n        mock_response = MagicMock()\n        mock_response.json.return_value = {\n            \"info\": {\"version\": \"2.5.0\"},\n            \"releases\": {\n                \"2.5.0\": [],\n                \"2.5.0b1\": [],\n                \"2.4.0\": [],\n            },\n        }\n\n        with patch(\"httpx.get\", return_value=mock_response):\n            version = _fetch_latest_version(include_prereleases=True)\n            assert version == \"2.5.0\"\n\n\nclass TestGetLatestVersion:\n    def test_returns_cached_version_if_fresh(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Uses cached version if cache is fresh.\"\"\"\n        cache_file = tmp_path / \"version_cache.json\"\n        cache_file.write_text(\n            json.dumps({\"latest_version\": \"2.5.0\", \"timestamp\": time.time()})\n        )\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: cache_file,\n        )\n\n        with patch(\n            \"fastmcp.utilities.version_check._fetch_latest_version\"\n        ) as mock_fetch:\n            version = get_latest_version()\n            assert version == \"2.5.0\"\n            mock_fetch.assert_not_called()\n\n    def test_fetches_if_cache_stale(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Fetches from PyPI if cache is stale.\"\"\"\n        cache_file = tmp_path / \"version_cache.json\"\n        old_timestamp = time.time() - CACHE_TTL_SECONDS - 100\n        cache_file.write_text(\n            json.dumps({\"latest_version\": \"2.4.0\", \"timestamp\": old_timestamp})\n        )\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: cache_file,\n        )\n\n        with patch(\n            \"fastmcp.utilities.version_check._fetch_latest_version\",\n            return_value=\"2.5.0\",\n        ) as mock_fetch:\n            version = get_latest_version()\n            assert version == \"2.5.0\"\n            mock_fetch.assert_called_once()\n\n    def test_returns_stale_cache_if_fetch_fails(\n        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n    ):\n        \"\"\"Returns stale cache if fetch fails.\"\"\"\n        cache_file = tmp_path / \"version_cache.json\"\n        old_timestamp = time.time() - CACHE_TTL_SECONDS - 100\n        cache_file.write_text(\n            json.dumps({\"latest_version\": \"2.4.0\", \"timestamp\": old_timestamp})\n        )\n        monkeypatch.setattr(\n            \"fastmcp.utilities.version_check._get_cache_path\",\n            lambda include_prereleases=False: cache_file,\n        )\n\n        with patch(\n            \"fastmcp.utilities.version_check._fetch_latest_version\", return_value=None\n        ):\n            version = get_latest_version()\n            assert version == \"2.4.0\"\n\n\nclass TestCheckForNewerVersion:\n    def test_returns_none_if_disabled(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Returns None if check_for_updates is off.\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"off\")\n\n        result = check_for_newer_version()\n        assert result is None\n\n    def test_returns_none_if_current(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Returns None if current version is latest.\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"stable\")\n        monkeypatch.setattr(fastmcp, \"__version__\", \"2.5.0\")\n\n        with patch(\n            \"fastmcp.utilities.version_check.get_latest_version\", return_value=\"2.5.0\"\n        ):\n            result = check_for_newer_version()\n            assert result is None\n\n    def test_returns_version_if_newer(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Returns new version if available.\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"stable\")\n        monkeypatch.setattr(fastmcp, \"__version__\", \"2.4.0\")\n\n        with patch(\n            \"fastmcp.utilities.version_check.get_latest_version\", return_value=\"2.5.0\"\n        ):\n            result = check_for_newer_version()\n            assert result == \"2.5.0\"\n\n    def test_returns_none_if_older_available(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Returns None if pypi version is older than current (dev version).\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"stable\")\n        monkeypatch.setattr(fastmcp, \"__version__\", \"3.0.0.dev1\")\n\n        with patch(\n            \"fastmcp.utilities.version_check.get_latest_version\", return_value=\"2.5.0\"\n        ):\n            result = check_for_newer_version()\n            assert result is None\n\n    def test_handles_invalid_versions(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Handles invalid version strings gracefully.\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"stable\")\n        monkeypatch.setattr(fastmcp, \"__version__\", \"invalid\")\n\n        with patch(\n            \"fastmcp.utilities.version_check.get_latest_version\",\n            return_value=\"also-invalid\",\n        ):\n            result = check_for_newer_version()\n            assert result is None\n\n    def test_prerelease_setting(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Prerelease setting passes include_prereleases=True.\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"prerelease\")\n        monkeypatch.setattr(fastmcp, \"__version__\", \"2.5.0\")\n\n        with patch(\n            \"fastmcp.utilities.version_check.get_latest_version\", return_value=\"2.6.0b1\"\n        ) as mock_get:\n            result = check_for_newer_version()\n            assert result == \"2.6.0b1\"\n            mock_get.assert_called_once_with(True)\n\n    def test_stable_setting(self, monkeypatch: pytest.MonkeyPatch):\n        \"\"\"Stable setting passes include_prereleases=False.\"\"\"\n        import fastmcp\n\n        monkeypatch.setattr(fastmcp.settings, \"check_for_updates\", \"stable\")\n        monkeypatch.setattr(fastmcp, \"__version__\", \"2.4.0\")\n\n        with patch(\n            \"fastmcp.utilities.version_check.get_latest_version\", return_value=\"2.5.0\"\n        ) as mock_get:\n            result = check_for_newer_version()\n            assert result == \"2.5.0\"\n            mock_get.assert_called_once_with(False)\n"
  },
  {
    "path": "v3-notes/get-methods-consolidation.md",
    "content": "# Consolidating Discovery Methods\n\nThis document captures the design decisions around component listing methods in FastMCP 3.0.\n\n## Problem\n\nThe server had parallel implementations for listing components:\n- `get_tools()` / `_list_tools()`\n- `get_resources()` / `_list_resources()`\n- `get_prompts()` / `_list_prompts()`\n- `get_resource_templates()` / `_list_resource_templates()`\n\nThese were nearly identical but with subtle differences in dedup keys, logging, and return types. The `_list_*` methods were internal and used by the MCP protocol handlers, while `get_*` methods were the public API.\n\n## Solution\n\nThe duplicate methods were consolidated into a single set of `list_*` methods. The old `get_*` plural methods and `_list_*` internal methods were both removed.\n\nThis happened in two phases:\n\n1. **Consolidation** (Dec 2025): Merged `get_*` and `_list_*` into a single `get_*` method with an `apply_middleware` parameter.\n2. **Rename** (Jan 2026): When `FastMCP` was refactored to inherit from `Provider`, the methods were renamed to `list_*` to align with the `Provider` interface. The `apply_middleware` parameter was renamed to `run_middleware` with a default of `True`.\n\n```python\nasync def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]:\n    \"\"\"Canonical method for listing tools.\"\"\"\n    ...\n```\n\n## Key Changes\n\n### Return Type: dict → list\n\nThe dict return type was removed because the key was redundant—components already have `.name` or `.uri` attributes.\n\n```python\n# Before (v2.x)\ntools = await server.get_tools()\ntool = tools[\"my_tool\"]\n\n# After (v3.0)\ntools = await server.list_tools()\ntool = next(t for t in tools if t.name == \"my_tool\")\n```\n\n### Middleware via Parameter\n\nThe `run_middleware=True` parameter (default) applies the middleware chain. This replaces the separate `_list_*_middleware()` methods.\n\n## Benefits\n\n1. **Single source of truth** - One method, not two\n2. **Consistent behavior** - Same dedup key, same visibility filtering\n3. **Clearer API** - Public method with explicit middleware opt-in\n4. **Provider alignment** - `FastMCP.list_tools()` overrides `Provider.list_tools()`\n5. **Less code** - Deleted ~200 lines of duplicate implementation\n\n## Implementation Files\n\n- `src/fastmcp/server/server.py` - Canonical `list_*` methods\n- `src/fastmcp/server/providers/` - Provider base class defines the interface\n"
  },
  {
    "path": "v3-notes/prompt-internal-types.md",
    "content": "# Prompt Internal Types - Message and PromptResult\n\n**Version:** 3.0.0\n**Impact:** Breaking change for prompts returning `mcp.types.PromptMessage`\n\n## Summary\n\nPrompts now use FastMCP's `Message` and `PromptResult` types internally, following the same pattern as resources (#2734). MCP SDK types are only used at the protocol boundary.\n\n## What Changed\n\n### Before (v2.x)\n```python\nfrom mcp.types import PromptMessage, TextContent\n\n@mcp.prompt\ndef my_prompt() -> PromptMessage:\n    return PromptMessage(\n        role=\"user\",\n        content=TextContent(type=\"text\", text=\"Hello\")\n    )\n```\n\n### After (v3.0)\n```python\nfrom fastmcp.prompts import Message\n\n@mcp.prompt\ndef my_prompt() -> Message:\n    return Message(\"Hello\")  # role defaults to \"user\"\n```\n\n## Type Constraints\n\n### Prompt Function Return Types\n```python\nstr | list[Message | str] | PromptResult\n```\n\n**Valid:**\n- `return \"Hello\"` → wrapped as single user Message\n- `return [Message(\"Hi\"), Message(\"Response\", role=\"assistant\")]`\n- `return [\"Hi\", \"Response\"]` → strings auto-wrapped as user Messages\n- `return PromptResult(messages=[...], meta={...})`\n\n**Invalid (now raises error):**\n- `return PromptMessage(...)` → Use `Message` instead\n- `return Message(...)` as single value → Use `PromptResult([Message(...)])` or return a list\n\n### Message Class\n```python\nMessage(\n    content: Any,  # Auto-serializes non-str to JSON\n    role: Literal[\"user\", \"assistant\"] = \"user\"\n)\n```\n\n**Auto-Serialization:**\n- `str` → passes through as TextContent\n- `dict` → JSON-serialized to text\n- `list` → JSON-serialized to text\n- `BaseModel` → JSON-serialized to text\n- `TextContent` / `EmbeddedResource` → passes through directly\n\n### PromptResult Class\n```python\nPromptResult(\n    messages: str | list[Message],  # str wrapped as single Message\n    description: str | None = None,\n    meta: dict[str, Any] | None = None\n)\n```\n\n## Why This Change?\n\n1. **Simpler API** - `Message(\"Hello\")` vs `PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))`\n2. **Auto-serialization** - Dicts/lists/models automatically become JSON\n3. **Consistent with resources** - Same pattern as `ResourceContent`/`ResourceResult`\n4. **Type safety** - Strict typing catches errors at development time\n\n## Migration Guide\n\n### Simple Message\n```python\n# Before\nfrom mcp.types import PromptMessage, TextContent\nreturn PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))\n\n# After\nfrom fastmcp.prompts import Message\nreturn Message(\"Hello\")\n```\n\n### Conversation\n```python\n# Before\nreturn [\n    PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hi\")),\n    PromptMessage(role=\"assistant\", content=TextContent(type=\"text\", text=\"Hello!\")),\n]\n\n# After\nreturn [\n    Message(\"Hi\"),\n    Message(\"Hello!\", role=\"assistant\"),\n]\n```\n\n### With Metadata\n```python\nfrom fastmcp.prompts import Message, PromptResult\n\nreturn PromptResult(\n    messages=[Message(\"Analyze this\")],\n    meta={\"priority\": \"high\"}\n)\n```\n\n## PR\n\n- #2738 - Introduce Message and PromptResult as canonical prompt types\n"
  },
  {
    "path": "v3-notes/provider-architecture.md",
    "content": "# Provider Architecture: FastMCPProvider + TransformingProvider\n\n**Version:** 3.0.0\n**Impact:** Breaking change - `MountedProvider` removed\n\n## Summary\n\nThe monolithic `MountedProvider` was split into two focused, composable components:\n\n- **`FastMCPProvider`**: Wraps a FastMCP server, exposing its components through the Provider interface\n- **`TransformingProvider`**: Wraps any provider to apply namespace prefixes and tool renames\n\n## Why the Split?\n\n`MountedProvider` was doing two things:\n1. Wrapping a FastMCP server as a provider\n2. Transforming component names with prefixes\n\nSeparating these concerns enables:\n- Reusing transformations on any provider (not just FastMCP servers)\n- Stacking transformations via composition\n- Clearer mental model\n\n## New API\n\n### FastMCPProvider\n\nWraps a FastMCP server to expose it through the Provider interface:\n\n```python\nfrom fastmcp.server.providers import FastMCPProvider\n\nsub_server = FastMCP(\"Sub\")\n\n@sub_server.tool\ndef greet(name: str) -> str:\n    return f\"Hello, {name}!\"\n\n# Wrap as provider\nprovider = FastMCPProvider(sub_server)\nmain_server.add_provider(provider)\n```\n\n### TransformingProvider\n\nWraps any provider to apply transformations:\n\n```python\n# Apply namespace to all components\nprovider = FastMCPProvider(server).with_namespace(\"api\")\n# \"my_tool\" → \"api_my_tool\"\n# \"resource://data\" → \"resource://api/data\"\n\n# Rename specific tools (bypasses namespace)\nprovider = FastMCPProvider(server).with_transforms(\n    namespace=\"api\",\n    tool_renames={\"verbose_tool_name\": \"short\"}\n)\n# \"verbose_tool_name\" → \"short\"\n# \"other_tool\" → \"api_other_tool\"\n```\n\n### Stacking Transformations\n\nTransformations compose via stacking:\n\n```python\nprovider = (\n    FastMCPProvider(server)\n    .with_namespace(\"inner\")\n    .with_namespace(\"outer\")\n)\n# \"tool\" → \"outer_inner_tool\"\n```\n\n## mount() Uses This Internally\n\n`FastMCP.mount()` now creates a `FastMCPProvider` + `TransformingProvider` internally:\n\n```python\nmain.mount(sub, namespace=\"api\")\n\n# Equivalent to:\nmain.add_provider(\n    FastMCPProvider(sub).with_namespace(\"api\")\n)\n```\n\n## Breaking Changes\n\n### MountedProvider Removed\n\n```python\n# Before (2.x)\nfrom fastmcp.server.providers import MountedProvider\nprovider = MountedProvider(server, prefix=\"api\")\n\n# After (3.x)\nfrom fastmcp.server.providers import FastMCPProvider\nprovider = FastMCPProvider(server).with_namespace(\"api\")\n```\n\n### prefix → namespace\n\n```python\n# Before (deprecated)\nmain.mount(sub, prefix=\"api\")\n\n# After\nmain.mount(sub, namespace=\"api\")\n```\n\n## Implementation PRs\n\n- #2653 - Split MountedProvider into FastMCPProvider + TransformingProvider\n- #2635 - Initial MountedProvider (superseded by #2653)\n"
  },
  {
    "path": "v3-notes/provider-test-pattern.md",
    "content": "# Provider Tests: Direct Server Calls\n\nThis document captures the design decision to test providers via direct server method calls rather than wrapping in a Client.\n\n## Problem\n\nProvider tests were using the Client pattern:\n\n```python\nasync with Client(mcp) as client:\n    result = await client.call_tool(\"add\", {\"x\": 1, \"y\": 2})\n    assert result.data == 3\n```\n\nThis conflated two concerns:\n1. Does the provider/server work correctly?\n2. Does the Client-Server interaction work correctly?\n\nAdditionally, ~1,200 lines of tests in `test_server_interactions.py` duplicated provider tests.\n\n## Solution\n\nProvider tests now call server methods directly:\n\n```python\nresult = await mcp.call_tool(\"add\", {\"x\": 1, \"y\": 2})\nassert result.structured_content == {\"result\": 3}\n```\n\nThis establishes clear test ownership:\n- **Provider tests** → verify server functionality\n- **Integration tests** → verify Client-Server interaction\n\n## Result Access Patterns\n\nDirect server calls return canonical FastMCP types, not MCP protocol types:\n\n| Component | Access Pattern |\n|-----------|----------------|\n| Tool | `result.structured_content` or `result.text` |\n| Resource | `result.contents[0].content` |\n| Prompt | `result.messages[0].content.text` |\n\n## Error Types\n\nDirect calls raise FastMCP exceptions:\n- `NotFoundError` - component not found\n- `DisabledError` - component disabled by visibility\n\nClient calls raise MCP protocol errors (wrapped in `McpError`).\n\n## Implementation\n\n- Consolidated duplicate tests from `test_server_interactions.py` into provider test files\n- Reduced `test_server_interactions.py` from 1,455 → 179 lines\n- Only `TestMeta` tests remain in interactions file (require Client for context injection)\n\n## PR\n\n- #2748 - Convert provider tests to use direct server calls\n"
  },
  {
    "path": "v3-notes/resource-internal-types.md",
    "content": "# Resource Internal Types - Strict Typing for Type Safety\n\n**Version:** 3.0.0\n**Impact:** Breaking change for resources returning dict/list\n\n## Summary\n\nResourceResult now enforces strict typing to catch errors at development time (via type checker) rather than at runtime (when a client reads a resource).\n\n## What Changed\n\n### Before (v2.x)\n```python\n@mcp.resource(\"data://config\")\ndef get_config() -> dict:  # Auto-serialized to JSON\n    return {\"key\": \"value\"}\n\n@mcp.resource(\"data://items\")\ndef get_items() -> list:  # Each item auto-wrapped\n    return [\"item1\", \"item2\"]\n\nResourceResult({\"key\": \"value\"})  # Dict auto-converted\nResourceResult([\"a\", \"b\"])         # List split into items\n```\n\n### After (v3.0)\n```python\n@mcp.resource(\"data://config\")\ndef get_config() -> str:  # Explicit JSON serialization\n    import json\n    return json.dumps({\"key\": \"value\"})\n\n@mcp.resource(\"data://items\")\ndef get_items() -> ResourceResult:  # Explicit multi-item response\n    return ResourceResult([\n        ResourceContent(\"item1\"),\n        ResourceContent(\"item2\"),\n    ])\n\nResourceResult([ResourceContent(...)])  # Explicit list wrapping\n# Dict/list raises TypeError\n```\n\n## Type Constraints\n\n### Resource.read() Return Type\n```python\nstr | bytes | ResourceResult\n```\n\n**Valid:**\n- `return \"text content\"`\n- `return b\"binary data\"`\n- `return ResourceResult([ResourceContent(...)])`\n\n**Invalid (now raises TypeError):**\n- `return {\"key\": \"value\"}` → Use `json.dumps()` instead\n- `return [\"item1\", \"item2\"]` → Use `ResourceResult([ResourceContent(...)])`\n- `return ResourceContent(...)` → Use `ResourceResult([ResourceContent(...)])`\n\n### ResourceResult Type Signature\n```python\nResourceResult(\n    contents: str | bytes | list[ResourceContent],\n    meta: dict[str, Any] | None = None\n)\n```\n\n**Valid:**\n- `ResourceResult(\"plain text\")`\n- `ResourceResult(b\"binary\")`\n- `ResourceResult([ResourceContent(...), ResourceContent(...)])`\n\n**Invalid (now raises TypeError):**\n- `ResourceResult({\"key\": \"value\"})` → Dict not supported\n- `ResourceResult([\"a\", \"b\"])` → Bare list not supported (must be list[ResourceContent])\n- `ResourceResult(resource_content_obj)` → Single item must be in list\n\n### ResourceContent Type Signature\n```python\nResourceContent(\n    content: Any,  # Auto-serializes non-str/bytes to JSON\n    mime_type: str | None = None,\n    meta: dict[str, Any] | None = None\n)\n```\n\n**Auto-Serialization in ResourceContent.__init__:**\n- `str` → passes through (mime_type defaults to \"text/plain\")\n- `bytes` → passes through (mime_type defaults to \"application/octet-stream\")\n- `dict` → JSON-serialized string (mime_type defaults to \"application/json\")\n- `list` → JSON-serialized string (mime_type defaults to \"application/json\")\n- `BaseModel` → JSON-serialized string (mime_type defaults to \"application/json\")\n\n## Why This Change?\n\nThe old auto-conversion behavior was convenient but hid errors:\n\n```python\n# Old behavior - silent failure\nreturn [\"item1\", \"item2\"]  # Client sees 2 items OR JSON array?\n# Ambiguous! Users would discover issues only when client reads resource\n\n# New behavior - caught at dev time\nreturn [\"item1\", \"item2\"]  # Type checker error immediately\n# Must explicitly write:\nreturn json.dumps([\"item1\", \"item2\"])  # Clear intent\n# OR:\nreturn ResourceResult([ResourceContent(\"item1\"), ResourceContent(\"item2\")])\n```\n\nType checkers now catch return type mismatches during development rather than at runtime.\n\n## Migration Guide\n\n### Returning JSON Data\n**Before:**\n```python\ndef get_config() -> dict:\n    return {\"key\": \"value\", \"nested\": {\"a\": 1}}\n```\n\n**After:**\n```python\nimport json\n\ndef get_config() -> str:\n    return json.dumps({\"key\": \"value\", \"nested\": {\"a\": 1}})\n```\n\n### Returning Multiple Items\n**Before:**\n```python\ndef get_items() -> list:\n    return [\"user1\", \"user2\", \"user3\"]\n```\n\n**After (Option 1: Single JSON array):**\n```python\nimport json\n\ndef get_items() -> str:\n    return json.dumps([\"user1\", \"user2\", \"user3\"])\n```\n\n**After (Option 2: Multiple content items):**\n```python\nfrom fastmcp.resources import ResourceContent, ResourceResult\n\ndef get_items() -> ResourceResult:\n    return ResourceResult([\n        ResourceContent(\"user1\"),\n        ResourceContent(\"user2\"),\n        ResourceContent(\"user3\"),\n    ])\n```\n\n### Returning Structured Data with Custom MIME Types\n**Before:**\n```python\ndef get_html() -> dict:\n    return {\"html\": \"<div>content</div>\"}\n```\n\n**After:**\n```python\nfrom fastmcp.resources import ResourceContent, ResourceResult\n\ndef get_html() -> ResourceResult:\n    return ResourceResult([\n        ResourceContent(\n            content=\"<div>content</div>\",\n            mime_type=\"text/html\"\n        )\n    ])\n```\n\n## Type Checking\n\nYour type checker will now catch these errors:\n\n```python\n@mcp.resource(\"data://test\")\ndef bad_resource() -> dict:  # ← Type error: should be str | bytes | ResourceResult\n    return {\"key\": \"value\"}\n```\n\nThis is intentional. The type system enforces correct typing at development time.\n\n## Backward Compatibility\n\n**This is a breaking change.** Code that returns dict or list from resources will:\n1. **Pass type checking**: If you ignore type warnings\n2. **Fail at runtime**: Raises `TypeError` when client reads the resource\n\nMigrate to explicit JSON serialization or ResourceResult.\n"
  },
  {
    "path": "v3-notes/task-meta-parameter.md",
    "content": "# Explicit task_meta Parameter for Background Tasks\n\nThis document captures the design decision to add explicit `task_meta` parameters to component execution methods, replacing context variable-based task routing.\n\n## Problem\n\nBackground task execution used context variables (`_task_metadata`, `_docket_fn_key`) to pass task metadata through the call stack. This was implicit and had several issues:\n\n1. **Hidden state** - Task metadata flowed through context vars, making it hard to trace\n2. **Fragile enrichment** - `fn_key` was enriched in 9 different places (component methods + provider wrappers)\n3. **Testing difficulty** - Required setting context vars to test background behavior\n4. **No programmatic API** - Users couldn't explicitly request background execution via `call_tool()`\n\n## Solution\n\nAdd explicit `task_meta: TaskMeta | None` parameters to:\n- `FastMCP.call_tool()`, `FastMCP.read_resource()`, `FastMCP.render_prompt()`\n- Component methods: `Tool._run()`, `Resource._read()`, `Prompt._render()`, `ResourceTemplate._read()`\n\n```python\nfrom fastmcp.server.tasks import TaskMeta\n\n# Explicit background execution\nresult = await server.call_tool(\"my_tool\", {\"arg\": \"value\"}, task_meta=TaskMeta(ttl=300))\n\n# Returns CreateTaskResult for background, ToolResult for sync\n```\n\n## fn_key Enrichment Centralization\n\nPreviously, `fn_key` (the Docket registry key) was set in 9 places:\n\n**Component methods (5):**\n- `Tool._run()`\n- `Resource._read()`\n- `ResourceTemplate._read()` (2 places)\n- `Prompt._render()`\n\n**Provider wrappers (4):**\n- `FastMCPProviderTool._run()`\n- `FastMCPProviderResource._read()`\n- `FastMCPProviderPrompt._render()`\n- `FastMCPProviderResourceTemplate._read()`\n\nNow, `fn_key` is set in **3 places** (server methods only):\n\n```python\n# In call_tool(), after finding the tool:\nif task_meta is not None and task_meta.fn_key is None:\n    task_meta = replace(task_meta, fn_key=tool.key)\n\n# In read_resource(), after finding resource or template:\nif task_meta is not None and task_meta.fn_key is None:\n    task_meta = replace(task_meta, fn_key=resource.key)  # or template.key\n\n# In render_prompt(), after finding the prompt:\nif task_meta is not None and task_meta.fn_key is None:\n    task_meta = replace(task_meta, fn_key=prompt.key)\n```\n\n## Why This Works for Mounted Servers\n\nFor mounted servers, `provider.get_tool(name)` returns a `FastMCPProviderTool` whose `.key` is already namespaced (e.g., `\"tool:child_multiply\"`). So setting `fn_key = tool.key` in the parent server gives the correct namespaced key.\n\nWhen the provider wrapper delegates to the child server, `fn_key` is already set, so the child server won't override it.\n\n## Type-Safe Overloads\n\nEach method uses `@overload` to provide correct return types:\n\n```python\n@overload\nasync def call_tool(\n    self, name: str, arguments: dict[str, Any], *, task_meta: None = None\n) -> ToolResult: ...\n\n@overload\nasync def call_tool(\n    self, name: str, arguments: dict[str, Any], *, task_meta: TaskMeta\n) -> ToolResult | mcp.types.CreateTaskResult: ...\n```\n\n## Middleware Runs Before Docket\n\nA key fix from #2663: background tasks now properly pass through all middleware stacks before being submitted to Docket. Previously, background task submission bypassed middleware entirely.\n\nThe flow is now:\n1. MCP handler extracts task metadata from request\n2. Server method (`call_tool`, etc.) finds component via provider\n3. Server enriches `task_meta.fn_key` with component key\n4. Component's `_run()`/`_read()`/`_render()` is called\n5. Middleware runs (logging, auth, rate limiting, etc.)\n6. `check_background_task()` submits to Docket if task_meta present\n\nFor mounted servers, the wrapper components delegate to the child server, which runs the child's middleware before the actual execution or Docket submission.\n\n## Removed Dead Code\n\n- `_task_metadata` context variable\n- `_docket_fn_key` context variable\n- `get_task_metadata()` function\n- `key` parameter in `check_background_task()` (backwards compat fallback)\n\n## Implementation PRs\n\n- #2663 - Components own execution; middleware runs before Docket\n- #2749 - `task_meta` for `call_tool()`\n- #2750 - `task_meta` for `read_resource()`\n- #2751 - `task_meta` for `render_prompt()` + fn_key centralization\n"
  },
  {
    "path": "v3-notes/visibility.md",
    "content": "# Visibility & Enable/Disable Design\n\nThis document captures the design decisions for the enable/disable system in FastMCP 3.0.\n\n## Core Principle\n\n**Components describe capabilities. Servers and providers control availability.**\n\nPreviously, each component had an `enabled` field that users could mutate directly. This caused a fundamental problem: when components pass through providers (especially TransformingProvider), you receive copies—and mutating a copy doesn't affect the original.\n\n## Solution: Hierarchical Visibility\n\nBoth servers and providers maintain their own `VisibilityFilter`. If a component is disabled at any level, it's disabled up the chain.\n\n```\nProvider A (filters) → Provider B (filters) → Server (filters) → Client sees only enabled components\n```\n\n## VisibilityFilter\n\nThe `VisibilityFilter` class (`src/fastmcp/utilities/visibility.py`) provides:\n\n### Blocklist (disable)\n```python\nserver.disable(keys=[\"tool:my_tool\"])  # Hide specific component\nserver.disable(tags={\"internal\"})       # Hide all components with tag\n```\n\n### Allowlist (enable with only=True)\n```python\nserver.enable(tags={\"public\"}, only=True)  # Show ONLY components with tag\n```\n\n### Blocklist Wins\nIf a component is in both blocklist and allowlist, blocklist wins. This ensures you can always hide something regardless of other filters.\n\n### Change Detection\nThe `VisibilityFilter` only sends notifications when visibility actually changes:\n- Disabling an already-disabled component: no notification\n- Enabling an already-enabled component: no notification\n- Actual state change: notification sent\n\n## Vocabulary\n\nConsistent verbs throughout the codebase:\n- `enable()` / `disable()` - methods on servers and providers\n- `is_enabled()` - check if component is visible\n- `_disabled_keys`, `_disabled_tags` - blocklist state\n- `_enabled_keys`, `_enabled_tags` - allowlist state\n- `_default_enabled` - True unless `only=True` was used\n\n## Notifications\n\n`VisibilityFilter` handles notifications directly via `_send_notification()`. This:\n1. Gets the current request context (if any)\n2. Queues the appropriate list-changed notification\n3. No-ops gracefully outside request context\n\nThis simplifies the code—no callback wiring needed between VisibilityFilter and its owners.\n\n## Migration from 2.x\n\n### Component enable/disable removed\n\n```python\n# Before (2.x) - BROKEN: mutates a copy\ntool.disable()\n\n# After (3.x)\nserver.disable(keys=[\"tool:my_tool\"])\n```\n\n### enabled field removed\n\n```python\n# Before (2.x)\n@mcp.tool(enabled=False)\ndef my_tool(): ...\n\n# After (3.x)\n@mcp.tool\ndef my_tool(): ...\n\nmcp.disable(keys=[\"tool:my_tool\"])\n```\n\n### include_tags/exclude_tags deprecated\n\n```python\n# Before (deprecated)\nmcp = FastMCP(\"server\", exclude_tags={\"internal\"})\n\n# After\nmcp = FastMCP(\"server\")\nmcp.disable(tags={\"internal\"})\n```\n\n## Component Keys\n\nComponents use prefixed keys for enable/disable:\n- Tools: `\"tool:function_name\"`\n- Prompts: `\"prompt:prompt_name\"`\n- Resources: `\"resource:resource://uri\"`\n- Templates: `\"template:resource://{param}/path\"`\n\nUse `component.key` to get the correct key format.\n\n## Implementation Files\n\n- `src/fastmcp/utilities/visibility.py` - VisibilityFilter class\n- `src/fastmcp/server/providers/base.py` - Provider.enable/disable\n- `src/fastmcp/server/server.py` - FastMCP.enable/disable\n- `src/fastmcp/utilities/components.py` - Component.enable/disable raise NotImplementedError\n"
  }
]